Source code for openalea.phenomenal.calibration.chessboard

# -*- python -*-
#
#       Copyright INRIA - CIRAD - INRA
#
#       Distributed under the Cecill-C License.
#       See accompanying file LICENSE.txt or copy at
#           http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html
#
# ==============================================================================


import cv2
import numpy
import json
import collections
import os
# ==============================================================================

__all__ = ["Target", "Chessboard", "Chessboards"]

# ==============================================================================


def _contour(corners, corners_w, corners_h):
    pts = corners[:,0,:].reshape((corners_h, corners_w, 2)).astype(int)
    return numpy.array((pts[0, 0, :], pts[0, -1, :], pts[-1, -1, :], pts[-1, 0, :]))


[docs] def detect_checkerboards(image, corners_w, corners_h, recursive=False, max_items=2): """Find chessboard corners , with recursive search for multiple chessboard on a single image Args: image: numpy array of pixel intensities (rgb_color or grayscale) corners_w: the number of corners (intersections of cheesboard squares) along chessboard width corners_h: the number of corners (intersections of cheesboard squares) along chessboard height recursive: should we recursively apply the search ? Returns: a list of pixel coordinates of corners for each chessboard found """ if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) shape = (corners_w, corners_h) corners = [] found = True while found: try: found, _corners = cv2.findChessboardCorners( image, shape, flags=cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE) if found: cv2.cornerSubPix( image, _corners, (11, 11), (-1, -1), criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) corners.append(_corners) print(f'{corners_w}x{corners_h} checkerboard found (x{len(corners)})') if recursive: mask = numpy.zeros_like(image) contour = _contour(_corners, corners_w=corners_w, corners_h=corners_h) cv2.fillPoly(mask, pts=[contour], color=255) image = cv2.bitwise_and(image, image, mask=cv2.bitwise_not(mask)) except cv2.error: found = False if not recursive: break if len(corners) == max_items: break return corners
[docs] class TargetFinder: """Find calibration targets of different types on a collection of images using minimal information"""
[docs] def __init__(self, targets=None, source_dir=None): if targets is None: self.targets = [{'class': 'Checkerboard', 'shape': (8, 6)}] else: self.targets = targets self.image_points = collections.defaultdict(dict) self.image_paths = collections.defaultdict(dict) self.image_sizes = dict() self.source_dir = source_dir
def abspath(self, path): if self.source_dir is None: return path else: return os.path.join(self.source_dir, path) def find_targets(self, path, camera_id, rotation, recursive=True, max_items=2): image = cv2.imread(self.abspath(path), cv2.IMREAD_GRAYSCALE) for target in self.targets: if target['class'] == 'Checkerboard': corners_w, corners_h = target['shape'] corners = detect_checkerboards(image=image, corners_w=corners_w, corners_h=corners_h, recursive=recursive, max_items=max_items) self.image_points[camera_id][rotation] = [c.tolist() for c in corners] if len(self.image_points[camera_id][rotation]) >= max_items: break h, w = image.shape[:2] self.image_sizes[camera_id] = (w, h) self.image_paths[camera_id][rotation] = path def to_dict(self): d = dict() d['targets'] = self.targets d['image_sizes'] = self.image_sizes d['image_points'] = self.image_points d['image_paths'] = self.image_paths return d @staticmethod def from_dict(d, source_dir=None): instance = TargetFinder(targets=d['targets'], source_dir=source_dir) for cid in d['image_points']: for rotation in d['image_points'][cid]: rot=int(float(rotation)) instance.image_points[cid][rot] = d['image_points'][cid][rotation] for cid in d['image_paths']: for rotation in d['image_paths'][cid]: rot=int(float(rotation)) instance.image_paths[cid][rot] = d['image_paths'][cid][rotation] instance.image_sizes = d['image_sizes'] return instance def dump(self, filename): with open(filename, 'w') as output_file: json.dump(self.to_dict(), output_file, sort_keys=True, indent=4, separators=(',', ': ')) @staticmethod def load(filename, source_dir=None): with open(filename, 'r') as input_file: d = json.load(input_file) return TargetFinder.from_dict(d, source_dir=source_dir)
[docs] def compass_position(rotation, south_rotation, clockwise=True, intercardinal=False): """Find the cardinal position of a rotated object Args: rotation: the (positive) rotation angle south: the (positive) rotation angle that position the object the southest clockwise : is the object rotating clockwise ? (default True) intercardinal : should the intercardinal position be returned ? Returns: (str) the cardinal position (one of 'North', 'South', 'East' or 'West') of the object, or if intercadinal = True the intercardinal position (one of 'NorthEast', 'SouthEast', 'SouthWest' or 'NorthWest') """ if intercardinal: quadrants = ['SouthWest', 'NorthWest', 'NorthEast', 'SouthEast'] if not clockwise: quadrants = ['SouthEast', 'NorthEast', 'NorthWest', 'SouthWest'] else: quadrants = ['South', 'West', 'North', 'East'] if not clockwise: quadrants = ['South', 'East', 'North', 'West'] # boundary angles for the different quadrants first_bound = 45 if intercardinal: first_bound = 90 boundaries = [(south_rotation + first_bound + 90 * i) % 360 for i in range(4)] sorter = numpy.argsort(boundaries) quadrants = [quadrants[i] for i in sorter] return quadrants[numpy.searchsorted(boundaries, rotation, sorter=sorter) % len(quadrants)]
[docs] class Target(object):
[docs] def __init__(self): self.image_points = collections.defaultdict(dict)
def add_image_points(self, camera_view, angle, image): pass def get_3d_local_points(self): pass def get_image_points(self): pass
[docs] class Chessboard(object):
[docs] def __init__(self, square_size=50, shape=(7, 7), facing_angles=None): """Instantiate a chessboard object Args: square_size: length (world units) of the side of an elemental square of the chessboard shape: (int, int) the number of square detected along chessboard width and height facing_angles: a {camera_id: facing_angle} dict indicating for what value of the turntable rotation consign the chessboard is facing the camera with topleft corner closest to topleft side of the image. """ self.square_size = square_size self.shape = shape self.image_points = collections.defaultdict(dict) self.image_ids = collections.defaultdict(dict) self.facing_angles = dict() self.image_sizes = dict() if facing_angles is not None: self.facing_angles = facing_angles
def __str__(self): s = ("Chessboard Attributes :\n" "Square size (mm): {}\n" "Shape : {}\n".format(self.square_size, self.shape)) return s
[docs] def get_corners_local_3d(self, old_style=False): """ Chessboard local frame is defined by chessboard center, x axis along width (left >right), Y-axis along height (bottom -> up) and z axis normal to chessboard plane Chessboard corners are returned ordered line by line, from top left to bottom right old_order returns in the expected order / origin for oldcalibration (phenomenal < 1.7.1) """ square_size = self.square_size width, height = self.shape corners_local_3d = list() if old_style: for y in range(height): for x in range(width): v = numpy.array([x * square_size, y * square_size, 0.0]) corners_local_3d.append(v) else: origin = numpy.array([width * square_size / 2., height * square_size / 2., 0]) for y in reversed(range(height)): for x in range(width): v = numpy.array([x * square_size, y * square_size, 0.0]) - origin corners_local_3d.append(v) return corners_local_3d
def get_corners_2d(self, id_camera): corners_2d = dict() if id_camera in self.image_points: for rotation in self.image_points[id_camera]: corners_2d[rotation] = self.image_points[id_camera][rotation][:, 0, :] return corners_2d
[docs] def order_image_points(self, image_points, rotation, facing_angle, clockwise_rotation=True, check_only=False): """ order image points to match order of corner points (see details) Args: image_points: image points detected by openCV findChessboardCorners rotation: (int) rotation consign (positive, in degrees). The rotation consign is the rounded angle by which the turntable has turned before image acquisition facing_angle: the turntable rotation consign that make the chessboard face the camera clockwise_rotation (bool): are targets rotating clockwise ? (default True) check_only (bool): if True, do not return ordered points, but a bool indicating whether input points were already ordered or not. Returns: image_points, in the expected order Details: Chessboard corners are detected with OpenCV findChessboardCorners function, that always return corners from top left to bottom right position on the image (left-right axis being chessboard width). This corresponds to expected order if chessboard upper side is pointing to the top of the image, but to the reversed expected order if chessboard upper side is pointing to the base of the image. We suppose that reversed order detection occurs for rotations +/- 90 deg far from facing_angle """ width, _ = self.shape # du, dv between fist and last point of first line du = image_points[width - 1, 0, 0] - image_points[0, 0, 0] dv = image_points[width - 1, 0, 1] - image_points[0, 0, 1] cpos = compass_position(rotation, south_rotation=facing_angle, clockwise=clockwise_rotation, intercardinal=True) ordered = True if abs(du) > abs(dv): # target is horizontal on image, use north/ south criteria if (cpos.startswith('South') and du < 0) or (cpos.startswith('North') and du > 0): ordered = False else: # target is vertical, use east / west criteria if (cpos.endswith('West') and dv < 0) or (cpos.endswith('East') and dv > 0): ordered = False if check_only: return ordered if ordered: return image_points else: return image_points[::-1]
[docs] def check_order(self, check_only=False): """Re-order detected image points using facing angles""" for id_camera in self.image_points: for rotation in self.image_points[id_camera]: facing = self.facing_angles[id_camera] ordered = self.order_image_points(self.image_points[id_camera][rotation], rotation, facing, check_only=check_only) if check_only: if not ordered: print('{}, angle {}: image points are not ordered'.format(id_camera, rotation)) else: self.image_points[id_camera][rotation] = ordered
[docs] def detect_corners(self, id_camera, rotation, image, check_order=True, image_id=None): """ Detection of pixel coordinates of chessboard corner points Args: id_camera: (str) label of the camera that acquired the image rotation: (int) rotation consign (positive, in degrees). The rotation consign is the rounded angle by which the turntable has turned before image acquisition image: (image) numpy array of pixel intensities (rgb_color or grayscale) check_order: (bool) Check if the detected image points are in the expected order: image points order should match local 3d coordinates order image_id (str, optional): if given, the image name is kept in image_ids instance variable. Returns: True if chessboard corner are found otherwise False. Side effects: image points are added to the instance image point list """ if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) if id_camera not in self.image_sizes: h, w = image.shape[:2] self.image_sizes[id_camera] = (w, h) if image_id is not None: self.image_ids[id_camera][rotation] = image_id corners_w, corners_h = self.shape _corners = detect_checkerboards(image, corners_w, corners_h, recursive=False) if len(_corners) < 1: found = False corners = [] else: found = True corners = _corners[0] if found: if check_order: if id_camera not in self.facing_angles: raise ValueError('facing rotation should be specified for order checking') corners = self.order_image_points(corners, rotation, self.facing_angles[id_camera]) self.image_points[id_camera][rotation] = corners return found
def image_resolutions(self): def _dist(pix1, pix2): x1, y1 = pix1 x2, y2 = pix2 return numpy.sqrt((x2-x1)**2 +(y2 -y1)**2) def pixel_area(a, w): topleft = a[0] topright = a[:w, :][-1] bottomleft = a[-w, :] bottomright = a[-1] top = _dist(topleft, topright) bottom = _dist(bottomleft, bottomright) left = _dist(topleft, bottomleft) right = _dist(topright, bottomright) return numpy.mean([top, bottom]) * numpy.mean([left, right]) width, height = self.shape area = width * height * self.square_size**2 resolutions = {cid: [] for cid in self.image_points} for id_camera, cam_pts in self.image_points.items(): for rotation in cam_pts: pts = self.get_corners_2d(id_camera)[rotation] pix_area = pixel_area(pts, width) resolutions[id_camera].append(numpy.sqrt(pix_area / area)) resolutions = {cid: numpy.mean(res) for cid, res in resolutions.items()} return resolutions def dump(self, filename): # Convert to json format image_points = collections.defaultdict(dict) for id_camera in self.image_points: for id_image in self.image_points[id_camera]: image_points[id_camera][id_image] = \ self.image_points[id_camera][id_image].tolist() save_class = dict() save_class['square_size'] = self.square_size save_class['shape'] = self.shape save_class['image_points'] = image_points if len(self.facing_angles) > 0: save_class['facing_angles'] = self.facing_angles if len(self.image_ids) > 0: save_class['image_ids'] = self.image_ids if len(self.image_sizes) > 0: save_class['image_sizes'] = self.image_sizes with open(filename, 'w') as output_file: json.dump(save_class, output_file, sort_keys=True, indent=4, separators=(',', ': ')) def get_image(self, id_camera, rotation, data_dir, show_corners=False): rgb = None if id_camera in self.image_ids: if rotation in self.image_ids[id_camera]: path = os.path.join(data_dir, self.image_ids[id_camera][rotation]) bgr = cv2.imread(path, cv2.IMREAD_COLOR) if show_corners: found = rotation in self.image_points[id_camera] if found: corners = self.image_points[id_camera][rotation] bgr = cv2.drawChessboardCorners(bgr, tuple(self.shape), corners, found) rgb = bgr[:, :, ::-1] return rgb @staticmethod def load(filename): with open(filename, 'r') as input_file: save_class = json.load(input_file) square_size = float(save_class['square_size']) shape = [int(val) for val in save_class['shape']] chessboard = Chessboard(square_size, shape) image_points = save_class['image_points'] # Convert to numpy format for id_camera in image_points: for rotation in image_points[id_camera]: rot = int(float(rotation)) # restore dtype of opencv func chessboard.image_points[id_camera][rot] = \ numpy.array(image_points[id_camera][rotation]).astype(numpy.float32) if 'facing_angles' in save_class: chessboard.facing_angles = save_class['facing_angles'] if 'image_ids' in save_class: image_ids = save_class['image_ids'] for id_camera in image_ids: for rotation in image_ids[id_camera]: rot = int(float(rotation)) chessboard.image_ids[id_camera][rot] = image_ids[id_camera][rotation] if 'image_sizes' in save_class: chessboard.image_sizes = save_class['image_sizes'] return chessboard
[docs] class Chessboards(object): """A class for handling a collection of Chessboards objects imaged in the same system"""
[docs] def __init__(self, chessboards): """ Args: chessboards: a {chessboard_id: Chessboard, ...} dict """ self.chessboards = chessboards
[docs] @staticmethod def load(filenames): """ Args: filenames: a {chessboard_id: chessboard_filename, ...} dict Returns: """ chessboards = {k: Chessboard.load(v) for k, v in filenames.items()} return Chessboards(chessboards)
def image_sizes(self): image_sizes = {} for chess in self.chessboards.values(): image_sizes.update(chess.image_sizes) return image_sizes def cameras(self): return self.image_sizes().keys() def image_resolutions(self): image_resolutions = {cid: [] for cid in self.cameras()} for chess in self.chessboards.values(): for cid, res in chess.image_resolutions().items(): image_resolutions[cid].append(res) return {cid: numpy.mean(res) for cid, res in image_resolutions.items()} def facings(self): return {k: v.facing_angles for k, v in self.chessboards.items()} def image_points(self): return {camera: {k: v.get_corners_2d(camera) for k, v in self.chessboards.items()} for camera in self.cameras()} def target_points(self): return {k: v.get_corners_local_3d() for k, v in self.chessboards.items()} def get_image(self, target, id_camera, rotation, data_dir, show_corners=False): return self.chessboards[target].get_image(id_camera, rotation, data_dir, show_corners)