"""2D/3D Calibration using OpenCV (:mod:`fluidimage.calibration.calib_cv`)
==========================================================================
.. autoclass:: ParamContainerCV
:members:
:private-members:
.. autoclass:: SimpleCircleGrid
:members:
:private-members:
.. autoclass:: CalibCV
:members:
:private-members:
"""
import os
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle
from matplotlib.transforms import Bbox
from scipy.interpolate import griddata
from fluiddyn.util.paramcontainer import ParamContainer
from fluidimage._opencv import cv2, error_import_cv2
[docs]class ParamContainerCV(ParamContainer):
"""A container to easily display OpenCV parameters"""
def __init__(self, params_cv, tag="OpenCV"):
super().__init__(tag)
self._set_internal_attr("_params_cv", params_cv)
attrs = {
a: getattr(params_cv, a)
for a in dir(params_cv)
if not callable(a) and not a.startswith("__")
}
for key, value in attrs.items():
self._set_attrib(key, value)
def _as_params_cv(self):
params_cv = self["_params_cv"]
for key in self._get_key_attribs():
value = self[key]
setattr(params_cv, key, value)
return params_cv
[docs]class SimpleCircleGrid:
"""Detect the centers of calibration target consisting of a grid of circle
shaped points. Use it to detect the image points.
"""
@staticmethod
def create_default_params():
if error_import_cv2:
raise error_import_cv2
params_cv = cv2.SimpleBlobDetector_Params()
# Slightly nicer defaults
params_cv.filterByColor = False
params_cv.minArea = 0.1
return ParamContainerCV(params_cv, "SimpleBlobDetector")
def __init__(self, params: ParamContainerCV) -> None:
self.params = params
params_cv = params._as_params_cv()
self.detector = cv2.SimpleBlobDetector_create(params_cv)
[docs] def detect_all(self, image: np.ndarray, debug=False):
"""Detects all blobs as per parameters without any constraints.
Parameters
----------
image: array
A calibration image of maximum intensity 255.
debug : bool
Plot the detected points and the bounding box
"""
image = image.astype(np.uint8)
keypoints = self.detector.detect(image)
if debug:
plt.figure()
for k in keypoints:
plt.scatter(*k.pt)
plt.show()
if len(keypoints) == 0:
raise ValueError("No blob detected")
return keypoints
[docs] def detect_grid(
self,
image: np.ndarray,
origin: tuple,
nx: int,
ny: int,
ds: float,
debug=False,
):
"""Detect a ``nx`` by ``ny`` circle grid centered around an origin.
Parameters
----------
image: array
A calibration image of maximum intensity 255.
origin: tuple
Origin / Principal point location in pixel coordinates
nx, ny : int
Shape of the grid, i.e. number of points
ds : float
Grid spacing in pixel coordinates
debug : bool
Plot the detected points and the bounding box
"""
keypoints = self.detect_all(image)
# Add 1 so that the points at the edge of the bounding box are included
w = (nx + 1) * ds
h = (ny + 1) * ds
originx, originy = origin
bbox = Bbox.from_bounds(originx - w // 2, originy - h // 2, w, h)
if debug:
print(bbox)
center_list = []
for k in keypoints:
if bbox.contains(*k.pt):
center_list.append(k.pt)
centers = np.array(center_list)
xround = np.round(centers[..., 0] / ds)
yround = np.round(centers[..., 1] / ds)
# Sort as a grid
ind = np.lexsort((xround, yround))
# Add another dimension
centers = np.array([centers[ind]], dtype=np.float32)
if len(centers[0]) != nx * ny:
raise AssertionError(f"Only {len(centers[0])} points were found")
if debug:
fig, ax = plt.subplots()
ax.imshow(image, cmap="gray")
ax.scatter(centers[..., 0], centers[..., 1])
l, b, w, h = bbox.bounds
ax.add_patch(
Rectangle(xy=(l, b), width=w, height=h, edgecolor="r", fill=False)
)
return centers
def construct_object_points(nx: int, ny: int, z: float, ds: float):
"""Prepare object points in world coordinates, as flattened list of
coordinates such as::
(0,0,z), (1,0,z), (2,0,z) ....,(6,5,z)
This format is expected by OpenCV's ``calibrateCamera`` function.
Parameters
----------
nx, ny : int
Shape of the grid, i.e. number of points
z : float
z-location in world coordinates
ds : float
Grid spacing in world coordinates
"""
objp = np.zeros((nx * ny, 3), np.float32)
assert nx % 2 == 1
assert ny % 2 == 1
stepx, stepy = np.array((nx, ny), dtype=int) * 1j
# nx, ny will refer the number of points on one side from now on
nx = (nx - 1) // 2
ny = (ny - 1) // 2
objp[:, :2] = np.mgrid[-ny:ny:stepy, -nx:nx:stepx].T.reshape(-1, 2) * ds
objp[:, 2] = z
return objp
[docs]class CalibCV:
"""Calibrate a camera and save them as HDF5 files. Also use this to load
saved calibrations and interpolate extrinsic parameters (rotation and
translation) while reconstructing.
"""
def __init__(self, path_file="cam.h5"):
if error_import_cv2:
raise error_import_cv2
self.path_file = str(path_file)
if os.path.exists(path_file):
print(f"Loading {path_file}.")
self.params = ParamContainer(path_file=self.path_file)
def save(self, zs, ret, mtx, dist, rvecs, tvecs):
self.params = params = ParamContainer(tag="CalibCV")
params._set_attribs(
{
"class": self.__class__.__name__,
"module": self.__module__,
"f": np.diag(mtx)[:2],
"C": mtx[0:2, 2],
"cam_mtx": mtx,
"kc": dist.T[0],
"rotation": np.array(rvecs),
"translate": tvecs,
"zs": np.array(zs),
}
)
path_dir = os.path.dirname(self.path_file)
os.makedirs(path_dir, exist_ok=True)
if os.path.exists(self.path_file):
print(f"WARNING: {self.path_file} already exists. Skipping save.")
else:
params._save_as_hdf5(self.path_file)
def rotmtx_from_rotvec(self, rot_vec):
rot_mtx, rot_jac = cv2.Rodrigues(rot_vec)
return rot_mtx
[docs] def get_rotation(self, znew):
"""Linearly interpolate the rotation vector based on z location."""
rot_vec = griddata(self.params.zs, self.params.rotation, znew)
return rot_vec
[docs] def get_translate(self, znew):
"""Linearly interpolate the translation vector based on z location."""
translate = griddata(self.params.zs, self.params.translate, znew)
return translate
[docs] def calibrate(
self,
imgpoints: list,
objpoints: list,
zs: list,
im_shape: tuple,
origin=None,
debug=False,
flags=None,
):
"""Calibrate a camera based on a list of image points (in pixel
coordinates) and object points (in world coordinates) and the z-locations
(in world coordinates).
Parameters
----------
imgpoints : list of arrays
Image points in pixel coordinates. Use `SimpleCircleGrid` to detect
them from a single calibration image. Append such image points
from multiple calibration images in a list.
objpoints : list of arrays
Object points as produced by function `construct_object_points`
constitute a single array. Likewise append them into a list for
multiple calibration images.
zs : list of float
List of z locations of the calibration targes in world coordinates.
im_shape : tuple of int
Image dimensions in pixels.
origin : tuple, optional
Origin / Principal point location in pixel coordinates
flags : int, optional
OpenCV specific calibration flags
debug : bool, optional
Return the result if true, else save it as an XML file.
"""
# Initial guesses
initial_mtx = np.eye(3)
if origin is not None:
# Origin / Principal point at center:
initial_mtx[0:2, 2] = origin
initial_dist = np.zeros((5, 1))
if flags is None:
flags = (
cv2.CALIB_USE_INTRINSIC_GUESS
+ cv2.CALIB_FIX_K4
+ cv2.CALIB_FIX_K5
)
result = cv2.calibrateCamera(
objpoints, imgpoints, im_shape, initial_mtx, initial_dist, flags=flags
)
if debug:
return result
else:
self.save(zs, *result)