Skip to content

About

EKF sensor fusion for rover state estimation : IMU, wheel odometry and GNSS, with online gyro-bias and encoder-scale calibration

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Rover State Estimation

An Extended Kalman Filter fusing IMU, wheel odometry and GNSS for a planar rover — which also estimates the gyro bias and encoder scale factor that make dead reckoning drift in the first place.

Over 20 independent missions: 0.37 m position RMSE (0.08% drift) against 52 m for dead reckoning and 5.8 m for raw GNSS, and 0.9 m of drift through a 45-second GNSS blackout.

CI Python License

Estimated versus true path


Read this first: the data is simulated

Results are measured against a simulated rover trajectory, not a real vehicle.

This is not a shortcut, it is the only way the question can be asked. Filter accuracy is the distance between an estimate and the truth — and on a real rover the truth is precisely what you do not have, which is why you are running a filter at all. Getting it requires motion capture or an RTK base station. Simulation provides it exactly and for free.

The honest cost: a simulator contains only the error sources its author modelled. Real IMUs have temperature-dependent bias and scale-factor error, real GNSS has multipath and correlated errors, real wheels slip in ways no Gaussian captures. Treat these numbers as a clean-room upper bound.


Results

20 independent missions, each with its own trajectory, its own gyro bias, and its own noise. Figures are median across runs.

estimator RMSE (m) p95 (m) max (m) final (m) drift (%) heading RMSE (deg) after 45s outage (m)
EKF 0.37 0.63 1.87 0.19 0.08 3.9 0.91
GNSS only 5.75 15.56 21.73 2.00 0.94 94.9 20.68
dead reckoning 52.14 100.18 104.81 91.51 47.54 86.0 35.07

Calibration states recovered (median absolute error): gyro bias to 0.0026 rad/s, encoder scale to 0.0025 — removing 90% of a 2.5% scale error that the rover was never told about.

Filter consistency: NIS = 2.02 against a theoretical 2.0 for a 2-DOF measurement. This matters more than the accuracy numbers. A filter can be accurate today and still be over-confident — understating its own uncertainty, which makes it reject good measurements and diverge later. The normalised innovation squared test catches that; raw error cannot.

Position error over time

GNSS outage behaviour

The outage starts two minutes in, after the filter has calibrated itself.

outage distance driven EKF error dead reckoning error EKF drift
15 s 11.5 m 0.37 m 33.6 m 3.2%
30 s 24.0 m 0.98 m 50.3 m 4.1%
60 s 38.9 m 1.97 m 40.3 m 5.1%
120 s 86.6 m 8.29 m 91.3 m 9.6%
240 s 171.2 m 17.30 m 114.5 m 10.1%

Degradation is graceful rather than catastrophic: error grows with distance, not explosively, and stays near 3-10% of distance driven. That is the practical payoff of estimating bias and scale — dead reckoning from calibrated sensors is far better than dead reckoning from raw ones.


Why fusion works here

The three sensors fail in complementary ways, which is the entire argument:

sensor strength failure mode
Rate gyro smooth, high-rate heading change bias — heading error grows linearly, position error quadratically
Wheel encoders accurate short-range distance scale error — systematic, grows with distance, never averages out
GNSS never drifts, absolute noisy (1.4 m), slow (5 Hz), disappears under cover

Neither odometry nor GNSS is usable alone at both timescales. Odometry is locally excellent and globally hopeless; GNSS is the reverse.

The interesting part is observability. A gyro cannot detect its own bias and encoders cannot detect their own scale error — from their own measurements, both errors are invisible. But an absolute position fix contradicts a drifting heading and a mis-scaled distance in ways the filter can attribute to the right cause. Adding them to the state turns two unknown error sources into two estimated quantities.

Calibration states converging

The scale factor converges from an uninformed 1.0 to the true 1.025 within about 60 seconds and stays locked. During the shaded outage the bias estimate goes flat — with no GNSS there is nothing to observe it with, so the filter correctly stops updating it rather than inventing information.


How it works

IMU 100Hz  ──predict──►  ┌──────────────────────────────────┐
                         │  state: x, y, θ, v, b_gyro, s_enc │
