"""
calibration
===========
Contains different calibration algorithms.
.. todo::
Implement circles calibration. N.B. after using ``cv2.findCirclesGrid()`` a point refinement algorithm is needed (like ``cv2.cornerSubPix()`` does for the chessboard).
"""
import os
import warnings
import numpy as np
import cv2
from scipy.ndimage import map_coordinates
import simplestereo as ss
# Constants definitions
DEFAULT_CHESSBOARD_SIZE = (6,7) # As inner (rows, columns)
DEFAULT_CORNERSUBPIX_WINSIZE = (11,11)
DEFAULT_TERMINATION_CRITERIA = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6)
[docs]
def chessboardSingle(images, chessboardSize = DEFAULT_CHESSBOARD_SIZE, squareSize=1, showImages=False, distortionCoeffsNumber=5):
"""
Calibrate a single camera with a chessboard pattern.
Parameters
----------
images : list or tuple
A list (or tuple) of image paths, e.g. ["one.png", "two.png", ...]
chessboardSize: tuple
Chessboard *internal* dimensions as (width, height). Dimensions should be different to avoid ambiguity.
Default to (7,6).
squareSize : float
Length of the square side in the chosen world units. For example, if the square size is set in mm,
measures in the 3D reconstruction will be in mm. Default to 1.
showImages : bool
If True, each processed image is showed to check for correct chessboard detection.
Default to False.
distortionCoeffsNumber: int, optional
Number of distortion coefficients to be used for calibration, either either 0, 5 or 8.
Coefficients are the same order as specified in :ref:`OpenCV documentation<https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html>`.
Please note that this library uses only a small subset of the advanced calibration options found in OpenCV.
If set to 0 distortion correction is disabled (coefficients will be 0).
Default to 5 (basic radial and tangential distortion correction).
Returns
-------
retval : bool
Same values of `cv2.calibrateCamera`.
cameraMatrix : numpy.ndarray
distCoeffs : numpy.ndarray
rvecs : numpy.ndarray
tvecs : numpy.ndarray
"""
# Prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0),...
objp = np.zeros((chessboardSize[0]*chessboardSize[1],3), np.float32)
objp[:,:2] = np.mgrid[0:chessboardSize[0],0:chessboardSize[1]].T.reshape(-1,2) * squareSize
# Arrays to store object points and image points from all the images.
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane.
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# Find the chess board corners
ret, corners = cv2.findChessboardCorners(gray, chessboardSize)
# If found, add object points and image points (after refining them)
if ret == True:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray,corners, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
imgpoints.append(corners2)
if showImages:
# Draw and display the corners
img = cv2.drawChessboardCorners(img, chessboardSize, corners2,ret)
cv2.imshow('Chessboard',img)
cv2.waitKey(0)
flags = _getCalibrationFlags(distortionCoeffsNumber)
return cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None, flags=flags, criteria=DEFAULT_TERMINATION_CRITERIA)
[docs]
def chessboardStereo(images, chessboardSize=DEFAULT_CHESSBOARD_SIZE, squareSize=1, distortionCoeffsNumber=5):
"""
Performs stereo calibration between two cameras and returns a StereoRig object.
First camera (generally left) will be put in world origin.
Parameters
----------
images : list or tuple
A list (or tuple) of 2 dimensional tuples (ordered left and
right) of image paths, e.g. [("oneL.png","oneR.png"),
("twoL.png","twoR.png"), ...]
chessboardSize: tuple
Chessboard *internal* dimensions as (width, height). Dimensions should be different to avoid ambiguity.
Default to (7,6).
squareSize : float, optional
Length of the square side in the chosen world units. For example, if the square size is set in mm,
measures in the 3D reconstruction will be in mm. Default to 1.
distortionCoeffsNumber: int, optional
Number of distortion coefficients to be used for calibration, either 0, 5 or 8.
Coefficients are the same order as specified in :ref:`OpenCV documentation<https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html>`.
Please note that this library uses only a small subset of the advanced calibration options found in OpenCV.
If set to 0 distortion correction is disabled (coefficients will be 0).
Default to 5 (basic radial and tangential distortion correction).
Returns
----------
StereoRig
A StereoRig object
..todo::
Iteratively exclude images that have high reprojection errors and re-calibrate.
"""
# Prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0),...
objp = np.zeros((chessboardSize[0]*chessboardSize[1],3), np.float32)
objp[:,:2] = np.mgrid[0:chessboardSize[0],0:chessboardSize[1]].T.reshape(-1,2) * squareSize
# Arrays to store image points from all the images.
imagePoints1 = []
imagePoints2 = []
counter = 0 # Count successful couples
for path1, path2 in images:
# Read as grayscale images
img1 = cv2.imread(path1, cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread(path2, cv2.IMREAD_GRAYSCALE)
# Check that the files exist
if img1 is None or img2 is None:
raise ValueError("File not found!")
# Find the chessboard corners
ret1, corners1 = cv2.findChessboardCorners(img1, chessboardSize)
ret2, corners2 = cv2.findChessboardCorners(img2, chessboardSize)
if ret1 and ret2:
# Refine the corner locations
corners1 = cv2.cornerSubPix(img1, corners1, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
corners2 = cv2.cornerSubPix(img2, corners2, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
# Save to main list
imagePoints1.append(corners1)
imagePoints2.append(corners2)
counter += 1
# Initialize parameters
R = np.eye(3, dtype=np.float64) # Rotation matrix between the 1st and the 2nd camera coordinate systems.
T = np.zeros((3, 1), dtype=np.float64) # Translation vector between the coordinate systems of the cameras.
cameraMatrix1 = np.eye(3, dtype=np.float64)
cameraMatrix2 = np.eye(3, dtype=np.float64)
distCoeffs1 = np.empty(5)
distCoeffs2 = np.empty(5)
flags = _getCalibrationFlags(distortionCoeffsNumber)
# Do stereo calibration
retval, intrinsic1, distCoeffs1, intrinsic2, distCoeffs2, R, T, E, F = cv2.stereoCalibrate( np.array([[objp]] * counter), imagePoints1, imagePoints2, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize = img1.shape[::-1], flags=flags, criteria = DEFAULT_TERMINATION_CRITERIA)
# Build StereoRig object
stereoRigObj = ss.StereoRig(img1.shape[::-1][:2], img2.shape[::-1][:2], intrinsic1, intrinsic2, distCoeffs1, distCoeffs2, R, T, F = F, E = E, reprojectionError = retval)
return stereoRigObj
[docs]
def chessboardProCam(images, projectorResolution, chessboardSize = DEFAULT_CHESSBOARD_SIZE, squareSize=1,
black_thr=40, white_thr=5, camIntrinsic=None, camDistCoeffs=None):
"""
Performs stereo calibration between a camera (reference) and a projector.
Adapted from the code available (MIT licence) at https://github.com/kamino410/procam-calibration
and based on the paper of Daniel Moreno and Gabriel Taubin, "Simple, accurate, and
robust projector-camera calibration", DOI: 10.1109/3DIMPVT.2012.77.
The camera will be put in world origin.
Parameters
----------
images : list or tuple
A list of lists (one per set) of image paths acquired by the camera.
Each set must be ordered like all the Gray code patterns (see ``cv2.structured_light_GrayCodePattern``)
followed by black, normal light and white images (in this order).
At least 5-6 sets are suggested.
projectorResolution: tuple
Projector pixel resolution as (width, height).
chessboardSize: tuple, optional
Chessboard *internal* dimensions as (width, height). Dimensions should be different to avoid ambiguity.
Default to (7,6).
squareSize : float, optional
If the square size is known, calibration can be in metric units. Default to 1.
black_thr : int, optional
Black threshold is a number between 0-255 that represents the minimum brightness difference
required for valid pixels, between the fully illuminated (white) and the not illuminated images (black).
Default to 40.
white_thr : int, optional
White threshold is a number between 0-255 that represents the minimum brightness difference
required for valid pixels, between the graycode pattern and its inverse images. Default to 5.
camIntrinsic : numpy.ndarray, optional
A 3x3 matrix representing camera intrinsic parameters. If not given it will be calculated.
camDistCoeffs : list, optional
Camera distortion coefficients of 4, 5, 8, 12 or 14 elements (refer to OpenCV documentation).
If not given they will be calculated.
normalize : bool
If True, the images are min-max normalized before processing. Default to False.
Returns
----------
StereoRig
A StereoRig object
.. todo::
Iteratively exclude images that have high reprojection errors and re-calibrate.
"""
# Prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0),...
objps = np.zeros((chessboardSize[0]*chessboardSize[1],3), np.float32)
objps[:,:2] = np.mgrid[0:chessboardSize[0],0:chessboardSize[1]].T.reshape(-1,2) * squareSize
# Gray Code setup
gc_width, gc_height = projectorResolution
graycode = cv2.structured_light_GrayCodePattern.create(width=gc_width, height=gc_height)
graycode.setBlackThreshold(black_thr)
graycode.setWhiteThreshold(white_thr)
cam_shape = cv2.imread(images[0][0], cv2.IMREAD_GRAYSCALE).shape
patch_size_half = int(np.ceil(cam_shape[1] / 180))
cam_corners_list = []
cam_objps_list = []
cam_corners_list2 = []
proj_objps_list = []
proj_corners_list = []
skipped = 0 # Skipped corners
# Iterate over sets of Gray code images
for imageset in images:
# Check that the input images are the right number
if len(imageset) != graycode.getNumberOfPatternImages() + 3:
raise ValueError(f'Invalid number of images in set {os.path.dirname(imageset[0])}!')
imgs = []
for fname in imageset:
img = cv2.imread(fname, cv2.IMREAD_GRAYSCALE)
if cam_shape != img.shape:
raise ValueError(f'Image size of {fname} is mismatch!')
imgs.append(img)
white_img = imgs.pop()
normal_img = imgs.pop()
black_img = imgs.pop()
res, cam_corners = cv2.findChessboardCorners(normal_img, chessboardSize)
if not res:
raise ValueError(f'Chessboard not found in set {os.path.dirname(imageset[0])}!')
# Subpixel refinement
cam_corners_sub = cv2.cornerSubPix(normal_img, cam_corners, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
cam_corners_list.append(cam_corners_sub)
cam_objps_list.append(objps)
proj_objps = []
proj_corners = []
cam_corners2 = []
for corner, objp in zip(cam_corners, objps):
c_x = int(round(corner[0][0]))
c_y = int(round(corner[0][1]))
src_points = []
dst_points = []
for dx in range(-patch_size_half, patch_size_half + 1):
for dy in range(-patch_size_half, patch_size_half + 1):
x = c_x + dx
y = c_y + dy
err, proj_pix = graycode.getProjPixel(imgs, x, y)
if not err:
src_points.append((x, y))
dst_points.append(np.array(proj_pix))
if len(src_points) < patch_size_half**2:
#warnings.warn(f"Corner {c_x},{c_y} was skipped because decoded pixel were too few.")
skipped+=1
continue
h_mat, inliers = cv2.findHomography(
np.array(src_points), np.array(dst_points))
point = h_mat@np.array([corner[0][0], corner[0][1], 1]).transpose()
point_pix = point[0:2]/point[2]
proj_objps.append(objp)
proj_corners.append([point_pix])
cam_corners2.append(corner)
if len(proj_corners) < 3:
raise ValueError(f"Not enough corners were found in set {os.path.dirname(imageset[0])} (less than 3). Skipping.")
proj_objps_list.append(np.float32(proj_objps))
proj_corners_list.append(np.float32(proj_corners))
cam_corners_list2.append(np.float32(cam_corners2))
if skipped>0:
warnings.warn(f"{skipped} over {len(proj_objps_list)*chessboardSize[0]*chessboardSize[1]} skipped corners.")
cam_rvecs = []
cam_tvecs = []
# Calibrate camera only if intrinsic parameters are not given
if camIntrinsic is None:
cam_rms, cam_int, cam_dist, cam_rvecs, cam_tvecs = cv2.calibrateCamera(
cam_objps_list, cam_corners_list, cam_shape[::-1], None, None, None, None)
else:
for objp, corners in zip(cam_objps_list, cam_corners_list):
cam_rms, cam_rvec, cam_tvec = cv2.solvePnP(objp, corners, camIntrinsic, camDistCoeffs)
cam_rvecs.append(cam_rvec)
cam_tvecs.append(cam_tvec)
cam_int = camIntrinsic
cam_dist = camDistCoeffs
# Calibrate projector
proj_rms, proj_int, proj_dist, proj_rvecs, proj_tvecs = cv2.calibrateCamera(
proj_objps_list, proj_corners_list, projectorResolution, None, None, None, None)
# Stereo calibrate
retval, intrinsic1, distCoeffs1, intrinsic2, distCoeffs2, R, T, E, F = cv2.stereoCalibrate(
proj_objps_list, cam_corners_list2, proj_corners_list, cam_int, cam_dist, proj_int, proj_dist, None, flags=cv2.CALIB_FIX_INTRINSIC)
# Build StereoRig object
stereoRigObj = ss.StereoRig(cam_shape[::-1], projectorResolution, intrinsic1, intrinsic2, distCoeffs1, distCoeffs2, R, T, F = F, E = E, reprojectionError = retval)
return stereoRigObj
def _getWhiteCenters(cam_corners_list, cam_int, cam_dist, chessboardSize, squareSize):
"""
From ordered camera chessboard corners and camera intrinsics, get
world coordinate of the center of the white squares.
These areas are less affected by ambiguity and noise due to the absence of
high contrast pixel values.
"""
# Index of corners situated at the upper-left of white squares
whiteUpperLeftIndexes = []
for i in np.arange(1,chessboardSize[0]*(chessboardSize[1]-1)-1,2):
sel = i
r = (i+1)//chessboardSize[0]
if r%2==1 and chessboardSize[0]%2==0:
sel+=1
# Skip end of row
if (sel+1)%chessboardSize[0]==0:
continue
whiteUpperLeftIndexes.append(sel)
# Prepare white object points, like (1,0,0), (3,0,0), (5,0,0)
whiteObjps = np.zeros((len(whiteUpperLeftIndexes),3), dtype=np.float32)
for i,w in enumerate(whiteUpperLeftIndexes):
whiteObjps[i,0] = (w//chessboardSize[0])*squareSize
whiteObjps[i,1] = (w%chessboardSize[0])*squareSize
# Not necessary, as origin is arbitrary
#whiteObjps[:,:2] += squareSize/2 # Shift wrt origin along x and y
cam_whiteCorners_list = []
# UNDISTORT CHESSBOARDS
for i,points in enumerate(cam_corners_list):
# Undistort points
points_undist = cv2.undistortPoints(points, cam_int, cam_dist)
whiteCenters_undist = []
# Given the upper left of valid white squares, find center
for w in whiteUpperLeftIndexes:
# As intersection of diagonals
xa,ya = points_undist[w,0]
xb,yb = points_undist[w+1,0]
xd,yd = points_undist[w+chessboardSize[0],0]
xc,yc = points_undist[w+chessboardSize[0]+1,0]
xCenter = (xb*(yd-yb)*(xc-xa)+(ya-yb)*(xd-xb)*(xc-xa) - xa*(yc-ya)*(xd-xb)) / ((yd-yb)*(xc-xa)-(yc-ya)*(xd-xb))
yCenter = (yc-ya)*(xCenter-xa)/(xc-xa) + ya
whiteCenters_undist.append([(xCenter,yCenter)])
whiteCenters_dist = ss.points.distortPoints(whiteCenters_undist,cam_dist)
# Assign back camera intrinsics
whiteCenters = cv2.perspectiveTransform(whiteCenters_dist,cam_int)
cam_whiteCorners_list.append(whiteCenters.astype(np.float32)) # List of [[x,y]]
return cam_whiteCorners_list, whiteObjps
[docs]
def chessboardProCamWhite(images, projectorResolution, chessboardSize = DEFAULT_CHESSBOARD_SIZE, squareSize=1,
black_thr=40, white_thr=5, camIntrinsic=None, camDistCoeffs=None, extended=False):
"""
Performs stereo calibration between a camera (reference) and a projector.
Requires a chessboard with *black* top-left square.
Adapted from the code available (MIT licence) at https://github.com/kamino410/procam-calibration
and based on the paper of Daniel Moreno and Gabriel Taubin, "Simple, accurate, and
robust projector-camera calibration", DOI: 10.1109/3DIMPVT.2012.77.
The camera will be put in world origin.
Parameters
----------
images : list or tuple
A list of lists (one per set) of image paths acquired by the camera.
Each set must be ordered like all the Gray code patterns (see ``cv2.structured_light_GrayCodePattern``)
followed by black, normal light conditions and white images (in this exact order).
At least 5-6 sets are suggested.
projectorResolution: tuple
Projector pixel resolution as (width, height).
chessboardSize: tuple, optional
Chessboard *internal* dimensions as (cols, rows). Dimensions should be different to avoid ambiguity.
Default to (6,7).
squareSize : float, optional
If the square size is known, calibration can be in metric units. Default to 1.
black_thr : int, optional
Black threshold is a number between 0-255 that represents the minimum brightness
difference required for valid pixels, between the fully illuminated (white) and
the not illuminated images (black); used in computeShadowMasks method.
Default to 40.
white_thr : int, optional
White threshold is a number between 0-255 that represents the minimum brightness difference
required for valid pixels, between the graycode pattern and its inverse images; used in
getProjPixel method.
Default to 5.
camIntrinsic : numpy.ndarray, optional
A 3x3 matrix representing camera intrinsic parameters. If not given it will be calculated.
camIntrinsic : list, optional
Camera distortion coefficients of 4, 5, 8, 12 or 14 elements (refer to OpenCV documentation).
If not given they will be calculated.
extended: bool
If True, `perViewErrors` of the stereo calibration are returned. Default to False.
Returns
-------
StereoRig
A StereoRig object
perViewErrors
Returned only if `extended` is True. For each pattern view, the
RMS error of camera and projector is returned.
"""
# Prepare camera object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0),...
objps = np.zeros((chessboardSize[0]*chessboardSize[1],3), np.float32)
objps[:,:2] = np.mgrid[0:chessboardSize[0],0:chessboardSize[1]].T.reshape(-1,2) * squareSize
# Gray Code setup
graycode = cv2.structured_light_GrayCodePattern.create(width=projectorResolution[0], height=projectorResolution[1])
# CALCULATE black_threshold and white_threshold as in paper
# TODO
graycode.setBlackThreshold(black_thr)
graycode.setWhiteThreshold(white_thr)
cam_shape = cv2.imread(images[0][0], cv2.IMREAD_GRAYSCALE).shape
patch_size_half = int(np.ceil(cam_shape[1] / 180))
cam_corners_list = []
cam_objps_list = []
cam_corners_list2 = []
proj_objps_list = []
proj_corners_list = []
skipped = 0 # Skipped corners
#### Camera calibration
cam_rvecs = []
cam_tvecs = []
# Calibrate camera only if intrinsic parameters are not given
for imageset in images:
# Load only the normal light image
normal_img = cv2.imread(imageset[-2], cv2.IMREAD_GRAYSCALE)
# Find chessboard corners
res, cam_corners = cv2.findChessboardCorners(normal_img, chessboardSize)
if not res:
raise ValueError(f'Chessboard not found in {imageset[-2]}!')
# Subpixel refinement
cam_corners_sub = cv2.cornerSubPix(normal_img, cam_corners, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
# Draw and display the corners
#img = cv2.drawChessboardCorners(normal_img, chessboardSize, cam_corners_sub, True)
#cv2.imshow('chessboard',img)
#cv2.waitKey(0)
cam_corners_list.append(cam_corners_sub)
cam_objps_list.append(objps)
# Do camera calibration if needed
if camIntrinsic is None:
# OpenCV prefers (width,height) as resolution
cam_rms, cam_int, cam_dist, cam_rvecs, cam_tvecs = cv2.calibrateCamera(
cam_objps_list, cam_corners_list, cam_shape[::-1], None, None, None, None)
else:
for objp, corners in zip(cam_objps_list, cam_corners_list):
cam_rms, cam_rvec, cam_tvec = cv2.solvePnP(objp, corners, camIntrinsic, camDistCoeffs)
cam_rvecs.append(cam_rvec)
cam_tvecs.append(cam_tvec)
cam_int = camIntrinsic
cam_dist = camDistCoeffs
# From camera corners, get centers of white squares
cam_whiteCorners_list, whiteObjps = _getWhiteCenters(cam_corners_list,
cam_int, cam_dist, chessboardSize, squareSize)
# Iterate over sets of Gray code images
for setnum, imageset in enumerate(images):
# Check that the input images are the right number
if len(imageset) != graycode.getNumberOfPatternImages() + 3:
raise ValueError(f'Invalid number of images in set {os.path.dirname(imageset[0])}!')
imgs = []
for fname in imageset[:-3]: # Exclude non pattern images
img = cv2.imread(fname, cv2.IMREAD_GRAYSCALE)
if cam_shape != img.shape:
raise ValueError(f'Image size of {fname} is mismatch!')
imgs.append(img)
proj_objps = []
proj_corners = []
cam_corners2 = []
for pointIndex, center in enumerate(cam_whiteCorners_list[setnum]):
c_x = int(round(center[0][0]))
c_y = int(round(center[0][1]))
src_points = []
dst_points = []
for dx in range(-patch_size_half, patch_size_half + 1):
for dy in range(-patch_size_half, patch_size_half + 1):
x = c_x + dx
y = c_y + dy
# Returns integer coord
err, proj_pix = graycode.getProjPixel(imgs, x, y)
if not err:
src_points.append((x, y))
dst_points.append(np.array(proj_pix))
if len(src_points) < patch_size_half**2:
#warnings.warn(f"Corner {c_x},{c_y} was skipped because decoded pixel were too few.")
skipped+=1
continue
proj_objps.append(whiteObjps[pointIndex])
h_mat, inliers = cv2.findHomography(
np.array(src_points), np.array(dst_points))
point = h_mat@np.array([center[0][0], center[0][1], 1]).transpose()
point_pix = point[0:2]/point[2]
proj_corners.append([point_pix])
cam_corners2.append(center)
if len(proj_corners) < 3:
raise ValueError(f"Not enough corners were found in set {os.path.dirname(imageset[0])} (less than 3). Skipping.")
proj_objps_list.append(np.float32(proj_objps))
proj_corners_list.append(np.float32(proj_corners))
cam_corners_list2.append(np.float32(cam_corners2))
if skipped>0:
warnings.warn(f"{skipped} over {len(proj_objps_list)*chessboardSize[0]*chessboardSize[1]} skipped corners.")
# Calibrate projector
proj_rms, proj_int, proj_dist, proj_rvecs, proj_tvecs = cv2.calibrateCamera(
proj_objps_list, proj_corners_list, projectorResolution, None, None, None, None)
# Stereo calibrate
retval, intrinsic1, distCoeffs1, intrinsic2, distCoeffs2, R, T, E, F = cv2.stereoCalibrate(
proj_objps_list, cam_corners_list2, proj_corners_list, cam_int, cam_dist, proj_int, proj_dist, None, flags=cv2.CALIB_FIX_INTRINSIC)
# Build StereoRig object
stereoRigObj = ss.StereoRig(cam_shape[::-1], projectorResolution, intrinsic1, intrinsic2, distCoeffs1, distCoeffs2, R, T, F = F, E = E, reprojectionError = retval)
if extended:
_, _, _, _, _, _, _, _, _, perViewErrors = cv2.stereoCalibrateExtended(
proj_objps_list, cam_whiteCorners_list, proj_corners_list, cam_int, cam_dist, proj_int, proj_dist, None, R, T, flags=cv2.CALIB_FIX_INTRINSIC)
return stereoRigObj, perViewErrors
else:
return stereoRigObj
[docs]
def phaseShift(periods, projectorResolution, cameraImages, chessboardSize=DEFAULT_CHESSBOARD_SIZE, squareSize=1,
camIntrinsic=None, camDistCoeffs=None):
"""
Calibrate camera and projector using phase shifting and the
heterodyne principle [Reich 1997].
Parameters
----------
periods : list of lists
Periods of fringe used in the projector as list of lists.
First list is for the horizontal fringes, second for the vertical ones.
In descending order (e.g. 1280, 1024, 512, ...).
projectorResolution: tuple
Projector pixel resolution as (width, height).
cameraImages : list or tuple
A list of lists (one per set) of image paths acquired by the camera.
Each set must be ordered, having 4 images for each period with
horizontal followed by vertical images.
In each set, last image is the one in normal light conditions.
At least 5-6 sets are suggested.
chessboardSize: tuple, optional
Chessboard *internal* dimensions as (cols, rows). Dimensions should be different to avoid ambiguity.
Default to (6,7).
squareSize : float, optional
If the square size is known, calibration can be in metric units. Default to 1.
camIntrinsic : numpy.ndarray, optional
A 3x3 matrix representing camera intrinsic parameters. If not given it will be calculated.
camIntrinsic : list, optional
Camera distortion coefficients of 4, 5, 8, 12 or 14 elements (refer to OpenCV documentation).
If not given they will be calculated.
Returns
-------
StereoRig
A StereoRig object
"""
# Internal functions
def getPhase(imgPaths):
"""
Get ordered images and retrieve wrapped phase map.
"""
I = []
for p in imgPaths:
img = cv2.imread(p, cv2.IMREAD_GRAYSCALE)
I.append(img.astype(float))
# cos(theta + i*pi/2) expected
# Output values in [0, 2pi)
return np.mod( np.arctan2(I[3]-I[1], I[0]-I[2]), 2*np.pi)
def unwrap(theta0, theta1, T0, T1):
"""
Given absolute phase `theta0`, wrapped phase `theta1` and their
respective periods `T0` and `T1`, unwrap `theta1`.
Returned phase is normalized in [0, 2pi).
"""
k = np.rint((theta0*T0/T1 - theta1)/(2*np.pi))
return (theta1 + 2*np.pi*k)*T1/T0
def getProjCoord(phase_x, phase_y):
"""
Return projector coordinate corresponding to absolute phase values.
"""
px = projectorResolution[0]*phase_x/(2*np.pi)
py = projectorResolution[1]*phase_y/(2*np.pi)
return np.hstack( (px, py) )
# Prepare camera object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0),...
objps = np.zeros((chessboardSize[0]*chessboardSize[1],3), np.float32)
objps[:,:2] = np.mgrid[0:chessboardSize[0],0:chessboardSize[1]].T.reshape(-1,2) * squareSize
cam_shape = cv2.imread(cameraImages[0][0], cv2.IMREAD_GRAYSCALE).shape
cam_corners_list = []
cam_objps_list = []
proj_corners_list = []
proj_objps_list = []
#### Camera calibration
cam_rvecs = []
cam_tvecs = []
for imageset in cameraImages:
# Get corners from normal.png as [(x1,y1), (x2,y2), ...]
normal_img = cv2.imread(imageset[-1], cv2.IMREAD_GRAYSCALE)
res, cam_corners = cv2.findChessboardCorners(normal_img, chessboardSize)
if not res:
raise ValueError(f'Chessboard not found in {imageset[-1]}!')
# Subpixel refinement. Output as [(x1,y1), (x2,y2), ...]
cam_corners_sub = cv2.cornerSubPix(normal_img, cam_corners, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
# Draw and display the corners
#img = cv2.drawChessboardCorners(normal_img, chessboardSize, cam_corners_sub, True)
#cv2.imshow('chessboard',img)
#cv2.waitKey(0)
cam_corners_list.append(cam_corners_sub.astype(np.float32))
cam_objps_list.append(objps.astype(np.float32))
# Get absolute phase using all the acquired fringes
i=0 # counter
phase = [None,None] # Horizontal and vertical absolute phases
for v in range(2):
for j,T in enumerate(periods[v]):
if j==0: # if it is the first (maximum period)
phase[v] = getPhase(imageset[i:i+4])
else:
phase2 = getPhase(imageset[i:i+4])
phase[v] = unwrap(phase[v], phase2, periods[v][0], T)
i+=4 # update counter
# Find camera-projector correspondences
#cam_corners_sub shape (n, 1, 2)
corners = cam_corners_sub.reshape(-1,2) # shape (n, 2) [[x1,y1], [x2,y2], ...]
corners = corners.T # (2, n)
corners = corners[[1,0]] # Swap x,y to get
# Linear interpolation of non-integer coordinates
phase_x = map_coordinates(phase[0], corners, order=1).reshape(-1,1)
phase_y = map_coordinates(phase[1], corners, order=1).reshape(-1,1)
# Get corresponding values on projector
proj_corners = getProjCoord(phase_x,phase_y) # Shape (n, 2)
proj_corners_list.append(proj_corners.astype(np.float32))
proj_objps_list.append(objps.astype(np.float32))
# Call OpenCV calibrate
# Calibrate camera only if intrinsic parameters are not given
if camIntrinsic is None:
cam_rms, cam_int, cam_dist, cam_rvecs, cam_tvecs = cv2.calibrateCamera(
cam_objps_list, cam_corners_list, cam_shape[::-1], None, None, None, None)
else:
for objp, corners in zip(cam_objps_list, cam_corners_list):
cam_rms, cam_rvec, cam_tvec = cv2.solvePnP(objp, corners, camIntrinsic, camDistCoeffs)
cam_rvecs.append(cam_rvec)
cam_tvecs.append(cam_tvec)
cam_int = camIntrinsic
cam_dist = camDistCoeffs
# Calibrate projector
proj_rms, proj_int, proj_dist, proj_rvecs, proj_tvecs = cv2.calibrateCamera(
proj_objps_list, proj_corners_list, projectorResolution, None, None, None, None)
# Stereo calibrate
retval, intrinsic1, distCoeffs1, intrinsic2, distCoeffs2, R, T, E, F = cv2.stereoCalibrate(
proj_objps_list, cam_corners_list, proj_corners_list, cam_int, cam_dist, proj_int, proj_dist, None, flags=cv2.CALIB_FIX_INTRINSIC)
# Build StereoRig object
stereoRigObj = ss.StereoRig(cam_shape[::-1], projectorResolution, intrinsic1, intrinsic2, distCoeffs1, distCoeffs2, R, T, F = F, E = E, reprojectionError = retval)
return stereoRigObj
[docs]
def phaseShiftWhite(periods, projectorResolution, cameraImages, chessboardSize=DEFAULT_CHESSBOARD_SIZE,
squareSize=1, camIntrinsic=None, camDistCoeffs=None, extended=False):
"""
Calibrate camera and projector using phase shifting and heterodyne
principle [Reich 1997]. Using center of white squares instead of
corners as targets.
The center of a white square is well defined and less subject to
noise or uncertanty of the phase value.
Parameters
----------
periods : list of lists
Periods of fringe used in the projector as list of lists.
First list is for the horizontal fringes, second for the vertical ones.
In descending order (e.g. 1280, 1024, 512, ...).
projectorResolution: tuple
Projector pixel resolution as (width, height).
cameraImages : list or tuple
A list of lists (one per set) of image paths acquired by the camera.
Each set must be ordered, having 4 images for each period with
horizontal followed by vertical images.
In each set, last image is the one in normal light conditions.
At least 5-6 sets are suggested.
chessboardSize: tuple, optional
Chessboard *internal* dimensions as (cols, rows). Dimensions should be different to avoid ambiguity.
Default to (6,7).
squareSize : float, optional
If the square size is known, calibration can be in metric units. Default to 1.
camIntrinsic : numpy.ndarray, optional
A 3x3 matrix representing camera intrinsic parameters. If not given it will be calculated.
camIntrinsic : list, optional
Camera distortion coefficients of 4, 5, 8, 12 or 14 elements (refer to OpenCV documentation).
If not given they will be calculated.
extended: bool
If True, `perViewErrors` of the stereo calibration are returned. Default to False.
Returns
-------
StereoRig
A StereoRig object
perViewErrors
Returned only if `extended` is True. For each pattern view, the
RMS error of camera and projector is returned.
"""
# Internal functions
def getPhase(imgPaths):
"""
Get ordered images and retrieve wrapped phase map.
"""
I = []
for p in imgPaths:
img = cv2.imread(p, cv2.IMREAD_GRAYSCALE)
I.append(img.astype(float))
# cos(theta + i*pi/2) expected
# Output values in [0, 2pi)
return np.mod( np.arctan2(I[3]-I[1], I[0]-I[2]), 2*np.pi)
def unwrap(theta0, theta1, T0, T1):
"""
Given absolute phase `theta0`, wrapped phase `theta1` and their
respective periods `T0` and `T1`, unwrap `theta1`.
Returned phase is normalized in [0, 2pi).
"""
k = np.rint((theta0*T0/T1 - theta1)/(2*np.pi))
return (theta1 + 2*np.pi*k)*T1/T0
def getProjCoord(phase_x, phase_y):
"""
Return projector coordinate corresponding to absolute phase values.
"""
px = projectorResolution[0]*phase_x/(2*np.pi)
py = projectorResolution[1]*phase_y/(2*np.pi)
return np.hstack( (px, py) )
# Prepare camera object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0),...
objps = np.zeros((chessboardSize[0]*chessboardSize[1],3), np.float32)
objps[:,:2] = np.mgrid[0:chessboardSize[0],0:chessboardSize[1]].T.reshape(-1,2) * squareSize
cam_shape = cv2.imread(cameraImages[0][0], cv2.IMREAD_GRAYSCALE).shape
cam_corners_list = []
cam_objps_list = []
proj_corners_list = []
proj_objps_list = []
#### Camera calibration
cam_rvecs = []
cam_tvecs = []
# Loop 1
for imageset in cameraImages:
# Get corners from normal.png as [(x1,y1), (x2,y2), ...]
normal_img = cv2.imread(imageset[-1], cv2.IMREAD_GRAYSCALE)
res, cam_corners = cv2.findChessboardCorners(normal_img, chessboardSize)
if not res:
raise ValueError(f'Chessboard not found in {imageset[-1]}!')
# Subpixel refinement. Output as [(x1,y1), (x2,y2), ...]
cam_corners_sub = cv2.cornerSubPix(normal_img, cam_corners, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
# Draw and display the corners
#img = cv2.drawChessboardCorners(normal_img, chessboardSize, cam_corners_sub, True)
#cv2.imshow('chessboard',img)
#cv2.waitKey(0)
cam_corners_list.append(cam_corners_sub.astype(np.float32))
cam_objps_list.append(objps.astype(np.float32))
# Call OpenCV calibrate
# Calibrate camera only if intrinsic parameters are not given
if camIntrinsic is None:
cam_rms, cam_int, cam_dist, cam_rvecs, cam_tvecs = cv2.calibrateCamera(
cam_objps_list, cam_corners_list, cam_shape[::-1], None, None, None, None)
else:
for objp, corners in zip(cam_objps_list, cam_corners_list):
cam_rms, cam_rvec, cam_tvec = cv2.solvePnP(objp, corners, camIntrinsic, camDistCoeffs)
cam_rvecs.append(cam_rvec)
cam_tvecs.append(cam_tvec)
cam_int = camIntrinsic
cam_dist = camDistCoeffs
# From camera corners, get centers of white squares
cam_whiteCorners_list, whiteObjps = _getWhiteCenters(cam_corners_list,
cam_int, cam_dist, chessboardSize, squareSize)
whiteObjps = whiteObjps.astype(np.float32)
cam_whiteObjps_list = [[whiteObjps] for _ in range(len(cam_whiteCorners_list))]
# Loop 2
for setnum, imageset in enumerate(cameraImages):
# Get absolute phase using all the acquired fringes
i=0 # counter
phase = [None,None] # Horizontal and vertical absolute phases
for v in range(2):
for j,T in enumerate(periods[v]):
if j==0: # if it is the first (maximum period)
phase[v] = getPhase(imageset[i:i+4])
else:
phase2 = getPhase(imageset[i:i+4])
phase[v] = unwrap(phase[v], phase2, periods[v][0], T)
i+=4 # update counter
### Find camera-projector correspondences
#cam_corners_sub shape (n, 1, 2)
corners = cam_whiteCorners_list[setnum].reshape(-1,2) # shape (n, 2) [[x1,y1], [x2,y2], ...]
corners = corners.T # (2, n)
corners = corners[[1,0]] # Swap to get (y,x) coords
# Linear interpolation of non-integer coordinates
phase_x = map_coordinates(phase[0], corners, order=1).reshape(-1,1)
phase_y = map_coordinates(phase[1], corners, order=1).reshape(-1,1)
# Get corresponding (x,y) values on projector
proj_corners = getProjCoord(phase_x,phase_y) # Shape (n, 2)
proj_corners_list.append(proj_corners.astype(np.float32))
proj_objps_list.append(whiteObjps)
# Calibrate projector
proj_rms, proj_int, proj_dist, proj_rvecs, proj_tvecs = cv2.calibrateCamera(
proj_objps_list, proj_corners_list, projectorResolution, None, None, None, None)
'''
# Calibrate projector WITHOUT LENS DISTORTION
proj_rms, proj_int, proj_dist, proj_rvecs, proj_tvecs = cv2.calibrateCamera(
proj_objps_list, proj_corners_list, projectorResolution, None, None, None, None, flags=cv2.CALIB_FIX_K1+cv2.CALIB_FIX_K2+cv2.CALIB_FIX_K3+cv2.CALIB_ZERO_TANGENT_DIST)
'''
# Stereo calibrate
retval, intrinsic1, distCoeffs1, intrinsic2, distCoeffs2, R, T, E, F = cv2.stereoCalibrate(
proj_objps_list, cam_whiteCorners_list, proj_corners_list, cam_int, cam_dist, proj_int, proj_dist, None, flags=cv2.CALIB_FIX_INTRINSIC)
# Build StereoRig object
stereoRigObj = ss.StereoRig(cam_shape[::-1], projectorResolution, intrinsic1, intrinsic2, distCoeffs1, distCoeffs2, R, T, F = F, E = E, reprojectionError = retval)
if extended:
_, _, _, _, _, _, _, _, _, perViewErrors = cv2.stereoCalibrateExtended(
proj_objps_list, cam_whiteCorners_list, proj_corners_list, cam_int, cam_dist, proj_int, proj_dist, None, R, T, flags=cv2.CALIB_FIX_INTRINSIC)
return stereoRigObj, perViewErrors
else:
return stereoRigObj
[docs]
def generateChessboardSVG(chessboardSize, filepath, squareSize=20, border=10):
"""
Write the desired chessboard to a SVG file.
*chessboardSize* is expressed as (columns, rows) tuple, counting *internal line* columns and rows
as OpenCV does (e.g. to obtain a 10x7 chessboard, use (9,6)).
Parameters
----------
chessboardSize : tuple
Size of the chessboard as (columns, rows).
filepath : string
File path where to save the SVG file.
squareSize : int
Side of the square in millimeters. Default to 20. However it may not be represented exactly, depending on software.
"""
cols, rows = chessboardSize
cols+=1
rows+=1
with open(filepath, "w") as f:
f.write('<?xml version="1.0" encoding="UTF-8"?>')
f.write(f'<svg xmlns="http://www.w3.org/2000/svg" width="{cols*squareSize}mm" height="{rows*squareSize}mm" viewBox="0 0 {cols} {rows}" style="border: {border}mm solid #FFF;">')
f.write('<rect fill="#FFF" x="0" y="0" width="{}" height="{}"/>'.format(cols, rows))
d = 'M0 0'
d += 'm0 2'.join(['H{}v1H0z'.format(cols) for _ in range((rows+1)//2)]) # Build rows
d += 'M1 0'
d += 'm2 0'.join(['V{}h1V0z'.format(rows) for _ in range(cols//2)]) # Build cols
f.write('<path fill="#000" d="{}"/></svg>'.format(d))
return
[docs]
def getFundamentalMatrixFromProjections(P1,P2):
"""
Compute the fundamental matrix from two projection matrices.
The algorithm is adapted from an original lesson of Cristina Turrini, UNIMI, Trento (09/04/2017).
Parameters
----------
P1, P2 : numpy.ndarray
3x4 camera projection matrices.
Returns
-------
F : numpy.ndarray
The 3x3 fundamental matrix.
"""
X = []
X.append(np.vstack((P1[1,:], P1[2,:])))
X.append(np.vstack((P1[2,:], P1[0,:])))
X.append(np.vstack((P1[0,:], P1[1,:])))
Y = []
Y.append(np.vstack((P2[1,:], P2[2,:])))
Y.append(np.vstack((P2[2,:], P2[0,:])))
Y.append(np.vstack((P2[0,:], P2[1,:])))
F = np.zeros((3,3))
for i in range(3):
for j in range(3):
F[i, j] = np.linalg.det(np.vstack((X[j], Y[i])))
return F
def _getCalibrationFlags(n):
"""
Get flags for calibration from the selected number of parameters.
Selecting distortion coefficients in OpenCV:
The distortion parameters are the radial coefficients k1, k2, k3, k4, k5, and k6 ,
p1 and p2 are the tangential distortion coefficients, and s1, s2, s3, and s4, are
the thin prism distortion coefficients. τx,τy are two rotations to correct for sensor orientation.
Higher-order coefficients are not considered in OpenCV.
The coefficients are passed or returned as
(k1,k2,p1,p2[,k3[,k4,k5,k6[,s1,s2,s3,s4,[τx,τy]]]])
See https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html
if distortionCoeffsNumber == 0:
Fix standard 5 coefficients to 0 so that optimisation is not affected.
"""
if n == 0:
return (
cv2.CALIB_ZERO_TANGENT_DIST
+ cv2.CALIB_FIX_K1
+ cv2.CALIB_FIX_K2
+ cv2.CALIB_FIX_K3
)
elif n == 8:
return cv2.CALIB_RATIONAL_MODEL
else:
# For n== 5 or any other case.
return 0
"""
# TODO
elif distortionCoeffsNumber == 12:
flags = (
cv2.CALIB_RATIONAL_MODEL
+ cv2.CALIB_THIN_PRISM_MODEL
)
elif distortionCoeffsNumber == 14:
flags = (
cv2.CALIB_RATIONAL_MODEL
+ cv2.CALIB_THIN_PRISM_MODEL
+ cv2.CALIB_TILTED_MODEL
)
"""