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

What an IMU actually measures

Specific force, angular rate, and the limits of stationary initialization.

IMU integration starts with the measurement model: what a gyroscope and accelerometer report, in which frame, and with which biases. A wrong gravity sign propagates into every later state update. By the end, you should be able to write both equations from memory and explain why stationary data cannot reveal every state variable.

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 further every second.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.

Before the equation, extract everything the symptom already tells you. A quadratic fall means a constant acceleration: \(z = \tfrac12 a t^2\). Its size was diagnostic: a small fraction of \(g\), far too large to be gyro bias or round-off and far too small to be a units mistake, but exactly the size of a cheap MEMS accelerometer's bias. So before reading any code you can already guess that some accelerometer bias ended up being treated as a real acceleration. The rest of this lesson shows precisely how.

A habit worth keeping

Characterise every symptom before you debug it: its shape over time and its rough size relative to the physical quantities involved. “It drifts” is not actionable. “It accelerates constantly, at about the size of a sensor bias” rules out most suspects at once.

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} = [0,\,0,\,-g]^{\top}\).
\(\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).

Negligible per step, fatal per minute

A bias can be negligible over one step and fatal over a minute. Over one LiDAR scan, an uncorrected gyro bias rotates the estimate by far less than anything the LiDAR could notice. Integrated for a minute without correction it becomes a heading error large enough that a map built from it is not a map. This is why glass-lio subtracts the bias at the source, before any integration, even though no single scan would show the difference.The measured initialisation values are in docs/implementation/1-imu-init.md, §1 and §5.

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, at the standard magnitude.

Three more cases, each a real situation

The last case is the reason the accelerometer cannot, by itself, tell tilt from acceleration, so an initialisation that treats the mean reading as gravity needs a quiet interval. Section 5 shows glass-lio's variation checks and why they cannot rule out steady acceleration.

Units trap

The Livox reports specific force in units of g, not m/s², so at rest it reads about one. 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.

The units trap deserves a second look, because of how quietly it fails. Forget the scaling and three things still work perfectly. Deskewing uses only the gyro, so it is untouched. Initialisation takes only the direction of the mean reading, and scaling a vector does not rotate it, so the world frame is still levelled correctly. Loosely coupled odometry never integrates acceleration at all. The system looks healthy. Then tight coupling integrates acceleration for the first time, every acceleration is too small by a factor of \(g\), and the estimator falls apart in a way that looks like a deep modelling bug.

glass-lio therefore checks the one quantity that does expose the mistake: the magnitude. After scaling, the mean reading at rest must be close to standard gravity, or the node logs an error that names the setting to fix.glasslio_node.cpp, lines 375–379. A check like this costs one line and turns a mystery into an error message. Write one for every unit conversion you depend on.

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.

The error, written down exactly

You can derive the fake acceleration in four lines, and it is worth doing once. Call the attitude at initialisation \(\mathbf{R}_0\). The faulty initialisation sets its gravity estimate from the mean reading,

$$ \hat{\mathbf{g}} = -\mathbf{R}_0\,\tilde{\mathbf{a}}_0 = \mathbf{g} - \mathbf{R}_0\,\mathbf{b}^{a}. $$

Later, at attitude \(\mathbf{R}(t)\), the estimator inverts eq. (28) assuming no bias: \(\hat{\mathbf{a}} = \mathbf{R}(t)\,\tilde{\mathbf{a}} + \hat{\mathbf{g}}\). Substitute the true reading and simplify:

$$ \hat{\mathbf{a}} = \mathbf{R}(t)\big[\mathbf{R}(t)^{\top}(\mathbf{a}-\mathbf{g}) + \mathbf{b}^{a}\big] + \mathbf{g} - \mathbf{R}_0\,\mathbf{b}^{a} = \mathbf{a} + \big(\mathbf{R}(t) - \mathbf{R}_0\big)\,\mathbf{b}^{a}. $$

Every property of the bug is visible in that last term:

Seed gravity at its true magnitude, carry \(\mathbf{b}^{a}\) as a body-frame state, and the term disappears, because the bias is then subtracted in the frame where it actually lives.

Explain the idea

“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 · What a static window can and cannot give you

glass-lio initialises from a short window of IMU data during which the platform is assumed still. Apply the two equations with \(\boldsymbol{\omega} = 0\) and \(\mathbf{a} = 0\) and you get, averaged over the window:

$$ \overline{\tilde{\boldsymbol{\omega}}} = \mathbf{b}^{g}, \qquad \overline{\tilde{\mathbf{a}}} = -\mathbf{R}_{WB}^{\top}\mathbf{g} + \mathbf{b}^{a}. $$

The first average is the gyro bias: nothing else survives. The second is the sum from section 4. glass-lio uses its direction to level the world, rotating it onto \(+Z\), and ignores its measured magnitude in favour of standard gravity. Here is the complete inventory of what the window delivers:

QuantityFrom a static window?Why
gyro biasyestrue rate is zero, so the mean is the bias
roll and pitchyes, approximatelythe direction of the mean specific force, tilted slightly by \(\mathbf{b}^{a}\)
yawnorotating about gravity changes no reading; it is set to zero
accel biasnoonly its sum with gravity is visible
velocitynosee “rest versus constant velocity” below

The yaw row is not a shortcut. Gravity is a single vector, so it pins down two of the three rotational degrees of freedom and says nothing about heading. Choosing zero yaw and saying so is the honest response to an unobservable quantity.

