Detect calibration drift (calibrex drift)¶
calibrex check judges a bag against a calibration you hand it. calibrex drift
answers a different question when you have several recordings of the same
rig (different days, after a service, after a bump): did the extrinsic
calibration change between them? No reference calibration is needed.
calibrex drift day1/ day2/ day3/ --output drift/ # every pair with an estimator
calibrex drift day1/ day2/ --pairs imu-lidar --max-duration-s 120 --output drift/ --html drift.html
calibrex drift a/ b/ c/ --vehicle-frame base_link --topic-kind /oxts/twist=wheel --output drift/
Give the bags in recording order. The flags of calibrex estimate apply
(--tf prior, --frame-map, --vehicle-frame, --topic-kind, --pairs,
--max-duration-s, --cache-dir, --no-cache, ...).
What it does¶
- Every bag is estimated with the machinery of
calibrex estimate(the per-bagslac.bag_estimate/v0.1artifact is written todrift/<bag-name>/bag_estimate.json). The estimator cache is shared withcheckandestimate, so re-runningdrifton bags it has seen costs seconds. - For each pair and each axis that the data observed in at least two bags
(an axis that was
unobservable,control_not_detectedor not estimated never enters a test), with estimatesx_iand standard deviationss_i:- pairwise difference
d_ij = x_i - x_jagainst the tolerancemax(3 * sqrt(s_i^2 + s_j^2), floor). The floor is the minimum detectable change of the axis type, the same policy ascheck: 0.5 deg rotation, 1.5 deg rotation forimu-lidarestimated on rigid scans (no deskew), 2 cm translation (--rotation-floor-deg,--rigid-scan-rotation-floor-deg,--translation-floor-m,--sigma-k); - a chi-square homogeneity test of the inverse-variance weighted mean
(
--chi2-alpha, default 0.01); - the axis is
driftwhen a pair exceeds its tolerance and the chi-square test rejects homogeneity,stablewhen not, andinconclusivewhen fewer than two bags observed it.
- pairwise difference
- Clock offset. The per-pair time offset of each bag's estimate (
time_offsetof theslac.bag_estimate) is compared with the same two tests, as the rowdtin ms. Only a bag whose offset the estimator marksestimated(with a finite std) enters; anunobservableoffset is skipped like an unobserved axis. The floor is per pair type: 1 ms forcamera-imu, 2 ms for every other pair (--time-offset-floor-mssets one floor for all).lidar-wheel_odometryis never compared: its offset sits on the odometry sample lattice (KITTI's OXTS proxy gives 66 to 70 ms) and is not a clock-offset measurement; the output says so. - Pair verdict:
driftif any axis or the clock offset drifts, elsestableif any was tested, elseinconclusive. Overall verdict: the worst pair (stable < inconclusive < drift). Exit status 1 ondriftby default;--fail-on inconclusive|neverchanges it. - Which bag moved. With three or more bags the deviating bags are those whose removal restores consistency (fewer than half the bags may be removed; if the bags split instead, nobody is blamed). With two bags a difference cannot be attributed, and the output says so. For each deviating bag the artifact gives its offset from the consensus per axis and, when all three rotation axes were observed, the angle of the rotation between the two estimated rotations, and its clock-offset change. The attribution uses the axes and the clock offset of the pair together. When only the clock offset differs the next steps say so (check the time synchronisation of that recording), because re-calibrating the extrinsic would not help.
Reading the output¶
imu-lidar: drift T_livox_lidar_livox_imu
axis stadtgarten_seq1 ... construction_seq2 sg1_z1p0 max|diff| min detectable status
roll -0.218 +- 0.058 ... -0.151 +- 0.053 -0.219 +- 0.058 0.176 0.500 stable
pitch +0.114 +- 0.058 ... +0.081 +- 0.050 +0.112 +- 0.058 0.123 0.500 stable
yaw -0.229 +- 0.069 ... -0.106 +- 0.073 -1.229 +- 0.069 1.123 0.500 drift
sg1_z1p0 vs the others: roll -0.038 deg, pitch +0.047 deg, yaw -1.047 deg; rotation 1.05 deg
deviating bag(s): sg1_z1p0
... columns are elided here.)
min detectable is the smallest pairwise tolerance among the bags that
observed the axis: a change smaller than that cannot be flagged, so a
stable verdict means "no change larger than this was found", not "nothing
changed". The numbers are the estimators' own standard deviations (analytic or
block jackknife), which are not guaranteed to be calibrated; the floor is what
protects against over-trusting a small std. Rotation axes are compared as the
estimators report them (rotation-vector components in degrees about the parent
frame's axes), which is exact enough for the sub-degree to few-degree changes
this is meant for.
drift/calibration_drift.json is slac.calibration_drift/v0.1
(schema): the bag references (path, digest, the
per-bag estimate's path and sha256, cache hits and runtime), per pair and axis
every bag's observation and the tests, the thresholds, the next steps and the
provenance. --html FILE writes a self-contained report with the per-axis
estimates and the minimum detectable change drawn as a band.
Known-bad control: a shifted IMU clock¶
tools/shift_stamps_in_bag.py copies a bag and adds a known offset to the header stamp of
every message of the chosen topics, which is what a changed clock offset (driver update, PTP
or hardware-timestamping change) does to a sensor's stamps:
python tools/shift_stamps_in_bag.py my_bag/ my_bag_dt/ --topic /alphasense/imu --shift-ms 2 \
--max-duration-s 130
calibrex drift my_bag/ other_bag/ third_bag/ my_bag_dt/ --pairs camera-imu --output drift/
The estimators read the header stamp of Imu, Image and PointCloud2 messages (the bag's
log time only when the header stamp is zero), so that is what is shifted; the log (receive)
time is left alone unless --shift-log-time is given, and per-point time fields of a cloud
are not touched. Shifting the IMU by +d moves the estimated camera-imu offset by +d.
Known-bad control: a remounted IMU¶
To check that the detector sees a real change, tools/rotate_imu_in_bag.py
copies a bag and rotates every IMU angular_velocity and linear_acceleration
vector by a known rotation, which is exactly what a physically remounted IMU
measures:
python tools/rotate_imu_in_bag.py my_bag/ my_bag_remounted/ --axis 0 0 1 --angle-deg 1.0 \
--max-duration-s 130 # optional: keep only the first 130 s (a small copy)
calibrex drift my_bag/ other_bag/ my_bag_remounted/ --pairs imu-lidar --output drift/
Results on real data, including the smallest rotation that was detected and the false-alarm behaviour on unmodified recordings, are on Drift on real data.
Limits¶
- The bags must be of the same rig: frames and sensors are matched by pair and frame names, and a pair whose frames differ between bags is not compared.
- Only axes the estimators observe are compared. Short or low-excitation
recordings leave most axes unobservable (a KITTI drive constrains one or two
of the vehicle pair's axes), and the pair is then
inconclusiverather thanstable. - Rotation axes are compared as rotation-vector components. A rotation near a half turn (an optical
frame to a LiDAR frame,
camera-lidar) has two forms that differ in every component, sodriftre-expresses each bag's vector in the form nearest the first bag's before comparing. Axis values of large rotations remain approximate: an error about one parent axis spreads over the components. - Intrinsics are not compared. Time offsets are compared only where the estimator reports
one as
estimated; the floors (1 mscamera-imu, 2 ms otherwise) were set from the recording-to-recording scatter of the two datasets on the evaluation page, not from a larger study. - A real change smaller than the minimum detectable change is not detected, and an underestimated std on a dataset the estimator handles poorly can produce a false alarm; the floors exist for the second case, and the evaluation page reports what was seen.