Module dynamo.calibration

Calibrate multiple Intel RealSense D4XX cameras to a single global coordinate system using a defined checkerboard

Distributed as a module of DynaMo: https://github.com/anderson-cu-bioastronautics/dynamo_realsense-capture

Source code
__doc__ = \
"""
Calibrate multiple Intel RealSense D4XX cameras to a single global coordinate system using a defined checkerboard

Distributed as a module of DynaMo: https://github.com/anderson-cu-bioastronautics/dynamo_realsense-capture
"""

##########################################################################################################################################
##                             License: Apache 2.0. See LICENSE and LICENSE.librealsense files in root directory.                               ##
##########################################################################################################################################
## This code was inspired from the librealsense box_dimensioner_multicam example, and may contain certain lines of code from this file: ##
## (https://github.com/IntelRealSense/librealsense/blob/master/wrappers/python/examples/box_dimensioner_multicam/calibration_kabsch.py).##                                          
##########################################################################################################################################



import pyrealsense2 as rs 
import cv2
import numpy as np
import time
import pickle

from .realsense_device_manager import DeviceManager
from .calculate_rmsd import *



def invTrans(matrix):
    """
    Returns inverse of a transformation matrix

    Parameters
    ----------
    matrix : (4,4) array
        Input transformation matrix

    Returns
    -------
    invMatrix : (4,4) array
        Inverse of input transformation matrix
    """
    invRot = np.eye(4)
    invRot[0:3,0:3] = matrix[0:3,0:3].T
    invTrans = np.eye(4)
    invTrans[0:3,3] = -matrix[0:3,3]
    invMatrix = np.matmul(invRot,invTrans)
    return invMatrix

def load(fileName):
    """ 
    Calibration parameters for previously connected cameras are loaded from a pickle file format.
    Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration 

    Parameters
    ----------
    fileName : str
        Filename of stored calibration parameters.

    Returns
    -------
    devicesTransformation : dict
        Keys of camera's serial number holding dictionary of calibration parameters per camera

    Example
    -------
    load('savedCalibration.cal')
    """
    file = open(fileName,'rb')
    devicesTransformation = pickle.load(file)


