Skip to content

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.

SensorRateDetail
cam0, cam120 Hz544×640 grayscale, hardware-synced
cam2 (SA16 7.3 only)4.29 Hzrolling shutter, tr estimated
IMU400 Hztimestamp, omega_xyz, alpha_xyz

Board: AprilGrid 6×6, tag_size 0.055 m, tag_spacing 0.3, family t36h11.

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:

Terminal window
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.

We calibrate in two stages: camera-only first, then camera-IMU. In stage 2, td is the camera–IMU time offset.

Terminal window
# 1. Camera-only — intrinsics + stereo extrinsics
mvis cam -c examples/configs/looperrobotics/cam_only.yaml
# 2. Camera-IMU — T_CtoI, td, IMU intrinsics and biases, intrinsics pinned
mvis cam-imu -c examples/configs/looperrobotics/cam_imu.yaml

The camera-IMU stage uses window_mode: motion. It picks the most-exciting 80 s of the 96.6 s log on its own.

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

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.

ResultValue
cam2 readout tr33.3 ms — 30.6 µs/line × 1088 rows
tr model-dependenceequi 33.30 ms vs radtan 33.77 ms — essentially none
cam2 reprojectionequidistant 2.02 px vs radtan 7.57 px
cam0 / cam1 reprojection0.78 / 0.76 px
Baseline98.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.

Two ~51 s stereo recordings, with the IMU’s oversampling ratio changed to 4.

ResultValue
Reprojection0.215 / 0.216 px — the cleanest looper solves
Baseline99.24 / 99.26 mm
t_gacollapsed −5.19 → −1.0 ms
Camera tdshifted +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.

ResultValue
Reprojection0.221 / 0.210 px — ties OSR4, on more excited data
t_ga−1.536 / −1.511 ms
Resampling needednone
Code changes needednone

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.

We validate every campaign end to end. apply-calib works on ROS1 bags, so we first convert the mav0 tree to a bag:

Terminal window
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-corrected

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.

ItemValue
Configsexamples/configs/looperrobotics/ — cam_only.yaml, cam_imu.yaml, sa16_73_*, osr4/, data_sync/
Prepbuild_mav0.py (--rotate cam2=cw for SA16 7.3)
Mergemerge_cam_only.py
BoardAprilGrid 6×6, 0.055 m, spacing 0.3, t36h11
Canonical modelequidistant
Resultsresults/looperrobotics/{sa16,sa16_73,osr4,data_sync}/