#!/usr/bin/env python import numpy as np from pylab import imread from itertools import chain from glob import glob from os.path import getmtime, isfile, splitext, realpath, join from ctypes import cdll from time import sleep # Usage: # ====== # # Load DLP GUI and create a structured light project with desired parameters. # You only need to set those parameters that don't have green outlines in the # block diagram view (since this script will override those). You must save # the project using the name "LogicProj" in the same directory as this script # and all the image files. Note that the batch file refers to .dbi files by # absolute path name, so do not move the files after generating the batch file. # # The choice of 1bpp or 8bpp is made when creating a new "solution" within a # project. I don't see any option to toggle between the modes in an extant # solution, or via the wizard. # # # Other Notes: # ============ # # Controlling the auto trigger (the onboard clock) via batchfiles is tricky. # The command "$L2.5 WriteReg 0xCD0 0x13880" will set a 5000 Hz framerate. So # adjusting the 0xCD0 register is an obvious place to look, but beware that it # may not be so simple for two reasons: # (1) The hex value has a mysterious '0' suffix (so '60' for 6, '7D00' for 2000) # (2) Other registers change too: 0x4C4, 0xCD4 and all of data bytes 58..78 # You can easily find the changes by compiling the sequence and saving the file # it generates in SL/Sequence/SeqCompilerOutput.bf and doing a diff on this # after making a change and recompiling. Some values must change in harmony with # the framerate, like the exposure time, but it's possible that these other # values correspond to checksums. Hence it is recommended to use the official # LightCommander software GUI to set framerate parameters and use this script # only for those parameters in green boxes (i.e. the one the GUI indicates as # being runtime-adjustable). Alternatively, use the software trigger instead of # the onboard clock. # # The LED duty cycle is not controllable via this script. Rather, you should # set it via the GUI. Try to use a value that matches your camera exposure time # and no longer, to reduce the power/heat used/produced by the LEDs. # # Read the notes displayed in each step of the GUI new-project wizard. It will # explain many of the parameters and list their default values. def globfiles(patterns): """ glob for multiple files types eg: globfiles( ["*.png", "*.bmp"] ) """ genfiles = ( glob(p) for p in patterns ) return sorted( chain.from_iterable(genfiles) ) def arr2dbi(X, binary=True): """ convert to 1024x768 array of 16bit ints byteswapping here doesn't work so make caller do it """ if X.dtype == np.float32: X = X * 255 if binary: X = (X > 0).astype( np.uint16 ) * 255 else: X = X.astype( np.uint16 ) return np.ascontiguousarray( X ) def dofile(ifilename, ofilename): """ True if ofilename doesn't exist, or if it does exist, then true if the ifilename was modified more recently """ if not isfile( ofilename ): return True else: return getmtime(ifilename) > getmtime(ofilename) def padarray(X): """ Zero-pad X up to 1024x768 """ ret = np.zeros( (768,1024), dtype=X.dtype ) r, c = X.shape if r > 768 or c > 1024: print( "Input array too large: cropping instead" ) return X[:768,:1024] l = (1024 - c) / 2 t = (768 - r) / 2 ret[t:t+768,l:l+1024] = X return ret # auto trigger uses reliable clock on projector # it has a fixed range of [6..5000] Hz # software uses triggers from API call # it has wider range but is subject to host computer delays #triggers = {'auto':3, 'software':6} if __name__ == "__main__": # set this true to convert to 1bpp, else 8bpp # we use 8bpp even on binary images for added flexibility # since it just means setting bpp flag to 8 instead of 1 # and writing 255 for pixel values rather than 1. The only # disadvantage is that we're limited to 716 Hz rather than # 5000 Hz. But since we don't have a camera that can go # that fast anyway... binary = False # fixed header for all 1024x768 DBI files H = np.zeros( 20, dtype=np.uint8 ) H[0:3] = (0x44, 0x42, 0x49) H[4] = 0x10 H[9] = 0x03 H[13] = 0x04 H[16] = 0x01 if binary else 0x08 # convert standard image files to DBI imtypes = ('png', 'bmp', 'pgm', 'jpg') patterns = [ '*.'+ext for ext in imtypes ] dbifiles = [] for f in globfiles( patterns ): bname, ext = splitext( f ) outname = bname + '.dbi' dbifiles.append( realpath(outname) ) if dofile( f, outname ): print( "Converting {} to DBI".format(f) ) I = imread( f ) # rgb2grey if necessary if I.ndim > 2: I = I[:,:,0] # bmp are upside down if ext.lower() == '.bmp': I = np.flipud( I ) # zeropad if necessary if I.shape != (768, 1024): disp( 'Zero-padding to 1024x768' ) I = padarray( I ) # convert to DBI format I = arr2dbi( I, binary ) with open( outname, 'wb' ) as out: out.write( H.data ) if np.little_endian: I.byteswap( True ) out.write( I.data ) # projection parameters - user adjustable # Danger Will Robinson! Do NOT set any individual LED to higher than 60% power LEDpower = (60, 60, 60, 50) # R-G-B-IR intensities hflip = True # flip image vflip = False # BUG - doesn't work if you set this to 'software' # not sure why. LogicProj was created with 10Hz autotrigger # and that's not adjustable via the batchfile, so we get # 10Hz flashing during pattern upload and only via the\ # time.sleep function can we modify the framerate here. # However, with LED illumination time (camera time) we can # choose any shutterspeed faster than that frame period. # despite the fact that we're issuing the batchfile command # to use the auto trigger, we are not in fact using it, # because later on we'll stop the pattern, and then advance # it manually using the software trigger trigger = 'auto' fps = 1 # N/A for auto trigger # projection parameters # do not adjust these unless you know what you're doing bpp = 1 if binary else 8 # bits per pixel compiled = join('LogicProj','Solutions','SL','Sequence','SeqCompilerOutput.bf') generated = 'My_DLP_BatchFile.bf' LEDactive = [ int(p>0 and p<=60) for p in LEDpower ] degamma = False # DLP doesn't support degamma in SL mode sync1 = (True, 0, 0, True) # active, delay, width, +ve polarity sync2 = (True, 0, 100, True) # 0 width|delay => use defaults sync3 = (True, 0, 0, True) minFps, maxFps = 6, 5000 if bpp==1 else 716 if trigger == 'auto' and (fpsmaxFps): fps = min( max(fps,6), 5000 ) print( "Clamping FPS to {}".format(fps) ) # generate batch file for controlling projector with open( generated, 'w' ) as out: with open( compiled, 'r' ) as template: out.write( ''.join(template.readlines()) ) out.write( "\n\n# The above was generated by the LogicPD GUI application\n" ) out.write( "# The following was generated by the dlp_genbatch.py script\n\n" ) src = 3 if trigger=='auto' else 6 out.write( "$L2.5 DLP_Source_SetDataSource {}\n".format(src) ) out.write( "$L2.5 DLP_Display_HorizontalFlip {}\n".format(int(hflip)) ) out.write( "$L2.5 DLP_Display_VerticalFlip {}\n".format(int(vflip)) ) out.write( "$L2.5 DLP_Display_SetDegammaEnable {}\n".format(int(degamma)) ) cmdA = "$L2.5 DLP_LED_SetLEDEnable" cmdB = "$L2.5 DLP_LED_SetLEDintensity" for i in range(4): out.write( "{} {}, {}\n".format(cmdA, i, LEDactive[i]) ) out.write( "{} {}, {}\n".format(cmdB, i, LEDpower[i]) ) syncs = (sync1, sync2, sync3) for i in range(3): out.write( "$L2.5 WriteSYNC {}, {:d}, {}, {}, {:d}\n".format(i+1,*syncs[i]) ) out.write( '\n' ) cmd = "$L2.5 WriteExternalImage" for (idx,f) in enumerate( dbifiles ): out.write( "{} \"{}\", {}\n".format(cmd, f, idx) ) out.write( '\n' ) order = ', '.join( str(i) for i in range(len(dbifiles)) ) cmd = "$L2.5 DLP_RegIO_WriteImageOrderLut" out.write( "{} {}, {}\n".format(cmd, bpp, order) ) # this function is unimplemented in current version of SDK # new SDK was supposed to have been released in May 2011... # note that the DisplayPatternAutoStepForMultiplePasses # function causes it to loop infinitely through patterns so # you have to instead use DisplayPatternManualStep #out.write( "L2.5 DLP_Display_DisplayPatternAutoStepForSinglePass" ) # python makes it very easy to call the DLL file for communicating # with the DLP. The API is documented in dlpa024a.pdf (on the wiki) # it may take a few minutes to upload all the data, during with the # projector will flash madly. print( "Writing DLP registers and uploading patterns" ) dll = cdll.LoadLibrary( "PortabilityLayer.dll" ) dll.RunBatchFile( generated, True ) dll.DLP_Display_DisplayStop() # note that time.sleep is quite inaccurate. Smallest possible sleep # interval is about 10-13ms (for reference, 16.7ms is 60fps) and the # jitter will be a couple ms, with delays coming from other CPU # activities beyond your control. Best practice is to pad the period # a little and use the LED illumination time on the projector to # control the integration time more precisely while the trigger merely # advances frames in a safe fashion. If the AutoStepForSinglePass # API function were available then I would say use the auto trigger for # faster framerates (> 6 fps) and the software trigger for anything # slower. However, since that function isn't available we have to use # software trigger all the time, which means we're limited to going # slower then 60fps _ = raw_input( "Connect camera and press return to display patterns" ) print( 0 ) dll.DLP_Display_DisplayPatternManualForceFirstPattern() period = 1.0 / fps for i in range( len(dbifiles)-1 ): print( i+1 ) sleep( period ) dll.DLP_Display_DisplayPatternManualStep() dll.DLP_Display_DisplayStop()