Two checks, and why the second one exists

The averages above describe rest or constant velocity with negligible rotation. glass-lio accepts a window only when both of these heuristics hold:imu_init.cpp, lines 46–71, with the thresholds in livox_mid_360.yaml, lines 259–265.

The gyro check misses straight-line acceleration. The accelerometer standard-deviation check detects acceleration that changes during the window, but a steady push can pass both checks: its constant forward acceleration is absorbed into the estimated gravity direction. These gates are practical stationarity heuristics, not proof that the platform was at rest.

When a window fails, it slides forward by one sample rather than being discarded wholesale. That detail matters more than it looks: clearing the buffer only ever tests non-overlapping blocks, so a genuine rest that straddles a block boundary is rejected by both halves. On handheld recordings, the sliding version finds rests that the clearing version rejects.

Rest versus constant velocity

Neither check can tell rest from constant velocity. Both give zero rotation and zero acceleration, because that is what “not accelerating” means, and eq. (28) contains no velocity at all. glass-lio's test bag begins with the robot already cruising. The window passes, correctly: gravity is uncorrupted when acceleration is zero. But the estimator is seeded with zero velocity, which is wrong, and the tightly coupled solver needs a correct velocity to start. glass-lio solves this by running the loosely coupled LiDAR odometry for a few scans, which measures the velocity, before switching the IMU in.

Read the log carefully

“Static window detected” means the samples passed the rotation-rate and acceleration-variation gates. It does not prove rest or even zero acceleration: constant velocity and steady acceleration can both pass, although only the former leaves gravity alignment uncorrupted.

6 · What the model leaves out

Eqs. (27)–(28) are a model, and every model is a list of things deliberately ignored. Knowing the list tells you when the model stops being good enough.

The Earth's rotation

A perfect gyro at rest on the ground reads the Earth's rotation. Next to a MEMS gyro's bias that is a tiny fraction, far inside the bias uncertainty, so treating the world frame as non-rotating costs nothing measurable. For a navigation-grade gyro, whose bias is orders of magnitude smaller, the Earth's rotation is the biggest signal in the reading and has to be modelled. The same equation, different sensor, different verdict.

The lever arm

The accelerometer measures the specific force at its own location. If the body rotates, a point at offset \(\mathbf{r}\) from the rotation centre accelerates even when the centre does not:

$$ \mathbf{a}_{\text{sensor}} = \mathbf{a}_{\text{centre}} + \dot{\boldsymbol{\omega}} \times \mathbf{r} + \boldsymbol{\omega} \times (\boldsymbol{\omega} \times \mathbf{r}). $$

Inside the Mid-360 the IMU chip sits a few centimetres from the LiDAR's optical origin.livox_mid_360.yaml, line 281. The centripetal term grows with the square of the turn rate, and during brisk turns it can rival the accelerometer bias. glass-lio tracks the pose of the IMU body itself, so the offset enters only when converting between LiDAR and IMU positions; for a system that tracked a different point on the vehicle, this term would be one of the first to show up.

Scale factor, misalignment, and temperature

Real sensors also have a small gain error per axis, axes that are not perfectly orthogonal, and biases that drift with temperature. On a good MEMS part these effects are small, and the measured magnitude of gravity at rest deviates from the standard value only slightly, through exactly these effects and the accelerometer bias. At that level they are absorbed by the bias states and the noise model. Estimators for poorer sensors add explicit intrinsic-calibration states; glass-lio does not need to.

7 · Four noise parameters, and what they mean

The noise terms \(\boldsymbol{\eta}\) cannot be subtracted, but they can be sized, and the sizing decides how much the solver believes the IMU. glass-lio configures four parameters for the Mid-360:livox_mid_360.yaml, lines 226–234.

SettingWhat it describes
gyro_noisewhite noise on each gyro sample
accel_noisewhite noise on each accelerometer sample
bias_rw_gyrohow fast the gyro bias may wander
bias_rw_accelhow fast the accelerometer bias may wander

The first two are densities: they describe noise per root-hertz, so the variance of an integrated quantity grows linearly with time (the toolkit's section 5 converts them). The second two describe the bias as a random walk: not a constant, but something that drifts slowly, and only slowly. That slowness is itself information. Without it, a gyro bias and a genuine slow turn would be indistinguishable, and the solver could explain any rotation by adjusting the bias.

These parameters come from the datasheet and are then checked on data. They are physical properties of the part, not tuning knobs. Lesson 5 shows how they become the covariance that decides, scan by scan, whether the IMU or the LiDAR wins an argument.

8 · 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.

9 · 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 a great many accelerometer samples. Why still can't it separate gravity from \(\mathbf{b}^{a}\)?

A tightly coupled estimator falls at a small, steady acceleration, starting only after the robot's first turn. The most likely cause is…

With gravity estimated as the static mean, how large is the fake acceleration after a half-turn?

A robot is pushed in a straight line, accelerating gently, during the initialisation window. Which check stops the bad initialisation?

A static window reports success while the robot cruises at constant velocity. Which initial value is now wrong?

From memory: write both measurement models, gyroscope and accelerometer, and label the frame of each term. Then explain the principle in your own words.

From memory: derive the fake acceleration produced when the static mean is used as gravity, starting from \(\hat{\mathbf{g}} = -\mathbf{R}_0\tilde{\mathbf{a}}_0\). Then list what a static window can and cannot estimate.