Source code for openalea.phenomenal.calibration.calibration

# -*- 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
#
# ==============================================================================
"""This module contains a calibration model for phenoarch experiment
where a target is rotating instead of a plant in the image acquisition system.
"""

# ==============================================================================
from __future__ import division, print_function, absolute_import

import warnings
import json
import math
import numpy
from scipy.optimize import minimize
import cv2
import os
from itertools import islice
from copy import deepcopy
from collections import defaultdict

from .frame import Frame, x_axis, y_axis, z_axis
from .transformations import concatenate_matrices, rotation_matrix
from .chessboard import Chessboard

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

__all__ = [
    "Calibrator",
    "CalibrationCamera",
    "CalibrationFrame",
    "CalibrationSetup",
    "Calibration",
    "OldCalibrationCamera",
    "OldCalibration",
]


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


[docs] def normalise_angle(angle): """normalise an angle to the [-pi, pi] range""" angle = numpy.array(float(angle)) modulo = 2 * numpy.pi angle %= modulo # force to [0, modulo] range angle = (angle + modulo) % modulo return angle - numpy.where(angle > modulo / 2.0, modulo, 0)
[docs] def angle3(v1, v2): """acute angle between 2 3d vectors""" x = numpy.dot(v1, v2) / (numpy.linalg.norm(v1) * numpy.linalg.norm(v2)) angle = numpy.arccos(numpy.clip(x, -1, 1)) return numpy.degrees(angle)
[docs] class CalibrationFrame: """A class for objects with local frames used for calibration The object local frame is a translated / rotated transform of the world frame around (fixed) world axis. """
[docs] def __init__(self): self._pos_x = None self._pos_y = None self._pos_z = None self._rot_x = None self._rot_y = None self._rot_z = None
def set_vars(self, d): for key, value in d.items(): setattr(self, key, value) def to_json(self): d = vars(self) return d @staticmethod def from_json(d): cf = CalibrationFrame() cf.set_vars(d) return cf @staticmethod def from_tuple(pars): cf = CalibrationFrame() cf._pos_x, cf._pos_y, cf._pos_z, cf._rot_x, cf._rot_y, cf._rot_z = pars return cf @staticmethod def load(filename): with open(filename, "r") as input_file: save_class = json.load(input_file) c = CalibrationFrame.from_json(save_class) return c def dump(self, filename): save_class = self.to_json() with open(filename, "w") as output_file: json.dump( save_class, output_file, sort_keys=True, indent=4, separators=(",", ": "), ) @staticmethod def frame(pos_x, pos_y, pos_z, rot_x, rot_y, rot_z): origin = (pos_x, pos_y, pos_z) mat_rot_x = rotation_matrix(rot_x, x_axis) mat_rot_y = rotation_matrix(rot_y, y_axis) mat_rot_z = rotation_matrix(rot_z, z_axis) fr_x = Frame(mat_rot_x[:3, :3].T) fr_y = Frame(mat_rot_y[:3, :3].T) fr_z = Frame(mat_rot_z[:3, :3].T) axes = fr_z.global_point( fr_y.global_point(fr_x.global_point((x_axis, y_axis, z_axis))) ) return Frame(axes, origin) def get_frame(self): return self.frame( self._pos_x, self._pos_y, self._pos_z, self._rot_x, self._rot_y, self._rot_z ) def get_extrinsic(self): extrinsic = numpy.identity(4) fr = self.get_frame() extrinsic[:3, :3] = fr.rotation_to_local() extrinsic[:3, 3] = fr.local_point((0, 0, 0)) return extrinsic[:3,] def __str__(self): out = "" out += "\tPosition X : " + str(self._pos_x) + "\n" out += "\tPosition Y : " + str(self._pos_y) + "\n" out += "\tPosition Z : " + str(self._pos_z) + "\n\n" out += "\tRotation X : {} rad / {} deg\n".format( self._rot_x, numpy.degrees(self._rot_x) ) out += "\tRotation Y : {} rad / {} deg\n".format( self._rot_y, numpy.degrees(self._rot_y) ) out += "\tRotation Z : {} rad / {} deg\n\n".format( self._rot_z, numpy.degrees(self._rot_z) ) return out
[docs] class CalibrationCamera(CalibrationFrame): """A class for calibration of Camera The camera is a perfect pinhole camera associated to a calibrationframe allowing its positioning in space. Camera and image frames are as depicted in https://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html That is camera frame origin is image center, z-axis points toward the scene (camera optical axis), x+ is left-> right along image width, y+ is up->down along image height. Image frame origin is top-left, u is left->right along image width, v is up->down along image height """
[docs] def __init__(self): CalibrationFrame.__init__(self) # Camera Parameters self._width_image = None self._height_image = None self._focal_length_x = None self._aspect_ratio = None
def __str__(self): out = "" fmm = numpy.round( self._focal_length_x / max(self._width_image, self._height_image) * 36 ) out += ( "\tFocal length X : " + str(self._focal_length_x) + " (" + str(fmm) + "mm)\n" ) out += "\tPixel aspect ratio : " + str(self._aspect_ratio) + "\n" if self._width_image is not None: out += "\tOptical Center X : " + str(self._width_image / 2.0) + "\n" out += "\tOptical Center Y : " + str(self._height_image / 2.0) else: out += "\tOptical Center X : " + str(self._width_image) + "\n" out += "\tOptical Center Y : " + str(self._height_image) out += "\n\n" out += CalibrationFrame.__str__(self) return out
[docs] @staticmethod def pixel_coordinates( point_3d, width_image, height_image, focal_length_x, aspect_ratio ): """Compute image coordinates of a 3d point positioned in camera frame Args: - point (float, float, float): a point/array of points in space expressed in camera frame coordinates return: - (int, int): coordinate of point in image in pix """ pt = numpy.array(point_3d) x, y, z = pt.T focal_length_y = aspect_ratio * focal_length_x u = x / z * focal_length_x + width_image / 2.0 v = y / z * focal_length_y + height_image / 2.0 if len(pt.shape) > 1: return numpy.column_stack((u, v)) else: return u, v
def get_pixel_coordinates(self): def pixel_coords(pts): return self.pixel_coordinates( pts, self._width_image, self._height_image, self._focal_length_x, self._aspect_ratio, ) return pixel_coords
[docs] @staticmethod def pixel_coordinates_2(point_3d, cx, cy, fx, a): """Compute image coordinates of a 3d point Args: - point (float, float, float): a point in space expressed in camera frame coordinates return: - (int, int): coordinate of point in image in pix """ pt = numpy.array(point_3d) x, y, z = pt.T fy = a * fx u = x / z * fx + cx v = y / z * fy + cy if len(pt.shape) > 1: return numpy.column_stack((u, v)) else: return u, v
def get_projection(self): fr_cam = self.get_frame() pixel_coords = self.get_pixel_coordinates() def projection(pts): return pixel_coords(fr_cam.local_point(pts)) return projection def image_shape(self): return self._height_image, self._width_image def get_intrinsic(self): intrinsic = numpy.identity(3) fx = self._focal_length_x fy = self._focal_length_x * self._aspect_ratio cx = self._width_image / 2.0 cy = self._height_image / 2.0 di = numpy.diag_indices(2) intrinsic[:2, 2] = (cx, cy) intrinsic[di] = (fx, fy) return intrinsic @staticmethod def from_json(save_class): c = CalibrationCamera() if "_focal_length_y" in save_class: fy = save_class.pop("_focal_length_y") save_class["_aspect_ratio"] = fy / save_class["_focal_length_x"] c.set_vars(save_class) return c @staticmethod def load(filename): with open(filename, "r") as input_file: save_class = json.load(input_file) if "cam_pos_x" in save_class: raise ValueError( "Old style calibration should now be loaded with OldCalibrationCamera.load method" ) c = CalibrationCamera.from_json(save_class) return c
[docs] class Calibrator: """Class for end to end calibration of rotating multiview acquisition system"""
[docs] def __init__( self, south_camera, cameras=None, targets=None, chessboards=None, image_paths=None, clockwise_rotation=True, world_unit="mm", data_dir=".", calibration_dir="./Calibration", ): """ Setup Calibrator with the calibration layout Args: south_camera: a (camera_id, inclination, distance) tuple defining the approximative position of the south camera. The south camera is used to define world frame during and after calibration (see details bellow). camera_id is a string referencing the camera, inclination is the (approximative) angle (degree, positive) between the axis of rotation and the axe passing through the turntable center and camera optical center, and distance is the (approximative) distance from turntable center to camera optical center (in world units). cameras: a {camera_id: (inclination, distance, rotation_to_south), ... } dict of 3-tuples specifying the approximative position of cameras (other than south camera). camera_id is a string referencing the camera, inclination is the (approximative) angle (degree, positive) between the axis of rotation and the axe passing through the turntable center and camera optical center, distance is the (approximative) distance from turntable center to camera optical center (in world units), and rotation_to_south is the (approximative) rotation angle (degrees, positive in the direction of rotation of the turntable) that align the camera to south. targets: a {target_id: (inclination, rotation_to_south), ...} dict of 2-tuples specifying the approximative position of calibration targets. target_id is a string referencing the target, inclination is the angle (degree, positive) between the axis of rotation and target normal, and rotation_to_south is the (approximative) rotation angle (degrees, positive in the direction of rotation of the turntable that align the target normal(or target base) toward south. chessboards: a {target_id: (between_corners, corners_h, corners_v), ...} dict of tuples describing the layout of chessboards corners points (intersections of cheesboard squares) associated to a target. target_id is a string referencing the target, between_corners is the distance (in world unit) between two corner points, corners_h is the number of corners in the horizontal direction, corners_v in the number of corners in the vertical direction. image_paths: a {camera_id : {rotation_angle : path, ...},...} nested dict of image paths pointing to the image camera 'camera_id' after a rotation of 'rotation_angle' degrees of the turntable. Path are relative to data_dir or absolute paths if data_dir is None clockwise_rotation: (bool): is the turntable rotating clockwise ? (default True) world_unit (str): a label describing world units data_dir: the path of the directory containing images calibration_dir: the path of the directory to be used for calibration outputs Details: The world 3D coordinates are expressed in a 'native' frame defined by the axis of rotation and the south camera as follows: - The axis of rotation of the rotating system, oriented toward the sky, defines the world Z+. - The altitude of the south camera defines world origin (Z=0). - the axis originating at south camera optical center and passing at world origin defines Y+. The world coordinates can be redefined by positioning user-defined frames in this native frame after calibration (see e.g. 'add_target_frame' or 'add_pot_frame' methods) """ if cameras is None: cameras = {} if targets is None: targets = {} if chessboards is None: chessboards = {} south_camera_id, south_inclination, south_distance = south_camera cameras = { camera_id: dict( inclination=inclination, distance=distance, rotation_to_south=rotation_to_south, ) for camera_id, (inclination, distance, rotation_to_south) in cameras.items() } cameras.update( { south_camera_id: dict( inclination=south_inclination, distance=south_distance, rotation_to_south=0, ) } ) self.layout = dict( south_camera=south_camera_id, cameras=cameras, targets={ target_id: dict( inclination=inclination, rotation_to_south=rotation_to_south ) for target_id, (inclination, rotation_to_south) in targets.items() }, chessboards={ target_id: dict( between_corners=between_corners, corners_h=corners_h, corners_v=corners_v, ) for target_id, ( between_corners, corners_h, corners_v, ) in chessboards.items() }, ) self.image_paths = image_paths self.clockwise = clockwise_rotation self.world_unit = world_unit self.data_dir = data_dir self.calibration_dir = calibration_dir self.image_points = {target_id: defaultdict(dict) for target_id in targets} self.image_resolutions = defaultdict(dict) self.image_sizes = defaultdict(dict) self._facings = defaultdict(dict) for camera_id, camera in self.layout["cameras"].items(): for target_id, target in self.layout["targets"].items(): self._facings[target_id].update( { camera_id: target["rotation_to_south"] - camera["rotation_to_south"] } )
def abspath(self, path): if self.data_dir is None: return path else: return os.path.join(self.data_dir, path) def alpha(self, rotation): angle = numpy.radians(rotation) if self.clockwise: angle *= -1 return angle
[docs] def aiming_angle(self, camera_id, target_id, rotation): """The angle between camera Z- (opposite camera look-at axis) and target Z+ for a given rotation""" px, py, pz = 0, 0, 0 rx, ry, rz = 0, 0, 0 rx += numpy.radians(self.layout["targets"][target_id]["inclination"]) rz += self.alpha( rotation - self.layout["targets"][target_id]["rotation_to_south"] ) fr_target = CalibrationFrame.frame(px, py, pz, rx, ry, rz) # px, py, pz = 0, 0, 0 rx, ry, rz = numpy.pi, 0, 0 rx += numpy.radians(self.layout["cameras"][camera_id]["inclination"]) rz += self.alpha(self.layout["cameras"][camera_id]["rotation_to_south"]) fr_camera = CalibrationFrame.frame(px, py, pz, rx, ry, rz) return angle3(fr_target.global_vec((0, 0, 1)), fr_camera.global_vec((0, 0, -1)))
@staticmethod def quadrant_mask(quadrant, image): if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) mask = numpy.empty_like(image) h, w = mask.shape if quadrant == "North": mask[: h // 2, :] = 255 elif quadrant == "South": mask[h // 2 :, :] = 255 elif quadrant == "West": mask[:, : w // 2] = 255 elif quadrant == "East": mask[:, w // 2 :] = 255 else: raise ValueError("Unimplemented quadrant: " + quadrant) return mask @staticmethod def roi_mask(image): mask = numpy.empty_like(image) return mask
[docs] def detect_corners(self, cameras=None, masks=None, maximal_aiming_angle=65): """Detection of pixel coordinates of chessboard corner points Args: cameras: a string or a list of elements specifying on which images should the detection be done. Element can either be a camera_id string or a (camera_id, list_of_angles) tuple specifying which angle to consider. If no angle are provided, all angles are considered. If cameras is None (default), all images are used masks: a {target_id : [(camera_id, rotation, mask),...],...} dict of list of tuples specifying if a mask should be applied to the image before detection. mask can either be a string specifying the image quadrant ('North', 'South', 'East' or 'West') containing the target, a list of image coordinates (u,v) specifying the Region of Interest containing the target. maximal_aiming_angle: images with a target that form an angle superior to this value are skipped """ consider = {} if cameras is None: consider = {k: list(v) for k, v in self.image_paths.items()} elif isinstance(cameras, str): consider = {cameras: list(self.image_paths[cameras])} else: for item in cameras: if isinstance(item, str): camera_id = item rotation_list = list(self.image_paths[camera_id]) else: camera_id, rotation_list = item consider[camera_id] = rotation_list mask_dict = {} if masks is not None: mask_dict = {tid: defaultdict(dict) for tid in masks} for tid, list_items in masks.items(): for cid, rot, mask in list_items: mask_dict[tid][cid][rot] = mask for target_id, c in self.layout["chessboards"].items(): chessboard = Chessboard( square_size=c["between_corners"], shape=(c["corners_h"], c["corners_v"]), facing_angles=self._facings[target_id], ) for camera_id, rotation_list in consider.items(): for rotation in rotation_list: path = self.image_paths[camera_id][rotation] found = False aiming_angle = self.aiming_angle(camera_id, target_id, rotation) if aiming_angle > maximal_aiming_angle: print( "Target {} Camera {} Angle {} - Skipped (angle {} > maximal_aiming_angle)".format( target_id, camera_id, str(rotation), str(aiming_angle) ) ) else: image = cv2.imread(self.abspath(path), cv2.IMREAD_GRAYSCALE) if target_id in mask_dict: if camera_id in mask_dict[target_id]: if rotation in mask_dict[target_id][camera_id]: mask = mask_dict[target_id][camera_id][rotation] if isinstance(mask, str): mask = self.quadrant_mask(mask, image) else: mask = self.roi_mask(image) image = numpy.bitwise_and(image, mask) found = chessboard.detect_corners( camera_id, rotation, image, check_order=True, image_id=path ) print( "Target {} Camera {} Angle {} - Chessboard corners {}".format( target_id, camera_id, str(rotation), "found" if found else "not found", ) ) if found: self.image_points[target_id][camera_id][rotation] = ( chessboard.image_points[camera_id][rotation] ) self.image_sizes.update(chessboard.image_sizes) for c, res in chessboard.image_resolutions().items(): self.image_resolutions[c][target_id] = res
[docs] def calibrate(self, verbose=True): """Compute calibration""" cameras = { camera_id: (d["distance"], d["inclination"]) for camera_id, d in self.layout["cameras"].items() } targets = { target_id: (0, d["inclination"]) for target_id, d in self.layout["targets"].items() } resolutions = { k: numpy.mean(list(v.values())) for k, v in self.image_resolutions.items() } setup = CalibrationSetup( cameras, targets, resolutions, self.image_sizes, self._facings, self.clockwise, ) reference_target = next(iter(self._facings)) reference_camera = self.layout["south_camera"] cameras, targets, reference_camera, clockwise = setup.setup_calibration( reference_camera=reference_camera, reference_target=reference_target ) target_points = {} image_points = defaultdict(dict) for target_id, d in self.layout["chessboards"].items(): if target_id in self.image_points: chessboard = Chessboard( square_size=d["between_corners"], shape=(d["corners_h"], d["corners_v"]), facing_angles=self._facings[target_id], ) chessboard.image_points = self.image_points[target_id] target_points[target_id] = chessboard.get_corners_local_3d() for camera_id in self.image_points[target_id]: image_points[camera_id][target_id] = chessboard.get_corners_2d( camera_id ) calibration = Calibration( targets=targets, cameras=cameras, target_points=target_points, image_points=image_points, reference_camera=reference_camera, clockwise_rotation=clockwise, ) # Three steps calibration yield better results than direct calibration calibration.calibrate(fit_cameras=False, verbose=verbose) if len(cameras) > 1: calibration.calibrate( fit_angle_factor=False, fit_reference_camera=False, fit_targets=False, verbose=verbose, ) calibration.calibrate(verbose=verbose) return calibration
[docs] @staticmethod def target_frame(calibration): """Add a new world frame in calibration, similar to native frame, but whose origin is at the mean height of targets centers""" z_moy = numpy.mean([fr._pos_z for fr in calibration._targets.values()]) return CalibrationFrame.from_tuple((0, 0, z_moy, 0, 0, 0))
[docs] @staticmethod def pot_frame(v_pot, rail_orientation, calibration, dl=2000): """Add a new world frame to calibration based orientation (x-axis) chosen from top and origin chosen on side image""" p = calibration.get_projection("side", 0, "native") pt = calibration.get_projection("top", 0, "native") origin = (0, 0, 0) u, v = p(origin) du, dv = numpy.diff(p([origin, (0, 0, -dl)]), axis=0)[0] ut, vt = pt(origin) dut, dvt = numpy.diff(pt([origin, (0, 0, -dl)]), axis=0)[0] utp, vtp = ut + dut / dv * (v_pot - v), vt + dvt / dv * (v_pot - v) alpha_p = numpy.radians(rail_orientation) frame_points = [origin, (0, "y", 0)] image_points = { "side": [(u + du / dv * (v_pot - v), v_pot), None], "top": [ None, (utp + numpy.cos(alpha_p) * dl, vtp - numpy.sin(alpha_p) * dl), ], } pot_frame, _ = calibration.find_frame( image_points, frame_points, fixed_parameters={"_pos_x": 0, "_pos_y": 0, "_rot_x": 0, "_rot_y": 0}, start={"_pos_z": 0, "_rot_z": -numpy.pi / 2}, ) return pot_frame
@staticmethod def check_dir(path): dirname = os.path.dirname(path) if not os.path.exists(dirname): os.makedirs(dirname) def check_target_masks(self, masks, resize=0.25): outdir = os.path.join(self.calibration_dir, "check_target_masks") for target_id, list_items in masks.items(): for camera_id, rotation, mask in list_items: target_image = cv2.imread( self.abspath(self.image_paths[camera_id][rotation]) ) path = os.path.join( outdir, "_".join([target_id, camera_id, str(rotation)]) + ".jpg" ) if isinstance(mask, str): mask = self.quadrant_mask(mask, target_image) else: mask = self.roi_mask(target_image) masked = cv2.bitwise_and(target_image, target_image, mask=mask) shape = [int(i * resize) for i in target_image.shape[:2]] resized = cv2.resize(masked, shape, interpolation=cv2.INTER_AREA) self.check_dir(path) cv2.imwrite(path, resized) def check_image_points(self, resize=0.25): outdir = os.path.join(self.calibration_dir, "check_image_points") for target_id in self.image_points: c = self.layout["chessboards"][target_id] chessboard_shape = (c["corners_h"], c["corners_v"]) for camera_id in self.image_points[target_id]: for rotation, img_pts in self.image_points[target_id][ camera_id ].items(): img = cv2.imread( self.abspath(self.image_paths[camera_id][rotation]) ) img = cv2.drawChessboardCorners( img, chessboard_shape, img_pts, True ) shape = [int(i * resize) for i in img.shape[:2]] resized = cv2.resize(img, shape, interpolation=cv2.INTER_AREA) path = os.path.join( outdir, "_".join([target_id, camera_id, str(rotation)]) + ".jpg" ) self.check_dir(path) cv2.imwrite(path, resized) def check_target_frame(self, calibration, resize=0.25, leaf=100): outdir = os.path.join(self.calibration_dir, "check_target_frame") if "target" not in calibration.frames: calibration.frames["target"] = self.target_frame(calibration) for camera_id, rot_dict in self.image_paths.items(): for rotation, path in rot_dict.items(): img = cv2.imread(self.abspath(path)) frame_lines = calibration.frame_lines( camera_id, rotation, frame="target", leaf=leaf ) for origin, end, col, w in frame_lines: cv2.line(img, origin, end, col, w) shape = [int(i * resize) for i in img.shape[:2]] resized = cv2.resize(img, shape, interpolation=cv2.INTER_AREA) path = os.path.join( outdir, "_".join([camera_id, str(rotation)]) + ".jpg" ) self.check_dir(path) cv2.imwrite(path, resized)
[docs] def save_image_points(self, prefix=None): """save image points to calibration dir""" for target_id, image_points in self.image_points.items(): c = self.layout["chessboards"][target_id] chessboard = Chessboard( square_size=c["between_corners"], shape=(c["corners_h"], c["corners_v"]), facing_angles=self._facings[target_id], ) chessboard.image_points = image_points chessboard.image_sizes = self.image_sizes # TODO filter non useed ids chessboard.image_ids = self.image_paths if prefix is None: items = [] else: items = [prefix] items += ["image_points", target_id] path = os.path.join(self.calibration_dir, "_".join(items) + ".json") self.check_dir(path) chessboard.dump(path)
[docs] def load_image_points(self, image_points=None): """load image points""" for target_id in self.layout["chessboards"]: if image_points is not None and target_id in image_points: chessboard = image_points[target_id] else: items = ["image_points", target_id] path = os.path.join(self.calibration_dir, "_".join(items) + ".json") chessboard = Chessboard.load(path) self.image_points[target_id] = chessboard.image_points self.image_paths = chessboard.image_ids self.image_sizes.update(chessboard.image_sizes) for c, res in chessboard.image_resolutions().items(): self.image_resolutions[c][target_id] = res
[docs] class CalibrationSetup: """A class for helping the setup of a multi-view imaging systems to be calibrated"""
[docs] def __init__( self, cameras=None, targets=None, image_resolutions=None, image_sizes=None, facings=None, clockwise_rotation=True, ): """Instantiate a CalibrationSetup for positioning cameras and targets of a multiview acquisition system Args: cameras:{camera_id: (distance, inclination), ...}: a dict of parameters for positioning cameras in the system. distance is the distance to the axis of rotation (word units), along camera optical axis. inclination is the angle (degree, positive) between world z+ (axis of rotation) and camera z- (z-axis being optical axis). targets: {target_id: (distance, inclination), ...}: a dict of parameters for positioning targets. distance is the distance to the axis of rotation (word units), along target normal direction. inclination is the angle (degree, positive) between world z+ (axis of rotation) and target normal. image_resolutions: a {camera_id: resolution, ...} dict of image resolution (pixel per world unit) for an object located near the axis of rotation image_sizes is a {camera_id: (width, height), ...} dict giving image dimension in pixels facings : a {target_id: {camera_id: angle, ...}, ...} dict of dict giving the rotation consigns (degree, positive) for which a chessboard is facing a camera (ie with with topleft corner closest to topleft side of the image). clockwise_rotation (bool): are targets rotating clockwise ? (default True) """ self.cameras = cameras self.targets = targets self.image_resolutions = image_resolutions self.image_sizes = image_sizes self.facings = facings self.clockwise = clockwise_rotation self.world_origin = 0 self.reference_target_facing = 0
[docs] def setup_calibration(self, reference_camera, reference_target): """Setup the cameras and targets Args: reference_camera (str): the camera_id of the camera to be used to define world frame (cf Calibration) reference_target (str): the target_id of the target to be used to position cameras Returns: targets and cameras """ cams, targs = {}, {} self.reference_target_facing = self.facings[reference_target][reference_camera] # determine setup world origin distance, inclination = self.cameras[reference_camera] ref_cam = self.setup_camera( inclination=inclination, facing=self.reference_target_facing, distance=distance, ) self.world_origin = ref_cam._pos_z # build targets for id_target, target in self.targets.items(): distance, inclination = target facing = self.facings[id_target][reference_camera] targs[id_target] = self.setup_target( inclination=inclination, facing=facing, distance=distance ) # build cameras for id_camera, camera in self.cameras.items(): distance, inclination = camera facing = self.facings[reference_target][id_camera] image_size = self.image_sizes[id_camera] resolution = self.image_resolutions[id_camera] cams[id_camera] = self.setup_camera( image_size=image_size, resolution=resolution, inclination=inclination, facing=facing, distance=distance, ) return cams, targs, reference_camera, self.clockwise
def alpha(self, rotation): angle = numpy.radians(rotation) if self.clockwise: angle *= -1 return angle
[docs] def setup_target(self, inclination=0, facing=0, distance=0): """setup a target frame Args: inclination: the angle (degree, positive) between world z+ (axis of rotation) and target normal. By default, inclination = 0, that corresponds to a horizontal target facing: the rotation consign (degree, positive) for which the chessboard is facing the reference camera (ie with topleft corner closest to topleft side of the image). distance: the distance to the axis of rotation (word units), along target normal direction. Returns: A CalibrationFrame positioned in world frame """ # starting frame : frame axis = world axis, ie horizontal target facing reference camera when facing = 0 px, py, pz = 0, 0, 0 rx, ry, rz = 0, 0, 0 rx += numpy.radians(inclination) rz -= self.alpha(facing) fr = CalibrationFrame.frame(px, py, pz, rx, ry, rz) px, py, pz = fr.global_point((0, 0, distance)) # pz -= self.world_origin # c = CalibrationFrame() c._pos_x, c._pos_y, c._pos_z = px, py, pz c._rot_x, c._rot_y, c._rot_z = ( normalise_angle(rx), normalise_angle(ry), normalise_angle(rz), ) return c
[docs] def setup_camera( self, image_size=None, resolution=None, inclination=0, facing=0, distance=0 ): """Set up a calibration camera from simple inputs Args: image_size: resolution: image resolution (pixel per world unit) for an object located near the axis of rotation inclination: the angle (degree, positive) between world z+ (axis of rotation) and camera z- (opposite optical axis) . By default, inclination = 0, that corresponds to a camera pointing downwards facing: the rotation consign (degree, positive) for which the reference target is facing the camera (ie with top left corner closest to top left side of the image). distance: the distance to the axis of rotation (word units), along camera optical axis. Returns: A CalibrationCamera positioned in calibration world frame """ w, h, f = None, None, None if image_size is not None and resolution is not None: w, h = image_size f = resolution * distance # starting frame: pi rotation around x of world frame, ie camera facing horizontal target aligned with world # axis px, py, pz = 0, 0, 0 rx, ry, rz = numpy.pi, 0, 0 rx += numpy.radians(inclination) rz += self.alpha(facing - self.reference_target_facing) fr = CalibrationFrame.frame(px, py, pz, rx, ry, rz) px, py, pz = fr.global_point((0, 0, -distance)) # pz -= self.world_origin c = CalibrationCamera() c._width_image, c._height_image = w, h c._focal_length_x = f c._aspect_ratio = 1 c._pos_x, c._pos_y, c._pos_z = px, py, pz c._rot_x, c._rot_y, c._rot_z = ( normalise_angle(rx), normalise_angle(ry), normalise_angle(rz), ) return c
[docs] class Calibration: """A class for calibration of multi-views imaging systems (fixed cameras, rotating targets)"""
[docs] def __init__( self, angle_factor=1, targets=None, cameras=None, target_points=None, image_points=None, reference_camera="side", clockwise_rotation=True, calibration_statistics=None, frames=None, ): """Instantiate a Calibration object with calibration data Args: angle_factor: start value for angle_factor. angle_factor is a float multiplier of rotation consign to obtain actual rotation angle (default value: 1) targets: a {target_id : CalibrationFrame,...} dict of targets to be used as starting guess for target frames, positioned using transformations of the base configuration of the imaging system (see CalibrationLayout) cameras: a {camera_id: CalibrationCamera,...} dict of cameras to be used as starting guess for cameras, positioned using transformations of the base configuration of the imaging system (see CalibrationLayout) target_points: a {target_id: [(x, y, z), ...], ...} dict of coordinates of target corner points, expressed in target local frame or native world frame if target_id == 'world' image_points: a {camera_id: {target_id : {rotation : [(u,v),...], ...}, ...,} dict of dict of dict of pixel coordinates of target corner points projected on camera images at a given rotation consign. The rotation consign is the rounded angle (degrees) by which the turntable has turned before image acquisition reference_camera (str): camera_id of the camera to be used to define world native frame (see details) clockwise_rotation (bool): are targets rotating clockwise ? (default True) calibration_statistics (dict): statitistics of current calibration frames (dict): a {frame_name: CalibrationFrame, ...} dict of user-defined frames positioned in world native frame specifying alternative coordinates systems Details: Calibration allows finding position and parameters of cameras and targets and compute the projection functions of cameras from different use-defined frames The world 3D coordinates are natively expressed in the frame defined by the axis of rotation and a reference camera as follow: - The axis of rotation of the rotating system, oriented toward the sky, defines the world Z+. - The altitude of the reference camera defines Z=0. - The vertical plane around Z+ intercepting the reference camera, oriented from the camera to the axe of rotation, defines world Y+ axis The world coordinates can be redefined by further positioning user-defined frames in this native frame. """ self.angle_factor = angle_factor self._targets = {} self._cameras = {} self._image_points = {} self._targets_points = {} self._nb_targets = 0 self._nb_cameras = 0 self._nb_image_points = 0 self.set_values(targets, target_points, cameras, image_points) self.clockwise = clockwise_rotation self.reference_camera = reference_camera self.fit_angle_factor = True self.fit_aspect_ratio = True self.fit_reference_camera = True self.fit_targets = True self.fit_cameras = True self.verbose = False self.calibration_statistics = calibration_statistics if frames is not None: self.frames = frames else: self.frames = {}
def set_values( self, targets=None, target_points=None, cameras=None, image_points=None ): if targets is not None: self._targets = deepcopy(targets) if target_points is not None: self._targets_points = target_points if cameras is not None: self._cameras = deepcopy(cameras) if image_points is not None: self._image_points = image_points self._nb_cameras = len(self._cameras) self._nb_targets = len(self._targets) self._nb_image_points = 0 for cam_pts in self._image_points.values(): for im_pts_t in cam_pts.values(): for im_pts in im_pts_t.values(): self._nb_image_points += len(im_pts) def __str__(self): out = "Calibration:\n\n" out += "Angle factor : " + str(self.angle_factor) + "\n" out += "Clockwise rotation : " + str(self.clockwise) + "\n\n" for id_camera, camera in self._cameras.items(): out += "Camera {}".format(id_camera) if id_camera == self.reference_camera: out += " (reference)" out += ": \n" out += str(camera) for id_target, target in self._targets.items(): out += "Target {}: \n".format(id_target) out += str(target) return out
[docs] @staticmethod def turntable_frame(rotation, angle_factor=1, clockwise=True): """Frame attached to turntable. This correspond to a rotation of the world native frame. Args: rotation: the rotation consign of the turning table angle_factor: a float multiplier of rotation consign to obtain actual rotation angle clockwise: is turntable rotating clockwise ? Returns: a frame object """ alpha = numpy.radians(rotation * angle_factor) if clockwise: alpha *= -1 return Frame( [ (numpy.cos(alpha), numpy.sin(alpha), 0), (-numpy.sin(alpha), numpy.cos(alpha), 0), (0, 0, 1), ] )
def get_turntable_frame(self, rotation): return self.turntable_frame(rotation, self.angle_factor, self.clockwise) def get_frame(self, frame="native"): if frame == "native": return Frame() elif frame in self.frames: return self.frames[frame].get_frame() else: warnings.warn( "frame: " + frame + " not defined, falling back to native world frame" ) return Frame() def get_projection(self, id_camera, rotation, world_frame="native"): camera = self._cameras[id_camera] fr_cam = camera.get_frame() fr_table = self.get_turntable_frame(rotation) fr_world = self.get_frame(world_frame) pixel_coords = camera.get_pixel_coordinates() def projection(pts): # native points npts = fr_world.global_point(pts) # rotated pts rotated = fr_table.global_point(npts) return pixel_coords(fr_cam.local_point(rotated)) return projection def get_image_shape(self, id_camera): return self._cameras[id_camera].image_shape() def split_parameters(self, x0): # decompose x0 into angle_factor, reference camera, targets and cameras list of parameters # number of parameters per above-mentioned items nbp_target = 6 nbp_camera = 8 if self.fit_aspect_ratio else 7 nb_pars = [0, 0, 0, 0] if self.fit_angle_factor: nb_pars[0] = 1 if self.fit_reference_camera: nb_pars[1] = 6 if self.fit_aspect_ratio else 5 if self.fit_targets: nb_pars[2] = nbp_target * self._nb_targets if self.fit_cameras: nb_pars[3] = nbp_camera * (self._nb_cameras - 1) # minus reference camera xx0 = iter(x0) turntable, ref_cam, targets, cameras = [list(islice(xx0, n)) for n in nb_pars] # further group per target and per camera targetss = [] for i in range(0, len(targets), nbp_target): targetss.append(targets[i : i + nbp_target]) camerass = [] for i in range(0, len(cameras), nbp_camera): camerass.append(cameras[i : i + nbp_camera]) return turntable, ref_cam, targetss, camerass def fit_errors(self, x0): turntable, ref_cam, targets, cameras = self.split_parameters(x0) # merge ref_cam to cameras if len(ref_cam) > 0: _pos_x = 0 _pos_z = 0 if self.fit_aspect_ratio: _focal_length_x, _aspect_ratio, _pos_y, _rot_x, _rot_y, _rot_z = ref_cam ref_cam = [ _focal_length_x, _aspect_ratio, _pos_x, _pos_y, _pos_z, _rot_x, _rot_y, _rot_z, ] else: _focal_length_x, _pos_y, _rot_x, _rot_y, _rot_z = ref_cam ref_cam = [ _focal_length_x, _pos_x, _pos_y, _pos_z, _rot_x, _rot_y, _rot_z, ] cameras.insert(0, ref_cam) # build frames angle_factor = self.angle_factor if len(turntable) > 0: angle_factor = turntable[0] target_frames = [] if "world" in self._targets_points: target_frames += [self.get_frame("native")] if len(targets) > 0: for target in targets: pos_x, pos_y, pos_z, rot_x, rot_y, rot_z = target target_frames.append( CalibrationFrame.frame(pos_x, pos_y, pos_z, rot_x, rot_y, rot_z) ) else: for target in self._targets.values(): target_frames.append(target.get_frame()) camera_frames = [] camera_focals = [] for camera in cameras: if self.fit_aspect_ratio: ( _focal_length_x, _aspect_ratio, _pos_x, _pos_y, _pos_z, _rot_x, _rot_y, _rot_z, ) = camera else: _aspect_ratio = 1 _focal_length_x, _pos_x, _pos_y, _pos_z, _rot_x, _rot_y, _rot_z = camera camera_frames.append( CalibrationCamera.frame(_pos_x, _pos_y, _pos_z, _rot_x, _rot_y, _rot_z) ) camera_focals.append([_focal_length_x, _aspect_ratio]) err = {} # cameras and image_points in the right order cams = [] im_pts_cam = [] labels = [] if len(ref_cam) > 0: cams += [self._cameras[self.reference_camera]] im_pts_cam += [self._image_points[self.reference_camera]] labels += [self.reference_camera] cams += [self._cameras[k] for k in self._cameras if k != self.reference_camera] im_pts_cam += [ self._image_points[k] for k in self._cameras if k != self.reference_camera ] labels += [k for k in self._cameras if k != self.reference_camera] # target_points in the right order target_points = self._targets_points.get("world", []) if len(target_points) > 0: target_points = [target_points] # target_points is a list of list of points target_points += [self._targets_points[k] for k in self._targets] for fr_cam, focals, camera, im_pts_c, lab in zip( camera_frames, camera_focals, cams, im_pts_cam, labels ): _targets = ["world"] if "world" in self._targets_points else [] _targets += [k for k in self._targets] im_pts_t = [im_pts_c[k] for k in _targets] for fr_target, t_pts, im_pts, t_name in zip( target_frames, target_points, im_pts_t, _targets ): for rotation, ref_pts in im_pts.items(): fr_table = Calibration.turntable_frame( rotation, angle_factor, self.clockwise ) target_pts = fr_table.global_point(fr_target.global_point(t_pts)) _focal_length_x, _aspect_ratio = focals pts = CalibrationCamera.pixel_coordinates( fr_cam.local_point(target_pts), camera._width_image, camera._height_image, _focal_length_x, _aspect_ratio, ) err["_".join([lab, t_name, str(rotation)])] = numpy.linalg.norm( numpy.array(pts) - ref_pts, axis=1 ).sum() if self.verbose: print(sum(err.values())) return err def fit_function(self, x0): err = self.fit_errors(x0) return sum(err.values()) def get_parameters(self): parameters = [] if self.fit_angle_factor: parameters.append(self.angle_factor) if self.fit_reference_camera: c = self._cameras[self.reference_camera] parameters += [c._focal_length_x] if self.fit_aspect_ratio: parameters += [c._aspect_ratio] parameters += [c._pos_y, c._rot_x, c._rot_y, c._rot_z] if self.fit_targets: for id_target, t in self._targets.items(): parameters += [ t._pos_x, t._pos_y, t._pos_z, t._rot_x, t._rot_y, t._rot_z, ] if self.fit_cameras: for id_camera, c in self._cameras.items(): if id_camera != self.reference_camera: parameters += [c._focal_length_x] if self.fit_aspect_ratio: parameters += [c._aspect_ratio] parameters += [ c._pos_x, c._pos_y, c._pos_z, c._rot_x, c._rot_y, c._rot_z, ] return parameters def find_parameters(self): start = self.get_parameters() parameters = minimize(self.fit_function, start, method="BFGS").x err = self.fit_function(parameters) if self.verbose: print("Result : ", parameters) print("Err : ", err / self._nb_image_points) return parameters def calibration_error(self, all_pars=False): if self._nb_image_points > 0: before = {} if all_pars: before["fit_angle_factor"] = self.fit_angle_factor before["fit_aspect_ratio"] = self.fit_aspect_ratio before["fit_reference_camera"] = self.fit_reference_camera before["fit_targets"] = self.fit_targets before["fit_cameras"] = self.fit_cameras self.fit_angle_factor = True self.fit_aspect_ratio = True self.fit_reference_camera = True self.fit_targets = True self.fit_cameras = True p = self.get_parameters() err = self.fit_function(p) if all_pars: self.fit_angle_factor = before["fit_angle_factor"] self.fit_aspect_ratio = before["fit_aspect_ratio"] self.fit_reference_camera = before["fit_reference_camera"] self.fit_targets = before["fit_targets"] self.fit_cameras = before["fit_cameras"] return err, err / self._nb_image_points else: raise ValueError( "Calibration corner points (target_points and image points) are required to compute " "calibration error" ) def _calibration_statistics(self): stats = {} if self._nb_image_points > 0: _, error = self.calibration_error(all_pars=True) stats["mean_error"] = error stats["total_points"] = self._nb_image_points for id_camera, camera in self._cameras.items(): d_origin = numpy.sqrt( camera._pos_x**2 + camera._pos_y**2 + camera._pos_z**2 ) focal = camera._focal_length_x * (1 + camera._aspect_ratio) / 2 pixel_size = d_origin / focal calibration_images = {} if self._nb_image_points > 0: for target in self._image_points[id_camera]: calibration_images[target] = len( self._image_points[id_camera][target] ) stats[id_camera] = { "distance to origin": d_origin, "pixel_size": pixel_size, "target_images": calibration_images, } return stats def get_target_projected(self, id_camera, id_target, rotation): proj = self.get_projection(id_camera, rotation) target_pts = self.get_target_points(id_target) return proj(target_pts)
[docs] def target_mask(self, id_camera, id_target, rotation, border=2): """Get image coordinate of a mask arround the target Args: border : the size of the border (in square_size units) """ pts = self.get_target_projected(id_camera, id_target, rotation) stats = self._calibration_statistics() targ = self._targets[id_target] bs = targ.square_size * border / stats[id_camera]["pixel_size"] pts = self._targets[id_target] u, v = zip(*pts) h, w = self._cameras[id_camera].image_shape() umin, vmin, umax, vmax = ( numpy.clip(min(u) - bs, 1, w), numpy.clip(min(v) - bs, 1, h), numpy.clip(max(u) + bs, 1, w), numpy.clip(max(v) + bs, 1, h), ) return [(umin, vmin), (umax, vmin), (umax, vmax), (umin, vmax)]
def get_target_points(self, id_target): if id_target == "world": fr_target = self.get_frame("native") else: fr_target = self._targets[id_target].get_frame() return fr_target.global_point(self._targets_points[id_target])
[docs] def calibrate( self, fit_angle_factor=True, fit_aspect_ratio=True, fit_reference_camera=True, fit_targets=True, fit_cameras=True, verbose=True, ): """Optimise the cameras and targets parameters to minimise the distance between observed image points and projections on images of target points Args: fit_angle_factor: should angle_factor be fitted ? (default True) fit_reference_camera: should reference camera parameters be fitted ? (default True) fit_targets: should target frame parameters be fitted ? (default True) fit_cameras: should other than reference camera parameters be fitted ? (default True) verbose: should total error be printed during optimisation (default True) Returns: the mean calibration reprojection error (pixels) """ self.fit_angle_factor = fit_angle_factor self.fit_aspect_ratio = fit_aspect_ratio self.fit_reference_camera = fit_reference_camera self.fit_targets = fit_targets self.fit_cameras = fit_cameras self.verbose = verbose parameters = self.find_parameters() turntable, ref_cam, target_pars, camera_pars = self.split_parameters(parameters) pos_labels = ["_pos_x", "_pos_y", "_pos_z"] rot_labels = ["_rot_x", "_rot_y", "_rot_z"] f_labels = ["_focal_length_x"] if self.fit_aspect_ratio: f_labels += ["_aspect_ratio"] if len(turntable) > 0: self.angle_factor = turntable[0] if len(ref_cam) > 0: camera = self._cameras[self.reference_camera] labels = f_labels + ["_pos_y"] + rot_labels d = dict(list(zip(labels, ref_cam))) for k in rot_labels: d[k] = normalise_angle(d[k]) camera.set_vars(d) if len(target_pars) > 0: labels = pos_labels + rot_labels for target, target_param in zip(self._targets.values(), target_pars): d = dict(list(zip(labels, target_param))) for k in rot_labels: d[k] = normalise_angle(d[k]) target.set_vars(d) if len(camera_pars) > 0: cams = [ self._cameras[k] for k in self._cameras if k != self.reference_camera ] for camera, camera_param in zip(cams, camera_pars): labels = f_labels + pos_labels + rot_labels d = dict(list(zip(labels, camera_param))) for k in rot_labels: d[k] = normalise_angle(d[k]) camera.set_vars(d) err = self.fit_function(parameters) self.calibration_statistics = self._calibration_statistics() return err / self._nb_image_points
[docs] def find_points(self, image_points, start=None, niter=100): """Find native 3D world coordinates of points from paired image coordinates on multiple cameras Args: image_points: a {camera_id: [(u1,v1),...], ...} dict of list of pixel coordinates of remarkable points taken on several images start (optional): an array-like list of 3D points guesses niter: (int) the number of iteration of the basin-hopping optimisation algorithm Returns: An array of 3D coordinates of points """ image_points = {k: numpy.array(v) for k, v in image_points.items()} if start is None: start = numpy.array([(0, 0, 0)] * len(list(image_points.values())[0])) def fit_function(x0): err = 0 pts = numpy.array(x0).reshape((int(len(x0) / 3), 3)) for id_camera in image_points: im_pts = image_points[id_camera] proj = self.get_projection(id_camera, 0) pix = proj(pts) err += numpy.linalg.norm(pix - im_pts, axis=1).sum() print(err) return err parameters = minimize(fit_function, start, method="BFGS").x print("Err : ", fit_function(parameters)) return parameters.reshape((int(len(parameters) / 3), 3))
[docs] def find_camera( self, image_points, target_points, image_size=None, fixed_parameters=None, guess=None, niter=10, ): """Find camera parameters from clicked image points of known 3D target points Args: image_points: an array-like list of image coordinates of remarkable points target_points: an array-like list of 3D coordinates of remarkable points image_size: (width, height) tuple describing image dimension (pixels). Alternatively the name of an existing camera with the same shape. If None, the shape of the reference camera is used fixed_parameters: a {parameter_name: value} dict of fixed (unfitted) camera parameters. Valid parameters names are '_pos_x', '_pos_y', '_pos_z', '_rot_x', '_rot_y', '_rot_z', '_focal_length_x', '_aspect_ratio' guess : a guessed Calibration camera niter: (int) the number of iteration of the basin-hopping optimisation algorithm Returns: A calibrated CalibrationCamera """ image_points = numpy.array(image_points) target_points = numpy.array(target_points) if fixed_parameters is None: fixed_parameters = {} if isinstance(image_size, tuple): w, h = image_size elif isinstance(image_size, str): if image_size in self._cameras: h, w = self.get_image_shape(image_size) else: h, w = self.get_image_shape(self.reference_camera) fixed_parameters.update({"_width_image": w, "_height_image": h}) pars = ( "_pos_x", "_pos_y", "_pos_z", "_rot_x", "_rot_y", "_rot_z", "_width_image", "_height_image", "_focal_length_x", "_aspect_ratio", ) free_pars = [p for p in pars if p not in fixed_parameters] nfree_pars = len(free_pars) if guess is None: start = numpy.zeros(nfree_pars) else: d = vars(guess) start = [d[p] for p in free_pars] def split_parameters(x0): pars = dict(list(zip(free_pars, x0[:nfree_pars]))) pars.update(fixed_parameters) return pars def fit_function(x0): pars = split_parameters(x0) camera = CalibrationCamera() camera.set_vars(pars) proj = camera.get_projection() pix = proj(target_points) err = numpy.linalg.norm(pix - image_points, axis=1).sum() print(err) return err parameters = minimize(fit_function, start, method="BFGS").x pars = split_parameters(parameters) for p in ("_rot_x", "_rot_y", "_rot_z"): pars[p] = normalise_angle(pars[p]) camera = CalibrationCamera() camera.set_vars(pars) return camera
[docs] def find_frame(self, image_points, frame_points, fixed_parameters=None, start=None): """Find Frame parameters and 3D local (frame based) coordinates of points from paired image coordinates Args: image_points: a {camera_id: [(u1,v1),...], ...} dict of list of pixel coordinates of frame points frame_points: an array-like list of 3D points coordinates, expressed in local coordinate of the searched frame. keywords 'x', 'y' and 'z' can be used to specify an unknown coordinate fixed_parameters: a {parameter_name: value} dict of fixed (unfitted) frame parameters. Valid parameters names are '_pos_x', '_pos_y', '_pos_z', '_rot_x', '_rot_y', '_rot_z' start : a frame to be used as guess Returns: A CalibrationFrame and the list of 3D coordinates matching frame_points """ if fixed_parameters is None: fixed_parameters = {} image_points = { k: numpy.array(v) if v is not None else v for k, v in image_points.items() } # free frame parameters pars = ("_pos_x", "_pos_y", "_pos_z", "_rot_x", "_rot_y", "_rot_z") free_pars = [p for p in pars if p not in fixed_parameters] n_free_pars = len(free_pars) # unknown coordinates n_free_x, n_free_y, n_free_z = 0, 0, 0 for pt in frame_points: if "x" in pt: n_free_x += 1 if "y" in pt: n_free_y += 1 if "z" in pt: n_free_z += 1 if start is None: start = numpy.zeros(n_free_pars + n_free_x + n_free_y + n_free_z) else: start = [start[p] for p in free_pars] + numpy.zeros( n_free_x + n_free_y + n_free_z ).tolist() def split_parameters(x0): nb_pars = [n_free_pars, n_free_x, n_free_y, n_free_z] xx0 = iter(x0) frame_pars, free_x, free_y, free_z = [list(islice(xx0, n)) for n in nb_pars] frame_pars = dict(zip(free_pars, frame_pars)) frame_pars.update(fixed_parameters) fr_points = [] for x, y, z in frame_points: if x == "x": x = free_x.pop() if y == "y": y = free_y.pop() if z == "z": z = free_z.pop() fr_points.append((x, y, z)) return frame_pars, fr_points def fit_function(x0): frame_pars, fr_points = split_parameters(x0) cframe = CalibrationFrame() cframe.set_vars(frame_pars) fr = cframe.get_frame() pts = fr.global_point(fr_points) err = 0 for id_camera in image_points: im_pts = [p for p in image_points[id_camera] if p is not None] world_pts = [ p for p, im_p in zip(pts, image_points[id_camera]) if im_p is not None ] proj = self.get_projection(id_camera, 0) pix = proj(world_pts) err += numpy.linalg.norm(pix - im_pts, axis=1).sum() print(err) return err parameters = minimize(fit_function, start, method="BFGS").x pars, fpts = split_parameters(parameters) for p in ("_rot_x", "_rot_y", "_rot_z"): pars[p] = normalise_angle(pars[p]) cframe = CalibrationFrame() cframe.set_vars(pars) return cframe, fpts
[docs] @staticmethod def world_frame(camera): """World frame defined by an alternative camera positioned in the current reference camera world frame""" ref_azim = -numpy.pi / 2 # by definition of the reference camera azim = numpy.arctan2(camera._pos_y, camera._pos_x) return CalibrationFrame.from_tuple((0, 0, camera._pos_z, 0, 0, azim - ref_azim))
def frame_lines(self, view, angle, frame="native", leaf=100, w=10, at=(0, 0, 0)): base_axis = numpy.array(((1, 0, 0), (0, 1, 0), (0, 0, 1))) p = self.get_projection(view, angle, frame) origin = tuple(numpy.array(p(at)).astype(int)) lines = [] for axe in base_axis: end = tuple(numpy.array(p(numpy.array(at) + leaf * axe)).astype(int)) col = tuple([int(x) for x in axe * 255]) lines.append((origin, end, col, w)) return lines def dump(self, filename): save_class = dict() save_class["angle_factor"] = self.angle_factor save_class["clockwise"] = self.clockwise save_class["reference_camera"] = self.reference_camera save_class["cameras_parameters"] = { id_camera: camera.to_json() for id_camera, camera in self._cameras.items() } save_class["targets_parameters"] = { id_target: t.to_json() for id_target, t in self._targets.items() } if self.calibration_statistics is not None: save_class["calibration_statistics"] = self.calibration_statistics if len(self.frames) > 0: save_class["frames"] = { id_frame: frame.to_json() for id_frame, frame in self.frames.items() } with open(filename, "w") as output_file: json.dump( save_class, output_file, sort_keys=True, indent=4, separators=(",", ": "), ) @staticmethod def from_dict(save_class): c = Calibration() c._cameras = { id_camera: CalibrationCamera.from_json(pars) for id_camera, pars in save_class["cameras_parameters"].items() } c._targets = { id_target: CalibrationFrame.from_json(pars) for id_target, pars in save_class["targets_parameters"].items() } c._nb_cameras = len(c._cameras) c._nb_targets = len(c._targets) c.angle_factor = save_class["angle_factor"] c.clockwise = save_class["clockwise"] c.reference_camera = save_class["reference_camera"] if "calibration_statistics" in save_class: c.calibration_statistics = save_class["calibration_statistics"] if "frames" in save_class: c.frames = { id_frame: CalibrationFrame.from_json(pars) for id_frame, pars in save_class["frames"].items() } return c @staticmethod def load(filename): with open(filename, "r") as input_file: save_class = json.load(input_file) return Calibration.from_dict(save_class)
[docs] class OldCalibrationCamera: """A class for using camera calibrated with older version of phenomenal (< 1.7.1)"""
[docs] def __init__(self): # Camera Parameters self._cam_width_image = None self._cam_height_image = None self._cam_focal_length_x = None self._cam_focal_length_y = None self._cam_pos_x = None self._cam_pos_y = None self._cam_pos_z = None self._cam_rot_x = None self._cam_rot_y = None self._cam_rot_z = None self._angle_factor = None self._cam_origin_axis = None
def __str__(self): out = "" out += "Camera Parameters : \n" out += "\tFocal length X : " + str(self._cam_focal_length_x) + "\n" out += "\tFocal length Y : " + str(self._cam_focal_length_y) + "\n" out += "\tOptical Center X : " + str(self._cam_width_image / 2.0) + "\n" out += "\tOptical Center Y : " + str(self._cam_height_image / 2.0) out += "\n\n" out += "\tPosition X : " + str(self._cam_pos_x) + "\n" out += "\tPosition Y : " + str(self._cam_pos_y) + "\n" out += "\tPosition Z : " + str(self._cam_pos_z) + "\n\n" out += "\tRotation X : " + str(self._cam_rot_x) + "\n" out += "\tRotation Y : " + str(self._cam_rot_y) + "\n" out += "\tRotation Z : " + str(self._cam_rot_z) + "\n" out += "\t Angle Factor : " + str(self._angle_factor) + "\n" out += "\tOrigin rotation position : \n" out += str(self._cam_origin_axis) + "\n\n" return out
[docs] @staticmethod def pixel_coordinates( point_3d, width_image, height_image, focal_length_x, focal_length_y ): """Compute image coordinates of a 3d point Args: - point (float, float, float): a point in space expressed in camera frame coordinates return: - (int, int): coordinate of point in image in pix """ pt = numpy.array(point_3d) x, y, z = pt.T u = x / z * focal_length_x + width_image / 2.0 v = y / z * focal_length_y + height_image / 2.0 if len(pt.shape) > 1: return numpy.column_stack((u, v)) else: return u, v
[docs] @staticmethod def pixel_coordinates_2(point_3d, cx, cy, fx, fy): """Compute image coordinates of a 3d point Args: - point (float, float, float): a point in space expressed in camera frame coordinates return: - (int, int): coordinate of point in image in pix """ # if point[2] < 1: # raise UserWarning("point too close to the camera") u = point_3d[0] / point_3d[2] * fx + cx v = point_3d[1] / point_3d[2] * fy + cy return u, v
@staticmethod def target_frame(pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, alpha): origin = [ pos_x * math.cos(alpha) - pos_y * math.sin(alpha), pos_x * math.sin(alpha) + pos_y * math.cos(alpha), pos_z, ] mat_rot_x = rotation_matrix(rot_x, x_axis) mat_rot_y = rotation_matrix(rot_y, y_axis) mat_rot_z = rotation_matrix(alpha + rot_z, z_axis) rot = concatenate_matrices(mat_rot_z, mat_rot_x, mat_rot_y) return Frame(rot[:3, :3].T, origin) @staticmethod def camera_frame(pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, origin_axis): origin = (pos_x, pos_y, pos_z) mat_rot_x = rotation_matrix(rot_x, x_axis) mat_rot_y = rotation_matrix(rot_y, y_axis) mat_rot_z = rotation_matrix(rot_z, z_axis) rot = concatenate_matrices(origin_axis, mat_rot_x, mat_rot_y, mat_rot_z) return Frame(rot[:3, :3].T, origin) def get_camera_frame(self): return self.camera_frame( self._cam_pos_x, self._cam_pos_y, self._cam_pos_z, self._cam_rot_x, self._cam_rot_y, self._cam_rot_z, self._cam_origin_axis, ) def get_pixel_coordinates(self): def pixel_coords(pts): return self.pixel_coordinates( pts, self._cam_width_image, self._cam_height_image, self._cam_focal_length_x, self._cam_focal_length_y, ) return pixel_coords def get_projection(self, alpha): fr_cam = self.get_camera_frame() angle = math.radians(alpha * self._angle_factor) def projection(pts): pts = numpy.array(pts) x = -pts[:, 0] * math.cos(angle) - pts[:, 1] * math.sin(angle) y = -pts[:, 0] * math.sin(angle) + pts[:, 1] * math.cos(angle) z = pts[:, 2] origin = numpy.column_stack((x, y, z)) return self.pixel_coordinates( fr_cam.local_point(origin), self._cam_width_image, self._cam_height_image, self._cam_focal_length_x, self._cam_focal_length_y, ) return projection def get_projection2(self, alpha): fr_cam = self.camera_frame( self._cam_pos_x, self._cam_pos_y, self._cam_pos_z, self._cam_rot_x, self._cam_rot_y, self._cam_rot_z, self._cam_origin_axis, ) angle = math.radians(alpha * self._angle_factor) def projection(pt): # -pt[0] = x <=> For inverse X axis orientation origin = [ pt[0] * math.cos(angle) - pt[1] * math.sin(angle), pt[0] * math.sin(angle) + pt[1] * math.cos(angle), pt[2], ] return self.pixel_coordinates( fr_cam.local_point(origin), self._cam_width_image, self._cam_height_image, self._cam_focal_length_x, self._cam_focal_length_y, ) return projection @staticmethod def load(filename): with open(filename, "r") as input_file: save_class = json.load(input_file) c = OldCalibrationCamera() c._cam_width_image = save_class["cam_width_image"] c._cam_height_image = save_class["cam_height_image"] c._cam_focal_length_x = save_class["cam_focal_length_x"] c._cam_focal_length_y = save_class["cam_focal_length_y"] c._cam_pos_x = save_class["cam_pos_x"] c._cam_pos_y = save_class["cam_pos_y"] c._cam_pos_z = save_class["cam_pos_z"] c._cam_rot_x = save_class["cam_rot_x"] c._cam_rot_y = save_class["cam_rot_y"] c._cam_rot_z = save_class["cam_rot_z"] c._angle_factor = save_class["angle_factor"] c._cam_origin_axis = ( numpy.array(save_class["cam_origin_axis"]) .reshape((4, 4)) .astype(numpy.float32) ) if "target_1_pos_x" in save_class: c._target_1_pos_x = save_class["target_1_pos_x"] c._target_1_pos_y = save_class["target_1_pos_y"] c._target_1_pos_z = save_class["target_1_pos_z"] c._target_1_rot_x = save_class["target_1_rot_x"] c._target_1_rot_y = save_class["target_1_rot_y"] c._target_1_rot_z = save_class["target_1_rot_z"] if "target_2_pos_x" in save_class: c._target_2_pos_x = save_class["target_2_pos_x"] c._target_2_pos_y = save_class["target_2_pos_y"] c._target_2_pos_z = save_class["target_2_pos_z"] c._target_2_rot_x = save_class["target_2_rot_x"] c._target_2_rot_y = save_class["target_2_rot_y"] c._target_2_rot_z = save_class["target_2_rot_z"] return c
[docs] class OldCalibration: """A class for loading, inspecting and convert old Calibration to new Calibration"""
[docs] def __init__(self, cameras, targets): """Instantiate an OldCalibration instance Args: cameras: a {id_camera: OldCalibrationCamera, ...} dict of calibrated cameras objects (see OldCameraCalibration class) chessboards: a {id_target: Chessboard, ...} dict of Chessboard objects (see Chessboard class in chessboard.py) """ self.cameras = cameras self.targets = targets
[docs] def calibration_error(self): """error (pixels) between detected target image points and reprojection of 3D target points""" image_points = { camera: {k: v.get_corners_2d(camera) for k, v in self.targets.items()} for camera in self.cameras } target_points = { k: v.get_corners_local_3d(old_style=True) for k, v in self.targets.items() } err = 0 nb_pts = 0 target_parameters = vars(self.cameras["side"]) for camera in image_points: for target in image_points[camera]: for angle in image_points[camera][target]: cam = self.cameras[camera] pars = [ target_parameters["_" + target + "_" + x] for x in ("pos_x", "pos_y", "pos_z", "rot_x", "rot_y", "rot_z") ] pars += [numpy.radians(cam._angle_factor * angle)] fr_target = cam.target_frame(*pars) fr_cam = cam.get_camera_frame() pix_coord = cam.get_pixel_coordinates() pts_ref = image_points[camera][target][angle] pts = pix_coord( fr_cam.local_point( fr_target.global_point(target_points[target]) ) ) nb_pts += len(pts) err += numpy.linalg.norm(pts - pts_ref, axis=1).sum() return err, float(err) / nb_pts
[docs] def guess_new_calibration(self): """Instantiate a Calibration object using fitted parameters Returns: An (unfitted) Calibration object """ cameras = {} targets = {} # angle_factor = self.cameras["side"]._angle_factor tpars = vars(self.cameras["side"]) for tn, target in self.targets.items(): w, h = target.shape size = target.square_size chess_origin = ((w / 2.0) * size, (h / 2.0) * size) t = CalibrationFrame() t._pos_x = -tpars["_" + tn + "_pos_x"] - chess_origin[0] t._pos_y = tpars["_" + tn + "_pos_y"] - chess_origin[1] t._pos_z = tpars["_" + tn + "_pos_z"] # change of definition for rot t._rot_x = normalise_angle( tpars["_" + tn + "_rot_x"] - tpars["_" + tn + "_rot_y"] ) t._rot_y = 0 t._rot_z = -normalise_angle(tpars["_" + tn + "_rot_z"]) targets[tn] = t for cn, camera in self.cameras.items(): c = CalibrationCamera() c._width_image = camera._cam_width_image c._height_image = camera._cam_height_image c._focal_length_x = camera._cam_focal_length_x c._focal_length_y = camera._cam_focal_length_y c._pos_x = -camera._cam_pos_x c._pos_y = camera._cam_pos_y c._pos_z = camera._cam_pos_z if cn == "side": # origin matrix for side cameras corresponds to -pi/2 rot around x axis rx = camera._cam_rot_x - numpy.pi / 2.0 ry = camera._cam_rot_y rz = camera._cam_rot_z else: rx = camera._cam_rot_x + numpy.pi ry = camera._cam_rot_y rz = camera._cam_rot_z + numpy.pi / 2.0 c._rot_x, c._rot_y, c._rot_z = ( normalise_angle(rx), normalise_angle(ry), normalise_angle(rz), ) cameras[cn] = c return Calibration( angle_factor=angle_factor, cameras=cameras, targets=targets, clockwise_rotation=True, reference_camera="side", )
# deprecated functions used in old calibration scripts (see equivalents in Calibration methods)
[docs] def find_position_3d_points(pt2d, calibration): """Find the coordinates of one point clicked on several image Args: pt2d: a {id_camera:{angle:(x, y),...},...} dict of pixel coordinates calibration: a calibrated Calibration object Returns: """ image_points = {id_cam: [pt2d[0]] for id_cam in pt2d} return calibration.find_points(image_points)
[docs] def find_position_3d_points_soil(im_pts, calibration): image_points = {id_cam: [im_pts[0]] for id_cam in im_pts} return calibration.find_frame( image_points, fixed_parameters={"_pos_x": 0, "_pos_y": 0}, fixed_coords={"z": 0} )