diff --git a/navipy/arenatools/cam_dlt.py b/navipy/arenatools/cam_dlt.py new file mode 100644 index 0000000000000000000000000000000000000000..5a9ad8eee325cae4ba82db888d201d38fe0dd68e --- /dev/null +++ b/navipy/arenatools/cam_dlt.py @@ -0,0 +1,176 @@ +import pandas as pd +import numpy as np + + +def dlt_reconstruct(coeff, campts, z=None): + """ + This function reconstructs the 3D position of a coordinate based on a set + of DLT coefficients and [u,v] pixel coordinates from 2 or more cameras + + :param coeff: - 11 DLT coefficients for all n cameras, [11,n] array + :param campts: - [u,v] pixel coordinates from all n cameras over f frames + :param z: the z coordinate of all points for reconstruction \ +from a single camera + :returns: xyz - the xyz location in each frame, an [f,3] array\ +rmse - the root mean square error for each xyz point, and [f,1] array,\ +units are [u,v] i.e. camera coordinates or pixels + """ + # number of cameras + ncams = campts.columns.levels[0].shape[0] + if (ncams == 1) and (z is None): + raise NameError('reconstruction from a single camera require z') + + # setup output variables + xyz = pd.DataFrame(index=campts.index, columns=['x', 'y', 'z']) + rmse = pd.Series(index=campts.index) + # process each frame + for ii, frame_i in enumerate(campts.index): + # get a list of cameras with non-NaN [u,v] + row = campts.loc[frame_i, :].unstack() + validcam = row.dropna(how='any') + cdx = validcam.index + if validcam.shape[0] >= 2: + # Two or more cameras + u = campts.loc[frame_i, cdx].u + v = campts.loc[frame_i, cdx].v + + # initialize least-square solution matrices + m1 = np.zeros((cdx.shape[0]*2, 3)) + m2 = np.zeros((cdx.shape[0]*2, 1)) + + m1[0: cdx.size*2: 2, 0] = u*coeff.loc[8, cdx]-coeff.loc[0, cdx] + m1[0: cdx.size*2: 2, 1] = u*coeff.loc[9, cdx]-coeff.loc[1, cdx] + m1[0: cdx.size*2: 2, 2] = u*coeff.loc[10, cdx]-coeff.loc[2, cdx] + m1[1: cdx.size*2: 2, 0] = v*coeff.loc[8, cdx]-coeff.loc[4, cdx] + m1[1: cdx.size*2: 2, 1] = v*coeff.loc[9, cdx]-coeff.loc[5, cdx] + m1[1: cdx.size*2: 2, 2] = v*coeff.loc[10, cdx]-coeff.loc[7, cdx] + m2[0: cdx.size*2: 2, 0] = coeff.loc[3, cdx]-u + m2[1: cdx.size*2: 2, 0] = coeff.loc[7, cdx]-v + + # get the least squares solution to the reconstruction + xyz.loc[frame_i, ['x', 'y', 'z']] = \ + np.linalg.lstsq(m1, m2, rcond=None)[0] + + # compute ideal [u,v] for each camera + uv = m1.dot(xyz.loc[frame_i, ['x', 'y', 'z']].transpose()) + + # compute the number of degrees of freedom in the reconstruction + dof = m2.size-3 + + # estimate the root mean square reconstruction error + rmse.loc[frame_i] = (np.sum((m2-uv) ** 2)/dof) ^ 0.5 + + elif (validcam.shape[0] == 1) and (z is not None): + # http://www.kwon3d.com/theory/dlt/dlt.html + # equation 19 with z = constant + # the term with z can be move to right side + # then equation 21 can be solved as follow: + u = campts.loc[frame_i, cdx].unstack().u + v = campts.loc[frame_i, cdx].unstack().v + + # initialize least-square solution matrices + m1 = np.zeros((cdx.shape[0]*2, 2)) + m2 = np.zeros((cdx.shape[0]*2, 1)) + + m1[0: cdx.size*2: 2, 0] = u*coeff.loc[8, cdx]-coeff.loc[0, cdx] + m1[0: cdx.size*2: 2, 1] = u*coeff.loc[9, cdx]-coeff.loc[1, cdx] + m1[1: cdx.size*2: 2, 0] = v*coeff.loc[8, cdx]-coeff.loc[4, cdx] + m1[1: cdx.size*2: 2, 1] = v*coeff.loc[9, cdx]-coeff.loc[5, cdx] + m2[0: cdx.size*2: 2, 0] = coeff.loc[3, cdx]-u + m2[1: cdx.size*2: 2, 0] = coeff.loc[7, cdx]-v + + m2[0: cdx.size*2: 2, 0] -= \ + (u*coeff.loc[10, cdx] - coeff.loc[2, cdx])*z.loc[frame_i] + m2[1: cdx.size*2: 2, 0] -= \ + (v*coeff.loc[10, cdx] - coeff.loc[6, cdx])*z.loc[frame_i] + + # get the least squares solution to the reconstruction + xyz.loc[frame_i, ['x', 'y']] = \ + np.squeeze(np.linalg.lstsq(m1, m2, rcond=None)[0]) + xyz.loc[frame_i, 'z'] = z.loc[frame_i] + + # compute ideal [u,v] for each camera + uv = m1.dot(xyz.loc[frame_i, ['x', 'y']].transpose()) + + # compute the number of degrees of freedom in the reconstruction + dof = m2.size-3 + + # estimate the root mean square reconstruction error + rmse.loc[frame_i] = (np.sum((m2-uv) ** 2)/dof) ** 0.5 + return xyz, rmse + + +def dlt_inverse(coeff, frames): + """ + This function reconstructs the pixel coordinates of a 3D coordinate as + seen by the camera specificed by DLT coefficients c + + :param coeff: - 11 DLT coefficients for the camera, [11,1] array + :param frames: - [x,y,z] coordinates over f frames,[f,3] array + :returns: uv - pixel coordinates in each frame, [f,2] array + """ + # write the matrix solution out longhand for vector operation over + # all pointsat once + uv = np.zeros((frames.shape[0], 2)) + frames = frames.loc[:, ['x', 'y', 'z']].values + + normalisation = frames[:, 0]*coeff[8] + \ + frames[:, 1]*coeff[9]+frames[:, 2]*coeff[10] + 1 + uv[:, 0] = frames[:, 0]*coeff[0]+frames[:, 1] * \ + coeff[1]+frames[:, 2]*coeff[2]+coeff[3] + uv[:, 1] = frames[:, 0]*coeff[4]+frames[:, 1] * \ + coeff[5]+frames[:, 2]*coeff[6]+coeff[7] + uv[:, 0] /= normalisation + uv[:, 1] /= normalisation + return uv + + +def dlt_compute_coeffs(frames, campts): + """ + A basic implementation of 11 parameters DLT + + : param frames: an array of x, y, z calibration point coordinates + : param campts: an array of u, v pixel coordinates from the camera + : returns: dlt coefficients and root mean square error + + Notes: frame and camera points must have the same number of rows and at \ +least contains six rows. Also the frame points must not all lie within a \ +single plane. + """ + + # remove NaNs + valid_idx = frames.dropna(how='any').index + valid_idx = campts.loc[valid_idx, :].dropna(how='any').index + + # valid df + vframes = frames.loc[valid_idx, :] + vcampts = campts.loc[valid_idx, :] + + # re arange the frame matrix to facilitate the linear least + # sqaures solution + matrix = np.zeros((vframes.shape[0]*2, 11)) # 11 for the dlt + for num_i, index_i in enumerate(vframes.index): + matrix[2*num_i, 0:3] = vframes.loc[index_i, ['x', 'y', 'z']] + matrix[2*num_i+1, 4:7] = vframes.loc[index_i, ['x', 'y', 'z']] + matrix[2*num_i, 3] = 1 + matrix[2*num_i+1, 7] = 1 + matrix[2*num_i, 8:11] = \ + vframes.loc[index_i, ['x', 'y', 'z']]*(-vcampts.loc[index_i, 'u']) + matrix[2*num_i+1, 8:11] = \ + vframes.loc[index_i, ['x', 'y', 'z']]*(-vcampts.loc[index_i, 'v']) + + # re argen the campts array for the linear solution + vcampts_f = np.reshape(np.flipud(np.rot90(vcampts)), vcampts.size, 1) + print(vcampts_f.shape) + print(matrix.shape) + # get the linear solution the 11 parameters + coeff = np.linalg.lstsq(matrix, vcampts_f, rcond=None)[0] + # compute the position of the frame in u,v coordinates given the linear + # solution from the previous line + + matrix_uv = dlt_inverse(coeff, vframes) + + # compute the rmse between the ideal frame u,v and the + # recorded frame u,v + rmse = np.sqrt(np.mean(np.sum((matrix_uv-vcampts)**2))) + return coeff, rmse diff --git a/navipy/scripts/dlt_calibrator.py b/navipy/scripts/dlt_calibrator.py new file mode 100644 index 0000000000000000000000000000000000000000..ed9b12a6d91b30acb4496bd8b1b5258028e6cf06 --- /dev/null +++ b/navipy/scripts/dlt_calibrator.py @@ -0,0 +1,160 @@ +import argparse +import pandas as pd +import numpy as np +import cv2 +import os +from navipy.arenatools.cam_dlt import dlt_inverse +from navipy.arenatools.cam_dlt import dlt_compute_coeffs + + +keybinding = dict() +keybinding['Quit without saving'] = 'q' +keybinding['Save and quite'] = 'e' +keybinding['Forward'] = 'f' +keybinding['Backward'] = 'b' +keybinding['Skip'] = 's' +keybinding['Calculate coeff'] = 'c' + + +def parser_dlt_calibrator(): + # Create command line options + + description = 'DLT calibrator provide a elementary user ' + description += 'interface to determine the dlt coeffs of ' + description += 'a camera from an image and calibration' + description += '\n\n' + description += 'Key bindings:\n' + description += '-------------\n' + for key, val in keybinding.items(): + description += '{} : {}\n'.format(val, key) + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter) + arghelp = 'Path to the calibration image' + parser.add_argument('-i', '--image', + required=True, + help=arghelp) + arghelp = 'Path to the csv files containing calibration points' + parser.add_argument('-p', '--points', + required=True, + help=arghelp) + arghelp = 'Scaling of the image' + parser.add_argument('-s', '--scale', + default=0.5, + help=arghelp) + return parser + + +def click(event, x, y, flags, param): + # grab references to the global variables + global campts, index_i, scale + # if the left mouse button was clicked, record the starting + # (x, y) coordinates and indicate that cropping is being + # performed + if event == cv2.EVENT_LBUTTONDOWN: + campts.loc[index_i, ['u', 'v']] = [x/scale, y/scale] + + +def main(): + # Fetch arguments + args = vars(parser_dlt_calibrator().parse_args()) + # Load the u,v points if any, otherwise + # set all u,v for each frame to nan, because + # we do not know the position of x,y,z points on cam + frames = pd.read_csv(args['points']) + if ('u' in frames.columns) and ('v' in frames.columns): + campts = frames.loc[:, ['u', 'v']] + frames = frames.drop('u', axis=1) + frames = frames.drop('v', axis=1) + else: + campts = pd.DataFrame(index=frames.index, columns=['u', 'v']) + # The image may need to be scale because screen may have + # less pixel than the image + scale = args['scale'] + imageref = cv2.imread(args['image']) + # define some constants + showframe_ref = 50 + showframe = showframe_ref + font = cv2.FONT_HERSHEY_SIMPLEX + coeff = None + + # create an image window + cv2.namedWindow("image") + cv2.setMouseCallback("image", click) + # Loop until users quit + idx = 0 # User will control it during the loop + while True: + # Make sure the idx do not go outof bound + idx = np.clip(idx, 0, frames.shape[0]-1) + # load image + index_i = frames.index[idx] + mimage = imageref.copy() + # Display stuff at given time + if showframe > 0: + cv2.putText(mimage, str(index_i), (50, 50), + font, 2, (0, 0, 255), 3, cv2.LINE_AA) + showframe -= 1 + for nbi, row in campts.dropna().iterrows(): + cv2.circle(mimage, (row.u.astype(int), row.v.astype(int)), + 5, (0, 0, 255), 1) + cv2.putText(mimage, str(nbi), + (row.u.astype(int), row.v.astype(int)), + font, 1, (0, 0, 255), 2, cv2.LINE_AA) + if coeff is not None: + matrix_uv = dlt_inverse(coeff, frames) + # print(matrix_uv) + matrix_uv = pd.DataFrame(data=matrix_uv, + index=frames.index, + columns=['u', 'v']) + matrix_uv[matrix_uv < 0] = np.nan + matrix_uv[matrix_uv.u > mimage.shape[0]] = np.nan + matrix_uv[matrix_uv.v > mimage.shape[1]] = np.nan + for nbi, row in matrix_uv.dropna().iterrows(): + cv2.circle(mimage, (row.u.astype(int), row.v.astype(int)), + 5, (0, 255, 0), 1) + cv2.putText(mimage, str(nbi), + (row.u.astype(int), row.v.astype(int)), + font, 1, (0, 255, 0), 2, cv2.LINE_AA) + mimage = cv2.resize(mimage, (0, 0), fx=scale, fy=scale) + cv2.imshow("image", mimage) + # Wait for keys + key = cv2.waitKey(20) & 0xFF # 20ms + # Key binding + if key == ord("q"): + print('quit without saving') + break + if key == ord("f"): + # forward + showframe = showframe_ref + idx += 1 + if key == ord("s"): + # skip + showframe = showframe_ref + campts.loc[index_i, :] = np.nan + idx += 1 + if key == ord("b"): + # backward + showframe = showframe_ref + idx -= 1 + if key == ord("e"): + print('save and quit') + frames['u'] = campts.u + frames['v'] = campts.v + frames.to_csv(args['points']) + break + if key == ord("c"): + print('calibrate') + coeff, rmse = dlt_compute_coeffs(frames, campts) + print(rmse) + print(coeff) + coeff = pd.Series(data=coeff) + coeff.to_csv(os.path.splitext(args['points'])[0]+'_coeff.csv') + + # close all open windows + cv2.destroyAllWindows() + print('End') + + +if __name__ == "__main__": + # execute only if run as a script + main() diff --git a/setup.py b/setup.py index c80e605ea9d5ae123987eb2bdd3b487424c0d1db..8dc76a06fd6ee5f45c119ba94a989756ee1fd878 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,8 @@ setup_dict = {'name': 'navipy', 'blendunittest=navipy.scripts.blendunittest:main', 'blendongrid=navipy.scripts.blend_ongrid:main', 'blendoverlaytraj=navipy.scripts.blend_overlaytraj:main', - 'blendalongtraj=navipy.scripts.blend_alongtraj:main' + 'blendalongtraj=navipy.scripts.blend_alongtraj:main', + 'dltcalibrator=navipy.scripts.dlt_calibrator:main' ]}, }