Skip to content

Right-Invariant MSCKF: the transformation graph

The OpenVINS MSCKF with the nav error taken on SE₂(3), right-invariant (R = Exp(δθ)·R̂, v = Exp(δθ)·v̂ + ν, p = Exp(δθ)·p̂ + ρ). Propagation’s Φ then doesn’t depend on the estimate, so the 4-D unobservable subspace is preserved by construction and no update can gain information along it: the property FEJ approximates in the body-frame filter.

A delta on OpenVINS MSCKF: it changes the nodes and data structures listed under Changes from the base, and keeps the rest.

The Right-Invariant MSCKF 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.

  • mourikis2007 — A. I. Mourikis and S. I. Roumeliotis, “A Multi-State Constraint Kalman Filter for Vision-aided Inertial Navigation”, ICRA 2007.
  • li2013 — M. Li and A. I. Mourikis, “High-precision, consistent EKF-based visual-inertial odometry”, IJRR 32(6), 2013 (MSCKF 2.0, first-estimates Jacobians).
  • geneva2020 — P. Geneva, K. Eckenhoff, W. Lee, Y. Yang and G. Huang, “OpenVINS: A Research Platform for Visual-Inertial Estimation”, ICRA 2020.
  • huang2010 — G. P. Huang, A. I. Mourikis and S. I. Roumeliotis, “Observability-based Rules for Designing Consistent EKF SLAM Estimators”, IJRR 29(5), 2010.
  • barrau2017 — A. Barrau and S. Bonnabel, “The Invariant Extended Kalman Filter as a Stable Observer”, IEEE TAC 62(4), 2017.
  • hartley2020 — R. Hartley, M. Ghaffari, R. M. Eustice and J. W. Grizzle, “Contact-aided invariant extended Kalman filtering for robot state estimation”, IJRR 39(4), 2020 (the right-invariant IMU error dynamics on SE₂(3)).

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

StageNodeDoesReads → writesPaperStatusCodeBench
S0sensor_modelpixel → normalized image point (undistort, unproject)ImageFrame, CameraCalib → NormalizedObsgeneva2020, camera modelsimplementedsdk/include/branes/sdk/msckf/stages/s0_sensor_model.hpptests/tools/stage_bench_s0_s1.cpp “S0 bench: every fixture passes in every type…”
S4trackimage → feature tracks (FAST + KLT), and the tracks that endedNormalizedObs, TrackDB → TrackDBgeneva2020, visual trackingimplementedsdk/include/branes/sdk/msckf/stages/s4_frontend.hpptests/tools/stage_bench_s4_s5.cpp “S4 bench: every fixture passes for every variant…”
S1ainitializean IMU window at rest (static), or a moving window of frames (dynamic VI alignment) → the nav seedImuSample, TrackDB → NavSeedgeneva2020, initializationimplementedsdk/include/branes/sdk/imu_init.hpptests/tools/stage_bench_s0_s1.cpp “S1 bench: static paths pass every fixture…”
S1bseed_statethe seed’s mean as it is (velocity included); the base’s body-frame prior mapped into the right-invariant error, P₀ = J·P_body·Jᵀ with δθ_world = R̂·δθ_body, ν = δv + [v̂]×δθ_world, ρ = δp + [p̂]×δθ_worldNavSeed → FilterStatebarrau2017, the invariant errorimplementedtools/include/branes/tools/invariant_backend_adapter.hpp—
S2propagateone IMU interval (the samples at both of its ends) → the propagated SE₂(3) mean; P ← Φ P Φᵀ + L Lᵀ with Φ independent of the estimateFilterState, ImuSample → FilterStatehartley2020, the right-invariant IMU error dynamicsimplementedsdk/include/branes/sdk/msckf/invariant_propagator.hpptests/tools/stage_bench_s2_s3.cpp “S2 bench: the invariant error maps the body-frame gauge…”
S3augmentthe state → the state plus a clone sharing the nav’s δθ and ρ, P′ = G P GᵀFilterState → FilterStatemourikis2007, §III-B, stochastic cloning, in the invariant errorimplementedsdk/include/branes/sdk/msckf/msckf_invariant_backend.hpp—
S5triangulatean ended track and the invariant clones → the feature position, linear then Gauss-NewtonTrackDB, FilterState → Landmarkmourikis2007, §III-Cimplementedsdk/include/branes/sdk/msckf/invariant_update.hpp—
S6msckf_updatetrack + landmark → the invariant H (nav columns zero) → null-space projection → χ² gate → EKF update, right-invariant retractionFilterState, TrackDB, Landmark → FilterState, UpdateDiagbarrau2017, the invariant measurement updateimplementedsdk/include/branes/sdk/msckf/msckf_invariant_backend.hpp—
S9marginalizethe state → the state without its oldest clone, after fusing every track that touches itFilterState, TrackDB → FilterState, TrackDBmourikis2007, §III-Eimplementedsdk/include/branes/sdk/msckf/invariant_vio_backend.hpp—
S8zero_velocityat rest → v = 0 in the invariant error (H_θ = −[v̂]×, H_ν = I, r = −v̂) and b_g = ω̄; the frame adds no cloneFilterState, ImuSample, TrackDB → FilterStategeneva2020, zero-velocity updateimplemented (optional)sdk/include/branes/sdk/msckf/msckf_invariant_backend.hpp—
S7slam_updatelong-lived features kept in the state as landmarks and updated every frameFilterState, TrackDB → FilterStategeneva2020, SLAM featuresmissing (optional)——
S10online_calibthe state ⊕ camera↔IMU extrinsics; MsckfInvariantBackend::enable_calibration exists, the EuRoC adapter doesn’t call itFilterState, CameraCalib → FilterStategeneva2020, online calibrationmissing (optional)——

