Looper SA16
This page shows how we calibrate the looperrobotics SA16 rig across four recording campaigns. It is the example closest to production use. The rig is a commercial stereo camera module, later with a third camera, recorded four different ways over several months.
This dataset pushed MVIS past a single clean sequence into:
- rolling shutter on a third camera
- IMU filter changes
- irregular timestamps
Here the evidence is agreement across campaigns, not a single residual.
The same device has a sensor page with its specs, solved rig and results.
The rig
Section titled “The rig”| Sensor | Rate | Detail |
|---|---|---|
| cam0, cam1 | 20 Hz | 544×640 grayscale, hardware-synced |
| cam2 (SA16 7.3 only) | 4.29 Hz | rolling shutter, tr estimated |
| IMU | 400 Hz | timestamp, omega_xyz, alpha_xyz |
Board: AprilGrid 6×6, tag_size 0.055 m, tag_spacing 0.3, family t36h11.
Getting the data into shape
Section titled “Getting the data into shape”The recordings are not in EuRoC format, but they are close. One prep step
reshapes them into a mav0/ tree. No MVIS code changes were needed for any
campaign:
python examples/configs/looperrobotics/build_mav0.py \ --root '/data/looperrobotics/SA16(96s record)'The script:
- symlinks the images
- writes
data.csv - comments out the IMU header
The IMU CSV column order already matches EuRoC.
The two-stage recipe
Section titled “The two-stage recipe”We calibrate in two stages: camera-only first, then camera-IMU. In stage 2,
td is the camera–IMU time offset.
# 1. Camera-only — intrinsics + stereo extrinsicsmvis cam -c examples/configs/looperrobotics/cam_only.yaml
# 2. Camera-IMU — T_CtoI, td, IMU intrinsics and biases, intrinsics pinnedmvis cam-imu -c examples/configs/looperrobotics/cam_imu.yamlThe camera-IMU stage uses window_mode: motion. It picks the most-exciting
80 s of the 96.6 s log on its own.
Four campaigns
Section titled “Four campaigns”The four campaigns at a glance:
- SA16 (96 s): stereo only, the baseline
- SA16 (7.3): adds a rolling-shutter third camera
- SA16 (OSR4): sets the IMU oversampling ratio to 4
- SA16 (data-sync): uses real hardware timestamps
SA16 (96 s) — the baseline
Section titled “SA16 (96 s) — the baseline”Stereo only. We fit the radtan lens model first, then equidistant. The
baseline (the distance between cam0 and cam1) is 98.42 mm.
SA16 (7.3) — a rolling-shutter third camera
Section titled “SA16 (7.3) — a rolling-shutter third camera”This campaign adds cam2 at 4.29 Hz. cam2 has a rolling shutter, so it reads its
rows one after another. We leave its readout time tr free.
In the table, reprojection is the reprojection error: how far, in pixels, the detected board corners sit from where the model predicts them.
| Result | Value |
|---|---|
cam2 readout tr | 33.3 ms — 30.6 µs/line × 1088 rows |
tr model-dependence | equi 33.30 ms vs radtan 33.77 ms — essentially none |
| cam2 reprojection | equidistant 2.02 px vs radtan 7.57 px |
| cam0 / cam1 reprojection | 0.78 / 0.76 px |
| Baseline | 98.70 mm (96 s: 98.42) |
equidistant is canonical here. It beats radtan by 3.7× on reprojection,
and that is the recurring result on this rig.
SA16 (OSR4) — IMU oversampling ratio 4
Section titled “SA16 (OSR4) — IMU oversampling ratio 4”Two ~51 s stereo recordings, with the IMU’s oversampling ratio changed to 4.
| Result | Value |
|---|---|
| Reprojection | 0.215 / 0.216 px — the cleanest looper solves |
| Baseline | 99.24 / 99.26 mm |
t_ga | collapsed −5.19 → −1.0 ms |
Camera td | shifted +0.4 ms common-mode |
Geometry unchanged, timing shifted. The OSR4 filter changes the IMU’s
internal group delays, differently for gyro and accel. t_ga (the time offset
between the gyroscope and accelerometer channels) and td absorb exactly that.
We confirmed the t_ga value estimator-free with
studies/tga_correlation_lever.py:
- −0.9 to −3.0 ms across bands
- 20–80 Hz agrees with the solver to ~0.1 ms
So it is a measurement of the hardware, not an artefact of the graph. See also Why calibrate t_ga.
SA16 (data-sync) — real hardware timestamps
Section titled “SA16 (data-sync) — real hardware timestamps”Two recordings (41.7 / 44.5 s), with the IMU hardware-synced to the camera trigger. This is the first looper set with non-idealised IMU timestamps:
- ~68 µs jitter
- 114/107 dropped samples
- a timebase running 766/783 ppm slow against the camera
That clock drift is independently observable for the first time. Every earlier set was quantised to exactly 2.5000 ms, so it had a ratio of 20.000000 by construction.
| Result | Value |
|---|---|
| Reprojection | 0.221 / 0.210 px — ties OSR4, on more excited data |
t_ga | −1.536 / −1.511 ms |
| Resampling needed | none |
| Code changes needed | none |
The irregular stream costs nothing. MVIS reads the jittery, gappy, drifting
timestamps as they are. A forced-400 Hz control proves the recorded stamps are
the right ones. Imposing the old regular convention costs 7× in
reprojection and drags td to −15.8 ms.
Round-trip
Section titled “Round-trip”We validate every campaign end to end. apply-calib works on ROS1 bags, so we
first convert the mav0 tree to a bag:
python examples/experiments/convert_mav0_to_bag.py \ --mav0 '/data/looperrobotics/SA16(7.3)/mav0' \ --out '/data/looperrobotics/SA16(7.3)/sa16_73.bag'
mvis roundtrip -c examples/configs/looperrobotics/sa16_73_cam_imu_ros1.yaml \ --out results/looperrobotics/sa16_73/roundtrip --keep-correctedWhat the campaigns together show
Section titled “What the campaigns together show”Agreement across independent recordings is the accuracy claim. Baselines are 98.42, 98.70, 99.24, and 99.26 mm across four recording setups. The setups differ in:
- camera count
- IMU filter
- timestamp regime
The last two match the robot-arm campaign. This agreement is a stronger accuracy claim than any single sub-pixel residual. It is exactly what the batch sweep workflow is built to produce.
Reproducibility
Section titled “Reproducibility”| Item | Value |
|---|---|
| Configs | examples/configs/looperrobotics/ — cam_only.yaml, cam_imu.yaml, sa16_73_*, osr4/, data_sync/ |
| Prep | build_mav0.py (--rotate cam2=cw for SA16 7.3) |
| Merge | merge_cam_only.py |
| Board | AprilGrid 6×6, 0.055 m, spacing 0.3, t36h11 |
| Canonical model | equidistant |
| Results | results/looperrobotics/{sa16,sa16_73,osr4,data_sync}/ |