Skip to content

Camera calibration

Camera-only calibration: intrinsics, distortion, extrinsics, time offsets, and rolling-shutter readout, solved jointly. This is also step one of camera–IMU calibration, which consumes the result.

AprilGrid target ArUco target
Boardboard_typeRequired fields
AprilGridapriltagrows, cols, tag_size (m), tag_spacing (ratio), tag_family
Chessboardchessboardrows, cols (inner corners), square_size (m)
ArUcoarucorows, cols, marker_size, marker_separation, aruco_dict

AprilGrid follows the Kalibr 2-bit border convention, and Kalibr target YAMLs load directly via apriltag_board.from_kalibr_yaml.

Fill the frame and cover the image to its corners — distortion is only constrained where tags were actually seen. Vary distance and angle; avoid motion blur.

Six input formats, all streaming one image at a time so memory stays bounded regardless of dataset size:

config.yaml
cameras:
- camera_id: 0
source_type: images # a folder, sorted by filename
source_path: /path/to/cam0_images
config.yaml
cameras:
- camera_id: 0
source_type: rosbag_topic # ROS1 — several cameras from one bag
source_path: /path/to/data.bag
topic: /cam0/image_raw
- camera_id: 1
source_type: rosbag_topic
source_path: /path/to/data.bag
topic: /cam1/image_raw
config.yaml
cameras:
- camera_id: 0
source_type: euroc # EuRoC / ASL — TUM-VI, Monado SLAM
source_path: /path/to/cam0

Also available: timestamped_images (folder + CSV) and ros2bag.

config.yaml
distortion_model: equidistant # radtan | equidistant | fov
# double_sphere | eucm | omni_radtan

radtan for normal lenses, equidistant for fisheye. Models can be mixed across cameras in one rig. The trade-offs — and the wide-FoV divergence to watch for — are in Camera model.

Terminal window
camcalib calibrate --config config.yaml

Worth knowing, because the failure messages name these stages:

StageWhat happens
DetectionAdaptive multi-scale AprilTag — probes scale 1.0, then up to 2 adaptive scales by observed module size; subpixel refinement at original resolution
Quality gatingCorner subsample, pooled-pose filter (≥20% tags across cameras), per-corner ≥5 views, per-camera ≥3 views, ≥6 tags/frame
1Per-camera init — vanishing-point focal + RANSAC PnP board poses + sanity check
2Per-camera GTSAM refinement, with rolling-shutter readout if enabled
3Trajectory alignment — extrinsics via RANSAC, time offsets via dual PnP + homography cross-correlation
4Joint multi-camera factor graph — the unified factor, all parameters
5Outlier-driven rerun with pathology guardrail

corner_subsample defaults to top_left: one corner per tag instead of four, 3–5× faster for under 0.1% quality drift, validated across eight dataset families. It auto-falls back to all on sparse data.

config.yaml
corner_subsample: top_left # top_left | two_diag | all

factor_hz subsamples frames if a long recording is slowing you down.

Two YAMLs — *_calib.yaml to deploy, *_full.yaml with board poses to replay — plus a self-contained HTML report. Check the motion-excitation grade before the reprojection error: see Evaluation & metrics.

Add entries to cameras: and set reference_camera. There is no separate stereo step — all cameras enter one graph, so a five-camera rig stays consistent instead of accumulating pairwise error. Enable optimize_time_offset: true for unsynchronised cameras.

Add an IMU: Camera–IMU calibration.