Source code for sksurgeryimage.calibration.charuco

# coding=utf-8

"""
Functions to support camera calibration using ChArUco chessboard markers.
"""
import cv2
from cv2 import aruco  # pylint: disable=no-name-in-module
import numpy as np


[docs]def make_charuco_board(dictionary, number_of_squares, size, image_size): """ Generates a ChArUco pattern. Don't forget to select an image size that is a nice multiple of the square size in millimetres, to avoid any interpolation artefacts. You should check the resultant image has only 2 values, [0|255], and nothing interpolated between these two numbers. :param dictionary: aruco dictionary definition :param number_of_squares: tuple of (number in x, number in y) :param size: tuple of (size of chessboard square, size of internal tag), mm. :param image_size: tuple of (image width, image height), pixels. :return: image, board """ number_in_x, number_in_y = number_of_squares size_of_square, size_of_tag = size board = aruco.CharucoBoard_create(number_in_x, number_in_y, size_of_square, size_of_tag, dictionary) image = board.draw(image_size, 1, 0) return image, board
[docs]def filter_out_wrong_markers(marker_corners, marker_ids, board): """ Filters out markers that were mis-labelled. For each inner corner on the ChArUco board, if both neighbouring markers are detected, look at the projected positions of this corner using the perspective transformations obtained form the two markers. If the two positions are not close (further than 20 pixels away), then at least one of the markers is mis-labelled but we won't know which one. Remove both markers. :param marker_corners: marker corners detected by OpenCV :param marker_ids: ids of markers detected :param board: charuco board definition :return: marker_corners, marker_ids """ number_of_markers = len(marker_ids) # Calculate local homographies for each marker transformations = [] for i in range(0, number_of_markers): marker_id = marker_ids[i][0] marker_obj_corners = board.objPoints[marker_id] marker_obj_corners_2d = marker_obj_corners[:, 0:2].astype(np.float32) marker_img_corners = marker_corners[i][0].astype(np.float32) trans = cv2.getPerspectiveTransform(marker_obj_corners_2d, marker_img_corners) transformations.append(trans) # For each charuco corner, calculate its projected positions based on # the closest markers' homographies mask = np.ones((number_of_markers, 1), dtype=bool) mask = mask.flatten() number_of_corners = board.chessboardCorners.shape[0] for i in range(0, number_of_corners): obj_point_2d = board.chessboardCorners[i, 0:2].astype(np.float32) obj_point_2d = np.reshape(obj_point_2d, (-1, 1, 2)) projected_positions = [] neighbour_markers = [] number_of_nearest_markers = len(board.nearestMarkerIdx[i]) assert number_of_nearest_markers == 2 for j in range(0, number_of_nearest_markers): try: marker_id = board.ids[board.nearestMarkerIdx[i][j][0]][0] except IndexError: marker_id = board.ids[board.nearestMarkerIdx[i][j]] marker_index = -1 for k in range(0, number_of_markers): if marker_ids[k][0] == marker_id: marker_index = k break if marker_index is not -1: # The input point array needs to be 3 dimensional! out = cv2.perspectiveTransform(obj_point_2d, transformations[marker_index]) projected_positions.append(out) neighbour_markers.append(marker_index) if len(projected_positions) > 1: dis = np.linalg.norm(projected_positions[0] - projected_positions[1]) if dis > 20: mask[neighbour_markers] = False marker_ids = marker_ids[mask] marker_corners = np.array(marker_corners) marker_corners = marker_corners[mask] return marker_corners, marker_ids
[docs]def detect_charuco_points(dictionary, board, image, camera_matrix=None, distortion_coefficients=None, filtering=False): """ Extracts ChArUco points. If you can provide camera matrices, it may be more accurate. :param dictionary: aruco dictionary definition :param board: aruco board definition :param image: grey scale image in which to search :param camera_matrix: if specified, the 3x3 camera intrinsic matrix :param distortion_coefficients: if specified, the distortion coefficients :param filtering: if True, filter out wrongly detected markers :return: marker_corners, marker_ids, chessboard_corners, chessboard_ids """ detection_parameters = aruco.DetectorParameters_create() detection_parameters.maxErroneousBitsInBorderRate = 0.1 detection_parameters.perspectiveRemovePixelPerCell = 30 detection_parameters.perspectiveRemoveIgnoredMarginPerCell = 0.3 marker_corners, marker_ids, _ =\ aruco.detectMarkers(image, dictionary, parameters=detection_parameters) chessboard_corners = None chessboard_ids = None if marker_corners: if filtering: marker_corners, marker_ids = \ filter_out_wrong_markers(marker_corners, marker_ids, board) _, chessboard_corners, chessboard_ids \ = aruco.interpolateCornersCharuco( markerCorners=marker_corners, markerIds=marker_ids, image=image, board=board, cameraMatrix=camera_matrix, distCoeffs=distortion_coefficients ) return marker_corners,\ marker_ids,\ chessboard_corners,\ chessboard_ids
[docs]def draw_charuco_corners(image, chessboard_corners, chessboard_ids): """ Function to draw chessboard corners on an image. :param image: input image :param chessboard_corners: from detect_charuco_points :param chessboard_ids: from detect_charuco_points :return: new image with corners marked """ cloned = image.copy() output = cv2.aruco.drawDetectedCornersCharuco( image=cloned, charucoCorners=chessboard_corners, charucoIds=chessboard_ids) return output
[docs]def erase_charuco_markers(image, marker_corners): """ Method to automatically blank out ChArUco markers, leaving an image that looks like it contains just a chessboard, rather than ChArUco board. It does this by drawing a plain white polygon, with vertices defined by the tag detection process. So, on a synthetic image, this works perfectly. On a real image, due to blurring or other artefacts such as combing, there may be some residual. :param image: image containing a view of a ChArUco board. :param marker_corners: detected corners :return: edited image """ cloned = image.copy() for marker in marker_corners: reshaped = marker.reshape(-1, 1, 2).astype(int) asarray = np.asarray(reshaped) cv2.fillConvexPoly(cloned, asarray, 255) return cloned
[docs]def make_charuco_with_chessboard( dictionary=cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250), charuco_squares=(19, 26), charuco_size=(5, 4), pixels_per_millimetre=10, chessboard_squares=(9, 14), chessboard_size=3, chessboard_border=0.7 ): """ Helper function to make an image of a calibration target combining ChArUco markers and a chessboard. It's up to the caller to work out a nice number of pixels per millimetre, so that the resultant image is correctly scaled. Defaults are as used in SmartLiver project. Not also, that we compute the image and coordinates in portrait, but it's used in landscape. :param dictionary: ChArUco dictionary :param charuco_squares: tuple of (squares in x, squares in y) :param charuco_size: tuple of (external size, internal tag size) in mm :param pixels_per_millimetre: which determines size of eventual image. :param chessboard_squares: tuple of (squares in x, squares in y) :param chessboard_size: size of chessboard squares in mm :param chessboard_border: border round chessboard, as fraction of square :return: calibration image """ charuco_pixels_per_square = charuco_size[0] * pixels_per_millimetre charuco_image, _ = make_charuco_board( dictionary, (charuco_squares[0], charuco_squares[1]), (charuco_size[0], charuco_size[1]), (charuco_squares[0] * charuco_pixels_per_square, charuco_squares[1] * charuco_pixels_per_square) ) centre_of_image = ( ((charuco_squares[0] * charuco_pixels_per_square) - 1) / 2.0, ((charuco_squares[1] * charuco_pixels_per_square) - 1) / 2.0 ) # Creates minimum size chessboard, one pixel per chessboard square. chessboard_image = np.zeros((chessboard_squares[1], chessboard_squares[0]), dtype=np.uint8) # pylint: disable=invalid-name for x in range(0, chessboard_squares[0]): for y in range(0, chessboard_squares[1]): if x % 2 == 0 and y % 2 == 0 or x % 2 == 1 and y % 2 == 1: chessboard_image[y][x] = 255 # Now we rescale it up, to the right number of pixels. chessboard_scale = chessboard_size * pixels_per_millimetre chessboard_image = cv2.resize(chessboard_image, None, fx=chessboard_scale, fy=chessboard_scale, interpolation=cv2.INTER_NEAREST) # Draw white polygon around chessboard corners = np.zeros((4, 2)) corner_offsets = ( ((chessboard_squares[0] / 2) + chessboard_border) * chessboard_scale, ((chessboard_squares[1] / 2) + chessboard_border) * chessboard_scale ) corners[0][0] = int(centre_of_image[0] - corner_offsets[0]) corners[0][1] = int(centre_of_image[1] - corner_offsets[1]) corners[1][0] = int(centre_of_image[0] + corner_offsets[0]) corners[1][1] = int(centre_of_image[1] - corner_offsets[1]) corners[2][0] = int(centre_of_image[0] + corner_offsets[0]) corners[2][1] = int(centre_of_image[1] + corner_offsets[1]) corners[3][0] = int(centre_of_image[0] - corner_offsets[0]) corners[3][1] = int(centre_of_image[1] + corner_offsets[1]) cv2.fillConvexPoly(charuco_image, corners.astype(np.int32), 255) # Insert chessboard inside white polygon s_x = int(centre_of_image[0] - ((chessboard_image.shape[1] - 1) / 2)) s_y = int(centre_of_image[1] - ((chessboard_image.shape[0] - 1) / 2)) charuco_image[s_y:s_y + chessboard_image.shape[0], s_x:s_x + chessboard_image.shape[1]] = chessboard_image return charuco_image