sensor_model (S0, Sensor model)

  • Pre: intrinsics valid; the pixel inside the image
  • Post: project ∘ unproject is the identity; the analytic Jacobian matches finite differences
  • Test: tests/sdk/msckf_stages.cpp “S0_sensor_model normalizes a pixel…”

track (S4, Visual front end)

  • Pre: consecutive frames; features detected inside the trackable band
  • Post: tracks inside the image; forward-backward error bounded; ended tracks released in id order
  • Test: tests/sdk/msckf_stages.cpp “S4_frontend ingests observations…”
  • Deviates from the paper: no RANSAC outlier rejection in the front end; the S6d gate carries it
  • Deviates from the paper: the shipped default churns (>50% of new tracks lost on their first frame); the border margin is opt-in (#474, #574)

initialize (S1a, Initialization)

  • Pre: an IMU window that is at rest, or frames with enough excitation and parallax
  • Post: gravity-aligned attitude; the method is recorded
  • Test: tests/sdk/imu_init.cpp “static init recovers gravity…”
  • Test: tests/sdk/msckf_dynamic_init.cpp “the backend bootstraps a moving start via dynamic VI init…”
  • Deviates from the paper: the dynamic path is excitation-limited on EuRoC and has open SfM defects (#254, #499–#501)

seed_state (S1b, Seed the filter in the invariant error)

  • Pre: a successful seed from the base’s initialize
  • Post: the mean equals the seed; P₀ = J·P_body·Jᵀ, positive-definite; the IMU sample in effect at the seed time is held, so propagation resumes at the seed time
  • Test: tests/tools/invariant_backend_adapter.cpp “invariant adapter: the body-to-invariant map matches the exact error…”
  • Test: tests/tools/invariant_backend_adapter.cpp “invariant adapter: initializes through the shipped MSCKF with its seed…”
  • Deviates from the paper: the base’s prior is itself an isotropic σ = 0.1 with no tilt-bias correlation (#609); this node maps it faithfully

propagate (S2, Invariant IMU propagation)

  • Pre: Δt > 0 and bounded
  • Post: R ∈ SO(3); P symmetric PSD; Φ preserves the gauge null space at any state
  • Test: tests/sdk/invariant_propagator.cpp “invariant propagation: Phi preserves the gauge null space at any state…”
  • Test: tests/sdk/invariant_propagator.cpp “invariant propagation: the predicted covariance matches a Monte-Carlo…”
  • Decision — the IMU input over each interval and the mean’s integrator: the base’s: both samples interpolated linearly, integrated by RK4 (#616) over the sample that starts the interval, held (#608 to #616). the mean is the same strapdown physics in either error convention, and one convention for both backends keeps a time-offset state (#332) from learning a backend-specific skew
  • Decision — Φ and L over the interval: unchanged: Φ’s nav block does not depend on the input, and its bias lever arms are taken at the start of the interval over lever arms at the interval’s midpoint. the lever-arm terms move by v·Δt over 5 ms, far below what the update resolves; the gauge test holds at any state

augment (S3, Invariant augmentation)

  • Pre: the state propagated to the image time
  • Post: P grows by 6 with an exact block copy; P PSD
  • Test: tests/sdk/invariant_joint_step_fd.cpp “DIAG FD: augmentation G under rotation…”
  • Test: tests/sdk/msckf_invariant_backend.cpp “invariant backend: propagate + augment + update runs and stays PSD…”

triangulate (S5, Invariant triangulation)

  • Pre: at least two observations from live clones
  • Post: positive depth in every camera; a low-parallax linear solve repaired by refinement
  • Test: tests/sdk/invariant_update.cpp “invariant triangulation: Gauss-Newton refinement repairs…”

msckf_update (S6, Invariant MSCKF update)

  • Pre: the landmark triangulated against the current clones
  • Post: H annihilates the 4-DoF gauge at the actual clone poses; P⁺ ⪯ P⁻; a non-finite δx is rolled back
  • Test: tests/sdk/invariant_update.cpp “invariant update: H annihilates the 4-DoF gauge…”
  • Test: tests/sdk/msckf_invariant_backend.cpp “invariant backend: the marginalized joint update annihilates the 4-DoF gauge…”
  • Test: tests/sdk/invariant_joint_step_fd.cpp “DIAG FD: measurement-update linearization (proj.r vs -H*e)…”
  • Test: tests/sdk/invariant_zero_velocity.cpp “invariant backend: every update reports its NIS, gated or not…”
  • Test: tests/sdk/invariant_zero_velocity.cpp “invariant backend: the chi-square gate needs alpha in (0,1)…”

marginalize (S9, Marginalization (invariant backend))

  • Pre: every track touching the clone has been fused
  • Post: P is the principal submatrix; no observation refers to the dropped clone
  • Test: tests/sdk/invariant_vio_backend.cpp “invariant VIO backend: end-to-end integration over a seed sweep…”

zero_velocity (S8, Invariant zero-velocity update)

  • Pre: both the IMU and the image tests say rest (the base’s detector)
  • Post: the posterior equals the Kalman update with H from finite differences of the retraction
  • Test: tests/sdk/invariant_zero_velocity.cpp “invariant S8: the zero-velocity posterior matches the retraction’s Jacobian…”
  • Test: tests/sdk/invariant_zero_velocity.cpp “invariant S8: the gyro-bias update is the additive Kalman update…”
  • Test: tests/tools/invariant_backend_adapter.cpp “invariant adapter: S8 holds a frame at rest out of the window…”
EdgeKindContentsTypeInvariants (✓ has a validator)
ImuSampleinputt, angular rate ω (rad/s), specific force a (m/s²)sdk/include/branes/sdk/vio_backend.hpp ImuMeasurement✓ timestamps strictly increasing; ✓ Δt within the sensor-rate bounds; ✓ samples finite
ImageFrameinputt, 8-bit image, camera idsdk/include/branes/sdk/vio_estimator.hpp feed_image✗ t increasing and inside the IMU span; ✗ the image matches the calibration’s resolution
CameraCalibinputintrinsics (fx, fy, cx, cy), radtan distortion, camera↔IMU extrinsic (R_imu_cam, p_imu_cam)math/include/branes/math/cameras/pinhole_radtan.hpp PinholeRadtanCamera✓ R_imu_cam ∈ SO(3); ✓ intrinsics positive, principal point inside the image
NormalizedObsdatafeature id, camera id, normalized image point (x, y)sdk/include/branes/sdk/msckf/stages/s0_sensor_model.hpp Result✗ finite, and the ray is in front of the camera (z > 0)
TrackDBdatafeature id → [(clone time, normalized observation)], plus the tracks that ended this framesdk/include/branes/sdk/msckf/stages/s4_frontend.hpp TrackTable✗ every observation’s clone time is a live clone, or the observation is purged; ✓ points inside the image; ✓ track length at most the clone window
NavSeeddataR, v, p, b_g, b_a, the time, and the prior P₀ in the filter’s error conventionsdk/include/branes/sdk/msckf/stages/s1_initialization.hpp seed_from_imu✓ gravity sign and magnitude; ✓ P₀ symmetric positive-definite; ✗ P₀ states what the init window does and does not determine (tilt-bias correlation)
FilterStatestatenav on SE₂(3) with additive biases (θ, ν, ρ, b_g, b_a: 15), clones (θ, ρ: 6 each) in the same right-invariant error, the joint covariance Psdk/include/branes/sdk/msckf/invariant_propagator.hpp InvariantNavState✓ P symmetric; ✓ P positive semidefinite; ✓ R and every clone rotation ∈ SO(3); ✗ every Jacobian and retraction uses R = Exp(δθ)·R̂, v = Exp(δθ)·v̂ + ν, p = Exp(δθ)·p̂ + ρ
Landmarkdatafeature position p_f (world), the triangulation’s parallax and conditioningsdk/include/branes/sdk/msckf/stages/s5_triangulation.hpp Result✓ positive depth in every observing camera; ✓ normal-matrix condition number reported
UpdateDiagoutputNIS, dof, the gate verdict, and where a rejected track stoppedsdk/include/branes/sdk/msckf/stages/s6_msckf_update.hpp Result✓ NIS ~ χ²(dof) over windows (bound χ²₁₋α(dof) per update, α = 0.05, from what msckf_update resolves: the S6d gate rejects above it, so a consistent filter loses α of its inliers and no more)
  • ImageFrame precision: integer pixels; no arithmetic-type choice
  • NormalizedObs precision: the narrowest edge that keeps sub-pixel accuracy; float holds (#532)
  • NavSeed deviates from the paper: P₀ is an isotropic σ = 0.1 on every core state, with no tilt-bias correlation (#609)
  • FilterState precision: stored in double; the Joseph form on EuRoC (the square-root form diverges there)

The driver is tools/include/branes/tools/invariant_backend_adapter.hpp.

on imu (200 Hz):
initialize [when not initialized] — the base's initializer
seed_state [when a seed is ready]
propagate [when initialized] — the base's interval convention: both samples, RK4 (#616)
on image (20 Hz):
sensor_model
track
initialize [when not initialized] — try the dynamic VI alignment
seed_state [when a seed is ready] — then return
propagate — to the image time
zero_velocity [when at rest] — then return; the frame adds no clone
msckf_update for each track touching the oldest clone [when window full]
marginalize [when window full]
augment
triangulate for each ended track (ascending id)
msckf_update for each ended track (ascending id)
InvariantCheckStatus
NEES ≈ dim(state) against ground truth (consistency)tools/include/branes/tools/pose_consistency.hpp PoseConsistencyunknown — consistent on ideal EuRoC tracks (yaw ANEES 1.26); on real KLT tracks not yet measured (#365)
NIS ≈ dim(measurement) per updatesdk/include/branes/sdk/msckf/invariant_vio_backend.hpp nis_consistencyfailing — real KLT tracks: normalized NIS 0.97–2.55 on MH, V1_01 and V2_01; 6.8–15.4 on V1_02, V1_03, V2_02 and V2_03 (#608)
innovations zero-mean and whitesdk/include/branes/sdk/msckf/invariant_vio_backend.hpp innovation_whitenessunknown — reported since #608
no information gained along global yaw and translation (the 4-D unobservable space)tests/sdk/msckf_invariant_backend.cpp “invariant backend: the marginalized joint update annihilates the 4-DoF gauge…”holds
told the true noise, the filter improves and its yaw σ never collapses (EuRoC ideal tracks)scripts/euroc-consistency.shfailing — with one truth and S2 integrating both samples (#616, median NIS 1.00): fails 1 (2 of 66 at 1 px, MH_03 and MH_04, the same draws as the MSCKF: the shared tilt and bias seed, #609), passes 2 (0.40× vs 0.41×; 0.18× vs 0.36×), passes 3 (0.984)
4-DoF ATE and rotation RPE within a factor of OpenVINS mono on the 11 EuRoC sequencesscripts/euroc-baseline.shfailing — median 2.62× OpenVINS mono (the MSCKF 2.83×; 7 of 11 sequences better); rotation RPE 0.90° vs 2.21° (#608)
  • replace edge FilterState: the error convention changes; every consumer is revisited below
  • replace node seed_state: #608
  • replace node propagate
  • replace node augment
  • replace node triangulate
  • replace node msckf_update: #608: the base’s χ²₁₋α(dof) gate (#481), and the NIS and innovation sum of every update
  • replace node marginalize: a principal submatrix is the same in any error convention; the track bookkeeping is the invariant backend’s own
  • replace node zero_velocity: #608
  • replace node online_calib
  • kept base node slam_update on changed edge FilterState: not implemented in either filter

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

  • edge ‘ImageFrame’: no validator for “t increasing and inside the IMU span”
  • edge ‘ImageFrame’: no validator for “the image matches the calibration’s resolution”
  • edge ‘NormalizedObs’: no validator for “finite, and the ray is in front of the camera (z > 0)”
  • edge ‘TrackDB’: no validator for “every observation’s clone time is a live clone, or the observation is purged”
  • edge ‘NavSeed’: no validator for “P₀ states what the init window does and does not determine (tilt-bias correlation)”
  • edge ‘NavSeed’: deviates from the paper: P₀ is an isotropic σ = 0.1 on every core state, with no tilt-bias correlation (#609)
  • edge ‘FilterState’: no validator for “every Jacobian and retraction uses R = Exp(δθ)·R̂, v = Exp(δθ)·v̂ + ν, p = Exp(δθ)·p̂ + ρ”
  • node ‘track’: deviates from the paper: no RANSAC outlier rejection in the front end; the S6d gate carries it
  • node ‘track’: deviates from the paper: the shipped default churns (>50% of new tracks lost on their first frame); the border margin is opt-in (#474, #574)
  • node ‘initialize’: deviates from the paper: the dynamic path is excitation-limited on EuRoC and has open SfM defects (#254, #499–#501)
  • node ‘seed_state’: no stage bench
  • node ‘seed_state’: deviates from the paper: the base’s prior is itself an isotropic σ = 0.1 with no tilt-bias correlation (#609); this node maps it faithfully
  • node ‘augment’: no stage bench
  • node ‘triangulate’: no stage bench
  • node ‘msckf_update’: no stage bench
  • node ‘marginalize’: no stage bench
  • node ‘zero_velocity’: no stage bench
  • node ‘slam_update’ (S7): not implemented
  • node ‘online_calib’ (S10): not implemented
  • system invariant ‘nis’: failing — real KLT tracks: normalized NIS 0.97–2.55 on MH, V1_01 and V2_01; 6.8–15.4 on V1_02, V1_03, V2_02 and V2_03 (#608)
  • system invariant ‘consistency_gate’: failing — with one truth and S2 integrating both samples (#616, median NIS 1.00): fails 1 (2 of 66 at 1 px, MH_03 and MH_04, the same draws as the MSCKF: the shared tilt and bias seed, #609), passes 2 (0.40× vs 0.41×; 0.18× vs 0.36×), passes 3 (0.984)
  • system invariant ‘accuracy’: failing — median 2.62× OpenVINS mono (the MSCKF 2.83×; 7 of 11 sequences better); rotation RPE 0.90° vs 2.21° (#608)