def new(fileName,deviceManager, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    New calibration parameters for each connected camera and are created and saved in a pickle file format.
    
    Cameras must be all be viewing the calibration checkerboard. 

    Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration 

    Parameters
    ----------
    fileName : str
        Filename to store calibration parameters.
    
    deviceManager : DeviceManager object
        realsense_device_manager object which manages connections to all cameras
    
    chessboardHeight : int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth : int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize : float
        Dimension of side of chessboard (m)

    Returns
    -------
    devicesTransformation : dict
        dictionary with keys of camera's serial number holding dictionary of calibration parameters per camera

    Example
    -----
        new('savedCalibration.cal')
    """

    deviceManager.enable_all_devices()
    file = open(fileName,'wb')             
    time.sleep(1) #let autoexposure on cameras stabilize over one second 
    cameraSet = deviceManager._enabled_devices
    chessboardLocations = detectChessboard(deviceManager, cameraSet, chessboardHeight, chessboardWidth, chessboardSquareSize) #return locations of chessboards from reference frame of each camera
    devicesTransformations = poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize) #return dictionary of 
    pickle.dump(devicesTransformations, file)
    file.close()
    return devicesTransformations

def newIterative(fileName,deviceManager, cameraList, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    New calibration parameters for each connected camera and are created and saved in a pickle file format.

    Function will iterate through camera list and will search for the checkerboard between each consecutive set of two cameras in cameraList.
    The user must move the checkerboard between the sets of cameras as the function works through the list. 

    Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration 

    Parameters
    ----------
    fileName : str
        Filename to store calibration parameters.
    
    deviceManager : DeviceManager object
        realsense_device_manager object which manages connections to all cameras

    cameraList : list
        list of serial numbers to calibrate cameras in order
    
    chessboardHeight : int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth : int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize : float
        Dimension of side of chessboard (m)

    Returns
    -------
    deviceTransformations : dict
        dictionary with keys of camera's serial number holding dictionary of calibration parameters per camera

    Example
    -----
        new('savedCalibration.cal')
    """

    deviceManager.enable_all_devices()
    file = open(fileName,'wb')        

    deviceTransformations = {}
    cameraSets = []
    for cam in range(0,len(cameraList)-1):
        cameraSets.extend([[cameraList[cam], cameraList[cam+1]]])

    time.sleep(1) #let autoexposure on cameras stabilize over one second 

    for cset in cameraSets:
        fstring = "Now calibrating cameras {0} and {1}. Press ENTER to start".format(cset[0],cset[1])
        input(fstring)
        setTransformations = {}
        chessboardLocations = detectChessboard(deviceManager, cset, chessboardHeight, chessboardWidth, chessboardSquareSize) #return locations of chessboards from reference frame of each camera
        setTransformations = poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize) #return dictionary of cameras' transformtion matrices
        deviceTransformations[cset[0]] = setTransformations[cset[0]]
        deviceTransformations[cset[1]] = setTransformations[cset[1]]
        deviceTransformations[cset[0]][0] = np.matmul(invTrans(setTransformations[cset[1]][0]),setTransformations[cset[0]][0])
        #deviceTransformations[cset[1]][0] = np.eye(4)
        fstring = "Cameras {0} and {1} calibrated. Press ENTER to continue".format(cset[0],cset[1])
        input(fstring)

    for c,cam in enumerate(cameraList):    
        if c < len(cameraList):
            matrices = []
            for cm in range(c+1,len(cameraList)):
                deviceTransformations[cam][0] = np.matmul(deviceTransformations[cameraList[cm]][0],deviceTransformations[cameraList[c]][0])
                print(str(c)+':'+str(cm))
            #deviceTransformations[cameraList[c]][0] = np.matmul(deviceTransformations[cameraList[1]][0],deviceTransformations[cameraList[0]][0])
    print(deviceTransformations)
    pickle.dump(deviceTransformations, file)
    file.close()
    return deviceTransformations


