diff --git a/calibration/automatic_filter.m b/calibration/automatic_filter.m new file mode 100644 index 0000000000000000000000000000000000000000..1eebc873946aa1ce35c1006912e74e5c20574181 --- /dev/null +++ b/calibration/automatic_filter.m @@ -0,0 +1,63 @@ +cd%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% automatic_filter.m +% +% utility to help find the best filter size for identifying markers +% for a given image file, the Laplacian of Gaussian method for finding +% markers will be applied and the centre three markers extracted +% the filter scale can be adjusted to allow experimentation + +addpath([pwd '/../../mex/minmax/']); +addpath([pwd '/../']); +addpath([pwd '/pinhole/']); +addpath([pwd '/../gmv/']); + +%% input parameters + +grid_dx = 2.54; % real units, mm +grid_dy = 2.54; +filter_type = 'disk'; + +input_file = '/nfs/scratch23/lawson/K62/170203/calib/POS01.cam1.tif'; + +%% load image +[~,~,ext] = fileparts(input_file); +if strcmpi(ext, '.gmv') + % loading of .gmv movie files + im = mean(double(read_gmv_matlab(input_file)), 3); + im = fliplr(im); +else + % loading of regular image files + im = double(imread(input_file)); + im = fliplr(im'); +end +Ni = size(im, 1); +Nj = size(im, 2); +i_axis = 0 : Ni-1; +j_axis = 0 : Nj-1; + +%% loop +while true + %% get filter scale + s_resp = input('Enter filter scale in pixels or q to quit: ', 's'); + if strcmp(s_resp, 'q') + break; + end + blob_scale = str2double(s_resp); + if isnan(blob_scale) + continue; + end + + %% filter + [ij_small,im_filt] = log_find_markers(i_axis, j_axis, im, blob_scale, 1000, filter_type); + [ij_big,im_filt_big] = log_find_markers(i_axis, j_axis, im, 2*blob_scale, 3, filter_type); + + %% plot + figure(1); + clf; + imagesc(i_axis, j_axis, im_filt'); + hold on; + plot(ij_small(1,:), ij_small(2,:), 'r.'); + plot(ij_big(1,:), ij_big(2,:), 'ro'); + axis equal tight xy; + colorbar; +end \ No newline at end of file diff --git a/calibration/bounding_box.m b/calibration/bounding_box.m new file mode 100644 index 0000000000000000000000000000000000000000..4dcd9cc2d4c26aacbfb4835fc1f3b3e983a04bd6 --- /dev/null +++ b/calibration/bounding_box.m @@ -0,0 +1,49 @@ +function [bb_lim, A, b] = bounding_box(camera) + % bbmax = bounding_box(calib) + % determine the corners of the smallest axis aligned bounding box + % which fit encapsulate the extent of the measurement volume + % + % bbmax is a 2 x 3 matrix containing the x, y and z limits of the + % smallest bounding box which encapsulate the entire measurement volume + + %% get view frustums + % M = M_ary(:,p,c) are the coefficients for the equation of plane p of the + % viewing frustum for camera c. + % i.e. + % M(1)x + M(2)y + M(3)z + M(4) >= 0 + % when a point lies within the measurement volume + n_cameras = length(camera); + M_ary = zeros(4,4,n_cameras); + for c = 1 : n_cameras + M_ary(:,:,c)= frustum(camera(c))'; + end + + %% solve linear programming problem + % to obtain volume bounds, we find the x that + % minimises c'x subject to Ax < b + A = reshape(-M_ary(1:3,:,:), [3 4*n_cameras])'; + b = reshape( M_ary(4 ,:,:), [4*n_cameras 1]); + bb_lim = zeros(2,3); + opts = optimoptions('linprog', 'Display', 'off'); + for i = 1 : 3 + e_i = [0 0 0]'; + e_i(i) = 1; + bb_lim(1,i) = dot(e_i, linprog(+e_i, A, b, [], [], [], [], [], opts)); + bb_lim(2,i) = dot(e_i, linprog(-e_i, A, b, [], [], [], [], [], opts)); + end + + %% test + %{ + N = 1E5; + bb_size = bb_lim(2,:) - bb_lim(1,:); + gl_pos = bsxfun(@plus, bb_lim(1,:)', diag(bb_size) * rand(3,N)); + bb_dist = bsxfun(@minus, A*gl_pos, b); + b_inside = all(bb_dist <= 0, 1); + figure(1); + clf; + plot3(gl_pos(1, b_inside), gl_pos(2, b_inside), gl_pos(3, b_inside), 'g.'); + hold on; + plot3(gl_pos(1,~b_inside), gl_pos(2,~b_inside), gl_pos(3,~b_inside), 'r.'); + axis equal tight xy; + %} +end \ No newline at end of file diff --git a/calibration/bounding_polygon.m b/calibration/bounding_polygon.m new file mode 100644 index 0000000000000000000000000000000000000000..d3d96dc3ae0ab4dfd62133adfa461896f6586217 --- /dev/null +++ b/calibration/bounding_polygon.m @@ -0,0 +1,34 @@ +function [A,b] = bounding_polygon(corners) + % [A,b] = bounding_polygon(corners) + % determine the coefficients of an inequality Ay <= b such that + % + % if Ay <= b + % if polygon is convex: + % y is inside polygon + % else + % y is inside the convex hull of the polygon + % else + % y is outside polygon + % + + %% get convex hull + idx = convhull(corners(1,:)', corners(2,:)'); + corners = corners(:,idx); + + %% get coefficients + n_vert = size(corners,2)-1; + A = zeros(n_vert,2); + b = zeros(n_vert,1); + + for v = 1 : n_vert + % get normal of edge from y0 -> y1 + y0 = corners(:, v); + y1 = corners(:, v+1); + y01 = y1 - y0; + n = [y01(2); -y01(1)]; + % we are inside if (y - y0).n <= 0 + % i.e. n.y <= n.y0 + A(v,:)= n'; + b(v) = dot(n, y0); + end +end \ No newline at end of file diff --git a/calibration/bounding_polyhedron.m b/calibration/bounding_polyhedron.m new file mode 100644 index 0000000000000000000000000000000000000000..33e548c4f008cf6baf7a4ed8c872ed5d68b2690d --- /dev/null +++ b/calibration/bounding_polyhedron.m @@ -0,0 +1,42 @@ +function [A_hull,b_hull] = bounding_polyhedron(tri, vert) + % [A,b] = bounding_polyhedron(tri,vert) + % + % determine the system of linear constraints + % Ax <= b + % that describe the inside of the convex polyhedron described by + % tri and vert. That is, if x lies within the polyhedron, Ax<=b + % + % vert is a 3 x n_vert array of vertex coordinates + % tri is a n_face x 3 array of indices, describing the triangular + % faces of the polyhedron + + n_vert = size(vert, 2); + n_face = size(tri, 1); + + %% determine surface normals + % vertices of each triangular face + A = vert(:, tri(:,1)); + B = vert(:, tri(:,2)); + C = vert(:, tri(:,3)); + D = mean(vert, 2); + DA = bsxfun(@minus, A, D); + % surface normal + n = cross(B-A, C-A, 1); + n = bsxfun(@rdivide, n, sqrt(sum(n.^2,1))); + % sign of surface normal is outwards positive + % D is inside polyhedron so (A-D).n >= 0 + S = sign(sum(DA.*n, 1)); + n = bsxfun(@times, S, n); + + %% equation of plane: x.n + d = 0 + d = -sum(n.*A, 1); + + %% determine linear constraints + % each equation of plane has been chosen so that + % n.x + d = 0 + % and if inside + % n.x + d > 0 + % since n points outwards + A_hull = n'; + b_hull = -d'; +end \ No newline at end of file diff --git a/calibration/bulk_marker_search.m b/calibration/bulk_marker_search.m new file mode 100644 index 0000000000000000000000000000000000000000..89b1d376cfb121abd447480f7d722852e969b348 --- /dev/null +++ b/calibration/bulk_marker_search.m @@ -0,0 +1,276 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% bulk_marker_search.m +% find markers in images of calibration plates +% then perform a 2D calibration to dewarp the calibration plate image + +addpath([pwd '/../mex/minmax/']); + +%% input parameters + +grid_dx = 10; % real units, mm +grid_dy = 10; +invert_image = 1; % invert image for black dots on white +blur_image = 1; % gaussian blur image +normalise_image = 1; +xlim_zoom = 1024*2/4 + [-512 512]/1; +ylim_zoom = 1024*2/4 + [-512 512]/1; + +output_dir = 'D:/bigtank-jul-2013/CAL-20-07-13/NEWSHEET/'; +input_file_list = { 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-01/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-02/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-03/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-04/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-05/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-06/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-07/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-08/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-09/PLATE/CAM1_000001.tif', ... + 'D:/bigtank-jul-2013/CAL-20-07-13/SHEET-10/PLATE/CAM1_000001.tif'}; +plate_origin = [-24 0 + -20 0 + -16 0 + -12 0 + -8 0 + -4 0 + 0 0 + 4 0 + 8 0 + 12 0]'; +camera_index = 1; +dot_half_window = [24 24]; +calib_idx_offset = 0; +origin_pos = [0 0]; + +camera_clim = [0 1]; + +%% generate calibration for each image + +nCalibrations = length(input_file_list); + +calib_idx = 1; + +while calib_idx <= nCalibrations + %% update user of status + input_file = input_file_list{calib_idx}; + output_file = sprintf('%s/CAM%d-%02d.mat', output_dir, camera_index, calib_idx+calib_idx_offset); + fprintf('Image %02d of %02d:\n', calib_idx, nCalibrations); + + % check if calibration already exists + if exist(output_file, 'file') + sInput = input('Calibration already exists - overwrite? y to overwrite: ', 's'); + if ~strcmpi(sInput, 'y') + calib_idx = calib_idx + 1; + continue; + end + end + % check if image input exists + if ~exist(input_file, 'file') + fprintf(' - could not find image file\n'); + end + + %% load image, request position + im = imread(input_file)'; + im = double(im); + im = fliplr(im); + Nx = size(im, 1); + Ny = size(im, 2); + + %% create filtered image for processing + im_filt = im; + if invert_image + im_filt = max(im(:))-im; + end + if blur_image + G = fspecial('gaussian', [5 5]); + im_filt = filter2(G, im_filt); + end + if normalise_image + [minv, maxv] = minmaxfiltnd(single(im_filt), dot_half_window*2+1); + im_filt = (im_filt - minv) ./ max(32, maxv-minv); + end + + %% display image + figure(gcf); + hold off; + imagesc(im_filt'); + axis equal tight xy; + colormap gray; + set(gca,'Clim', camera_clim); + + %% get center, right and top marker position + bDone = false; + while ~bDone + %% get marker position from user + fprintf('Please identify the origin, right and top markers:\n'); + xlim(xlim_zoom); + ylim(ylim_zoom); + [i_coord,j_coord] = ginput(3); + axis tight; + + %% find barycenter of centre marker + dot_img_i = round(i_coord(1)) + (-dot_half_window(1) : dot_half_window(1)); + dot_img_j = round(j_coord(1)) + (-dot_half_window(2) : dot_half_window(2)); + [imat,jmat] = ndgrid(dot_img_i, dot_img_j); + dot_img = im_filt(dot_img_i, dot_img_j); + ij_center(1) = sum(dot_img(:) .* imat(:)) / sum(dot_img(:)); + ij_center(2) = sum(dot_img(:) .* jmat(:)) / sum(dot_img(:)); + + ij_right = [i_coord(2) j_coord(2)]; + ij_top = [i_coord(3) j_coord(3)]; + + %% ask user if they want to keep it + hold on; + plot_markers = plot( ij_center(1), ij_center(2), 'bs', ... + ij_right(1), ij_right(2), 'ro', ... + ij_top(1), ij_top(2), 'gd'); + %legend('Origin', 'Right', 'Top', 'Location', 'SouthOutside'); + + sDone = input('Enter y if you would like to try again: ', 's'); + bDone = true; + if strcmpi(sDone,'y') + bDone = false; + end + delete(plot_markers); + end + + %% find marker positions + [ij_markers, xy_markers, onx_idx, ony_idx] = find_calibration_markers(im_filt, ij_center, ij_right, ij_top, dot_half_window); + on_x_axis = false(1,size(ij_markers,2)); + on_y_axis = false(1,size(ij_markers,2)); + on_x_axis(onx_idx) = true; + on_y_axis(ony_idx) = true; + xy_markers(1,:) = xy_markers(1,:) * grid_dx + origin_pos(1); + xy_markers(2,:) = xy_markers(2,:) * grid_dy + origin_pos(2); + + %% find positions more accurately using barycentres + Nmarkers = size(ij_markers,2); + for marker_idx = 1 : Nmarkers + dot_img_i = round(ij_markers(1,marker_idx)) + (-dot_half_window(1) : dot_half_window(1)); + dot_img_j = round(ij_markers(2,marker_idx)) + (-dot_half_window(2) : dot_half_window(2)); + dot_img_i = dot_img_i(dot_img_i > 1 & dot_img_i < Nx); + dot_img_j = dot_img_j(dot_img_j > 1 & dot_img_j < Ny); + dot_img = im_filt(dot_img_i, dot_img_j); + [imat,jmat] = ndgrid(dot_img_i, dot_img_j); + ij_markers(1,marker_idx) = sum(dot_img(:).*imat(:)) / sum(dot_img(:)); + ij_markers(2,marker_idx) = sum(dot_img(:).*jmat(:)) / sum(dot_img(:)); + end + + %% throw away any markers too near edges + bad_markers = ij_markers(1,:) < 10 | ij_markers(1,:) > (Nx-10) ... + | ij_markers(2,:) < 10 | ij_markers(2,:) > (Ny-10); + xy_markers = xy_markers(:, ~bad_markers); + ij_markers = ij_markers(:, ~bad_markers); + on_x_axis = on_x_axis(~bad_markers); + on_y_axis = on_y_axis(~bad_markers); + + %% plot x/y coords of each marker + n_markers = size(ij_markers, 2); + + for idx = 1 : n_markers + str = sprintf('(%0.0f,%0.0f)', xy_markers(1,idx), xy_markers(2,idx)); + text( 'Position', ij_markers(:,idx) + [-20; -20], ... + 'String', str, ... + 'FontSize', 8, ... + 'Color', 'red'); + end + + %% query user for markers to delete + bDone = false; + while ~bDone + figure(gcf); + plot_markers = plot( ij_markers(1,:), ij_markers(2,:), 'r+', ... + ij_markers(1,on_x_axis), ij_markers(2,on_x_axis), 'go', ... + ij_markers(1,on_y_axis), ij_markers(2,on_y_axis), 'gd', ... + ij_center(1), ij_center(2), 'bs'); + %legend('Markers', 'X axis', 'Y axis', 'Center'); + + bDone = true; + sDelete = input('Would you like to delete a marker? y to delete: ', 's'); + if strcmp(sDelete, 'y') + bDone = false; + fprintf('Click near the marker you''d like to delete\n'); + [i_delete,j_delete] = ginput(1); + [idx, distance] = knnsearch(ij_markers', [i_delete,j_delete], 'K', 1); + if distance > 10 + fprintf('You didn''t click anywhere near a marker!\n'); + continue; + end + keep = (1 : size(ij_markers,2)) ~= idx; + ij_markers = ij_markers(:, keep); + xy_markers = xy_markers(:, keep); + on_x_axis = on_x_axis(keep); + on_y_axis = on_y_axis(keep); + + delete(plot_markers); + end + end + + %% create calibration and dewarp + xy_to_i_coeffs = cubicfit_2d(xy_markers(1,:), xy_markers(2,:), ij_markers(1,:), 1); + xy_to_j_coeffs = cubicfit_2d(xy_markers(1,:), xy_markers(2,:), ij_markers(2,:), 1); + ij_to_x_coeffs = cubicfit_2d(ij_markers(1,:), ij_markers(2,:), xy_markers(1,:), 1); + ij_to_y_coeffs = cubicfit_2d(ij_markers(1,:), ij_markers(2,:), xy_markers(2,:), 1); + + i_fit = cubicmap_2d(xy_markers(1,:), xy_markers(2,:), xy_to_i_coeffs); + j_fit = cubicmap_2d(xy_markers(1,:), xy_markers(2,:), xy_to_j_coeffs); + i_error = ij_markers(1,:) - i_fit; + j_error = ij_markers(2,:) - j_fit; + px_error = sqrt(i_error.^2 + j_error.^2); + rms_error = sqrt(mean(px_error.^2)); + + fprintf('RMS fit error: %0.2f px\n', rms_error); + + % work out x and y axes + [imat,jmat] = ndgrid(1 : 32 : Nx, 1 : 32 : Ny); + xmat = cubicmap_2d(imat, jmat, ij_to_x_coeffs); + ymat = cubicmap_2d(imat, jmat, ij_to_y_coeffs); + x_lim = fix([min(xmat(:)), max(xmat(:))]); + y_lim = fix([min(ymat(:)), max(ymat(:))]); + x_axis = linspace(x_lim(1), x_lim(2), Nx); + y_axis = linspace(y_lim(1), y_lim(2), Ny); + i_axis = 1 : Nx; + j_axis = 1 : Ny; + % calculate dewarping + [xmat,ymat] = ndgrid(x_axis, y_axis); + imat = cubicmap_2d(xmat, ymat, xy_to_i_coeffs); + jmat = cubicmap_2d(xmat, ymat, xy_to_j_coeffs); + im_xy = interp2(i_axis, j_axis, im', imat, jmat, 'linear'); + hold off; + imagesc(x_axis, y_axis, im_xy'); + xlim(x_lim); + ylim(y_lim); + colormap gray; + axis xy equal tight; + hold on; + set(gca,'Clim', camera_clim); + for x = (round(x_lim(1)/grid_dx) : round(x_lim(end)/grid_dx)) * grid_dx + plot([x x], [y_axis(1) y_axis(end)], 'r-'); + end + for y = (round(y_lim(1)/grid_dy) : round(y_lim(end)/grid_dy)) * grid_dy + plot([x_axis(1) x_axis(end)], [y y], 'r-'); + end + + %% query user to save calibration or repeat + + sInput = input('Would you like to repeat the calibration? y to repeat: ', 's'); + if strcmpi(sInput, 'y') + continue; % continue without incrementing the counter + end + + %% save output + fprintf('Saving ... '); + fstruct = struct('source_image', im, ... + 'dewarped_image', im_xy, ... + 'plate_pos', plate_origin(:, calib_idx), ... + 'xy_to_i_coeffs', xy_to_i_coeffs, 'xy_to_j_coeffs', xy_to_j_coeffs, ... + 'ij_to_x_coeffs', ij_to_x_coeffs, 'ij_to_y_coeffs', ij_to_y_coeffs, ... + 'i_axis', i_axis, 'j_axis', j_axis, ... + 'x_axis', x_axis, 'y_axis', y_axis, 'x_lim', x_lim, 'y_lim', y_lim, ... + 'grid_dx', grid_dx, 'grid_dy', grid_dy, ... + 'ij_markers', ij_markers, 'xy_markers', xy_markers, ... + 'ij_center', ij_center, 'ij_right', ij_right, 'ij_top', ij_top, ... + 'camera_index', camera_index); + save(output_file, '-struct', 'fstruct'); + fprintf('done\n'); + calib_idx = calib_idx + 1; +end \ No newline at end of file diff --git a/calibration/bundle_calibration_fit.m b/calibration/bundle_calibration_fit.m new file mode 100644 index 0000000000000000000000000000000000000000..b04c8a315b1b4b33b31d7dad93ab65323eabb54d --- /dev/null +++ b/calibration/bundle_calibration_fit.m @@ -0,0 +1,185 @@ +function new_setup = bundle_calibration_fit(setup) + % perform a bundle calibration for a set of cameras and scene + % information provided in setup + + %% extract basic information + n_cameras = setup.n_cameras; + n_views = setup.n_views; + + %% initialise setup with a regular pinhole fit + fprintf('initial pinhole fit ... '); + s_cam_calib = pinhole_camera_fit(setup); + for c = 1 : n_cameras + setup.camera(c).calib = s_cam_calib(c); + end + fprintf('done\n'); + + %% initialise perturbation model + setup.omega = eye(3); + setup.gl_plate_dx = zeros(3, n_views); + + %% fill arrays for least squares fitting + % uv_data should be n_total x 4 array of [u, v, cam, view] + % ij_data should be n_total x 2 array of [i, j] position of markers + n_total = 0; + uv_data = []; + ij_data = []; + for c = 1 : n_cameras + camera = setup.camera(c); + for v = 1 : n_views + %% extract marker position in plate coord sys and image + % xdata contains (u,v) coord of marker on plate + % as well as the index of the plate position and camera + % that the projection of this marker in image space corresponds to + uv_mark = camera.view(v).uv_markers; + ij_mark = camera.view(v).ij_markers; + n_mark = size(uv_mark,2); + + uv_mark = [uv_mark; c*ones(1,n_mark); v*ones(1,n_mark)]; + uv_data = [uv_data; uv_mark']; + ij_data = [ij_data; ij_mark']; + n_total = n_total + n_mark; + end + end + + %% least squares fitting operation + fprintf('doing bundle fit ...'); + X0 = pack_model(setup); + NX = length(X0); + opts = optimset('MaxFunEvals', NX*10000, 'MaxIter', 10000, 'Display', 'on'); + X = lsqcurvefit(@(X,xdata) bundle_model(X,xdata,setup), X0, uv_data, ij_data, [], [], opts); + new_setup = unpack_model(X, setup); + fprintf('done\n'); +end + +function ij_selected = bundle_model(perturb_vec, uv_mark, setup) + % given a perturbation (packed into perturbation vector) + % project markers with perturbed model + setup = unpack_model(perturb_vec, setup); + + %% perturbed position and orientation of plate + gl_plate_pos = setup.gl_plate_pos + setup.gl_plate_dx; + pl_to_gl = setup.omega * setup.pl_to_gl; + + %% project markers + n_cameras = setup.n_cameras; + n_markers = size(uv_mark, 1); + cam = uv_mark(:, 3); + view = uv_mark(:, 4); + % gl_mark and pl_mark are 3 x n_markers matrices + pl_mark = [uv_mark(:, 1:2), zeros(n_markers,1)]'; + gl_mark = pl_to_gl*pl_mark + gl_plate_pos(:, view); + + % project markers in *every* camera + ij_mark = zeros(2, n_markers, n_cameras); + for c = 1 : n_cameras + ij_mark(:,:,c) = pinhole_model(gl_mark, setup.camera(c).calib); + end + + % return only the appropriate projections, according to uv_mark(:,3) + ij_selected = zeros(n_markers, 2); + ij_selected(:,1) = ij_mark(sub2ind([2 n_markers n_cameras], 1*ones(n_markers,1), (1:n_markers)', cam)); + ij_selected(:,2) = ij_mark(sub2ind([2 n_markers n_cameras], 2*ones(n_markers,1), (1:n_markers)', cam)); +end + +function X = pack_model(setup) + % function to pack a representation of the internal + % camera and scene model into a vector for least squares optimisation + % routine + + n_cameras = setup.n_cameras; + n_views = setup.n_views; + + %% camera model + X_cam = zeros(15, n_cameras); + for c = 1 : n_cameras + calib = setup.camera(c).calib; + G = calib.G; + R = calib.R; + r_vec = vrrotmat2vec(R); + r_vec = r_vec(1:3) * r_vec(4); + xc = calib.xc; + kr = calib.kr; + ic = calib.ic; + + i_axis = setup.camera(c).i_axis; + j_axis = setup.camera(c).j_axis; + diag_px = sqrt((i_axis(end)-i_axis(1))^2 + (j_axis(end)-j_axis(1))^2); + + X_cam(1,c) = G(1,1); + X_cam(2,c) = G(1,2); + X_cam(3,c) = G(1,3) / diag_px; + X_cam(4,c) = G(2,2); + X_cam(5,c) = G(2,3) / diag_px; + X_cam(6:8,c) = r_vec(:); + X_cam(9:11,c) = xc(:); + % dewarping parameters are normalised to dimensionless quantities, + % scaled by sensor size in px + X_cam(12:13,c) = [kr(1)*diag_px^2; kr(2)*diag_px^4]; + X_cam(14:15,c) = ic(:) / diag_px; + end + + %% plate perturbation model + % plate perturbation consists of a displacement and rotation + X_plate = setup.gl_plate_dx([1 3], :); + omega_vec = vrrotmat2vec(setup.omega); + X_rot = omega_vec(1:3) * omega_vec(4); + + X = [X_cam(:); X_plate(:); X_rot(:)]'; +end + +function setup = unpack_model(X, setup) + % extract the perturbation model specified in X + % and update setup structure + n_views = setup.n_views; + n_cameras = setup.n_cameras; + + %% extract + off = 1; + X_cam = reshape(X(off : off-1 + n_cameras*15), [15 n_cameras]); + off = off + n_cameras*15; + X_plate = reshape(X(off : off-1 + n_views*2), [2 n_views]); + off = off + n_views*2; + X_rot = X(off : off + 2); + + %% update camera parameters + for c = 1 : n_cameras + i_axis = setup.camera(c).i_axis; + j_axis = setup.camera(c).j_axis; + diag_px = sqrt((i_axis(end)-i_axis(1))^2 + (j_axis(end)-j_axis(1))^2); + + % intrinsic parameters + fx = X_cam(1,c); + tau = X_cam(2,c); + ox = X_cam(3,c) * diag_px; + fy = X_cam(4,c); + oy = X_cam(5,c) * diag_px; + G = [fx tau ox; + 0 fy oy; + 0 0 1]; + % rotation + r_vec = X_cam(6:8,c); + R = vrrotvec2mat([r_vec(:)/(eps+norm(r_vec)); norm(r_vec)]); + + % position + xc = X_cam(9:11,c); + + % dewarping parameters are normalised to dimensionless quantities, + % scaled by sensor size in px + kr = [X_cam(12,c)/diag_px^2; X_cam(13,c)/diag_px^4]; % undo normalisation + ic = X_cam(14:15,c) * diag_px; + + % full projection model + P = G*R*[eye(3), -xc(:)]; + + calib = struct('P', P, 'G', G, 'R', R, 'xc', xc, 'kr', kr, 'ic', ic); + setup.camera(c).calib = calib; + end + + %% update plate position parameters + omega_vec = [X_rot(:)/(eps+norm(X_rot)); norm(X_rot)]; + setup.omega = vrrotvec2mat(omega_vec); + setup.gl_plate_dx = [X_plate(1,:); zeros(1,n_views); X_plate(2,:)]; +end + + diff --git a/calibration/calcmask.m b/calibration/calcmask.m new file mode 100644 index 0000000000000000000000000000000000000000..3f2f086863725126a7ccfd829f737427dcf7e03d --- /dev/null +++ b/calibration/calcmask.m @@ -0,0 +1,107 @@ +function mask = calcmask(camset, c0) + % mask = calcmask(camset, c) + % + % for each pixel in camera c of camset, determine the number + % of other cameras its epipolar ray is visible in + + %% get things + n_cameras= length(camset); + cam0 = camset(c0); + Ni = length(cam0.i_axis); + Nj = length(cam0.j_axis); + Npx = Ni*Nj; + + %% dewarp + [im,jm] = ndgrid(cam0.i_axis, cam0.j_axis); + ui_pos = inverse_radial_distortion([im(:),jm(:)]', cam0.calib.ic, cam0.calib.kr); + + %% get equation of ray + % we write the equation of ray as + % x = x0 + s*e_s + ytilde = [ui_pos; ones(1,Npx)]; + GR = cam0.calib.G * cam0.calib.R; + e_ray = GR \ ytilde; + x0_ray = cam0.calib.xc; + + %% determine mask + mask = zeros(Ni, Nj); + for c1 = 1 : n_cameras + %% skip if same camera + if c0 == c1 + continue; + end + + %% find linear inequality for bounds test + % the test for whether a point y is within the bounds of image space + % of this camera is described as an inequality Ay <= b. If satisfied, + % the point is within bounds, if not, it is outside bounds. we + % construct this inequality based on borders of image in undistorted + % image space + cam1 = camset(c1); + i_axis = cam1.i_axis(:); + j_axis = cam1.j_axis(:); + Ni1 = length(i_axis); + Nj1 = length(j_axis); + ir = [1 : floor(Ni1/4) : Ni1, Ni1]; + jr = [1 : floor(Nj1/4) : Nj1, Nj1]; + i_corners = [i_axis(ir([1 1 1 1 1])); + i_axis(ir ); + i_axis(ir([5 5 5 5 5])); + i_axis(ir(end:-1:1 ))]; + j_corners = [j_axis(jr ); + j_axis(jr([5 5 5 5 5])); + j_axis(jr(end:-1:1 )); + j_axis(jr([1 1 1 1 1]))]; + di_corners = [i_corners'; j_corners']; + ui_corners = inverse_radial_distortion(di_corners, cam1.calib.ic, cam1.calib.kr); + [A,b] = bounding_polygon(ui_corners); + + %% get equation of ray in image coordinates + % we want an equation of the form + % y = y0 + s * n + % that describes the equation of each ray from camera 0 in the + % (undistorted) image space of camera 1 + + GR1 = cam1.calib.G * cam1.calib.R; + z0 = GR1(1:2,:)*(x0_ray - cam1.calib.xc); + zprime = GR1(1:2,:)*e_ray; + lambda0 = GR1( 3,:)*(x0_ray - cam1.calib.xc); + lprime = GR1( 3,:)*e_ray; + + y0 = bsxfun(@rdivide, zprime, lprime); + e_n = bsxfun(@minus, z0, lambda0*y0); + e_n = bsxfun(@rdivide, e_n, sqrt(sum(e_n.^2,1))); % normalise + + %% determine existence of solutions for inequality + % subsituting in y = y0 + s*e_n + % into our inequality Ay <= b + % yields the inequality + % L*s <= R + % where L = A*e_n and R = b - A*y0 + % if this inequality has solutions for any s, then there is at least + % one point on the ray which is visible in camera 1 + + L = A*e_n; + R = bsxfun(@minus, b, A*y0); + gamma_lim = R ./ L; + is_ub = L >= 0; + is_lb = L < 0; + + % find smallest upper bound + gamma_ub = gamma_lim; + gamma_ub(is_lb) = inf; % ignore all those lower bounds + gamma_ub = min(gamma_ub, [], 1); + + % find largest lower bound + gamma_lb = gamma_lim; + gamma_lb(is_ub) = -inf; % ignore all the upper bounds + gamma_lb = max(gamma_lb, [], 1); + + b_visible = gamma_lb <= gamma_ub; + + %% add to sum + b_visible = reshape(b_visible, [Ni Nj]); + mask = mask + b_visible; + end +end + \ No newline at end of file diff --git a/calibration/calibration_to_config.m b/calibration/calibration_to_config.m new file mode 100644 index 0000000000000000000000000000000000000000..307ea705e3dd2f1e9b1f114124f667513c2be463 --- /dev/null +++ b/calibration/calibration_to_config.m @@ -0,0 +1,135 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% based on a camera calibration performed in MATLAB using John Lawson's +% code, create a corresponding .cfg file for use with Haitao Xu's PTV code +% + +addpath([pwd '/../calibration-mpids/']); + +%% input parameters +calib_dir = '/data/cluster/160323/'; +calib_file_mat = [calib_dir '/camera-model-160323.mat']; +calib_file_cfg = [calib_dir '/PTVSetup_160323.cfg']; +points_file_pat = [calib_dir '/calibpointspos.cam%d.dat']; + +pixel_pitch_mm = [10E-3 10E-3]; + +%% load calibration +fs = importdata(calib_file_mat); +n_cameras = fs.n_cameras; +camera = fs.camera; + +%% calculate Tsai camera calibration +fprintf('Calculating Tsai calibration ... '); + +for c = 1 : n_cameras + %% load parameters + i_axis = camera(c).i_axis; + j_axis = camera(c).j_axis; + Ni = length(i_axis); + Nj = length(j_axis); + cparams = struct('Npixh', Nj, 'Npixw', Ni, ... + 'hpix', pixel_pitch_mm(1), ... + 'wpix', pixel_pitch_mm(2)); + + %% load markers and calculate residuals + gl_markers = camera(c).calib.gl_markers; + ij_markers = camera(c).calib.ij_markers; + ij_markers_proj = pinhole_model(gl_markers, camera(c).calib, true); + model_res = ij_markers - ij_markers_proj; + + %% generate Tsai calibration + % Tsai calibration uses a different coordinate system for image plane + % in our calibration, (i,j) corresponds to x,y coordinate, as measured + % from the bottom left corner. ij the Tsai calibration, (i,j) + % coordinates start from the top left corner and move down and across. + ij_markers_perm = [ij_markers(1,:); + Nj-1 - ij_markers(2,:)]; + + [tsai_calib, tsai_res] = calib_Tsai(ij_markers_perm', gl_markers', cparams); + tsai_res = tsai_res'; + camera(c).tsai_calib = tsai_calib; + + %% save calibpointspos file + points_file = sprintf(points_file_pat, c - 1); + fid = fopen(points_file, 'w'); + fprintf(fid, '# pimg_x (pix)\t pimg_y (pix) \t x (mm) \t y (mm) \t z (mm) \t Np \t sum(x) \t sum(y) \t sum(I) \n'); + fprintf(fid, '%12.7f\t%12.7f\t%12.7f\t%12.7f\t%12.7f\t0\n', [ij_markers_perm; gl_markers]); + fclose(fid); + + %% generate statistics of error + model_rms = rms(model_res, 2); + tsai_rms = rms(tsai_res, 2); + + %% report on error + fprintf('Camera %d\n', c); + fprintf(' Regular: %0.3f %0.3f\n', model_rms); + fprintf(' Tsai: %0.3f %0.3f\n', tsai_rms); +end +fprintf('done\n'); + +%% write config file +fprintf('Saving to file ... '); + +% Now write the configuration file +fid = fopen(calib_file_cfg, 'w'); +fprintf(fid, '# PTV experiment configuration file\n'); +fprintf(fid, '\n %d\t# ncams\n\n', n_cameras); +for c = 1 : n_cameras + %% load info + tsai_calib = camera(c).tsai_calib; + i_axis = camera(c).i_axis; + j_axis = camera(c).j_axis; + Ni = length(i_axis); + Nj = length(j_axis); + + %% write to file + fprintf(fid, '######## cam %d ########\n\n', c - 1); + fprintf(fid, '%d\t\t\t# Npix_x\n', Ni); + fprintf(fid, '%d\t\t\t# Npix_y\n', Nj); + fprintf(fid, '%11.8f\t\t\t# pixsize_x (mm)\n', pixel_pitch_mm(1)); + fprintf(fid, '%11.8f\t\t# pixsize_y (mm)\n', pixel_pitch_mm(1)); + fprintf(fid, '%12.8f\t\t# effective focal length (mm)\n', tsai_calib.f_eff); + % Note the sign change for k1, because its meaning is different in calib_Tsai and the stereomatching code + fprintf(fid, '%15.8e\t# radial distortion k1 (1/pixel)\n', -tsai_calib.k1); + fprintf(fid, '%15.8e\t# tangential distortion p1 (1/pixel)\n', tsai_calib.p1); + fprintf(fid, '%15.8e\t# tangential distortion p2 (1/pixel)\n', tsai_calib.p2); + fprintf(fid, '0.0\t\t# x0\n'); + fprintf(fid, '0.0\t\t# y0\n'); + % Parameters for camera movement correction + fprintf(fid, '0.0\t\t# x0_offset (pixel)\n'); + fprintf(fid, '0.0\t\t# y0_offset (pixel)\n'); + fprintf(fid, '1.0\t\t# x_rot (cos(theta))\n'); + fprintf(fid, '0.0\t\t# y_rot (sin(theta))\n'); + fprintf(fid, '# rotation matrix R\n'); + fprintf(fid, '%12.8f\t%12.8f\t%12.8f\n', (tsai_calib.R)'); + fprintf(fid, '# translation vector T\n'); + fprintf(fid, '%15.8f\n', tsai_calib.T); + fprintf(fid, '# inverse rotation matrix Rinv\n'); + fprintf(fid, '%12.8f\t%12.8f\t%12.8f\n', (tsai_calib.Rinv)'); + fprintf(fid, '# inverse translation vector Tinv\n'); + fprintf(fid, '%15.8f\n', tsai_calib.Tinv); + fprintf(fid, '# rms distance between particle centers found on image plane and their projections\n'); + fprintf(fid, '# %15.8f\t\t# err_x\n', tsai_calib.err_x); + fprintf(fid, '# %15.8f\t\t# err_y\n', tsai_calib.err_y); + fprintf(fid, '# %15.8f\t\t# err_t\n', tsai_calib.err_t); + fprintf(fid, '\n'); +end +% laser beam parameters +fprintf(fid, '##### laser beam #####\n'); +fprintf(fid, '\n'); +fprintf(fid, '0\t\t# finite volume illumination\n'); +fprintf(fid, '-50\t\t# illum_xmin\n'); +fprintf(fid, '+50\t\t# illum_xmax\n'); +fprintf(fid, '-50\t\t# illum_ymin\n'); +fprintf(fid, '+50\t\t# illum_ymax\n'); +fprintf(fid, '-50\t\t# illum_zmin\n'); +fprintf(fid, '+50\t\t# illum_zmax\n'); +fprintf(fid, '\n'); +% 3D matching parameters +fprintf(fid, '#### thresholds for 3D matching ####\n'); +fprintf(fid, '\n'); +fprintf(fid, '3.0\t\t# mindist_pix (pixel)\n'); +fprintf(fid, '1.0\t\t# maxdist_3D (mm)\n'); +fclose(fid); + +fprintf('done\n'); \ No newline at end of file diff --git a/calibration/calpts2mat.m b/calibration/calpts2mat.m new file mode 100644 index 0000000000000000000000000000000000000000..e8a089f07c016d2077052112b18a6789ef240ede --- /dev/null +++ b/calibration/calpts2mat.m @@ -0,0 +1,82 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% load a text file from Jan/Holly's calibration +% containing indices/image coordinates of calibration points +% and convert this to the common .MAT file format + +addpath([pwd '/pinhole/']); + +%% input parameters +calib_dir = '/nfs/scratch21/lawson/zugspitze/calib/'; +mat_file_pat = '%s/CAM%d-%02d.mat'; +calpts_file_pat = '%s/CalibPts%d.txt'; + +camera_index = 3; +camera_id = camera_index - 1; +grid_dx = 0.5; % real units, mm +grid_dy = 0.5; + +img_width = 1280; +img_height = 800; +i_axis = 0 : 1 : img_width - 1; +j_axis = 0 : 1 : img_height - 1; + +DO_DEBUG_PLOT = true; + +%% load calibration points file +fname = sprintf(calpts_file_pat, calib_dir, camera_id); +C = textread(fname); +gl_markers = C(:, 1:3)'; +ij_markers = C(:, [5 4])'; + +%% get plate pos from z +plate_pos = unique(gl_markers(3,:))'; +n_views = length(plate_pos); +plate_pos = [zeros(n_views,2), plate_pos]; + +%% iterate over planes +for calib_idx = 1 : n_views + %% load subset belonging to this plane + rng = gl_markers(3,:) == plate_pos(calib_idx, 3); + xy_mark = gl_markers(1:2, rng); + ij_mark = ij_markers(:, rng); + gl_plate_pos = plate_pos(calib_idx,:); + + %% fit pinhole + [P2D, ij_res] = p2d_fit(xy_mark, ij_mark); + ij_rms = rms(ij_res, 2); + + %% axes and limits + x_lim = [min(xy_mark(1,:)), max(xy_mark(1,:))]; + y_lim = [min(xy_mark(2,:)), max(xy_mark(2,:))]; + Nx = img_width; + Ny = img_height; + x_axis = linspace(x_lim(1), x_lim(2), Nx); + y_axis = linspace(y_lim(1), y_lim(2), Ny); + + %% plot + if DO_DEBUG_PLOT + figure(1); + clf; + plot3(xy_mark(1,:), xy_mark(2,:), ij_mark(1,:), 'b.'); + hold on; + plot3(xy_mark(1,:), xy_mark(2,:), ij_mark(2,:), 'r.'); + end + + %% save + output_file = sprintf(mat_file_pat, calib_dir, camera_index, calib_idx); + fprintf('Saving to %s... ', output_file); + fstruct = struct('plate_pos', gl_plate_pos, ... + 'P', P2D, ... + 'i_axis', i_axis, 'j_axis', j_axis, ... + 'x_axis', x_axis, 'y_axis', y_axis, 'x_lim', x_lim, 'y_lim', y_lim, ... + 'grid_dx', grid_dx, 'grid_dy', grid_dy, ... + 'ij_markers', ij_mark, 'xy_markers', xy_mark, ... + 'camera_index', camera_index, ... + 'id', camera_id); + save(output_file, '-struct', 'fstruct'); + fprintf('done\n'); + + %% report + fprintf('2D fit plane %d:\n', calib_idx); + fprintf(' RMS: %0.3f %0.3f px\n', ij_rms); +end \ No newline at end of file diff --git a/calibration/camera_calibration.m b/calibration/camera_calibration.m new file mode 100644 index 0000000000000000000000000000000000000000..f0831384ffe2b0b30d0ff0b5ded531940170fdef --- /dev/null +++ b/calibration/camera_calibration.m @@ -0,0 +1,280 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% camera_calibration.m +% +% create a pinhole camera calibration for one or more cameras +% from a set of views of the same calibration plate in different positions +% a compensation is made for error in calibration plate position and +% orientation + +addpath([pwd '/pinhole/']); +addpath([pwd '/../../misc/']); +addpath([pwd '/../../misc/mtimesx/']); +addpath([pwd '/../../misc/mmx']) +addpath([pwd '/../matching/']); + +%% input parameters + +calib_dir = '/nfs/scratch23/lawson/K62/170203/calib/'; +cam_calib_file_pat = [calib_dir '/CAM%d-%02d.mat']; +cam_model_file = [calib_dir '/camera-model-original.mat']; + +cam_index_range = [1 2]; +cam_view_range = 1 : 7; +n_views = length(cam_view_range); +n_cameras = length(cam_index_range); +n_iterations = 2; + +DO_DEBUG_PLOT = true; + +%% estimate of plate orientation relative to global coordinate system +plate_eu = [1 0 0]; +plate_ev = [0 1 0]; +plate_ew = [0 0 1]; +pl_to_gl = [plate_eu(:) plate_ev(:) plate_ew(:)]; + +%% build setup structure + +fprintf('Loading data from file ... '); + +gl_plate_pos = zeros(3, n_views); +pl_to_gl_mat = repmat(pl_to_gl, [1 1 n_views]); +camera = struct('i_axis', [], 'j_axis', [], ... + 'id', [], 'view', []); +camera.view = struct('uv_markers', [], ... + 'ij_markers', []); +camera.view(1:n_views) = camera.view; +emptyCamera = camera; + +setup = struct('pl_to_gl', pl_to_gl, ... + 'pl_to_gl_mat', pl_to_gl_mat, ... + 'gl_plate_pos', gl_plate_pos, ... + 'omega', eye(3), ... + 'gl_plate_dx', zeros(3, n_views), ... + 'gl_plate_omega', zeros(3, n_views), ... + 'n_cameras', n_cameras, ... + 'n_views', n_views, ... + 'camera', camera); + +for cam_idx = 1 : n_cameras + camera = emptyCamera; + + for view_idx = 1 : n_views + calib_file = sprintf(cam_calib_file_pat, cam_index_range(cam_idx), cam_view_range(view_idx)); + f = importdata(calib_file); + + camera.view(view_idx) = ... + struct('uv_markers', f.xy_markers, ... + 'ij_markers', f.ij_markers); + end + + camera.id = f.id; + camera.i_axis = f.i_axis; + camera.j_axis = f.j_axis; + setup.camera(cam_idx)= camera; +end + +for view_idx = 1 : n_views + calib_file = sprintf(cam_calib_file_pat, cam_index_range(cam_idx), cam_view_range(view_idx)); + f = importdata(calib_file); + gl_plate_pos(:, view_idx) = f.plate_pos; +end +% enforce barycentre of plate positions to be zero +gl_plate_pos = bsxfun(@minus, gl_plate_pos, mean(gl_plate_pos,2)); +setup.gl_plate_pos = gl_plate_pos; +fprintf('done\n'); + +%% pinhole fit before bundle calibration +fprintf('fitting camera model to setup before bundle calibration:\n'); +[s_old_calib,res_before]= pinhole_camera_fit(setup); +fprintf('fit quality before bundle calibration:\n'); +for cam_idx = 1 : n_cameras + + fprintf(' camera %d:\n', cam_idx); + fprintf(' RMS of i residual: %0.3f px\n', rms(res_before{cam_idx}(1,:))); + fprintf(' RMS of j residual: %0.3f px\n', rms(res_before{cam_idx}(2,:))); + %fprintf(' RMS of x residual: %0.3f mm\n', rms( + setup.camera(cam_idx).calib = s_old_calib(cam_idx); +end +old_setup = setup; + +%% iterate error fitting +% 1) use pinhole model to esimate error in plate position +% 2) update model of plate position +% 3) fit a pinhole camera model +% 4) go to 1 if iteration limit not exceeded +for it = 1 : n_iterations + %% calculate model of error + fprintf(' fitting model of error ... '); + new_setup = traverse_error_fit(setup); + fprintf('done\n'); + + %% apply correction + new_setup.gl_plate_pos = new_setup.gl_plate_pos + new_setup.gl_plate_dx; + %new_setup.pl_to_gl = new_setup.omega * new_setup.pl_to_gl; + for v = 1 : n_views + omega_vec = new_setup.gl_plate_omega(:,v); + Omega = vrrotvec2mat([omega_vec(:)/(norm(omega_vec)+eps); norm(omega_vec)]); + new_setup.pl_to_gl_mat(:,:,v) = Omega * new_setup.pl_to_gl_mat(:,:,v); + end + new_setup.omega = eye(3); + new_setup.gl_plate_dx = zeros(3, n_views); + new_setup.gl_plate_omega= zeros(3, n_views); + + %% calculate new camera calibration + % assume no perturbation to plate position + fprintf('Iteration %d of %d\n', it, n_iterations); + fprintf(' fitting pinhole camera models ... \n'); + [s_cam_calib, res] = pinhole_camera_fit(new_setup); + + for cam_idx = 1 : n_cameras + fprintf(' camera %d:\n', cam_idx); + fprintf(' RMS of i residual: %0.3f\n', rms(res{cam_idx}(1,:))); + fprintf(' RMS of j residual: %0.3f\n', rms(res{cam_idx}(2,:))); + end + + %% fix reference frame + % stop setup shuffling about by enforcing barycentre of plate positions + % to be origin + origin_shift = -mean(new_setup.gl_plate_pos, 2); + new_setup.gl_plate_pos = bsxfun(@plus, new_setup.gl_plate_pos, origin_shift); + for cam_idx = 1 : n_cameras + calib = s_cam_calib(cam_idx); + calib.xc = calib.xc(:) + origin_shift; + calib.P = calib.G * calib.R * [eye(3), -calib.xc(:)]; + new_setup.camera(cam_idx).calib = calib; + end + + %% update + setup = new_setup; +end +new_setup = setup; + +%% pinhole fit for final setup +fprintf('fitting camera model to new setup:\n'); +[s_new_calib, res_after]= pinhole_camera_fit(new_setup); +fprintf('fit quality after bundle calibration:\n'); +for cam_idx = 1 : n_cameras + fprintf(' camera %d:\n', cam_idx); + fprintf(' RMS of i residual: %0.3f\n', rms(res_after{cam_idx}(1,:))); + fprintf(' RMS of j residual: %0.3f\n', rms(res_after{cam_idx}(2,:))); + setup.camera(cam_idx).calib = s_new_calib(cam_idx); +end + +%% determine size + center of volume +% bounding volume +[gl_origin, r_sphere] = volume_bounds(new_setup); +r_sphere_lim = min(r_sphere); + +% add to setup +new_setup.gl_origin = gl_origin; +new_setup.r_sphere = r_sphere; +new_setup.r_sphere_lim = r_sphere_lim; + +%% difference compared to old setup +gl_plate_dx = new_setup.gl_plate_pos - old_setup.gl_plate_pos; +gl_plate_omega = zeros(4, n_views); +for v = 1 : n_views + Omega = new_setup.pl_to_gl_mat(:,:,v) / old_setup.pl_to_gl_mat(:,:,v); + gl_plate_omega(:,v) = vrrotmat2vec(Omega); +end +gl_plate_dtheta = gl_plate_omega(4,:); +gl_plate_rotaxis = gl_plate_omega(1:3,:); + +%% report +fprintf('------------------------------------------------------------------\n'); +fprintf('Traverse position error modelling complete\n'); +fprintf('\n'); +for c = 1 : n_cameras + fprintf('Camera %d\n', c); + fprintf('Before fit: %0.3f %0.3f px rms %0.3f %0.3f px max\n', ... + rms(res_before{c},2), max(abs(res_before{c}),[],2) ); + fprintf('After fit: %0.3f %0.3f px rms %0.3f %0.3f px max\n', ... + rms(res_after{c},2), max(abs(res_after{c}),[],2) ); + fprintf('Largest inscribed sphere:\n'); + fprintf(' r = %0.2f mm, d = %0.2fmm\n', r_sphere(c), r_sphere(c)*2); + fprintf('\n'); +end +fprintf('Largest rotation angle: %0.3f mrad\n', max(abs(gl_plate_dtheta))*1000); +fprintf('Largest plate dx: %0.3f\n', max(abs(gl_plate_dx(1,:)))); +fprintf('Largest plate dy: %0.3f\n', max(abs(gl_plate_dx(2,:)))); +fprintf('Largest plate dz: %0.3f\n', max(abs(gl_plate_dx(3,:)))); + + +% report +fprintf('\n\n\n'); +fprintf('Origin is at:\n'); +fprintf(' %+0.5f\n', gl_origin); +fprintf('Inscribed sphere has dimension:\n'); +fprintf(' r = %0.2f mm, d = %0.2f mm\n', r_sphere_lim, 2*r_sphere_lim); + +%% save camera model to file +fprintf('Saving ... '); +save(cam_model_file, '-struct', 'new_setup'); +fprintf('done\n'); + +%% plot error in traverse position +figure(10); +quiver(gl_plate_pos(1,:), gl_plate_pos(3,:), ... + gl_plate_dx(1,:), gl_plate_dx(3,:), 'k-'); +axis equal tight; +xlabel('Traverse x position / mm'); +ylabel('Traverse z position / mm'); +title('Estimated error in traverse position'); + +%% plot to compare new and old setups +if DO_DEBUG_PLOT + %% colours for different views + cmap = jet; + cvec_map = linspace(0,1,size(cmap,1)); + cvec_int = linspace(0,1,n_views); + marker_colour = zeros(n_views,3); + for rgb = 1 : 3 + marker_colour(:,rgb) = interp1(cvec_map, cmap(:,rgb), cvec_int, 'linear'); + end + marker_colour(1,:) = [1 0 0]; + marker_colour(end,:) = [0 0 1]; + + %% loop over setups + setup_ary = cell(1,2); + setup_ary{1} = old_setup; + setup_ary{2} = new_setup; + + for s = 1 : 2 + %% initialise + setup = setup_ary{s}; + figure(s); + clf; + subplot1(2,2,'Gap',[0.05 0.05]); + %% plot each camera + for cam_idx = 1 : n_cameras + subplot1(cam_idx); + cla; + hold on; + + camera = setup.camera(cam_idx); + for v = n_views%[1 , n_views] + uv_mark = camera.view(v).uv_markers; + n_mark = size(uv_mark, 2); + gl_plate = setup.gl_plate_pos(:,v); + gl_mark = bsxfun(@plus, gl_plate, setup.pl_to_gl_mat(:,:,v)*[uv_mark; zeros(1,n_mark)]); + + ij_mark = camera.view(v).ij_markers; + ij_proj = pinhole_model(gl_mark, camera.calib); + + plot(ij_mark(1,:), ij_mark(2,:), 'o', 'color', 'k'); %marker_colour(v,:)); + plot(ij_proj(1,:), ij_proj(2,:), 'x', 'color', 'r'); %marker_colour(v,:)); + %quiver( ij_mark(1,:), ij_mark(2,:), ... + % ij_proj(1,:)-ij_mark(1,:), ij_mark(2,:)-ij_proj(2,:),'k-'); + end + axis equal tight xy; + xlim(sort(camera.i_axis([1 end]))); + ylim(sort(camera.j_axis([1 end]))); + make_plot_pretty('i / px', 'j / px', sprintf('Camera %d', cam_index_range(cam_idx)), 20); + if cam_idx ~= 1 + ylabel(''); + end + set(gca,'xtick',camera.i_axis(1):256:camera.i_axis(end)); + set(gca,'ytick',camera.j_axis(1):256:camera.j_axis(end)); + end + end +end \ No newline at end of file diff --git a/calibration/check_calibration.m b/calibration/check_calibration.m new file mode 100644 index 0000000000000000000000000000000000000000..ccc4b0ba5074ef4c3e2c873312b33d3e834de8ba --- /dev/null +++ b/calibration/check_calibration.m @@ -0,0 +1,120 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% check calibration by loading and dewarping particle images +% + +addpath([pwd '/../reconstruction/']); + +%% input parameters + +omega_y_mrad = -40; + +% image to dewarp and plot +cam_index = 2; +figure_index = 100*cam_index; +sheet_index_range = 20;%[11:14]; +volume_index = 1; +series_index = 1; + +n_sheets = length(sheet_index_range); + +% calibration +calibration_file = 'F:/bigtank-jul-2013/CAL-06-08-13/scanning-setup.mat'; + +% image processing options +DO_IMAGE_FLIPUPDOWN = 0; +DO_IMAGE_FLIPLEFTRIGHT = 0; +DO_DARK_IMAGE = 1; +image_clim = [0 4]; + +% path of files to reconstruct +source_img_dir = 'F:/bigtank-jul-2013/MEAS-06-08-13/SCAN-05/'; +dark_img_dir = 'F:/bigtank-jul-2013/DARK/MEAS-06-08-13/SCAN-05/'; +source_img_file_pat = 'CAMID%d-series-%02d-volume-%02d-frame-%02d.raw12'; +dark_img_file_pat = 'CAMID%d-frame-%02d.raw12'; + +source_file_name_fun = @(camid,series,volume,frame) sprintf(['%s/' source_img_file_pat], source_img_dir, camid, series, volume, frame); +dark_file_name_fun = @(camid,frame) sprintf(['%s/' dark_img_file_pat], dark_img_dir, camid, frame); + +%% load the calibration data +fprintf('------------------------------------------------------------------\n'); +fprintf('CALIBRATION:\n'); + +fstruct = importdata(calibration_file); +% camera id +camera_id = fstruct.camera_id(cam_index); +% coordinate system and axes +volume_x = fstruct.X; +volume_y = fstruct.Y; +volume_z = fstruct.Z; +volume_Lx = volume_x(end) - volume_x(1); +volume_Ly = volume_y(end) - volume_y(1); +volume_Lz = volume_z(end) - volume_z(1); +volume_Nx = length(volume_x); +volume_Ny = length(volume_y); +volume_Nz = length(volume_z); +% camera mapping functions +sCamCalib = fstruct.camera_calibration(cam_index); +re_plane_coeffs = fstruct.re_plane_coeffs; +% image coordinate system +im_i = fstruct.i; +im_j = fstruct.j; + + +%% modify plane normal + +re_plane_normal = re_plane_coeffs(:, 1:3); +Omega = vrrotvec2mat([0 1 0 omega_y_mrad/1000]); +re_plane_normal = (Omega*re_plane_normal')'; +re_plane_coeffs(:,1:3) = re_plane_normal; + +%% read the images from file +fprintf(' --- loading images ... '); +source_file_names = arrayfun(source_file_name_fun, ones(1,n_sheets)*camera_id, ... + ones(1,n_sheets)*series_index, ... + ones(1,n_sheets)*volume_index, ... + sheet_index_range, 'UniformOutput', false); +[imgvol, fail] = load_image_array(source_file_names, DO_IMAGE_FLIPUPDOWN, DO_IMAGE_FLIPLEFTRIGHT); + +if DO_DARK_IMAGE + dark_file_names = arrayfun( dark_file_name_fun, ones(1, n_sheets)*camera_id, ... + sheet_index_range, 'UniformOutput', false); + darkvol = load_image_array(dark_file_names, DO_IMAGE_FLIPUPDOWN, DO_IMAGE_FLIPLEFTRIGHT); + % subtract dark images + imgvol = max(imgvol - darkvol, 0); +end + +fprintf('done\n'); + +%% dewarp each image +% back-projection onto planes with regular x and y +% (z coordinate determined by plane of laser sheet) +imgvol_dewarped = zeros(volume_Nx, volume_Ny, n_sheets); + +[xmat, ymat] = ndgrid(volume_x, volume_y); + +for it = 1 : n_sheets + %% calculate z coords on this plane and get image coords + sheet_index = sheet_index_range(it); + M = re_plane_coeffs(sheet_index, :); + zmat = -(M(4) + M(1)*xmat + M(2)*ymat) / M(3); + xyzmat = [xmat(:) ymat(:) zmat(:)]'; + ij = pinhole_model(xyzmat, sCamCalib); + + %% load image and interpolate + im = imgvol(:, :, it); + im_int = interp2(im_i, im_j, im', ij(1,:), ij(2,:), 'linear', nan); + im_int = reshape(im_int, [volume_Nx volume_Ny]); + imgvol_dewarped(:,:,it) = im_int; +end + +%% display + +for it = 1 : n_sheets + figure(figure_index + sheet_index_range(it)); + hold off; + im = imgvol_dewarped(:,:,it); + imagesc(volume_x, volume_y, im' / rms(im(:))); + axis equal tight xy; + set(gca,'clim',image_clim); +end diff --git a/calibration/combine_calibrations.m b/calibration/combine_calibrations.m new file mode 100644 index 0000000000000000000000000000000000000000..493c4dc875529def2f1b67f9f739b734ea4c70ed --- /dev/null +++ b/calibration/combine_calibrations.m @@ -0,0 +1,309 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% combine_c.m +% +% create a pinhole camera calibration for one or more cameras +% from a set of views of the same calibration plate in different positions +% a compensation is made for error in calibration plate position and +% orientation + +addpath([pwd '/pinhole/']); + +%% input parameters + +calib_dir = 'D:/bigtank-jul-2013/CAL-20-07-13/NEWSHEET/'; +cam_model_file = [calib_dir '/camera-model-280415.mat']; +sheet_model_file = [calib_dir '/sheet-model-280415.mat']; +setup_file = [calib_dir '/scanning-setup-280415.mat']; +poly_order = 5; + +%% load camera calibration + +fstruct = importdata(cam_model_file); +n_cameras = fstruct.n_cameras; +camera_setup = fstruct.camera; +im_i = camera_setup(1).i_axis; +im_j = camera_setup(1).j_axis; + +%% load laser sheet calibration + +fstruct = importdata(sheet_model_file); +n_sheets = fstruct.n_sheets; +gl_eZ_mat = fstruct.gl_sheet_normal; +gl_xs_mat = fstruct.gl_sheet_pos; +gl_on_sheet = fstruct.gl_on_sheet; +w_vec = fstruct.sheet_width; + +%% define orientation of [X,Y,Z] coordinate system + +% orentation of reconstruction coordinate system in global system +gl_eZ = mean(gl_eZ_mat, 1); +gl_eX = cross([0 1 0]', gl_eZ); +gl_eX = gl_eX / norm(gl_eX); +gl_eY = cross(gl_eZ, gl_eX); + +% transfer matrices for gl->re and re->gl orientation +re_to_gl = [gl_eX(:), gl_eY(:), gl_eZ(:)]; +gl_to_re = re_to_gl^-1; + +% origin of reconstruction coordinate system +gl_X0 = [0 0 0]'; % origin in gl coordinates +re_X0 = gl_to_re * gl_X0; % origin in re +re_eZ_mat = (gl_to_re * gl_eZ_mat')'; +re_xs_mat = (gl_to_re * (gl_xs_mat - ones(n_sheets,1)*gl_X0(:)')')'; + +%% calculate the plane coefficients +% these are a set of 4 coefficients per plane, M(1:4) +% all points [X,Y,Z] on this plane satisfy XM(1) + YM(2) + ZM(3) + M(4) = 0 + +re_plane_coeffs = zeros(n_sheets, 4); + +for sheet_idx = 1:n_sheets + % get equation of plane dot(M,X) = 1 + gl_sheet_xs = gl_xs_mat(sheet_idx, :)'; + gl_sheet_en = gl_eZ_mat(sheet_idx, :)'; + re_sheet_xs = gl_to_re * (gl_sheet_xs - gl_X0); + re_sheet_en = gl_to_re * gl_sheet_en; + re_sheet_en = re_sheet_en / norm(re_sheet_en); + % save to matrix + re_plane_coeffs(sheet_idx,1:3) = re_sheet_en; + re_plane_coeffs(sheet_idx,4) = -dot(re_sheet_xs, re_sheet_en); +end + +% smooth out errors by replacing measured value with fitted value +k = 0 : n_sheets - 1; +re_plane_coeffs_o = re_plane_coeffs; +for i = 1:4 + p = polyfit(k, re_plane_coeffs(:,i)', poly_order); + re_plane_coeffs(:,i) = polyval(p, k); +end +% fix coefficients so M(1:3) is a unit vector +for i = 1 : n_sheets + en = re_plane_coeffs(i,1:3); + d = re_plane_coeffs(i,4); + re_plane_coeffs(i,:) = [en d] / norm(en); +end + +sheet_spacing = polyfit(k, re_plane_coeffs(:,4)', 1); +sheet_spacing = abs(sheet_spacing(1)); + +%% convert camera calibration to reconstruction coordinate system +% incorporates shift of origin and rotation + +for cam_idx = 1 : n_cameras + gl_pinhole_model = camera_setup(cam_idx).calib; + gl_G = gl_pinhole_model.G; + gl_R = gl_pinhole_model.R; + gl_xc = gl_pinhole_model.xc; + re_R = gl_R * re_to_gl; + re_xc = gl_to_re * (gl_xc(:) - gl_X0(:)); + re_P = gl_G * re_R * [eye(3) -re_xc]; + + re_pinhole_model = gl_pinhole_model; + re_pinhole_model.P = re_P; + re_pinhole_model.R = re_R; + re_pinhole_model.xc = re_xc; + camera_setup(cam_idx).calib = re_pinhole_model; + camera_setup(cam_idx).gl_calib= gl_pinhole_model; +end + +%% calculate axes for reconstruction volume +% with knowledge of each camera's pinhole model +% and the laser sheet setup +% we can define axes for the reconstruction volume + +X_lim_mat = zeros(n_cameras, 2); +Y_lim_mat = zeros(n_cameras, 2); +Z_lim_mat = zeros(n_cameras, 2); + +ilim = [im_i(3) im_i(end-2)]; +jlim = [im_j(3) im_j(end-2)]; + +% report on what we are about to do +fprintf('------------------------------------------------------------------\n'); +fprintf('CAMERA CALIBRATION\n'); +fprintf('Number of cameras: %d\n', n_cameras); + +for cam_idx = 1 : n_cameras + %% find the extent of the visible volume + % the largest possible volume, bound by first and last laser sheets + % which projects onto the space between ilim(1) < i < ilim(2) + % and jlim(1) < j < lim(2) + M1 = re_plane_coeffs(1, :); + Mend = re_plane_coeffs(end, :); + camera_calib = camera_setup(cam_idx).calib; + P = camera_calib.P; + [XLim,YLim,ZLim] = find_largest_volume(P, ilim, jlim, M1, Mend); + % add a little extra bit of space in reconstruction around edges in z + % direction + ZLim = ZLim + sheet_spacing*[-2 2]'; + % save + X_lim_mat(cam_idx,:)= XLim; + Y_lim_mat(cam_idx,:)= YLim; + Z_lim_mat(cam_idx,:)= ZLim; + + %% report for this camera + fprintf('Camera %d, ID = %d\n', cam_idx, camera_setup(cam_idx).id); + fprintf('X limits: %0.1f to %0.1f\n', XLim(1), XLim(2)); + fprintf('Y limits: %0.1f to %0.1f\n', YLim(1), YLim(2)); + fprintf('Z limits: %0.1f to %0.1f\n', ZLim(1), ZLim(2)); + fprintf('Camera position: %0.2f %0.2f %0.2f\n', camera_calib.xc); + fprintf('Internal camera parameters:\n'); + disp(camera_calib.G); +end + +%% define a coordinate system visible to all cameras +X_lim = [max(X_lim_mat(:,1)) min(X_lim_mat(:,2))]; +Y_lim = [max(Y_lim_mat(:,1)) min(Y_lim_mat(:,2))]; +Z_lim = [max(Z_lim_mat(:,1)) min(Z_lim_mat(:,2))]; +% fix resolution +re_dY = (Y_lim(2) - Y_lim(1)) / (jlim(2) - jlim(1)); +re_dX = re_dY; +re_dZ = re_dX; +re_X = X_lim(1) : re_dX : X_lim(2); +re_Y = Y_lim(1) : re_dY : Y_lim(2); +re_Z = Z_lim(1) : re_dZ : Z_lim(2); + +fprintf('------------------------------------------------------------------\n'); +fprintf('Combined calibration:\n'); +fprintf('X limits: %0.1f to %0.1f (%d vox)\n', X_lim(1), X_lim(2), length(re_X)); +fprintf('Y limits: %0.1f to %0.1f (%d vox)\n', Y_lim(1), Y_lim(2), length(re_Y)); +fprintf('Z limits %0.1f to %0.1f (%d vox)\n', Z_lim(1), Z_lim(2), length(re_Z)); +fprintf('voxel size: %0.2f x %0.2f x %0.2f\n', re_dX, re_dY, re_dZ); + +%% save to file + +fprintf('------------------------------------------------------------------\n'); +fprintf('Multiple camera calibration complete\n'); +fprintf('Saving to file ... '); + +fstruct = struct('n_sheets', n_sheets, ... + 'gl_sheet_normal', gl_eZ_mat, 're_sheet_normal', re_eZ_mat, ... + 'gl_sheet_pos', gl_xs_mat, 're_sheet_pos', re_xs_mat, ... + 're_plane_coeffs', re_plane_coeffs, ... + 'sheet_width', w_vec, ... + 'gl_X0', gl_X0, ... + 'gl_to_re', gl_to_re, ... + 'i', im_i, 'j', im_j, ... + 'X', re_X, 'Y', re_Y, 'Z', re_Z, ... + 'n_cameras', n_cameras, ... + 'camera_calibration', [camera_setup.calib], ... + 'camera_id', [camera_setup.id]); +save(setup_file, '-struct', 'fstruct'); +fprintf('done\n'); + +%% plot residuals of fit for sheet coefficients +figure(1); +clf; +plot(k, re_plane_coeffs_o(:,1) - re_plane_coeffs(:,1), 'r.', ... + k, re_plane_coeffs_o(:,2) - re_plane_coeffs(:,2), 'g.', ... + k, re_plane_coeffs_o(:,3) - re_plane_coeffs(:,3), 'b.'); +xlabel('sheet index'); +ylabel('coefficient residual (unit vectors)'); +title('residuals for polynomial fit of laser sheet normal'); + +figure(2); +plot(k, (re_plane_coeffs_o(:,4) - re_plane_coeffs(:,4))/re_dX, 'k.', ... + [0 n_sheets-1], [1 1]*+0.01 * sheet_spacing / re_dX, 'k--', ... + [0 n_sheets-1], [1 1]*-0.01 * sheet_spacing / re_dX, 'k--'); +ylim(sheet_spacing * [-1 1]*0.1 / re_dX); +xlabel('sheet index'); +ylabel('coefficient residual (normal distance) / vx'); +title('residuals for polynomial fit of laser sheet position'); + +%% plot projection of reconstruction volume in each view + +figure(9); +clf; +for cam_idx = 1 : n_cameras + %% create figure + subplot(1,n_cameras,cam_idx); + cla; + + %% plot corners + camera_calib = camera_setup(cam_idx).calib; + corners_xyz = [X_lim([1 1 1 1 2 2 2 2]); + Y_lim([1 1 2 2 1 1 2 2]); + Z_lim([1 2 1 2 1 2 1 2])]; + corners_ij = pinhole_model(corners_xyz, camera_calib); + plot( corners_ij(1,[1 2 3 4 1]), corners_ij(2, [1 2 3 4 1]), 'k.-', ... + corners_ij(1,[5 6 7 8 5]), corners_ij(2, [5 6 7 8 5]), 'k.-', ... + corners_ij(1,[1 3 7 5 1]), corners_ij(2, [1 3 7 5 1]), 'k.-', ... + corners_ij(1,[2 4 8 6 2]), corners_ij(2, [2 4 8 6 2]), 'k.-'); + % plot without pinhole model + hold on + camera_calib.kr = [0 0]; + camera_calib.ic = [512 512]; + corners_ij = pinhole_model(corners_xyz, camera_calib); + plot( corners_ij(1,[1 2 3 4 1]), corners_ij(2, [1 2 3 4 1]), 'r.-', ... + corners_ij(1,[5 6 7 8 5]), corners_ij(2, [5 6 7 8 5]), 'r.-', ... + corners_ij(1,[1 3 7 5 1]), corners_ij(2, [1 3 7 5 1]), 'r.-', ... + corners_ij(1,[2 4 8 6 2]), corners_ij(2, [2 4 8 6 2]), 'r.-'); + %% make pretty + axis equal tight xy; + xlim(im_i([1 end])); + ylim(im_j([1 end])); + xlabel('i'); + ylabel('j'); + title(sprintf('Camera %d, ID %d', cam_idx, camera_setup(cam_idx).id)); +end + + +%% plot in reconstruction coordinate system + +figure(10); +clf; +hold off; + +% plot planes +M1 = re_plane_coeffs(1,:); +Mend = re_plane_coeffs(end,:); +x_corners = [X_lim(1) X_lim(1) X_lim(2) X_lim(2) X_lim(1)]; +y_corners = [Y_lim(1) Y_lim(2) Y_lim(2) Y_lim(1) Y_lim(1)]; +% first plane +z_corners = -(M1(1)*x_corners + M1(2)*y_corners + M1(4)) / M1(3); +P1 = patch(x_corners, y_corners, z_corners, 'g'); + +% last plane +z_corners = -(Mend(1)*x_corners + Mend(2)*y_corners + Mend(4)) / Mend(3); +Pend = patch(x_corners, y_corners, z_corners, 'g'); +set([P1 Pend], 'FaceAlpha', 0.5, 'edgealpha', 1); + +hold on; + +% plot viewing frustum of each camera +ilim = [im_i(1) im_i(end)]; +jlim = [im_j(1) im_j(end)]; +i_corners = [ilim(1) ilim(1) ilim(2) ilim(2)]; +j_corners = [jlim(1) jlim(2) jlim(2) jlim(1)]; +ij_corners = [i_corners; j_corners]; + +str_colour = {'r','k','b'}; + +for cam_idx = 1 : n_cameras + calib = camera_setup(cam_idx).calib; + Gc = calib.G; + Rc = calib.R; + xc = calib.xc; + ic = calib.ic; + kr = calib.kr; + + % convert dewarped image coordinates to pinhole image coordinates + ij_undist= inverse_radial_distortion(ij_corners, ic, kr); + for corner = 1 : 4 + ytilde= [ij_undist(:, corner); 1]; + e_ray = Rc^-1 * Gc^-1 * ytilde; + e_ray = e_ray / norm(e_ray); + + x_far = xc + e_ray * norm(xc) * 1.2; + x_near= xc; + + plot3([x_near(1) x_far(1)], ... + [x_near(2) x_far(2)], ... + [x_near(3) x_far(3)], '-', 'Color', str_colour{cam_idx}); + end +end + +axis equal;% tight xy; +xlim([-100 100]); +ylim([-100 100]); +zlim([-100 100]); \ No newline at end of file diff --git a/calibration/combine_markers.m b/calibration/combine_markers.m new file mode 100644 index 0000000000000000000000000000000000000000..fa31d1ce34e7a15abe5c27a68c03943f5790a806 --- /dev/null +++ b/calibration/combine_markers.m @@ -0,0 +1,316 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% combine_markers.m +% +% combine calibration images of the same markers from a single camera +% +addpath([pwd '/../../piv2d/']); +addpath([pwd '/pinhole/']); + +%% input parameters + +calibration_dir = '/nfs/data/lawson/160715/calib/'; +calib_infile_pat = 'CAM2-%02d.mat'; +calib_outfile_pat = 'C-CAM2-%02d.mat'; +calib_file_range = 1 : 16; +peak_mag_threshold= 0.5; + +marker_size = [0.25 0.25]; % mm, interpreted as half-size +marker_min_bright = 50; % minimum brightness of marker image + +%% load calibration files + +x_lim = [inf -inf]; +y_lim = [inf -inf]; +n_calibrations = length(calib_file_range); + +fprintf('------------------------------------------------------------------\n'); +fprintf('Loading %d calibration files ... ', n_calibrations); + +for file_idx = calib_file_range + file_name = sprintf([calibration_dir '/' calib_infile_pat], file_idx); + fstruct = importdata(file_name); + + x_lim = [min([fstruct.x_lim(1) x_lim(1)]) max([fstruct.x_lim(2) x_lim(2)])]; + y_lim = [min([fstruct.y_lim(1) y_lim(1)]) max([fstruct.y_lim(2) y_lim(2)])]; +end + +grid_dx = fstruct.grid_dx; +grid_dy = fstruct.grid_dy; +Nx = size(fstruct.source_image, 1); +Ny = size(fstruct.source_image, 2); + +fprintf('done\n'); + +%% resample images onto common grid + +fprintf('Resampling images onto common grid ... '); + +x = linspace(x_lim(1), x_lim(2), Nx); +y = linspace(y_lim(1), y_lim(2), Ny); +dx = x(2) - x(1); +dy = y(2) - y(1); +[xmat, ymat] = ndgrid(x, y); + +dewarped_image_mat= zeros(n_calibrations, Nx, Ny); +for calib_idx = 1 : n_calibrations + file_idx = calib_file_range(calib_idx); + file_name = sprintf([calibration_dir '/' calib_infile_pat], file_idx); + fstruct = importdata(file_name); + + source_image = fstruct.source_image; + xytilde = [xmat(:) ymat(:) ones(Nx*Ny,1)]'; + ijtilde = fstruct.P * xytilde; + imat = reshape(ijtilde(1,:) ./ ijtilde(3,:), [Nx Ny]); + jmat = reshape(ijtilde(2,:) ./ ijtilde(3,:), [Nx Ny]); + i_axis = fstruct.i_axis; + j_axis = fstruct.j_axis; + dewarped_image = interp2(i_axis, j_axis, source_image', imat, jmat, 'linear', nan); + + dewarped_image_mat(calib_idx, :, :) = dewarped_image; +end + +fprintf('done\n'); + +%% identify which markers are in which images + +x_lim_grid = fix(x_lim/grid_dx) * grid_dx; +y_lim_grid = fix(y_lim/grid_dy) * grid_dy; +x_grid = x_lim_grid(1) : grid_dx : x_lim_grid(2); +y_grid = y_lim_grid(1) : grid_dy : y_lim_grid(2); +Nx_grid = length(x_grid); +Ny_grid = length(y_grid); + +[x_grid_mat, y_grid_mat] = ndgrid(x_grid, y_grid); +n_shared_views = zeros(Nx_grid, Ny_grid); +b_marker_visible = false(Nx_grid, Ny_grid, n_calibrations); + +fprintf('Searching for common markers ... '); + +for ix = 1 : Nx_grid + for iy = 1 : Ny_grid + %% find which images contain this marker + % extrapolated values in images are nans + % so markers we cannot see have nans near them + marker_is_visible = false(1, n_calibrations); + xrange = abs(x - x_grid(ix)) < marker_size(1); + yrange = abs(y - y_grid(iy)) < marker_size(2); + for calib_idx = 1 : n_calibrations + dewarped_image = dewarped_image_mat(calib_idx, :, :); + dewarped_image = squeeze(dewarped_image); + marker_image = dewarped_image(xrange,yrange); + marker_brightness = mean(marker_image(:)); + marker_is_visible(calib_idx) = ... + all(~isnan(marker_image(:))) && marker_brightness > marker_min_bright; + end + n_shared_views(ix,iy) = sum(marker_is_visible); + b_marker_visible(ix, iy, :) = marker_is_visible; + end +end + +fprintf('done\n'); + +%% report on what markers are common +b_all_common = n_shared_views == n_calibrations; +b_some_common = n_shared_views >= 2; +n_all_common = sum(b_all_common(:)); +n_some_common = sum(b_some_common(:)); + +fprintf('Found %d markers common across all views\n', n_all_common); +fprintf('Found %d markers in at least 2 views\n', n_some_common); + +sum_of_all_images = mean(dewarped_image_mat, 1); +sum_of_all_images = squeeze(sum_of_all_images); + +figure(1); +hold off; +imagesc(x, y, sum_of_all_images'); +hold on; +plot(x_grid_mat(b_all_common ), y_grid_mat(b_all_common ), 'go'); +plot(x_grid_mat(b_some_common), y_grid_mat(b_some_common), 'rx'); +axis equal tight xy; +colormap gray; +xlim(x_lim); +ylim(y_lim); +title('Average of all images'); +xlabel('x / mm'); +ylabel('y / mm'); + +sInput = input('Do you want to check markers? (y): ', 's'); +if strcmp(sInput, 'y') + for calib_idx = 1 : n_calibrations + dewarped_image = squeeze(dewarped_image_mat(calib_idx, :, :)); + marker_visible = squeeze(b_marker_visible(:,:,calib_idx)); + + figure(1); + hold off; + imagesc(x, y, dewarped_image'); + axis equal tight xy; + colormap gray; + xlim(x_lim); + ylim(y_lim); + hold on; + plot(x_grid_mat(marker_visible), y_grid_mat(marker_visible), 'go'); + + fprintf('Image %d of %d\n', calib_idx, n_calibrations); + pause(); + end +end + +%% cross-correlate to find offset + +fprintf('Finding optimal shift of markers ... '); + +marker_shift_x = zeros(Nx_grid, Ny_grid, n_calibrations); +marker_shift_y = zeros(Nx_grid, Ny_grid, n_calibrations); + +for ix = 1 : Nx_grid + for iy = 1 : Ny_grid + %% can't make a correction if there are <2 views + if n_shared_views(ix, iy) < 2 + continue; + end + + %% get image of marker in first view + i_shared_views = find(b_marker_visible(ix, iy, :)); + xrange = abs(x - x_grid(ix)) < marker_size(1); + yrange = abs(y - y_grid(iy)) < marker_size(2); + first_view_idx = i_shared_views(1); + first_view_img = dewarped_image_mat(first_view_idx, xrange, yrange); + first_view_img = squeeze(first_view_img); + first_view_img = first_view_img - mean(first_view_img(:)); + first_view_e = sum(first_view_img(:).^2); + + %% cross correlate to find offset + for iv = 2 : n_shared_views(ix, iy) + other_view_idx = i_shared_views(iv); + other_view_img = dewarped_image_mat(other_view_idx, xrange, yrange); + other_view_img = squeeze(other_view_img); + other_view_img = other_view_img - mean(other_view_img(:)); + other_view_e = sum(other_view_img(:).^2); + + xx = xcorr2(first_view_img, other_view_img); + xx = xx / sqrt(first_view_e * other_view_e); + peak_mag = max(xx(:)); + peak_loc = PIV_2d_peaklocate(xx); + + % check for sufficient correlation + if peak_mag < peak_mag_threshold + fprintf('bad match marker (%0.1f,%0.2f) in view %d\n', x_grid(ix), y_grid(iy), other_view_idx); + marker_shift= [0 0]; + b_marker_visible(ix, iy, iv) = false; + else + marker_shift= (peak_loc(:)' - size(first_view_img)) .* [dx dy]; + end + + marker_shift_x(ix, iy, other_view_idx) = -marker_shift(1); + marker_shift_y(ix, iy, other_view_idx) = -marker_shift(2); + end + + % update which views share an image of this marker + i_shared_views = find(b_marker_visible(ix, iy, :)); + + %% average out shifts so no image is preferred + average_shift_x = mean(marker_shift_x(ix, iy, i_shared_views)); + average_shift_y = mean(marker_shift_y(ix, iy, i_shared_views)); + marker_shift_x(ix, iy, i_shared_views) = marker_shift_x(ix, iy, i_shared_views) - average_shift_x; + marker_shift_y(ix, iy, i_shared_views) = marker_shift_y(ix, iy, i_shared_views) - average_shift_y; + end +end + +fprintf('done\n'); + +%% re-calibrate and re-dewarp + +fprintf('Recomputing calibration ... \n'); + +rewarped_image_mat = dewarped_image_mat; + +for calib_idx = 1 : n_calibrations + %% report + fprintf(' - calibration %2d of %2d ... ', calib_idx, n_calibrations); + + %% find markers on this image + b_visible_markers = b_marker_visible(:, :, calib_idx); + marker_x = x_grid_mat + marker_shift_x(:, :, calib_idx); + marker_y = y_grid_mat + marker_shift_y(:, :, calib_idx); + marker_x_old = marker_x(b_visible_markers); + marker_y_old = marker_y(b_visible_markers); + marker_x_new = x_grid_mat(b_visible_markers); + marker_y_new = y_grid_mat(b_visible_markers); + n_visible_markers = sum(b_visible_markers(:)); + + %% get updated i/j coordinates of marker positions + file_idx = calib_file_range(calib_idx); + file_name = sprintf([calibration_dir '/' calib_infile_pat], file_idx); + fstruct = importdata(file_name); + [marker_i,marker_j] = pinholemap_2d(marker_x_old(:), marker_y_old(:), fstruct.P); + + %% recalibrate + marker_ij_tilde = [marker_i(:) marker_j(:) ones(n_visible_markers,1)]'; + marker_xy_tilde = [marker_x_new(:) marker_y_new(:) ones(n_visible_markers,1)]'; + P2D_new = dlt33(marker_ij_tilde, marker_xy_tilde); + + %% re-dewarp image + im = fstruct.source_image; + i_axis = fstruct.i_axis; + j_axis = fstruct.j_axis; + + [imat,jmat] = pinholemap_2d(xmat, ymat, P2D_new); + rewarped_img = interp2(i_axis, j_axis, im', imat, jmat, 'linear', nan); + rewarped_image_mat(calib_idx, :, :) = rewarped_img; + + %% save + fstruct.P = P2D_new; + fstruct.ij_markers = [marker_i(:) marker_j(:)]'; + fstruct.xy_markers = [marker_x_new(:) marker_y_new(:)]'; + fstruct.x_axis = x; + fstruct.y_axis = y; + fstruct.dewarped_image = rewarped_img; + + output_file = sprintf([calibration_dir '/' calib_outfile_pat], file_idx); + save(output_file, '-struct', 'fstruct'); + + fprintf('done\n'); +end + +fprintf('Done\n'); + +%% compare dewarped and re-dewarped images + +for img_idx = 1 : n_calibrations + discrepancy_x = squeeze( marker_shift_x(:,:, img_idx) ); + discrepancy_y = squeeze( marker_shift_y(:,:, img_idx) ); + marker_x = x_grid_mat + discrepancy_x; + marker_y = y_grid_mat + discrepancy_y; + dewarped_img = squeeze(dewarped_image_mat(img_idx, :, :)); + rewarped_img = squeeze(rewarped_image_mat(img_idx, :, :)); + + figure(2); + %{ + subplot(1, 2, 1); + hold off; + imagesc(x, y, dewarped_img'); + hold on; + plot(marker_x(:), marker_y(:), 'rx'); + axis xy equal tight; + grid on; + xlim(x_lim); + ylim(y_lim); + %xlim([-25 25]); + %ylim([-25 25]); + title(sprintf('dewarped %d', img_idx)); + + + subplot(1, 2, 2); + %} + hold off; + imagesc(x, y, rewarped_img'); + colormap jet; + grid on; + axis xy equal tight; + xlim(x_lim); + ylim(y_lim); + title(sprintf('rewarped %d', img_idx)); + + pause(); +end diff --git a/calibration/cubicfit_2d.m b/calibration/cubicfit_2d.m new file mode 100644 index 0000000000000000000000000000000000000000..bd24206443094e13607a1b7c4918f034ab7dd87c --- /dev/null +++ b/calibration/cubicfit_2d.m @@ -0,0 +1,64 @@ +function C = cubicfit_2d(i, j, X, Niter) + % fits coefficients C to observations of X(i,j) to create a model of X + % according to + % X* = C(1) + C(2)*i + C(3)*i.^2 + C(4)*i.^3 + ... + % + C(5)*j + C(6)*j.^2 + C(7)*j.^3 + ... + % + C(8)*i.*j + C(9)*i.^2.*j + C(10)*i.*j.^2; + %{ + ii = i.^2; + iii = i.^3; + jj = j.^2; + jjj = j.^3; + ij = i.*j; + iij = i.^2.*i; + ijj = i.*j.^2; + Np = numel(i); + %} + myfun = @(X,xdata) cubicmap_2d(xdata(:,1), xdata(:,2), X); + C0 = zeros(1,10); + opts = optimset('MaxFunEvals', 100000, 'MaxIter', 5000, 'Display', 'off'); + [C,~,res] = lsqcurvefit(myfun, C0, [i(:) j(:)], X(:), [], [], opts); + if nargin > 3 + for iter = 2 : Niter + % cut out the chaff + thresh = prctile(res, 97); + ok = res < thresh; + i = i(ok); + j = j(ok); + X = X(ok); + % repeat + [C,~,res]= lsqcurvefit(myfun, C0, [i(:) j(:)], X(:), [], [], opts); + end + end + + + %{ + A = [ones(Np,1) i(:) ii(:) iii(:) j(:) jj(:) jjj(:) ij(:) iij(:) ijj(:)]; + b = X(:); + [C,~,res] = lsqlin(A, b, [], []); + + % iteratively remove outliers + if nargin > 3 + for iter = 2 : Niter + % cut out the chaff + thresh = prctile(res, 97); + ok = res < thresh; + i = i(ok); + ii = ii(ok); + iii = iii(ok); + j = j(ok); + jj = jj(ok); + jjj = jjj(ok); + ij = ij(ok); + iij = iij(ok); + ijj = ijj(ok); + X = X(ok); + Np = numel(i); + % repeat + A = [ones(Np,1) i(:) ii(:) iii(:) j(:) jj(:) jjj(:) ij(:) iij(:) ijj(:)]; + b = X(:); + [C,~,res]= lsqlin(A, b, [], []); + end + end + %} +end \ No newline at end of file diff --git a/calibration/cubicmap_2d.m b/calibration/cubicmap_2d.m new file mode 100644 index 0000000000000000000000000000000000000000..77edbde8930e1dbdbee846015b4bea5d4f40bf9d --- /dev/null +++ b/calibration/cubicmap_2d.m @@ -0,0 +1,7 @@ +function X = cubicmap_2d(i, j, C) + % maps [i,j] onto X = X(i,j) using coefficients provided in C + % coefficients derived from cubicfit_2d + X = C(1) + C(2)*i + C(3)*i.^2 + C(4)*i.^3 + ... + + C(5)*j + C(6)*j.^2 + C(7)*j.^3 + ... + + C(8)*i.*j + C(9)*i.^2.*j + C(10)*i.*j.^2; +end \ No newline at end of file diff --git a/calibration/dewarp_2d.m b/calibration/dewarp_2d.m new file mode 100644 index 0000000000000000000000000000000000000000..184366fe54b25a87506292516194dcd34fc7a92e --- /dev/null +++ b/calibration/dewarp_2d.m @@ -0,0 +1,23 @@ +function dewarped_img = dewarp_2d(raw_img, xy_to_i, xy_to_j, x, y) + % reinterpolates raw_img onto grid [x,y] from [i,j] using the dewarping coefficients + % xy_to_i and xy_to_j + + Nx = length(x); + Ny = length(y); + [xmat, ymat] = ndgrid(x, y); + imat = cubicmap_2d(xmat, ymat, xy_to_i); + jmat = cubicmap_2d(xmat, ymat, xy_to_j); + + ivec = 1 : size(raw_img, 1); + jvec = 1 : size(raw_img, 2); + dewarped_img = interp2(ivec, jvec, double(raw_img'), imat, jmat, 'linear', 0); + + % lanczos interpolation is faster than matlab's interp2 and better + % quality. kernel size is specified by ksize. 18 is good. + %{ + ksize = 10; + xi = [imat(:)-1, jmat(:)-1]'; + dewarped_img = interp2lanczos(single(raw_img), single(xi), ksize); + dewarped_img = reshape(dewarped_img, Nx, Ny); + %} +end \ No newline at end of file diff --git a/calibration/dewarp_pinhole_2d.m b/calibration/dewarp_pinhole_2d.m new file mode 100644 index 0000000000000000000000000000000000000000..f521baa05ebd824d7db396b454b098321b3beed6 --- /dev/null +++ b/calibration/dewarp_pinhole_2d.m @@ -0,0 +1,13 @@ +function im_uv = dewarp_pinhole_2d(im, P2D, u_axis, v_axis) + Nu = length(u_axis); + Nv = length(v_axis); + [umat, vmat] = ndgrid(u_axis, v_axis); + uv_tilde = [umat(:) vmat(:) ones(Nu*Nv,1)]'; + ij_tilde = P2D * uv_tilde; + lambda = ij_tilde(3, :); + imat = reshape(ij_tilde(1,:) ./ lambda, [Nu Nv]); + jmat = reshape(ij_tilde(2,:) ./ lambda, [Nu Nv]); + i_axis = 1 : size(im, 1); + j_axis = 1 : size(im, 2); + im_uv = interp2(i_axis, j_axis, im', imat, jmat, 'linear'); +end \ No newline at end of file diff --git a/calibration/distortion_test.m b/calibration/distortion_test.m new file mode 100644 index 0000000000000000000000000000000000000000..af101713f9992cfe9810492e5a1ca9a03e7f04ba --- /dev/null +++ b/calibration/distortion_test.m @@ -0,0 +1,158 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% fit calibration data obtained from pinhole camera model fit +% to a higher order camera model +% +% we follow Machioane et al. 2016 to extract a transfer function that +% maps image space coordinates to rays through object space +% +% 1) obtain 2D transfer function for each calibration plane +% 2) trace rays back for each pixel, based on intersection +% with each calibration plane +% 3) fit a transfer function to map rays to + +addpath([pwd '/pinhole']); +addpath([pwd '/poly']); + +%% input parameters + +calib_dir = '/nfs/data/lawson/160715/calib/'; +cam_calib_file_pat = [calib_dir '/CAM%d-%02d.mat']; +cam_model_file = [calib_dir '/camera-model-original.mat']; + +cam_index_range = [1 2 3]; +cam_view_range = 1 : 16; +n_views = length(cam_view_range); +n_cameras = length(cam_index_range); +n_iterations = 2; + +DO_DEBUG_PLOT = false; + +%% load model +setup = importdata(camera_model_file); + +% camera calibration +n_cameras = setup.n_cameras; +n_views = setup.n_views; + +% plate pose +gl_plate_pos = setup.gl_plate_pos; +pl_to_gl_mat = repmat(setup.pl_to_gl, [1 1 n_views]); +camera_model = setup.camera; +gl_to_re = eye(3); +gl_origin = setup.gl_origin; +r_sphere = min(setup.r_sphere); +n_views = setup.n_views; + +%% obtain 2D transfer function for each plane +fprintf('Fitting 2D transfer function for each plate in each camera\n'); +fprintf('This gives best case scenario for how advanced model will reduce error\n'); + +quiver_scale = 10; +for c = 1 : n_cameras + %% initialise + camera = setup.camera(c); + i_axis = camera.i_axis; + j_axis = camera.j_axis; + ij_res_poly = []; + ij_res_pin = []; + + gl_mark_ary = []; + ij_mark_ary = []; + sh_mark_ary = []; + + %% loop over views + for v = 1 : n_views + %% get plate, image and world coordinates of markers + pl_to_gl = pl_to_gl_mat(:,:,v); + gl_pos = gl_plate_pos(:,v); + uv_mark = camera.view(v).uv_markers; + n_mark = size(uv_mark, 2); + gl_mark = bsxfun(@plus, gl_pos, pl_to_gl*[uv_mark; zeros(1,n_mark)]); + ij_mark = camera.view(v).ij_markers; + + %% get sphere coordinates of markers + R = camera.calib.R; + xc = camera.calib.xc; + eu_mark = R * bsxfun(@minus, gl_mark, xc); + eu_len = sqrt(sum(eu_mark.^2,1)); + eu_mark = bsxfun(@rdivide, eu_mark, eu_len); + theta_mark = acos(eu_mark(3,:)); + phi_mark = atan2(eu_mark(2,:), eu_mark(1,:)); + + sh_mark = [theta_mark; phi_mark]; + + ij_mark_ary = [ij_mark_ary, ij_mark]; + gl_mark_ary = [gl_mark_ary, gl_mark]; + sh_mark_ary = [sh_mark_ary, sh_mark]; + + %% create transfer function + P_i2p = polyfit2d3(ij_mark, uv_mark); + P_p2i = polyfit2d3(uv_mark, ij_mark); + camera.view(v).P_i2p = P_i2p; + camera.view(v).P_p2i = P_p2i; + + %% compare transfer function to pinhole model + uv_res = polymap2d3(P_i2p, ij_mark) - uv_mark; + ij_res_poly_view = polymap2d3(P_p2i, uv_mark) - ij_mark; + ij_res_pin_view = pinhole_model(gl_mark, camera.calib, true) - ij_mark; + + ij_res_poly = [ij_res_poly, ij_res_poly_view]; + ij_res_pin = [ij_res_pin, ij_res_pin_view]; + + %% debug plot + if DO_DEBUG_PLOT + %% residuals + figure(1); + clf; + quiversc(ij_mark(1,:), ij_mark(2,:), ... + ij_res_poly_view(1,:), ij_res_poly_view(2,:), quiver_scale, 'k-'); + hold on; + quiversc(ij_mark(1,:), ij_mark(2,:), ... + ij_res_pin_view(1,:), ij_res_pin_view(2,:), quiver_scale, 'r-'); + axis equal xy; + xlim(i_axis([1 end])); + ylim(j_axis([1 end])); + %% + pause(); + end + end + + %% polynomial map, undistorted image -> image + ij_proj_ary = pinhole_model(gl_mark_ary, camera.calib, false); + ij_res_ary = ij_proj_ary - ij_mark_ary; + P_u2d = polyfit2d4(ij_proj_ary, ij_mark_ary); + P_d2u = polyfit2d4(ij_mark_ary, ij_proj_ary); + + ij_dist_ary = polymap2d4(P_u2d, ij_proj_ary); + ij_res_pp = ij_dist_ary - ij_mark_ary; + + %% polynomial map, spherical -> image + P_s2i = polyfit2d3(sh_mark_ary, ij_mark_ary); + P_i2s = polyfit2d3(ij_mark_ary, sh_mark_ary); + camera.calib.P_s2i = P_s2i; + camera.calib.P_i2s = P_i2s; + + %% mapping + figure(2); + quiversc(ij_mark_ary(1,:), ij_mark_ary(2,:), ... + ij_res_pp(1,:), ij_res_pp(2,:), 2, 'k-'); + axis equal tight xy; + xlim(i_axis([1 end])); + ylim(j_axis([1 end])); + + %% save + setup.camera(c) = camera; + + %% report differences + ij_rms_poly = rms(ij_res_poly, 2); + ij_rms_pin = rms(ij_res_pin, 2); + ij_rms_pp = rms(ij_res_pp, 2); + ij_max_poly = max(abs(ij_res_poly), [], 2); + ij_max_pin = max(abs(ij_res_pin ), [], 2); + ij_max_pp = max(abs(ij_res_pp ), [], 2); + fprintf('Residuals in camera %d\n', c); + fprintf(' polynomial: %0.3f %0.3f px rms %0.3f %0.3f max\n', ij_rms_poly, ij_max_poly); + fprintf(' pinhole+dist: %0.3f %0.3f px rms %0.3f %0.3f max\n', ij_rms_pp , ij_max_pp); + fprintf(' pinhole+rad: %0.3f %0.3f px rms %0.3f %0.3f max\n', ij_rms_pin , ij_max_pin); +end + diff --git a/calibration/dlt/dlt_maths.m b/calibration/dlt/dlt_maths.m new file mode 100644 index 0000000000000000000000000000000000000000..8d89c2773ed66f693ade85f98f62bec0bf069c0f --- /dev/null +++ b/calibration/dlt/dlt_maths.m @@ -0,0 +1,59 @@ +%% work out maths for parallel DLT +% we have a problem where +% y1_k ~ T1 * x_k +% y2_k ~ T2 * x_k +% but we don't know x_k, or T1 or T2 +% +% so this can be written as +% a_k * y1_k = T1 * x_k +% b_k * y2_k = T2 * x_k +% for k = 1 .. N equations +% +% if we choose an antisymmetric matrix Hi, then +% a_k * y1_k' * Hi * y2_k = y1_k' * Hi * T1 * x_k +% = 0 +% b_k * y2_k' * Hj * y2_k = y2_k' * Hj * T2 * x_k +% = 0 + +%% set up T1 and T2 + +% we need two random orthogonal matrices +Omega1 = randortho(3); +Omega2 = randortho(3); +% and random displacements xc1 and xc2 +xc1 = randn(3,1) * 100 + [0 1000 0]'; +xc2 = randn(3,1) * 100 + [1000 0 0]'; +T1 = Omega1 * [eye(3) -xc1]; +T2 = Omega2 * [eye(3) -xc2]; +T = [T1; T2]; + +%% set up observations of xk, y1k and y2k + +N = 1000; +xk = randn(3,N) * 100; +xk_tilde = [xk; ones(1,N)]; +y1k = T1 * xk; +y2k = T2 * xk; +zk = [y1k; y2k]; + +%% generate initial guess of xk, Omega1/2, xc1 and xc2 + +xk_guess = xk; +SNR = 100; +for k = 1 : N + xk_guess(:,k) = xk(:,k) + randn(3,1)*norm(xk(:,k))/SNR; +end + +omega1 = vrrotmat2vec(Omega1); +omega2 = vrrotmat2vec(Omega2); +omega1 = omega1(1:3) * omega1(4); +omega2 = omega2(1:3) * omega2(4); +omega1_guess= omega1 + randn(3,1) * norm(omega1) / SNR; +omega2_guess= omega2 + randn(3,1) * norm(omega2) / SNR; +xc1_guess = xc1 + norm(xc1)*randn(3,1)/SNR; +xc2_guess = xc2 + norm(xc2)*randn(3,1)/SNR; + + + +%% generate initial guess for model + diff --git a/calibration/dlt/dlt_model.m b/calibration/dlt/dlt_model.m new file mode 100644 index 0000000000000000000000000000000000000000..21053d0c8b90f4f4193932f3218921c262951fa4 --- /dev/null +++ b/calibration/dlt/dlt_model.m @@ -0,0 +1,40 @@ +function residuals = dlt_model(params, y1k, y2k, xc1, xc2) + %% extract model parameters + omega1 = params(1:3); + omega2 = params(4:6); + delta = [ 0 0 0]';%params(7:9); + xc1_prime= xc1(:) + delta(:); + xc2_prime= xc2(:) + delta(:); + + % get rotation matrices + theta1 = norm(omega1); + theta2 = norm(omega2); + omega1 = [omega1/theta1; theta1]; + omega2 = [omega2/theta2; theta2]; + if theta1 < 1E-6; omega1 = [1 0 0 0]; end + if theta2 < 1E-6; omega2 = [1 0 0 0]; end + Omega1 = vrrotvec2mat(omega1); + Omega2 = vrrotvec2mat(omega2); + + % get T1 and T2 + T1 = Omega1 * [eye(3), -xc1_prime]; + T2 = Omega2 * [eye(3), -xc2_prime]; + + % get model of xk + N = (length(params) - 6) / 3; + xk = reshape(params(7:end), [3 N]); + + %% produce model result + H1 = [0 0 0; 0 0 -1; 0 1 0]; + H2 = [0 0 1; 0 0 0; -1 0 0]; + H3 = [0 -1 0; 1 0 0; 0 0 0]; + residuals= zeros(6*N, 1); + for k = 1 : N + residuals((k-1)*6 + 1) = y1k(:,k)' * H1 * T1 * [xk(:,k); 1]; + residuals((k-1)*6 + 2) = y1k(:,k)' * H2 * T1 * [xk(:,k); 1]; + residuals((k-1)*6 + 3) = y1k(:,k)' * H3 * T1 * [xk(:,k); 1]; + residuals((k-1)*6 + 4) = y2k(:,k)' * H1 * T2 * [xk(:,k); 1]; + residuals((k-1)*6 + 5) = y2k(:,k)' * H2 * T2 * [xk(:,k); 1]; + residuals((k-1)*6 + 6) = y2k(:,k)' * H3 * T2 * [xk(:,k); 1]; + end +end \ No newline at end of file diff --git a/calibration/dlt/dlt_test.m b/calibration/dlt/dlt_test.m new file mode 100644 index 0000000000000000000000000000000000000000..dad8d0236c5a471803b3005d64e59ce212daa861 --- /dev/null +++ b/calibration/dlt/dlt_test.m @@ -0,0 +1,157 @@ +%% test solution of parallel DLT +% we have a problem where +% y1_k ~ T1 * x_k +% y2_k ~ T2 * x_k +% but we don't know x_k, or T1 or T2 +% +% so this can be written as +% a_k * y1_k = T1 * x_k +% b_k * y2_k = T2 * x_k +% for k = 1 .. N equations +% +% if we choose an antisymmetric matrix Hi, then +% a_k * y1_k' * Hi * y2_k = y1_k' * Hi * T1 * x_k +% = 0 +% b_k * y2_k' * Hj * y2_k = y2_k' * Hj * T2 * x_k +% = 0 +% +% solve using a non-linear solver +% to find x_k, T1 and T2 +clear; +clc; + +%% set up T1 and T2 + +% we need two random orthogonal matrices +Omega1 = randortho(3); +Omega2 = randortho(3); +% and random displacements xc1 and xc2 +xc1 = randn(3,1) * 100 + [0 1000 0]'; +xc2 = randn(3,1) * 100 + [1000 0 0]'; +delta = [0 0 0]';%randn(3,1) * 10; +xc1_prime = xc1 + delta; +xc2_prime = xc2 + delta; +T1 = Omega1 * [eye(3), -xc1_prime]; +T2 = Omega2 * [eye(3), -xc2_prime]; +T = [T1; T2]; + +%% set up observations of xk, y1k and y2k + +N = 100; +xk = randn(3,N) * 100; +xk_tilde = [xk; ones(1,N)]; +y1k = T1 * xk_tilde; +y2k = T2 * xk_tilde; +% apply arbitrary scaling a_k and b_k +y1k_norm = y1k; +y2k_norm = y2k; +for k = 1 : N + y1k_norm(:,k) = y1k(:,k) / norm(y1k(:,k)); + y2k_norm(:,k) = y2k(:,k) / norm(y2k(:,k)); +end + +zk = [y1k; y2k]; + +%% generate initial guess of xk, Omega1/2, xc1 and xc2 + +xk_guess = xk; +SNR = 100; +for k = 1 : N + xk_guess(:,k) = xk(:,k) + randn(3,1)*norm(xk(:,k))/SNR; +end + +omega1 = vrrotmat2vec(Omega1); +omega2 = vrrotmat2vec(Omega2); +omega1 = omega1(1:3)' * omega1(4); +omega2 = omega2(1:3)' * omega2(4); +omega1_guess= omega1 + randn(3,1) * norm(omega1) / SNR; +omega2_guess= omega2 + randn(3,1) * norm(omega2) / SNR; +delta_guess = [0 0 0]'; + +%% solve model + +X0 = [omega1_guess; + omega2_guess; + xk_guess(:)]; +fun = @(X) dlt_model(X, y1k, y2k, xc1, xc2); + +fprintf('Solving ... '); +opts = optimset('TolFun', 1E-8, 'Display', 'on'); +[X_sol,n,res,flag] = lsqnonlin(fun, X0, [], [], opts); +fprintf('done\n'); +fprintf('Solver exited with flag %d\n', flag); +fprintf('RMS residual: %0.3e\n', sqrt(mean(res.^2))); +fprintf('Norm of residuals: %0.3e\n', norm(res)); + +%% extract solution + +omega1_sol = X_sol(1:3); +omega2_sol = X_sol(4:6); +delta_sol = [0 0 0]'; +xk_sol = reshape(X_sol(7:end), [3 N]); + +Omega1_sol = vrrotvec2mat([omega1_sol/norm(omega1_sol); norm(omega1_sol)]); +Omega2_sol = vrrotvec2mat([omega2_sol/norm(omega2_sol); norm(omega2_sol)]); + +fprintf('|omega1 error| / |omega1| %0.3f %%\n', norm(omega1_sol-omega1)/norm(omega1)*100); +fprintf('|omega2 error| / |omega2| %0.3f %%\n', norm(omega2_sol-omega2)/norm(omega2)*100); +fprintf('|delta error| / |delta| %0.3f %%\n', norm(delta_sol-delta)/norm(delta)*100); + +figure(1); +hold off; +plot3(xk(1,:), xk(2,:), xk(3,:), 'b.'); +hold on; +plot3(xk_sol(1,:), xk_sol(2,:), xk_sol(3,:), 'ro'); +axis equal +title('Points in object space and their model solutions'); + +%% check back-projected points line up +T1_sol = Omega1_sol * [eye(3), -(xc1+delta_sol)]; +T2_sol = Omega2_sol * [eye(3), -(xc2+delta_sol)]; +y1k_sol = T1_sol * [xk_sol; ones(1,N)]; +y2k_sol = T2_sol * [xk_sol; ones(1,N)]; +% normalise +for k = 1 : N + y1k_sol(:,k) = y1k_sol(:,k) / norm(y1k_sol(:,k)); + y2k_sol(:,k) = y2k_sol(:,k) / norm(y2k_sol(:,k)); +end + +% compare unit vectors +figure(2); +hold off; +plot3(y1k_norm(1,:), y1k_norm(2,:), y1k_norm(3,:), 'b.'); +hold on; +plot3(y1k_sol(1,:) , y1k_sol(2,:) , y1k_sol(3,:) , 'bo'); + +plot3(y2k_norm(1,:), y2k_norm(2,:), y2k_norm(3,:), 'r.'); +plot3(y2k_sol(1,:) , y2k_sol(2,:) , y2k_sol(3,:) , 'ro'); + +xlim([-1 1]); +ylim([-1 1]); +zlim([-1 1]); +%{ + +%% check for rotation and translation + +% find best fit xk_sol = M * xk + x0 +% then find nearest orthogonal M +M1 = lsqlin([xk' ones(N,1)], xk_sol(1,:)', [], []); +M2 = lsqlin([xk' ones(N,1)], xk_sol(2,:)', [], []); +M3 = lsqlin([xk' ones(N,1)], xk_sol(3,:)', [], []); +M = [M1(1:3)'; M2(1:3)'; M3(1:3)']; +d = [M1(4) M2(4) M3(4)]'; + +figure(2); +%[U,D,V] = svd(M); +%M = U*V'; +M_xk = M * xk; +M_xk(1,:)= M_xk(1,:) + M1(4); +M_xk(2,:)= M_xk(2,:) + M2(4); +M_xk(3,:)= M_xk(3,:) + M3(4); + +figure(2); +hold off; +plot3(M_xk(1,:), M_xk(2,:), M_xk(3,:), 'b.'); +hold on; +plot3(xk_sol(1,:), xk_sol(2,:), xk_sol(3,:), 'r.'); +%} \ No newline at end of file diff --git a/calibration/dlt/randomorthogonal.m b/calibration/dlt/randomorthogonal.m new file mode 100644 index 0000000000000000000000000000000000000000..cfb5359fff4714fb14e0c80bd8d349d068b2ca81 --- /dev/null +++ b/calibration/dlt/randomorthogonal.m @@ -0,0 +1,5 @@ +function Q = randomorthogonal(N) + S = randn(N); + [Q,D] = eig(S+S'); + Q = Q * det(Q); +end \ No newline at end of file diff --git a/calibration/dlt/randortho.m b/calibration/dlt/randortho.m new file mode 100644 index 0000000000000000000000000000000000000000..a31c3d0451d9c60b2352db1f9b74c0b95f94f91e --- /dev/null +++ b/calibration/dlt/randortho.m @@ -0,0 +1,6 @@ +function Q = randortho(N) + % create a random, orthogonal NxN matrix + S = randn(N); + [Q,D] = eig(S+S'); + Q = Q * det(Q); +end \ No newline at end of file diff --git a/calibration/find_calibration_markers.m b/calibration/find_calibration_markers.m new file mode 100644 index 0000000000000000000000000000000000000000..de47e973454f78df82653cebc6282d4fa4b1e3eb --- /dev/null +++ b/calibration/find_calibration_markers.m @@ -0,0 +1,195 @@ +function [ij_markers, xy_markers, on_x_axis, on_y_axis] = find_calibration_markers(img, ij_center, ij_right, ij_top, dot_half_size) + % find_markers(img, ij_center, ij_top, ij_right) + % + % find markers on a regular calibration grid in the image img + % x_markers is returned, which contains the [i,j] coordinates (in image + % space) of each marker identified. in addition, also return the [x,y] + % coordinates of each marker. these are, by definition, integer values + % + % to start the search, the user must provide the [i,j] coordinates of + % ij_center - the calibration marker at the center of the coordinate + % system + % ij_top - the calibration marker immediately above the center marker + % ij_right - the calibration marker immediately to the right of the + % center marker + + %% put data in right format + ij_center = ij_center(:); + ij_top = ij_top(:); + ij_right = ij_right(:); + + %% get size of image + Nx = size(img, 1); + Ny = size(img, 2); + + thresh = 0.6; % minimum correlation coefficient required for match + + %% get dot img + dot_size = dot_half_size * 2 + 1; + dot_img_i = round(ij_center(1)) + (-dot_half_size(1) : dot_half_size(1)); + dot_img_j = round(ij_center(2)) + (-dot_half_size(2) : dot_half_size(2)); + if dot_img_i(1) < 1 || dot_img_i(end) > Nx || dot_img_j(1) < 1 dot_img_j(end) > Ny + error('Center dot is too close to edge of image'); + end + dot_img = img(dot_img_i, dot_img_j); + + %% convolve image and find indices of maxima + dot_img = dot_img - mean(dot_img(:)); + % subtract sliding average so DC component of x-correlation windows is + % zero + img_sliding_av = filter2(ones(size(dot_img)), img) / numel(dot_img); + img = img - img_sliding_av; + % cross-correlate + convolved = filter2(dot_img, img); + % normalise by image energy + image_energy = filter2(ones(size(dot_img)), img.^2); + dot_energy = sum(dot_img(:).^2); + correl = convolved ./ (sqrt(image_energy) * sqrt(dot_energy)); + %correl = correl / correl(round(ij_center(1)), round(ij_center(2))); + + correl_left = inf([Nx Ny]); + correl_right = inf([Nx Ny]); + correl_above = inf([Nx Ny]); + correl_below = inf([Nx Ny]); + + correl_left(1:Nx-2, 2:Ny-1) = correl(2:Nx-1, 2:Ny-1); + correl_right(3:Nx, 2:Ny-1) = correl(2:Nx-1, 2:Ny-1); + correl_above(2:Nx-1, 1:Ny-2) = correl(2:Nx-1, 2:Ny-1); + correl_below(2:Nx-1, 3:Ny) = correl(2:Nx-1, 2:Ny-1); + + bw_markers = correl > correl_left ... + & correl > correl_right ... + & correl > correl_below ... + & correl > correl_above ... + & correl > thresh; + %figure(2); + %imagesc(correl' > thresh); + %axis equal tight xy; + %% identify likely markers which are on the grid + ivec = 1 : Nx; + jvec = 1 : Ny; + [imat,jmat] = ndgrid(ivec, jvec); + + Nmarkers = sum(bw_markers(:)); + ij_markers = zeros(2, Nmarkers); + ij_markers(1,:)= imat(bw_markers); + ij_markers(2,:)= jmat(bw_markers); + + % find the position of the top/right markers more accurately + ij_right = ij_markers(:, knnsearch(ij_markers', ij_right', 'K', 1)); + ij_top = ij_markers(:, knnsearch(ij_markers', ij_top', 'K', 1)); + + % search for dots on the i/j axes + dx = norm(ij_center - ij_right); + dy = norm(ij_center - ij_top); + e_x = (ij_right - ij_center) / dx; + e_y = (ij_top - ij_center) / dy; + % angle tolerance, in radians, that must be satisfied in order for a + % point to be considered on the x or y axes. angle is taken between the + % x/y axis and the line between the center and the point under + % consideration + delta_theta_x = 0.5 * dy / Nx; + delta_theta_y = 0.5 * dx / Ny; + + ij_rel_markers = ij_markers - ij_center * ones(1,Nmarkers); + ij_rel_length = sqrt(sum(ij_rel_markers.^2,1)); + [~,center_idx] = min(ij_rel_length); + e_x_mat = e_x * ones(1,Nmarkers); + e_y_mat = e_y * ones(1,Nmarkers); + cos_theta_x = dot(ij_rel_markers, e_x_mat, 1) ./ ij_rel_length; + cos_theta_y = dot(ij_rel_markers, e_y_mat, 1) ./ ij_rel_length; + + % get indices of points on x and y axes + % include the center marker in this + on_x_axis = find(abs(cos_theta_x) > cos(delta_theta_x)); + on_y_axis = find(abs(cos_theta_y) > cos(delta_theta_y)); + on_x_axis = sort(unique([on_x_axis, center_idx])); + on_y_axis = sort(unique([on_y_axis, center_idx])); + Nonx = length(on_x_axis); + Nony = length(on_y_axis); + + %% eliminate markers which are not at the right spacing + % if distance is more than a percentage threshold out, reject + x_on_x = dot(ij_rel_markers(:,on_x_axis), e_x*ones(1,Nonx), 1); + y_on_y = dot(ij_rel_markers(:,on_y_axis), e_y*ones(1,Nony), 1); + [x_valid,x_ind]= find_equispaced_values(x_on_x, dx); + [y_valid,y_ind]= find_equispaced_values(y_on_y, dy); + % truncate markers which are not equispaced + on_x_axis = on_x_axis(x_valid); + on_y_axis = on_y_axis(y_valid); + Nonx = length(on_x_axis); + Nony = length(on_y_axis); + % identify which coordinate each marker corresponds to on x/y axes + [~,x_center] = min(abs(x_on_x(x_valid))); + [~,y_center] = min(abs(y_on_y(y_valid))); + x_axis = x_ind - x_ind(x_center); + y_axis = y_ind - y_ind(y_center); + + %% improve estimate of e_x and e_y + px = polyfit(ij_markers(1,on_x_axis), ij_markers(2,on_x_axis), 1); + py = polyfit(ij_markers(2,on_y_axis), ij_markers(1,on_y_axis), 1); + e_x = [1; px(1)] / sqrt(1 + px(1)^2); + e_y = [py(1); 1] / sqrt(1 + py(1)^2); + + %% identify markers which lie on the grid + b_on_grid = false(1, Nmarkers); + b_on_grid(center_idx)= true; + b_on_grid(on_x_axis) = true; + b_on_grid(on_y_axis) = true; + xy_markers = zeros(2, Nmarkers); + xy_markers(1,on_x_axis) = x_axis; + xy_markers(2,on_y_axis) = y_axis; + % for each point on x-axis, search up/down for points // to y axis + for xidx = 1 : Nonx + % identify markers on the axis with x = const, // to y axis + marker_idx = on_x_axis(xidx); + ij_rel_markers = ij_markers - ij_markers(:,marker_idx)*ones(1, Nmarkers); + ij_rel_length = sqrt(sum(ij_rel_markers.^2,1)); + cos_theta_y = dot(ij_rel_markers, e_y_mat, 1) ./ ij_rel_length; + on_this_axis = abs(cos_theta_y) > cos(delta_theta_y); + b_on_grid = b_on_grid | on_this_axis; + % save the x/y coord of these markers + Nonthis = sum(on_this_axis); + y_component = dot(ij_rel_markers(:,on_this_axis), e_y*ones(1,Nonthis), 1); + xy_markers(1,on_this_axis) = x_axis(xidx); + xy_markers(2,on_this_axis) = round(y_component/dy); + end + + %% delete markers are not on grid or accidentally have duplicates + b_on_x_axis = false(1, Nmarkers); + b_on_x_axis(on_x_axis) = true; + b_on_y_axis = false(1, Nmarkers); + b_on_y_axis(on_y_axis) = true; + + ij_markers = ij_markers(:, b_on_grid); + xy_markers = xy_markers(:, b_on_grid); + b_on_x_axis = b_on_x_axis(b_on_grid); + b_on_y_axis = b_on_y_axis(b_on_grid); + % get rid of markers which are repeated more than once + [~,IA] = unique(xy_markers', 'rows'); + repeats = setxor(IA, 1 : size(xy_markers,2)); + [xy_markers,IA]= setxor(xy_markers', xy_markers(:, repeats)', 'rows'); + xy_markers = xy_markers'; + ij_markers = ij_markers(:, IA); + b_on_x_axis = b_on_x_axis(IA); + b_on_y_axis = b_on_y_axis(IA); + on_x_axis = find(b_on_x_axis); + on_y_axis = find(b_on_y_axis); + idx_center = find(ij_markers(1,:)==ij_center(1) & ij_markers(2,:)== ij_center(2)); + + %% plot things + %{ + hold off; + imagesc(img'); axis xy; + hold on; + plot(ij_markers(1,:), ij_markers(2,:), 'r+'); + axis equal tight; + colormap gray; + plot(ij_markers(1,on_x_axis), ij_markers(2,on_x_axis), 'go'); + plot(ij_markers(1,on_y_axis), ij_markers(2,on_y_axis), 'gd'); + plot(ij_markers(1,idx_center), ij_markers(2,idx_center), 'bs'); + colormap gray; + %} +end + + diff --git a/calibration/find_equispaced_values.m b/calibration/find_equispaced_values.m new file mode 100644 index 0000000000000000000000000000000000000000..22861dd663d00f12d092c0e6e42c24f3d37cee31 --- /dev/null +++ b/calibration/find_equispaced_values.m @@ -0,0 +1,135 @@ +function [equispaced, index] = find_equispaced_values(x, dx_guess) + % for a set of values x + % which are approximately equispaced, but with some invalid points added + % that are not equispaced, try to find the points which are equally + % spaced apart + + %% sort x ascending + [x,idx] = sort(x, 'ascend'); + [~,reverse] = sort(idx, 'ascend'); + Nx = length(x); + + %% identify potential collisions + % place values into bins which are a fraction of initial guess dx + % if values are repeated approximately dx apart, then there should only + % be one per bin + + bin_width = 0.8 * dx_guess; + bin_index = round(x / bin_width); + bin_centres = bin_width * (min(bin_index) : max(bin_index)); + n_bins = length(bin_centres); + n_in_bin = zeros(1, n_bins); % number of elements in this bin + idx_in_bin = cell(1, n_bins); % index of elements in x belonging to this bin + possible_repeat= false(1, Nx); + for i = 1 : n_bins + centre_distance= abs(x - bin_centres(i)); + in_this_bin = centre_distance < bin_width / 2; + n_in_bin(i) = sum(in_this_bin); + idx_in_bin{i} = find(in_this_bin); + if n_in_bin(i) > 1 + possible_repeat(in_this_bin) = true; + end + end + + %% find typical spacing of values not likely to be collisions + dx_vec = x(2:end) - x(1:end-1); + dx_vec = dx_vec(:); + dx_vec = ([dx_vec; dx_vec(end)] + [dx_vec(1); dx_vec]) / 2; % mean of left and right spacing + x_filtered = x(~possible_repeat); + dx_filtered = dx_vec(~possible_repeat); + dx_meas = median(dx_filtered); + + %% preliminary search + % filter the data into a non-contiguous set of bins + % which are spaced dx_meas apart, but are narrow + % and find the offset that maximises the number of items identified + % + % ----x----x----x----x---- 0 fall into bins + % ----x----x----x----x---- 0 + % ----x----x----x----x---- 0 + % ----x----x----x----x---- 1 + % ----x----x----x----x---- 4 + % * * * ** * * + bin_spacing = dx_meas; + bin_width = 0.15 * bin_spacing; % treated as half-width + bin_offset = (-dx_meas/2) : bin_width/4 : (dx_meas/2); + Nbins = length(bin_offset); + Nvalid = zeros(1, Nbins); + for iter = 1 : Nbins + x_offset = x_filtered - bin_offset(iter); + in_a_bin = abs(x_offset - round(x_offset/bin_spacing)*bin_spacing) < bin_width; + Nvalid(iter)= sum(in_a_bin(:)); + end + [~,best] = max(Nvalid); + best_bin_offset= bin_offset(best); + + % identify values classified as equispaced + % ignoring repeat values + x_offset = x - bin_offset(best); + bin_index = round(x_offset / bin_spacing); + bin_centres = bin_spacing * (min(bin_index) : max(bin_index)); + n_bins = length(bin_centres); + n_in_bin = zeros(1, n_bins); + equispaced = false(1, Nx); + for i = 1 : n_bins + centre_distance= abs(x_offset - bin_centres(i)); + [d,closest] = min(centre_distance); + if d < bin_width + equispaced(closest) = true; + end + end + + % select only one contiguous block of equispaced points + [labelled,N] = bwlabeln(equispaced); + blocksize = zeros(1,N); + for i = 1 : N + blocksize(i) = sum(labelled == i); + end + [~,biggest_block] = max(blocksize); + equispaced = equispaced & labelled == biggest_block; + + %% apply less conservative search + % first search is usually too conservative, because of error in dx_meas + % however, it gives us a good base for the next step + % starting from the left, decide if a point in x belongs in the + % equispaced set, because it's approximately dx_meas away from the last + % point in that set + bin_width = 0.15 * dx_meas; + for candidate = 2 : Nx + last_good = find(equispaced & ((1:Nx) < candidate), 1, 'last'); + if isempty(last_good) + continue; + end + dx = x(candidate) - x(last_good); + if abs(dx - round(dx/dx_meas)*dx_meas) < bin_width && abs(round(dx/dx_meas)) >= 1 + equispaced(candidate) = true; + end + end + % apply same search from right to left + for candidate = Nx-1 : -1 : 1 + first_good = find(equispaced & ((1:Nx) > candidate), 1, 'first'); + if isempty(first_good) + continue; + end + dx = x(first_good) - x(candidate); + if abs(dx - round(dx/dx_meas)*dx_meas) < bin_width && abs(round(dx/dx_meas)) >= 1 + equispaced(candidate) = true; + end + end + + %% index the markers + Nvalid = sum(equispaced(:)); + i_valid = find(equispaced); + index = zeros(1,Nvalid); + for it = 2 : Nvalid + dx = x(i_valid(it)) - x(i_valid(it-1)); + index(it) = index(it-1) + round(dx/dx_meas); + end + index_full = nan(1, Nx); + index_full(equispaced) = index; + + %% undo the sorting + equispaced = equispaced(reverse); + index_full = index_full(reverse); + index = index_full(equispaced); +end \ No newline at end of file diff --git a/calibration/find_intersection.m b/calibration/find_intersection.m new file mode 100644 index 0000000000000000000000000000000000000000..9241f2c4e5eac2a56ffce9d79f270c061cc8180d --- /dev/null +++ b/calibration/find_intersection.m @@ -0,0 +1,142 @@ +function [m, c, width, line_u, line_v, bSuccess] = find_intersection(u, v, sheet_img, max_thickness, v_min, v_max, grid_du, grid_dv) + % find_intersection(image) + % + % finds the line of intersection of a laser sheet and a calibration + % plate in the image provided, in the format provided by readimx + % + % the image should have been dewarped in Davis first + % + % returns the line of intersection u = m*v + c in the image coordinate + % system [u,v] + % as well as the e^-2 thickness w of the laser sheet, as it is observed when + % projected onto the calibration plate + % + % a gaussian profile of intensity for the laser sheet is assumed + % + % additionally, it is possible to specify: + % max_thickness - the maximum thickness the laser sheet should be + % v_min - minimum v to search for the intersection + % v_max - maximum v to search for the intersection + + %% load coordinate system + search_width = 10; + grid_du = 10; + grid_dv = 10; + Nu = length(u); + Nv = length(v); + + %% filter image + G = fspecial('gaussian', [7 7], 2); + sheet_img_filt = filter2(G, sheet_img); + + %% identify points on sheet + % find maximum intensity along rows + ok_range = v > v_min & v < v_max; + line_v = v; + line_u = zeros(1, Nv); + bSuccess = false; + for j = 1:Nv + [m,i_maximum] = max(sheet_img_filt(:, j)); + if m > 500 + bSuccess = true; + end + line_u(j) = u(i_maximum); + end + + if(~bSuccess) + m = nan(); + c = nan(); + width = nan(); + return; + end + + % find equation of fit + line_coeffs = polyfit(line_v(ok_range), line_u(ok_range), 1); + m = line_coeffs(1); + c = line_coeffs(2); + + %% check whether fit is any good + % if it's not, don't bother continuing + fit_error = line_u(ok_range) - polyval(line_coeffs, line_v(ok_range)); + std_error = sqrt(prctile(fit_error.^2,66)); + max_error = abs(u(2) - u(1)) * 10; + if std_error > max_error + m = nan(); + c = nan(); + width = nan(); + bSuccess = false; + return; + end + bSuccess = true; + + %% improve fit and measure e^-2 width + % hack to avoid measurements across calibration markers + v_min = round(v_min/grid_dv) * grid_dv; + v_max = round(v_max/grid_dv) * grid_dv; + v_min = max(min(v), v_min); + v_max = min(max(v), v_max); + line_v = linspace(v_min, v_max, Nrows); + for row = 1 : Nrows + rem = line_v(row) - round(line_v(row)/grid_dv)*grid_dv; + if abs(rem) < 0.15 * grid_dv + if rem < 0 + line_v(row) = (round(line_v(row)/grid_dv)-0.15) * grid_dv; + else + line_v(row) = (round(line_v(row)/grid_dv)+0.15) * grid_dv; + end + end + end + % v must be at integer pixel location + row_idx = round(interp1(v, 1 : Nv, line_v)); + row_idx = min(max(row_idx,1),Nv); + line_v = v(row_idx); + line_u = zeros(1, Nrows); + line_width = zeros(1, Nrows); + + for row = 1 : Nrows + j = row_idx(row); + u_line = m*v(j) + c; + nearby = abs(u' - u_line) < search_width/2; + dc_level = mean(sheet_img(:, j)); + thresh = max(sheet_img(nearby,j) - dc_level) * exp(-2); + lb = find(nearby & (sheet_img(:,j)-dc_level) > thresh, 1, 'first'); + ub = find(nearby & (sheet_img(:,j)-dc_level) > thresh, 1, 'last'); + nearby = lb : ub; + u_nearby = u(nearby); + sig_nearby = sheet_img(nearby, j) + 1; + + % fit a cubic polynomial, + % then around the maximum, evaluate coefficients for a quadratic + % polynomial in that vicinity. + %{ + p = polyfit(u_nearby(:), log(sig_nearby(:)), 3); + p_prime = [3*p(1) 2*p(2) p(3)]; + extrema = roots(p_prime); + if isreal(extrema) + % identify which extrema is the maximum, find taylor series + % expansion around it + p_2prime = [6*p(1) 2*p(2)]; + extrema_type = polyval(p_2prime, extrema); + x0 = extrema(extrema_type < 0); + f_x0 = polyval(p, x0); + fp_x0 = polyval(p_prime, x0); + fpp_x0 = polyval(p_2prime, x0); + p = [(fpp_x0/2), (fp_x0 - fpp_x0*x0), (f_x0 - fp_x0*x0 + fpp_x0*x0^2/2)]; + else + % the fit doesn't have a local maximum/minimum, + % i.e. we get imaginary roots. resort to a quadratic fit + p = polyfit(u_nearby(:), log(sig_nearby(:)), 2); + end + %} + p = polyfit(u_nearby(:), log(sig_nearby(:)), 2); + + line_u(row) = -p(2) / (2*p(1)); + line_width(row)= sqrt(-8 / p(1)); + end + + ok = ~imag(line_width) & ~isnan(line_width); + width = mean(line_width(ok)); + line_coeffs = polyfit(line_v(ok), line_u(ok), 1); + m = line_coeffs(1); + c = line_coeffs(2); +end diff --git a/calibration/find_largest_volume.m b/calibration/find_largest_volume.m new file mode 100644 index 0000000000000000000000000000000000000000..fc66240e80c693235d44d1d471de5562b8f5892f --- /dev/null +++ b/calibration/find_largest_volume.m @@ -0,0 +1,50 @@ +function [XLim, YLim, ZLim] = find_largest_volume(P, ilim, jlim, M1, M2) + % for the projection matrix P + % which maps + % [i j 1]' ~ P[x y z 1]' + % + % find the largest volume + % xlim(1) <= x <= xlim(2) + % ylim(1) <= y <= ylim(2) + % zlim(1) <= z <= zlim(2) + % + % which is bound by planes represented by M1 and M2 + % points on a plane satisfy M(1:3)*x + M(4) = 0 + % + % which may be projected onto [i,j] and still fit within + % ilim(1) < i < ilim(2) + % jlim(1) < j < jlim(2) + % + % + + % M1 and M2 are equ + P1 = [P; M1]; + P2 = [P; M2]; + P1i = P1^-1; + P2i = P2^-1; + + % corners on image plane + i_corners = [ilim(1) ilim(1) ilim(2) ilim(2)]; + j_corners = [jlim(1) jlim(2) jlim(2) jlim(1)]; + ij10 = [i_corners(:) j_corners(:) ones(4,1) zeros(4,1)]; + + % corners on plane 1 + lambda_inv = ij10 * P1i(4,:)'; + x_corners = ij10 * P1i(1,:)' ./ lambda_inv; + y_corners = ij10 * P1i(2,:)' ./ lambda_inv; + z_corners = ij10 * P1i(3,:)' ./ lambda_inv; + + % corners on plane 2 + lambda_inv = ij10 * P2i(4,:)'; + x_corners = [x_corners; ij10 * P2i(1,:)' ./ lambda_inv]; + y_corners = [y_corners; ij10 * P2i(2,:)' ./ lambda_inv]; + z_corners = [z_corners; ij10 * P2i(3,:)' ./ lambda_inv]; + + % use this simple trick to work out largest box that fits + XLim = sort(x_corners, 'ascend'); + YLim = sort(y_corners, 'ascend'); + ZLim = sort(z_corners, 'ascend'); + XLim = XLim([4 5]); + YLim = YLim([4 5]); + ZLim = ZLim([4 5]); +end \ No newline at end of file diff --git a/calibration/flip_calibration.m b/calibration/flip_calibration.m new file mode 100644 index 0000000000000000000000000000000000000000..7f1ac81ee3d97963c3336caf0a554b38dde816d1 --- /dev/null +++ b/calibration/flip_calibration.m @@ -0,0 +1,30 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% flip_calibration.m +% +% flip the axes of a camera calibration + +%% input parameters +FLIP_X = false; +FLIP_Y = true; +calib_dir = '/nfs/data/lawson/iso60/160726/calib/'; +calib_file_in = [calib_dir 'camera-model-original.mat']; +calib_file_out = [calib_dir 'camera-model-flip.mat']; + +%% load +fprintf('Loading %s\n', calib_file_in); +setup = importdata(calib_file_in); + +% quick fix of camera model +for c = 1 : setup.n_cameras + if FLIP_X + setup.camera(c).i_axis = setup.camera(c).i_axis(end:-1:1); + end + if FLIP_Y + setup.camera(c).j_axis = setup.camera(c).j_axis(end:-1:1); + end +end + +%% save +fprintf('Saving %s\n', calib_file_out); +save(calib_file_out, '-struct', 'setup'); +fprintf('done\n'); \ No newline at end of file diff --git a/calibration/frustum.m b/calibration/frustum.m new file mode 100644 index 0000000000000000000000000000000000000000..6fdccc9326be02e84b30eb1c9c8fa447bee60be7 --- /dev/null +++ b/calibration/frustum.m @@ -0,0 +1,60 @@ +function [M,U,xc] = frustum(cmodel) + % [M,U,xc] = frustum(cmodel) + % + % Calculate the viewing frustum for the pinhole camera model specified + % by cmodel + % + % M is a 4x4 matrix whose rows provide the equation of plane for each + % plane of the viewing frustum. That is to say, for each plane i, + % points satisfying: + % M(i,1)*x + M(i,2)*y + M(i,3)*z + M(i,4) = 0 + % lie on that plane + % + % The plane normals are oriented so that (0,0,0) always corresponds to a + % positive distance + % + % U is a 3x4 matrix whose columns describe the unit vectors of lines + % describing the viewing frustum. Each edge i of the frustum is given by + % the equation of a line: + % + % x(s) = xc + u(:,i)*s + + %% extract pinhole model parameters + i_axis = cmodel.i_axis; + j_axis = cmodel.j_axis; + ic = cmodel.calib.ic; + kr = cmodel.calib.kr; + G = cmodel.calib.G; + R = cmodel.calib.R; + GR = G*R; + xc = cmodel.calib.xc; + + %% calculate corners + ij_corners = [i_axis([1 1 end end]); + j_axis([1 end end 1])]; + % account for distortion + ij_corners = inverse_radial_distortion(ij_corners, ic, kr); + + %% find unit vectors + ij_tilde = [ij_corners; ones(1,4)]; + U = GR \ ij_tilde; + U_norm = sqrt(sum(U.^2,1)); + U = U ./ ([1;1;1]*U_norm); + % orientation is such that u . xc < 0 + flip = -sign(U.' * xc)'; + U = U .* ([1;1;1]*flip); + + %% find equations of plane + M = zeros(4,4); + for c = 1 : 4 + m = cross(U(:,c), U(:,mod(c,4)+1)); + m = m / norm(m); + d = -dot(m, xc); + % plane normals are oriented to ensure (0,0,0) is always on +ve side + if d<0 + d = -d; + m = -m; + end + M(c,:) = [m', d]; + end +end \ No newline at end of file diff --git a/calibration/local_maxima2.m b/calibration/local_maxima2.m new file mode 100644 index 0000000000000000000000000000000000000000..2f3b5e3beae3eee86503f3ce3b0783a11b7a0fec --- /dev/null +++ b/calibration/local_maxima2.m @@ -0,0 +1,16 @@ +function locmin = local_maxima2(im) + sz = size(im); + rx = 2 : sz(1)-1; + ry = 2 : sz(2)-1; + locmin = false(sz); + locmin(rx,ry) = true; + + for i = -1 : 1 + for j = -1 : 1 + if i == j + continue; + end + locmin(rx,ry) = locmin(rx,ry) & im(rx,ry) > im(rx+i,ry+j); + end + end +end \ No newline at end of file diff --git a/calibration/local_maxima3.m b/calibration/local_maxima3.m new file mode 100644 index 0000000000000000000000000000000000000000..16bebfa7f00c8a40bfb74f5e2c9280e45c0b871e --- /dev/null +++ b/calibration/local_maxima3.m @@ -0,0 +1,22 @@ +function locmin = local_minima3(X) + % return bool array of points that are local minima in the array x + sz = size(X); + rx = 2 : sz(1) - 1; + ry = 2 : sz(2) - 1; + rz = 2 : sz(3) - 1; + + locmin = true(sz-2); + + for i = -1 : 1 + for j = -1 : 1 + for k = -1 : 1 + if i == j && j == k + continue; + end + + locmin = locmin & X(rx,ry,rz) > X(rx+i,ry+j,rz+k); + end + end + end + locmin = padarray(locmin, [1 1 1], false, 'both'); +end diff --git a/calibration/local_minima.m b/calibration/local_minima.m new file mode 100644 index 0000000000000000000000000000000000000000..cde435bb3d31ff1b271482f6e25171e493c276b5 --- /dev/null +++ b/calibration/local_minima.m @@ -0,0 +1,15 @@ +function locmin = local_minima(im) + sz = size(im); + locmin = true(sz); + rx = 2 : sz(1)-1; + ry = 2 : sz(2)-1; + + for i = -1 : 1 + for j = -1 : 1 + if i == j + continue; + end + locmin(rx,ry) = locmin(rx,ry) & im(rx,ry) < im(rx+i,ry+j); + end + end +end \ No newline at end of file diff --git a/calibration/local_minima3.m b/calibration/local_minima3.m new file mode 100644 index 0000000000000000000000000000000000000000..f90b25908e6173d0470a6dd26f2c3243a6e05bf1 --- /dev/null +++ b/calibration/local_minima3.m @@ -0,0 +1,22 @@ +function locmin = local_minima3(X) + % return bool array of points that are local minima in the array x + sz = size(X); + rx = 2 : sz(1) - 1; + ry = 2 : sz(2) - 1; + rz = 2 : sz(3) - 1; + + locmin = true(sz-2); + + for i = -1 : 1 + for j = -1 : 1 + for k = -1 : 1 + if i == j && j == k + continue; + end + + locmin = locmin & X(rx,ry,rz) < X(rx+i,ry+j,rz+k); + end + end + end + locmin = padarray(locmin, [1 1 1], false, 'both'); +end diff --git a/calibration/log_find_markers.m b/calibration/log_find_markers.m new file mode 100644 index 0000000000000000000000000000000000000000..4ad8c50319867f24ebb45b286d648d96f469c733 --- /dev/null +++ b/calibration/log_find_markers.m @@ -0,0 +1,69 @@ +function [ij_mark, im_filt] = log_find_markers(i_axis, j_axis, im, scale, n_max, filter_type) + % use Laplacian of Gaussian filter to find markers in image im + % at lengthscale scale. + % + % returns n_max locations where the LoG filter is a local maximum + % ordered by magnitude of the peak + % + % local quadratic peak finding is applied to get accurate locations of + % maxima + + %% define filter + if nargin < 6 + filter_type = 'log'; + end + if strcmp(filter_type, 'log') + % default laplacian of Gaussian filter + kernel_size = round(scale*[1 1]*6); + kernel_size = 2*floor(kernel_size/2) + 1; % always use odd kernel + G = fspecial('log', kernel_size, scale); + elseif strcmp(filter_type, 'disk') + % disk filter + G = -fspecial('disk', round(scale)); + kernel_size = size(G); + end + im_filt = single(conv2(im, G, 'same')); + + %% identify local maxima + % grid + Ni = length(i_axis); + Nj = length(j_axis); + [imat,jmat] = ndgrid(i_axis, j_axis); + % search for local maximum in neighbourhood of kernel + im_filt_max = maxfiltnd(im_filt, kernel_size); % sliding maximum filter over kernel_size area + local_max = im_filt == im_filt_max; + peak_mag = im_filt(local_max); + % identify locations of local maxima + [pli,plj] = find(local_max); + ij_mark = [imat(local_max), jmat(local_max)]; + % sort by size of peak + [~,reidx] = sort(peak_mag, 'descend'); + ij_mark = ij_mark(reidx, :); + plij = [pli(reidx), plj(reidx)]; + + % exclude those near edges + edge = round(scale); + i_lim = sort(i_axis([edge+1 end-edge]), 'ascend'); + j_lim = sort(j_axis([edge+1 end-edge]), 'ascend'); + near_edge = ij_mark(:,1) < i_lim(1) | ij_mark(:,1) > i_lim(2) ... + | ij_mark(:,2) < j_lim(1) | ij_mark(:,2) > j_lim(2); + ij_mark = ij_mark(~near_edge,:); + plij = plij(~near_edge, :); + + %% recover n_max largest local maxima + n_mark = min(n_max, size(ij_mark,1)); + ij_mark = ij_mark(1:n_mark, :)'; + plij = plij(1:n_mark,:); + + %% apply local quadratic peak finding + rng = (-edge : edge); + di = (i_axis(end) - i_axis(1))/(Ni-1); + dj = (j_axis(end) - j_axis(1))/(Nj-1); + for n = 1 : n_mark + p_x = polyfit(rng*di, im_filt(plij(n,1) + rng, plij(n,2))', 2); + p_y = polyfit(rng*dj, im_filt(plij(n,1), plij(n,2) + rng) , 2); + ij_mark(1,n) = ij_mark(1,n) - p_x(2) / (2*p_x(1)); + ij_mark(2,n) = ij_mark(2,n) - p_y(2) / (2*p_y(1)); + end +end + \ No newline at end of file diff --git a/calibration/log_test.m b/calibration/log_test.m new file mode 100644 index 0000000000000000000000000000000000000000..cf3bb997ec78e95f46cac8fdd2b3f076bec361f7 --- /dev/null +++ b/calibration/log_test.m @@ -0,0 +1,179 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% test laplacian of Gaussian method for automatic identification of markers +% in image + + +blob_scale = 3; +grid_tol = 0.2; +n_iter = 3; +grid_size_x = 21; % must be odd # +grid_size_y = 21; % must be odd # +img_file = '/data/LowRe-2013/calib/CAMERA-01/CAM1_000001.tif'; + +%% load image +im = imread(img_file)'; +im = double(im); +im = fliplr(im); +sz = size(im); +Ni = sz(1); +Nj = sz(2); +i_axis = 1 : sz(1); +j_axis = 1 : sz(2); +[imat,jmat] = ndgrid(i_axis,j_axis); + +%% filter image at twice blob scale, get 3 big markers +[ij_big,im_filt] = log_find_markers(i_axis, j_axis, im, 2*blob_scale, 3); + +% identify top, right and center markers +% calculate unit vectors corresponding to each edge +normals = [(ij_big(:,3) - ij_big(:,2))' / norm(ij_big(:,3) - ij_big(:,2)); + (ij_big(:,3) - ij_big(:,1))' / norm(ij_big(:,3) - ij_big(:,1)); + (ij_big(:,2) - ij_big(:,1))' / norm(ij_big(:,2) - ij_big(:,1))]; +% if n_12 is nearly parallel to y |n_12.y| ~= 1 +% then vertex 3 is the right hand vertex. use this trick to find right hand +% vertex +[~,i_right] = max(abs(normals(:,2))); +% remaining vertices are top & center. top is higher up in image than +% center +i_tb = setdiff(1:3, i_right); +if ij_big(2, i_tb(1)) > ij_big(2, i_tb(2)) + i_top = i_tb(1); + i_center = i_tb(2); +else + i_top = i_tb(2); + i_center = i_tb(1); +end + +ij_center = ij_big(:,i_center); +ij_top = ij_big(:,i_top); +ij_right = ij_big(:,i_right); +ij_big = [ij_center, ij_right, ij_top]; +xy_big = [[0;0], [1;0], [0;1]]; + +% calculate included area of triangle +tri_area_px = abs(ij_big(1,1)*(ij_big(2,2) - ij_big(2,3)) ... + + ij_big(1,2)*(ij_big(2,3) - ij_big(2,1)) ... + + ij_big(1,3)*(ij_big(2,1) - ij_big(2,2))) / 2; +tri_area_mm = 0.5; +n_markers_est = floor(Ni * Nj / tri_area_px / 2); +search_tolerance = 0.05 * norm(ij_top - ij_right); +scale_px_to_mm = sqrt(tri_area_px / tri_area_mm); + +%% debug: plot identified markers +figure(1); +clf; +imagesc(i_axis, j_axis, im_filt'); +hold on; +plot(ij_center(1), ij_center(2), 'ro'); +plot(ij_top(1), ij_top(2), 'go'); +plot(ij_right(1), ij_right(2), 'bo'); +legend('center', 'top', 'right'); +%plot(ij_markers(1,:), ij_markers(2,:), 'k.'); + +axis equal tight xy; +colormap gray; +xlim(i_axis([1 end])); +ylim(j_axis([1 end])); + +%% filter image at blob scale, find ordinary markers +[ij_ordinary,im_filt]= log_find_markers(i_axis, j_axis, im, blob_scale, n_markers_est); +% exclude those near cent025er, top + right markers +d_center = sqrt(sum(bsxfun(@minus, ij_ordinary, ij_center).^2, 1)); +d_right = sqrt(sum(bsxfun(@minus, ij_ordinary, ij_right ).^2, 1)); +d_top = sqrt(sum(bsxfun(@minus, ij_ordinary, ij_top ).^2, 1)); +too_near = d_center < search_tolerance*4 ... + | d_right < search_tolerance*4 ... + | d_top < search_tolerance*4; +ij_ordinary = ij_ordinary(:, ~too_near); +% combine set with location of top, center and right markers +ij_markers = [ij_center, ij_right, ij_top, ij_ordinary]; + +%% determine affine transformation, select extra markers +A_xy_to_ij = [ij_right-ij_center, ij_top-ij_center]; +xy_markers = A_xy_to_ij \ bsxfun(@minus, ij_markers, ij_center); + +% choose a set of nearby markers +x_near = -1 : 1; +y_near = x_near; +[xm_near,ym_near] = ndgrid(x_near, y_near); +xy_near = [xm_near(:), ym_near(:)]'; + +% find matching markers and calcuate pinhole model +[xy_near,ij_near] = search_markers(xy_near, ij_markers, search_tolerance, 'affine', A_xy_to_ij, ij_center); +n_near = size(xy_near, 2); +[P2D, ij_res] = p2d_fit(xy_near, ij_near); + +%% iteratively refine model, search for more markers +max_grid_size = max(grid_size_x, grid_size_y); +for grid_size = 3 : 2 : max_grid_size + %% choose a set of nearby markers to search for + search_size_x = min(grid_size, grid_size_x); + search_size_y = min(grid_size, grid_size_y); + x_near = -(search_size_x-1)/2 : +(search_size_x-1)/2; + y_near = -(search_size_y-1)/2 : +(search_size_y-1)/2; + [xm_near,ym_near] = ndgrid(x_near, y_near); + xy_near = [xm_near(:), ym_near(:)]'; + + %% find matching markers and refine pinhole model + [xy_near,ij_near,ij_pred] = search_markers(xy_near, ij_markers, search_tolerance, 'pinhole', P2D); + n_near = size(xy_near, 2); + [P2D, ij_res] = p2d_fit(xy_near, ij_near); +end + +%% report statistics of residuals +fprintf('found %d markers\n', n_near); +fprintf('RMS residual:\n'); +fprintf(' x %0.3f px\n', sqrt(mean(ij_res(1,:).^2))); +fprintf(' y %0.3f px\n', sqrt(mean(ij_res(2,:).^2))); +fprintf('Largest residual:\n'); +fprintf(' x %0.3f px\n', max(abs(ij_res(1,:)))); +fprintf(' y %0.3f px\n', max(abs(ij_res(2,:)))); + +%% dewarp with pinhole model +x_lim = [min(xy_near(1,:)), max(xy_near(1,:))]; +y_lim = [min(xy_near(2,:)), max(xy_near(2,:))]; +Nx = round((x_lim(2)-x_lim(1))*scale_px_to_mm); +Ny = round((y_lim(2)-y_lim(1))*scale_px_to_mm); +x_axis = linspace(x_lim(1), x_lim(2), Nx); +y_axis = linspace(y_lim(1), y_lim(2), Ny); +[imat_dewarped,jmat_dewarped] = p2d_dewarp(P2D, x_axis, y_axis); +im_dewarped = interp2(i_axis, j_axis, im', imat_dewarped, jmat_dewarped); + +xy_near_true = p2d_map_im2obj(P2D, ij_near); +xy_big = p2d_map_im2obj(P2D, ij_big); +xy_center = xy_big(:,1); +xy_right = xy_big(:,2); +xy_top = xy_big(:,3); + +%% plot +figure(2); +clf; +imagesc(x_axis, y_axis, im_dewarped'); +% plot markers +hold on; +plot(xy_near_true(1,:), xy_near_true(2,:), 'r.'); +viscircles(xy_center', 0.1, 'DrawBackgroundCircle', true, 'EdgeColor', 'red'); +viscircles(xy_right' , 0.1, 'DrawBackgroundCircle', true, 'EdgeColor', 'blue'); +viscircles(xy_top' , 0.1, 'DrawBackgroundCircle', true, 'EdgeColor', 'green'); + +axis equal tight xy; +xlim(x_lim); +ylim(y_lim); +colormap gray; +grid on; +set(gca, 'xtick', x_lim(1) : x_lim(2)); +set(gca, 'ytick', y_lim(1) : y_lim(2)); +%{ +%plot(ij_center(1), ij_center(2), 'rx'); +%plot(ij_top(1), ij_top(2), 'gx'); +%plot(ij_right(1), ij_right(2), 'kx'); +%plot(ij_markers(1,:), ij_markers(2,:), 'k.'); +plot(ij_near(1,:), ij_near(2,:), 'rx'); +viscircles(ij_pred', search_tolerance*ones(n_near,1)); +for n = 1 : n_near + text(ij_near(1,n)+20, ij_near(2,n), sprintf('%d,%d', xy_near(:,n))); +end +axis equal tight xy; +xlim(i_axis([1 end])); +ylim(j_axis([1 end])); +%} \ No newline at end of file diff --git a/calibration/make_dark.m b/calibration/make_dark.m new file mode 100644 index 0000000000000000000000000000000000000000..c72494cdc6928bb2e84d7abb58bca9e28bde6443 --- /dev/null +++ b/calibration/make_dark.m @@ -0,0 +1,92 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% generate a dark image based on particle tracking movies + +addpath([pwd '/../gmv']); + +%% input parameters +root_dir = '/scratch21/lawson/iso960/'; +gmv_pat = '%s/gmv/series_%04d.cam%d.gmv'; +dark_pat = '%s/calib/dark_160803.cam%d.gmv'; + +cam_idx = 0; +series_range = 0 : 40 : 2999; +frame_skip = 35; +c_lim = [0 128]; + +%% load first image +gmv_name = sprintf(gmv_pat,root_dir,series_range(1),cam_idx); +[dark_img,header] = read_gmv(gmv_name, 0, 1); +threshold = floor(header.threshold / 256); +dark_img = ones(size(dark_img), 'uint16') * threshold; +n_frames = header.n_frames; + +%% iterate over series +n_series = length(series_range); +for is = 1 : n_series + %% report + series_idx = series_range(is); + gmv_name = sprintf(gmv_pat,root_dir,series_idx,cam_idx); + fprintf('Series %4d: %s\n', series_idx, gmv_name); + + %% load frames + for f = 0 : frame_skip : n_frames-1 + frm = read_gmv(gmv_name, f, 1); + frm = uint16(frm); + nz = frm ~= 0; + dark_img(nz) = min(dark_img(nz), frm(nz)); + end +end + +%% save dark image +dark_name = sprintf(dark_pat, root_dir, cam_idx); +fprintf('Saving to %s\n', dark_name); +b_write = false; +if exist(dark_name, 'file') + str = input('File already exists. Overwrite? y/n ', 's'); + if strcmp(str, 'y') + b_write = true; + end +else + b_write = true; +end + +if b_write + save_gmv(dark_name, dark_img, header); +end +fprintf('done\n'); + +%% plot + +figure(1); +imagesc(dark_img'); +ax = gca; +axis equal tight ij; +set(gca, 'clim', [0 threshold]); +colormap(gray); +colorbar; +title(sprintf('Dark image of camera %d', cam_idx)); + +%% plot last frame with dark correction +frm_corr = frm - dark_img; +figure(2); +imagesc(frm_corr'); +ax(2)=gca; +axis equal tight ij; +set(gca, 'clim', c_lim); +colormap(1-gray); +colorbar; +title(sprintf('Corrected image from camera %d', cam_idx)); + +%% plot last frame without dark correction +figure(3); +imagesc(frm'); +ax(3)=gca; +axis equal tight ij; +set(gca, 'clim', c_lim); +colormap(1-gray); +colorbar; +title(sprintf('Uncorrected image from camera %d', cam_idx)); + +%% link axes +linkaxes(ax, 'xy'); + diff --git a/calibration/manual_sheet_thickness.m b/calibration/manual_sheet_thickness.m new file mode 100644 index 0000000000000000000000000000000000000000..d027111ad07bc1e9d19cf329695a8943a379430d --- /dev/null +++ b/calibration/manual_sheet_thickness.m @@ -0,0 +1,61 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% manually measure laser sheet thickness + +im_file = '/nfs/scratch23/lawson/K62/170119/sheet/sheet_thickness_9213.tif'; + +%% load image +im = imread(im_file); +im = im(:,:,1); +im = double(im); + +%% plot +figure(1); +clf; +ax = [0,0]; +ax(2) = subplot(1,2,2); +ax(1) = subplot(1,2,1); + +imagesc(im); +axis equal tight ij; + +%% get points +fprintf('Select 2 points\n'); +ij_points = ginput(2); +ij_start = round(ij_points(1,:)); +ij_end = round(ij_points(2,:)); +ij_end = [ij_end(1), ij_start(2)]; +ij_points = [ij_start; ij_end]; + +%% get brightness and fit +i_axis = ij_start(1) : ij_end(1); +j_axis = ij_start(2) * ones(size(i_axis)); +line_profile= im(ij_start(2), i_axis); + +ft = fittype('A*exp( -((x-x0)/s).^2 ) + C', 'indep', 'x', 'coefficients', {'A', 'x0', 'C', 's'}); + +% initial guess +A0 = max(line_profile) - min(line_profile); +C0 = min(line_profile); +x0 = mean(i_axis); +s0 = max(i_axis) - min(i_axis); + +P0 = [A0, x0, C0, s0]; +fitfun = fit(i_axis(:), line_profile(:), ft, 'StartPoint', P0); + +sigma = fitfun.s; +width = 2*sqrt(2)*sigma; + +%% report +fprintf('Measured at y = %d px\n', ij_start(2)); +fprintf('Thickness: %0.1f px\n', sigma); +fprintf('e^-2 thickness: %0.1f px\n', width); + +%% plot points +hold(ax(1), 'on'); + +plot(ax(1), ij_points(:,1), ij_points(:,2), 'rx-'); + +hold(ax(2), 'off'); +plot(ax(2), i_axis, line_profile, 'kx'); +hold(ax(2), 'on'); +plot(ax(2), i_axis, fitfun(i_axis), 'k-'); \ No newline at end of file diff --git a/calibration/min_bounding_box.m b/calibration/min_bounding_box.m new file mode 100644 index 0000000000000000000000000000000000000000..d5485a7dc3886b5579549bae62eb076bfd7df063 --- /dev/null +++ b/calibration/min_bounding_box.m @@ -0,0 +1,79 @@ +function [bb_lim, R, A, b] = min_bounding_box(camera) + % [bb_lim, R] = min_bounding_box(calib) + % determine the corners of the smallest arbitrarily oriented bounding box + % which fit encapsulate the extent of the measurement volume + % + % bb_lim is a 2 x 3 matrix containing the x, y and z limits of the + % smallest bounding box which encapsulate the entire measurement volume + % in a rotated coordinate system + % + % R is a rotation matrix describing the rotation of object space + % coordinates into a coordinate system in which all points fit within + % the limits + + %% get view frustums + % M = M_ary(:,p,c) are the coefficients for the equation of plane p of the + % viewing frustum for camera c. + % i.e. + % M(1)x + M(2)y + M(3)z + M(4) >= 0 + % when a point lies within the measurement volume + n_cameras = length(camera); + M_ary = zeros(4,4,n_cameras); + U_ary = zeros(3,4,n_cameras); + for c = 1 : n_cameras + [M,U] = frustum(camera(c)); + M_ary(:,:,c)= M'; + U_ary(:,:,c)= U; + end + + %% get orientation of view frustum + R_ary = zeros(3,3,n_cameras); + normalise = @(x) x / norm(x); + for c = 1 : n_cameras + e1 = normalise(U_ary(:,1,c)); + e2 = normalise(U_ary(:,2,c) - U_ary(:,1,c)); + e3 = normalise(cross(e1,e2)); + R_ary(:,:,c)= [e1, e2, e3]; + end + + %% setup linear programming problem + % to obtain volume bounds, we find the x that + % minimises c'x subject to Ax < b + A = reshape(-M_ary(1:3,:,:), [3 4*n_cameras])'; + b = reshape( M_ary(4 ,:,:), [4*n_cameras 1]); + bb_lim = zeros(2,3,n_cameras); + bb_vol = zeros(1,n_cameras); + opts = optimoptions('linprog', 'Display', 'off'); + + %% try orientations corresponding to orientation of cameras + for c = 1 : n_cameras + for i = 1 : 3 + e_i = R_ary(:,i,c); + bb_lim(1,i,c) = dot(e_i, linprog(+e_i, A, b, [], [], [], [], [], opts)); + bb_lim(2,i,c) = dot(e_i, linprog(-e_i, A, b, [], [], [], [], [], opts)); + end + bb_size = bb_lim(2,:,c) - bb_lim(1,:,c); + bb_vol(c) = abs(prod(bb_size)); + end + + %% identify smallest volume bounding box + [~,c_best] = min(bb_vol); + R = R_ary(:,:,c_best); + bb_lim = bb_lim(:,:,c_best); + + %% test + %{ + N = 1E5; + bb_size = bb_lim(2,:) - bb_lim(1,:); + bb_pos = bsxfun(@plus, bb_lim(1,:)', diag(bb_size) * rand(3,N)); + gl_pos = R*bb_pos; + bb_dist = bsxfun(@minus, A*gl_pos, b); + b_inside = all(bb_dist <= 0, 1); + figure(1); + clf; + plot3(gl_pos(1, b_inside), gl_pos(2, b_inside), gl_pos(3, b_inside), 'g.'); + hold on; + plot3(gl_pos(1,~b_inside), gl_pos(2,~b_inside), gl_pos(3,~b_inside), 'r.'); + axis equal tight xy; + %} +end \ No newline at end of file diff --git a/calibration/p2d_dewarp.m b/calibration/p2d_dewarp.m new file mode 100644 index 0000000000000000000000000000000000000000..7cac550967a1552799f0d7f404439d45a4f36884 --- /dev/null +++ b/calibration/p2d_dewarp.m @@ -0,0 +1,9 @@ +function [imat,jmat] = p2d_dewarp(P2D, x_axis, y_axis) + Nx = length(x_axis); + Ny = length(y_axis); + [xmat, ymat] = ndgrid(x_axis, y_axis); + xy = [xmat(:), ymat(:)]'; + ij = p2d_map_obj2im(P2D, xy); + imat = reshape(ij(1,:), [Nx Ny]); + jmat = reshape(ij(2,:), [Nx Ny]); +end \ No newline at end of file diff --git a/calibration/plot_calibration_residuals.m b/calibration/plot_calibration_residuals.m new file mode 100644 index 0000000000000000000000000000000000000000..2378ee9069a321a6badfbe52b1fa85189e7f60c9 --- /dev/null +++ b/calibration/plot_calibration_residuals.m @@ -0,0 +1,57 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% plot residuals in calibration procedure, comparing projected points +% according to model and observed points + +addpath([pwd '/../calibration-mpids/']); + +%% input parameters +calib_dir = '/nfs/scratch21/lawson/161019/calib/'; +calib_file_mat = [calib_dir '/camera-model-original.mat']; +font_size = 20; +pixel_pitch_mm = [10E-3 10E-3]; + +%% load calibration +fs = importdata(calib_file_mat); +n_cameras = fs.n_cameras; +camera = fs.camera; + +%% iterate over cameras +for c = 1 : n_cameras + %% load parameters + i_axis = camera(c).i_axis; + j_axis = camera(c).j_axis; + Ni = length(i_axis); + Nj = length(j_axis); + + %% load markers and calculate residuals + gl_markers = camera(c).calib.gl_markers; + ij_markers = camera(c).calib.ij_markers; + ij_markers_proj = pinhole_model(gl_markers, camera(c).calib, true); + ij_markers_res = ij_markers - ij_markers_proj; + + %% Tsai calibration + cparams = struct('Npixh', Nj, 'Npixw', Ni, ... + 'hpix', pixel_pitch_mm(1), ... + 'wpix', pixel_pitch_mm(2)); + [tsai_calib, tsai_res] = calib_Tsai(ij_markers', gl_markers', cparams); + tsai_res = -tsai_res'; + + %% plot + title_str = sprintf('Camera %d', c); + + figure(c); + clf; + quiver(ij_markers(1,:), ij_markers(2,:), ... + ij_markers_res(1,:), ij_markers_res(2,:), 'b'); + %hold on; + %quiver(ij_markers(1,:), ij_markers(2,:), ... + % tsai_res(1,:), tsai_res(2,:), 'r'); + axis equal tight xy; + xlim(i_axis([1 end])); + ylim(j_axis([1 end])); + + % make pretty + title(title_str, 'interpreter', 'latex', 'fontsize', font_size); + xlabel('$i$ / px', 'interpreter', 'latex', 'fontsize', font_size); + ylabel('$j$ / px', 'interpreter', 'latex', 'fontsize', font_size); +end \ No newline at end of file diff --git a/calibration/plot_camera_calibration_3d.m b/calibration/plot_camera_calibration_3d.m new file mode 100644 index 0000000000000000000000000000000000000000..1c8e9996429067235e3fd79a6b5e022a45a007be --- /dev/null +++ b/calibration/plot_camera_calibration_3d.m @@ -0,0 +1,202 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% camera_calibration.m +% +% create a pinhole camera calibration for one or more cameras +% from a set of views of the same calibration plate in different positions +% a compensation is made for error in calibration plate position and +% orientation + +addpath([pwd '/pinhole/']); +%% input parameters +camera_model_file = '/scratch21/lawson/p1344_kMeta2/calib/camera-model-160728-0000-otf-gauss2.mat'; +font_size = 20; + +%% load model +setup = importdata(camera_model_file); + +% camera calibration +n_cameras = setup.n_cameras; +n_views = setup.n_views; + +% plate pose +gl_plate_pos = setup.gl_plate_pos; +pl_to_gl_mat = repmat(setup.pl_to_gl, [1 1 n_views]); +camera_model = setup.camera; +gl_to_re = eye(3); +gl_origin = setup.gl_origin; +r_sphere = min(setup.r_sphere); + +%% get bounding frustums +M_ary = zeros(4,4,n_cameras); % equation of plane +U_ary = zeros(3,4,n_cameras); % unit vectors of viewing pyramid edges +xc_ary = zeros(3,n_cameras); % camera origin +xf_ary = zeros(3,4,n_cameras); % points on corners of viewing pyramid +for c = 1 : n_cameras + [M,U,xc] = frustum(setup.camera(c)); + M_ary(:,:,c) = M; + U_ary(:,:,c) = U; + xc_ary(:,c) = xc; + % get four corners of view frustum + for p = 1 : 4 + len = dot(gl_origin - xc, U(:,p)); + xf_ary(:,p,c) = xc + (len + 2*r_sphere)*U(:,p); + end +end + +%% determine bounding boxes +[gl_bb_lim, gl_bb_A, gl_bb_b]= bounding_box(setup.camera); +[xg,yg,zg] = ndgrid(linspace(gl_bb_lim(1,1), gl_bb_lim(2,1), 100), ... + linspace(gl_bb_lim(1,2), gl_bb_lim(2,2), 100), ... + linspace(gl_bb_lim(1,3), gl_bb_lim(2,3), 100)); +gl_pts = [xg(:), yg(:), zg(:)]'; +bb_dist = bsxfun(@minus, gl_bb_A*gl_pts, gl_bb_b); +b_inside = all(bb_dist <= 0, 1); +gl_inside = gl_pts(:,b_inside); + +% determine minimum bounding box +[mv_bb_lim, mv_to_gl, mv_bb_A, mv_bb_b] = min_bounding_box(setup.camera); +mv_bb_size = mv_bb_lim(2,:) - mv_bb_lim(1,:); + +% save in setup +setup.gl_bb_lim = gl_bb_lim; +setup.gl_bb_A = gl_bb_A; +setup.gl_bb_b = gl_bb_b; +setup.mv_bb_lim = mv_bb_lim; +setup.mv_to_gl = mv_to_gl; +setup.mv_bb_A = mv_bb_A; +setup.mv_bb_b = mv_bb_b; + +%% convex bounding hull +[tri_hull, V_hull] = convhull(gl_inside(1,:)', gl_inside(2,:)', gl_inside(3,:)'); +fprintf('Convex hull has volume %0.1f mm^2\n', V_hull); + +%% calculate coordinates of inscribed sphere +theta = linspace(0, pi, 21); +phi = linspace(0, 2*pi, 41); +[tmat,pmat] = ndgrid(theta,phi); +en_sphere = [sin(tmat(:)').*cos(pmat(:)'); + sin(tmat(:)').*sin(pmat(:)'); + cos(tmat(:)')]; +gl_sphere = bsxfun(@plus, r_sphere * en_sphere, gl_origin); + +%% project inscribed sphere onto different views +n_sphere = size(gl_sphere, 2); +ij_sphere_ary = zeros(2, n_sphere, n_cameras); +ij_orgin_ary = zeros(2, n_cameras); +for c = 1 : n_cameras + ij_sphere_ary(:,:,c) = pinhole_model(gl_sphere, setup.camera(c).calib, true); + ij_origin_ary(:,c) = pinhole_model(gl_origin, setup.camera(c).calib, true); +end + +%% plot 3D view of frustum and inscribed sphere +figure(1); +clf; +hold on; + +% inscribed sphere +plot3(gl_sphere(1,:), gl_sphere(2,:), gl_sphere(3,:), 'k.'); +% convex bounding hull +S = trisurf(tri_hull, gl_inside(1,:), gl_inside(2,:), gl_inside(3,:)); +set(S, 'FaceColor', 'r', 'facealpha', 0.3, 'edgecolor', 'none'); + +% viewing frustum of each camera +%{ +colours = {'red', 'green', 'blue'}; +for c = 1 : n_cameras + plot3(xc_ary(1,c), xc_ary(2,c), xc_ary(3,c), 'o', 'Color', colours{c}); + for p = 1 : 4 + tri = [xc_ary(:,c), xf_ary(:,p,c), xf_ary(:, mod(p,4)+1, c)]; + P = patch(tri(1,:), tri(2,:), tri(3,:), colours{c}); + set(P, 'facealpha', 0.2); + end +end +%} + +set(gca, 'CameraTarget', gl_origin); +% plot settings +axis equal tight xy +xlim(gl_origin(1) + 3*r_sphere*[-1 1]); +ylim(gl_origin(2) + 3*r_sphere*[-1 1]); +zlim(gl_origin(3) + 3*r_sphere*[-1 1]); +xlabel('x [mm]', 'interpreter', 'latex', 'fontsize', font_size); +ylabel('y [mm]', 'interpreter', 'latex', 'fontsize', font_size); +zlabel('z [mm]', 'interpreter', 'latex', 'fontsize', font_size); + +%% plot the projection in each camera +figure(2); +clf; + +for c = 1 : n_cameras + %% bits + i_axis = setup.camera(c).i_axis; + j_axis = setup.camera(c).j_axis; + ij_sphere = ij_sphere_ary(:,:,c); + ij_origin = ij_origin_ary(:,c); + id = setup.camera(c).id; + titstr = sprintf('Camera %d, id = %d', c, id); + + %% plot + subplot(2,2,c); + cla; + hold on; + plot(ij_sphere(1,:), ij_sphere(2,:), 'k.'); + plot(ij_origin(1), ij_origin(2), 'ro'); + axis equal tight xy; + xlim(sort(i_axis([1 end]))); + ylim(sort(j_axis([1 end]))); + title(titstr, 'interpreter', 'latex', 'fontsize', font_size); + xlabel('i / px', 'interpreter', 'latex', 'fontsize', font_size); + xlabel('j / px', 'interpreter', 'latex', 'fontsize', font_size); +end + +%% calculate marker positions for each view +re_marker_ary = cell(1,2); + +for c = 1 : 2 + for v = 1 : n_views + uv_markers = camera_model(c).view(v).uv_markers; + n_markers = size(uv_markers, 2); + %% append to vectors + gl_pos = gl_plate_pos(:,v); + pl_to_gl = pl_to_gl_mat(:,:,v); + + u_marker = uv_markers(1,:); + v_marker = uv_markers(2,:); + gl_markers = bsxfun(@plus, gl_pos, pl_to_gl * [u_marker; v_marker; zeros(1,n_markers)]); + re_markers = gl_to_re * bsxfun(@plus, -gl_origin, gl_markers); + + re_marker_ary{c} = [re_marker_ary{c}, re_markers]; + end +end + +% find markers in camera 1 only, camera 2 only and both +re_marker_c1 = setdiff(re_marker_ary{1}', re_marker_ary{2}', 'rows')'; +re_marker_c2 = setdiff(re_marker_ary{2}', re_marker_ary{1}', 'rows')'; +re_marker_c12 = intersect(re_marker_ary{1}', re_marker_ary{2}', 'rows')'; + +%% plot +figure(10); +clf; +hold on; + +% plot markers +plot3(re_marker_c1(1,:), re_marker_c1(2,:), re_marker_c1(3,:), '.', 'color', [1 0 0]); +plot3(re_marker_c2(1,:), re_marker_c2(2,:), re_marker_c2(3,:), '.', 'color', [0 0 1]); +plot3(re_marker_c12(1,:), re_marker_c12(2,:), re_marker_c12(3,:), '.', 'color', [0.8 0 0.8]); + +% plot corners of reconstruction +re_corners = [re_x_lim([1 1 1 1 2 2 2 2]); + re_y_lim([1 1 2 2 1 1 2 2]); + re_z_lim([1 2 1 2 1 2 1 2])]; +plot3(re_corners(1,[1 2 3 4 1]), re_corners(2, [1 2 3 4 1]), re_corners(3, [1 2 3 4 1]), 'k.-', ... + re_corners(1,[5 6 7 8 5]), re_corners(2, [5 6 7 8 5]), re_corners(3, [5 6 7 8 5]), 'k.-', ... + re_corners(1,[1 3 7 5 1]), re_corners(2, [1 3 7 5 1]), re_corners(3, [1 3 7 5 1]), 'k.-', ... + re_corners(1,[2 4 8 6 2]), re_corners(2, [2 4 8 6 2]), re_corners(3, [2 4 8 6 2]), 'k.-'); + +axis equal tight xy; +view(3); +camup([0 1 0]); + +%xlabel('$x$ / mm', 'interpreter', 'latex', 'fontsize', 20); +%ylabel('$y$ / mm', 'interpreter', 'latex', 'fontsize', 20); +%zlabel('$z$ / mm', 'interpreter', 'latex', 'fontsize', 20); \ No newline at end of file diff --git a/calibration/plot_dewarped.m b/calibration/plot_dewarped.m new file mode 100644 index 0000000000000000000000000000000000000000..07e67165ef1620926395e3b33ee8a63275b9f121 --- /dev/null +++ b/calibration/plot_dewarped.m @@ -0,0 +1,102 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% load a raw image from file and dewarp it with a 2D dewarping specified +% in the provided calibration file + +%% input parameters + +img_file = '/data/cluster/160309/9219-003.TIF'; +calib_file = '/data/cluster/160309/CAM1-01.mat'; +x0 = [7, -2]; + +%% load calibration +fs = importdata(calib_file); +P2D = fs.P; +i_axis = fs.i_axis; +j_axis = fs.j_axis; +x_axis = fs.x_axis; +y_axis = fs.y_axis; +Ni = length(i_axis); +Nj = length(j_axis); +grid_dx = fs.grid_dx; +grid_dy = fs.grid_dy; + +%% load image and dewarp +[xmat,ymat] = ndgrid(x_axis, y_axis); +[imat_xy,jmat_xy] = p2d_dewarp(P2D, x_axis, y_axis); +im_ij = double(fliplr(imread(img_file)')); +im_xy = interp2(i_axis, j_axis, im_ij', imat_xy, jmat_xy, 'linear', 0); + +G = fspecial('gaussian', [33 33], 3); +im_filt = filter2(G, im_xy); + +%% cut a small subregion out +x_prime = x_axis - x0(1); +y_prime = y_axis - x0(2); +xrange = abs(x_prime) < 30; +yrange = abs(y_prime) < 30; +im_cut = im_filt(xrange,yrange); +x_cut = x_prime(xrange); +y_cut = y_prime(yrange); +[xmc, ymc] = ndgrid(x_cut, y_cut); + +%% plot +figure(1); +S = surf(x_cut, y_cut, im_cut'); set(S, 'edgecolor', 'none'); +axis equal tight xy; +grid on; +colormap jet; + +%% fit gaussian +fitfun = fittype('A*exp(-((x-x0).^2 + (y-y0).^2)/s^2)', ... + 'dependent', 'z', ... + 'indep', {'x','y'}, ... + 'coeff', {'A', 'x0', 'y0', 's'}); + +[fm,good] = fit([xmc(:),ymc(:)], im_cut(:), fitfun, 'StartPoint', startpoint); +im_model = fm(xmc, ymc); +im_res = im_cut - im_model; + +disp(fm); + +%% plot solution +figure(2); +clf; + +subplot(1,2,1); +cla; +hold off; +imagesc(x_cut, y_cut, im_cut'); +hold on; +[C,h] = contour(x_cut, y_cut, im_model'/fm.A, linspace(0, 1, 11)); +clabel(C,h); + +axis equal tight xy; +grid on; +colormap jet; +colorbar; + +subplot(1,2,2); +imagesc(x_cut, y_cut, im_res'); +axis equal tight xy; +grid on; +colormap jet; +colorbar; + +%% get points and draw +%{ +xy_circ = ginput(); +%% +xy_orig = mean(xy_circ, 1); +xy_dist = sqrt(sum(bsxfun(@minus,xy_circ, xy_orig).^2, 2)); +radius = mean(xy_dist); + +theta = linspace(0,2*pi); +cost = cos(theta); +sint = sin(theta); + +hold on; +plot(xy_circ(:,1), xy_circ(:,2), 'rx'); +plot(xy_orig(1), xy_orig(2), 'ro'); +plot(xy_orig(1)+cost*radius, xy_orig(2)+sint*radius, 'r-'); +fprintf('radius = %0.3f mm\n', radius); +%} \ No newline at end of file diff --git a/calibration/plot_dewarped_3d.m b/calibration/plot_dewarped_3d.m new file mode 100644 index 0000000000000000000000000000000000000000..df88480934d02d3d2b39c78c002a7f7b909178f1 --- /dev/null +++ b/calibration/plot_dewarped_3d.m @@ -0,0 +1,51 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% load a raw image from file and dewarp it with a 2D dewarping specified +% in the provided calibration file + +%% input parameters +model_file = '/scratch23/lawson/K62/170118/calib/camera-model-original.mat'; +calib_file = '/scratch23/lawson/K62/170118/calib/CAM2-05.mat'; + +%% load calibration +model = importdata(model_file); +fs = importdata(calib_file); +cam_idx = fs.camera_index; +cam = model.camera(cam_idx); + +i_axis = fs.i_axis; +j_axis = fs.j_axis; +Ni = length(i_axis); +Nj = length(j_axis); + +x_axis = fs.x_axis*2; +y_axis = fs.y_axis*2; +gl_plate = fs.plate_pos; +Nx = length(x_axis); +Ny = length(y_axis); +im_ij = fs.source_image; +grid_dx = fs.grid_dx; +grid_dy = fs.grid_dy; + +%% load image and dewarp +[xmat,ymat,zmat] = ndgrid(x_axis, y_axis, gl_plate(3)); +gl_mat = [xmat(:) ymat(:) zmat(:)]'; +ij_mat = pinhole_model(gl_mat, cam.calib, true); +imat_xy = reshape(ij_mat(1,:), [Nx Ny]); +jmat_xy = reshape(ij_mat(2,:), [Nx Ny]); + +im_xy = interp2(i_axis, j_axis, im_ij', imat_xy, jmat_xy, 'linear', 0); + +G = fspecial('gaussian', [33 33], 3); +im_filt = filter2(G, im_xy); + +%% plot +figure(1); +clf; + +xtick = min(x_axis) : grid_dx : max(x_axis); +ytick = min(y_axis) : grid_dy : max(y_axis); + +imagesc(x_axis, y_axis, im_xy'); +axis equal tight xy; +set(gca, 'xtick', xtick, 'ytick', ytick); +grid on; \ No newline at end of file diff --git a/calibration/plot_marker_search.m b/calibration/plot_marker_search.m new file mode 100644 index 0000000000000000000000000000000000000000..3511ceb44653be0e7ba5bb24f764c9945cda4ced --- /dev/null +++ b/calibration/plot_marker_search.m @@ -0,0 +1,40 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% make a simple plot of the markers identified by search +% for a given calibration +addpath([pwd '/../misc']); +calib_file = 'D:\bigtank-jul-2013\CAL-20-07-13\CAMERA\CAM1-11.mat'; + +%% load +fs = importdata(calib_file); +i_axis = fs.i_axis; +j_axis = fs.j_axis; +ij_markers = fs.ij_markers; +ij_center = fs.ij_center; +ij_right = fs.ij_right; +ij_top = fs.ij_top; +uv_marker = fs.xy_markers; +im_source = fs.source_image; + +%% plot +draw_arrow = @(x,y,varargin) quiver( x(1),y(1),x(2)-x(1),y(2)-y(1),0, varargin{:} ); + + +figure(1); +clf; +imagesc(i_axis, j_axis, im_source'); +axis equal tight xy; +colormap gray; + +hold on; +plot(ij_markers(1,:), ij_markers(2,:), 'rx', 'markersize', 4); +plot(ij_center(1), ij_center(2), 'ro', 'MarkerSize', 6); +plot(ij_right(1), ij_right(2), 'bo', 'MarkerSize', 6); +plot(ij_top(1), ij_top(2), 'go', 'MarkerSize', 6); + +%clim([0 1024]); +set(gca, 'xtick', []); +set(gca, 'ytick', []); +%annotation('arrow', [ij_center(1) ij_right(1)], [ij_center(2) ij_right(2)], '-', 'Color', [0 0 1]); +%annotation('arrow', [ij_center(1) ij_top(1)], [ij_center(2) ij_top(2)], '-', 'Color', [0 1 0]); + + diff --git a/calibration/plot_residuals.m b/calibration/plot_residuals.m new file mode 100644 index 0000000000000000000000000000000000000000..c2410d2e8485192012485ff4744dc672e5d1f19c --- /dev/null +++ b/calibration/plot_residuals.m @@ -0,0 +1,16 @@ +file_name_pat = '/data/050216/calib/CAM1-%02d.mat'; + +for n = 1 : 11 + file_name = sprintf(file_name_pat, n); + load(file_name); + + ij_pred = p2d_map_obj2im(P, xy_markers); + res = ij_pred-ij_markers; + + figure(1); + clf; + quiver(ij_markers(1,:),ij_markers(2,:), res(1,:),res(2,:), 'b-'); + axis equal tight xy; + + pause(); +end \ No newline at end of file diff --git a/calibration/plot_residuals_3d.m b/calibration/plot_residuals_3d.m new file mode 100644 index 0000000000000000000000000000000000000000..4610e2fb4c8addd3598717ae8282a8d6a077b806 --- /dev/null +++ b/calibration/plot_residuals_3d.m @@ -0,0 +1,176 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% plot residuals of calibration in 3D + +addpath([pwd '/pinhole/']); + +%% input parameters +calib_file_pat = '/scratch23/lawson/K62/170118/calib/CAM%d-%02d.mat'; +camera_model_file = '/scratch23/lawson/K62/170118/calib/camera-model-original.mat'; +font_size = 20; +DO_DEBUG_PLOT = false; + +%% load model +setup = importdata(camera_model_file); + +% camera calibration +n_cameras = setup.n_cameras; +n_views = setup.n_views; + +% plate pose +gl_plate_pos = setup.gl_plate_pos; +pl_to_gl_mat = repmat(setup.pl_to_gl, [1 1 n_views]); +camera_model = setup.camera; +gl_to_re = eye(3); +gl_origin = setup.gl_origin; +r_sphere = min(setup.r_sphere); +n_views = setup.n_views; + +%% calculate coordinates of inscribed sphere +theta = linspace(0, pi, 21); +phi = linspace(0, 2*pi, 41); +[tmat,pmat] = ndgrid(theta,phi); +en_sphere = [sin(tmat(:)').*cos(pmat(:)'); + sin(tmat(:)').*sin(pmat(:)'); + cos(tmat(:)')]; +gl_sphere = bsxfun(@plus, r_sphere * en_sphere, gl_origin); + +%% triangulate markers from plate +gl_markers_ref = []; +gl_markers_tri = []; +ij_res_ary = cell(1, n_cameras); + +for v = 1 : n_views + %% load markers + fprintf('View %d\n', v); + ij_markers = cell(n_cameras, 1); + uv_markers = cell(n_cameras, 1); + n_markers = zeros(n_cameras, 1); + for c = 1 : n_cameras + fname = sprintf(calib_file_pat, c, v); + fs = importdata(fname); + ij_markers{c} = fs.ij_markers; + uv_markers{c} = fs.xy_markers; + n_markers(c) = size(fs.ij_markers,2); + + fprintf(' c%d: %d markers\n', c, n_markers(c)); + end + + %% identify common markers + uv_common = uv_markers{1}; + for c = 2 : n_cameras + + uv_common = intersect(uv_common.', uv_markers{c}.', 'rows').'; + end + n_common = size(uv_common, 2); + ij_common = zeros(2, n_common, n_cameras); + for c = 1 : n_cameras + [~, ia, ib] = intersect(uv_markers{c}.', uv_common.', 'rows'); + ij_common(:,:,c) = ij_markers{c}(:, ia); + end + fprintf(' %d common markers\n', n_common); + + %% global coords of common markers + pl_to_gl = pl_to_gl_mat(:,:,v); + gl_plate = gl_plate_pos(:,v); + gl_common = mtimesx(pl_to_gl, [uv_common; zeros(1,n_common)]); + gl_common = bsxfun(@plus, gl_plate, gl_common); + + %% triangulate + [gl_tri, gl_res, ij_res] = triangulate(setup.camera, ij_common, true); + + %% add to array + gl_markers_tri = [gl_markers_tri, gl_tri]; + gl_markers_ref = [gl_markers_ref, gl_common]; + for c = 1 : n_cameras + ij_res_ary{c} = [ij_res_ary{c}; ij_res(:,c)]; + end + + %% plot common markers + style = {'r.', 'g.', 'b.'}; + if DO_DEBUG_PLOT + figure(1); + clf; + plot(uv_common(1,:), uv_common(2,:), 'ko'); + hold on; + for c = 1 : n_cameras + plot(uv_markers{c}(1,:), uv_markers{c}(2,:), style{c}); + end + axis equal tight xy; + make_plot_pretty('$u$ [mm]', '$v$ [mm]', '', 20); + end +end +ij_res_ary = cell2mat(ij_res_ary); + +%% calculate disparity +gl_disp = gl_markers_tri - gl_markers_ref; +ij_rms = rms(ij_res_ary, 1); +ij_max = max(ij_res_ary, [], 1); +gl_rms = rms(gl_disp, 2); +gl_max = max(abs(gl_disp), [], 2); +gl_mean = mean(gl_disp, 2); + +%% report +fprintf('Disparity after triangulation:\n'); +fprintf(' c%d: %0.3f px rms %0.3f px max\n', [1:n_cameras; ij_rms; ij_max]); +fprintf(' x: %6.3f um rms %6.3f um max %+6.3f um mean\n', 1000*gl_rms(1), 1000*gl_max(1), 1000*gl_mean(1)); +fprintf(' y: %6.3f um rms %6.3f um max %+6.3f um mean\n', 1000*gl_rms(2), 1000*gl_max(2), 1000*gl_mean(2)); +fprintf(' z: %6.3f um rms %6.3f um max %+6.3f um mean\n', 1000*gl_rms(3), 1000*gl_max(3), 1000*gl_mean(3)); + +%% plot markers and their triangulation +quiver_scale = 40; +figure(2); +clf; +quiver3sc( gl_markers_ref(1,:), gl_markers_ref(2,:), gl_markers_ref(3,:), ... + gl_disp(1,:), gl_disp(2,:), gl_disp(3,:), quiver_scale, 'k-'); +hold on; +plot3(gl_sphere(1,:), gl_sphere(2,:), gl_sphere(3,:), 'b.'); + +axis equal tight; +xlabel('$x$ [mm]', 'interpreter', 'latex', 'fontsize', font_size); +ylabel('$y$ [mm]', 'interpreter', 'latex', 'fontsize', font_size); +zlabel('$z$ [mm]', 'interpreter', 'latex', 'fontsize', font_size); + +%% triangulate markers using calibration +error('stop'); + +gl_ref = []; +gl_meas = []; +ij_meas = []; + +for v = 1 : n_views + %% get plate, image and world coordinates of markers + pl_to_gl = pl_to_gl_mat(:,:,v); + gl_pos = gl_plate_pos(:,v); + uv_mark = camera.view(v).uv_markers; + n_mark = size(uv_mark, 2); + gl_mark = bsxfun(@plus, gl_pos, pl_to_gl*[uv_mark; zeros(1,n_mark)]); + ij_mark = camera.view(v).ij_markers; + + %% calculate equation of plane + n = pl_to_gl(:,3); + d = -dot(gl_pos, n); + M = [n; d]; + + %% back-project + gl_proj = back_project_1c(camera.calib, M, ij_mark, true); + + %% save + gl_ref = [gl_ref , gl_mark]; + gl_meas = [gl_meas, gl_proj]; + ij_meas = [ij_meas, ij_mark]; +end + +% calculate disparity +gl_disp = gl_meas - gl_ref; + +%% plot +quiver_scale = 10; +figure(1); +clf; +quiver3sc( gl_ref(1,:) , gl_ref(2,:) , gl_ref(3,:) , ... + gl_disp(1,:), gl_disp(2,:), gl_disp(3,:), quiver_scale, 'k-'); +axis equal tight xy; + +xlabel('x / mm'); +ylabel('y / mm'); +zlabel('z / mm'); diff --git a/calibration/plot_traverse_plate_alignment.m b/calibration/plot_traverse_plate_alignment.m new file mode 100644 index 0000000000000000000000000000000000000000..d883b5fe29d253f928e359c58d86c1a0e08111c9 --- /dev/null +++ b/calibration/plot_traverse_plate_alignment.m @@ -0,0 +1,60 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% make a pretty plot for thesis of the traverse/plate alignment +addpath([pwd '/../misc/']); +trav_x = [-30 +-25 +-20 +-15 +-10 +-5 +0 +5 +10 +15 +20 +25 +30]; +trav_z1 = [-0.17 +-0.17 +-0.18 +-0.19 +-0.19 +-0.21 +-0.23 +-0.25 +-0.26 +-0.27 +-0.26 +-0.26 +-0.28]; +trav_z2 = [-0.2 +-0.19 +-0.16 +-0.14 +-0.15 +-0.16 +-0.16 +-0.17 +-0.18 +-0.17 +-0.16 +-0.15 +-0.16]; +trav_z = (trav_z1 + trav_z2)/2; +p = polyfit(trav_x, trav_z, 1); + +figure(1); +clf; +plot( trav_x, trav_z1, 'ko', ... + trav_x, trav_z2, 'kx', ... + trav_x, polyval(p,trav_x),'k-'); +L = legend('Repeat 1', 'Repeat 2', 'Fit', 'Location', 'North'); +set(L, 'interpreter', 'latex', 'fontsize', 20); +xlim([-30 30]); +ylim([-1 1]); +set(gca, 'ytick', -1:0.2:1); +grid on; +daspect([15 1 1]); + +make_plot_pretty('$x$ position / mm', ... + '$z$ position / mm', '', 20); diff --git a/calibration/poly/polyfit2d3.m b/calibration/poly/polyfit2d3.m new file mode 100644 index 0000000000000000000000000000000000000000..fcd9c9e8f22e3b24957dcf9167dd703de9f5e6f9 --- /dev/null +++ b/calibration/poly/polyfit2d3.m @@ -0,0 +1,26 @@ +function P = polyfit2d3(u, uprime) + % P = polyfit2d(u, uprime) + % find best fit coefficients P for the polynomial map u' = f(u; P) + % + % f(u) is a 2D, 3rd order polynomial, defined in terms of 20 polynomial + % coefficients P_ij, 10 for each component + % + % f_0(x,y) = P_00 + P_01 x + P_13 y + P_04 x^2 + P_05 xy + P_06 y^2 + P_07 x^3 + P_08 x^2y + P_09 xy^2 + P_09 y^3 + % f_1(x,y) = P_10 + P_11 x + P_13 y + P_14 x^2 + P_15 xy + P_16 y^2 + P_17 x^3 + P_18 x^2y + P_19 xy^2 + P_19 y^3 + % + + %% extract parameters + N = size(u,2); + x = u(1,:); + y = u(2,:); + X = uprime(1,:); + Y = uprime(2,:); + + %% least squares fit + T = [ones(1,N); x; y; x.^2; x.*y; y.^2; x.^3; x.*x.*y; x.*y.*y; y.^3].'; + P0 = T \ X(:); + P1 = T \ Y(:); + + P = [P0.'; P1.']; +end + \ No newline at end of file diff --git a/calibration/poly/polyfit2d4.m b/calibration/poly/polyfit2d4.m new file mode 100644 index 0000000000000000000000000000000000000000..fb98e830fbe71eea82f7bcdd814a9fda668bb403 --- /dev/null +++ b/calibration/poly/polyfit2d4.m @@ -0,0 +1,30 @@ +function P = polyfit2d4(u, uprime) + % P = polyfit2d(u, uprime) + % find best fit coefficients P for the polynomial map u' = f(u; P) + % + % f(u) is a 2D, 3rd order polynomial, defined in terms of 20 polynomial + % coefficients P_ij, 10 for each component + % + % f_0(x,y) = P_00 + P_01 x + P_13 y + P_04 x^2 + P_05 xy + P_06 y^2 + P_07 x^3 + P_08 x^2y + P_09 xy^2 + P_09 y^3 + % f_1(x,y) = P_10 + P_11 x + P_13 y + P_14 x^2 + P_15 xy + P_16 y^2 + P_17 x^3 + P_18 x^2y + P_19 xy^2 + P_19 y^3 + % + + %% extract parameters + N = size(u,2); + x = u(1,:); + y = u(2,:); + X = uprime(1,:); + Y = uprime(2,:); + + %% least squares fit + T = [ones(1,N); + x; y; + x.^2; x.*y; y.^2; x.^3; + x.*x.*y; x.*y.*y; y.^3; + x.^4; x.^3.*y; x.^2.*y.^2; x.*y.^3; y.^4].'; + P0 = T \ X(:); + P1 = T \ Y(:); + + P = [P0.'; P1.']; +end + \ No newline at end of file diff --git a/calibration/poly/polymap2d3.m b/calibration/poly/polymap2d3.m new file mode 100644 index 0000000000000000000000000000000000000000..3d1faf39d4c1c271196a5f5469bb82abcb99276f --- /dev/null +++ b/calibration/poly/polymap2d3.m @@ -0,0 +1,31 @@ +function X = polymap2d3(P, u) + % X = polymap2d3(P, u) + % + % apply the 2D, 3rd order polynomial mapping X = f(x; P) + % to a set of points x, with polynomial coefficients P + % + % Input: + % x 2 x N array of coordinates + % P 2 x 10 array of polynomial coefficients + % + % Output: + % X 2 x N array of transformed coordinates + % + % The polynomial is a simple 2D, 3rd order mapping + % + % X_0 = P_00 + P_01 x + P_13 y + P_04 x^2 + P_05 xy + P_06 y^2 + P_07 x^3 + P_08 x^2y + P_09 xy^2 + P_09 y^3 + % X_1 = P_10 + P_11 x + P_13 y + P_14 x^2 + P_15 xy + P_16 y^2 + P_17 x^3 + P_18 x^2y + P_19 xy^2 + P_19 y^3 + + N = size(u, 2); + + x = u(1,:); + y = u(2,:); + X = P(1,1) + P(1,2)*x + P(1,3)*y ... + + P(1,4)*x.^2 + P(1,5)*x.*y + P(1,6)*y.^2 ... + + P(1,7)*x.^3 + P(1,8)*x.^2.*y + P(1,9)*x.*y.^2 + P(1,10)*y.^3; + Y = P(2,1) + P(2,2)*x + P(2,3)*y ... + + P(2,4)*x.^2 + P(2,5)*x.*y + P(2,6)*y.^2 ... + + P(2,7)*x.^3 + P(2,8)*x.^2.*y + P(2,9)*x.*y.^2 + P(2,10)*y.^3; + + X = [X; Y]; +end \ No newline at end of file diff --git a/calibration/poly/polymap2d4.m b/calibration/poly/polymap2d4.m new file mode 100644 index 0000000000000000000000000000000000000000..c6f55387d89381591fff3f0e39eb6c4d728e8c8d --- /dev/null +++ b/calibration/poly/polymap2d4.m @@ -0,0 +1,33 @@ +function X = polymap2d4(P, u) + % X = polymap2d3(P, u) + % + % apply the 2D, 3rd order polynomial mapping X = f(x; P) + % to a set of points x, with polynomial coefficients P + % + % Input: + % x 2 x N array of coordinates + % P 2 x 10 array of polynomial coefficients + % + % Output: + % X 2 x N array of transformed coordinates + % + % The polynomial is a simple 2D, 3rd order mapping + % + % X_0 = P_00 + P_01 x + P_13 y + P_04 x^2 + P_05 xy + P_06 y^2 + P_07 x^3 + P_08 x^2y + P_09 xy^2 + P_09 y^3 + % X_1 = P_10 + P_11 x + P_13 y + P_14 x^2 + P_15 xy + P_16 y^2 + P_17 x^3 + P_18 x^2y + P_19 xy^2 + P_19 y^3 + + N = size(u, 2); + + x = u(1,:); + y = u(2,:); + X = P(1,1) + P(1,2)*x + P(1,3)*y ... + + P(1,4)*x.^2 + P(1,5)*x.*y + P(1,6)*y.^2 ... + + P(1,7)*x.^3 + P(1,8)*x.^2.*y + P(1,9)*x.*y.^2 + P(1,10)*y.^3 ... + + P(1,11)*x.^4 + P(1,12)*x.^3.*y + P(1,13)*x.^2.*y.^2 + P(1,14)*x.*y.^3 + P(1,15)*y.^4; + Y = P(2,1) + P(2,2)*x + P(2,3)*y ... + + P(2,4)*x.^2 + P(2,5)*x.*y + P(2,6)*y.^2 ... + + P(2,7)*x.^3 + P(2,8)*x.^2.*y + P(2,9)*x.*y.^2 + P(2,10)*y.^3 ... + + P(2,11)*x.^4 + P(2,12)*x.^3.*y + P(2,13)*x.^2.*y.^2 + P(2,14)*x.*y.^3 + P(2,15)*y.^4; + + X = [X; Y]; +end \ No newline at end of file diff --git a/calibration/refraction_correction.m b/calibration/refraction_correction.m new file mode 100644 index 0000000000000000000000000000000000000000..97d770aad71962c9ba521f9dd4d9348a09277380 --- /dev/null +++ b/calibration/refraction_correction.m @@ -0,0 +1,61 @@ +%% calculate +t_over_r = linspace(0, 0.1, 100); +phi1 = linspace(0, 60 / 180*pi, 61); +phi2 = linspace(0, 60 / 180*pi, 61); +n1 = 1.00; +n2 = 1.52; + +[phi1_mat, t_mat] = ndgrid(phi1, t_over_r); +sin_phi1 = sin(phi1_mat); +tan_phi1 = tan(phi1_mat); +sin_phi2 = n1/n2 * sin_phi1; +phi2_mat = asin(sin_phi2); +tan_phi2 = tan(phi2_mat); + +tan_ratio = @(phi1) n1 / n2 * sqrt((1 - sin(phi1).^2) ./ (1 - n1^2/n2^2*sin(phi1).^2)); + +dilation = (1 + t_mat) ./ (1 + t_mat .* tan_ratio(phi1_mat)); +%dilation(1,:) = 1; + +%% plot +figure(1); +c_levels = 0 : 0.2 : 6; +[c,h] = contour(phi1 * 180/pi, t_over_r, (dilation'-1)*100, c_levels, 'k-'); +clabel(c,h,'LabelSpacing',300); +axis tight xy; + +xlabel('Camera angle $\Theta / ^\circ$', 'interpreter', 'latex', 'fontsize', 20); +ylabel('Thickness $t/r$', 'interpreter', 'latex', 'fontsize', 20); +ylim(t_over_r([1 end])); +xlim(phi1([1 end])*180/pi); +titstr = sprintf('Percentage dilation $100(\\Delta'' / \\Delta - 1)$ for $n_1 = %0.2f$ $n_2 = %0.2f$', n1, n2); +title(titstr, 'interpreter', 'latex', 'fontsize', 20); + +set(gca, 'clim', c_levels([1 end])); + +%% calculate +t_over_r = 0.01; +[phi1_mat, phi2_mat] = ndgrid(phi1, phi2); +dil21 = (1 + t_over_r * tan_ratio(phi2_mat)) ./ (1 + t_over_r * tan_ratio(phi1_mat)); + +%% plot +figure(2); +clf; + +c_levels = -1 : 0.02 : 1; +[c,h] = contour(phi1 * 180/pi, phi2 * 180/pi, (dil21'-1)*100, c_levels, 'k-'); +clabel(c,h,'LabelSpacing',300); +axis equal tight xy; + +hold on; +for fov = -20 : 5 : 20 + plot(phi1 / pi*180, phi1/pi*180 + fov, 'r--'); +end + +xlabel('$\Theta_1 / ^\circ$', 'interpreter', 'latex', 'fontsize', 20); +ylabel('$\Theta_2 / ^\circ$', 'interpreter', 'latex', 'fontsize', 20); + +xlim(phi1([1 end])*180/pi); +ylim(phi2([1 end])*180/pi); +titstr = sprintf('Relative dilation $100(\\Delta_2''/\\Delta_1'' - 1)$ for $n_1 = %0.2f$ $n_2 = %0.2f$, $t/r = %0.2f$', n1, n2, t_over_r); +title(titstr, 'interpreter', 'latex', 'fontsize', 20); diff --git a/calibration/search_markers.m b/calibration/search_markers.m new file mode 100644 index 0000000000000000000000000000000000000000..0ff168c6c328795a460fdb4e2a60a3527d295bf1 --- /dev/null +++ b/calibration/search_markers.m @@ -0,0 +1,46 @@ +function [xy_match, ij_match, ij_pred] = search_markers(xy_pred, ij_markers, thresh, proj_type, varargin) + % [xy_match, ij_match] = search_markers(xy_pred, ij_markers, proj_type, varargin) + % + % Given a list of markers in image space ij_markers, search for + % those in this set that best correspond to grid locations xy_pred + % and are within a given distance thresh (in image space) + % + % + % Grid locations in xy space are mapped onto image space using either: + % * proj_type = 'affine' + % an affine transformation determined from the coordinates + % of the center, top and right markers (ij_center,ij_top,ij_right) + % * proj_type = 'pinhole' + % a 2D pinhole model P2D + % + + %% project prediction of marker coord into image space + n_pred = size(xy_pred, 2); + if strcmpi(proj_type, 'affine') + % affine transformation + A = varargin{1}; + b = varargin{2}; + ij_pred = bsxfun(@plus, A * xy_pred, b); + elseif strcmpi(proj_type, 'pinhole') + % 2D pinhole model + P2D = varargin{1}; + ij_pred = p2d_map_obj2im(P2D, xy_pred); + end + + %% search for nearest matching marker + xy_match = xy_pred; + ij_match = zeros(2, n_pred); + d_match = zeros(1, n_pred); + for n = 1 : n_pred + dist = sqrt(sum(bsxfun(@minus, ij_markers, ij_pred(:, n)).^2, 1)); + [mindist,imin] = min(dist); + ij_match(:,n) = ij_markers(:, imin); + d_match(n) = mindist; + end + + %% eliminate those outside threshold + ok = d_match < thresh; + xy_match = xy_match(:, ok); + ij_match = ij_match(:, ok); + ij_pred = ij_pred(:, ok); +end diff --git a/calibration/sheet_calibration.m b/calibration/sheet_calibration.m new file mode 100644 index 0000000000000000000000000000000000000000..933ba8f55c4d8208e4089dcdbadccd3b8aec140a --- /dev/null +++ b/calibration/sheet_calibration.m @@ -0,0 +1,280 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% sheet_calibration.m +% +% calibration using bundle method to determine the position and thickness +% of laser sheets, from images of laser sheets projected onto a calibration +% target + +%% input parameters + +num_sheets = 50; % number of laser sheets + +calib_dir = 'D:/bigtank-jul-2013/CAL-20-07-13/NEWSHEET/'; +cam_model_file = [calib_dir '/camera-model-280415.mat']; +cam_idx = 1; +calib_file_pat = [calib_dir '/C-CAM1-%02d.mat']; +sheet_img_file_pat= [calib_dir '/../SHEET-%02d/CAM1-volume-01-frame-%02d.tif']; +sheet_model_file = [calib_dir '/sheet-model-280415.mat']; + +calib_file_range = 1 : 10; +num_set_points = length(calib_file_range); + +% additional tweaking stuff +max_thickness = 10; % max sheet thickness +v_min = -60; % min v coord for line fit +v_max = 60; % max v coord for line fit +plate_du = 10; % distance between dots in u direction +plate_dv = 10; % and in v direction + +DO_DEBUG_PLOT = false; + +%% load camera model and calibration setup +calibration_setup = importdata(cam_model_file); +gl_plate_pos = calibration_setup.gl_plate_pos; +pl_to_gl = calibration_setup.pl_to_gl; +camera_setup = calibration_setup.camera(cam_idx); +camera_calib = calibration_setup.camera(cam_idx).calib; +i_axis = camera_setup.i_axis; +j_axis = camera_setup.j_axis; +Ni = length(i_axis); +Nj = length(j_axis); + +%% find intersections with laser sheet + +fprintf('------------------------------------------------------------------\n'); +fprintf('LASER SHEET CALIBRATION:\n'); + +% matrix which converts from plate to global coordinate system +% [x;y;z] = pl_to_gl * [u;v;w] + [xp; yp; zp] +plate_eu = pl_to_gl(:,1); +plate_ev = pl_to_gl(:,2); +plate_ew = pl_to_gl(:,3); + +m_mat = nan(num_sheets, num_set_points); % m = dot(ev,er) / dot(eu, er) +c_mat = nan(num_sheets, num_set_points); % c is the u coordinate at v = 0 along the line of intersection +k_mat = nan(num_sheets, num_set_points); % k = dot(ex,er) / dot(ez, er) +w_vec = zeros(1, num_sheets); % w is e^-2 sheet width +w_mat = nan(num_sheets, num_set_points); + +gl_eZ_mat = zeros(num_sheets, 3); % sheet normal vector in global c sys +gl_xs_mat = zeros(num_sheets, 3); % sheet origin in global c sys +gl_on_sheet = nan(num_sheets, 10000, 3); + +% prepare figure +figure(1); +clf; + +for sheet_idx = 1:num_sheets + %% report + fprintf('Calculating plane of sheet %03d of %03d\n', sheet_idx, num_sheets); + + %% find intersection for each set point + Nrows = 12; + w_proj_vec = nan(1, num_set_points); + gl_on_this_sheet = nan(3, Nrows, num_set_points); + + for set_point = 1 : num_set_points + %% load plate image + calib_file_idx = calib_file_range(set_point); + image_fname = sprintf(sheet_img_file_pat, calib_file_idx, sheet_idx); + sheet_img_raw = double( fliplr(imread(image_fname)') ); + + %% get u,v axes from original calibration + calib_fname = sprintf(calib_file_pat, calib_file_idx); + original_calib = importdata(calib_fname); + u = original_calib.x_axis; + v = original_calib.y_axis; + + %% dewarp + plate_pos = gl_plate_pos(:,set_point); + [umat,vmat] = ndgrid(u,v); + gl_px_mat = bsxfun(@plus, plate_pos, pl_to_gl*[umat(:) vmat(:) zeros(Ni*Nj,1)]'); + ij_px_mat = pinhole_model(gl_px_mat, camera_calib); + imat = reshape(ij_px_mat(1,:), [Ni Nj]); + jmat = reshape(ij_px_mat(2,:), [Ni Nj]); + sheet_img = interp2(i_axis, j_axis, sheet_img_raw', imat, jmat); + + %% find best fit of intersection to line u = mv + c + [line_coeffs, w_proj, line_u, line_v] = find_sheet_width(u, v, sheet_img, [v_min v_max], [plate_du plate_dv]); + m = line_coeffs(1); + c = line_coeffs(2); + bFound = true; + if isnan(w_proj) + bFound = false; + end + + if bFound + % work out object space coords of points on sheet + % save them to matrix + gl_plate_origin = [plate_pos(1), 0, plate_pos(2)]'; + pl_on_sheet = [line_u(:) line_v(:) zeros(Nrows,1)]'; + gl_on_this_sheet(:,:,set_point) = (pl_to_gl * pl_on_sheet) + gl_plate_origin*ones(1,Nrows); + % save basic results of fit + m_mat(sheet_idx, set_point)= m; + c_mat(sheet_idx, set_point)= c; + w_proj_vec(set_point) = w_proj; + w_mat(sheet_idx, set_point)= w_proj; + end + %% plot + if DO_DEBUG_PLOT + figure(11); + clf; + row = 4; + %subplot1(1,2,'Gap',[0.06 0.05]); + %subplot1(1); + + %% image of laser sheet + imagesc(u,v,sheet_img'); + colormap gray; + clim([0 1024]); + axis equal tight xy; + hold on; + plot(polyval(line_coeffs, v([1 end])), v([1 end]), 'r-'); + plot(line_u, line_v, 'ro'); + plot(u([1 end]), line_v(row)*[1 1], 'w:'); + make_plot_pretty('u / mm', 'v / mm', '', 20); + + %% profile of laser sheet at different u + %subplot1(2); + figure(12); + clf; + hold on; + fit_object = fittype('A * exp(-8*(x-x0)^2/w^2)', 'coefficients', {'A','x0','w'}, 'independent', 'x'); + + [~,jj] = min(abs(v - line_v(row))); + in_range = abs(u - line_u(row)) < w_proj/2; + row_u = u(in_range); + row_profile = sheet_img(in_range, jj); + amp = max(row_profile); + fm = fit(row_u(:), row_profile(:), fit_object, 'startpoint', [amp, line_u(row), w_proj]); + plot(u, sheet_img(:,jj), 'k.'); + plot(u, fm(u), 'r-', 'linewidth', 2); + plot(row_u(1 )*[1 1], [0 700], 'k:', ... + row_u(end)*[1 1], [0 700], 'k:'); + + axis square; + make_plot_pretty('u / mm', 'Intensity / counts', '', 20); + xlim(round(line_u(1) + [-10 10])); + ylim([0 700]); + set(gca,'ytick', 0 : 100 : 700); + legend('Measurement', 'Fit', 'Fit range'); + end + end + + %% find eZ, the sheet normal vector + % use a least squares fit to the equation of plane + % c = dot(x_p, gl_eZ) + Np = size(gl_on_this_sheet,2) * size(gl_on_this_sheet,3); + gl_on_this_sheet = reshape(gl_on_this_sheet, [3 Np]); + % repeatedly apply fit, excluding outliers + gl_on_sheet_filt = gl_on_this_sheet(:, ~isnan(gl_on_this_sheet(1,:))); + for fit_iter = 1:3 + % apply fit + Np = size(gl_on_sheet_filt, 2); + [M,~,res] = lsqlin(gl_on_sheet_filt', ones(Np,1), [], []); + % identify outliers + res = res.^2; + res_thresh = prctile(res, 95); + keep = res < res_thresh; + gl_on_sheet_filt = gl_on_sheet_filt(:, keep); + end + % fit solves M(1)*x + M(2)*y + M(3)*z = 1 for the best M + gl_eZ = M / norm(M); + gl_eZ = gl_eZ * sign(gl_eZ(1)); + gl_pos = M / norm(M)^2; + + gl_xs_mat(sheet_idx, :) = gl_pos; + gl_eZ_mat(sheet_idx, :) = gl_eZ; + + Np = size(gl_on_sheet_filt, 2); + gl_on_sheet(sheet_idx, 1:Np, :) = gl_on_sheet_filt'; + + %% find sheet width + sin_theta = sqrt(1 - dot(gl_eZ, plate_ew)^2); + esq_width = w_mat(sheet_idx, :); + esq_width = mean(esq_width(~isnan(esq_width))); + esq_width = esq_width * sin_theta; + w_vec(sheet_idx) = esq_width; + + %% report + fprintf(' e_r = [%0.3f %0.3f %0.3f]\n', gl_eZ(1), gl_eZ(2), gl_eZ(3)); + fprintf(' x_p = [%0.3f %0.3f %0.3f]\n', gl_pos(1), gl_pos(2), gl_pos(3)); + fprintf(' w = %0.3f mm\n', w_vec(sheet_idx)); + + %% plot points + figure(1); + %plot3(gl_on_sheet(1,:), gl_on_sheet(2,:), gl_on_sheet(3,:), 'k.'); + hold on; + plot3(gl_on_sheet_filt(1,:), gl_on_sheet_filt(2,:), gl_on_sheet_filt(3,:), 'r.'); + + gl_pop_x = [-1 -1 +1 +1]*25 - 5; + gl_pop_y = [-1 +1 +1 -1]*60; + gl_pop_z = (dot(gl_pos,gl_eZ) - gl_eZ(1)*gl_pop_x - gl_eZ(2)*gl_pop_y) / gl_eZ(3); + S = fill3(gl_pop_x, gl_pop_y, gl_pop_z, 'green'); + set(S, 'FaceAlpha', 0.15, 'EdgeAlpha', 0); +end + +gl_on_sheet = reshape(gl_on_sheet, [numel(gl_on_sheet)/3 3]); +keep = ~isnan(gl_on_sheet(:,1)); +gl_on_sheet = gl_on_sheet(keep, :)'; + +%% pretty plot of markers +figure(1); +clf; +% draw points on sheet +plot3(gl_on_sheet(1,:), gl_on_sheet(2,:), gl_on_sheet(3,:), 'r.'); +hold on; +% draw sheets +S_patches = zeros(num_sheets,1); +for k = 1 : num_sheets + gl_eZ = gl_eZ_mat(k,:); + gl_pos = gl_xs_mat(k,:); + gl_pop_x = [-1 -1 +1 +1]*25 - 5; + gl_pop_y = [-1 +1 +1 -1]*60; + gl_pop_z = (dot(gl_pos,gl_eZ) - gl_eZ(1)*gl_pop_x - gl_eZ(2)*gl_pop_y) / gl_eZ(3); + S_patches(k) = fill3(gl_pop_x, gl_pop_y, gl_pop_z, 'green'); +end +set(S_patches, 'EdgeColor', 'none', 'FaceAlpha', 0.10); + +%figure(1); +xlabel('x'); +ylabel('y'); +zlabel('z'); +axis equal tight xy; + +%% find sheet spacing + +gl_eZ = mean(gl_eZ_mat, 1)'; +esq_width = mean(w_vec); +sheet_dist = gl_xs_mat * gl_eZ; +scan_coeffs = polyfit(1:num_sheets, sheet_dist', 1); +scan_dz = abs(scan_coeffs(1)); +position_error = (sheet_dist' - polyval(scan_coeffs, 1:num_sheets)); +std_position_error = std(position_error); + +figure(2); +plot(1 : num_sheets, sheet_dist, 'b.', ... + 1 : num_sheets, polyval(scan_coeffs,1:num_sheets), 'k-'); +xlabel('Sheet index'); +ylabel('Sheet distance / mm'); + +%% report + +fprintf('\n'); +fprintf('Mean sheet spacing: %0.2f mm\n', scan_dz); +fprintf('RMS error in sheet position: %0.4f mm\n', std_position_error); +fprintf('RMS error as percentage of dx: %0.2f %%\n', std_position_error / scan_dz * 100); +fprintf('Mean sheet thickness: %0.2f mm\n', esq_width); +fprintf('w / dz %0.2f\n', esq_width / scan_dz); + +%% save to file +fprintf(' - Saving calibration to file ... '); +fstruct = struct('n_sheets', num_sheets, ... + 'plate_eu', plate_eu, 'plate_ev', plate_ev, 'plate_ew', plate_ew, ... + 'gl_sheet_normal', gl_eZ_mat, ... + 'gl_sheet_pos', gl_xs_mat, ... + 'sheet_width', w_vec, ... + 'gl_on_sheet', gl_on_sheet); +save(sheet_model_file, '-struct', 'fstruct'); +fprintf('done\n'); \ No newline at end of file diff --git a/calibration/test_bounding_polygon.m b/calibration/test_bounding_polygon.m new file mode 100644 index 0000000000000000000000000000000000000000..5a05f493c502d4ab82feb5b94bdfadbb18ab7ba3 --- /dev/null +++ b/calibration/test_bounding_polygon.m @@ -0,0 +1,19 @@ +x = rand(25,1); +y = rand(25,1); +idx = convhull(x,y); +corners = [x(idx), y(idx)]'; + +[A,b] = bounding_polygon(corners); + +xx = rand(1000,1); +yy = rand(1000,1); +pts = [xx,yy]'; +b_inside = all(bsxfun(@minus, A*pts, b) <= 0, 1); + +figure(1); +clf; +%plot(x,y,'k.'); +hold on; +plot(corners(1,:), corners(2,:), 'ro-'); +plot(xx(b_inside), yy(b_inside), 'r.'); +plot(xx(~b_inside), yy(~b_inside), 'k.'); diff --git a/calibration/test_pinhole.m b/calibration/test_pinhole.m new file mode 100644 index 0000000000000000000000000000000000000000..a7c4f7bef9b14156fd36df00494cf096a1777119 --- /dev/null +++ b/calibration/test_pinhole.m @@ -0,0 +1,47 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% test computation of Jacobian in pinhole model + +%% input parameters +calib_dir = '/nfs/data.lfpn/lawson/160412/calib/'; +calib_file = 'camera-model-160418-0000-0099.mat'; +n_pos = 1000; +cam_idx = 3; + +dx = rand(3,1); +dx = dx / norm(dx); +dx = 0.2 * dx; + +%% load calibration +fname = [calib_dir '/' calib_file]; +setup = importdata(fname); +camera = setup.camera; +n_cameras = setup.n_cameras; +r_sphere = setup.r_sphere_lim; +gl_origin = setup.gl_origin; + +calib = camera(cam_idx).calib; + +%% generate random points in sphere +gl_ctr = r_sphere * (0.5 - rand(3,n_pos)); +gl_shift = bsxfun(@plus, gl_ctr, dx); +gl_dx = gl_shift - gl_ctr; + +[ij_ctr, jac] = pinhole_model(gl_ctr, calib, true); +ij_shift = pinhole_model(gl_shift, calib, true); +ij_est = ij_ctr + squeeze(mtimesx(jac, reshape(gl_dx, [3 1 n_pos]))); + +ij_res = ij_est - ij_shift; +ij_res_rms = rms(ij_res,2); +ij_mov = ij_shift - ij_ctr; +ij_mov_rms = rms(ij_mov,2); + +fprintf('RMS displacment: %0.3f %0.3f px \n', ij_mov_rms); +fprintf('Error of estimator %0.3f %0.3f px\n', ij_res_rms); + +%% plot +figure(1); +clf; +plot(ij_shift(1,:), ij_shift(2,:), 'k.'); +hold on +plot(ij_ctr(1,:), ij_ctr(2,:), 'r.'); +plot(ij_est(1,:), ij_est(2,:), 'ko'); \ No newline at end of file diff --git a/calibration/volume_bounds.m b/calibration/volume_bounds.m new file mode 100644 index 0000000000000000000000000000000000000000..46da91b8af5b533167f74b414165ece5eaf029de --- /dev/null +++ b/calibration/volume_bounds.m @@ -0,0 +1,30 @@ +function [gl_origin, r_sphere] = volume_bounds(setup) + % gl_origin, r_sphere = volume_bounds(setup) + % + % determine the largest sphere which is visible in each camera + % of the camera setup specified by setup + % + % returns the origin and radius of the sphere in each view + + %% determine size + center of volume + % find point closest to center of each image in all cameras + n_cameras = setup.n_cameras; + ij_origin = zeros(2,1,n_cameras); + for cam_idx = 1 : n_cameras + i_axis = setup.camera(cam_idx).i_axis; + j_axis = setup.camera(cam_idx).j_axis; + ij_origin(:,1,cam_idx)= [sum(i_axis([1 end]))/2; + sum(j_axis([1 end]))/2]; + end + gl_origin = triangulate(setup.camera, ij_origin, true); + + % find size of inscribed sphere + r_sphere = zeros(1, n_cameras); + for c = 1 : n_cameras + M = frustum(setup.camera(c)); + frustum_dist = M * [gl_origin(:);1]; + r_sphere(c) = min(abs(frustum_dist)); + end +end + + diff --git a/stereo/call_spiv_selfcal.m b/stereo/call_spiv_selfcal.m new file mode 100644 index 0000000000000000000000000000000000000000..0a87119acab3071bb7a3e374167721051447b612 --- /dev/null +++ b/stereo/call_spiv_selfcal.m @@ -0,0 +1,8 @@ +function ws = call_spiv_selfcal(opts) + %% extract user defined options + src_dir = opts.src_dir; + spiv_selfcal; + + %% save workspace + saveas('test.mat'); +end \ No newline at end of file diff --git a/stereo/define_planar_coord_system.m b/stereo/define_planar_coord_system.m new file mode 100644 index 0000000000000000000000000000000000000000..80d0606da6e7ca27cbe50f58580aad1284d0017e --- /dev/null +++ b/stereo/define_planar_coord_system.m @@ -0,0 +1,26 @@ +function [pl_to_re, x_0] = define_planar_coord_system(M, e_up) + % pl_to_re, x_0 = define_planar_coord_system(M, e_up) + % define a coordinate system for the plane specified by + % M*[x; 1] = 0 + % such that the resulting coordinate system + % x = x_0 + u*e_u + v*e_v + w*e_w + % corresponds to + % w = 0 on the plane + % and + % e_v is aligned with e_up + + e_up = e_up(:); + M = reshape(M, [1 4]); + M(1:3) = M(1:3) / norm(M(1:3)); + + + e_w = M(1:3)'; + % "up" vector, usually aligned with y + e_v = e_up - dot(e_w,e_up)*e_w; + e_v = e_v / norm(e_v); + % "right" vector, orthogonal to up and normal + e_u = cross(e_v, e_w); + % origin + x_0 = -e_w * M(4); + pl_to_re = [e_u(:) e_v(:) e_w(:)]; +end \ No newline at end of file diff --git a/stereo/fit_plane.m b/stereo/fit_plane.m new file mode 100644 index 0000000000000000000000000000000000000000..ef2a4c28853f2e4c7cd201799317a637012a687d --- /dev/null +++ b/stereo/fit_plane.m @@ -0,0 +1,18 @@ +function M = fit_plane(x, e_u) + % M = fit_plane(x, e_u) + % fit a set of points x = [3 x N] to a plane defined by + % M * [x;1] = 0 + % + % this problem is uniquely specified up to the orientation of + % the unit normal (provided x do not lie on a line or point) + % + % the orientation of the unit normal can be chosen so it is aligned + % with a unit vector e_u, in the sense that n.e_u > 0 + % + + [M,~] = svds([x; ones(1,size(x,2))], 1, 'smallest'); + M = M' / norm(M(1:3)); + if nargin > 1 + M = M * sign(dot(M(1:3), e_u)); + end +end \ No newline at end of file diff --git a/stereo/fwhm.m b/stereo/fwhm.m new file mode 100644 index 0000000000000000000000000000000000000000..8f045cce0e4f17bea2f814c83b38c8f47850fdff --- /dev/null +++ b/stereo/fwhm.m @@ -0,0 +1,9 @@ +function W = fwhm(x, f) + % W = fwhm(x, f) + % measure full width half maximum of f(x) + f = f - min(f); + pk = max(f); + i0 = find(f > 0.5*pk, 1, 'first'); + i1 = find(f > 0.5*pk, 1, 'last'); + W = x(i1) - x(i0); +end \ No newline at end of file diff --git a/stereo/gauss2d.m b/stereo/gauss2d.m new file mode 100644 index 0000000000000000000000000000000000000000..59257b641e4f3d58b8505c012889d3378118779e --- /dev/null +++ b/stereo/gauss2d.m @@ -0,0 +1,39 @@ +function [f, jac] = gauss2d(X, xdata) + % [f,jac] = gauss2d(X, xdata) + % + % Gaussian fit model function + % f = A * exp(-(x-x0)^2/sx^2 - (y-y0)^2/sy^2) + % with jacobian + % df/dX_i + % + % where the model is specified by + % X(1) = A + % X(2) = x0 + % X(3) = y0 + % X(4) = sx + % X(5) = sy + % + % and x = xdata(:,1), y = xdata(:,2) + + %% unpack + A = X(1); + x0 = X(2); + y0 = X(3); + sx = X(4); + sy = X(5); + + %% evaluate + x = xdata(:,1); + y = xdata(:,2); + f = A * exp(-(x-x0).^2/sx^2 - (y-y0).^2/sy^2); + + %% jacobian + if nargout > 1 + dfdA = f/A; + dfdx0 = 2*f.*(x-x0)/sx^2; + dfdy0 = 2*f.*(y-y0)/sy^2; + dfdsx = 2*f.*(x-x0).^2/sx^3; + dfdsy = 2*f.*(y-y0).^2/sy^3; + jac = [dfdA dfdx0 dfdy0 dfdsx dfdsy]; + end +end \ No newline at end of file diff --git a/stereo/gauss2d_peaklocate.m b/stereo/gauss2d_peaklocate.m new file mode 100644 index 0000000000000000000000000000000000000000..d8eb70656392bbac080c4f4e2097fa99a8d7ce66 --- /dev/null +++ b/stereo/gauss2d_peaklocate.m @@ -0,0 +1,56 @@ +function [pk_loc, A, sx, sy, C_fit] = gauss2d_peaklocate(C) + % Gaussian 2D peak location using five parameter least-squares Gaussian + % fit + + %% exclude points outside wsize/2 shift + S = size(C); + S2 = S; + %{ + ir = (1+S(1)/4) : S(1)*3/4; + jr = (1+S(2)/4) : S(2)*3/4; + Cs = C(ir,jr); + %} + ir = 1 : S(1); + jr = 1 : S(2); + Cs = C; + + %% find index of a strict global maximum + is_max = Cs(2:end-1,2:end-1) > Cs(1:end-2, 2:end-1) ... + & Cs(2:end-1,2:end-1) > Cs(3:end-0, 2:end-1) ... + & Cs(2:end-1,2:end-1) > Cs(2:end-1, 1:end-2) ... + & Cs(2:end-1,2:end-1) > Cs(2:end-1, 3:end-0); + is_max = Cs .* padarray(is_max, [1 1], 0, 'both'); + [A, maxind] = max(is_max(:)); + [imax,jmax] = ind2sub(S2, maxind); + + %% handle corner case where correlation plane is negative valued + if A <= 0 || imax == 1 || imax == S(1) || jmax == 1 || jmax == S(2) + pk_loc = nan; + sx = nan; + sy = nan; + C_fit = nan*Cs; + return; + end + + %% fit + [im,jm] = ndgrid(ir, jr); + xdata = [im(:) jm(:)]; + ydata = Cs(:); + X0 = [A, imax+ir(1)-1, jmax+jr(1)-1, 4, 4]; + X_lb = [0, S(1)/4, S(1)/4, 0 , 0]; + X_ub = [inf,3*S(2)/4, 3*S(2)/4, S(1), S(2)]; + fitopts = optimoptions('lsqcurvefit', 'display', 'off', 'SpecifyObjectiveGradient', false); + [X,~,~,flag,output] = lsqcurvefit(@gauss2d, X0, xdata, ydata, X_lb, X_ub, fitopts); + %disp(flag); + %disp(output); + + %% residual + C_fit = reshape(gauss2d(X,xdata), size(Cs)); + %C_fit = padarray(C_fit, S/4, 0, 'both'); + + %% extract + A = X(1); + pk_loc = [X(2);X(3)]; + sx = X(4); + sy = X(5); +end \ No newline at end of file diff --git a/stereo/is_local_maximum.m b/stereo/is_local_maximum.m new file mode 100644 index 0000000000000000000000000000000000000000..c7942c5e48d45dbce6251f06de9cb24423e314c8 --- /dev/null +++ b/stereo/is_local_maximum.m @@ -0,0 +1,7 @@ +function b = is_local_maximum(im) + b = im(2:end-1,2:end-1) > im(1:end-2,2:end-1) ... + & im(2:end-1,2:end-1) > im(3:end ,2:end-1) ... + & im(2:end-1,2:end-1) > im(2:end-1,3:end ) ... + & im(2:end-1,2:end-1) > im(2:end-1,1:end-2); + b = padarray(b, [1 1], 0, 'both'); +end \ No newline at end of file diff --git a/stereo/loadvec_pair.m b/stereo/loadvec_pair.m new file mode 100644 index 0000000000000000000000000000000000000000..7c164693bf25fbedd1f8612856e4efcc6bc2ff98 --- /dev/null +++ b/stereo/loadvec_pair.m @@ -0,0 +1,26 @@ +function [VC1, VC2] = loadvec_pair(vec_name) + % [VC1, VC2] = loadvec_pair(vec_name) + % load a pair of vector fields from either MATLAB or VC7 format + % + + [~,~,ext] = fileparts(vec_name); + if strcmpi(ext, '.vc7') + % load vc7 vector field + F = readimx(vec_name); + VC1 = create2DVec(F.Frames{1}); + VC2 = create2DVec(F.Frames{2}); + % get PIV delta t + attrs = readAttributes(F); + dt = attrs('Reference time dt 1'); + VC1.dt = dt; + VC2.dt = dt; + else + % load matlab vector field + % in same format as create2DVec + fs = load(vec_name); + [xm1, ym1] = ndgrid(fs.C1.win_x, fs.C1.win_y); + [xm2, ym2] = ndgrid(fs.C2.win_x, fs.C2.win_y); + VC1 = struct('X', xm1, 'Y', ym1, 'U', fs.C1.ux, 'V', fs.C1.uy, 'C', fs.C1.peak_choice, 'dt', 1); + VC2 = struct('X', xm2, 'Y', ym2, 'U', fs.C2.ux, 'V', fs.C2.uy, 'C', fs.C2.peak_choice, 'dt', 1); + end +end \ No newline at end of file diff --git a/stereo/refine_marker.m b/stereo/refine_marker.m new file mode 100644 index 0000000000000000000000000000000000000000..e4bd6716267b5e8131685200dc8e376e03466373 --- /dev/null +++ b/stereo/refine_marker.m @@ -0,0 +1,48 @@ +function pl_refined = refine_marker(u, v, im, pl_marker, radius) + % pl_refined = refine_marker(u_axis, v_axis, im, pl_marker, radius) + % + % refine the position measurement of circular markers on calibration + % image + % + % Input: + % u u coordinate axis + % v v coordinate axis + % im calibration image (dewarped) + % pl_marker 2 x N array of approximate marker locations + % radius nominal radius of markers + + w_search = 6*radius; + n_markers = size(pl_marker,2); + du = mean(diff(u)); + dv = mean(diff(v)); + assert(abs(abs(du/dv) - 1) < 0.01, 'pixels must be square'); + r_px = radius / sqrt(du*dv); + + + for m = 1 : n_markers + %% cut out region containing just one marker + ir = abs(u - pl_marker(1,m)) < w_search/2; + jr = abs(v - pl_marker(2,m)) < w_search/2; + im_samp = im(ir,jr); + bg = min(im_samp(:)); + im_samp = im_samp - bg; + u_samp = u(ir); + v_samp = v(jr); + + %% adaptively choose threshold + % based on reasoning that marker is brightest thing in view + % and that it occupies a known fraction of the cut out region + A_samp = abs(u_samp([1 end])*[-1;1] * v_samp([1 end])*[-1;1]); + thresh = prctile(im_samp(:), 100*(1 - (pi*(1.0*radius)^2)/A_samp)); + im_filt = im_samp > thresh; + [C2,R2] = imfindcircles(im_filt', round(r_px*[0.8 1.2]), 'objectpolarity', 'bright'); + [C1,R1] = imfindcircles(im_samp', round(r_px*[0.8 1.2]), 'objectpolarity', 'bright'); + + imagesc(im_samp'); + viscircles(C2,R2,'color','r'); + viscircles(C1,R1,'color','g'); + axis equal tight xy; + end +end + + \ No newline at end of file diff --git a/stereo/rescale_field.m b/stereo/rescale_field.m new file mode 100644 index 0000000000000000000000000000000000000000..13213cb513d044cd05cc63ae1a3e7a0f21fd838d --- /dev/null +++ b/stereo/rescale_field.m @@ -0,0 +1,4 @@ +function V = rescale_field(V, scale) + V.U = V.U * scale; + V.V = V.V * scale; +end \ No newline at end of file diff --git a/stereo/robust_fit_plane.m b/stereo/robust_fit_plane.m new file mode 100644 index 0000000000000000000000000000000000000000..c9c7b1cd489c51b5b21d1251f7b95393cc21bec3 --- /dev/null +++ b/stereo/robust_fit_plane.m @@ -0,0 +1,7 @@ +function M = robust_fit_plane(X) + xy = X(1:2,:)'; + z = X(3,:)'; + B = robustfit(xy, z); + M = [-B(2) -B(3) 1 -B(1)]; + M = M / norm(M(1:3)); +end \ No newline at end of file diff --git a/stereo/roi_axes.m b/stereo/roi_axes.m new file mode 100644 index 0000000000000000000000000000000000000000..17b3809d515758799efdd13c04e64da85ffe0618 --- /dev/null +++ b/stereo/roi_axes.m @@ -0,0 +1,71 @@ +function ax = roi_axes(cmodel, pl_corners, P) + % ax = roi_axes(c_A, c_B, re_corners, pl_corners, P) + % + % define a set of coordinate axes, based on an ROI defined by + % the corners + % + % Input: + % cmodel camera model array + % pl_corners corners of ROI on reconstruction plane + % P transformation matrix mapping reconstruction->world + + %% get camera model and image axes + c_A = cmodel(1).calib; + c_B = cmodel(2).calib; + i_lim = [min(cmodel(1).i_axis) max(cmodel(1).i_axis)]; + j_lim = [min(cmodel(1).j_axis) max(cmodel(1).j_axis)]; + + %% estimate a magnification + % based on matching reconstruction resolution to image resolution + re_corners = P * [pl_corners; 1 1 1 1]; + scale = roi_magnification(c_A, c_B, re_corners, pl_corners); + + %% define axes + pl_lim = [min(pl_corners,[],2) max(pl_corners,[],2)]; + u_lim = pl_lim(1,:); + v_lim = pl_lim(2,:); + w_roi = abs(u_lim*[-1 1]'); + h_roi = abs(v_lim*[-1 1]'); + n_u = ceil(w_roi * scale(1)); + n_v = ceil(h_roi * scale(2)); + u_axis = linspace(u_lim(1), u_lim(2), n_u); + v_axis = linspace(v_lim(1), v_lim(2), n_v); + + %% produce mapping function to image coordinates in both cameras + [um,vm,wm,tm] = ndgrid(u_axis, v_axis, 0, 1); + uvw1_mat = [um(:) vm(:) wm(:) tm(:)].'; + xyz_mat = P * uvw1_mat; + + [ijA, jacA_re] = pinhole_model(xyz_mat, c_A); + [ijB, jacB_re] = pinhole_model(xyz_mat, c_B); + iA = reshape(ijA(1,:), [n_u n_v]); + jA = reshape(ijA(2,:), [n_u n_v]); + iB = reshape(ijB(1,:), [n_u n_v]); + jB = reshape(ijB(2,:), [n_u n_v]); + + % convert jacobian from world coordinates to sheet coordinates + jacA_pl = mtimesx(jacA_re, P(:,1:3)); + jacB_pl = mtimesx(jacB_re, P(:,1:3)); + % reshape to 2 x 3 x n_u x n_v + jacA_pl = reshape(jacA_pl, [2 3 n_u n_v]); + jacB_pl = reshape(jacB_pl, [2 3 n_u n_v]); + + %% determine which points are out-of-bounds of image + imA_mask = iA < i_lim(1) | iA > i_lim(2) | jA < j_lim(1) | jA > j_lim(2); + imB_mask = iB < i_lim(1) | iB > i_lim(2) | jB < j_lim(1) | jB > j_lim(2); + + %% create map structure + map = struct('im', iA, 'jm', jA, 'mask', imA_mask, 'jac', jacA_pl); + map(2) = struct('im', iB, 'jm', jB, 'mask', imB_mask, 'jac', jacB_pl); + + %% save in struct + ax = struct( 'n_u', n_u, 'n_v', n_v, ... + 'u', u_axis, 'v', v_axis, ... + 'W', w_roi, 'H', h_roi, ... + 'scale', scale, ... + 'map', map, ... + 'cmodel', cmodel, ... + 'P', P, ... + 're_corners', re_corners, ... + 'pl_corners', pl_corners); +end \ No newline at end of file diff --git a/stereo/roi_build_map.m b/stereo/roi_build_map.m new file mode 100644 index 0000000000000000000000000000000000000000..20149eda3df6907f9249b91ec0b07ae9739cb3b0 --- /dev/null +++ b/stereo/roi_build_map.m @@ -0,0 +1,37 @@ +function roi = roi_build_map(roi) + %% grab parameters + u_axis = roi.u; + v_axis = roi.v; + P = roi.P; + c_A = cmodel(1).calib; + c_B = cmodel(2).calib; + + %% produce mapping function to image coordinates in both cameras + [um,vm,wm,tm] = ndgrid(u_axis, v_axis, 0, 1); + uvw1_mat = [um(:) vm(:) wm(:) tm(:)].'; + xyz_mat = P * uvw1_mat; + + [ijA, jacA_re] = pinhole_model(xyz_mat, c_A); + [ijB, jacB_re] = pinhole_model(xyz_mat, c_B); + iA = reshape(ijA(1,:), [n_u n_v]); + jA = reshape(ijA(2,:), [n_u n_v]); + iB = reshape(ijB(1,:), [n_u n_v]); + jB = reshape(ijB(2,:), [n_u n_v]); + + % convert jacobian from world coordinates to sheet coordinates + jacA_pl = mtimesx(jacA_re, P(:,1:3)); + jacB_pl = mtimesx(jacB_re, P(:,1:3)); + % reshape to 2 x 3 x n_u x n_v + jacA_pl = reshape(jacA_pl, [2 3 n_u n_v]); + jacB_pl = reshape(jacB_pl, [2 3 n_u n_v]); + + %% determine which points are out-of-bounds of image + imA_mask = iA < i_lim(1) | iA > i_lim(2) | jA < j_lim(1) | jA > j_lim(2); + imB_mask = iB < i_lim(1) | iB > i_lim(2) | jB < j_lim(1) | jB > j_lim(2); + + %% create map structure + map = struct('im', iA, 'jm', jA, 'mask', imA_mask, 'jac', jacA_pl); + map(2) = struct('im', iB, 'jm', jB, 'mask', imB_mask, 'jac', jacB_pl); + + roi.map = map; +end \ No newline at end of file diff --git a/stereo/roi_magnification.m b/stereo/roi_magnification.m new file mode 100644 index 0000000000000000000000000000000000000000..cc7c680cc55a8fdd7a9514b52808ef45b5f94c26 --- /dev/null +++ b/stereo/roi_magnification.m @@ -0,0 +1,32 @@ +function scale = roi_magnification(c_A, c_B, re_corners, pl_corners) + % scale = roi_magnification(c_A, c_B, re_corners, pl_corners) + % + % estimate a magnification, based on matching reconstruction resolution + % to image resolution + % + % Input: + % c_A camera A calibration structure + % c_B camera B calibration structure + % re_corners corners of ROI in world coordinates + % pl_corners corners of ROI in reconstruction plane + % coordinates + % + % Output: + % scale approximate scale factor [px/mm] between image and + % reconstruction plane coordinates + % + + %% project ROI corners onto image + im_corners_A = pinhole_model(re_corners, c_A); + im_corners_B = pinhole_model(re_corners, c_B); + + %% determine affine transformation between world and image + U_inv = pinv([pl_corners(1:2,:); 1 1 1 1]); + Aff_A = im_corners_A * U_inv; + Aff_B = im_corners_B * U_inv; + + %% average resolution across cameras + scale_A = eig(Aff_A(:,1:2)); + scale_B = eig(Aff_B(:,1:2)); + scale = sqrt(abs(scale_A .* scale_B)); +end \ No newline at end of file diff --git a/stereo/spiv_calibration.m b/stereo/spiv_calibration.m new file mode 100644 index 0000000000000000000000000000000000000000..ebd7e96138df71e8705bf11cda2dee2b52730d72 --- /dev/null +++ b/stereo/spiv_calibration.m @@ -0,0 +1,355 @@ +addpath(genpath([pwd '/../'])); + +%% input parameters +calib_dir = 'H:\davis\multiscale\Properties\Calibration History\Gen1_123_CAL05_433.33\'; +marker_file = 'MarkPositionTable.xml'; % Calibration marker position file (usually 'MarkPositionTable.xml') +model_file = 'refined.mat'; + +b_square = true; % enforce square pixels? +b_skew = false; % allow for skewness? (Scheimpflug adapters) +b_distortion= true; % compensate for radial distortion? + +c_lim = [0 256]; + +%% get marker coordinates +% Retrieve calibration markers coordinate for both left and right camera +calibname = [calib_dir marker_file]; +[Coord,corners_cam] = retrieve_marker_coord(calibname); +i_axis = corners_cam(1,1) : corners_cam(1,2)-1; +j_axis = corners_cam(2,1) : corners_cam(2,3)-1; + +%% load image +% left image +FA = readimx(sprintf('%s/camera1/B00001.im7', calib_dir)); +imA = FA.Frames{1}.Components{1}.Planes{1}; +if length(FA.Frames{1}.Components) > 1 + imA_mask= FA.Frames{1}.Components{2}.Planes{1}; +else + imA_mask= zeros(size(imA)); +end + +% right image +FB = readimx(sprintf('%s/camera2/B00001.im7', calib_dir)); +imB = FB.Frames{1}.Components{1}.Planes{1}; +if length(FB.Frames{1}.Components) > 1 + imB_mask= FB.Frames{1}.Components{2}.Planes{1}; +else + imB_mask= zeros(size(imB)); +end + +% extract image and world coordinates +im_mark_A = Coord.Left(:,1:2).'; +im_mark_B = Coord.Right(:,1:2).'; +re_mark_A = Coord.Left(:,3:5).'; +re_mark_B = Coord.Right(:,3:5).'; +n_A = size(im_mark_A,2); +n_B = size(im_mark_B,2); + +% categorise markers on front/back planes +re_mark = union(re_mark_A', re_mark_B', 'rows')'; +re_mark_front = re_mark(:, re_mark(3,:)>0); +re_mark_back = re_mark(:, re_mark(3,:)<0); +n_front = size(re_mark_front,2); +n_back = size(re_mark_back, 2); +r_nom = 0.75; + +%% fix depth +warning('correcting for plate depth'); +correction = 1.0;% 25.4*40/1000 / 1.0; +re_mark_A(3,:) = re_mark_A(3,:) * correction; +re_mark_B(3,:) = re_mark_B(3,:) * correction; + +%% fit pinhole model +fprintf('Fitting pinhole model ... \n'); +fitopts = struct('square', b_square, 'skew', b_skew, 'distortion', b_distortion); +[pA,im_res_A] = pinhole_fit_3d_reg(im_mark_A, re_mark_A, fitopts); +[pB,im_res_B] = pinhole_fit_3d_reg(im_mark_B, re_mark_B, fitopts); +im_res_A_rms = rms(im_res_A,2); +im_res_B_rms = rms(im_res_B,2); + +im_proj_A = pinhole_model(re_mark_A, pA, true); +im_proj_B = pinhole_model(re_mark_B, pB, true); + +fprintf('Residual:\n'); +fprintf(' C1: %0.3f %0.3f px\n', im_res_A_rms); +fprintf(' C2: %0.3f %0.3f px\n', im_res_B_rms); + +%% measure angle +% angle is measured in x-z plane, relative to [0;0;1] +theta1 = atand(-pA.xc(1) / pA.xc(3)); +theta2 = atand(+pB.xc(1) / pB.xc(3)); +fprintf('theta_1 = %6.2f deg\n', theta1); +fprintf('theta_2 = %6.2f deg\n', theta2); + +%% plot +figure(10); +clf; +plot3([pA.xc(1) 0], [pA.xc(2) 0], [pA.xc(3) 0], 'ro-'); +hold on; +plot3([pB.xc(1) 0], [pB.xc(2) 0], [pB.xc(3) 0], 'go-'); +axis equal tight xy; +view(0,180); + +%% save pinhole model +fprintf('------------------------------------------------------------------\n'); +fprintf('Camera calibration complete\n'); +fprintf('Saving to file ... '); + +s_model = struct(); +s_model.pl_to_gl = eye(3); +s_model.pl_to_gl_mat= repmat(eye(3),[1 1 2]); +s_model.gl_plate_pos= [0 0 -0.5; 0 0 +0.5]'; +s_model.gl_origin = [0 0 0]'; +s_model.n_cameras = 2; +s_model.n_views = 2; + +% camera calibration +pA.im = imA; +pB.im = imB; +cA = struct('calib', pA, 'id', 1, 'i_axis', i_axis, 'j_axis', j_axis); +cB = struct('calib', pB, 'id', 2, 'i_axis', i_axis, 'j_axis', j_axis); +s_model.camera = [cA, cB]; + +% bounding volume +[~, r_sphere] = volume_bounds(s_model); +r_sphere_lim = min(r_sphere); +s_model.r_sphere = r_sphere; +s_model.r_sphere_lim= r_sphere_lim; + +save([calib_dir model_file], '-struct', 's_model'); + +fprintf('done\n'); +error('stop'); +%% dewarp images with pinhole model +fprintf('Dewarping images ... \n'); +P_front = [s_model.pl_to_gl, [0;0;+0.5]]; +P_back = [s_model.pl_to_gl, [0;0;-0.5]]; + +% front plane map +fprintf(' front ... \n'); +front = StereoPlane(s_model.camera, P_front); +AR = front.scale(2) / front.scale(1); +[u_front,v_front]= front.make_axes([1 AR]); +front.axes(u_front,v_front,false); + +pl_front_A = interpn(i_axis, j_axis, double(imA), front.map(1).im, front.map(1).jm, 'linear'); +pl_front_B = interpn(i_axis, j_axis, double(imB), front.map(2).im, front.map(2).jm, 'linear'); +pl_front_sum = imfuse(pl_front_A', pl_front_B'); + +% back plane map +fprintf(' back ... \n'); +back = StereoPlane(s_model.camera, P_back); +AR = back.scale(2)/back.scale(1); +[u_back,v_back] = back.make_axes([1 AR]); +back.axes(u_back,v_back,false); + +pl_back_A = interpn(i_axis, j_axis, double(imA), back.map(1).im, back.map(1).jm, 'linear'); +pl_back_B = interpn(i_axis, j_axis, double(imB), back.map(2).im, back.map(2).jm, 'linear'); +pl_back_sum = imfuse(pl_back_A', pl_back_B'); + +%% plot +figure(1); +clf; + +% left image with markers +subplot(1,2,1); +imagesc(i_axis,j_axis,imA'); +axis equal tight ij; +xlim(i_axis([1 end])); +ylim(j_axis([1 end])); +hold on; +plot(im_mark_A(1,:), im_mark_A(2,:), 'ro'); +plot(im_proj_A(1,:), im_proj_A(2,:), 'r.'); +quiver(im_proj_A(1,:), im_proj_A(2,:), im_res_A(1,:), im_res_A(2,:), 'r-'); +set(gca,'clim',c_lim); + +title('Camera 1'); +for n = 1 : n_A + str=sprintf('(%0.0f,%0.0f,%0.1f)', re_mark_A(:,n)); + if re_mark_A(3,n) > 0 + color = 'r'; + else + color = 'g'; + end + text(im_mark_A(1,n),im_mark_A(2,n)+64,str,'color',color); +end + + +% right image with markers +subplot(1,2,2); +imagesc(i_axis,j_axis,imB'); +axis equal tight ij; + +xlim(i_axis([1 end])); +ylim(j_axis([1 end])); +hold on; +plot(im_mark_B(1,:), im_mark_B(2,:), 'ro'); +title('Camera 2'); +set(gca,'clim',c_lim); + +%% sum of camera images +figure(2); +clf; +subplot(1,2,1); +image(front.u, front.v, pl_front_sum); +axis equal tight xy; +title('Sum of cameras, front'); +ax = gca; + +% plot circles +viscircles(ax(1), re_mark_front(1:2,:)' - [0.14 -0.16], r_nom*ones(n_front,1)); + +subplot(1,2,2); +image(back.u, back.v, pl_back_sum); +axis equal tight xy; +title('Sum of cameras, back'); +ax(2) = gca; + +viscircles(ax(2), re_mark_back(1:2,:)' - [0.20 -0.16], r_nom*ones(n_back,1)); + +linkaxes(ax, 'xy'); + +error('stop'); + +%% refine marker location +re_mark = {re_mark_A, re_mark_B}; +pl_front = cat(3, pl_front_A, pl_front_B); +pl_back = cat(3, pl_back_A, pl_back_B); + +re_mark_refined = cell(1,2); +im_mark_refined = cell(1,2); + +for c = 1 : 2 + fprintf('refining markers in camera %d ...\n', c); + + %% identify front/back planes + b_front = re_mark{c}(3, :) > 0; + b_back = ~b_front; + re_front = re_mark{c}(:, b_front); + re_back = re_mark{c}(:, b_back); + r_px = r_nom / mean(diff(front.u)); + r_lim_px = round(r_px * [0.8 1.2]); + du = mean(diff(front.u)); + dv = mean(diff(front.v)); + + %% refine markers + re_refined = refine_marker(front.u, front.v, pl_front(:,:,c), re_front, r_nom); + + %% search for markers + sensitivity = 0.9; + [C_front, R_front] = imfindcircles(pl_front(:,:,c)', r_lim_px, 'Sensitivity', sensitivity); + [C_back , R_back ] = imfindcircles(pl_back(:,:,c)' , r_lim_px, 'Sensitivity', sensitivity); + % transform from coord sys + re_front_refined = [front.u(1); front.v(1); mean(re_front(3,:))] ... + + [du 0; 0 dv; 0 0] * C_front'; + re_back_refined = [back.u(1); back.v(1); mean(re_back(3,:))] ... + + [du 0; 0 dv; 0 0] * C_back'; + R_front = R_front*du; + R_back = R_back *du; + + %% intersect with original definitions of markers + r_tol = r_nom*2; + % front plane + [idx, dist] = knnsearch(re_front_refined', re_front', 'K', 1); + b_ok = dist < r_tol; + re_front_refined = re_front_refined(:, idx(b_ok)); + R_front = R_front(idx(b_ok)); + % back plane + [idx, dist] = knnsearch(re_back_refined', re_back', 'K', 1); + b_ok = dist < r_tol; + re_back_refined = re_back_refined(:, idx(b_ok)); + R_back = R_back(idx(b_ok)); + + %% reproject to images + im_front_refined = pinhole_model(re_front_refined, s_model.camera(c).calib, true); + im_back_refined = pinhole_model(re_back_refined , s_model.camera(c).calib, true); + pl_front_refined = [round(re_front_refined(1:2,:)/10)*10; re_front_refined(3,:)]; + pl_back_refined = [round(re_back_refined(1:2,:)/10)*10; re_back_refined(3,:)]; + + %% store markers + re_mark_refined{c} = cat(2, pl_front_refined, pl_back_refined); + im_mark_refined{c} = cat(2, im_front_refined, im_back_refined); + + %% plot + figure(10+c); + clf; + subplot(1,2,1); + imagesc(front.u,front.v,pl_front(:,:,c)'); + axis equal tight xy; + viscircles(re_front_refined(1:2,:)', R_front); + title('front plane'); + ax(1)=gca; + hold on; + + subplot(1,2,2); + imagesc(back.u, back.v, pl_back(:,:,c)'); + axis equal tight xy; + viscircles(re_back_refined(1:2,:)', R_back); + title('back plane'); + ax(2)=gca; + hold on; + + linkaxes(ax,'xy'); +end + +%% fit pinhole model +fprintf('Fitting pinhole model to refined markers ... \n'); +fitopts = struct('square', b_square, 'skew', b_skew, 'distortion', b_distortion); +[pA,im_res_A] = pinhole_fit_3d_reg(im_mark_refined{1}, re_mark_refined{1}, fitopts); +[pB,im_res_B] = pinhole_fit_3d_reg(im_mark_refined{2}, re_mark_refined{2}, fitopts); +im_res_A_rms = rms(im_res_A,2); +im_res_B_rms = rms(im_res_B,2); + +im_proj_A = pinhole_model(re_mark_refined{1}, pA, true); +im_proj_B = pinhole_model(re_mark_refined{2}, pB, true); + +fprintf('Residual:\n'); +fprintf(' C1: %0.3f %0.3f px\n', im_res_A_rms); +fprintf(' C2: %0.3f %0.3f px\n', im_res_B_rms); + +%% measure angle +% angle is measured in x-z plane, relative to [0;0;1] +theta1 = atand(-pA.xc(1) / pA.xc(3)); +theta2 = atand(+pB.xc(1) / pB.xc(3)); +fprintf('theta_1 = %6.2f deg\n', theta1); +fprintf('theta_2 = %6.2f deg\n', theta2); + +%% plot +figure(10); +clf; +plot3([pA.xc(1) 0], [pA.xc(2) 0], [pA.xc(3) 0], 'ro-'); +hold on; +plot3([pB.xc(1) 0], [pB.xc(2) 0], [pB.xc(3) 0], 'go-'); +axis equal tight xy; +view(0,180); + +%% save pinhole model +fprintf('------------------------------------------------------------------\n'); +fprintf('Camera calibration complete\n'); +fprintf('Saving to file ... '); + +s_model = struct(); +s_model.pl_to_gl = eye(3); +s_model.pl_to_gl_mat= repmat(eye(3),[1 1 2]); +s_model.gl_plate_pos= [0 0 -0.5; 0 0 +0.5]'; +s_model.gl_origin = [0 0 0]'; +s_model.n_cameras = 2; +s_model.n_views = 2; + +% camera calibration +pA.im = imA; +pB.im = imB; +cA = struct('calib', pA, 'id', 1, 'i_axis', i_axis, 'j_axis', j_axis); +cB = struct('calib', pB, 'id', 2, 'i_axis', i_axis, 'j_axis', j_axis); +s_model.camera = [cA, cB]; + +% bounding volume +[~, r_sphere] = volume_bounds(s_model); +r_sphere_lim = min(r_sphere); +s_model.r_sphere = r_sphere; +s_model.r_sphere_lim= r_sphere_lim; + +save([calib_dir model_file], '-struct', 's_model'); + +fprintf('done\n'); + diff --git a/stereo/spiv_imcovar.m b/stereo/spiv_imcovar.m new file mode 100644 index 0000000000000000000000000000000000000000..7fd69b5db6d4584e352e7c6a3e4472f77e337202 --- /dev/null +++ b/stereo/spiv_imcovar.m @@ -0,0 +1,196 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% measurement of covariance between stereo PIV images, for determination of +% wall position +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 29/09/2019 +% +addpath(genpath([pwd '/../'])); + +%% input parameters + +% path of images for self calibration +%src_dir = 'H:\davis\multiscale\Gen1_12\U10\U10_x-axis=241.317_x-axis=241.317_x-axis=213.316'; +src_pat = '%s/B%05d.im7'; +src_range = 1 : 4 : 1000; +n_src = length(src_range); + +% path of MATLAB calibration file +%cal_dir = src_dir; +%cal_pat = '%s/pinhole.shifted.mat'; +% destination +dst_dir = src_dir; +dst_pat = '%s/dewarped.mat'; + +b_plot_covar = false; +b_plot_dewarped = false; +b_plot_source = false; + +%% derived parameters +src_fun = @(n) sprintf(src_pat, src_dir, n); +cal_file = sprintf(cal_pat, cal_dir); + +%% load camera model +fprintf('loading camera model ...\n'); +s_model = importdata(cal_file); +i_axis = s_model.camera(1).i_axis; +j_axis = s_model.camera(1).j_axis; +c_A = s_model.camera(1).calib; +c_B = s_model.camera(2).calib; + +%% calculate axes for reconstruction +% based on first-guess in model structure +pl_to_gl = s_model.pl_to_gl; +x_0 = s_model.gl_origin; +P_0 = [pl_to_gl, x_0]; +e_n0 = pl_to_gl(:,3); +z0 = dot(x_0, e_n0); +M = [e_n0; -z0]'; + +%% build a stereo PIV reconstruction object +fprintf('Building reconstruction object...\n'); +stereo = StereoPlane(s_model.camera, P_0); +[u_axis,v_axis] = stereo.make_axes([1 1]); % 1 pixel resolution +stereo.axes(u_axis,v_axis,false); +n_u = stereo.n_u; +n_v = stereo.n_v; +map = stereo.map; +map(1).jac = []; +map(2).jac = []; +map(1).e_c = []; +map(2).e_c = []; + +fprintf('Resolution: %5.1f x %5.1f px/mm\n', stereo.scale); +fprintf('ROI size: %5.1f x %5.1f mm\n', stereo.W, stereo.H); +fprintf(' %5d x %5d px\n', n_u, n_v); + +%% set up grid for sum-of-correlation +pl_sz = [n_u n_v]; +I_AB_mean = zeros(pl_sz, 'single'); +I_AA_mean = zeros(pl_sz, 'single'); +I_BB_mean = zeros(pl_sz, 'single'); +I_A_mean = zeros(pl_sz, 'single'); +I_B_mean = zeros(pl_sz, 'single'); + +%% iterate +for n = 1 : n_src + %% load image + src_name = src_fun(src_range(n)); + fprintf('[%3d of %3d] %s\n', n, n_src, src_name); + imx = readimx(src_name); + im_A = single(imx.Frames{1}.Components{1}.Planes{1}); + im_B = single(imx.Frames{3}.Components{1}.Planes{1}); + + %% dewarp image + %fprintf(' dewarping ...\n'); + pl_im_A = interpn(i_axis, j_axis, im_A, map(1).im, map(1).jm, 'linear', 0); + pl_im_B = interpn(i_axis, j_axis, im_B, map(2).im, map(2).jm, 'linear', 0); + + %% filter + %G = fspecial('disk', 3); + %pl_im_A = conv2(pl_im_A, G, 'same'); + %pl_im_B = conv2(pl_im_B, G, 'same'); + + %% cross-correlate + %fprintf(' cross correlating ...\n'); + % add to mean + I_AA_mean = I_AA_mean + pl_im_A.*pl_im_A / n_src; + I_BB_mean = I_BB_mean + pl_im_B.*pl_im_B / n_src; + I_AB_mean = I_AB_mean + pl_im_A.*pl_im_B / n_src; + I_A_mean = I_A_mean + pl_im_A / n_src; + I_B_mean = I_B_mean + pl_im_B / n_src; +end + +%% covariance +A_var = max(0, I_AA_mean - I_A_mean.^2); +B_var = max(0, I_BB_mean - I_B_mean.^2); +AB_covar = I_AB_mean - I_A_mean .* I_B_mean; +R_AB = AB_covar ./ sqrt(A_var .* B_var); + +%% save to file +dst_name = sprintf(dst_pat, dst_dir); +fs = struct(); +fs.u = stereo.u; +fs.v = stereo.v; +fs.P = stereo.P; +fs.M = stereo.M; +fs.cmodel = stereo.cmodel; +fs.im_mean = {I_A_mean, I_B_mean}; +fs.im_var = {A_var, B_var}; +fs.im_covar = AB_covar; +fs.src_range = src_range; +fs.src_pat = src_pat; +fs.src_dir = src_dir; +fprintf('saving to %s ...\n', dst_name); +save(dst_name, '-struct', 'fs'); +fprintf('done\n'); + +%% plot correlation +if b_plot_covar + figure(1); + clf; + subplot(3,1,1); + imagesc(stereo.u, stereo.v, R_AB'); + axis equal tight xy + title('Image covariance'); + clim([0 0.5]); + ylim([-72 -60]); + + subplot(3,1,2); + imagesc(stereo.u, stereo.v, I_A_mean'); + axis equal tight xy + title('A mean'); + clim([0 512]); + ylim([-72 -60]); + + subplot(3,1,3); + imagesc(stereo.u, stereo.v, I_B_mean'); + axis equal tight xy + title('B mean'); + clim([0 512]); + ylim([-72 -60]); + %set(gca,'clim', [0 0.08]); +end + +%% display dewarped images +if b_plot_dewarped + figure(3); + clf; + subplot(1,2,1); + ax = gca; + imagesc(stereo.u, stereo.v, pl_im_A'); + axis equal xy; + xlim(stereo.u([1 end])); + ylim(stereo.v([1 end])); + title('Camera 1, dewarped'); + %ylim(j_axis([end-512 end])); + %xlim(i_axis([1 end])); + + subplot(1,2,2); + ax(2)=gca; + imagesc(stereo.u, stereo.v, pl_im_B'); + axis equal xy; + xlim(stereo.u([1 end])); + ylim(stereo.v([1 end])); + title('Camera 2, dewarped'); + + %quiver(um, vm,-squeeze(uv_shift(1,:,:)),-squeeze(uv_shift(2,:,:)), 'r-'); + + linkaxes(ax, 'xy'); +end + +%% plot raw images +if b_plot_source + figure(4); + clf; + subplot(1,2,1); + imagesc(i_axis, j_axis, im_A'); + axis equal tight ij; + title('Camera 2, raw'); + + subplot(1,2,2); + imagesc(i_axis, j_axis, im_B'); + axis equal tight ij; + title('Camera 1, raw'); +end \ No newline at end of file diff --git a/stereo/spiv_mask.m b/stereo/spiv_mask.m new file mode 100644 index 0000000000000000000000000000000000000000..023beb6d9ba285be9797b812867acf04118df64f --- /dev/null +++ b/stereo/spiv_mask.m @@ -0,0 +1,119 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% algorithmically generate a mask for PIV processing +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 27/06/2019 +% +addpath(genpath([pwd '/../../piv2d/'])); +addpath([pwd '/../../misc/mtimesx/']); + +%% input parameters +% path of images for self calibration +src_dir = 'H:\davis\multiscale\Gen1\U10\U10_x-axis=107.986_x-axis=107.985_x-axis=79.9874\'; +src_pat = '%s/B%05d.im7'; +src_range = 1 : 100 : 1000; +src_fun = @(n) sprintf(src_pat, src_dir, n); +n_src = length(src_range); +wsize = [1 11]; + +%% load prototype image +src_name = src_fun(src_range(1)); +fprintf('load %s ...\n', src_name); +imx = readimx(src_name); +n_frames = length(imx.Frames); +im_size = size(imx.Frames{1}.Components{1}.Planes{1}); + +im_min_ary = arrayfun(@(x) zeros(im_size), 1:n_frames, 'UniformOutput', false); +im_max_ary = arrayfun(@(x) zeros(im_size), 1:n_frames, 'UniformOutput', false); +im_mean_ary = arrayfun(@(x) zeros(im_size), 1:n_frames, 'UniformOutput', false); +im_var_ary = arrayfun(@(x) zeros(im_size), 1:n_frames, 'UniformOutput', false); + +%% gather image statistics +fprintf('------------------------------------------------------------------\n'); +fprintf('PROCESSING START %s\n', datestr(now)); +G = fspecial('log', wsize, 0.1*wsize(2)); + + +for n = 1 : n_src + %% load image + src_name = src_fun(src_range(n)); + fprintf('[%3d of %3d] %s\n', n, n_src, src_name); + imx = readimx(src_name); + src_ary = cellfun(@(f) double(f.Components{1}.Planes{1}), imx.Frames, 'UniformOutput', false); + + %% time filter to accentuate particle images + % particle images tend to change over frames, background does not + im_ary(1:2) = {src_ary{2} - src_ary{1}}; + im_ary(3:4) = {src_ary{4} - src_ary{3}}; + + %% add to average + for f = 1 : n_frames + % high pass filter to emphasise particle images + im_filt = conv2(im_ary{f}, G, 'same'); + + %im_filt = double(maxfiltnd(single(im_ary{f}), wsize)); + %im_filt = conv2(im_ary{f}, G, 'same'); + + im_min_ary{f} = min(im_min_ary{f}, im_filt); + im_max_ary{f} = max(im_max_ary{f}, im_filt); + im_mean_ary{f} = im_mean_ary{f} + im_filt / n_src; + im_var_ary{f} = im_var_ary{f} + im_filt.^2 / n_src; + end +end + +for f = 1 : n_frames + im_var_ary{f} = max(0,im_var_ary{f} - im_mean_ary{f}.^2); + im_std_ary{f} = sqrt(im_var_ary{f}); +end + +%% display +figure(1); +clf; +s_el = strel('disk', 5); +ir = 1 : im_size(1); +jr = 1 : im_size(2) - 500; +cmap = jet; +cmap(1,:) = 0; +j_axis = 1 : im_size(2); + +for f = 1 : n_frames + %% compute threshold + im_std = sqrt(im_var_ary{f}); + im_samp = im_std(ir,jr); + thresh = prctile(im_samp(:), 2); + + %% mask + mask = im_var_ary{f} > thresh^2; + mask = imerode(mask, s_el); + mask = imdilate(mask, s_el); + %{ + [label,n_labels] = bwlabel(mask); + + n_l = zeros(1,n_labels); + for k = 1 : n_labels + n_l(k)= nnz(label == k); + end + [~,l_max]= max(n_l); + mask = label == l_max; + mask = imfill(mask, 'holes'); + + %% find boundary + im = 1 : im_size(1); + jm = zeros(1, im_size(1)); + for ii = im + j_min = find(~mask(ii,:) & j_axis > jr(end), 1, 'first'); + if isempty(j_min) + j_min = jr(end); + end + jm(ii)= j_min; + end + %} + %% plot + subplot(1,n_frames,f); + imagesc(mask'); + axis equal tight ij; + hold on; + %plot(im,jm,'r-'); + %colormap(cmap); +end diff --git a/stereo/spiv_mask_manual.m b/stereo/spiv_mask_manual.m new file mode 100644 index 0000000000000000000000000000000000000000..207c64b7e2d42028dcfe64417e0c675d13c9d935 --- /dev/null +++ b/stereo/spiv_mask_manual.m @@ -0,0 +1,177 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% algorithmically generate a mask for PIV processing +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 27/06/2019 +% +close all; clear; clc; +addpath([pwd '/../readimx']); + +%% input parameters +% path of images for self calibration +src_root = 'H:\davis\multiscale\Gen1_12\U10\'; +% pattern to match source directories +src_dir_pat = '^U10_x-axis='; + +% usually, you will not need to change the following parameters: +src_pat = '%s/%s/B00001.im7'; % pattern for source image input +msk_pat = '%s/%s/MASK/B0001.im7'; % pattern for mask image output +mat_pat = '%s/%s/mask.mat'; % patlab for MATLAB .mat output +y_lim = 4872 + [-750 0]; % ROI +c_lim = [-1 1]*128; % +font_size = 24; % figure font size + +%% list all directories +src_dir_list = dir(src_root); +b_match = arrayfun(@(f) ~isempty(regexp(f.name, src_dir_pat, 'once')) && f.isdir, src_dir_list); +src_dir_list = src_dir_list(b_match); +n_src = length(src_dir_list); + +fprintf('In root %s\n', src_root); +fprintf(' %s\n', src_dir_list.name); +fprintf(' %d directories to process\n', n_src); + +%% draw figure +cmap = jet; +cmap(1,:) = 0; + +%% loop over files to be processed +for n = 1 : n_src + %% check for existing files + src_name = sprintf(src_pat, src_root, src_dir_list(n).name); + msk_name = sprintf(msk_pat, src_root, src_dir_list(n).name); + mat_name = sprintf(mat_pat, src_root, src_dir_list(n).name); + fprintf('load %s \n', src_name); + if ~exist(src_name, 'file') + warning('source %s does not exist, skipping\n', src_name); + continue; + end + + if exist(msk_name, 'file') + s = input('mask already exists, do you wish to overwrite? [y/N] ', 's'); + b_overwrite = strcmpi(s, 'y'); + if ~b_overwrite + fprintf('OK, skipping ...\n'); + continue; + end + end + + %% make mask directory if necessary + msk_dir = fileparts(msk_name); + if ~exist(msk_dir, 'dir') + mkdir(msk_dir); + end + + %% load prototype image + fprintf(' loading ... \n'); + imx = readimx(src_name); + n_frames = length(imx.Frames); + im_size = size(imx.Frames{1}.Components{1}.Planes{1}); + src_ary = cellfun(@(f) double(f.Components{1}.Planes{1}), imx.Frames, 'UniformOutput', false); + + im_ary = {src_ary{1}-src_ary{2}, src_ary{3}-src_ary{4}}; + n_mask = length(im_ary); + msk_ary = im_ary; + msk_corners = cell(1,n_mask); + + %% loop over number of masks + for m = 1 : n_mask + %% loop until we get an acceptable mask + b_continue = true; + msk_ary{m} = false(im_size); + while b_continue + %% render image + figure(1); + clf; + + subplot(2,1,1); + imagesc((src_ary{(m-1)*2+1})'); + axis equal tight ij; + ylim(y_lim); + set(gca, 'clim', [0 512]); + colormap(gca, parula); + title('Source image'); + + drawnow; + + + subplot(2,1,2); + imagesc((im_ary{m})'); + axis equal tight ij; + ylim(y_lim); + colormap(gca, redblue); + set(gca, 'clim', c_lim); + drawnow; + + %% specify masked region + fprintf('Select region to mask out\n'); + fprintf(' right click to add points\n'); + fprintf(' left click to finish adding points\n'); + fprintf(' context menu -> create mask to finalise mask\n'); + title('Select region to mask out', 'fontsize', font_size); + [mask, ic, jc] = roipoly; + mask = mask'; + + %% present masked region + new_mask = msk_ary{m} | mask; + imagesc(((~new_mask).*im_ary{m})'); + axis equal tight ij; + ylim(y_lim); + drawnow; + set(gca, 'clim', c_lim); + set(gca,'colormap', redblue); + title('Finish [f] / Repeat [r] / Add another [a]?', 'fontsize', font_size); + s = input('Finish [f] / Repeat (try again) [r] / Add another region [a]? [F] ', 's'); + + if strcmpi(s, 'R') + %% repeat + continue; + elseif strcmpi(s, 'A') + %% add current region, but continue + msk_ary{m} = new_mask; + msk_corners{m}{end+1} = [ic,jc]; + else + %% add current region and finish + msk_ary{m} = new_mask; + msk_corners{m}{end+1} = [ic,jc]; + b_continue = false; + end + end + end + + %% add mask field to IMX + msk_map = [1 1 2 2]; + for f = 1 : 4 + imx.Frames{f}.ComponentNames{2} = 'MASK'; + imx.Frames{f}.Components{2} = imx.Frames{f}.Components{1}; + imx.Frames{f}.Components{2}.Name = 'MASK'; + imx.Frames{f}.Components{2}.Planes{1} = ~msk_ary{msk_map(f)}; + end + + %% save mask to file + fprintf('save mask to %s\n', msk_name); + if exist(msk_name, 'file') && ~b_overwrite + warning('Mask file already exists. Cannot overwrite.'); + continue; + end + writeimx(imx, msk_name); + + %% copy .set + src_set = sprintf('%s/%s.set', src_root, src_dir_list(n).name); + msk_set = sprintf('%s/%s/Mask.set', src_root, src_dir_list(n).name); + if ~exist(src_set, 'file') + warning('Source .set file for mask not present'); + elseif exist(msk_set, 'file') + warning('Mask .set file already present. skipping'); + else + fprintf('copy %s -> %s\n', src_set, msk_set); + copyfile(src_set, msk_set); + end + + %% save matlab version + fs = struct(); + fs.mask = msk_ary; + fs.corners = msk_corners; + save(mat_name, '-struct', 'fs'); +end \ No newline at end of file diff --git a/stereo/spiv_reconstruct.m b/stereo/spiv_reconstruct.m new file mode 100644 index 0000000000000000000000000000000000000000..00f2d104a94760305014040e5d0bfffee107652b --- /dev/null +++ b/stereo/spiv_reconstruct.m @@ -0,0 +1,218 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% perform reconstruction of stereo PIV vector fields by combining separate +% planar PIV vector fields +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 26/06/2019 +% +% +addpath(genpath([fileparts(mfilename('fullpath')) '/../'])); + +%% input parameters +% source of PIV data +%piv_dt = 35E-6; % hard code PIV delta t for now +%src_dir = 'M:\multiscale\Gen1\U10_x-axis=382.988_x-axis=382.985_x-axis=354.987\C12\'; +src_pat = '%s/B%05d.mat'; +src_range = 1:1000; +src_fun = @(n) sprintf(src_pat, src_dir, n); +% path of camera model +%calib_dir = [src_dir '/../']; +%calib_file = 'pinhole.shifted.mat'; +% destination for processed files +dst_dir = src_dir; +dst_pat = '%s/V%05d.mat'; +dst_fun = @(n) sprintf(dst_pat, dst_dir, n); + +b_plot_recon = false; +b_plot_error = false; + +b_overwrite = true; + +%% emit warning +if b_overwrite + warning('files will be overwritten if they already exist'); +end + +%% load calibration +f_model = [calib_dir calib_file]; +fprintf('Load calibration %s ...\n', f_model); +s_model = load(f_model); + +%% load a sample vector field +src_name = src_fun(src_range(1)); +fprintf('Load %s\n', src_name); +[VC1, VC2] = loadvec_pair(src_name); + +if exist('piv_dt', 'var') + warning('Override delta t, dt = %f us', piv_dt*1E6); +else + piv_dt = VC1.dt; +end + + +% modify axis limits +s_model.camera(1).i_axis = VC1.X(:,1)'; +s_model.camera(1).j_axis = VC1.Y(1,:); +s_model.camera(2).i_axis = VC2.X(:,1)'; +s_model.camera(2).j_axis = VC2.Y(1,:); + +%% build stereo reconstruction model +fprintf(' building calibration model...\n'); +P = [s_model.pl_to_gl, s_model.gl_origin]; +stereo = StereoPlane(s_model.camera, P); +% define grid for stereo vector field interpolation +% average vector spacing +di = s_model.camera(1).i_axis([1 2]) * [-1;1]; +dj = s_model.camera(1).j_axis([1 2]) * [-1;1]; +[u_axis,v_axis] = stereo.make_axes([di dj]); +stereo.axes(u_axis, v_axis); +% rotation +pl_to_gl = stereo.P(:,1:3); +n_u = stereo.n_u; +n_v = stereo.n_v; + +%% loop to process fields +fprintf('------------------------------------------------------------------\n'); +fprintf('PROCESSING START %s\n', datestr(now)); + +n_src = length(src_range); + +for n = 1 : n_src + %% load planar vector fields + src_name = src_fun(src_range(n)); + dst_name = dst_fun(src_range(n)); + fprintf('[%3d of %3d] %s\n', n, n_src, src_name); + if ~exist(src_name, 'file') + warning('source does not exist, skipping'); + continue; + end + if exist(dst_name, 'file') && ~b_overwrite + warning('destination already exists, skipping'); + continue; + end + + [VC1,VC2] = loadvec_pair(src_name); + + % account for PIV delta-t being different in source fields + % this can occur if we require output in pixel units + VC1 = rescale_field(VC1, VC1.dt/piv_dt); + VC2 = rescale_field(VC2, VC2.dt/piv_dt); + + %% extract mask + M1 = (VC1.U==0 & VC1.V == 0) | isnan(VC1.U) | isnan(VC1.V); + M2 = (VC2.U==0 & VC2.V == 0) | isnan(VC2.U) | isnan(VC2.V); + VC1.U(M1) = nan; + VC1.V(M1) = nan; + VC2.U(M2) = nan; + VC2.V(M2) = nan; + + %% reconstruct velocity + % get coordinates of vectors in world system + n_vec = n_u * n_v; + gl_pos = stereo.P * [stereo.um(:) stereo.vm(:) zeros(n_vec,1) ones(n_vec,1)]'; + gl_pos = reshape(gl_pos.', [n_u n_v 3]); + % reconstruct velocity in world system + [~,gl_vel,v1,v2]= stereo.reconstruct(VC1, VC2); + gl_vel = gl_vel / 1E3; + % direction of common measurement vector + e_c = reshape(stereo.map(1).e_c, [3 1 n_u n_v]); + e_c = mtimesx(pl_to_gl, e_c); + b_mask = isnan(gl_vel(:,:,1)); + + %% store in structure + V = struct( 'X', gl_pos(:,:,1), ... + 'Y', gl_pos(:,:,2), ... + 'Z', gl_pos(:,:,3), ... + 'U', gl_vel(:,:,1), ... + 'V', gl_vel(:,:,2), ... + 'W', gl_vel(:,:,3), ... + 'mask', b_mask, ... + 'v1', v1, ... + 'v2', v2, ... + 'e_v', e_c, ... + 'u_axis', stereo.u, ... + 'v_axis', stereo.v, ... + 'P', stereo.P, ... + 'dt', piv_dt); + %% check for crazy answers + if nnz(abs(V.U) > 20) > 0.01*numel(V.U) + warning('it''s happened agaiiiiin...'); + end + + %% save to file + %fprintf(' save %s\n', dst_name); + if exist(dst_name, 'file') && ~b_overwrite + warning('file already exists, will not overwrite'); + else + parsave(dst_name, V);%, '-v7.3', '-nocompression'); + end + + %% plot reconstructed field + if b_plot_recon + figure(1); + clf; + surf(V.X,V.Y,V.Z,V.U,'EdgeColor','none'); + hold on; + rx=1:4:n_u; + ry=1:4:n_v; + quiver3sc(V.X(rx,ry), V.Y(rx,ry), V.Z(rx,ry), V.U(rx,ry), V.V(rx,ry), V.W(rx,ry), 0.5, 'k'); + set(gca,'clim',[0 10]); + axis equal tight xy; + view(0,90); + end + + %% plot residual error in co-measured component + if b_plot_error + figure(2); + clf; + subplot(1,3,1); + imagesc(stereo.u, stereo.v, v1' / 1E3); + axis equal tight xy; + hold on; + ax = gca; + title('Camera 1, v component'); + %quiver(F1.X, F1.Y, F1.U, F1.V, 'k'); + set(gca,'clim',[-2 2]); + + subplot(1,3,2); + imagesc(stereo.u, stereo.v, v2'/ 1E3); + axis equal tight xy; + hold on; + ax(2) = gca; + title('Camera 2, v component'); + %quiver(F2.X, F2.Y, F2.U, F2.V, 'k'); + set(gca,'clim',[-2 2]); + + subplot(1,3,3); + imagesc(stereo.u, stereo.v, (v1-v2)'/1E3); + axis equal tight xy; + title('Difference, v component'); + set(gca,'clim',[-1 1]*2); + set(gca,'colormap',redblue); + ax(3) = gca; + + + linkaxes(ax, 'xy'); + end + + %% measure residual error + continue; + dv = 1/2*(v1-v2); + sv = 1/2*(v1+v2); + dx = mean(diff(stereo.u)); + dy = mean(diff(stereo.v)); + [dvdx,dvdy] = lsqgradient2d(sv, dx, dy); + d2vx = conv2(sv, [-1 2 -1]' / dx^2, 'same'); + d2vy = conv2(sv, [-1 2 -1] / dx^2, 'same'); + + dvdx = conv2(dvdx, ones(10), 'same'); + ok = ~isnan(sv + dvdx + dvdy); + ok(:,1:100)=false; % manually mask shitty region + disp(std(sv(ok))); + disp(std(dv(ok))); + disp(corrcoef(dvdx(ok), dv(ok))); +end + +fprintf('------------------------------------------------------------------\n'); +fprintf('PROCESSING COMPLETE %s\n', datestr(now)); \ No newline at end of file diff --git a/stereo/spiv_reconstruct_davis.m b/stereo/spiv_reconstruct_davis.m new file mode 100644 index 0000000000000000000000000000000000000000..000653bc5a6831a901211cfe355f0f2725b623bf --- /dev/null +++ b/stereo/spiv_reconstruct_davis.m @@ -0,0 +1,184 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% perform reconstruction of stereo PIV vector fields by combining separate +% planar PIV vector fields +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 26/06/2019 +% +addpath(genpath([pwd '/../'])); + +%% input parameters +% source of PIV data +%src_dir = 'H:\davis\multiscale\Gen1\U10\U10_x-axis=82.986_x-axis=82.9855_x-axis=54.9874\SeqPIV_MP(2x32x32_75%ov)_GPU_C12\'; +%src_dir = 'H:\davis\multiscale\Gen1_12\U10\U10_x-axis=266.32_x-axis=266.317_x-axis=238.315/PIV_MP(2x32x32_75%ov)_GPU_C12/'; +src_pat = '%s/B%05d.vc7'; +src_range = 1 : 11; +src_fun = @(n) sprintf(src_pat, src_dir, n); +% path of camera model +calib_dir = [src_dir '/../']; +calib_file = 'pinhole.shifted.mat'; +% destination for processed files +dst_dir = src_dir; +dst_pat = '%s/B%05d.mat'; +dst_fun = @(n) sprintf(dst_pat, dst_dir, n); + +b_plot_recon = false; +b_plot_error = false; + +%% load calibration +f_model = [calib_dir calib_file]; +fprintf('Load calibration %s ...\n', f_model); +s_model = importdata(f_model); + +%% load a sample vector field +src_name = src_fun(src_range(1)); +fprintf('Load %s\n', src_name); +F = readimx(src_name); +VC1 = create2DVec(F.Frames{1}); +VC2 = create2DVec(F.Frames{2}); +% get PIV delta t +attrs = cell2mat(F.Attributes); +i = find(strcmp({attrs.Value}, 'Reference time dt 1')); +name = attrs(i).Name; +name = strrep(name, 'DevDataName', 'DevDataTrace'); +name = strrep(name, 'DevDataAlias', 'DevDataTrace'); +warning('Override delta t'); +piv_dt = 40E-6; double(F.Attributes{strcmp({attrs.Name}, name)}.Value) * 1E-6; + +% modify axis limits +s_model.camera(1).i_axis = VC1.X(:,1)'; +s_model.camera(1).j_axis = VC1.Y(1,:); +s_model.camera(2).i_axis = VC2.X(:,1)'; +s_model.camera(2).j_axis = VC2.Y(1,:); + +%% build stereo reconstruction model +fprintf(' building calibration model...\n'); +P = [s_model.pl_to_gl, s_model.gl_origin]; +stereo = StereoPlane(s_model.camera, P); +% define grid for stereo vector field interpolation +% average vector spacing +di = s_model.camera(1).i_axis([1 2]) * [-1;1]; +dj = s_model.camera(1).j_axis([1 2]) * [-1;1]; +[u_axis,v_axis] = stereo.make_axes([di dj]); +stereo.axes(u_axis, v_axis); +% rotation +pl_to_gl = stereo.P(:,1:3); +n_u = stereo.n_u; +n_v = stereo.n_v; + +%% loop to process fields +fprintf('------------------------------------------------------------------\n'); +fprintf('PROCESSING START %s\n', datestr(now)); + +n_src = length(src_range); + +for n = 1 : n_src + %% load planar vector fields + src_name = src_fun(src_range(n)); + fprintf('[%3d of %3d]:\n', n, n_src); + fprintf(' %s\n', src_name); + F = readimx(src_name); + VC1 = create2DVec(F.Frames{1}); + VC2 = create2DVec(F.Frames{2}); + + % account for PIV delta -t + VC1 = rescale_field(VC1, 1/piv_dt); + VC2 = rescale_field(VC2, 1/piv_dt); + + %% reconstruct velocity + % get coordinates of vectors in world system + + n_vec = n_u * n_v; + gl_pos = stereo.P * [stereo.um(:) stereo.vm(:) zeros(n_vec,1) ones(n_vec,1)]'; + gl_pos = reshape(gl_pos.', [n_u n_v 3]); + % reconstruct velocity in world system + [~,gl_vel,v1,v2]= stereo.reconstruct(VC1, VC2); + gl_vel = gl_vel / 1E3; + % direction of common measurement vector + e_c = reshape(stereo.map(1).e_c, [3 1 n_u n_v]); + e_c = mtimesx(pl_to_gl, e_c); + + %% store in structure + V = struct( 'X', gl_pos(:,:,1), ... + 'Y', gl_pos(:,:,2), ... + 'Z', gl_pos(:,:,3), ... + 'U', gl_vel(:,:,1), ... + 'V', gl_vel(:,:,2), ... + 'W', gl_vel(:,:,3), ... + 'v1', v1, ... + 'v2', v2, ... + 'e_v', e_c, ... + 'P', stereo.P, ... + 'dt', piv_dt); + V.Attributes = F.Attributes; + + %% save to file + dst_name = dst_fun(src_range(n)); + fprintf(' save %s\n', dst_name); + save(dst_name, '-struct', 'V'); + + %% plot reconstructed field + if b_plot_recon + figure(1); + clf; + surf(V.X,V.Y,V.Z,V.U,'EdgeColor','none'); + hold on; + rx=1:4:n_u; + ry=1:4:n_v; + quiver3sc(V.X(rx,ry), V.Y(rx,ry), V.Z(rx,ry), V.U(rx,ry), V.V(rx,ry), V.W(rx,ry), 0.5, 'k'); + set(gca,'clim',[0 10]); + axis equal tight xy; + view(0,90); + end + + %% plot residual error in co-measured component + if b_plot_error + figure(2); + clf; + subplot(1,3,1); + imagesc(stereo.u, stereo.v, v1' / 1E3); + axis equal tight xy; + hold on; + ax = gca; + title('Camera 1, v component'); + %quiver(F1.X, F1.Y, F1.U, F1.V, 'k'); + set(gca,'clim',[-2 2]); + + subplot(1,3,2); + imagesc(stereo.u, stereo.v, v2'/ 1E3); + axis equal tight xy; + hold on; + ax(2) = gca; + title('Camera 2, v component'); + %quiver(F2.X, F2.Y, F2.U, F2.V, 'k'); + set(gca,'clim',[-2 2]); + + subplot(1,3,3); + imagesc(stereo.u, stereo.v, (v1-v2)'/1E3); + axis equal tight xy; + title('Difference, v component'); + set(gca,'clim',[-1 1]*2); + set(gca,'colormap',redblue); + ax(3) = gca; + + + linkaxes(ax, 'xy'); + end + + %% measure residual error + dv = 1/2*(v1-v2); + sv = 1/2*(v1+v2); + dx = mean(diff(stereo.u)); + dy = mean(diff(stereo.v)); + [dvdx,dvdy] = lsqgradient2d(sv, dx, dy); + d2vx = conv2(sv, [-1 2 -1]' / dx^2, 'same'); + d2vy = conv2(sv, [-1 2 -1] / dx^2, 'same'); + + dvdx = conv2(dvdx, ones(10), 'same'); + ok = ~isnan(sv + dvdx + dvdy); + ok(:,1:100)=false; % manually mask shitty region + disp(std(sv(ok))); + disp(std(dv(ok))); + disp(corrcoef(dvdx(ok), dv(ok))); +end diff --git a/stereo/spiv_selfcal.m b/stereo/spiv_selfcal.m new file mode 100644 index 0000000000000000000000000000000000000000..6f66adb9b4dae6277fb772c56f195bc5ec2dea59 --- /dev/null +++ b/stereo/spiv_selfcal.m @@ -0,0 +1,392 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% self-calibration for position of laser sheet +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 26/06/2019 +% +function spiv_selfcal(opts) +addpath(genpath([pwd '/../'])); +addpath([pwd '/../../misc']); + +%% input parameters +% user specified input parameters +% overridden when caller specifies these +b_interactive = nargin < 1; + +% path of images for self calibration +src_dir = 'H:\davis\multiscale\Gen1_123\U10\U10_x-axis=632.986_x-axis=632.985_x-axis=604.987'; +src_pat = '%s/B%05d.im7'; +src_range = 1 : 50 : 1000; +n_src = length(src_range); + +% path of MATLAB calibration file +cal_dir = src_dir; +cal_pat = '%s/plate_900.rotated.mat'; + +% cross correlation options +wsize = [256 256]*1; + +% if called with arguments, extract these +if ~b_interactive + % extract + fn = fieldnames(opts); + for n = 1 : length(fn) + %assignin('base', fn{n}, opts.(fn{n})); + eval(sprintf('%s=opts.(fn{n});', fn{n})); + end +end + +%% derived parameters +src_fun = @(n) sprintf(src_pat, src_dir, n); +cal_file = sprintf(cal_pat, cal_dir); + +%% load camera model +fprintf('loading camera model ...\n'); +s_model = importdata(cal_file); +i_axis = s_model.camera(1).i_axis; +j_axis = s_model.camera(1).j_axis; +c_A = s_model.camera(1).calib; +c_B = s_model.camera(2).calib; + +%% calculate axes for reconstruction +% based on first-guess in model structure +pl_to_gl = s_model.pl_to_gl; +x_0 = s_model.gl_origin; +P_0 = [pl_to_gl, x_0]; +e_n0 = pl_to_gl(:,3); +z0 = dot(x_0, e_n0); +M = [e_n0; -z0]'; + +%% build a stereo PIV reconstruction object +fprintf('Building reconstruction object...\n'); +stereo = StereoPlane(s_model.camera, P_0); +[u_axis,v_axis] = stereo.make_axes([1 1]); % 1 pixel resolution +stereo.axes(u_axis,v_axis,false); + +fprintf('Resolution: %5.1f x %5.1f px/mm\n', stereo.scale); +fprintf('ROI size: %5.1f x %5.1f mm\n', stereo.W, stereo.H); +fprintf(' %5d x %5d px\n', stereo.n_u, stereo.n_v); + +%% set up grid for sum-of-correlation +n_windows = floor([stereo.n_u stereo.n_v]./wsize); +ii_window = (1:wsize(1)) - wsize(1)/2; +jj_window = (1:wsize(2)) - wsize(2)/2; + +ii0 = floor(mod(stereo.n_u, wsize(1))/2); +jj0 = floor(mod(stereo.n_v, wsize(2))/2); +ir = ii0 + (1 : n_windows(1)*wsize(1)); +jr = jj0 + (1 : n_windows(2)*wsize(2)); +ii_ctr = ir(1 : wsize(1) : end) + wsize(1)/2; +jj_ctr = jr(1 : wsize(2) : end) + wsize(2)/2; +u_ctr = stereo.u(ii_ctr); +v_ctr = stereo.v(jj_ctr); + +window = @(x) permute(reshape(x, [wsize(1) n_windows(1) wsize(2) n_windows(2)]), [1 3 2 4]); +mosaic = @(x) reshape(permute(x, [1 3 2 4]), [wsize(1)*n_windows(1) wsize(2)*n_windows(2)]); + +C_AB_mean = zeros([wsize n_windows]); +C_AA_mean = zeros([wsize n_windows]); +C_BB_mean = zeros([wsize n_windows]); +I_A_mean = zeros([wsize n_windows]); +I_B_mean = zeros([wsize n_windows]); + +%% iterate +for n = 1 : n_src + %% load image + src_name = src_fun(src_range(n)); + fprintf('[%3d of %3d]\n', n, n_src); + fprintf(' load %s ...\n', src_name); + imx = readimx(src_name); + imA = double(imx.Frames{1}.Components{1}.Planes{1}); + imB = double(imx.Frames{3}.Components{1}.Planes{1}); + + %% dewarp image + fprintf(' dewarping ...\n'); + pl_imA = interpn(i_axis, j_axis, imA, stereo.map(1).im, stereo.map(1).jm, 'linear', 0); + pl_imB = interpn(i_axis, j_axis, imB, stereo.map(2).im, stereo.map(2).jm, 'linear', 0); + + %% cross-correlate + fprintf(' cross correlating ...\n'); + imA_window = window(pl_imA(ir,jr)); + imB_window = window(pl_imB(ir,jr)); + % fourier transform + imA_ft = fft2(imA_window); + imB_ft = fft2(imB_window); + % subtract mean + imA_ft(1,1,:,:)=0; + imB_ft(1,1,:,:)=0; + % measure energy + %E_A = squeeze(sum(sum(abs(imA_ft).^2, 2),1)) / prod(wsize); + %E_B = squeeze(sum(sum(abs(imB_ft).^2, 2),1)) / prod(wsize); + % correlate + C_AB = imA_ft .* conj(imB_ft); + C_AA = abs(imA_ft).^2; + C_BB = abs(imB_ft).^2; + % add to mean + C_AB_mean = C_AB_mean + C_AB / n_src; + C_AA_mean = C_AA_mean + C_AA / n_src; + C_BB_mean = C_BB_mean + C_BB / n_src; + %E_A_mean = E_A_mean + E_A / n_src; + %E_B_mean = E_B_mean + E_B / n_src; + I_A_mean = I_A_mean + imA_ft / n_src; + I_B_mean = I_B_mean + imB_ft / n_src; +end + +%% inverse transform to get correlation +R_AB_mean = ifft2(C_AB_mean - I_A_mean .* conj(I_B_mean), 'symmetric'); +R_AA_mean = ifft2(C_AA_mean - I_A_mean .* conj(I_A_mean), 'symmetric'); +R_BB_mean = ifft2(C_BB_mean - I_B_mean .* conj(I_B_mean), 'symmetric'); +% shift correlation, peak now occurs relative to index wsize/2+1 +R_AB_mean = fftshift(fftshift(R_AB_mean,2),1); +R_AA_mean = fftshift(fftshift(R_AA_mean,2),1); +R_BB_mean = fftshift(fftshift(R_BB_mean,2),1); +% normalise correlation with average energy +E_A_mean = squeeze(sum(sum(C_AA_mean,1),2)) / prod(wsize); +E_B_mean = squeeze(sum(sum(C_BB_mean,1),2)) / prod(wsize); +R_AB_norm = R_AB_mean./sqrt(shiftdim(E_A_mean.*E_B_mean,-2)); + +%% measure correlation peak +fprintf('Measuring correlation peak ...\n'); +uv_shift = zeros([2 n_windows]); +pk_mag = zeros(n_windows); +sig_fit = zeros([2 n_windows]); +du = stereo.u(2) - stereo.u(1); +dv = stereo.v(2) - stereo.v(1); +duv = diag([du dv]); +for kk = 1 : prod(n_windows) + [pk_loc,pk_mag(kk),sx,sy] = gauss2d_peaklocate(R_AB_norm(:,:,kk)); + uv_shift(:,kk) = duv * (pk_loc - (wsize(:)/2 + 1)); + sig_fit(1,kk) = sx; + sig_fit(2,kk) = sy; +end +u_shift = squeeze(uv_shift(1,:,:)); +v_shift = squeeze(uv_shift(2,:,:)); +pl_sigma = diag([du dv]) * reshape(sig_fit, [2 prod(n_windows)]); +pl_sigma = reshape(pl_sigma, [2 n_windows]); + +%% map correspondences to image coordinates +% define mask +b_mask = true(n_windows); +b_mask(:,[1:3 end]) = false; +%b_mask([1 end],:) = false; + +% centers on reconstruction plane +[um, vm] = ndgrid(u_ctr, v_ctr); +uv_ctr = [um(:) vm(:)].'; +% correct point correspondences +uv_ctr_A = uv_ctr + 1/2 *[uv_shift(1,:); uv_shift(2,:)]; +uv_ctr_B = uv_ctr - 1/2 *[uv_shift(1,:); uv_shift(2,:)]; +% project to image plane +n_vec = prod(n_windows); +gl_ctr = P_0 * [uv_ctr; zeros(1,n_vec); ones(1,n_vec)]; +gl_ctr_A = P_0 * [uv_ctr_A; zeros(1,n_vec); ones(1,n_vec)]; +gl_ctr_B = P_0 * [uv_ctr_B; zeros(1,n_vec); ones(1,n_vec)]; +im_ctr_A = pinhole_model(gl_ctr_A, c_A); +im_ctr_B = pinhole_model(gl_ctr_B, c_B); +im_ctr = cat(3, im_ctr_A, im_ctr_B); +% triangulate +[gl_ctr_new,gl_dist,im_dist]=triangulate(s_model.camera, im_ctr, true); + +%% solve for new best fit plane in the least squares sense +% excluding masked points +ok = b_mask(:) & ~any(isnan(gl_ctr_new),1)'; +n_ok = nnz(ok); +u_shift_rms = rms(u_shift(ok)); +v_shift_rms = rms(v_shift(ok)); + +% solve +%M_new = fit_plane(gl_ctr_new(:,ok), M(1:3)); +M_new = robust_fit_plane(gl_ctr_new(:,ok)); +% preserve sense of orientation of sheet +M_new = M_new * sign(dot(M_new(1:3), M(1:3))); + +%% extract rotation of laser sheet +alpha = acosd(dot(M_new(1:3), M(1:3))); +e_rot = cross(M(1:3), M_new(1:3)); +e_rot = e_rot / norm(e_rot); +dz = M_new(4) - M(4); + +%% measure new residual +% define new coordinate system +[pl_to_gl_new, x_0_new] = define_planar_coord_system(M_new, [0 1 0]'); +% back-project point correspondences onto new measurement plane +gl_bp_A = back_project_1c(c_A, M_new, im_ctr_A, true); +gl_bp_B = back_project_1c(c_B, M_new, im_ctr_B, true); +% convert to plane coordinate system +pl_bp_A = pl_to_gl_new \ bsxfun(@minus, gl_bp_A, x_0_new); +pl_bp_B = pl_to_gl_new \ bsxfun(@minus, gl_bp_B, x_0_new); +% measure disparity in plane +pl_disparity = pl_bp_A - pl_bp_B; +u_new_rms = rms(pl_disparity(1,ok)); +v_new_rms = rms(pl_disparity(2,ok)); +%w_new_rms = rms(pl_disparity(3,ok)); + +%% corners of new fit plane +[uc,vc,wc] = ndgrid(u_axis([1 end]), v_axis([1 end]), 0); +gl_corners = pl_to_gl_new*[uc([1 2 4 3]); vc([1 2 4 3]); wc([1 2 4 3])] + x_0_new; + +%% save laser sheet measurement in structure +sheet = struct(); +sheet.gl_pos = gl_ctr_new(:,ok); % position of points on sheet +sheet.pl_sigma = pl_sigma(:,ok); % peak width +sheet.pl_to_gl = pl_to_gl_new; % orientation of sheet +sheet.gl_origin = x_0_new; % origin of sheet + +% point correspondences +sheet.im_pos_A = im_ctr_A(:,ok); % points on image A +sheet.im_pos_B = im_ctr_B(:,ok); % points on image B + +%% report +fprintf('------------------------------------------------------------------\n'); +fprintf('SELF CALIBRATION COMPLETE\n\n'); +fprintf('RMS disparity:\n'); +fprintf(' before:\n'); +fprintf(' u = %0.3f mm [%0.3f px]\n', u_shift_rms, u_shift_rms / du); +fprintf(' v = %0.3f mm [%0.3f px]\n', v_shift_rms, v_shift_rms / dv); +fprintf(' after:\n'); +fprintf(' u = %0.3f mm [%0.3f px]\n', u_new_rms, u_new_rms / du); +fprintf(' v = %0.3f mm [%0.3f px]\n', v_new_rms, v_new_rms / dv); + +fprintf('Old plane:\n'); +fprintf(' normal %6.3f %6.3f %6.3f\n', M(1:3)); +fprintf(' shift %6.3f mm\n', M(4)); +fprintf('New plane:\n'); +fprintf(' normal %6.3f %6.3f %6.3f\n', M_new(1:3)); +fprintf(' shift %6.3f mm\n', M_new(4)); +fprintf('Rotation:\n'); +fprintf(' %6.3f degrees about %6.3f %6.3f %6.3f\n', alpha, e_rot); +fprintf('Shift:\n'); +fprintf(' %6.3f mm\n', M_new(4)-M(4)); + +%% plot plane fit +if b_interactive + figure(10); + clf; + plot3(gl_ctr(1,:), gl_ctr(2,:), gl_ctr(3,:), 'rx', 'DisplayName', 'old'); + + hold on; + plot3(gl_ctr_new(1,:), gl_ctr_new(2,:), gl_ctr_new(3,:), 'k.', 'DisplayName', 'new'); + plot3(gl_ctr_new(1,ok), gl_ctr_new(2,ok), gl_ctr_new(3,ok), 'ko', 'DisplayName', 'new'); + + xl = xlim; + yl = ylim; + + + %x_fit = xl([1 1 2 2]); + %y_fit = yl([1 2 2 1]); + %z_fit = (-M_new(4) - M_new(1)*x_fit - M_new(2)*y_fit)/M_new(3); + patch(gl_corners(1,:),gl_corners(2,:),gl_corners(3,:),1,'facecolor', 'b', 'facealpha', 0.5); +end + +%% plot correlation +if b_interactive + figure(1); + clf; + R_AB_mosaic = mosaic(R_AB_norm); + imagesc(stereo.u(ir), stereo.v(jr), R_AB_mosaic'); + axis equal tight xy + set(gca,'clim', [0 0.08]); + title('Mean of image correlation'); +end + +%% plot sample correlation +if b_interactive + figure(2); + clf; + ii = floor(n_windows(1)/2); + jj = floor(n_windows(2)/2); + C = R_AB_norm(:,:,ii,jj); + + du_ax = du*((0:wsize(1)-1)-wsize(1)/2); + dv_ax = dv*((0:wsize(2)-1)-wsize(2)/2); + [dum,dvm]= ndgrid(du_ax,dv_ax); + + plot3(dum, dvm, C, 'k.'); + %surf(du_ax, dv_ax, C'); + + title('Correlation at center of image'); + view(3); + + [~,~,~,~,C_fit] = gauss2d_peaklocate(C); + + hold on; + S_fit = surf(dum', dvm', C_fit'); + set(S_fit, 'facealpha', 0.5, 'edgecolor', 'none'); +end + +%% display dewarped images +if b_interactive + figure(3); + clf; + subplot(1,2,1); + ax = gca; + imagesc(stereo.u, stereo.v, pl_imA'); + axis equal xy; + xlim(stereo.u([1 end])); + ylim(stereo.v([1 end])); + title('Camera 1, dewarped'); + %ylim(j_axis([end-512 end])); + %xlim(i_axis([1 end])); + hold on; + plot(um(:), vm(:), 'ro'); + quiversc(um, vm, u_shift, v_shift, 10, 'r-'); + + subplot(1,2,2); + ax(2)=gca; + imagesc(stereo.u, stereo.v, pl_imB'); + axis equal xy; + xlim(stereo.u([1 end])); + ylim(stereo.v([1 end])); + title('Camera 2, dewarped'); + hold on; + plot(um+u_shift, vm+v_shift, 'ro'); + + %quiver(um, vm,-squeeze(uv_shift(1,:,:)),-squeeze(uv_shift(2,:,:)), 'r-'); + + linkaxes(ax, 'xy'); +end + +%% plot raw images +if b_interactive + figure(4); + clf; + subplot(1,2,1); + imagesc(i_axis, j_axis, imA'); + axis equal tight ij; + hold on; + plot(im_ctr_A(1,:), im_ctr_A(2,:), 'ro'); + title('Camera 2, raw'); + + subplot(1,2,2); + imagesc(i_axis, j_axis, imB'); + axis equal tight ij; + hold on; + plot(im_ctr_B(1,:), im_ctr_B(2,:), 'ro'); + title('Camera 1, raw'); +end + +%% save result to file +b_save = true; +if b_interactive + b_save = input('Save new calibration to file? y/N ', 's'); + b_save = strcmpi(b_save, 'Y'); +end + +if b_save + %% build new model and save + fprintf('Saving %s\n', cal_file); + + % store + s_model.pl_to_gl = pl_to_gl_new; + s_model.gl_origin = x_0_new; + s_model.sheet = sheet; + % update self-calibration counter + if isfield(s_model, 'n_selfcal_iter') + s_model.n_selfcal_iter = s_model.n_selfcal_iter + 1; + else + s_model.n_selfcal_iter = 1; + end + + save(cal_file, '-struct', 's_model'); +end \ No newline at end of file diff --git a/stereo/spiv_selfcal_camera.m b/stereo/spiv_selfcal_camera.m new file mode 100644 index 0000000000000000000000000000000000000000..e3194faaa33978c51bad6740865cb25c15befa82 --- /dev/null +++ b/stereo/spiv_selfcal_camera.m @@ -0,0 +1,256 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% manual self-calibration for correction of camera model +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 04/07/2019 +% + +% path of MATLAB calibration file +cal_dir = 'H:\davis\multiscale\Gen1_123\U10\U10_x-axis=632.986_x-axis=632.985_x-axis=604.987'; +cal_pat = '%s/plate_900.mat'; +cal_pat_new = '%s/plate_900.rotated.mat'; +cal_file = sprintf(cal_pat, cal_dir); +cal_file_new = sprintf(cal_pat_new, cal_dir); + +% plot options +c_lim = [0 512]; +y_lim = [4871-750 4871]; +font_size = 24; + +%% load camera model +fprintf('------------------------------------------------------------------\n'); +fprintf('loading camera model\n'); +s_model = importdata(cal_file); +i_axis = s_model.camera(1).i_axis; +j_axis = s_model.camera(1).j_axis; +cmodel = s_model.camera; +c_A = cmodel(1).calib; +c_B = cmodel(2).calib; + +im_size = [length(i_axis) length(j_axis)]; + +% sheet model +pl_to_gl = s_model.sheet.pl_to_gl; +x_0 = s_model.sheet.gl_origin; +M = [pl_to_gl(:,3); -dot(x_0, pl_to_gl(:,3))]'; + +% point correspondnces on sheet +im_pos_A = s_model.sheet.im_pos_A; +im_pos_B = s_model.sheet.im_pos_B; +gl_old = s_model.sheet.gl_pos; +im_pos = cat(3, im_pos_A, im_pos_B); +n_points = size(im_pos_A,2); + +pl_corners = pl_to_gl^-1 * (gl_old - s_model.sheet.gl_origin); +u_lim = [min(pl_corners(1,:)) max(pl_corners(1,:))]; +v_lim = [min(pl_corners(2,:)) max(pl_corners(2,:))]; + +%% test model +[gl_pos,gl_dist,im_dist]= triangulate(cmodel, im_pos, true); +d = M * [gl_pos; ones(1, n_points)]; +d_max = max(abs(d)); +d_rms = rms(d); + +fprintf('before self-calibration:\n'); +fprintf(' rms triangulation error: %6.3f mm\n', rms(gl_dist(:))); +fprintf(' in camera 1: %6.3f px\n', rms(im_dist(:,1))); +fprintf(' in camera 2: %6.3f px\n', rms(im_dist(:,2))); +fprintf(' max deviation: %6.3f mm\n', d_max); +fprintf(' rms deviation: %6.3f mm\n', d_rms); + +%% optimise rotation of first camera to match observation +% we try to minimise the triangulation error by adjusting the rotation +% matrix of one camera +X0 = [0 0 0] * eps; +Gamma = 1E3; % regularisation factor +cfun = @(X) triangulation_cost(X, cmodel, im_pos, true, Gamma); +X = lsqnonlin(cfun, X0); + +%% make new model +[~,newmodel] = triangulation_cost(X, cmodel, im_pos, true, Gamma); + +% extract rotation +R1p = newmodel(1).calib.R; +R2p = newmodel(2).calib.R; +Omega1 = R1p * cmodel(1).calib.R^-1; +Omega2 = R2p * cmodel(2).calib.R^-1; +omega1 = vrrotmat2vec(Omega1); +omega2 = vrrotmat2vec(Omega2); +e1 = omega1(1:3); +theta1 = omega1(4); +e2 = omega2(1:3); +theta2 = omega2(4); +% extract shift +dx1 = newmodel(1).calib.xc - cmodel(1).calib.xc; +dx2 = newmodel(2).calib.xc - cmodel(2).calib.xc; + +% relative rotation of cameras +R12 = R2p * R1p^-1; +r12 = vrrotmat2vec(R12); +theta12 = r12(4); +e12 = r12(1:3) / norm(r12(1:3)); + +% fit new model of laser sheet +[gl_pos,gl_dist,im_dist]= triangulate(newmodel, im_pos, true); +M_new = fit_plane(gl_pos); +% ensure sense is same +M_new = M_new * sign(dot(M_new(1:3), M(1:3))); + +[pl_to_gl_new, x_0_new] = define_planar_coord_system(M_new, [0 1 0]'); +dp = M_new * [gl_pos; ones(1, n_points)]; +dp_max = max(abs(dp)); +dp_rms = rms(d); + +%% corners of new fit plane +[uc,vc,wc] = ndgrid(u_lim([1 end]), v_lim([1 end]), 0); +gl_corners = pl_to_gl_new*[uc([1 2 4 3]); vc([1 2 4 3]); wc([1 2 4 3])] + x_0_new; + +%% test new model +fprintf('after self-calibration:\n'); +fprintf(' C1 rotates %8.3f deg about %6.3f %6.3f %6.3f\n', theta1/pi*180, e1); +fprintf(' C2 rotates %8.3f deg about %6.3f %6.3f %6.3f\n', theta2/pi*180, e2); +fprintf(' C1->C2 %8.3f deg about %6.3f %6.3f %6.3f\n', theta12/pi*180, e12); +fprintf(' C1 translates %6.3f %6.3f %6.3f mm\n', dx1); +fprintf(' C2 translates %6.3f %6.3f %6.3f mm\n', dx2); +fprintf(' sheet normal %6.3f %6.3f %6.3f\n', M_new(1:3)); +fprintf(' sheet shift %6.3f mm\n', M_new(4)); +fprintf('\n'); +fprintf(' rms triangulation error: %6.3f mm\n', rms(gl_dist(:))); +fprintf(' in camera 1: %6.3f px\n', rms(im_dist(:,1))); +fprintf(' in camera 2: %6.3f px\n', rms(im_dist(:,2))); + +fprintf(' max deviation: %6.3f mm\n', dp_max); +fprintf(' rms deviation: %6.3f mm\n', dp_rms); + +%% plot plane fit +figure(10); +clf; +% current points +plot3(gl_pos(1,:), gl_pos(2,:), gl_pos(3,:), 'rx'); +hold on; +% old estimate of points +plot3(gl_old(1,:), gl_old(2,:), gl_old(3,:), 'ko'); +patch(gl_corners(1,:),gl_corners(2,:),gl_corners(3,:),1,'facecolor', 'b', 'facealpha', 0.5); +daspect(1 ./ (M_new(1:3)+0.05)); +xlabel('x'); +ylabel('y'); +zlabel('z'); + +%% save +fprintf('save to %s\n', cal_file_new); +s_model.camera = newmodel; +s_model.sheet.gl_pos = gl_pos; +s_model.sheet.pl_to_gl = pl_to_gl_new; +s_model.sheet.gl_origin = x_0_new; +s_model.pl_to_gl = pl_to_gl_new; +s_model.gl_origin = x_0_new; +save(cal_file_new, '-struct', 's_model'); + +%{ +error('stop'); + +%% convert to canonical camera model +R12 = cmodel(2).calib.R * R1p^-1; +x12 = cmodel(2).calib.xc - cmodel(1).calib.xc; + +rmodel = newmodel; +rmodel(1).calib.xc = [0 0 0]'; +rmodel(2).calib.xc = R1p * x12; +rmodel(1).calib.R = eye(3); +rmodel(2).calib.R = R12; + +rmodel(1).calib.P = rmodel(1).calib.G * [rmodel(1).calib.R -rmodel(1).calib.xc]; +rmodel(2).calib.P = rmodel(2).calib.G * [rmodel(2).calib.R -rmodel(2).calib.xc]; + +%% + +% the camera chosen should not matter, since + +% see Hartley & Zisserman chapters 9-11 + +% get intrinsic parameters from camera calibration +G_A = c_A.G; +G_B = c_B.G; +% correct for radial distortion +dw_pos_A = inverse_radial_distortion(im_pos_A, c_A.ic, c_A.kr); +dw_pos_B = inverse_radial_distortion(im_pos_B, c_B.ic, c_B.kr); + +% compute essential matrix E +cpA = cameraParameters('IntrinsicMatrix', G_A'); +cpB = cameraParameters('IntrinsicMatrix', G_B'); +[E,inliers,status] = estimateEssentialMatrix(dw_pos_A.', dw_pos_B.', cpA, cpB, 'MaxDistance', 1); + +% compute +F = estimateFundamentalMatrix(dw_pos_A.', dw_pos_B.'); +Ep = G_B.' * F * G_A; + +fprintf('Computed essential matrix, status %d, %d / %d inliers\n', status, nnz(inliers), n_points); +[U,D,V] = svd(E); +d = diag(D); +assert(norm(d - [1 1 0]') < 1E-10); % svds of E should be 1,1,0 (approx) + +PA = [eye(3), [0;0;0]]; +% following Hartley &Zisserman section 9.6.2, p258 +% extract the canonical rotation matrix and camera baseline from the +% essential matrix + +W = [0 -1 0; 1 0 0; 0 0 1]; % (9.13) +Z = [0 1 0; -1 0 0; 0 0 0]; % (9.13) +S = U*Z*U.'; % (9.14) +R1 = U*W*V.'; % (9.14) +R2 = U*(W.')*(V.'); % (9.14) + + +%% load image +fprintf('------------------------------------------------------------------\n'); +fprintf('loading images ...\n'); +im_min = arrayfun(@(x) inf(im_size), 1:n_frames, 'uniformoutput', false); +im_max = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); +im_mean = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); +im_ms = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); + +for n = 1 : n_src + %% load image + src_name = src_fun(src_range(n)); + fprintf('[%3d of %3d] %s ...\n', n, n_src, src_name); + imx = readimx(src_name); + im_ary = cellfun(@(f) double(f.Components{1}.Planes{1}), imx.Frames, 'uniformoutput', false); + + for f = 1 : n_frames + im_min{f} = min(im_ary{f}, im_min{f}); + im_max{f} = max(im_ary{f}, im_max{f}); + im_mean{f} = im_mean{f} + im_ary{f} / n_src; + im_ms{f} = im_ms{f} + im_ary{f}.^2 / n_src; + end +end + +%% select min/max/mean/std +imA = im_min{1}; +imB = im_min{3}; + +%% draw +figure(1); +clf; +subplot(2,1,1); +ax=gca; +imagesc(i_axis, j_axis, imA'); +axis equal tight ij; +hold on; +plot(im_pos_A(1,:), im_pos_A(2,:), 'ro'); +ylim(y_lim); +set(gca, 'clim', c_lim); +title('Camera 1', 'fontsize', font_size); + + +subplot(2,1,2); +imagesc(i_axis, j_axis, imB'); +ax(2)=gca; +axis equal tight ij; +hold on; +plot(im_pos_B(1,:), im_pos_B(2,:), 'ro'); +ylim(y_lim); + +set(gca, 'clim', c_lim); +title('Camera 2', 'fontsize', font_size); +%} \ No newline at end of file diff --git a/stereo/spiv_selfcal_manual.m b/stereo/spiv_selfcal_manual.m new file mode 100644 index 0000000000000000000000000000000000000000..cfc79844668b8c748f446b00f9efcc5b26652036 --- /dev/null +++ b/stereo/spiv_selfcal_manual.m @@ -0,0 +1,167 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% manual self-calibration for correction of camera model +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 04/07/2019 +% + +% sample images for self calibration +src_dir = 'H:\davis\multiscale\Gen1\U10\U10_x-axis=316.313_x-axis=316.32_x-axis=288.317'; +src_pat = '%s/B%05d.im7'; +src_range = 1 : 50 : 1000; +src_fun = @(n) sprintf(src_pat, src_dir, n); +n_src = length(src_range); +n_frames = 4; +i_cam = 1; + +% path of MATLAB calibration file +cal_dir = src_dir; +cal_pat = '%s/pinhole.rotated.mat'; +cal_file = sprintf(cal_pat, cal_dir); + +% plot options +c_lim = [0 512]; +y_lim = [4871-750 4871]; +font_size = 24; + +%% load camera model +fprintf('------------------------------------------------------------------\n'); +fprintf('loading model for camera %d\n', i_cam); +s_model = importdata(cal_file); +i_axis = s_model.camera(i_cam).i_axis; +j_axis = s_model.camera(i_cam).j_axis; +cmodel = s_model.camera(i_cam).calib; +im_size = [length(i_axis) length(j_axis)]; + +%% load image +fprintf('------------------------------------------------------------------\n'); +fprintf('loading images ...\n'); +im_min = arrayfun(@(x) inf(im_size), 1:n_frames, 'uniformoutput', false); +im_max = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); +im_mean = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); +im_ms = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); + +for n = 1 : n_src + %% load image + src_name = src_fun(src_range(n)); + fprintf('[%3d of %3d] %s ...\n', n, n_src, src_name); + imx = readimx(src_name); + im_ary = cellfun(@(f) double(f.Components{1}.Planes{1}), imx.Frames, 'uniformoutput', false); + + for f = 1 : n_frames + im_min{f} = min(im_ary{f}, im_min{f}); + im_max{f} = max(im_ary{f}, im_max{f}); + im_mean{f} = im_mean{f} + im_ary{f} / n_src; + im_ms{f} = im_ms{f} + im_ary{f}.^2 / n_src; + end +end + +%% select min/max/mean/std +imA = im_min{(i_cam-1)*2+1}; +imB = s_model.camera(i_cam).calib.im; +% prefilter +wsize = [15 15]; +G_log = fspecial('log', wsize, wsize(1)/4); +imA = conv2(imA, G_log, 'same'); +imB = conv2(imB, G_log, 'same'); + +%% draw +figure(1); +clf; +subplot(2,1,1); +ax=gca; +imagesc(i_axis, j_axis, imA'); +axis equal tight ij; +ylim(y_lim); +%set(gca, 'clim', c_lim); +set(gca, 'clim', [-1 1]*0.5); + +title('Calibration image', 'fontsize', font_size); +hold on; + +subplot(2,1,2); +imagesc(i_axis, j_axis, imB'); +ax(2)=gca; +axis equal tight ij; +ylim(y_lim); +hold on; +%set(gca, 'clim', c_lim); +set(gca, 'clim', [-1 1]*0.5); +title('Measurement', 'fontsize', font_size); + +%% prompt user to identify point correspondences +fprintf('------------------------------------------------------------------\n'); +fprintf('Draw pairs of point correspondences\n'); +fprintf('Left click to add correspondences\n'); +fprintf('Right click to stop\n'); + +imA_pts = []; +imB_pts = []; +n_points = 0; +delete(findobj(ax,'type','text')); + +while true + %% get a pair of correspondences + % camera 1 + axes(ax(1)); %#ok<LAXES> + [x1,y1,button] = ginput(1); + if button ~= 1 + break; + end + text(x1, y1, num2str(n_points+1)); + plot(x1, y1, 'ko'); + + % camera 2 + axes(ax(2)); %#ok<LAXES> + [x2,y2,button] = ginput(1); + if button ~= 1 + break; + end + text(x2, y2, num2str(n_points+1)); + plot(x2, y2, 'ko'); + + %% add to array + n_points = n_points + 1; + imA_pts = [imA_pts, [x1;y1]]; %#ok<AGROW> + imB_pts = [imB_pts, [x2;y2]]; %#ok<AGROW> + fprintf('[%d] (%0.1f, %0.1f) <-> (%0.1f, %0.1f)\n', n_points, x1, y1, x2, y2); +end + +%% report +fprintf('identified %d correspondences\n', n_points); +fprintf('shift:\n'); +dij = imB_pts - imA_pts; +for n = 1 : n_points + fprintf('[%d] %6.3f, %6.3f px\n', n, dij(:,n)); +end + +%% measure unit vectors +G = cmodel.G; +y_ref_tilde = [imA_pts; ones(1, n_points)]; +y_rot_tilde = [imB_pts; ones(1, n_points)]; +r_ref = unitvector(G \ y_ref_tilde); +r_rot = unitvector(G \ y_rot_tilde); + +sint0 = sqrt(sum(cross(r_rot, r_ref, 1).^2, 1)); + +[e_rot, sint] = unitvector(mean(cross(r_rot, r_ref, 1), 2)); +theta = asin(sint); +Omega = vrrotvec2mat([e_rot; theta]); + +cost_residual = dot(r_rot, Omega * r_ref, 1); +theta_residual = acos(cost_residual); +theta_rms = rms(theta_residual); + +fprintf('rotate %6.3f deg about %6.3f %6.3f %6.3f\n', theta/pi*180, e_rot); + +%% modify camera model +cmodel.R = Omega * cmodel.R; +cmodel.P = cmodel.G * cmodel.R * [eye(3), -cmodel.xc]; +s_model.camera(i_cam).calib = cmodel; + +s = input('Write new calibration to file? [y/N]', 's'); +b_ok = strcmpi(s, 'y'); +if b_ok + save(cal_file, '-struct', 's_model'); +end diff --git a/stereo/spiv_selfcal_manual_shift.m b/stereo/spiv_selfcal_manual_shift.m new file mode 100644 index 0000000000000000000000000000000000000000..9f187017c058aa47368fe0dd9f309e86266c1fd0 --- /dev/null +++ b/stereo/spiv_selfcal_manual_shift.m @@ -0,0 +1,149 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% manual self-calibration for correction of camera model +% +% Author: John Lawson +% j.m.lawson@soton.ac.uk +% Created: 04/07/2019 +% + +calib_dir = 'H:\davis\multiscale\Properties\Calibration History\Gen1_mid_33.33\'; + +% sample images for self calibration +src_dir = 'H:\davis\multiscale\Gen1\U10\U10_x-axis=257.986_x-axis=257.985_x-axis=229.987\'; +src_pat = '%s/B%05d.im7'; +src_range = 1 : 50 : 1000; +src_fun = @(n) sprintf(src_pat, src_dir, n); +n_src = length(src_range); +n_frames = 4; +% path of MATLAB calibration file +cal_dir = src_dir; +cal_pat = '%s/pinhole.mat'; +cal_file = sprintf(cal_pat, cal_dir); + +% plot options +c_lim = [0 512]; +y_lim = [4871-750 4871]; +font_size = 24; + +%% load camera model +fprintf('loading camera model ...\n'); +s_model = importdata(cal_file); +i_axis = s_model.camera(1).i_axis; +j_axis = s_model.camera(1).j_axis; +c_A = s_model.camera(1).calib; +c_B = s_model.camera(2).calib; +im_size = [length(i_axis) length(j_axis)]; + +%% load image +fprintf('loading images ...\n'); +im_min = arrayfun(@(x) inf(im_size), 1:n_frames, 'uniformoutput', false); +im_max = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); +im_mean = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); +im_ms = arrayfun(@(x) zeros(im_size), 1:n_frames, 'uniformoutput', false); + +for n = 1 : n_src + %% load image + src_name = src_fun(src_range(n)); + fprintf('[%3d of %3d] %s ...\n', n, n_src, src_name); + imx = readimx(src_name); + im_ary = cellfun(@(f) double(f.Components{1}.Planes{1}), imx.Frames, 'uniformoutput', false); + + for f = 1 : n_frames + im_min{f} = min(im_ary{f}, im_min{f}); + im_max{f} = max(im_ary{f}, im_max{f}); + im_mean{f} = im_mean{f} + im_ary{f} / n_src; + im_ms{f} = im_ms{f} + im_ary{f}.^2 / n_src; + end +end + +%% process +fprintf('processing ...\n'); +wsize = [33 33]; +G = ones(wsize)/prod(wsize); + +im_var = cell(1,n_frames); +im_std = cell(1,n_frames); + +for f = 1 : n_frames + im_var{f} = max(0,im_ms{f} - im_mean{f}.^2); + im_std{f} = sqrt(im_var{f}); +end + +%% select min/max/mean/std +imA = im_min{1}; +imB = s_model.camera(1).calib.im; + +%% draw +figure(1); +clf; +subplot(2,1,1); +ax=gca; +imagesc(i_axis, j_axis, imA'); +axis equal tight ij; +ylim(y_lim); +set(gca, 'clim', c_lim); +title('Camera 1', 'fontsize', font_size); +hold on; + +subplot(2,1,2); +imagesc(i_axis, j_axis, imB'); +ax(2)=gca; +axis equal tight ij; +ylim(y_lim); +hold on; +set(gca, 'clim', c_lim); +title('Camera 2', 'fontsize', font_size); + +%% prompt user to identify point correspondences +fprintf('------------------------------------------------------------------\n'); +fprintf('Draw pairs of point correspondences\n'); +fprintf('Left click to add correspondences\n'); +fprintf('Right click to stop\n'); + +imA_pts = []; +imB_pts = []; +n_points = 0; +delete(findobj(ax,'type','text')); + +while true + %% get a pair of correspondences + % camera 1 + axes(ax(1)); %#ok<LAXES> + [x1,y1,button] = ginput(1); + if button ~= 1 + break; + end + text(x1, y1, num2str(n_points+1)); + plot(x1, y1, 'ko'); + + % camera 2 + axes(ax(2)); %#ok<LAXES> + [x2,y2,button] = ginput(1); + if button ~= 1 + break; + end + text(x2, y2, num2str(n_points+1)); + plot(x2, y2, 'ko'); + + %% add to array + n_points = n_points + 1; + imA_pts = [imA_pts, [x1;y1]]; + imB_pts = [imB_pts, [x2;y2]]; + fprintf('[%d] (%0.1f, %0.1f) <-> (%0.1f, %0.1f)\n', n_points, x1, y1, x2, y2); +end + +%% triangulate +if n_points < 2 + error('Too few points selected to proceed'); +end +im_pts = cat(3, imA_pts, imB_pts); + +% triangulate +[gl_pts, gl_dist, ij_dist] = triangulate(s_model.camera, im_pts); +% reproject +im_proj_A = pinhole_model(gl_pts, s_model.camera(1).calib, true); +im_proj_B = pinhole_model(gl_pts, s_model.camera(2).calib, true); + +% plot +plot(ax(1), im_proj_A(1,:), im_proj_A(2,:), 'ro'); +plot(ax(2), im_proj_B(1,:), im_proj_B(2,:), 'ro'); diff --git a/stereo/triangulation_cost.m b/stereo/triangulation_cost.m new file mode 100644 index 0000000000000000000000000000000000000000..28acde07cc3fd8b242b6f2db707b7bedab3a0973 --- /dev/null +++ b/stereo/triangulation_cost.m @@ -0,0 +1,42 @@ +function [f_cost, camera] = triangulation_cost(X, camera, ij_pos, b_dewarp, Gamma) + % extract rotation, camera 1 + e1 = X(1:3) / (eps + norm(X(1:3))); + theta1 = norm(X(1:3)); + % extract rotation, camera 2 + e2 = e1; + theta2 = -theta1; + % extract shift, camera 1 + dx1 = [0 0 0]'; + dx2 = [0 0 0]'; + + % create rotation matrices + Omega1 = vrrotvec2mat([e1, theta1]); + Omega2 = vrrotvec2mat([e2, theta2]); + + % apply rotation to cameras + R1p = Omega1 * camera(1).calib.R; + R2p = Omega2 * camera(2).calib.R; + camera(1).calib.R = R1p; + camera(2).calib.R = R2p; + + % apply translation + x1p = camera(1).calib.xc + dx1; + x2p = camera(2).calib.xc + dx2; + camera(1).calib.xc= x1p; + camera(2).calib.xc= x2p; + + % update camera projection matrix + camera(1).calib.P = camera(1).calib.G * camera(1).calib.R * [eye(3), -camera(1).calib.xc]; + camera(2).calib.P = camera(2).calib.G * camera(2).calib.R * [eye(3), -camera(2).calib.xc]; + + % triangulate + [gl_pos,gl_dist]= triangulate(camera, ij_pos, b_dewarp); + f_cost = gl_dist(:,1); + + % fit plane + M = fit_plane(gl_pos, [0 1 0]'); + d = M * [gl_pos; ones(1,size(gl_pos,2))]; + + % add regularisation factor + f_cost = [f_cost; d'; Gamma*theta1]; +end \ No newline at end of file