Skip to content

Camera–IMU calibration

Camera–IMU calibration is a two-step workflow in CamCalib: calibrate the cameras first, then feed that result into the cam-IMU solve. Pinning intrinsics that a board sequence already constrained well keeps them from being traded against the lever arm.

Run a camera-only calibration on a board sequence (Camera calibration):

Terminal window
camcalib calibrate -c camcalib_cam0_ds.yaml

Its output_file becomes the cam-IMU config’s camera_input.path.

cam_imu.yaml
input:
format: ros1 # ros1 | ros2 | euroc
path: /data/dataset-calib-imu1.bag
imu_topics: ["/imu0"] # first entry is the base IMU
window_mode: manual
t0: 3.0
window: 45.0
cache_dir: /tmp/camcalib_detcache
board: {board_type: apriltag, rows: 6, cols: 6, tag_size: 0.088, tag_spacing: 0.3, tag_family: t36h11}
camera_input:
path: /tmp/calib_ds_cam0.yaml # from step 1
free_intrinsics: false
estimate_readout: false
solve:
state_hz: 30.0
max_iterations: 40
imu_intrinsics: dw_da
imu_intrinsics_model: kalibr # kalibr | rpng | calyx
corner_subsample: top_left
output:
path: /tmp/cam_imu.json
Terminal window
camcalib cam-imu -c cam_imu.yaml

Solves camera–IMU extrinsics T_CtoI, time offset td, rolling-shutter readout, IMU intrinsics, and biases — for single or multi cam/IMU rigs, and for all six distortion models.

Pose, velocity, and bias state nodes are placed at state_hz, which defaults to 30 Hz. A study across MIC, TUM-VI raw, and T265 found:

  • On dynamic-motion rigs, reprojection error, lever arm, and td all alias at low rates and plateau around 30 Hz.
  • On gentle calibration sequences it is flat — the rate does not matter.
  • The stereo baseline and IMU intrinsics/biases are rate-invariant either way.

So: leave it at 30 unless you are on a gentle sequence and want the speed, and suspect it first if td or the lever arm look wrong on a dynamic rig.

Batch: a whole dataset family from one config

Section titled “Batch: a whole dataset family from one config”

List sequences under input.bags instead of a single path:

cam_imu_batch.yaml
input:
format: ros1
bags:
- /data/seq01.bag
- /data/seq02.bag
- /data/seq03.bag

Each is calibrated into a discoverable bag<NN>/cam_imu.json. ROS1 bags and EuRoC mav0 directories both work, each inheriting the config’s format. This is how the repeatability study is run — the spread of extrinsics and intrinsics across sequences is a far better accuracy signal than any single run’s residual.

Two options exist for accelerometers you cannot trust:

Gyro-only — calibrate an auxiliary IMU from its gyro alone (extrinsic rotation, td, Dw, gyro bias; lever, Da, Tg pinned):

cam_imu.yaml
input:
imus:
- {} # base IMU — needs the accel
- {gyro_only: true} # aux

Not allowed on the base IMU, which needs the accelerometer for gravity and metric scale.

Estimate t_ga — solve the base IMU’s internal gyro-vs-accel timing offset:

cam_imu.yaml
solve:
estimate_t_ga: true

Both target the same failure: a driver that interpolates the accelerometer to the gyro rate injects a timing offset that corrupts td. The T265 (Bosch BMI055, 62.5 Hz accel interpolated to 200 Hz) is the canonical case — it measures t_ga ≈ −6.2 ms, stable to ~0.2 ms across bags. Details in IMU model.

Do not stop at the residual:

Terminal window
camcalib roundtrip -c cam_imu.yaml

Round-trip validation applies the calibration to its own bag and recalibrates — the check that catches a good-looking, wrong answer.