def test_empty_config_is_preserved_exactly_across_dictionary_conversion(): config = calib.Config() dictionary = config.to_dict() loaded = calib.Config.from_dict(eval(repr(dictionary))) np.set_printoptions(threshold=sys.maxsize) assert loaded == config
def test_config_populated_from_paths_matches_config_populated_from_same_loaded_images(): path = "sample_images/002h.bmp" path_config = calib.Config() assert path_config.populate_lens_parameters_from_chessboard(path, 6, 8) assert path_config.populate_keystone_and_real_parameters_from_chessboard( path, 8, 6, 90.06, 64.45 ) image = cv.imread(path) image_config = calib.Config() assert image_config.populate_lens_parameters_from_chessboard(image, 6, 8) assert image_config.populate_keystone_and_real_parameters_from_chessboard( image, 8, 6, 90.06, 64.45 ) assert path_config == image_config
def test_partial_config_is_preserved_exactly_across_dictionary_conversion(): config = calib.Config() config.populate_lens_parameters_from_chessboard("sample_images/002h.bmp", 6, 8) dictionary = config.to_dict() loaded = calib.Config.from_dict(eval(repr(dictionary))) np.set_printoptions(threshold=sys.maxsize) assert loaded == config
def test_points_transform_from_combined_config_to_100_microns(): params = cv.SimpleBlobDetector_Params() params.minArea = 50 params.maxArea = 1000 params.filterByArea = True params.minCircularity = 0.2 params.filterByCircularity = True params.blobColor = 0 params.filterByColor = True detector = cv.SimpleBlobDetector_create(params) config = calib.Config() assert config.populate_lens_parameters_from_chessboard( "sample_images/002h.bmp", 6, 8 ), "Unable to populate distortion parameters" assert config.populate_keystone_and_real_parameters_from_symmetric_dot_pattern( "sample_images/distcor_01_cleaned.bmp", detector, 116, 170, 84.5, 57.5 ), "Unable to populate homography parameters" # Points determined by dot centers from running an openCV blob detector over sample_images/distcor_01_cleaned.bmp points = np.array( [ [[3584.902, 2468.0232]], # bottom right [[71.22837, 2466.539]], # bottom left [[68.2684, 62.64333]], # top left [[3600.0374, 78.32093]], # top right [[1804.8428, 38.65753]], # middle top [[1799.092, 2498.543]], # middle bottom [[47.950756, 1299.2955]], # middle left [[3611.6602, 1307.9681]], # middle right [[1800.4049, 1304.3975]], # center ], np.float32, ) expectations = np.array( [ [84.5, 57.5], [0, 57.5], [0, 0], [84.5, 0], [83 / 2.0, 0], [83 / 2.0, 57.5], [0, 59 / 2.0], [84.5, 59 / 2.0], [83 / 2.0, 59 / 2.0], ], np.float32, ) assess_points_transform_to_given_absolute_accuracy( config, points, expectations, 0.1 )
def test_homography_border_doesnt_affect_point_transforms(): default_border_config = calib.Config() assert default_border_config.populate_lens_parameters_from_chessboard( "sample_images/002h.bmp", 6, 8 ), "Unable to populate distortion parameters" assert default_border_config.populate_keystone_and_real_parameters_from_chessboard( "sample_images/002h.bmp", 8, 6, 90.06, 64.45 ), "Unable to populate homography parameters" custom_border_config = calib.Config() assert custom_border_config.populate_lens_parameters_from_chessboard( "sample_images/002h.bmp", 6, 8 ), "Unable to populate distortion parameters" assert custom_border_config.populate_keystone_and_real_parameters_from_chessboard( "sample_images/002h.bmp", 8, 6, 90.06, 64.45, border=200 ), "Unable to populate homography parameters" found, corners = cv.findChessboardCorners( cv.imread("sample_images/mocked checkboard.png"), (15, 10) ) targets = np.zeros((len(corners), 2), np.float32) for i in range(15): for j in range(10): targets[j * 15 + i, 0] = 70 * (14 - i) / 14.0 targets[j * 15 + i, 1] = 45 * (9 - j) / 9.0 default_border_transformed = calib.correct_points( corners, default_border_config, calib.Correction.lens_keystone_and_real_coordinates, ) custom_border_transformed = calib.correct_points( corners, custom_border_config, calib.Correction.lens_keystone_and_real_coordinates, ) assert np.allclose( default_border_transformed, custom_border_transformed ), "Real world coordinates not consistent with image borders"
def test_partial_config_is_preserved_exactly_across_save_and_load(): file = TemporaryFile() config = calib.Config() config.populate_lens_parameters_from_chessboard("sample_images/002h.bmp", 6, 8) config.save(file) file.seek(0) loaded = calib.Config.load(file) np.set_printoptions(threshold=sys.maxsize) assert loaded == config
def test_points_transform_from_only_mock_chessboard_to_100_microns(): config = calib.Config() assert config.populate_lens_parameters_from_chessboard( "sample_images/mocked checkboard.png", 10, 15 ), "Unable to populate distortion parameters" assert config.populate_keystone_and_real_parameters_from_chessboard( "sample_images/mocked checkboard.png", 15, 10, 70, 45 ), "Unable to populate homography parameters" found, corners = cv.findChessboardCorners( cv.imread("sample_images/mocked checkboard.png"), (15, 10) ) targets = np.zeros((len(corners), 2), np.float32) for i in range(15): for j in range(10): targets[j * 15 + i, 0] = 70 * (14 - i) / 14.0 targets[j * 15 + i, 1] = 45 * (9 - j) / 9.0 assess_points_transform_to_given_absolute_accuracy(config, corners, targets, 0.1)
def test_points_transform_from_only_chessboard_to_100_microns(): config = calib.Config() assert config.populate_lens_parameters_from_chessboard( "sample_images/002h.bmp", 6, 8 ), "Unable to populate distortion parameters" assert config.populate_keystone_and_real_parameters_from_chessboard( "sample_images/002h.bmp", 8, 6, 90.06, 64.45 ), "Unable to populate homography parameters" found, corners = cv.findChessboardCorners( cv.imread("sample_images/002h.bmp"), (8, 6) ) targets = np.zeros((len(corners), 2), np.float32) for i in range(8): for j in range(6): targets[j * 8 + i, 0] = 90.06 * (7 - i) / 7.0 targets[j * 8 + i, 1] = 64.45 * (5 - j) / 5.0 assess_points_transform_to_given_absolute_accuracy(config, corners, targets, 0.1)
def test_separate_lens_and_keystone_image_correction_calls_are_equivalent_to_a_single_combined_call( ): image = cv.imread("sample_images/002h.bmp") config = calib.Config() config.populate_lens_parameters_from_chessboard(image, 6, 8) config.populate_keystone_and_real_parameters_from_chessboard( image, 8, 6, 90.06, 64.45) lens_corrected = calib.correct_image(image, config, calib.Correction.lens_distortion) keystone_corrected = calib.correct_image( lens_corrected, config, calib.Correction.keystone_distortion) combo_corrected = calib.correct_image(image, config, calib.Correction.lens_and_keystone) assert np.array_equal(keystone_corrected, combo_corrected)
def test_separate_lens_keystone_and_real_point_correction_calls_are_equivalent_to_a_single_combined_call( ): image = cv.imread("sample_images/002h.bmp") config = calib.Config() config.populate_lens_parameters_from_chessboard(image, 6, 8) config.populate_keystone_and_real_parameters_from_chessboard( image, 8, 6, 90.06, 64.45) _, points = cv.findChessboardCorners(image, (6, 8)) camera_corrected = calib.correct_points(points, config, calib.Correction.lens_and_keystone) real_converted = calib.correct_points(camera_corrected, config, calib.Correction.real_coordinates) combo_corrected = calib.correct_points( points, config, calib.Correction.lens_keystone_and_real_coordinates) assert np.array_equal(real_converted, combo_corrected)
def assess_config(image_path, chess_image, distorted_grid, corner_only_homography): config = calib.Config() config.populate_lens_parameters_from_chessboard(chess_image, rows, cols) config.populate_keystone_and_real_parameters_from_chessboard( chess_image, cols, rows, grid_width, grid_height, corners_only=corner_only_homography, border=200, ) cv.imshow( "{} {} lens".format(image_path, corner_only_homography), calib.correct_image(chess_image, config, calib.Correction.lens_distortion), ) cv.imshow( "{} {} lens and keystone".format(image_path, corner_only_homography), calib.correct_image(chess_image, config, calib.Correction.lens_and_keystone), ) # grid[0] -> bottom left # grid[10] -> top left # grid[-11] -> bottom right # grid[-1] -> top right expectations = np.zeros((len(distorted_grid), 2), np.float32) for i in range(rows): for j in range(cols): expectations[i + j * rows, 0] = square_edge_mm * j expectations[i + j * rows, 1] = grid_height - (square_edge_mm * i) corrected_points = calib.correct_points( distorted_grid, config, calib.Correction.lens_keystone_and_real_coordinates) print(config) print(config.to_dict()) distances = [] distance_hist = {} highest = 0 for i in range(len(corrected_points)): original = distorted_grid[i, 0] point = corrected_points[i, 0] expectation = expectations[i] assert len(point) == 2 dist = distance(point, expectation) if dist > highest: highest = dist print("new highest: px{} res{} exp{} dist{}".format( original, point, expectation, dist)) distances.append(dist) mm = math.ceil(dist) if mm in distance_hist: distance_hist[mm] += 1 else: distance_hist[mm] = 1 print("image: {} corners_only_homography: {}".format( image_path, corner_only_homography)) print("average deviation:\n{}mm".format(sum(distances) / len(distances))) print("max deviation:\n{}mm".format(max(distances))) print("deviation spread:") pprint(distance_hist)
""" Generate a calibration configuration using a chessboard image. Then use that configuration to generate distortion corrected image files of that chessboard. """ import camera_calibration as calib import cv2 image_path = "../sample_images/002h.bmp" rows = 6 cols = 8 config = calib.Config() config.populate_lens_parameters_from_chessboard(image_path, rows, cols) config.populate_keystone_and_real_parameters_from_chessboard( image_path, cols, rows, 90.06, 64.45) bgr = cv2.imread(image_path) undistorted = calib.correct_image(bgr, config, calib.Correction.lens_distortion) cv2.imwrite("undistorted.png", undistorted) grid_aligned = calib.correct_image(undistorted, config, calib.Correction.keystone_distortion) cv2.imwrite("grid_aligned.png", grid_aligned)
def test_points_transform_from_only_dot_grid_to_20_microns(): config = calib.Config() assert config.populate_lens_parameters_from_chessboard( "sample_images/002h.bmp", 6, 8 ), "Unable to populate distortion parameters" params = cv.SimpleBlobDetector_Params() params.minArea = 50 params.maxArea = 1000 params.filterByArea = True params.minCircularity = 0.2 params.filterByCircularity = True params.blobColor = 0 params.filterByColor = True dot_detector = cv.SimpleBlobDetector_create(params) rows = 116 cols = 170 dot_image = cv.imread("sample_images/distcor_01_cleaned.bmp") undistorted_dot_image = calib.correct_image( dot_image, config, calib.Correction.lens_distortion ) print("searching for grid in undistorted image") found, undistorted_grid = cv.findCirclesGrid( undistorted_dot_image, (cols, rows), cv.CALIB_CB_SYMMETRIC_GRID + cv.CALIB_CB_CLUSTERING, dot_detector, cv.CirclesGridFinderParameters(), ) assert found, "Unable to find dot grid in initially undistorted image" print("grid found in undistorted image") print("looking for dots in distorted image") distorted_points = cv.KeyPoint_convert(dot_detector.detect(dot_image)) print("dots found in distorted image") distorted_points = np.array([[point] for point in distorted_points], np.float32) transformed_points = cv.undistortPoints( distorted_points, config.distorted_camera_matrix, config.distortion_coefficients, P=config.undistorted_camera_matrix, ) def distance(p1, p2): return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) print("searching for dot mapping") distorted_grid = np.zeros(undistorted_grid.shape, undistorted_grid.dtype) for i in range(len(undistorted_grid)): if i % 100 == 0: print("progress: {}/{}".format(i, cols * rows), end="\r") # get the point at i in the grid grid_member = undistorted_grid[i] # find the nearest member of transformed_points nearest_distance = sys.float_info.max original_point = None for j in range(len(transformed_points)): transformed_point = transformed_points[j] separation = distance(grid_member[0], transformed_point[0]) if separation < nearest_distance: nearest_distance = separation original_point = distorted_points[j] if separation < 1: break # get the untransformed point that matches the transformed_point assert original_point is not None # set it to position i in the new grid distorted_grid[i, 0, 0] = original_point[0, 0] distorted_grid[i, 0, 1] = original_point[0, 1] # np.save('full_grid', distorted_grid) print("generating sparse grid") # Use fewer points to improve distortion performance from not finishing in >20mins while using all available RAM sparse_rows = rows // 2 sparse_cols = cols // 2 sparse_grid = np.zeros((sparse_rows * sparse_cols, 1, 2), np.float32) for i in range(sparse_rows): for j in range(sparse_cols): sparse_grid[i * sparse_cols + j, 0, 0] = distorted_grid[ (i * 2) * cols + (j * 2), 0, 0 ] sparse_grid[i * sparse_cols + j, 0, 1] = distorted_grid[ (i * 2) * cols + (j * 2), 0, 1 ] print("generating config") h, w = dot_image.shape[:2] dot_config = calib.Config() dot_config.populate_lens_parameters_from_grid( sparse_grid, sparse_cols, sparse_rows, w, h ) undistorted_distorted_grid = cv.undistortPoints( distorted_grid, dot_config.distorted_camera_matrix, dot_config.distortion_coefficients, P=dot_config.undistorted_camera_matrix, ) dot_config.populate_keystone_and_real_parameters_from_grid( undistorted_distorted_grid, cols, rows, 84.5, 57.5 ) print("initial config:") print(config) print("dot config:") print(dot_config) expectations = np.zeros((len(undistorted_grid), 2), np.float32) for i in range(rows): for j in range(cols): expectations[i * cols + j, 0] = 84.5 - (0.5 * j) expectations[i * cols + j, 1] = 57.5 - (0.5 * i) assess_points_transform_to_given_absolute_accuracy( dot_config, distorted_grid, expectations, 0.02 )
chess_grid_cols = 8 dot_grid_rows = 116 dot_grid_cols = 170 dot_spacing = 0.5 corner_only_homography = False dot_grid_width = (dot_grid_cols - 1) * dot_spacing dot_grid_height = (dot_grid_rows - 1) * dot_spacing def distance(p1, p2): return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) # Determine a rough lens distortion correction using the chessboard image chess_config = calib.Config() configured = chess_config.populate_lens_parameters_from_chessboard( chessboard_image_path, chess_grid_rows, chess_grid_cols ) if not configured: print( "Could not determine lens correction properties from {}".format( chessboard_image_path ) ) exit() params = cv.SimpleBlobDetector_Params() params.minArea = 50 params.maxArea = 1000
def assess_config(image_path, dot_image, distorted_grid, corner_only_homography): sparse_rows = rows // 2 sparse_cols = cols // 2 sparse_grid = np.zeros((sparse_rows * sparse_cols, 1, 2), np.float32) for i in range(sparse_rows): for j in range(sparse_cols): sparse_grid[i * sparse_cols + j, 0, 0] = distorted_grid[(i * 2) * cols + (j * 2), 0, 0] sparse_grid[i * sparse_cols + j, 0, 1] = distorted_grid[(i * 2) * cols + (j * 2), 0, 1] h, w = dot_image.shape[:2] dot_config = calib.Config() dot_config.populate_lens_parameters_from_grid(sparse_grid, sparse_cols, sparse_rows, w, h) undistorted_distorted_grid = cv.undistortPoints( distorted_grid, dot_config.distorted_camera_matrix, dot_config.distortion_coefficients, P=dot_config.undistorted_camera_matrix, ) dot_config.populate_keystone_and_real_parameters_from_grid( undistorted_distorted_grid, cols, rows, 84.5, 57.5, corners_only=corner_only_homography, ) expectations = np.zeros((len(distorted_grid), 2), np.float32) for i in range(rows): for j in range(cols): expectations[i * cols + j, 0] = 84.5 - (0.5 * j) expectations[i * cols + j, 1] = 57.5 - (0.5 * i) corrected_points = calib.correct_points( distorted_grid, dot_config, calib.Correction.lens_keystone_and_real_coordinates) distances = [] distance_hist = {} highest = 0 for i in range(len(corrected_points)): original = distorted_grid[i, 0] point = corrected_points[i, 0] expectation = expectations[i] assert len(point) == 2 dist = distance(point, expectation) if dist > highest: highest = dist print("new highest: px{} res{} exp{} dist{}".format( original, point, expectation, dist)) distances.append(dist) microns = math.ceil(dist * 1000) if microns in distance_hist: distance_hist[microns] += 1 else: distance_hist[microns] = 1 print("image: {} corners_only_homography: {}".format( image_path, corner_only_homography)) print("average deviation:\n{}mm".format(sum(distances) / len(distances))) print("max deviation:\n{}mm".format(max(distances))) print("deviation spread:") pprint(distance_hist)
def search_for_best_config(): chess_config = calib.Config() chess_config.populate_lens_parameters_from_chessboard( "sample_images/002h.bmp", 6, 8) params = cv.SimpleBlobDetector_Params() params.minArea = 50 params.maxArea = 1000 params.filterByArea = True params.minCircularity = 0.2 params.filterByCircularity = True params.blobColor = 0 params.filterByColor = True dot_detector = cv.SimpleBlobDetector_create(params) for image_path in ( "sample_images/distcor_01_cleaned.bmp", "sample_images/distcor_02_cleaned.bmp", "sample_images/distcor_03_cleaned.bmp", "sample_images/distcor_04_cleaned.bmp", "sample_images/distcor_05_cleaned.bmp", "sample_images/distcor_06_cleaned.bmp", "sample_images/distcor_07_cleaned.bmp", "sample_images/distcor_08_cleaned.bmp", "sample_images/distcor_09_cleaned.bmp", "sample_images/distcor_10_cleaned.bmp", "sample_images/distcor_11_cleaned.bmp", ): dot_image = cv.imread(image_path) undistorted_dot_image = calib.correct_image( dot_image, chess_config, calib.Correction.lens_distortion) found, undistorted_grid = cv.findCirclesGrid( undistorted_dot_image, (cols, rows), cv.CALIB_CB_SYMMETRIC_GRID + cv.CALIB_CB_CLUSTERING, dot_detector, cv.CirclesGridFinderParameters(), ) if not found: print("Could not find dot grid in {}".format(image_path)) continue distorted_points = np.array( [[point] for point in cv.KeyPoint_convert(dot_detector.detect(dot_image))], np.float32, ) transformed_points = cv.undistortPoints( distorted_points, chess_config.distorted_camera_matrix, chess_config.distortion_coefficients, P=chess_config.undistorted_camera_matrix, ) distorted_grid = np.zeros(undistorted_grid.shape, undistorted_grid.dtype) for i in range(len(undistorted_grid)): if i % 100 == 0: print("progress: {}/{}".format(i, cols * rows), end="\r") # get the point at i in the grid grid_member = undistorted_grid[i] # find the nearest member of transformed_points nearest_distance = sys.float_info.max original_point = None for j in range(len(transformed_points)): transformed_point = transformed_points[j] separation = distance(grid_member[0], transformed_point[0]) if separation < nearest_distance: nearest_distance = separation original_point = distorted_points[j] if separation < 1: break # get the untransformed point that matches the transformed_point assert original_point is not None # set it to position i in the new grid distorted_grid[i, 0, 0] = original_point[0, 0] distorted_grid[i, 0, 1] = original_point[0, 1] for corner_only_homography in (True, False): assess_config(image_path, dot_image, distorted_grid, corner_only_homography)