├── .gitignore ├── .gitignore~ ├── README.md ├── deflectometry.m ├── dither.m ├── fringe_gen.py ├── imcrop2.m ├── load_rescale.m ├── response_map.m ├── surface_reconstruction.m └── take_images.py /.gitignore: -------------------------------------------------------------------------------- 1 | images/* 2 | *~ 3 | output.png 4 | -------------------------------------------------------------------------------- /.gitignore~: -------------------------------------------------------------------------------- 1 | images/ 2 | *~ 3 | output.png 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | deflectometry 2 | ============= 3 | 4 | Python + Octave code for measuring surface height using fringe deflectometry. Probably not useful to anyone else at this point. 5 | -------------------------------------------------------------------------------- /deflectometry.m: -------------------------------------------------------------------------------- 1 | %clear all; close all; clc; 2 | more off; 3 | 4 | <<<<<<< HEAD 5 | imdir = 'images/new/100/' 6 | spacing = 3 7 | 8 | if(0) 9 | if size(argv()) != 2 10 | disp('Need to supply test directory and fringe spacing') 11 | quit() 12 | end 13 | 14 | 15 | if argv(){1}(end) != '/' 16 | disp('Expecting a directory ending in / for first argument') 17 | quit() 18 | end 19 | 20 | % directory of test optic images 21 | imdir = argv(){1} 22 | spacing = str2num(argv(){2}) 23 | end 24 | 25 | ======= 26 | >>>>>>> 2d54b00f43316af0098a9123a015c5a96eb6d5e9 27 | 28 | %%%%% 29 | %%%%% PARAMETERS 30 | 31 | 32 | imdir = 'images/thin/' 33 | spacing = 20 34 | 35 | d = 2000; % mirror -> fringe distance in mm 36 | p_tilt = spacing; % horizontal fringe spacing in mm 37 | p_yaw = spacing; % vertical fringe spacing in mm 38 | 39 | %box = [530 275 430 215]; % thin 40 | box = [700,410,300,100]; % slide2 41 | %box = [700,405,300,100]; % slide 42 | 43 | 44 | scale = 0.23; % projected pixel size 45 | 46 | imdir_ref = './images/reference/'; 47 | 48 | % get response map for reference and test surface 49 | % maps how displayed intensities converted to detector readings 50 | disp('Generating reference response map');[response_ref, flat_ref] = response_map(imdir_ref,box); 51 | disp('Generating response map');[response, flat] = response_map(imdir_ref,box); 52 | 53 | 54 | %%%%% 55 | %%%%% VERTICAL FRINGES 56 | 57 | % load reference fringes 58 | disp('Loading vertical fringes'); 59 | v00ref = load_rescale(strcat(imdir_ref,num2str(p_yaw),'v00.png'),box,response_ref,flat_ref); 60 | v90ref = load_rescale(strcat(imdir_ref,num2str(p_yaw),'v90.png'),box,response_ref,flat_ref); 61 | v180ref = load_rescale(strcat(imdir_ref,num2str(p_yaw),'v180.png'),box,response_ref,flat_ref); 62 | v270ref = load_rescale(strcat(imdir_ref,num2str(p_yaw),'v270.png'),box,response_ref,flat_ref); 63 | 64 | 65 | % load test fringes 66 | v00 = load_rescale(strcat(imdir,num2str(p_yaw),'v00.png'),box,response,flat); 67 | v90 = load_rescale(strcat(imdir,num2str(p_yaw),'v90.png'),box,response,flat); 68 | v180 = load_rescale(strcat(imdir,num2str(p_yaw),'v180.png'),box,response,flat); 69 | v270 = load_rescale(strcat(imdir,num2str(p_yaw),'v270.png'),box,response,flat); 70 | 71 | % generate test and reference phasemaps using 4 step algorithm 72 | disp('Generating vertical phasemap'); 73 | % phase maps must be unwrapped in both directions after generation 74 | vphase_ref = unwrap(unwrap(atan2((v270ref - v90ref),(v00ref - v180ref))),[],2); 75 | vphase = unwrap(unwrap(atan2(v270 - v90,v00 - v180)),[],2); 76 | 77 | 78 | 79 | %%%%% 80 | %%%%% HORIZONTAL FRINGES 81 | 82 | % repeat all of above 83 | disp('Loading horizontal fringes'); 84 | h00ref = load_rescale(strcat(imdir_ref,num2str(p_tilt),'h00.png'),box,response_ref,flat_ref); 85 | h90ref = load_rescale(strcat(imdir_ref,num2str(p_tilt),'h90.png'),box,response_ref,flat_ref); 86 | h180ref = load_rescale(strcat(imdir_ref,num2str(p_tilt),'h180.png'),box,response_ref,flat_ref); 87 | h270ref = load_rescale(strcat(imdir_ref,num2str(p_tilt),'h270.png'),box,response_ref,flat_ref); 88 | h00 = load_rescale(strcat(imdir,num2str(p_tilt),'h00.png'),box,response,flat); 89 | h90 = load_rescale(strcat(imdir,num2str(p_tilt),'h90.png'),box,response,flat); 90 | h180 = load_rescale(strcat(imdir,num2str(p_tilt),'h180.png'),box,response,flat); 91 | h270 = load_rescale(strcat(imdir,num2str(p_tilt),'h270.png'),box,response,flat); 92 | disp('Generating horizontal phasemap'); 93 | hphase_ref = unwrap(unwrap(atan2( (h270ref - h90ref),(h00ref - h180ref) ),[],2)); 94 | hphase = unwrap(unwrap(atan2( (h270 - h90),(h00 - h180)),[],2)); 95 | 96 | 97 | %%%%% 98 | %%%%% RECONSTRUCT SURFACE 99 | 100 | disp('Finding surface normals') 101 | % construct angle matrices based upon phase differences 102 | tiltmatrix = (hphase_ref - hphase)*p_tilt/(2*d*pi); 103 | yawmatrix = (vphase_ref - vphase)*p_yaw/(2*d*pi); 104 | 105 | shape = size(tiltmatrix);ypix = [1:shape(2)]; xpix = [1:shape(1)]; 106 | 107 | disp('Reconstructing surface... this may take a while'); 108 | surfmatrix2 = surface_reconstruction(tiltmatrix,yawmatrix,scale); 109 | disp('...done!'); 110 | 111 | imagesc(ypix*scale,xpix*scale,dither(surfmatrix2)); 112 | set (gca, "dataaspectratio", [1 shape(1)/shape(2) 1]); 113 | c=colorbar; ylabel(c,'Height (um)'); 114 | xlabel('y (mm)');ylabel('x (mm)'); 115 | disp('Saving image'); 116 | print('output.png'); 117 | 118 | 119 | -------------------------------------------------------------------------------- /dither.m: -------------------------------------------------------------------------------- 1 | function clipped = dither(array) 2 | 3 | neighbours(:,:,1) = circshift(array,[0,1]); 4 | neighbours(:,:,2) = circshift(array,[0,-1]); 5 | neighbours(:,:,3) = circshift(array,[1,0]); 6 | neighbours(:,:,4) = circshift(array,[-1,0]); 7 | neighbours(:,:,5) = circshift(array,[-1,-1]); 8 | neighbours(:,:,6) = circshift(array,[-1,1]); 9 | neighbours(:,:,7) = circshift(array,[1,-1]); 10 | neighbours(:,:,8) = circshift(array,[1,1]); 11 | array = median(neighbours,3); 12 | 13 | clipped = array; 14 | 15 | end 16 | -------------------------------------------------------------------------------- /fringe_gen.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import math 3 | import numpy as np 4 | from pygame.locals import * 5 | 6 | 7 | # constants 8 | h = 1600 9 | w = 900 10 | 11 | # initialise pygame display to screen size 12 | def setupScreen(): 13 | pygame.init() 14 | size=(w,h) 15 | return pygame.display.set_mode(size,pygame.RESIZABLE) 16 | 17 | # place fringes on the screen 18 | def fringeGen(screen, desired_period, calib = False, vert=True, phase=0): 19 | 20 | rgb_img = np.zeros((w,h,3)) 21 | 22 | # if we want a calibration screen 23 | if calib: 24 | img = np.ones((w,h))*(desired_period%255) 25 | 26 | else: 27 | 28 | pixel_size = 440./h # pixel size in mm, calculated by width of screen (~440mm) 29 | 30 | p = desired_period / pixel_size 31 | 32 | if vert: 33 | 34 | x = range(w) 35 | z = [] 36 | 37 | for i in x: 38 | value = (math.sin( x[i]*2.*math.pi/p + phase)+1)*127. 39 | z.append(value) 40 | 41 | img = np.zeros((w,h)) 42 | 43 | for i in range(h): 44 | img[:,i] = z 45 | 46 | else: 47 | 48 | y = range(h) 49 | z = [] 50 | 51 | for i in y: 52 | value = (math.sin( y[i]*2.*math.pi/p + phase)+1)*127. 53 | z.append(value) 54 | 55 | img = np.zeros((w,h)) 56 | 57 | for i in range(w): 58 | img[i,:] = z 59 | 60 | 61 | rgb_img[...,0] = img 62 | rgb_img[...,1] = img 63 | rgb_img[...,2] = img 64 | 65 | I=pygame.surfarray.make_surface(rgb_img) 66 | 67 | screen.blit(I,(0,0)) 68 | pygame.display.update() # update the display 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /imcrop2.m: -------------------------------------------------------------------------------- 1 | function img = imcrop2(img,box) 2 | 3 | img = double(img(box(2):box(2)+box(4)-1,box(1):box(1)+box(3)-1)); 4 | 5 | end 6 | -------------------------------------------------------------------------------- /load_rescale.m: -------------------------------------------------------------------------------- 1 | function image = load_rescale(filename,box,response,flatfield) 2 | 3 | image = double(imcrop2(imread(filename),box)); % load and crop 4 | image = image./flatfield; % mult by flatfield (mostly amp noise) 5 | image = (polyval(response,image) - 127)/127; % attempt return to digital (0-255) values and recentre on 0 6 | disp(' dithering image'); 7 | image = dither(image); % dither image to remove hotpixels and some noise 8 | image = image - mean(image(:)); % brute force recentre on 0, assuming range is roughly right by now 9 | 10 | end 11 | -------------------------------------------------------------------------------- /response_map.m: -------------------------------------------------------------------------------- 1 | function [response, flat] = response_map(imdir,box) 2 | % maps the response from several calibration frames using polyfit 3 | % pixel values can be reclaimed using polyval, the rescaled from 0:255 to -1:1 4 | 5 | 6 | calib0 = double(imcrop2(imread(strcat(imdir,'c0.png')),box)); 7 | c0 = median(calib0(:)); 8 | calib25 = double(imcrop2(imread(strcat(imdir,'c25.png')),box)); 9 | c25 = median(calib25(:)); 10 | calib50 = double(imcrop2(imread(strcat(imdir,'c50.png')),box)); 11 | c50 = median(calib50(:)); 12 | calib75 = double(imcrop2(imread(strcat(imdir,'c75.png')),box)); 13 | c75 = median(calib75(:)); 14 | calib100 = double(imcrop2(imread(strcat(imdir,'c100.png')),box)); 15 | c100 = median(calib100(:)); 16 | calib125 = double(imcrop2(imread(strcat(imdir,'c125.png')),box)); 17 | c125 = median(calib125(:)); 18 | calib150 = double(imcrop2(imread(strcat(imdir,'c150.png')),box)); 19 | c150 = median(calib150(:)); 20 | calib175 = double(imcrop2(imread(strcat(imdir,'c175.png')),box)); 21 | c175 = median(calib175(:)); 22 | calib200 = double(imcrop2(imread(strcat(imdir,'c200.png')),box)); 23 | c200 = median(calib200(:)); 24 | calib225 = double(imcrop2(imread(strcat(imdir,'c225.png')),box)); 25 | c225 = median(calib225(:)); 26 | calib250 = double(imcrop2(imread(strcat(imdir,'c250.png')),box)); 27 | c250 = median(calib250(:)); 28 | 29 | 30 | flat = dither(calib100)/c100; % dither flatfield to remove hotpixels 31 | flat = flat./mean(flat(:)); % make sure it does not amplify image 32 | 33 | levels = [0,25,50,75,100,125,150,175,200,225,250]; 34 | response_values = [c0,c25,c50,c75,c100,c125,c150,c175,c200,c225,c250]; 35 | 36 | response = polyfit(response_values,levels,3); % fit digital levels as a function of response value 37 | 38 | end 39 | -------------------------------------------------------------------------------- /surface_reconstruction.m: -------------------------------------------------------------------------------- 1 | % we start in the 'bottom left' and 'top right' corners 2 | % tilt and yaw are given in this frame of reference 3 | % i.e. tilt is gradient in dim 1 and yaw in dim 2 4 | function surfmatrix = surface_reconstruction(yaw,tilt,scale) 5 | 6 | shape = size(tilt);ypix = [1:shape(2)]; xpix = [1:shape(1)]; 7 | 8 | 9 | %%% FIRST PASS 10 | height1 = zeros(shape); 11 | height1(1,1) = 1; 12 | 13 | % generate sides 14 | for i = 2:shape(1) 15 | height1(i,1) = height1(i-1,1) + tilt(i-1,1); 16 | end 17 | for j = 2:shape(2) 18 | height1(1,j) = height1(1,j-1) + yaw(1,j-1); 19 | end 20 | 21 | % vertical reconstruction 22 | for j = 1:shape(2)-1 23 | for i = 1:shape(1)-1 24 | height1(i+1,j+1) = 0.5*(height1(i,j+1) + height1(i+1,j)) + 0.25*( tilt(i,j+1) + tilt(i+1,j+1) + yaw(i+1,j) + yaw(i+1,j+1)); 25 | end 26 | end 27 | 28 | %%% SECOND PASS 29 | height2 = zeros(shape); 30 | height2(end,end) = 1; 31 | 32 | % generate sides 33 | for i = shape(1):-1:2 34 | height2(i-1,end) = height2(i,end) - tilt(i,end); 35 | end 36 | 37 | for j = shape(2):-1:2 38 | height2(end,j-1) = height2(end,j) - yaw(end,j); 39 | end 40 | 41 | % horizontal reconstruction 42 | for i = shape(1):-1:2 43 | for j = shape(2):-1:2 44 | height2(i-1,j-1) = 0.5*(height2(i-1,j) + height2(i,j-1)) - 0.25*(tilt(i-1,j) + tilt(i,j) + yaw(i,j-1) + yaw(i,j)); 45 | end 46 | end 47 | 48 | % now take the average 49 | surfmatrix = 1000*scale*0.5*(height1 + height2); 50 | 51 | % remove yaw,tilt and piston 52 | tilt = polyval(polyfit(ypix,mean(surfmatrix,1),1),ypix); 53 | yaw = polyval(polyfit(xpix,mean(surfmatrix,2)',1),xpix); 54 | 55 | for x = xpix 56 | surfmatrix(x,:) = surfmatrix(x,:) - tilt; 57 | end 58 | for y = ypix 59 | surfmatrix(:,y) = surfmatrix(:,y) - yaw'; 60 | end 61 | surfmatrix = surfmatrix - mean(surfmatrix(:)); 62 | 63 | -------------------------------------------------------------------------------- /take_images.py: -------------------------------------------------------------------------------- 1 | import ueye 2 | import time 3 | import Image 4 | import numpy as np 5 | import fringe_gen as fg 6 | 7 | 8 | def numCameras(): 9 | ncam=ueye.GetNumberOfCameras() 10 | return ncam 11 | 12 | def setupCamera(): #### uEye camera init 13 | global cam 14 | cam = ueye.Cam() 15 | cMode = cam.SetColorMode(6) # greyscale 8 bit = 6 16 | gMode = cam.SetGainBoost(3) # turn of analogue gain = 3 (on = 2) 17 | cam.SetHardwareGain(1,1,1,1) # only change first for master gain 0-100 18 | rv=cam.SetBinning(0) 19 | rv=cam.SetSubSampling(0) 20 | pnMin, pnMax=cam.GetPixelClockRange() # set pixel clock to minimum 21 | pClock=cam.SetPixelClock(pnMin) 22 | fMin,fMax,intervall= cam.GetFrameTimeRange() # set framerate to min 23 | fps = cam.SetFrameRate(fMin) 24 | eMin,eMax,interval=cam.GetExposureRange() # set exposure to max 25 | exposure = cam.SetExposureTime(eMax) 26 | 27 | 28 | def grabResponse(screen,gain = 100): 29 | for i in range(0,275,25): 30 | fg.fringeGen(screen,i, calib = True) 31 | time.sleep(1) 32 | im = grabStack(screen,5,gain) 33 | saveImage('images/new/c'+str(i)+'.png',im) 34 | time.sleep(1) 35 | 36 | 37 | def grabAll(screen,spacing,stacksize = 9,gain = 100): 38 | fg.fringeGen(screen,spacing, calib = False, vert = False, phase = 0) 39 | time.sleep(1) 40 | im = grabStack(screen,stacksize,gain) 41 | saveImage('images/new/'+str(spacing)+'h00.png',im) 42 | 43 | fg.fringeGen(screen,spacing, calib = False, vert = False, phase = 90) 44 | time.sleep(1) 45 | im = grabStack(screen,stacksize,gain) 46 | saveImage('images/new/'+str(spacing)+'h90.png',im) 47 | 48 | fg.fringeGen(screen,spacing, calib = False, vert = False, phase = 180) 49 | time.sleep(1) 50 | im = grabStack(screen,stacksize,gain) 51 | saveImage('images/new/'+str(spacing)+'h180.png',im) 52 | 53 | fg.fringeGen(screen,spacing, calib = False, vert = False, phase = 270) 54 | time.sleep(1) 55 | im = grabStack(screen,stacksize,gain) 56 | saveImage('images/new/'+str(spacing)+'h270.png',im) 57 | 58 | fg.fringeGen(screen,spacing, calib = False, vert = True, phase = 0) 59 | time.sleep(1) 60 | im = grabStack(screen,stacksize,gain) 61 | saveImage('images/new/'+str(spacing)+'v00.png',im) 62 | 63 | fg.fringeGen(screen,spacing, calib = False, vert = True, phase = 90) 64 | time.sleep(1) 65 | im = grabStack(screen,stacksize,gain) 66 | saveImage('images/new/'+str(spacing)+'v90.png',im) 67 | 68 | fg.fringeGen(screen,spacing, calib = False, vert = True, phase = 180) 69 | time.sleep(1) 70 | im = grabStack(screen,stacksize,gain) 71 | saveImage('images/new/'+str(spacing)+'v180.png',im) 72 | 73 | fg.fringeGen(screen,spacing, calib = False, vert = True, phase = 270) 74 | time.sleep(1) 75 | im = grabStack(screen,stacksize,gain) 76 | saveImage('images/new/'+str(spacing)+'v270.png',im) 77 | 78 | def grabStack(screen,stacksize,gain): 79 | cam.SetHardwareGain(gain,1,1,1) 80 | stack = np.zeros((1024,1280,stacksize)) 81 | for i in range(stacksize): 82 | stack[...,i] = cam.GrabImage() 83 | time.sleep(0.5) 84 | im = np.median(stack,axis=2) 85 | return im 86 | 87 | 88 | def saveImage(filename,data): 89 | tmp = (data).astype(np.uint8) 90 | im = Image.fromarray(tmp) 91 | im.save(filename) 92 | --------------------------------------------------------------------------------