Calibration¶
Turn captures of a real camera into a measured catalog entry; see the
calibration guide.
geometry
¶
Camera geometry from ChArUco captures: intrinsics, distortion, and stereo extrinsics.
A ChArUco board is a chessboard with ArUco markers in its white squares, so every detected corner has an identity: partial views, oblique angles and boards at the image edges all count. That is what a wide-angle lens needs, since the distortion lives at the edges.
Conventions match twinrobo.geometry.CameraIntrinsics and OpenCV: pixel (0, 0)'s
center is 0; distortion is OpenCV's k1 k2 p1 p2 k3 on normalized coordinates,
which is what a catalog camera.yaml stores (calibration.distortion.model: opencv).
Board
dataclass
¶
Board(squares_x: int = 11, squares_y: int = 8, square_m: float = 0.015, marker_m: float = 0.011, dictionary: str = 'DICT_5X5_100')
A ChArUco calibration board.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
squares_x
|
int
|
Squares across. |
11
|
squares_y
|
int
|
Squares down. |
8
|
square_m
|
float
|
Printed side of a chessboard square, in meters (measure the print). |
0.015
|
marker_m
|
float
|
Printed side of an ArUco marker, in meters. |
0.011
|
dictionary
|
str
|
OpenCV ArUco dictionary name. |
'DICT_5X5_100'
|
image
¶
The board as a uint8 image for printing (print it flat, then measure a square).
GeometryResult
dataclass
¶
GeometryResult(width: int, height: int, fx: float, fy: float, cx: float, cy: float, k1: float, k2: float, p1: float, p2: float, k3: float, rms_px: float, views: int, corners: int, per_view_rms_px: list[float] = list())
Fitted intrinsics and distortion at the images' resolution.
rms_px: reprojection error over all corners; below ~0.3 px is a good
calibration, above ~1 px usually means blurred or mis-measured views.
scaled
¶
scaled(width: int, height: int) -> GeometryResult
The same camera read out at width x height (e.g. calibrated on a binned mode).
Distortion acts on normalized coordinates, so only the intrinsics scale.
fov_deg
¶
Pinhole field of view (before distortion), horizontal and vertical.
to_spec
¶
The calibration.intrinsic / calibration.distortion blocks of a camera.yaml.
StereoResult
dataclass
¶
StereoResult(translation_m: tuple[float, float, float], rotation_rpy_deg: tuple[float, float, float], baseline_m: float, rms_px: float, views: int)
The right eye's pose in the left eye's camera frame (a module's eyes.right).
translation_m and rotation_rpy_deg are in TwinRobo's camera frame (x right,
y up, looking along -z), as module.yaml stores them.
detect
¶
detect(image: ndarray, board: Board)
ChArUco corners of one image: (object_points [N, 3] m, image_points [N, 2] px, ids).
None if fewer than 6 corners are found (too few to constrain a view).
calibrate
¶
calibrate(images: list[ndarray], board: Board, fix_k3: bool = False, min_views: int = 5) -> GeometryResult
Fit intrinsics and k1 k2 p1 p2 k3 from ChArUco images of one camera.
Capture 15-40 views: the board filling the frame and in every corner, tilted up
to ~45 degrees, at the working distance, with the camera's own focus. fix_k3
helps narrow lenses, where k3 is poorly constrained.
calibrate_stereo
¶
calibrate_stereo(left: list[ndarray], right: list[ndarray], board: Board, left_geometry: GeometryResult, right_geometry: GeometryResult) -> StereoResult
The right eye's pose from simultaneous ChArUco pairs (each eye calibrated first).
load_images
¶
Images as arrays (grayscale or RGB), in the given order.
sharpness
¶
Sharpness from slanted edges (ISO 12233 style), and the lens focus that explains it.
A slanted edge is a straight dark/bright boundary tilted a few degrees off the pixel grid. Every row samples the edge at a slightly different sub-pixel phase, so projecting all pixels onto the edge normal gives the edge spread function (ESF) at 4x the pixel rate. Its derivative is the line spread function (LSF), and the LSF's Fourier magnitude is the MTF. MTF50, the frequency where contrast halves, is the usual single-number sharpness.
fit_focus compares measured MTF curves at known object distances with the lens
model (DeepLens) refocused to candidate distances, and returns the focus distance
that explains the captures best: the one number of the blur model a catalog entry
cannot take from a datasheet when the camera's focus is fixed and unpublished.
EdgeMTF
dataclass
¶
MTF of one slanted edge. freq in cycles per pixel (Nyquist = 0.5).
EdgeMeasurement
dataclass
¶
EdgeMeasurement(distance_m: float, field: tuple[float, float], mtf: list[float], vertical: bool = True, source: str | None = None)
A measured edge: where it was, and its MTF.
field: the ROI center in DeepLens' normalized field coordinates ([-1, 1],
+y up); distance_m: the target's distance from the camera; mtf: the
edge's MTF at FREQS, in cycles per pixel of the spec's resolution.
mtf_at
¶
First frequency where mtf falls to level (linear interpolation).
slanted_edge_mtf
¶
slanted_edge_mtf(roi: ndarray, oversample: int = 4) -> EdgeMTF
MTF of the single slanted edge in roi (grayscale or RGB, linear or sRGB).
The ROI should hold one straight edge, tilted about 2-15 degrees from vertical or horizontal, with some flat dark and bright area on both sides. Use linear (not gamma-encoded) pixel values when available; sRGB images overstate MTF a little.
psf_mtf
¶
MTF of a sampled PSF [ks, ks] across a vertical (or horizontal) edge.
sample_mtf
¶
An MTF curve resampled at at (0 beyond the measured range).
model_mtf
¶
model_mtf(optics, distance_m: float, edges: list[EdgeMeasurement]) -> list[ndarray]
The lens model's MTF (green) at FREQS for edges at one distance, at its current focus.
fit_focus
¶
fit_focus(optics, measurements: list[EdgeMeasurement], coarse: int = 15) -> FocusFit
The focus distance at which the lens model's MTF best matches the measured edges.
optics is a twinrobo.optics.deeplens.DeepLensOptics; it is refocused while
fitting and left at the best focus. The search is in diopters (1/m), 10 cm to
infinity: a coarse grid, then a golden-section refinement around its best point.
Measure edges at several distances in front of and behind the expected focus: one
distance cannot tell near from far focus.
vignetting
¶
Relative illumination (vignetting) from flat-field captures.
Point the camera at a uniformly lit, featureless surface (a diffuser over the lens, a light panel or an evenly lit white wall), defocus if needed, and average a few frames at a mid-level exposure. The falloff towards the corners is the camera's relative illumination: lens vignetting, cos^4 falloff and the sensor's chief-ray angle response together.
Vignetting
dataclass
¶
Vignetting(a2: float, a4: float, a6: float, corner: float, rms_residual: float, width: int, height: int)
Radial relative illumination I(r) = 1 + a2 r^2 + a4 r^4 + a6 r^6.
r is the distance from the image center over the half-diagonal (1 at the
corners); corner is I(1).
flat_field
¶
flat_field(images: list[ndarray], black_level: float = 0.0, saturation: float | None = None) -> Vignetting
Fit relative illumination to flat-field images (averaged; linear values preferred).
The center is normalized to 1. Pixels at or above saturation (default: the
integer type's maximum for integer images, none for float images) are ignored;
keep the exposure below clipping.
spec_writer
¶
Write calibration results into a catalog entry: a measured camera.yaml / module.yaml.
measured_camera_yaml
¶
measured_camera_yaml(spec_path: str | Path, geometry=None, focus_m: float | None = None, vignetting=None, notes: str = '') -> str
The camera.yaml text of spec_path updated with measured values.
geometry (GeometryResult, rescaled to the spec's resolution if needed),
focus_m (from fit_focus) and vignetting (Vignetting) replace the
corresponding fields; validation.status becomes measured. The original
header is kept and a "Measured" section is appended to it. Inline comments
below the header are not preserved.
measured_module_yaml
¶
The module.yaml text with the right eye's measured pose (StereoResult).