├── .gitignore ├── README.md ├── README.txt ├── calibrate.py ├── decode_gray.py ├── env.sh ├── fbo_sandbox.py ├── grid.py ├── mygl.py ├── scanner.py ├── to_jpeg.sh └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | jpegs 3 | *.pyc 4 | venv 5 | 6 | inputes 7 | sandbox 8 | inputs 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Structured Light Scanner 2 | ------------------------ 3 | 4 | This is being developed for the production of [Shadows in the Grass][sitg], and is very far from a complete solution. 5 | 6 | Currently, this will project gray codes at 30Hz. The code to analyze the data is yet to be written. 7 | 8 | 9 | [sitg]: https://shadowsinthegrass.com 10 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | 2 | PROJECTING 3 | ========== 4 | 5 | - 1080p, 30Hz refresh, 30fps scanner, 11bit scanner 6 | - both cameras at 30fps and 360° shutters 7 | - pick one of T or W on the projector 8 | 9 | - scan the chart on every shot 10 | - do not overexpose 11 | - do not zoom any of the lenses 12 | 13 | 14 | In General 15 | ========== 16 | 17 | XF100 on left 18 | projector in middle 19 | FS700 on right 20 | 21 | 22 | TORSO SCANS 23 | =========== 24 | 25 | from floor marker 26 | 27 | XF100 28 | 29" left 29 | 60" back 30 | 43.5" hight 31 | FS700 32 | 40" right 33 | 69" back 34 | 43.5" high 35 | 36 | Projector 37 | 3" right 38 | 76" back 39 | 33" high 40 | 41 | grid 42 | 40" away 43 | 44" high" 44 | -------------------------------------------------------------------------------- /calibrate.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | Roughly following http://docs.opencv.org/trunk/doc/tutorials/calib3d/camera_calibration/camera_calibration.html 4 | 5 | ''' 6 | 7 | import argparse 8 | import os 9 | 10 | import cv, cv2 11 | import numpy as np 12 | 13 | 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument('-x', '--width', type=int, default=18) 16 | parser.add_argument('-y', '--height', type=int, default=9) 17 | parser.add_argument('-s', '--size', type=float, default=1.0) 18 | parser.add_argument('-t', '--temp') 19 | parser.add_argument('image', nargs='+') 20 | 21 | args = parser.parse_args() 22 | 23 | pattern_size = (args.width, args.height) 24 | 25 | pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32 ) 26 | pattern_points[:,:2] = np.indices(pattern_size).T.reshape(-1, 2) 27 | pattern_points *= args.size 28 | 29 | 30 | obj_points = [] 31 | img_points = [] 32 | 33 | 34 | for image_i, path in enumerate(args.image): 35 | 36 | print path 37 | image = cv2.imread(path) 38 | w, h = image.shape[:2] 39 | image = cv2.resize(image, (1024, int(1024 * w / h))) 40 | 41 | print ' finding rough corners' 42 | found, corners = cv2.findChessboardCorners(image, pattern_size, 0, cv.CV_CALIB_CB_ADAPTIVE_THRESH) 43 | if found: 44 | print ' found', corners.shape 45 | 46 | # print 'finding fine corners' 47 | # TODO: move this back to the large frame. 48 | print ' refine corners' 49 | criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) 50 | # cv2.cornerSubPix(image[...,1], corners, (5, 5), (-1,-1), criteria) 51 | 52 | cv2.drawChessboardCorners(image, pattern_size, corners, found) 53 | 54 | img_points.append(corners.reshape(-1, 2)) 55 | obj_points.append(pattern_points) 56 | 57 | if args.temp: 58 | new_path = os.path.join(args.temp, os.path.basename(path)) 59 | cv2.imwrite(new_path, image) 60 | 61 | print '---' 62 | 63 | rms, camera_matrix, dist_coefs, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, image.shape[:2], None, None) 64 | print "RMS:", rms 65 | print "camera matrix:\n", camera_matrix 66 | print "distortion coefficients: ", dist_coefs.ravel() 67 | print "rvecs: ", rvecs 68 | print "tvecs: ", tvecs 69 | 70 | print '---' 71 | 72 | fovx, fovy, focal_length, principal_point, aspect_ratio = cv2.calibrationMatrixValues(camera_matrix, image.shape[:2], 23.9, 35.8) 73 | print 'fovx:', fovx 74 | print 'fovy:', fovy 75 | print 'focal_length:', focal_length 76 | print 'principal_point:', principal_point 77 | print 'aspect_ratio:', aspect_ratio 78 | 79 | 80 | if args.temp: 81 | for image_i, path in enumerate(args.image): 82 | image = cv2.imread(path) 83 | w, h = image.shape[:2] 84 | image = cv2.resize(image, (1024, int(1024 * w / h))) 85 | image = cv2.undistort(image, camera_matrix, dist_coefs) 86 | 87 | new_path = os.path.join(args.temp, os.path.basename(path) + '-square.jpg') 88 | cv2.imwrite(new_path, image) 89 | 90 | 91 | -------------------------------------------------------------------------------- /decode_gray.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import argparse 4 | import os 5 | 6 | import cv, cv2 7 | import numpy as np 8 | 9 | 10 | def decode_gray(images): 11 | 12 | on = images[0] 13 | off = images[1] 14 | 15 | print 'decoding x' 16 | x, width = decode_axis(on, off, images[2::2]) 17 | print 'decoding y' 18 | y, height = decode_axis(on, off, images[3::2]) 19 | 20 | rgb = np.zeros(on.shape) 21 | rgb[...,2] = x / width 22 | rgb[...,1] = y / height 23 | return rgb 24 | 25 | 26 | def remap(images): 27 | 28 | on = images[0] 29 | off = images[1] 30 | dist = distance(on, off) 31 | 32 | print 'decoding x' 33 | xcoords, width = decode_axis(on, off, images[2::2], 'x') 34 | cv2.imwrite('sandbox/xcoords.jpg', xcoords / width * 256) 35 | print np.max(xcoords), width 36 | print 'decoding y' 37 | ycoords, height = decode_axis(on, off, images[3::2], 'y') 38 | cv2.imwrite('sandbox/ycoords.jpg', ycoords / height * 256) 39 | print np.max(ycoords), width 40 | 41 | accum = np.zeros((width, height, 3)) 42 | count = np.zeros((width, height, 3)) 43 | print 'remapping' 44 | for i in xrange(on.shape[0]): 45 | print i / on.shape[0] 46 | for j in xrange(on.shape[1]): 47 | x = xcoords[i,j] 48 | y = ycoords[i,j] 49 | if dist[i,j] > 0.05: 50 | accum[height - y - 1, x] = on[i,j] 51 | # count[x,y] += 1 52 | 53 | return accum 54 | 55 | 56 | def solve(images): 57 | 58 | on = images[0] 59 | off = images[1] 60 | dist = distance(on, off) 61 | 62 | print 'decoding x' 63 | xcoords, width = decode_axis(on, off, images[2::2], 'x') 64 | print 'decoding y' 65 | ycoords, height = decode_axis(on, off, images[3::2], 'y') 66 | 67 | cam_points = [] 68 | proj_points = [] 69 | 70 | for i in xrange(on.shape[0]): 71 | print i / on.shape[0] 72 | for j in xrange(on.shape[1]): 73 | x = xcoords[i,j] 74 | y = ycoords[i,j] 75 | if dist[i,j] > 0.25: 76 | cam_points.append((i, j)) 77 | proj_points.append((height - y - 1, x)) 78 | 79 | print len(cam_points), 'points' 80 | 81 | print 'finding fundamental matrix' 82 | cam_array = np.array(cam_points) / 1024 - 1 83 | proj_array = np.array(proj_points) / 1024 - 1 84 | F, status = cv2.findFundamentalMat(cam_array, proj_array, cv.CV_FM_RANSAC) 85 | 86 | print 'stereo rectify' 87 | cam_array = np.array(cam_points) / 1024 - 1 88 | proj_array = np.array(proj_points) / 1024 - 1 89 | res, H1, H2 = cv2.stereoRectifyUncalibrated(cam_array, proj_array, F, on.shape[:2]) 90 | 91 | 92 | 93 | 94 | 95 | def distance(high, low): 96 | return intensity(high - low) 97 | 98 | def intensity(x): 99 | r = x[...,0] 100 | g = x[...,1] 101 | b = x[...,2] 102 | return np.sign(r) * r**2 + np.sign(g) * g**2 + np.sign(b) * b**2 103 | 104 | 105 | def decode_axis(on, off, images, prefix='x'): 106 | 107 | avg = intensity((on + off) / 2) 108 | 109 | gray = np.zeros(shape=avg.shape, dtype='uint32') 110 | 111 | for i, image in reversed(list(enumerate(images))): 112 | 113 | bit = np.greater(intensity(image), avg) 114 | gray = (gray << 1) + bit.astype(int) 115 | cv2.imwrite('sandbox/bit-%s%d.jpg' % (prefix, i), bit * 256) 116 | 117 | mask = gray 118 | for i in xrange(len(images)): 119 | mask = mask >> 1 120 | gray = gray ^ mask 121 | 122 | return gray, 2**len(images) 123 | 124 | 125 | 126 | if __name__ == '__main__': 127 | 128 | parser = argparse.ArgumentParser() 129 | parser.add_argument('-s', '--skip', action='store_true') 130 | parser.add_argument('-o', '--output') 131 | parser.add_argument('-b', '--bits', type=int, default=11) 132 | parser.add_argument('-x', '--solve', action='store_true') 133 | parser.add_argument('images', nargs='+') 134 | args = parser.parse_args() 135 | 136 | paths = args.images 137 | if args.skip: 138 | paths = [args.images[i] for i in xrange(0, len(args.images), 2)] 139 | 140 | if args.bits: 141 | paths = paths[:2 + 2 * args.bits] 142 | 143 | images = [] 144 | for path in paths: 145 | print 'reading {} ...'.format(path), 146 | image = cv2.imread(path) 147 | if image.dtype == np.uint8: 148 | image = image.astype(float) / 256 149 | if image.dtype != float: 150 | raise TypeError('image not convertable to float; got %r' % image.dtype) 151 | print image.shape, image.dtype 152 | images.append(image) 153 | 154 | 155 | if args.solve: 156 | solve(images) 157 | else: 158 | out = remap(images) 159 | out_path = args.output or (os.path.dirname(paths[0]) + '.jpg') 160 | print out_path 161 | cv2.imwrite(out_path, out * 256) 162 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | export PYTHONPATH=/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages 2 | -------------------------------------------------------------------------------- /fbo_sandbox.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import sys 4 | import time 5 | import math 6 | import random 7 | import os 8 | 9 | from mygl import gl, glu, glut, Shader 10 | 11 | 12 | class App(object): 13 | 14 | def __init__(self, argv): 15 | 16 | # Initialize GLUT and out render buffer. 17 | glut.init(argv) 18 | glut.initDisplayMode(glut.DOUBLE | glut.RGBA | glut.DEPTH | glut.MULTISAMPLE) 19 | 20 | # Initialize the window. 21 | self.width = 800 22 | self.height = 600 23 | glut.initWindowSize(self.width, self.height) 24 | glut.createWindow(argv[0]) 25 | 26 | gl.clearColor(0, 0, 0, 1) 27 | 28 | # Turn on a bunch of OpenGL options. 29 | gl.enable(gl.CULL_FACE) 30 | gl.enable(gl.DEPTH_TEST) 31 | gl.enable(gl.COLOR_MATERIAL) 32 | gl.enable('blend') 33 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 34 | gl.enable('multisample') 35 | 36 | 37 | self.fbo = gl.genFramebuffers(1) 38 | gl.bindFramebuffer(gl.FRAMEBUFFER, self.fbo) 39 | 40 | self.render_texture = gl.genTextures(1) 41 | gl.bindTexture(gl.TEXTURE_2D, self.render_texture) 42 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, self.width, self.height, 0, gl.RGB, gl.UNSIGNED_BYTE, 0) 43 | 44 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 45 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 46 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 47 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 48 | 49 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, self.render_texture, 0) 50 | 51 | 52 | gl.bindFramebuffer(gl.FRAMEBUFFER, 0) 53 | 54 | self.dilate = 0 55 | 56 | self.fixed = Shader(''' 57 | void main(void) { 58 | gl_Position = ftransform(); 59 | } 60 | ''', ''' 61 | void main(void) { 62 | gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); 63 | } 64 | ''') 65 | 66 | self.shader = Shader(''' 67 | void main(void) { 68 | gl_Position = ftransform(); 69 | gl_TexCoord[0] = gl_MultiTexCoord0; 70 | } 71 | ''', ''' 72 | uniform sampler2D texture1; 73 | uniform float width, height; 74 | uniform float dilate; 75 | void main(void) { 76 | 77 | float dmin = -dilate; 78 | float dmax = dilate + 0.5; 79 | float dx = 1.0; 80 | 81 | vec4 texel = vec4(0.0, 0.0, 0.0, 1.0); 82 | for (float x = dmin; x < dmax; x+=dx) { 83 | for (float y = dmin; y < dmax; y+=dx) { 84 | texel = max(texel, texture2D(texture1, gl_TexCoord[0].st + vec2(x / width, y / height))); 85 | } 86 | } 87 | 88 | gl_FragColor = texel * vec4(gl_FragCoord.x / width, gl_FragCoord.y / height, 0.0, 1.0); 89 | } 90 | ''') 91 | 92 | self.curves = [( 93 | random.random() * 10 + 1, 94 | random.random() * 2 * 3.14159, 95 | random.random(), 96 | ) for _ in xrange(10)] 97 | self.frame = 0 98 | 99 | # Attach some GLUT event callbacks. 100 | glut.reshapeFunc(self.reshape) 101 | glut.displayFunc(self.display) 102 | glut.keyboardFunc(self.keyboard) 103 | 104 | self.frame_rate = 24.0 105 | glut.timerFunc(int(1000 / self.frame_rate), self.timer, 0) 106 | 107 | 108 | def keyboard(self, key, mx, my): 109 | if key == '\x1b': # ESC 110 | exit(0) 111 | elif key == 'f': 112 | glut.fullScreen() 113 | elif key == 'a': 114 | self.dilate += 1 115 | elif key == 'z': 116 | self.dilate = max(0, self.dilate - 1) 117 | else: 118 | print 'unknown key %r at %s,%d' % (key, mx, my) 119 | 120 | def run(self): 121 | return glut.mainLoop() 122 | 123 | def reshape(self, width, height): 124 | """Called when the user reshapes the window.""" 125 | self.width = width 126 | self.height = height 127 | 128 | gl.viewport(0, 0, width, height) 129 | gl.matrixMode(gl.PROJECTION) 130 | gl.loadIdentity() 131 | gl.ortho(0, width, 0, height, -100, 100) 132 | gl.matrixMode(gl.MODELVIEW) 133 | gl.loadIdentity() 134 | gl.translate(0.5, 0.5, 0) 135 | 136 | def timer(self, value): 137 | self.frame += 1 138 | glut.postRedisplay() 139 | glut.timerFunc(int(1000 / self.frame_rate), self.timer, 0) 140 | 141 | def display(self): 142 | 143 | gl.bindFramebuffer(gl.FRAMEBUFFER, self.fbo) 144 | 145 | # Wipe the window. 146 | gl.enable('depth_test') 147 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 148 | 149 | gl.loadIdentity() 150 | 151 | gl.disable('texture_2d') 152 | self.fixed.use() 153 | gl.color(1, 1, 1, 1) 154 | 155 | for freq, offset, amp in self.curves: 156 | with gl.begin('line_strip'): 157 | for x in xrange(0, self.width + 1, 10): 158 | gl.vertex(x, self.height * (0.5 + 0.5 * amp * math.sin(4 * self.frame / self.frame_rate + offset + x * freq / self.width)), 0) 159 | 160 | gl.bindFramebuffer(gl.FRAMEBUFFER, 0) 161 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 162 | gl.enable('texture_2d') 163 | gl.enable('depth_test') 164 | gl.color(1, 1, 1, 1) 165 | 166 | print 'I need to print so it doesnt explode.' 167 | self.shader.use() 168 | location = gl.getUniformLocation(self.shader._prog, "texture1") 169 | if location >= 0: 170 | gl.uniform1i(location, 0) 171 | location = gl.getUniformLocation(self.shader._prog, "width") 172 | if location >= 0: 173 | gl.uniform1f(location, float(self.width)) 174 | location = gl.getUniformLocation(self.shader._prog, "height") 175 | if location >= 0: 176 | gl.uniform1f(location, float(self.height)) 177 | location = gl.getUniformLocation(self.shader._prog, "dilate") 178 | if location >= 0: 179 | gl.uniform1f(location, float(self.dilate)) 180 | 181 | with gl.begin('polygon'): 182 | gl.texCoord(0, 0) 183 | gl.vertex(0, 0) 184 | gl.texCoord(1, 0) 185 | gl.vertex(self.width, 0) 186 | gl.texCoord(1, 1) 187 | gl.vertex(self.width, self.height) 188 | gl.texCoord(0, 1) 189 | gl.vertex(0, self.height) 190 | 191 | glut.swapBuffers() 192 | 193 | 194 | if __name__ == '__main__': 195 | app = App(sys.argv) 196 | exit(app.run()) 197 | 198 | -------------------------------------------------------------------------------- /grid.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import sys 4 | import time 5 | import math 6 | import random 7 | import os 8 | import itertools 9 | 10 | from mygl import gl, glu, glut, Shader 11 | 12 | 13 | class App(object): 14 | 15 | def __init__(self, argv): 16 | 17 | # Initialize GLUT and out render buffer. 18 | glut.init(argv) 19 | glut.initDisplayMode(glut.DOUBLE | glut.RGB) 20 | 21 | # Initialize the window. 22 | self.width = 1024 23 | self.height = 768 24 | glut.initWindowSize(self.width, self.height) 25 | glut.createWindow(argv[0]) 26 | 27 | gl.clearColor(0, 0, 0, 1) 28 | 29 | self.offset_x = self.offset_y = 0 30 | self.base_size = 32 31 | self.layers = 3 32 | self.base_width = 1 33 | 34 | glut.reshapeFunc(self.reshape) 35 | glut.displayFunc(self.display) 36 | glut.keyboardFunc(self.keyboard) 37 | 38 | 39 | def keyboard(self, key, mx, my): 40 | 41 | if key in ('q', '\x1b'): # ESC 42 | exit(0) 43 | elif key == 'f': 44 | glut.fullScreen() 45 | 46 | elif key == 'a': 47 | self.base_size += 1 48 | elif key == 'z': 49 | self.base_size = max(2, self.base_size - 1) 50 | 51 | elif key == 's': 52 | self.layers += 1 53 | elif key == 'x': 54 | self.layers = max(1, self.layers - 1) 55 | 56 | elif key == 'd': 57 | self.base_width += 1 58 | elif key == 'c': 59 | self.base_width = max(1, self.base_width - 1) 60 | 61 | else: 62 | print 'unknown key %r at %s,%d' % (key, mx, my) 63 | 64 | glut.postRedisplay() 65 | 66 | def run(self): 67 | return glut.mainLoop() 68 | 69 | def reshape(self, width, height): 70 | """Called when the user reshapes the window.""" 71 | 72 | self.width = width 73 | self.height = height 74 | 75 | print 'reshape to', width, height 76 | 77 | gl.viewport(0, 0, width, height) 78 | gl.matrixMode(gl.PROJECTION) 79 | gl.loadIdentity() 80 | gl.ortho(0, self.width, 0, self.height, -100, 100) 81 | gl.matrixMode(gl.MODELVIEW) 82 | 83 | 84 | 85 | def display(self): 86 | 87 | gl.clear(gl.COLOR_BUFFER_BIT) 88 | gl.color(1, 1, 1, 1) 89 | 90 | 91 | max_bits = max(int(math.log(self.width - 1, 2)), int(math.log(self.height - 1, 2))) + 1 92 | assert max_bits < 16 93 | 94 | for power in xrange(0, self.layers): 95 | 96 | size = self.base_size * 2**power 97 | gl.lineWidth(self.base_width + 2 * power) 98 | 99 | v = (power + 1) / float(self.layers) 100 | gl.color(v, v, v, 1) 101 | 102 | with gl.begin(gl.LINES): 103 | for x in xrange(size, self.width, size): 104 | gl.vertex(x, 0, 0) 105 | gl.vertex(x, self.height, 0) 106 | for y in xrange(size, self.height, size): 107 | gl.vertex(0, y, 0) 108 | gl.vertex(self.width, y, 0) 109 | 110 | glut.swapBuffers() 111 | 112 | 113 | if __name__ == '__main__': 114 | app = App(sys.argv) 115 | exit(app.run()) 116 | 117 | -------------------------------------------------------------------------------- /mygl.py: -------------------------------------------------------------------------------- 1 | '''Mikes wrapper for the visualizer???''' 2 | from contextlib import contextmanager 3 | from functools import partial 4 | 5 | import OpenGL 6 | import OpenGL.GLUT 7 | import OpenGL.GLU 8 | import OpenGL.GL 9 | import OpenGL.GL.framebufferobjects 10 | import OpenGL.GL.shaders 11 | 12 | __all__ = ''' 13 | gl 14 | glu 15 | glut 16 | '''.strip().split() 17 | 18 | 19 | class ModuleProxy(object): 20 | 21 | def __init__(self, name, *modules): 22 | self._name = name 23 | self._modules = modules 24 | self._module = modules[0] 25 | 26 | def __getattr__(self, raw_name): 27 | 28 | # Constants. 29 | if raw_name.isupper(): 30 | name = self._name.upper() + '_' + raw_name 31 | 32 | # Methods. 33 | else: 34 | name = raw_name.split('_') 35 | name = [x[0].upper() + x[1:] for x in name] 36 | name = self._name + ''.join(name) 37 | 38 | for module in self._modules: 39 | try: 40 | value = getattr(module, name) 41 | except AttributeError: 42 | continue 43 | else: 44 | setattr(self, raw_name, value) 45 | setattr(self, name, value) 46 | return value 47 | 48 | raise AttributeError(raw_name) 49 | 50 | 51 | class GLProxy(ModuleProxy): 52 | 53 | @contextmanager 54 | def matrix(self): 55 | self._module.glPushMatrix() 56 | try: 57 | yield 58 | finally: 59 | self._module.glPopMatrix() 60 | 61 | @contextmanager 62 | def attrib(self, *args): 63 | mask = 0 64 | for arg in args: 65 | if isinstance(arg, basestring): 66 | arg = getattr(self._module, 'GL_%s_BIT' % arg.upper()) 67 | mask |= arg 68 | self._module.glPushAttrib(mask) 69 | try: 70 | yield 71 | finally: 72 | self._module.glPopAttrib() 73 | 74 | def enable(self, *args, **kwargs): 75 | self._enable(True, args, kwargs) 76 | return self._apply_on_exit(self._enable, False, args, kwargs) 77 | 78 | def disable(self, *args, **kwargs): 79 | self._enable(False, args, kwargs) 80 | return self._apply_on_exit(self._enable, True, args, kwargs) 81 | 82 | def _enable(self, enable, args, kwargs): 83 | todo = [] 84 | for arg in args: 85 | if isinstance(arg, basestring): 86 | arg = getattr(self._module, 'GL_%s' % arg.upper()) 87 | todo.append((arg, enable)) 88 | for key, value in kwargs.iteritems(): 89 | flag = getattr(self._module, 'GL_%s' % key.upper()) 90 | value = value if enable else not value 91 | todo.append((flag, value)) 92 | for flag, value in todo: 93 | if value: 94 | self._module.glEnable(flag) 95 | else: 96 | self._module.glDisable(flag) 97 | 98 | def begin(self, arg): 99 | if isinstance(arg, basestring): 100 | arg = getattr(self._module, 'GL_%s' % arg.upper()) 101 | self._module.glBegin(arg) 102 | return self._apply_on_exit(self._module.glEnd) 103 | 104 | @contextmanager 105 | def _apply_on_exit(self, func, *args, **kwargs): 106 | try: 107 | yield 108 | finally: 109 | func(*args, **kwargs) 110 | 111 | 112 | gl = GLProxy('gl', OpenGL.GL, OpenGL.GL.framebufferobjects) 113 | glu = ModuleProxy('glu', OpenGL.GLU) 114 | glut = ModuleProxy('glut', OpenGL.GLUT) 115 | 116 | 117 | class Shader(object): 118 | 119 | def __init__(self, vert_src, frag_src): 120 | self._vert = OpenGL.GL.shaders.compileShader(vert_src, gl.VERTEX_SHADER) 121 | self._frag = OpenGL.GL.shaders.compileShader(frag_src, gl.FRAGMENT_SHADER) 122 | self._prog = OpenGL.GL.shaders.compileProgram(self._vert, self._frag) 123 | self._uniforms = {} 124 | 125 | def use(self): 126 | gl.useProgram(self._prog) 127 | 128 | def unuse(self): 129 | gl.useProgram(0) 130 | 131 | def __getattr__(self, name): 132 | if name.startswith('uniform'): 133 | func = partial(self._uniform, name) 134 | setattr(self, name, func) 135 | return func 136 | raise AttributeError(name) 137 | 138 | def _uniform(self, funcname, name, *args): 139 | try: 140 | location = self._uniforms[name] 141 | except KeyError: 142 | location = gl.getUniformLocation(self._prog, name) 143 | self._uniforms[name] = location 144 | if location < 0: 145 | raise NameError('no uniform %r' % name) 146 | return getattr(gl, funcname)(location, *args) 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /scanner.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import argparse 4 | import sys 5 | import time 6 | import math 7 | import random 8 | import os 9 | import itertools 10 | 11 | from mygl import gl, glu, glut, Shader 12 | 13 | 14 | bits = 12 15 | code_count = 2**bits 16 | gray_codes = [(i >> 1) ^ i for i in xrange(code_count)] 17 | gray_bits = [[gray_codes[i] & (2**b) for i in xrange(code_count)] for b in xrange(bits)] 18 | 19 | binary_codes = range(code_count) 20 | binary_bits = [[binary_codes[i] & (2**b) for i in xrange(code_count)] for b in xrange(bits)] 21 | 22 | 23 | class App(object): 24 | 25 | def __init__(self, argv): 26 | 27 | # Initialize GLUT and out render buffer. 28 | glut.init(argv) 29 | glut.initDisplayMode(glut.DOUBLE | glut.RGB) 30 | # glut.initDisplayMode(2048) 31 | 32 | # Initialize the window. 33 | self.width = 1024 34 | self.height = 768 35 | glut.initWindowSize(self.width, self.height) 36 | glut.createWindow(argv[0]) 37 | 38 | gl.clearColor(0, 0, 0, 1) 39 | 40 | self.gray_texture = gl.genTextures(1) 41 | gl.activeTexture(gl.TEXTURE0) 42 | gl.bindTexture(gl.TEXTURE_2D, self.gray_texture) 43 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RED, code_count, bits, 0, gl.RED, gl.FLOAT, sum(gray_bits, [])) 44 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 45 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 46 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 47 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 48 | 49 | self.binary_texture = gl.genTextures(1) 50 | gl.activeTexture(gl.TEXTURE1) 51 | gl.bindTexture(gl.TEXTURE_2D, self.binary_texture) 52 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RED, code_count, bits, 0, gl.RED, gl.FLOAT, sum(binary_bits, [])) 53 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 54 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 55 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 56 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 57 | 58 | self.shader = Shader(''' 59 | 60 | void main(void) { 61 | gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 62 | } 63 | ''', ''' 64 | 65 | uniform float bits, code_count; 66 | uniform sampler2D texture; 67 | uniform float bit; 68 | uniform int axis; 69 | 70 | void main(void) { 71 | 72 | float i = floor(axis > 0 ? gl_FragCoord.y: gl_FragCoord.x); 73 | float v = texture2D(texture, vec2(i / code_count, bit / bits)).r; 74 | gl_FragColor = vec4(v, v, v, 1.0); 75 | } 76 | 77 | ''') 78 | 79 | self.frame = 0 80 | 81 | parser = argparse.ArgumentParser() 82 | parser.add_argument('-m', '--mosaic', action='store_true') 83 | parser.add_argument('-t', '--time', action='store_true') 84 | parser.add_argument('-g', '--gray', action='store_true') 85 | parser.add_argument('-b', '--binary', action='store_true') 86 | parser.add_argument('-r', '--grid', action='store_true') 87 | parser.add_argument('-i', '--info', action='store_true') 88 | parser.add_argument('-j', '--info2', action='store_true') 89 | parser.add_argument('-s', '--strobe', action='store_true') 90 | parser.add_argument('-f', '--fps', type=float, default=30.0) 91 | parser.add_argument('-n', '--noblack', action='store_true') 92 | args = parser.parse_args(argv[1:]) 93 | 94 | self.stages = [] 95 | if args.time: 96 | self.stages.append(self.time_stage) 97 | if args.mosaic: 98 | self.stages.append(self.mosaic_stage) 99 | if args.gray: 100 | self.stages.append(self.gray_stage) 101 | if args.binary: 102 | self.stages.append(self.binary_stage) 103 | if args.grid: 104 | self.stages.append(self.grid_stage) 105 | if args.info: 106 | self.stages.append(self.info_stage) 107 | if args.info2: 108 | self.stages.append(self.info_stage2) 109 | if args.strobe: 110 | self.stages.append(self.strobe_stage) 111 | if not self.stages: 112 | parser.print_usage() 113 | exit(1) 114 | 115 | self.stepper = None 116 | 117 | glut.reshapeFunc(self.reshape) 118 | glut.displayFunc(self.display) 119 | glut.keyboardFunc(self.keyboard) 120 | 121 | self.frame_rate = args.fps 122 | self.no_black = args.noblack 123 | 124 | 125 | def keyboard(self, key, mx, my): 126 | if key in ('q', '\x1b'): # ESC 127 | exit(0) 128 | elif key == 'f': 129 | glut.fullScreen() 130 | elif key == ' ': 131 | self.scan() 132 | glut.postRedisplay() 133 | elif key == 'r': 134 | self.stepper = self.iter_scan() 135 | next(self.stepper) 136 | elif key == 'x': 137 | if not self.stepper: 138 | self.stepper = self.iter_scan() 139 | try: 140 | next(self.stepper) 141 | except StopIteration: 142 | self.stepper = None 143 | 144 | else: 145 | print 'unknown key %r at %s,%d' % (key, mx, my) 146 | 147 | def run(self): 148 | return glut.mainLoop() 149 | 150 | def reshape(self, width, height): 151 | """Called when the user reshapes the window.""" 152 | 153 | self.width = width 154 | self.height = height 155 | 156 | print 'reshape to', width, height 157 | 158 | gl.viewport(0, 0, width, height) 159 | gl.matrixMode(gl.PROJECTION) 160 | gl.loadIdentity() 161 | gl.ortho(0, self.width, 0, self.height, -100, 100) 162 | gl.matrixMode(gl.MODELVIEW) 163 | 164 | def reset_timer(self): 165 | self.last_frame = time.time() 166 | self.dropped = False 167 | 168 | def tick(self): 169 | next_frame = self.last_frame + 1.0 / self.frame_rate 170 | delta = next_frame - time.time() 171 | if delta < -0.004: # Window is 45deg of 1/30, or 1/240, or ~0.008. 172 | self.dropped = True 173 | print 'dropped frame; out by %dms' % abs(1000 * delta) 174 | elif delta > 0: 175 | time.sleep(delta) 176 | self.last_frame = next_frame 177 | 178 | def scan(self): 179 | 180 | sync_to = 0.1 181 | x = math.fmod(time.time(), sync_to) 182 | x = sync_to - x 183 | time.sleep(x) 184 | 185 | self.reset_timer() 186 | 187 | scan_iter = self.iter_scan() 188 | while True: 189 | 190 | try: 191 | next(scan_iter) 192 | except StopIteration: 193 | break 194 | 195 | self.tick() 196 | 197 | if not self.no_black: 198 | gl.clear(gl.COLOR_BUFFER_BIT) 199 | glut.swapBuffers() 200 | self.tick() 201 | 202 | if self.dropped: 203 | gl.color(1, 0, 0, 1) 204 | self.polyfill() 205 | glut.swapBuffers() 206 | time.sleep(0.5) 207 | return 208 | 209 | 210 | def iter_scan(self): 211 | 212 | stages = [stage() for stage in self.stages] 213 | for stage in stages: 214 | while True: 215 | 216 | gl.clear(gl.COLOR_BUFFER_BIT) 217 | gl.color(1, 1, 1, 1) 218 | 219 | try: 220 | next(stage) 221 | except StopIteration: 222 | break 223 | 224 | glut.swapBuffers() 225 | yield 226 | 227 | def polyfill(self): 228 | with gl.begin('polygon'): 229 | gl.vertex(0, 0) 230 | gl.vertex(self.width, 0) 231 | gl.vertex(self.width, self.height) 232 | gl.vertex(0, self.height) 233 | 234 | def mosaic_stage(self): 235 | size = 64 236 | for x in xrange(0, self.width, size): 237 | for y in xrange(0, self.height, size): 238 | gl.color( 239 | random.choice((0, 1)), 240 | random.choice((0, 1)), 241 | random.choice((0, 1)) 242 | ) 243 | with gl.begin(gl.POLYGON): 244 | gl.vertex(x, y) 245 | gl.vertex(x + size, y) 246 | gl.vertex(x + size, y + size) 247 | gl.vertex(x, y + size) 248 | yield 249 | 250 | def time_stage(self): 251 | 252 | seconds = int(time.time()) 253 | blocks = [0, 0, 0] 254 | while seconds: 255 | seconds, block = divmod(seconds, 8) 256 | blocks.append(block) 257 | 258 | size = 8 259 | for i, x in enumerate(xrange(0, self.width, size)): 260 | bi = i % len(blocks) 261 | block = blocks[bi] 262 | 263 | if bi < 3: 264 | pattern = (1, 0.5, 0) 265 | gl.color( 266 | pattern[(bi + 0) % 3], 267 | pattern[(bi + 1) % 3], 268 | pattern[(bi + 2) % 3] 269 | ) 270 | 271 | else: 272 | gl.color( 273 | int(bool(block // 4)), 274 | int(bool(block // 2 % 2)), 275 | int(bool(block % 2)) 276 | ) 277 | 278 | with gl.begin(gl.POLYGON): 279 | gl.vertex(x, 0) 280 | gl.vertex(x + size, 0) 281 | gl.vertex(x + size, self.height) 282 | gl.vertex(x, self.height) 283 | yield 284 | 285 | 286 | def gray_stage(self, texture=0): 287 | 288 | if not texture: 289 | gl.color(0, 1, 0, 1) 290 | else: 291 | gl.color(0, 1, 1, 1) 292 | self.polyfill() 293 | yield 294 | 295 | gl.color(1, 1, 1, 1) 296 | self.polyfill() 297 | yield 298 | 299 | yield 300 | 301 | # Subtract 1 so that 1024 only takes 9 bits. 302 | max_bits = int(math.ceil(max(math.log(self.width, 2), math.log(self.height, 2)))) 303 | assert max_bits < 16 304 | assert self.width <= 2**max_bits 305 | assert self.height <= 2**max_bits 306 | assert self.width > 2**(max_bits-1) or self.height > 2**(max_bits-1) 307 | 308 | self.shader.use() 309 | self.shader.uniform1i('texture', texture) 310 | self.shader.uniform1f('bits', bits) 311 | self.shader.uniform1f('code_count', code_count) 312 | 313 | for bit in range(max_bits): 314 | self.shader.uniform1f('bit', bit) 315 | for axis in (0, 1): 316 | self.shader.uniform1i('axis', axis) 317 | self.polyfill() 318 | yield 319 | 320 | self.shader.unuse() 321 | 322 | gl.color(1, 0, 1, 1) 323 | self.polyfill() 324 | yield 325 | 326 | def binary_stage(self): 327 | return self.gray_stage(1) 328 | 329 | def grid_stage(self): 330 | 331 | gl.color(0, 0, 1, 1) 332 | self.polyfill() 333 | yield 334 | 335 | max_bits = max(int(math.log(self.width - 1, 2)), int(math.log(self.height - 1, 2))) + 1 336 | assert max_bits < 16 337 | 338 | for power in xrange(2, max_bits): 339 | gl.lineWidth(power - 1) 340 | self.grid(power) 341 | yield 342 | 343 | gl.color(1, 0, 1, 1) 344 | self.polyfill() 345 | yield 346 | 347 | def grid(self, power): 348 | size = 2**power 349 | with gl.begin(gl.LINES): 350 | for x in xrange(size, self.width, size): 351 | gl.vertex(x, 0, 0) 352 | gl.vertex(x, self.height, 0) 353 | for y in xrange(size, self.height, size): 354 | gl.vertex(0, y, 0) 355 | gl.vertex(self.width, y, 0) 356 | 357 | def info_stage2(self): 358 | 359 | i = 0 360 | for power in xrange(1, 4): 361 | blocks = 2**power 362 | 363 | dx = self.width / float(blocks) 364 | dy = self.height / float(blocks) 365 | 366 | for x in xrange(blocks): 367 | for y in xrange(blocks): 368 | 369 | i = (i + 1) % 3 370 | gl.color(int(i == 0), int(i == 1), int(i == 2), 1) 371 | with gl.begin(gl.POLYGON): 372 | gl.vertex(dx * x , dy * y) 373 | gl.vertex(dx * (x + 1), dy * y) 374 | gl.vertex(dx * (x + 1), dy * (y + 1)) 375 | gl.vertex(dx * x , dy * (y + 1)) 376 | 377 | if not i: 378 | yield 379 | 380 | def info_stage(self): 381 | 382 | for power in xrange(1, 5): 383 | blocks = 2**power 384 | 385 | dx = self.width / float(blocks) 386 | dy = self.height / float(blocks) 387 | 388 | for i in xrange(blocks): 389 | 390 | with gl.begin(gl.POLYGON): 391 | gl.vertex(dx * i, 0) 392 | gl.vertex(dx * (i + 1), 0) 393 | gl.vertex(dx * (i + 1), self.height) 394 | gl.vertex(dx * i, self.height) 395 | yield 396 | 397 | with gl.begin(gl.POLYGON): 398 | gl.vertex(0 , dy * i) 399 | gl.vertex(self.width, dy * i) 400 | gl.vertex(self.width, dy * (i + 1)) 401 | gl.vertex(0 , dy * (i + 1)) 402 | yield 403 | 404 | def strobe_stage(self): 405 | while True: 406 | self.polyfill() 407 | yield 408 | yield 409 | 410 | def display(self): 411 | gl.clear(gl.COLOR_BUFFER_BIT) 412 | # gl.color(0, 0.0625, 0.125, 1) 413 | # self.polyfill() 414 | 415 | gl.lineWidth(2) 416 | gl.color(0.2, 0.2, 0.05, 1) 417 | 418 | for power in (1, ): 419 | 420 | # gl.lineWidth((3 - power) ** 2) 421 | 422 | dx = self.width // (2**power) 423 | dy = self.height // (2**power) 424 | 425 | with gl.begin(gl.LINES): 426 | for x in xrange(dx, self.width, dx): 427 | gl.vertex(x, 0, 0) 428 | gl.vertex(x, self.height, 0) 429 | for y in xrange(dy, self.height, dy): 430 | gl.vertex(0, y, 0) 431 | gl.vertex(self.width, y, 0) 432 | 433 | 434 | glut.swapBuffers() 435 | 436 | 437 | if __name__ == '__main__': 438 | app = App(sys.argv) 439 | exit(app.run()) 440 | 441 | -------------------------------------------------------------------------------- /to_jpeg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -d inputs/new ]]; then 4 | rm -rf inputs/new 5 | fi 6 | 7 | if [[ "$2" ]]; then 8 | mkdir -p "$2" 9 | dst="$2" 10 | else 11 | dst="$1" 12 | dst="${dst%.*}" 13 | fi 14 | 15 | mkdir -p "$dst" 16 | ffmpeg -i "$1" -q:v 1 $dst/%04d.jpg 17 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | 2 | - README 3 | - calibrate before every scan; consider having the grid on a stand 4 | behind the subject 5 | - don't move around the room 6 | 7 | - see if I can have the keyboard handler draw several frames 8 | - it will then do a full iteration 9 | - display handler will draw a warning grid 10 | - self.start_timer resets the last_frame and dropped_frames 11 | - self.tick waits for the next frame 12 | - blast red as soon as it drops a frame 13 | 14 | - use 3 digit binary for status codes: 15 | 4 -> error 16 | 1 to 7 can be start/stop of various things 17 | 2, 1 for gray 18 | 3, 5 for binary 19 | 6, 7 for grids 20 | block of 4 if there was an error 21 | 22 | - framework for animating things with pauses for user input 23 | - base object should be animatable 24 | - list of python functions, pausing for user between each 25 | - somehow representing tweens 26 | 27 | - objects which update the attributes of the object, which then draws itself 28 | 29 | --------------------------------------------------------------------------------