Skip to content

EuRoC Consistency Gate: the transformation graph

For each sequence, track-noise level S, seed and R (matched to S, or the filter’s default): fit a smooth truth through the ground truth, synthesize the IMU and the feature tracks from it with known noise, run the backend under test, score its trajectory and pose consistency against that truth; then judge the grid. Every backend runs with the same S6d gate.

The EuRoC Consistency Gate transformation graph: boxes are transformations (green implemented, amber partial, blue planned, red missing; dashed optional), parallelograms are sensor inputs and outputs, the cylinder is the shared filter state, notes are the data structures passed between transformations, and a double-headed arrow is a transformation that updates a structure in place.

  • kim1995 — M.-J. Kim, M.-S. Kim and S. Y. Shin, “A General Construction Scheme for Unit Quaternion Curves with Simple High Order Derivatives”, SIGGRAPH 1995 (the cumulative B-spline on rotations).
  • sommer2020 — C. Sommer, V. Usenko, D. Schubert, N. Demmel and D. Cremers, “Efficient Derivative Computation for Cumulative B-Splines on Lie Groups”, CVPR 2020 (angular velocity and acceleration of a cumulative spline).
  • mueggler2018 — E. Mueggler, G. Gallego, H. Rebecq and D. Scaramuzza, “Continuous-Time Visual-Inertial Odometry for Event Cameras”, IEEE T-RO 34(6), 2018 (an IMU synthesized from a spline trajectory).
  • barshalom2001 — Y. Bar-Shalom, X. R. Li and T. Kirubarajan, “Estimation with Applications to Tracking and Navigation”, Wiley 2001 (NEES, NIS and their χ² bands).
  • zhang2018 — Z. Zhang and D. Scaramuzza, “A Tutorial on Quantitative Trajectory Evaluation for Visual(-Inertial) Odometry”, IROS 2018 (4-DoF alignment, ATE, RPE).

Each has the shape (state, inputs, params) → (state′, diagnostics).

StageNodeDoesReads → writesPaperStatusCodeBench
G1fit_splineground-truth poses → a cumulative cubic B-spline through control poses sampled from themGroundTruth → TruthSplinekim1995, the cumulative basis; derivatives from sommer2020implementedtools/include/branes/tools/gt_spline.hpp—
G2synth_imuthe spline at the real IMU timestamps → ω and specific force in the body frame, plus noise and bias walks at the configured densitiesTruthSpline, NoiseModel → ImuStreammueggler2018, the IMU from a spline trajectoryimplementedtools/include/branes/tools/gt_spline.hpp—
G3synth_tracksthe camera pose from the truth at each image time → projections of seeded landmarks, plus N(0, S²)TruthSpline, CameraCalib, NoiseModel → TrackStreamzhang2018, ideal measurements for a controlled evaluationimplementedtools/include/branes/tools/ideal_tracks.hpp—
G4run_filterthe IMU and the tracks → the estimate, with R matched to S or the default, Q from NoiseModel, and the shipped χ² gate for every backendImuStream, TrackStream, NoiseModel → Estimatebarshalom2001, a filter told the true noise is consistentimplementedtools/src/vio_pipeline.cpp—
G5scorethe estimate against the truth → window ATE ratio, pose NEES split, world-yaw σ, NISEstimate, TruthSpline → Scorezhang2018, 4-DoF alignment and ATEimplementedtools/include/branes/tools/pose_consistency.hpp—
G6verdictevery run’s score → the three conditions (no divergence, matched beats default, no yaw collapse) and the gate’s verdictScore → GateVerdictbarshalom2001, consistency tests over Monte-Carlo runsimplementedscripts/euroc-consistency.sh—

fit_spline (G1, Fit the truth)

  • Pre: the ground truth covers the sequence; no gap longer than one knot interval
  • Post: pose, ω and a are defined on the span; ω and a agree with finite differences of the pose; it stays within a bound of the ground truth
  • Test: tests/tools/gt_spline.cpp “gt spline: velocity, acceleration and body rate are the derivatives of its pose…”
  • Test: tests/tools/gt_spline.cpp “gt spline: it stays close to the ground truth it was built from…”

synth_imu (G2, Synthesize the IMU)

  • Pre: the IMU timestamps lie inside the spline’s span
  • Post: noise-free and integrated by the shipped S2 from both samples of each interval, it closes on the spline within a tenth of 0.5 px over a clone window; the noise statistics match the densities
  • Test: tests/tools/gt_spline.cpp “gt spline: S2 closure, the shipped S2 within what the camera resolves (#616)…”
  • Test: tests/tools/gt_spline.cpp “gt spline: the synthesized noise has the configured densities…”

synth_tracks (G3, Synthesize the tracks)

  • Pre: the truth covers the image time
  • Post: noise-free, every observation is the exact projection; the noise is the injected noise only
  • Test: tests/tools/ideal_tracks.cpp “ideal tracks are projections of fixed landmarks…”
  • Test: tests/tools/ideal_tracks.cpp “ideal tracks add the noise and only the noise…”
  • Test: tests/tools/ideal_tracks.cpp “ideal tracks can draw the noise in normalized coordinates…”

run_filter (G4, Run the backend under test)

  • Pre: R and Q are exactly the injected noise when matched; every backend uses the same S6d gate line
  • Post: converged runs have NIS within its χ² band
  • Test: tests/tools/pose_consistency.cpp “Pose consistency: errors drawn from the covariance give an ANEES…”

score (G5, Score the run)

  • Pre: the estimate and the truth overlap in time
  • Post: the gauge is anchored once; errors are taken in the filter’s own convention
  • Test: tests/tools/pose_consistency.cpp “Pose consistency: the gauge anchor absorbs yaw and position…”

verdict (G6, Judge the grid)

  • Pre: every run scored, or marked diverged or unavailable
  • Post: an unavailable result cannot be recorded
EdgeKindContentsTypeInvariants (✓ has a validator)
GroundTruthinputthe EuRoC state_groundtruth_estimate0 poses (Vicon or Leica, 200 Hz)tools/include/branes/tools/ideal_tracks.hpp IdealTracks✓ timestamps strictly increasing
NoiseModelinputthe gyro and accelerometer noise densities and bias random walks (VioConfig’s), the track pixel σ S, and the seedsdk/include/branes/sdk/vio_backend.hpp VioConfig✗ the filter’s Q and the synthesis use the same densities; matched R equals S
CameraCalibinputEuRoC cam0 intrinsics, distortion and camera↔IMU extrinsictools/include/branes/tools/euroc_cam0.hpp euroc_cam0✓ R_imu_cam ∈ SO(3)
TruthSplinedataa uniform cumulative cubic B-spline on SO(3) × ℝ³ with control poses at ground-truth samples every 0.05 s; pose, angular velocity and acceleration at any time inside its spantools/include/branes/tools/gt_spline.hpp PoseSpline✓ C² in position and rotation; ω and a match finite differences of its pose; ✓ stays within a stated bound of the ground truth (it approximates, it is not fitted)
ImuStreamdataat the real IMU timestamps, ω_m = ω + b_g + n_g, a_m = Rᵀ(p̈ − g) + b_a + n_a, with b_g, b_a random walks from zerotools/include/branes/tools/gt_spline.hpp synthesize_imu✓ noise-free, integrated by the shipped S2 from both samples of each interval, it closes on the spline within a tenth of 0.5 px over a clone window, in rotation and position (see s2_closure, #616); ✓ the noise matches NoiseModel’s densities (per-axis variance, per-sample whiteness)
TrackStreamdataper image time, pixel observations of fixed landmarks seen from the truth’s camera pose, plus N(0, S²) per axistools/include/branes/tools/ideal_tracks.hpp IdealTracks✓ noise-free, each observation is the exact projection of its landmark; ✓ the added noise is N(0, (S/fx)²) per axis in normalized coordinates and nothing else
Estimatedatathe backend’s trajectory (TUM) and per-frame pose covariance in its own error conventiontools/src/vio_pipeline.cpp write_tum✗ finite; one pose per tracked frame
Scoredatawindow 4-DoF ATE ÷ OpenVINS mono, the pose NEES split, the world-yaw σ over the run, NIStools/include/branes/tools/pose_consistency.hpp PoseConsistencySummary✗ scored against TruthSpline, not the raw ground truth; ✗ a run with no trajectory counts as diverged; one that cannot be scored is unavailable
GateVerdictoutputPASS, FAIL or UNAVAILABLE per condition, and the recorded resultsscripts/euroc-consistency.json—
  • NoiseModel precision: double throughout; the bench is not a precision study

The driver is scripts/euroc-consistency.sh.

on run (sequence × S × seed × R):
fit_spline
synth_imu
synth_tracks
run_filter
score
on grid complete:
verdict
InvariantCheckStatus
the median normalized NIS over the matched runs is ≈ 1 (the bench’s noise model is true)scripts/euroc-consistency.shholds — 1.00 for both backends at f1551cc (1.03 at c6b87ec before #616; 1.65–4.3 with the real IMU and the raw ground truth)
every sequence’s matched runs have normalized NIS within the χ² band for their dofscripts/euroc-consistency.shholds — 1.00 on every sequence for both backends at f1551cc. Before #616 it was 1.1–1.9 on V1_02, V1_03, V2_02, V2_03: S2’s sample-hold skew, an estimator defect
the shipped S2, fed the synthesized IMU noise-free, closes on the truth within what the camera resolves (a tenth of 0.5 px over a 0.55 s clone window: 1.1e-4 rad, 0.22 mm at 2 m), at second order (bound 1.09e-4 rad rotation, 0.22 mm position at 2 m, over a 0.55 s clone window, from what run_filter resolves: the backend’s update resolves a clone pose only to the track noise, 0.5 px at fx = 458.654 (1.09e-3 rad); propagation error must stay a tenth of that over the clone window so it never competes with the measurement, and the same angle at a 2 m landmark depth is 0.22 mm)tests/tools/gt_spline.cpp “gt spline: S2 closure, the shipped S2 within what the camera resolves…”holds — since #616, S2 integrates each interval from both samples by RK4: 0.27 of the rotation bound and 0.10 of the position bound at 200 Hz on a 1.3 rad/s roll/pitch flight, converging at second order. Before it, the end-sample hold missed the rotation bound 28× at first order; the companion tests pin that, and that centring the input fixes the rotation while the integrator fixes the position
every backend runs with the same S6d gate linescripts/euroc-consistency.shholds — since #608 both use the χ² quantile; before it the R-IEKF used 16 per dof
each condition’s result changes with the estimator, not with the gate’s line—holds — at c6b87ec each remaining failure was rerun with the gate at 5 per dof: one divergence per backend vanished, the others persisted, and condition 2’s fast-sequence loss was #616’s S2 skew. At f1551cc condition 2 passes for both; both backends diverge on the same two noise draws, from the shared initializer’s seed (#609); the MSCKF’s condition 3 is S6’s yaw leak. Conditions 1 and 2 still depend on the noise draw at three seeds (#613)

What pipeline_graph.py gaps reports for this spec (12 items):

  • edge ‘NoiseModel’: no validator for “the filter’s Q and the synthesis use the same densities; matched R equals S”
  • edge ‘Estimate’: no validator for “finite; one pose per tracked frame”
  • edge ‘Score’: no validator for “scored against TruthSpline, not the raw ground truth”
  • edge ‘Score’: no validator for “a run with no trajectory counts as diverged; one that cannot be scored is unavailable”
  • node ‘fit_spline’: no stage bench
  • node ‘synth_imu’: no stage bench
  • node ‘synth_tracks’: no stage bench
  • node ‘run_filter’: no stage bench
  • node ‘score’: no stage bench
  • node ‘verdict’: no stage bench
  • node ‘verdict’: no unit tests
  • system invariant ‘verdict_attributable’: no check