def detectChessboard(deviceManager, cameraSet, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    Chessboard locations are computed for each connected RealSense camera

    Parameters
    ----------
    deviceManager : DeviceManager object
        realsense_device_manager object which manages connections to all cameras

    cameraSet : list
        list of camera serial numbers to detect the chessboard
    
    chessboardHeight: int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth: int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize: float
        Dimension of side of chessboard (m)

    Returns
    -------
    chessboardLocations : dict
        dictionary with keys of camera's serial number holding detected corners, local 2D and 3D coordinates of detected corners, and their valid depth points

    Example
    -----
        detectChessboard(deviceManager, chessboardHeight, chessboardWidth, chessboardSquareSize)
    """
    chessboardDeviceCount = 0
    devicesChessboardLocations = {}

    while len(devicesChessboardLocations) < len(cameraSet): #iterate through detecting chessboard until all available devices see chessboard
        cameraFrames = deviceManager.poll_frames()
        devicesIntrinsics = deviceManager.get_device_intrinsics(cameraFrames)
        
        for device, frames in cameraFrames.items(): #this will iterate through each device's serial number (which are used as keys in the frames object)
            if not device in devicesChessboardLocations and device in cameraSet: #if the camera has not already detected the chessboard

                align = rs.align(rs.stream.depth) #align the color sensor to the depth sensor using the factory extrinsics
                alignedFrames = align.process(frames)
                
                colorImage = np.asanyarray(alignedFrames.get_color_frame().get_data()) 
                bwImage = cv2.cvtColor(colorImage,cv2.COLOR_BGR2GRAY) #convert color image to B&W image to use in openCV functions

                depthFrame = alignedFrames.get_depth_frame()
                depthIntrinsics = devicesIntrinsics[device][rs.stream.depth]  #obtain the depth sensor intrinsic properties

                chessboardFound, corners = cv2.findChessboardCorners(bwImage, (chessboardWidth, chessboardHeight)) #use openCV function to detect chessboard corners

                if chessboardFound: #if the camera sees the chessboard
                    print(device," sees the chessboard!")
                    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) #subpix criteria
                    points2D = cv2.cornerSubPix(bwImage, corners, (11,11), (-1,-1), criteria) #further refine corners of chessboard using sub pixeling

                    cv2.drawChessboardCorners(bwImage, (chessboardWidth, chessboardHeight), points2D, chessboardFound) #draw chessboard corners in white over b&w image for verification
                    cv2.imshow(device,bwImage) #show the detected chessboard corners on the image
                    cv2.waitKey(50)

                    points2D = np.transpose(corners, (2,0,1)) 
                    points3D = np.zeros((3, len(points2D[0]))) #preallocate array to turn 2D chessboard corner points from camera into 3D points 
                    validPoints = [False] * len(points2D[0])

                    for index in range(len(points2D[0])): #iterate over every corner to find 3D location of chessboard corner
                        corner = points2D[:,index].flatten()
                        depth = depthFrame.as_depth_frame().get_distance(round(corner[0]), round(corner[1])) #this gets the depth at the pixel for each corner
                        if depth != 0 and depth is not None: #if the corner point has a valid depth value from the depth sensor
                            validPoints[index] = True #sets points which have a depth value as valid

                            #formualtion for finding 3D location of 2d point:
                            points3D[0, index] = (corner[0]-depthIntrinsics.ppx)/depthIntrinsics.fx*depth 
                            points3D[1, index] = (corner[1]-depthIntrinsics.ppy)/depthIntrinsics.fy*depth
                            points3D[2, index] = depth

                    devicesChessboardLocations[device] = corners, points2D, points3D, validPoints #save array for each camera of detected corners, their 2D locations, their 3D locations, and if they have a valid depth value
                    chessboardDeviceCount += 1

                if not chessboardFound: #if the camera doesn't see the chessboard
                    devicesChessboardLocations = {}
                    #chessboardDeviceCount = 0
                    cv2.imshow(device,bwImage)
                    cv2.waitKey(50)
                    print(device," cannot detect the chessboard!")

        time.sleep(1)
        cv2.destroyAllWindows()
    return devicesChessboardLocations
        


def poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    Transformation matrices and error are computed for each connected RealSense camera

    Parameters
    ----------
    chessboardLocations : dict
        dictionary with keys of camera's serial number holding detected corners, local 2D and 3D coordinates of detected corners, and their valid depth points

    chessboardHeight: int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth: int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize: float
        Dimension of side of chessboard (m)

    Returns
    -------
    devicesTransformation: dict
        dictionary with keys of camera's serial number with each camera's 4x4 transformation matrix and root-mean squared error

    Example
    -----
        poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize)
    """
    devicesTransformation = {}
    for (serial, [corners, points2D, points3D, validPoints]) in chessboardLocations.items(): #for every camera which has detected the chessboard

        if len(points2D[0])<5: #check if there are at least 5 points to be able to compute transformation matrix
            print(serial, " does not have enough points to have a valid depth for calculating the transformatoin")

        else:
            chessboardPoints = np.zeros((chessboardWidth*chessboardHeight,3), np.float32) #container for 3d coordinates of chessboard corners in global coordinates of chessboard
            chessboardPoints[:,:2] = np.mgrid[0:chessboardWidth, 0:chessboardHeight].T.reshape(-1,2)
            chessboardPoints = chessboardPoints.transpose() * chessboardSquareSize
            validchessboardPoints = chessboardPoints[:,validPoints].transpose()
            validobservedchessboardPoints = points3D[:, validPoints].transpose() #take chessboard points which have been detected by depth sensor

            chessboardPointsCentered = validchessboardPoints - centroid(validchessboardPoints) #center global chessboard points so that reference frame is same for every camera
            observedchessboardCentered = validobservedchessboardPoints - centroid(validobservedchessboardPoints) 

            rotationMatrix = kabsch(chessboardPointsCentered, observedchessboardCentered) #calculate rotation between local coordiante system and global coordinate system
            rmsdValue = kabsch_rmsd(chessboardPointsCentered, observedchessboardCentered) #calculate error of rotation matrix

            translationVector = centroid(validobservedchessboardPoints) - np.matmul(centroid(validchessboardPoints), rotationMatrix) #calculate translation between local and global coordinate system
            trans = -np.matmul(rotationMatrix, translationVector.transpose())

            poseMat = np.zeros((4,4)) #build 4x4 transformation matrix
            poseMat[:3,:3] = rotationMatrix
            poseMat[:3,3] = trans.flatten()
            poseMat[3,3] = 1
            
            devicesTransformation[serial] = [poseMat, rmsdValue]
    return devicesTransformation

if __name__ == "__main__":
    new('newCalibration.cal')

Functions

def detectChessboard(deviceManager, cameraSet, chessboardHeight, chessboardWidth, chessboardSquareSize)

Chessboard locations are computed for each connected RealSense camera

Parameters

deviceManager : DeviceManager object
realsense_device_manager object which manages connections to all cameras
cameraSet : list
list of camera serial numbers to detect the chessboard
chessboardHeight : int
Number of chessboard intersections defining height of target chessboard
chessboardWidth : int
Number of chessboard intersections defining width of target chessboard
chessboardSquareSize : float
Dimension of side of chessboard (m)

Returns

chessboardLocations : dict
dictionary with keys of camera's serial number holding detected corners, local 2D and 3D coordinates of detected corners, and their valid depth points

Example

detectChessboard(deviceManager, chessboardHeight, chessboardWidth, chessboardSquareSize)
Source code
def detectChessboard(deviceManager, cameraSet, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    Chessboard locations are computed for each connected RealSense camera

    Parameters
    ----------
    deviceManager : DeviceManager object
        realsense_device_manager object which manages connections to all cameras

    cameraSet : list
        list of camera serial numbers to detect the chessboard
    
    chessboardHeight: int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth: int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize: float
        Dimension of side of chessboard (m)

    Returns
    -------
    chessboardLocations : dict
        dictionary with keys of camera's serial number holding detected corners, local 2D and 3D coordinates of detected corners, and their valid depth points

    Example
    -----
        detectChessboard(deviceManager, chessboardHeight, chessboardWidth, chessboardSquareSize)
    """
    chessboardDeviceCount = 0
    devicesChessboardLocations = {}

    while len(devicesChessboardLocations) < len(cameraSet): #iterate through detecting chessboard until all available devices see chessboard
        cameraFrames = deviceManager.poll_frames()
        devicesIntrinsics = deviceManager.get_device_intrinsics(cameraFrames)
        
        for device, frames in cameraFrames.items(): #this will iterate through each device's serial number (which are used as keys in the frames object)
            if not device in devicesChessboardLocations and device in cameraSet: #if the camera has not already detected the chessboard

                align = rs.align(rs.stream.depth) #align the color sensor to the depth sensor using the factory extrinsics
                alignedFrames = align.process(frames)
                
                colorImage = np.asanyarray(alignedFrames.get_color_frame().get_data()) 
                bwImage = cv2.cvtColor(colorImage,cv2.COLOR_BGR2GRAY) #convert color image to B&W image to use in openCV functions

                depthFrame = alignedFrames.get_depth_frame()
                depthIntrinsics = devicesIntrinsics[device][rs.stream.depth]  #obtain the depth sensor intrinsic properties

                chessboardFound, corners = cv2.findChessboardCorners(bwImage, (chessboardWidth, chessboardHeight)) #use openCV function to detect chessboard corners

                if chessboardFound: #if the camera sees the chessboard
                    print(device," sees the chessboard!")
                    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) #subpix criteria
                    points2D = cv2.cornerSubPix(bwImage, corners, (11,11), (-1,-1), criteria) #further refine corners of chessboard using sub pixeling

                    cv2.drawChessboardCorners(bwImage, (chessboardWidth, chessboardHeight), points2D, chessboardFound) #draw chessboard corners in white over b&w image for verification
                    cv2.imshow(device,bwImage) #show the detected chessboard corners on the image
                    cv2.waitKey(50)

                    points2D = np.transpose(corners, (2,0,1)) 
                    points3D = np.zeros((3, len(points2D[0]))) #preallocate array to turn 2D chessboard corner points from camera into 3D points 
                    validPoints = [False] * len(points2D[0])

                    for index in range(len(points2D[0])): #iterate over every corner to find 3D location of chessboard corner
                        corner = points2D[:,index].flatten()
                        depth = depthFrame.as_depth_frame().get_distance(round(corner[0]), round(corner[1])) #this gets the depth at the pixel for each corner
                        if depth != 0 and depth is not None: #if the corner point has a valid depth value from the depth sensor
                            validPoints[index] = True #sets points which have a depth value as valid

                            #formualtion for finding 3D location of 2d point:
                            points3D[0, index] = (corner[0]-depthIntrinsics.ppx)/depthIntrinsics.fx*depth 
                            points3D[1, index] = (corner[1]-depthIntrinsics.ppy)/depthIntrinsics.fy*depth
                            points3D[2, index] = depth

                    devicesChessboardLocations[device] = corners, points2D, points3D, validPoints #save array for each camera of detected corners, their 2D locations, their 3D locations, and if they have a valid depth value
                    chessboardDeviceCount += 1

                if not chessboardFound: #if the camera doesn't see the chessboard
                    devicesChessboardLocations = {}
                    #chessboardDeviceCount = 0
                    cv2.imshow(device,bwImage)
                    cv2.waitKey(50)
                    print(device," cannot detect the chessboard!")

        time.sleep(1)
        cv2.destroyAllWindows()
    return devicesChessboardLocations
def invTrans(matrix)

Returns inverse of a transformation matrix

Parameters

matrix : (4,4) array
Input transformation matrix

Returns

invMatrix : (4,4) array
Inverse of input transformation matrix
Source code
def invTrans(matrix):
    """
    Returns inverse of a transformation matrix

    Parameters
    ----------
    matrix : (4,4) array
        Input transformation matrix

    Returns
    -------
    invMatrix : (4,4) array
        Inverse of input transformation matrix
    """
    invRot = np.eye(4)
    invRot[0:3,0:3] = matrix[0:3,0:3].T
    invTrans = np.eye(4)
    invTrans[0:3,3] = -matrix[0:3,3]
    invMatrix = np.matmul(invRot,invTrans)
    return invMatrix
def load(fileName)

Calibration parameters for previously connected cameras are loaded from a pickle file format. Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration

Parameters

fileName : str
Filename of stored calibration parameters.

Returns

devicesTransformation : dict
Keys of camera's serial number holding dictionary of calibration parameters per camera

Example

load('savedCalibration.cal')

Source code
def load(fileName):
    """ 
    Calibration parameters for previously connected cameras are loaded from a pickle file format.
    Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration 

    Parameters
    ----------
    fileName : str
        Filename of stored calibration parameters.

    Returns
    -------
    devicesTransformation : dict
        Keys of camera's serial number holding dictionary of calibration parameters per camera

    Example
    -------
    load('savedCalibration.cal')
    """
    file = open(fileName,'rb')
    devicesTransformation = pickle.load(file)
def new(fileName, deviceManager, chessboardHeight, chessboardWidth, chessboardSquareSize)

New calibration parameters for each connected camera and are created and saved in a pickle file format.

Cameras must be all be viewing the calibration checkerboard.

Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration

Parameters

fileName : str
Filename to store calibration parameters.
deviceManager : DeviceManager object
realsense_device_manager object which manages connections to all cameras
chessboardHeight : int
Number of chessboard intersections defining height of target chessboard
chessboardWidth : int
Number of chessboard intersections defining width of target chessboard
chessboardSquareSize : float
Dimension of side of chessboard (m)

Returns

devicesTransformation : dict
dictionary with keys of camera's serial number holding dictionary of calibration parameters per camera

Example

new('savedCalibration.cal')
Source code
def new(fileName,deviceManager, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    New calibration parameters for each connected camera and are created and saved in a pickle file format.
    
    Cameras must be all be viewing the calibration checkerboard. 

    Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration 

    Parameters
    ----------
    fileName : str
        Filename to store calibration parameters.
    
    deviceManager : DeviceManager object
        realsense_device_manager object which manages connections to all cameras
    
    chessboardHeight : int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth : int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize : float
        Dimension of side of chessboard (m)

    Returns
    -------
    devicesTransformation : dict
        dictionary with keys of camera's serial number holding dictionary of calibration parameters per camera

    Example
    -----
        new('savedCalibration.cal')
    """

    deviceManager.enable_all_devices()
    file = open(fileName,'wb')             
    time.sleep(1) #let autoexposure on cameras stabilize over one second 
    cameraSet = deviceManager._enabled_devices
    chessboardLocations = detectChessboard(deviceManager, cameraSet, chessboardHeight, chessboardWidth, chessboardSquareSize) #return locations of chessboards from reference frame of each camera
    devicesTransformations = poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize) #return dictionary of 
    pickle.dump(devicesTransformations, file)
    file.close()
    return devicesTransformations
def newIterative(fileName, deviceManager, cameraList, chessboardHeight, chessboardWidth, chessboardSquareSize)

New calibration parameters for each connected camera and are created and saved in a pickle file format.

Function will iterate through camera list and will search for the checkerboard between each consecutive set of two cameras in cameraList. The user must move the checkerboard between the sets of cameras as the function works through the list.

Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration

Parameters

fileName : str
Filename to store calibration parameters.
deviceManager : DeviceManager object
realsense_device_manager object which manages connections to all cameras
cameraList : list
list of serial numbers to calibrate cameras in order
chessboardHeight : int
Number of chessboard intersections defining height of target chessboard
chessboardWidth : int
Number of chessboard intersections defining width of target chessboard
chessboardSquareSize : float
Dimension of side of chessboard (m)

Returns

deviceTransformations : dict
dictionary with keys of camera's serial number holding dictionary of calibration parameters per camera

Example

new('savedCalibration.cal')
Source code
def newIterative(fileName,deviceManager, cameraList, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    New calibration parameters for each connected camera and are created and saved in a pickle file format.

    Function will iterate through camera list and will search for the checkerboard between each consecutive set of two cameras in cameraList.
    The user must move the checkerboard between the sets of cameras as the function works through the list. 

    Calibration parameters for each camera include a 4x4 transformation matrix and rmsd error of calibration 

    Parameters
    ----------
    fileName : str
        Filename to store calibration parameters.
    
    deviceManager : DeviceManager object
        realsense_device_manager object which manages connections to all cameras

    cameraList : list
        list of serial numbers to calibrate cameras in order
    
    chessboardHeight : int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth : int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize : float
        Dimension of side of chessboard (m)

    Returns
    -------
    deviceTransformations : dict
        dictionary with keys of camera's serial number holding dictionary of calibration parameters per camera

    Example
    -----
        new('savedCalibration.cal')
    """

    deviceManager.enable_all_devices()
    file = open(fileName,'wb')        

    deviceTransformations = {}
    cameraSets = []
    for cam in range(0,len(cameraList)-1):
        cameraSets.extend([[cameraList[cam], cameraList[cam+1]]])

    time.sleep(1) #let autoexposure on cameras stabilize over one second 

    for cset in cameraSets:
        fstring = "Now calibrating cameras {0} and {1}. Press ENTER to start".format(cset[0],cset[1])
        input(fstring)
        setTransformations = {}
        chessboardLocations = detectChessboard(deviceManager, cset, chessboardHeight, chessboardWidth, chessboardSquareSize) #return locations of chessboards from reference frame of each camera
        setTransformations = poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize) #return dictionary of cameras' transformtion matrices
        deviceTransformations[cset[0]] = setTransformations[cset[0]]
        deviceTransformations[cset[1]] = setTransformations[cset[1]]
        deviceTransformations[cset[0]][0] = np.matmul(invTrans(setTransformations[cset[1]][0]),setTransformations[cset[0]][0])
        #deviceTransformations[cset[1]][0] = np.eye(4)
        fstring = "Cameras {0} and {1} calibrated. Press ENTER to continue".format(cset[0],cset[1])
        input(fstring)

    for c,cam in enumerate(cameraList):    
        if c < len(cameraList):
            matrices = []
            for cm in range(c+1,len(cameraList)):
                deviceTransformations[cam][0] = np.matmul(deviceTransformations[cameraList[cm]][0],deviceTransformations[cameraList[c]][0])
                print(str(c)+':'+str(cm))
            #deviceTransformations[cameraList[c]][0] = np.matmul(deviceTransformations[cameraList[1]][0],deviceTransformations[cameraList[0]][0])
    print(deviceTransformations)
    pickle.dump(deviceTransformations, file)
    file.close()
    return deviceTransformations
def poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize)

Transformation matrices and error are computed for each connected RealSense camera

Parameters

chessboardLocations : dict
dictionary with keys of camera's serial number holding detected corners, local 2D and 3D coordinates of detected corners, and their valid depth points
chessboardHeight : int
Number of chessboard intersections defining height of target chessboard
chessboardWidth : int
Number of chessboard intersections defining width of target chessboard
chessboardSquareSize : float
Dimension of side of chessboard (m)

Returns

devicesTransformation : dict
dictionary with keys of camera's serial number with each camera's 4x4 transformation matrix and root-mean squared error

Example

poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize)
Source code
def poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize):
    """ 
    Transformation matrices and error are computed for each connected RealSense camera

    Parameters
    ----------
    chessboardLocations : dict
        dictionary with keys of camera's serial number holding detected corners, local 2D and 3D coordinates of detected corners, and their valid depth points

    chessboardHeight: int
        Number of chessboard intersections defining height of target chessboard

    chessboardWidth: int
        Number of chessboard intersections defining width of target chessboard
    
    chessboardSquareSize: float
        Dimension of side of chessboard (m)

    Returns
    -------
    devicesTransformation: dict
        dictionary with keys of camera's serial number with each camera's 4x4 transformation matrix and root-mean squared error

    Example
    -----
        poseTransformation(chessboardLocations, chessboardHeight, chessboardWidth, chessboardSquareSize)
    """
    devicesTransformation = {}
    for (serial, [corners, points2D, points3D, validPoints]) in chessboardLocations.items(): #for every camera which has detected the chessboard

        if len(points2D[0])<5: #check if there are at least 5 points to be able to compute transformation matrix
            print(serial, " does not have enough points to have a valid depth for calculating the transformatoin")

        else:
            chessboardPoints = np.zeros((chessboardWidth*chessboardHeight,3), np.float32) #container for 3d coordinates of chessboard corners in global coordinates of chessboard
            chessboardPoints[:,:2] = np.mgrid[0:chessboardWidth, 0:chessboardHeight].T.reshape(-1,2)
            chessboardPoints = chessboardPoints.transpose() * chessboardSquareSize
            validchessboardPoints = chessboardPoints[:,validPoints].transpose()
            validobservedchessboardPoints = points3D[:, validPoints].transpose() #take chessboard points which have been detected by depth sensor

            chessboardPointsCentered = validchessboardPoints - centroid(validchessboardPoints) #center global chessboard points so that reference frame is same for every camera
            observedchessboardCentered = validobservedchessboardPoints - centroid(validobservedchessboardPoints) 

            rotationMatrix = kabsch(chessboardPointsCentered, observedchessboardCentered) #calculate rotation between local coordiante system and global coordinate system
            rmsdValue = kabsch_rmsd(chessboardPointsCentered, observedchessboardCentered) #calculate error of rotation matrix

            translationVector = centroid(validobservedchessboardPoints) - np.matmul(centroid(validchessboardPoints), rotationMatrix) #calculate translation between local and global coordinate system
            trans = -np.matmul(rotationMatrix, translationVector.transpose())

            poseMat = np.zeros((4,4)) #build 4x4 transformation matrix
            poseMat[:3,:3] = rotationMatrix
            poseMat[:3,3] = trans.flatten()
            poseMat[3,3] = 1
            
            devicesTransformation[serial] = [poseMat, rmsdValue]
    return devicesTransformation