Lesson 1 · Inertial state estimation · 30–45 minutes first study · 15 minutes revision

What an IMU actually measures

Two equations, one sign, and the bug it caused in glass-lio.

Every IMU-preintegration derivation an interviewer asks for starts from the measurement model: what the gyroscope and the accelerometer actually report. Get one sign wrong there and everything built on it is wrong too. By the end of this page you should be able to write both equations from memory, and explain in one sentence why an early version of glass-lio's tight coupling drifted hundreds of metres a minute.

New to matrices, derivatives, or uncertainty? Start with the mathematical toolkit. It introduces the notation used throughout these lessons.

1 · The bug

An early version of glass-lio's tight coupling averaged the accelerometer over a static window and used that mean as gravity. The estimator then accelerated slowly and steadily away, with Z falling about 380 m in a minute.The post-mortem is in the code, where the fix lives: glasslio_node.cpp, lines 349–358. Nothing crashed and nothing reported an error. One equation explains it.

2 · The two equations

This is how Forster et al. write the IMU model:Forster, Carlone, Dellaert & Scaramuzza, On-Manifold Preintegration for Real-Time Visual-Inertial Odometry, IEEE T-RO, §V, eqs. (27)–(28). This page uses their notation.

$$ {}_{B}\tilde{\boldsymbol{\omega}}_{WB}(t) = {}_{B}\boldsymbol{\omega}_{WB}(t) + \mathbf{b}^{g}(t) + \boldsymbol{\eta}^{g}(t) \tag{27} $$
$$ {}_{B}\tilde{\mathbf{a}}(t) = \mathbf{R}_{WB}^{\top}(t)\,\big({}_{W}\mathbf{a}(t) - {}_{W}\mathbf{g}\big) + \mathbf{b}^{a}(t) + \boldsymbol{\eta}^{a}(t) \tag{28} $$
\(\tilde{x}\)
A tilde means measured: what the sensor reports, not the truth.
\({}_{B}x,\ {}_{W}x\)
The frame a vector is expressed in: \(B\) is the body (IMU) frame, \(W\) the world frame.
\(\mathbf{R}_{WB}\)
Rotates body-frame vectors into the world, so \(\mathbf{R}_{WB}^{\top}\) takes world vectors into the body frame.
\({}_{W}\mathbf{a}\)
The sensor's true acceleration, in the world.
\({}_{W}\mathbf{g}\)
The gravity vector in the world. It points down: with \(z\) up, \(\mathbf{g} \approx [0,\,0,\,-9.81]^{\top}\,\mathrm{m/s^2}\).
\(\mathbf{b}\)
Bias: a slowly drifting offset, added in the body frame.
\(\boldsymbol{\eta}\)
White noise.

The gyroscope equation holds no surprises: true rate plus bias plus noise. All the interesting behaviour is in the accelerometer.Forster also neglects the Earth's rotation, which amounts to treating \(W\) as an inertial frame (§V).

3 · The sign that matters: specific force

An accelerometer doesn't measure acceleration \(\mathbf{a}\). It measures \(\mathbf{a} - \mathbf{g}\), expressed in its own body frame. This is called specific force, and two cases make it concrete:

glass-lio's code relies on this. ImuInit averages the static accelerometer readings, stores the mean as gravity_, and rotates that vector onto +Z, pointing up.imu_init.cpp, lines 73–79. Strictly, by eq. (28) that mean is \(-\mathbf{R}_{WB}^{\top}\mathbf{g} + \mathbf{b}^{a}\): specific force, not gravity. The variable name is loose, but the maths is consistent. The node then seeds the estimator's gravity pointing down, as \([0,\,0,\,-9.81]\).

Units trap

The Livox reports specific force in g, not m/s², so at rest it reads about 1.0, not 9.81. That's why glass-lio has an imu.accel_in_g setting and multiplies every accelerometer sample by accel_scale before integrating.lio_estimator.cpp, lines 270–275.

4 · The bug, explained

Put \(\mathbf{a} = 0\) into eq. (28) and drop the noise. A stationary IMU reports

$$ \tilde{\mathbf{a}} = -\mathbf{R}_{WB}^{\top}\,\mathbf{g} + \mathbf{b}^{a} $$

Every sample in a static window gives the same sum. Averaging more samples makes the sum more precise, but never separates it into its two parts. And those parts behave differently:

The early version treated the whole sum as a world-fixed gravity vector, which baked \(\mathbf{b}^{a}\) into the world frame. As soon as the robot turned, that baked-in bias pointed the wrong way. It became an orientation-dependent fake acceleration, on the scale of \(|\mathbf{b}^{a}|\), which was integrated twice into position. The fix was to seed gravity at its known magnitude and estimate \(\mathbf{b}^{a}\) as a body-frame state.The same equation also tells you when the two become separable: once \(\mathbf{R}_{WB}\) changes, the gravity term moves and the bias doesn't. Suitable changes of orientation, together with motion constraints from other measurements, can separate the terms. Pure yaw about gravity leaves the gravity reading unchanged and is not sufficient.

Say it at a whiteboard

“An accelerometer measures specific force, \(\mathbf{a} - \mathbf{g}\), in the body frame. At rest you only ever see \(-\mathbf{R}^{\top}\mathbf{g} + \mathbf{b}^{a}\) as one sum, so a static initialisation can't separate gravity from the accelerometer bias. You constrain gravity's magnitude and use sufficiently varied motion and external motion measurements to separate bias from gravity and attitude.”

5 · Where this lives in the code

Every preintegration step in glass-core starts from eqs. (27)–(28) rearranged, by subtracting the current bias estimates:preintegration.cpp, lines 29–33. The noise terms \(\boldsymbol{\eta}\) aren't subtracted, because they can't be. They come back as the covariance the preintegration carries (lines 64–66).

const Eigen::Vector3d w = gyro - bias_gyro_;
const Eigen::Vector3d a = accel - bias_accel_;

Notice what that function doesn't do: it never touches gravity. So where does \(\mathbf{g}\) come back in? That's the next lesson.

6 · Practice

Answer from memory, without scrolling back up. Having to work to recall it is what makes it stick.

A level IMU sits still on a table, with its \(z\) axis pointing up. Ignoring bias and noise, what does the accelerometer's \(z\) axis read?

The same IMU is dropped and is now in free fall. What does its \(z\) axis read?

In eq. (28), which frame is the accelerometer bias \(\mathbf{b}^{a}\) expressed in?

A static window averages 10,000 accelerometer samples. Why still can't it separate gravity from \(\mathbf{b}^{a}\)?

From memory: write both measurement models, gyroscope and accelerometer, and label the frame of each term. Then say the whiteboard sentence out loud.