encoder 50Hz ──update──► │  covariance: 6×6                  │ ──► pose
                         └──────────────────────────────────┘
GNSS 5Hz    ──update──►         gated on innovation

State: [px, py, theta, v, b_gyro, s_enc] — pose and speed, plus the two error sources estimated alongside them.

Prediction uses the IMU as a control input (strapdown mechanisation): the gyro says how heading changed, the accelerometer how speed changed, both corrupted by the biases in the state. Position integrates at the midpoint heading, which is markedly more accurate than the start-of-step heading on curves.

Encoder update models the measurement as s · v, not v. Writing it that way is what makes the scale factor estimable instead of silently corrupting the speed estimate.

GNSS update is gated on normalised innovation squared, so a multipath jump is rejected rather than absorbed. (On simulated data the gate is near-free insurance rather than a demonstrated win — see docs/RESULTS.md.) Covariance uses the Joseph form, which stays symmetric and positive definite far longer in floating point than the shorter (I − KH)P.


Three things worth knowing

The Jacobian term everyone drops. Heading at the midpoint depends on the gyro bias, so the bias propagates into position through ∂x/∂b = ½·v·sin(θ)·dt². It is a second-order term and easy to omit — and omitting it makes the filter over-confident about position, which shows up as inflated NIS long before it shows up as visible error.

Angle wrapping is not cosmetic. An estimate of +179° against a truth of −179° is 2° of error, not 358°. Every heading comparison in this package goes through one wrap_angle function, and interpolating a heading series is done through its sine and cosine so a wrap does not produce a spurious sweep around the circle.

Outage error can be lower than steady-state error. Counter-intuitive but real: during normal operation the filter is continuously pulled around by 1.4 m GNSS noise, while during a short outage it propagates smoothly on calibrated odometry. For outages under roughly 30 seconds the smooth estimate is closer to truth than the GNSS-corrected one. This inverts once accumulated drift exceeds the GNSS noise floor.


Quick start

git clone https://github.com/Ashok-Kumar17/rover-state-estimation.git
cd rover-state-estimation
pip install -e ".[dev]"

python -m rse demo                          # one mission, printed
python -m rse evaluate --out results --figures   # the table above (~30 s)
python -m rse outage --out results --figures     # the outage sweep (~60 s)

pytest

As a library

from rse import generate, simulate_sensors, run_all, evaluate

traj = generate(seed=1)
suite = simulate_sensors(traj, seed=2)
estimates = run_all(traj, suite)

for name, est in estimates.items():
    print(name, round(evaluate(traj, est).position_rmse, 3), "m RMSE")

Using the filter directly

from rse import RoverEKF

ekf = RoverEKF()
ekf.predict(gyro_z=0.02, accel_x=0.1, dt=0.01)   # IMU sample
ekf.update_encoder(v_meas=1.2)                    # wheel speed
ekf.update_gnss(x_meas=10.4, y_meas=3.1)          # position fix

print(ekf.position, ekf.position_std)

Project layout

src/rse/
├── trajectory.py   ground-truth motion, angle wrapping
├── sensors.py      IMU / encoder / GNSS models with realistic errors
├── ekf.py          the filter: predict, updates, innovation gating
├── fusion.py       runners for the EKF and both baselines
├── metrics.py      accuracy, outage drift, NIS consistency
├── pipeline.py     Monte Carlo experiment and outage sweep
├── plots.py        trajectory, error, convergence figures
└── cli.py          python -m rse
tests/              80 tests

Limitations

  • Simulated data, as described above.
  • Planar only. No roll, pitch, or terrain slope. A real rover on rough ground needs a full 3D attitude filter.
  • No loop closure. This is pure filtering, not SLAM — with GNSS gone indefinitely, error grows without bound.
  • Gaussian noise assumption. Real GNSS multipath is heavy-tailed; the innovation gate helps but does not make the filter robust in a statistical sense.
  • EKF, not UKF. The linearisation is fine at these rates and noise levels, but an Unscented filter would handle a poorer initial heading estimate better.

License

MIT — see LICENSE.

About

EKF sensor fusion for rover state estimation : IMU, wheel odometry and GNSS, with online gyro-bias and encoder-scale calibration

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages