This capstone assembles the measurement models, factors, covariances, and geometry from lessons 1–8 into one LiDAR–inertial update. Identify what each term constrains and how it is weighted, then follow one implementation from raw scan to updated map.
0 · Warm-up, from memory
Lessons 3 and 4 in one line: the IMU residual this solve stacks.
From memory: write the velocity row of the IMU residual, with the bias-corrected delta.
- State side: \(\mathbf{R}_i^{\top}(\cdots)\) with gravity, from lesson 3
- Minus the delta, corrected to the current bias, from lesson 4's eq. (44)
1 · The one equation
A tightly coupled estimator combines motion and geometric evidence in one objective. A prior anchors what the current measurements cannot determine; IMU and LiDAR factors contribute residuals scaled by their uncertainties. A robust loss limits the influence of incorrect point-to-plane correspondences:
The exact state, prior, and number of factors depend on the estimator. This pattern is the LiDAR counterpart of the visual–inertial MAP objective in Forster et al. On-Manifold Preintegration…, §IV, eq. (26); the IMU factor with bias updates is §VI-D, eq. (45).
Case study: a current-state solve
glass-lio instantiates this objective with one IMU interval and point-to-plane LiDAR residuals. It solves for the new
state and gravity, an 18-dimensional increment \([\delta\boldsymbol{\phi},\ \delta\mathbf{p},\ \delta\mathbf{v},\ \delta\mathbf{b}^{g},\ \delta\mathbf{b}^{a},\ \delta\mathbf{g}]\):nav_state.hpp,
lines 46–79: the state layout, and boxplus, which composes rotation on the right. Gravity is appended in
tight_registration.hpp.
Four blocks: every LiDAR point (lesson 6, Huber \(\rho\)), the IMU factor (lessons 3–5), a bias prior, and a gravity
prior. All four fold into the same \(\mathbf{H}\), "That sum IS the sensor fusion."tight_registration.cpp,
lines 59–170, and glass-lio's docs/implementation/5-registration.md §3.11, "One
tight solve, four residual blocks".
eq.addScalar(r * inv_sigma, Jl * inv_sigma, huber_whitened);
eq.addBlock<9>(imuResidual(xi, x, pre, g), Jimu, imu_information);
eq.addBlock<6>(biasResidual(xi, x), Jb, bias_information);
eq.addBlock<3>(g - gravity_prior, Jg, gravity_information);
2 · One scan, in order
Across implementations, the reusable sequence is: choose a common time and frame for the measurements; predict motion from the IMU; associate LiDAR points with map geometry; combine residuals and priors; solve and check the result; then update the map only when the pose is trusted. Each decision needs its own uncertainty model.
Case study: glass-lio's scan update
The following steps show one implementation. Its warm-up length, acceptance threshold, covariance approximation, and gyro-bias feedback gate are design choices, not requirements of tight coupling.
- Deskew the points with the gyro, integrated on SO(3) (lesson 2).
- Loose for the first few scans, to measure the velocity that a static init can't observe (lesson 1).
- Preintegrate the IMU over (last scan's end, this scan's end]: the deltas and their covariance (lessons 3, 5).
- Grow the bias covariance by its random walk over that interval.
- Predict the new state with
predictState, putting gravity back in the world (lesson 2). - Iterate the solve: re-associate every point to a plane (lesson 7), stack the four blocks with bias-corrected deltas (lesson 4), solve 18×18, retract.
- Accept or coast: too few correspondences, a non-finite solve, or RMSE above a configured limit, commits the prediction and keeps the scan out of the map.
- Carry the posteriors: the bias and navigation blocks of \(H_{\rm total}^{-1}\), the latter becoming the next \(\mathbf{P}_i\) (lesson 8).
- Resync deskew's gyro bias, but only if the LiDAR alone constrains rotation (lesson 7's eigen-ratio, on the rotation block).
- Insert the deskewed scan into the map, only if the pose was trusted.
This flow spans three functions.processScan()
(deskew, downsample, register, insert), registerScan()
(the warm-up switch), and registerScanTight(), lines 284–407.
Now close your eyes on the list and rebuild it:
- Deskew the scan with the gyro
- Preintegrate the IMU since the last scan
- Predict the new state with the deltas
- Stack LiDAR, IMU, bias and gravity rows
- Solve the 18×18 system and retract
- Accept, or coast on the prediction
- Carry the posterior covariances forward
- Insert the scan into the map
Steps 4–5 repeat until the increment is small, re-associating points each time, the ICP loop from lesson 6 over a bigger state. Coasting (step 6) skips steps 7 and 8, and the deskew resync sits between them.
The loose path's translation eigen-ratio is not a rejection gate in this tight loop. The rotation-block ratio controls only deskew bias feedback. Carrying a posterior means extracting blocks of the full inverse information, as derived in lesson 8; the total information already includes the priors.
3 · Justify every weight
For any estimator, a residual scale states how much error that measurement model expects. Compare weights only after accounting for units, correlations, and robust losses. Calibrate them from the sensor and the full residual, then test their effect on held-out motion and geometry.
Case study: measured weight failures
These historical experiments motivated the weights below. The outcomes depend on the dataset and estimator revision; changing a noise setting does not universally produce the same failure:glass-lio
docs/implementation/5-registration.md §3.11–3.12; the case study is in
docs/implementation/testing.md §12.
| Block | Weight | Set wrong, glass-lio measured… |
|---|---|---|
| LiDAR | \(1/\sigma^2\), with \(\sigma\) the sensor's real point-to-plane noise | set larger than the real noise, the IMU over-integrated velocity and the trajectory drifted far; calibrated, it matched the loose baseline |
| IMU | \(\Sigma_{\text{eff}}^{-1}\), \(\Sigma_{\text{pre}} + \mathbf{J}_i\mathbf{P}_i\mathbf{J}_i^{\top}\) | with \(\mathbf{x}_i\) treated as exact, the IMU's information overruled the LiDAR and pinned the pose ("the FREEZE") |
| Bias prior | \(P_b^{-1}\), the inverse carried bias covariance | a random-walk constant alone can't say how wrong the bias was to begin with, so the accel bias would stay frozen |
| Gravity prior | \(\mathbf{I}/\sigma_g^2\), stiff, anchored to \(\mathbf{g}_{\text{init}}\) | anchored to its own estimate, \(\mathbf{g}\) wandered far from its true value and threw the pose off the planet |
One structural fact ties them together: a LiDAR row has zeros in the velocity, bias and gravity columns (lesson 6). The IMU couples velocity, biases, and gravity to the pose; the bias and gravity priors also constrain their own variables. LiDAR influences velocity indirectly through the joint IMU constraints, including their orientation and bias couplings. Its weight affects the estimate, but does not alone determine velocity error.
x = boxplus(x, dx.head<kNavDim>());
g += dx.segment<3>(kIdxGrav);
That's the retraction: \(\mathrm{Exp}\) on the right for rotation, plain addition for everything else, gravity included.tight_registration.cpp,
lines 148–169: solve, retract, and stop once both the translation and rotation increments are small.
Explain the idea: one scan
Put LiDAR and IMU measurements on a consistent time interval and in consistent frames. Use the IMU to predict motion, associate LiDAR points with map geometry, and combine their residuals with priors using justified uncertainty weights. Iterate the state update when the problem is nonlinear, then check whether the measurements constrained the answer. Carry an appropriate uncertainty summary forward and add the scan to the map only when its pose is trusted. The exact state, solver, gates, and feedback rules depend on the estimator.
4 · Who holds what: the information budget of one scan
Section 3 justified each weight on its own. Put them side by side and you can see which block actually determines each part of the state over one scan:
| state | LiDAR | IMU factor (one scan) | prior | so it is held by… |
|---|---|---|---|---|
| position | strong, along the normals it sees | strong | none | both, and they argue |
| rotation | strong where points have lever arms | very strong | none | both |
| velocity | zero | moderate | none | the IMU, disciplined through position |
| gyro bias | zero | through rotation | moderate initially | prior, then rotation evidence |
| accel bias | zero | weakly, needs turns | weak initially | almost nothing, for a long time |
| gravity | zero | weakly, over one scan | stiff | the prior, on purpose |
Read the right-hand column as a list of vulnerabilities. Velocity has no direct measurement at all: the LiDAR row has zeros there, so velocity is only as good as the IMU's integration, checked through the position it produces. That is why an under-trusted LiDAR let velocity run away. The accelerometer bias starts weakly known and gains little per scan (lesson 4: only turns reveal it); its random walk adds only a tiny variance per scan compared with its initial uncertainty, so the prior dominates for a long time. That is the design working as intended: a weakly observable state should move slowly. Gravity is the one state where the prior is meant to win, because over one scan nothing else can tell a small gravity tilt from a small velocity or bias error. An anchor that followed its own estimate would have given it no restoring force, which is exactly the runaway failure.
5 · Debugging symptoms and state fingerprints
In the glass-lio case study, the hardest bugs were not found by staring at the trajectory. They were found by logging the
state: velocity, bias, gravity, rejections, scan by scan. Each failure has a fingerprint in
those numbers that points at one term of this solve.docs/implementation/testing.md
§12.4, “Log the state, not the pose”, and the case study around it. Here is every failure this
course has met, arranged as you would meet them:
| what you see | fingerprint in the state | cause | lesson |
|---|---|---|---|
| height falls, quadratically, after the first turn | constant fake acceleration about the size of a sensor bias, zero before turning | accelerometer bias baked into a world-fixed gravity | 1 |
| a stationary sensor drifts | velocity grows at rest | specific-force sign, accel units, or rotated gravity | 1, 2 |
| accelerates along the direction of travel | error ∝ speed, every scan | preintegration window longer than the interval between the poses | 2 |
| a snapshot sensor coasts forever | every IMU window has zero length | scan duration zero; the interval never advances | 2, testing §12.6 |
| other sensors outvoted, residuals grow unrejected | IMU information dwarfs everything | previous state treated as exact | 3, 8 |
| rejections rise after “loosening” the bias | \(|\mathbf{b}^a|\) ramps, absorbing error | a free bias against an over-certain previous state | 4, 8 |
| velocity ramps smoothly on bland stretches | \(|\mathbf{v}|\) climbs to implausible speeds, biases sane | lidar_sigma larger than the real noise | 5 |
| trajectory far too long, RMSE healthy | small eigen-ratio of \(\mathbf{H}_t\) | degenerate geometry | 7 |
| the pose leaves the planet | \(\mathbf{g}\) drifts steadily away from its initial value | gravity prior anchored to its own estimate | this lesson, §3 |
Notice what the second column has in common: every fingerprint is a state quantity, and almost none is visible in the pose alone. A pose that runs away looks the same whatever the cause; the velocity, the bias and the gravity vector each break in their own way. When you build an estimator of your own, log every state variable from the first day, not only the trajectory.
6 · What this solve still leaves out
An honest capstone ends with the edges. glass-lio's tight solve is a careful, measured design, and it is also a deliberately partial one. Each gap below is a real next step, and each connects to a lesson you now have:
- No window. Only the newest state is solved; the previous one is marginalized out of a single factor (lesson 8). A fixed-lag window would let later scans correct earlier states, and it is where preintegration's savings grow largest (lesson 3).
- Partial covariance. The carried 15×15 navigation block includes position–bias correlations, but gravity cross-correlations and the complete joint history are not carried forward (lesson 8). A full-state filter would propagate the complete current-state covariance.
- A fixed gravity anchor. Gravity can move only within its prior around the initial value. That is right for odometry; a system that ran for hours might re-level gravity against a longer history.
- No loop closure. The map forgets geometry beyond a configured range, so revisiting a place corrects nothing. This is odometry, not SLAM, by choice.
- Consistency is untested. The covariance has never been checked against ground truth with a statistic like NEES (lesson 8). It is plausible; it is not yet proven honest.
Where to go next
Read FAST-LIO2's iterated Kalman filter to see the full-covariance route to the same problem, and
VINS-Mono's marginalizing window to see the other one. Then read glass-lio's
docs/implementation/testing.md
§12 end to end. It is the whole of this course happening for real, including the parts where careful
reasoning was wrong and a logged number was right.
7 · Practice
From memory, no scrolling. The last question comes from lesson 1, where this started.
Why might a tight LiDAR–inertial solver need an external velocity estimate at startup?
A static window can't tell rest from constant velocity, and the test bag starts already moving. Seed \(\mathbf{v} = 0\) and the IMU factor fights the LiDAR. Loose ICP measures the velocity first; the tight solver starts from it.
What does \(\Sigma_{\text{eff}} = \Sigma_{\text{pre}} + \mathbf{J}_i\mathbf{P}_i\mathbf{J}_i^{\top}\) prevent?
With \(\mathbf{x}_i\) held exact (\(\mathbf{P}_i = 0\)), the IMU's information over one scan is enormous and pins the pose. Inflating by \(\mathbf{x}_i\)'s uncertainty is "the closed-form marginalization of a Gaussian x_i out of this one factor": lesson 8, applied.
Why is the gravity prior anchored to the initial value, not to the previous scan's estimate?
Gravity is nearly unobservable over one scan. An anchor that chases the estimate has no restoring force, so every small pose error is explained by tilting \(\mathbf{g}\): it wandered far from its true value and threw the pose off the planet.
In the historical experiment described above, what happened when lidar_sigma was set larger than the sensor's real noise?
A bigger \(\sigma\) trusts the LiDAR less. In that experiment, reducing LiDAR's influence on the joint IMU constraints allowed velocity to drift on a bland stretch. A global scale only cancels in the LiDAR-only loose path, never once a second sensor shares \(\mathbf{H}\).
When should an estimator avoid feeding a newly estimated gyro bias back into LiDAR deskew?
Below the rotation eigen-ratio gate, the refined bias is mostly IMU dead-reckoning the LiDAR can't verify. On Corridor02, resyncing anyway corrupted every later scan's geometry and diverged.
From lesson 1. What does the accelerometer in this whole pipeline actually measure?
Specific force, \(\mathbf{R}^{\top}(\mathbf{a} - \mathbf{g})\) plus bias and noise. Every line of this lesson rests on that one equation.
Which block of the tight solve measures velocity directly?
The LiDAR rows have zero velocity columns. Velocity enters only through the IMU factor, which ties it to position and rotation, so it is disciplined indirectly, through the position the LiDAR does measure.
In a log you see \(|\mathbf{v}|\) and \(|\mathbf{b}^a|\) behaving sensibly while \(\mathbf{g}\) drifts steadily away from its initial value. Which term do you inspect first?
A gravity vector that wanders while velocity and bias stay sane is the fingerprint of a gravity prior with no restoring force: anchored to its own moving estimate instead of the initial value.
Early in a run, what mostly determines the accelerometer-bias estimate?
The LiDAR has zero columns for it, and over one scan it is indistinguishable from a small gravity tilt (lesson 4). Its prior information dominates until enough turns have separated it from gravity.
From memory: name the four blocks of the tight solve, what each one's weight is, and one thing that went wrong when glass-lio set it badly. Then distinguish direct LiDAR constraints from the IMU couplings and the bias/gravity priors.
- LiDAR: \(1/\sigma^2\) per point, \(\sigma\) the real point-to-plane noise, Huber; set too large, velocity ran away
- IMU: \(\Sigma_{\text{eff}}^{-1} = (\Sigma_{\text{pre}} + \mathbf{J}_i\mathbf{P}_i\mathbf{J}_i^{\top})^{-1}\); without the inflation, the freeze
- Bias prior: inverse of the carried bias marginal covariance; update that covariance from the bias block of \(H_{\rm total}^{-1}\) after the solve
- Gravity prior: stiff, anchored to \(\mathbf{g}_{\text{init}}\); anchored to itself, gravity ran away
- Velocity, both biases and gravity: the LiDAR's columns there are zero
That's the whole solve in five lines. Try this box again after a night's sleep; it's the best single check that the course has stuck.