Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

EqVIO Equivariant Filter Example

An Equivariant Filter for Visual-Inertial Odometry

This notebook demonstrates the Equivariant Filter (EqF) for state estimation in a visual-inertial odometry context, based on the paper:

“EqVIO: An Equivariant Filter for Visual Inertial Odometry” by van Goor et al.

In this tutorial, we will utilize a subset of the EuRoC MAV dataset, which is a series of stereo images and synchronized IMU measurements collected on micro-aerial-vehicles (MAVs). We will:

  • load a processed 60 second EqVIO replay log,

  • step through EqVIOFilter construction and the replay loop that calls initializeFromIMU, predict, and update,

  • compare the estimated translation trajectory against EuRoC ground truth in 3D, and

  • compare the estimated velocity against EuRoC ground truth over time.

This demo uses the gtsam_unstable.EqVIOFilter class which provides a clean interface for predict/update operations.

Note: CSV parsing, IMU segmentation helpers, and plotting utilities are tucked into a collapsible cell; the filter driver itself is written out in the main narrative cells.

Try it out in Colab!

Open In Colab

Setup and Imports

We will use:

  • gtsam_unstable.eqvio for the filter

  • plotly for plotting

  • the processed replay CSV for IMU and feature measurements

  • the original EuRoC ground-truth CSV for clean pose and velocity references

The data files are located in this AwesomeEqF repository under the data folder.

Notebook Cell

Grab the Input Files

We preprocess the EuRoC MAV dataset such that the first 60 seconds of Vicon Room 1 are available to us in eqvio_processed_60s.csv. Our preprocessing runs GIFT (a library developed by van Goor to perform invariant feature tracking) as well so that we can abstract away the minutae and focus on the filter logic. Here’s what the feature tracker looks like visually, with sampled points being tracked over time as the MAV moves through the room:

feature tracker

We also obtain the ground truth data from the published EuRoC dataset to compare against the filter output.

(PosixPath('/Users/apollo/dev/research/borg/AwesomeEqF/data/eqvio_processed_60s.csv'), PosixPath('/Users/apollo/dev/research/borg/AwesomeEqF/data/eqvio_ground_truth.csv'))

Data Structures

The processed replay log makes use of three kinds of rows:

  • meta rows for configuration and calibration,

  • imu rows for inertial samples, and

  • vision_feature rows for tracked feature bearings.

We load those into simple Python dataclasses so the replay logic stays easy to read.

Notebook Cell

Load the Replay Log and EuRoC Ground Truth

The replay log is the compact event stream that drives the filter. The EuRoC ground-truth file gives us the reference position, orientation, and world-frame velocity trajectory we will use for evaluation.

Replay events: 13202
IMU events: 12001
Vision frames: 1201
Vision features: 29531
Replay duration (metadata): 60 s
Ground-truth samples: 28712

Replay infrastructure

The next cell keeps CSV parsing, calibration readers, IMU buffering between camera times, and plotting utilities grouped so the next few cells can focus on the EqVIO predict / update calls.

Helpers worth skimming:

  • make_buffered_imu_propagation turns irregular IMU timestamps into (IMUInput, Δt) pairs from the last vision time to the current frame.

  • Ground-truth alignment (rigid_align_points, _body_frame_velocity, …): we rigidly align EuRoC positions for trajectory plots and express velocity in the body frame to match EqVIOFilter.velocity().

Notebook Cell

Build EqVIOFilter

xi_ref is the fixed reference element on the homogeneous space. We optionally install the camera–IMU extrinsic (cameraOffset) from CSV metadata. initial_covariance scales a 21×2121 \times 21 template of initial uncertainties (biases, attitude, position, velocity, camera pose relative to IMU). EqVIOFilterParams holds process noise, feature retention, and other tuning read from metadata.

The camera object is a normalized pinhole: bearings in the CSV are already (u,v)(u,v) on the normalized image plane, so the calibration is identity-scale with zero skew and principal point at the origin. We need a camera to handle projection for tracked features in the data!

The replay keeps an IMU buffer plus gravity_initialized / current_time state that the main loop uses next.

Main replay loop: predict then update

Events are sorted by sequence. IMU rows create the filter on first use, call initializeFromIMU once gravity can be inferred, and append samples to imu_buffer.

Vision rows then carry all bearing measurements for one exposure. In order, this is what happens:

  1. make_buffered_imu_propagation converts buffer contents into inertial steps up to this frame time.

  2. Each step calls filter_eqf.predict(imu_in, dt): the EqF is propagated piecewise for each IMU measurement between camera times.

  3. We build measurement noise R (block 2×22 \times 2 per landmark) and filter_eqf.update(event.vision, camera, R) to fuse every feature at this time.

  4. Store translation and velocity for the plots below.

This matches the C++ EqVIOFilterExample pattern: integrate IMU between asynchronous images, then apply one visual update per frame.

Package ReplayResults for evaluation

Keep only samples inside the ground-truth time span, interpolate EuRoC position/velocity, rigidly align translations (the filter’s world frame is arbitrary), rotate world velocity into the body frame for comparison with EqVIOFilter.velocity(), and wrap everything in ReplayResults for plotting.

{'samples_compared': 1180, 'overlap_start_sec': 1.0499999523162842, 'overlap_end_sec': 60.0, 'position_rmse_m': 0.06750979142277792, 'final_position_error_m': 0.07123843159684429, 'velocity_rmse_mps': 0.04277568828237973, 'final_velocity_error_mps': 0.08379393501786624, 'measurement_noise_variance': 1.775565600641238e-05, 'final_landmark_count': 29}

The overlap starts a little after t_rel = 0 because the EuRoC ground-truth stream begins slightly later than the processed replay log. That is normal for this dataset slice.

Plot 1: 3D Pose Trajectory

This figure compares the translation component of the estimated pose against ground truth.

It is generated with Plotly, so feel free to zoom, pan, and orbit to take a look at how well the filter estimates the true position of the MAV!

Loading...

Plot 2: Velocity Comparison

For velocity, we can compare more directly. The Python wrapper’s velocity() output matches a body-frame convention well, so we rotate the EuRoC world-frame velocity into the body frame using the ground-truth orientation and then plot the components over time.

Loading...