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.
1. Calibrate the cameras
Section titled “1. Calibrate the cameras”Run a camera-only calibration on a board sequence (Camera calibration):
camcalib calibrate -c camcalib_cam0_ds.yamlIts output_file becomes the cam-IMU config’s camera_input.path.
2. Write the cam-IMU config
Section titled “2. Write the cam-IMU config”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.json3. Run
Section titled “3. Run”camcalib cam-imu -c cam_imu.yamlSolves 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.
state_hz is the knob that matters
Section titled “state_hz is the knob that matters”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
tdall 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:
input: format: ros1 bags: - /data/seq01.bag - /data/seq02.bag - /data/seq03.bagEach 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.
Awkward IMUs
Section titled “Awkward IMUs”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):
input: imus: - {} # base IMU — needs the accel - {gyro_only: true} # auxNot 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:
solve: estimate_t_ga: trueBoth 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.
4. Validate
Section titled “4. Validate”Do not stop at the residual:
camcalib roundtrip -c cam_imu.yamlRound-trip validation applies the calibration to its own bag and recalibrates — the check that catches a good-looking, wrong answer.