Sensor Fusion: Classical to Modern · Lesson 17 of 22

LiDAR-Inertial Fusion & Factor Graphs

A LiDAR draws the world in precise three-dimensional ink — but it draws slowly, smears the page when the robot moves, and goes blind in a featureless tunnel. An IMU is fast, never blinks, and drifts. Fuse them and you get a map you can trust. And the language that ties it all together — the modern lingua franca of robot estimation — is the factor graph.

Prerequisites: the Kalman filter (Lesson 5) + the IMU (Lesson 13) + a little linear algebra. We re-derive what we need.
11
Chapters
5
Simulations
0
Assumed Knowledge

Chapter 0: A Beautiful Map, Drawn by a Sensor That Can't Hold Still

Picture a delivery robot rolling down a warehouse aisle. On its roof spins a LiDAR — a laser rangefinder that fires thousands of beams a second, each one bouncing off a shelf or a wall and coming back to report, to the centimeter, exactly how far away that surface is. Sweep the laser around and you get a point cloud: a dense, metric, three-dimensional sketch of the room. No camera gives you geometry this clean. The walls are walls, the shelves are shelves, and every point carries a real distance in meters. If you wanted to draw a robot a map of the world, the LiDAR is the pen you'd hand it.

But that pen has three flaws, and they are the entire reason this lesson exists. First, it is slow. A spinning LiDAR completes one full revolution about ten times a second — 10 Hz. Between two complete scans, 100 milliseconds pass, and in 100 milliseconds a robot moving at walking speed has traveled fifteen centimeters and possibly turned several degrees. So you get a gorgeous picture of the world only ten times a second, with nothing to tell you what happened in between.

Second — and this one is sneaky — the LiDAR smears. A spinning laser does not capture the whole point cloud in a single instant, the way a camera flash freezes a scene. It sweeps. The first beam in a scan fires at the start of the revolution; the last beam fires 100 ms later, after the robot has moved. So the "single" point cloud is really thousands of points captured from thousands of different robot positions. If the robot is moving, a straight wall comes back bent, because the early points were measured from one place and the late points from another. This is motion distortion, or scan skew, and an uncorrected skewed cloud poisons everything downstream.

Third, the LiDAR can go blind in plain sight. Roll it into a long, straight, featureless tunnel — or out into an open field, or down a corridor of identical doors — and the laser sees the same geometry no matter where along the axis the robot sits. The point cloud at the tunnel entrance looks identical to the point cloud ten meters in. The LiDAR can tell you precisely how far the walls are sideways, but it has no idea how far the robot has slid forward. We call this geometric blindness degeneracy, and it is the LiDAR's tunnel-equivalent of GPS dying under an overpass.

Common misconception. "A better, faster LiDAR fixes all this." A faster scanner reduces smear and a denser one sees finer features — but no LiDAR, at any price, can recover forward motion in a perfectly self-similar tunnel, because the information simply isn't in the geometry. Degeneracy is structural, exactly like GPS needing a satellite line-of-sight. The cure is not a better laser; it is a second sensor whose blind spots are different. That second sensor is the IMU.

Now bring in the IMU — the accelerometer-and-gyroscope chip you met in Lesson 13. It is the LiDAR's mirror image. It runs at hundreds to a thousand hertz (fast where the LiDAR is slow). It measures motion continuously and instantaneously, so it knows exactly where the robot was at each microsecond a laser beam fired (it can un-smear the scan). And it works everywhere — tunnel, field, corridor — because it feels motion directly rather than reading geometry (it sees through degeneracy). Its only flaw is the one we hammered in Lessons 0 and 13: it drifts, because position comes from integrating noisy acceleration twice.

Line them up and the complementarity is almost too perfect:

PropertyLiDARIMU
Rateslow (~10 Hz)fast (~100–1000 Hz)
What it givesprecise, metric 3-D geometryraw acceleration + angular rate
Absolute or relative?absolute geometry of the scenechange only (must integrate)
Error over timebounded (a scan is as good now as in an hour)drifts (grows as t²)
Motion distortionsmears while it sweepsnone — samples a point in time
Failure modedegenerate in self-similar scenesworks anywhere, always
What it does for the partnerkills the IMU's long-term driftde-skews the scan & bridges scans

Read the last row top to bottom and the fusion writes itself. The IMU de-skews the LiDAR's smeared sweep and bridges the 100 ms gaps between scans, giving a continuous high-rate pose. The LiDAR, in return, registers each de-skewed scan against the world and resets the IMU's drift ten times a second. Each sensor patches exactly the hole the other has. This is the same complementary logic as GPS+IMU from Lesson 1 — only now the absolute anchor is geometry instead of satellites, which means it works indoors, underground, and on Mars.

Watch it before we name anything. Below, a robot drives a path while a LiDAR sweeps. Run it on LiDAR alone (slow, sparse, and — if you crank the speed — smeared), on IMU alone (smooth but drifting), and fused (de-skewed LiDAR resets the drift every scan).

LiDAR alone smears & gaps · IMU alone drifts · fused wins

A robot drives the dashed truth path. Pick a mode and press Run. LiDAR-only jumps between slow scans and bends them at high speed; IMU-only is smooth but slides off truth; fused uses the IMU between scans and snaps to the de-skewed LiDAR each scan. Slide speed up to worsen the LiDAR smear.

Speed 3 ready

LiDAR alone stutters from scan to scan, and as you raise the speed the registered map bends where the smear corrupts it. IMU alone glides but its line peels away from the truth as the run goes on. The fused line is smooth like the IMU and glued to the truth like the LiDAR — because each scan, the laser's geometry yanks the IMU's accumulating drift back to zero.

The one-sentence thesis of this lesson. LiDAR and the IMU are the modern complementary pair: the IMU de-skews the laser and carries pose between scans; the laser resets the IMU's drift with absolute geometry. And the unifying machinery that lets us combine them — plus GPS, plus loop closures, plus everything else — into one consistent estimate is the factor graph, the conceptual capstone of this entire series.

Why this lesson is the capstone

You have spent sixteen lessons building up the pieces: the Kalman filter (Lesson 5), the IMU (Lesson 13), GPS+IMU complementary fusion (Lesson 1). This lesson assembles them into the representation that modern robotics actually ships: the factor graph, where every sensor reading — a LiDAR registration, an IMU integration, a GPS fix, a loop closure — becomes a factor (a soft constraint), and the robot's whole trajectory becomes a graph of variables (poses) tied together by those factors. Solving the graph means finding the trajectory that best satisfies all the constraints at once. And here is the payoff we will prove in Chapter 3: the Kalman filter you already know is just the simplest possible factor graph — a single chain. Everything in this series has been a factor graph in disguise.

Why can't we cure the LiDAR's tunnel problem (degeneracy) just by buying a faster, denser LiDAR?

Chapter 1: Un-bending the Scan — Motion De-skew with the IMU

Before a LiDAR scan can be used for anything, it must be repaired. We saw why in Chapter 0: a spinning laser captures its thousands of points over a full 100 ms sweep, and if the robot moves during that sweep, the points are measured from thousands of slightly different poses. The cloud arrives distorted. The first job of any LiDAR-inertial system — before registration, before mapping, before anything — is to undo that distortion. This is motion de-skew (also called motion compensation or de-warping), and it is the single most important thing the IMU does for the LiDAR.

Let's be precise about what goes wrong. Suppose the robot drives straight at 1.5 m/s past a perfectly flat wall. The LiDAR begins its sweep at time t = 0 and finishes at t = 0.1 s. A point measured early in the sweep is recorded relative to the robot's pose at t = 0; a point measured at the end is recorded relative to the robot's pose at t = 0.1 s — but the firmware naively stamps all of them as if they came from a single pose. Over the 0.1 s sweep the robot moved 1.5 × 0.1 = 0.15 m. So the late points are reported 15 cm closer (or farther) than they truly are relative to the assumed pose. The flat wall comes back as a wall with a 15 cm slope baked in — a wall that isn't real.

The panorama analogy. You've seen this exact failure in a phone camera. Take a panoramic photo by sweeping the phone across a scene while a car drives through it, and the car comes out stretched, bent, or sliced in two — because the panorama, like a LiDAR scan, is built up over time from many instants, not captured in one. A LiDAR sweep is a panorama made of laser pulses, and a moving robot is the car driving through the frame. De-skew is the algorithm that un-bends the panorama using a record of how the camera (the robot) moved.

How the IMU un-bends it

The fix is conceptually simple and it is exactly why we need a fast sensor. The IMU runs at, say, 200 Hz — so during the 100 ms LiDAR sweep it produces about 20 motion samples. Integrate those samples and you get a little movie of the robot's pose throughout the sweep: pose at t = 0, at t = 5 ms, at t = 10 ms, and so on. Now, for every laser point, you know its exact firing time (the LiDAR stamps each point), so you can look up where the robot was at that instant and transform the point back into a single common reference frame — usually the pose at the start (or end) of the sweep.

Formally, let T(τ) be the robot's pose at time τ within the sweep (a position and orientation, obtained by integrating the IMU from the sweep's start). A point p measured at time τ in the sensor frame is corrected to the reference frame by:

pcorrected = Tref−1 · T(τ) · p

In words: take the raw point p, push it out into the world using the pose it was actually measured from, T(τ), then pull it back into the reference pose Tref. Every point gets the transform appropriate to its own timestamp. After this, all points live in one frame as if the robot had been frozen for the whole sweep — the bent wall straightens out.

A worked de-skew, with real numbers

Let's de-skew a single point by hand, keeping it 1-D for clarity. The robot drives forward (call it the +x direction) at a constant 1.5 m/s. The LiDAR sweep runs from t = 0 to t = 0.1 s, and we choose the reference frame to be the robot's pose at the start, t = 0. A particular laser point comes back reporting a wall 3.00 m ahead, and it was fired at τ = 0.08 s into the sweep (near the end).

Step 1 — where was the robot at τ = 0.08 s, relative to the start? It drove forward 1.5 × 0.08 = 0.12 m. So when that beam fired, the robot was 0.12 m ahead of the reference pose.

Step 2 — the raw point says "wall is 3.00 m ahead of me right now," but "me right now" is 0.12 m ahead of the reference. So in the reference frame the wall is at 3.00 + 0.12 = 3.12 m... wait — that's the wrong sign, and getting the sign right is the whole game, so let's be careful. The robot has moved toward the wall by 0.12 m, so by the time the beam fired the robot was closer; a wall that reads 3.00 m from the moved-forward pose is actually 3.00 + 0.12 = 3.12 m from the original reference pose. The de-skewed range is:

rangeref = 3.00 + 0.12 = 3.12 m

Without de-skew, the system would have logged this point at 3.00 m in the reference frame — a 12 cm error on a single point, and points fired even later in the sweep would be off by even more. Multiply that by thousands of points across the sweep, each smeared by a different amount, and the "flat wall" registers as a tilted, fattened smear. De-skew removes it point by point, each correction proportional to how late in the sweep that point was fired.

Why de-skew needs the IMU and not just a velocity estimate. If the robot moved in a perfectly straight line at constant speed, you could de-skew with a single velocity number. But real robots rotate during a sweep, and rotation smears points in a way that depends on their angle and range — a constant-velocity assumption can't capture it. The IMU's gyroscope measures that rotation directly, hundreds of times per sweep, so de-skew can apply the correct rotational correction to each point. This is the deep reason a high-rate IMU is mandatory and a low-rate one (or wheel odometry) is not good enough: the correction must be sampled finely within a single 100 ms sweep.

Below, drive the robot through a structured scene and watch the LiDAR's raw (smeared) scan versus the IMU-de-skewed scan, with the registered map building up. Crank the speed slider and the raw scan bends badly while the de-skewed one stays crisp.

Flagship: raw vs IMU-de-skewed sweep, map building up

A robot drives through a room of walls (the true geometry, dashed). The LiDAR sweeps as it moves. Raw scan is smeared by motion; de-skewed scan uses the IMU to un-bend it; the accumulated map grows from the de-skewed clouds. Raise speed to worsen the smear — watch the raw walls bend while the de-skewed walls stay straight.

Speed 4
press Drive — toggle de-skew off to see the map smear
A LiDAR sweeps for 100 ms while the robot drives forward at 2 m/s. Without de-skew, by roughly how much is a point fired at the very end of the sweep mislocated, and what fixes it?

Chapter 2: Scan Registration — Aligning Two Clouds, the Right Way

We now have clean, de-skewed point clouds — one every 100 ms. The next question is the heart of LiDAR odometry: given two overlapping point clouds, how much did the robot move between them? The answer is registration: find the rigid transform (a rotation and translation) that best snaps the new cloud onto the previous one. If you know how the clouds line up, you know how the robot moved, because the world didn't move — only the sensor did.

The classic algorithm is Iterative Closest Point (ICP), and it's a beautifully simple loop. (1) For each point in the new cloud, find its closest point in the old cloud — guess that they're the same physical surface. (2) Compute the transform that, on average, best aligns those guessed pairs. (3) Apply the transform, which moves the new cloud closer. (4) Repeat — the closest-point guesses get better each round, and the clouds slide into alignment over a handful of iterations.

Why point-to-point ICP is the wrong residual

The naive version, point-to-point ICP, measures alignment error as the straight-line distance between each paired point and its closest neighbor. Minimize the sum of those squared distances and you've aligned the clouds. It works — but it fights the geometry, and here's why. A LiDAR scan samples a surface, but it never samples the same spot on that surface twice. Sweep a flat wall now, drive forward, sweep it again: the second scan hits the wall at slightly different spots than the first. Point-to-point ICP, demanding that each new point land exactly on top of an old point, will try to slide the clouds along the wall to pair up the nearest dots — a motion that doesn't actually reduce the real misalignment at all. The points are allowed to slide freely within the surface, and point-to-point can't tell good sliding from bad.

The key insight: a point only needs to land on the surface, not on a specific point. What we actually want is for each new point to lie on the plane (or edge) that the old points define — not on top of a particular old point. Two scans of a wall are aligned when the new points sit in the wall, free to be anywhere along it. So the right error to minimize is the distance from each new point to the local surface of the old cloud, measured perpendicular to that surface. This is point-to-plane ICP, and it converges far faster and more robustly than point-to-point because it doesn't penalize the harmless sliding-along-the-surface motion.

The point-to-plane residual, derived

Here is the residual that point-to-plane minimizes. Take a new point p and its corresponding old surface, described by a point q on the surface and the surface's unit normal n (the direction perpendicular to the local plane). After applying a candidate transform that moves p to p', the error is the distance from p' to the plane — which is just the component of the gap (p' − q) along the normal:

r = n · (p′ − q)

Read each symbol. p' is the transformed new point. q is a point on the old surface. n is the unit normal of that surface. The dot product n · (p' − q) projects the gap onto the normal direction, throwing away any component that runs along the surface — exactly the component we want to ignore. When p' sits anywhere on the plane, the gap is purely sideways, the dot product is zero, and the residual vanishes. Point-to-plane minimizes the sum of over all correspondences, sliding the cloud until every new point lies in its surface.

A point-to-plane residual, by hand

Numbers make it concrete. Suppose a new point sits at p' = (2.1, 0.3) (2-D for clarity). It corresponds to a wall whose nearest sampled point is q = (2.0, 0.0), and the wall is vertical, so its normal points horizontally: n = (1, 0). The gap is:

p′ − q = (2.1 − 2.0,   0.3 − 0.0) = (0.1, 0.3)

The point-to-point error would be the full length of that gap: √(0.1² + 0.3²) = √0.10 ≈ 0.316. But that 0.3 in the second coordinate is motion along the wall — harmless. The point-to-plane residual projects onto the normal:

r = n · (p′ − q) = (1)(0.1) + (0)(0.3) = 0.1

The point-to-plane residual is just 0.1 m — the true perpendicular distance into the wall — while point-to-point reported 0.316, three times larger, padded with the irrelevant sideways gap. Minimizing 0.1 pushes the point straight into the wall and stops; minimizing 0.316 would also try to drag it sideways to the nearest old dot, a motion that registers nothing. Point-to-plane sees the real misalignment; point-to-point sees noise on top of it.

LOAM: edges and planes, not raw clouds

In 2014 Zhang and Singh published LOAM (LiDAR Odometry and Mapping), which made LiDAR odometry fast and accurate enough to be practical, and its central trick is feature extraction. Rather than registering every one of the tens of thousands of raw points (expensive and noisy), LOAM extracts two kinds of feature points from each scan by looking at local curvature — how much a point's neighbors bend around it:

So LOAM registers each scan by minimizing a mix of point-to-edge and point-to-plane residuals over only the few hundred most informative feature points — a fraction of the full cloud. The edges pin down the directions a plane can't constrain (a smooth wall leaves you free to slide along it, but a corner doesn't), and the planes pin down the directions the edges leave loose. Together they constrain the full 6-degree-of-freedom motion — except in degenerate scenes where the features don't span all directions, which is exactly the failure we'll dissect in Chapter 6.

The registration takeaway. Registration recovers robot motion by aligning a new scan to the previous scan / map. Use point-to-plane (and point-to-edge) residuals, not point-to-point, because LiDAR samples surfaces, never the same point twice — you want each point to land on the surface, free to slide along it, not on top of a specific old point. LOAM extracts a few hundred edge and planar feature points by curvature and registers only those, making it fast and the de-facto template every modern LiDAR-odometry system builds on.

One ICP iteration in code

Here is a single point-to-plane ICP step in commented numpy. It finds correspondences, builds the linearized least-squares system, solves for the incremental transform, and applies it. Run this in a loop and the clouds converge.

python
import numpy as np
from scipy.spatial import cKDTree

def icp_point_to_plane_step(P, Q, normals, R, t):
    # P: new cloud (N,3). Q: old cloud (M,3) with per-point unit normals.
    # R,t: current estimate of the transform mapping P into Q's frame.
    P_t = (R @ P.T).T + t                         # apply current transform to new cloud
    tree = cKDTree(Q)
    _, idx = tree.query(P_t)                       # closest old point for each new point
    q  = Q[idx]                                     # matched surface points
    n  = normals[idx]                               # surface normal at each match

    # Linearize: small rotation w (3,) + translation d (3,). For each pair,
    # residual r = n . (P_t + w x P_t + d - q). Stack into A x = b, x = [w, d].
    A = np.hstack([np.cross(P_t, n), n])           # (N,6) Jacobian rows
    b = -np.sum(n * (P_t - q), axis=1)            # (N,) point-to-plane residuals
    x, *_ = np.linalg.lstsq(A, b, rcond=None)     # normal equations, least squares
    w, d = x[:3], x[3:]

    dR = np.eye(3) + np.array([[0,-w[2],w[1]],   # small-angle rotation update
                          [w[2],0,-w[0]],
                          [-w[1],w[0],0]])
    R_new = dR @ R
    t_new = dR @ t + d
    rms = np.sqrt(np.mean(b**2))                  # registration error this iteration
    return R_new, t_new, rms                        # loop until rms stops dropping

The compact mental model: each iteration linearizes the point-to-plane error around the current guess, solves one least-squares system for the best small twist (rotation w + translation d), applies it, and re-pairs the points. Five or six iterations and the rms bottoms out — that final transform is the robot's motion between the two scans, the raw material of LiDAR odometry.

Play with a registration below: two clouds start offset, and each Step finds correspondences and pulls them into point-to-plane alignment while the residual readout drops toward zero.

Point-to-plane ICP — iterate two clouds into alignment

The reference cloud (a corner of two walls) is fixed. The new cloud starts offset and rotated. Press Step to run one point-to-plane ICP iteration — correspondences (thin lines) form, the cloud snaps onto the surfaces, and the residual falls. Auto runs to convergence; the slider sets the starting offset.

Start offset 60
iteration 0 — press Step
Why does point-to-plane ICP converge faster and more robustly than point-to-point ICP when aligning two LiDAR scans of a flat wall?

Chapter 3: Factor Graphs — The Grammar of Robot Estimation

We can now de-skew a scan and register it to recover motion. But a robot doesn't take one step — it takes thousands, accumulating odometry that drifts, occasionally getting a GPS fix, occasionally recognizing a place it has been before. How do we fuse all of these heterogeneous measurements into one consistent estimate of the entire trajectory? The answer, and the conceptual heart of this lesson, is the factor graph. It is the representation that unifies everything in this series — including, as we'll prove, the Kalman filter you already know.

A factor graph is a picture of an estimation problem with two kinds of nodes:

The graph is bipartite: factors only connect to variables, never to each other. An odometry measurement (the robot moved forward 1 m) becomes a factor connecting two consecutive pose variables. A GPS fix becomes a factor connecting one pose to a fixed global anchor. A loop closure (the robot recognizes it's back at an earlier place) becomes a factor connecting two non-consecutive poses, reaching across the graph. An IMU integration becomes a factor connecting two poses (and their biases/velocities). Every sensor reading is a factor; the trajectory is the variables.

What a factor really is: a negative-log-likelihood. Each factor is a little function that scores how surprised a measurement would be by a candidate value of its variables. Precisely, a factor is the negative log-likelihood of its measurement — small when the variables explain the measurement well, large when they don't. Because sensor noise is (modeled as) Gaussian, that negative log-likelihood is a squared residual: a factor's "cost" is residual² / σ² — the registration error, the GPS gap, the IMU mismatch, each weighted by how noisy that sensor is. A factor graph is, quite literally, a sum of weighted squared residuals waiting to be minimized.

MAP estimation = minimize the sum of squared residuals

The whole point of building the graph is to solve it. "Solving" means finding the assignment of all the variables (the whole trajectory) that is most probable given all the measurements — the maximum a posteriori (MAP) estimate. Since each factor contributes a squared residual and probabilities multiply (so log-probabilities add), MAP estimation becomes a single grand least-squares problem:

x̂ = argminxi  || ri(x) ||² / σi²

where x is the stacked vector of all variables (every pose), each ri(x) is the residual of factor i (how badly the current trajectory violates measurement i), and σi² is that measurement's noise variance (precise sensors get a small σ, so their factors weigh heavily — the inverse-variance weighting from Lesson 1, now operating on the whole graph at once). The trajectory that minimizes this sum is the one that best reconciles all the sensors simultaneously. We solve it with Gauss-Newton (or Levenberg-Marquardt): linearize each residual, solve one big linear system for the best correction, update, repeat until it stops improving — exactly the inner loop of the ICP step from Chapter 2, now applied to the entire trajectory.

A worked 2-pose factor-graph solve, by hand

Let's solve the smallest non-trivial graph completely, in 1-D, to see MAP estimation actually compute. Two pose variables, x0 and x1, and three factors:

The cost is the weighted sum of squared residuals:

J = (x0)²/1  +  ((x1−x0)−10)²/1  +  (x1−11)²/4

Minimize by setting the two partial derivatives to zero. ∂J/∂x0 = 0 gives 2x0 − 2((x1−x0)−10) = 0, i.e. 2x0 − x1 = −10. And ∂J/∂x1 = 0 gives ((x1−x0)−10) + (x1−11)/4 = 0, i.e. −x0 + 1.25 x1 = 12.75. Two linear equations:

2x0 − x1 = −10     −x0 + 1.25 x1 = 12.75

Solve. From the first, x1 = 2x0 + 10. Substitute: −x0 + 1.25(2x0 + 10) = 12.75 → −x0 + 2.5x0 + 12.5 = 12.75 → 1.5x0 = 0.25 → x0 = 0.167. Then x1 = 2(0.167) + 10 = 10.33.

Look at what the graph did. The odometry said "moved +10 from the start," the GPS said "you're at 11," and the prior said "start at 0." These three disagree (0+10 = 10, but GPS says 11). The MAP solution, x1 = 10.33, sits between the odometry's 10 and the GPS's 11 — closer to 10 because the GPS factor is four times noisier (σ² = 4 vs 1), so it pulls more gently. The prior and odometry also nudged x0 slightly off zero to 0.167. Every measurement got a vote weighted by its precision, and the graph found the single trajectory that compromises among all of them optimally. That is MAP estimation, and it scales from these two poses to a million-pose lifelong map by exactly the same recipe.

The punchline of the whole series: the Kalman filter is a chain-structured factor graph. A Kalman filter (Lesson 5) processes poses one at a time: a motion model links pose k to pose k+1 (an odometry factor), a measurement constrains each pose (a GPS-like factor), and that's it — a single chain of poses with no loops. Running the filter forward is exactly solving that chain graph, but only ever looking at the newest variable and summarizing the past into one Gaussian (the current estimate + covariance). The factor graph is the general object; the Kalman filter is the special case where the graph is a chain and you refuse to revisit old variables. Everything you learned about the Kalman filter — inverse-variance weighting, the gain as a trust slider — is the chain-graph instance of the general MAP solve above.

Why the graph view buys you so much

If the Kalman filter already solves the chain, why bother with the full graph? Because real robotics breaks the chain. A loop closure — "I've been here before" — is a factor connecting two poses that are far apart in time but close in space; it makes the graph non-chain, with a cycle, and a forward-only filter literally cannot represent it (it already threw away the old pose). The factor graph keeps all the poses as variables, so a loop-closure factor can reach back and tie pose 1000 to pose 5, and re-solving the graph propagates that correction backward through the whole trajectory — un-bending all the accumulated drift at once. That global, retroactive correction is smoothing (as opposed to the filter's forward-only filtering), and it is what the next chapters are built on.

In a factor graph, what does a single factor represent, and what is MAP estimation actually computing?

Chapter 4: Two Architectures — The Graph Way and the Filter Way

With de-skew, registration, and factor graphs in hand, we can describe the two great families of modern LiDAR-inertial systems. They make opposite engineering bets, and choosing between them is the central practical decision of the field. One is the factor-graph / loosely-coupled approach, exemplified by LIO-SAM; the other is the tightly-coupled iterated filter, exemplified by FAST-LIO2.

LIO-SAM: everything is a factor in one pose graph

Shan and colleagues' LIO-SAM (2020) is the canonical factor-graph LiDAR-inertial system, and it is "loosely coupled" in a specific sense: the LiDAR scans are first processed into relative pose measurements (LiDAR odometry, à la LOAM), and those poses, not the raw points, enter the graph. The graph then fuses four kinds of factors into one pose graph solved incrementally by iSAM2 / GTSAM:

What "IMU preintegration" means and why it's clever. Between two keyframes that are 100 ms apart there are ~20 IMU samples. Naively you'd add 20 tiny factors, ballooning the graph. Preintegration (Forster et al., 2015) instead sums those 20 readings ahead of time into a single relative-motion measurement — "between these two keyframes, the IMU says you moved like this" — with its noise correctly propagated. The trick is that the sum is computed independently of the (unknown) initial pose and bias, so it doesn't have to be redone every time the optimizer changes those during solving. One compact, reusable factor replaces twenty. Preintegration is what makes IMU-in-a-factor-graph fast enough to be real.

The win of the graph approach is everything Chapter 3 promised: it naturally fuses heterogeneous sensors (just add a factor type), it does smoothing (loop closures retroactively fix the whole trajectory), and it's globally consistent (GPS factors anchor it to the world). The cost is that maintaining and re-solving a graph — even incrementally — is heavier than a filter, and LIO-SAM works on keyframe poses (processed LiDAR odometry) rather than raw points, which is the "loose" coupling.

FAST-LIO2: fuse raw points directly in a tight iterated filter

Xu and colleagues' FAST-LIO / FAST-LIO2 (2021–2022) makes the opposite bet. It is tightly coupled — it fuses the raw LiDAR points directly with the IMU, with no intermediate "LiDAR odometry" step — and it does so not with a graph but with an iterated error-state Kalman filter (IEKF). The IMU drives a high-rate prediction (propagating the state forward through each IMU sample), and every de-skewed LiDAR point is folded in as a point-to-plane measurement update, with the filter iterating the update (re-linearizing several times) to handle the nonlinearity, like a mini Gauss-Newton inside each step.

The obvious worry: a single scan has tens of thousands of points, so the measurement vector is enormous. A textbook Kalman update inverts a matrix the size of the measurement count — that would be hopelessly slow for thousands of points per scan. FAST-LIO's headline contribution is a reformulation of the Kalman gain using a matrix identity (the Sherman-Morrison-Woodbury identity) so the update's cost scales with the state dimension (a couple dozen numbers: pose, velocity, biases) instead of the measurement dimension (thousands of points):

cost: O(state-dim³)   instead of   O(#measurements³)

Because the state is tiny and fixed (~18 numbers) while the measurements are many and growing, this flips an impossible computation into a cheap one — thousands of LiDAR points get fused per scan in a couple of milliseconds. The second ingredient is the ikd-Tree (incremental k-d tree): a map data structure that supports inserting new points and deleting far-away ones and nearest-neighbor queries, all incrementally, so the map updates in real time without rebuilding. Together these make FAST-LIO2 the low-latency champion: raw points, tightly fused, at very high rate.

The spine: graph vs filter, side by side

LIO-SAM (factor graph)FAST-LIO2 (iterated filter)
Estimation enginefactor graph, iSAM2/GTSAMiterated error-state Kalman filter
Couplingloose (LiDAR odometry → poses)tight (raw points + IMU)
What enters the estimatorrelative-pose factors, preintegrated IMU, GPS, loopsevery de-skewed point as a point-to-plane update
Loop closure / global consistencynative (just add a factor; GPS too)not built in (it's pure odometry)
Smoothing vs filteringsmoothing (revisits the past)filtering (forward-only)
Latencylow, but graph upkeep adds overheadlowest — gain trick + ikd-Tree
Best whenmulti-sensor, loop closure, global mapstightest latency, fewest moving parts
The architecture takeaway. Both fuse LiDAR + IMU, but at opposite ends of a spectrum. LIO-SAM turns scans into pose factors and pours them — with IMU preintegration, GPS, and loop closures — into one incrementally-smoothed factor graph, giving native multi-sensor fusion and global consistency. FAST-LIO2 skips the graph, fuses raw points straight into an iterated Kalman filter, and uses a gain reformulation + ikd-Tree to make thousands of per-scan measurements cheap, giving the lowest latency. Graph = consistency and flexibility; filter = raw speed. Chapter 8 turns this into a concrete choosing guide.

FAST-LIO reformulates the Kalman gain via a matrix identity. What problem does this solve?

Chapter 5: Incremental Smoothing — iSAM2 and the Magic of Loop Closure

Chapter 3 said MAP estimation is one big least-squares solve over the whole graph. But a robot that runs for hours accumulates millions of poses — re-solving a million-variable least-squares problem from scratch every time a new scan arrives is hopeless. The thing that makes lifelong smoothing actually real-time is incremental smoothing, and the algorithm is iSAM2 (incremental Smoothing And Mapping) by Kaess and colleagues (2012), the engine inside GTSAM and LIO-SAM.

The insight is that when a new measurement arrives, it does not change the whole trajectory — it only meaningfully affects a small, nearby part of the graph. A new odometry factor between poses 999 and 1000 mostly touches poses near the end; poses 1 through 900 barely move. So instead of re-solving everything, iSAM2 re-solves only the part of the graph that actually changed. The bookkeeping that makes this exact (not approximate) is a clever data structure called the Bayes tree — a tree-shaped factorization of the graph in which adding a measurement requires updating only the branches from the affected variables up to the root, leaving the rest untouched.

The spreadsheet analogy. A giant spreadsheet with a million formulas: if you change one cell, a good spreadsheet doesn't recompute all million formulas — it recomputes only the cells that depend on the one you changed, following the dependency graph. iSAM2's Bayes tree is exactly that dependency structure for a factor graph. A new measurement "dirties" a few variables; only the formulas downstream of them are recomputed. That's how you smooth a million-pose trajectory at sensor rate — you almost never touch most of it.

Loop closure: the payoff of keeping the past

Here is where smoothing earns its keep, and it is the most visually dramatic moment in all of SLAM. A robot drives a big loop — down a hall, around a building, back toward where it started. Along the way its LiDAR odometry drifts: small registration errors accumulate, so by the time it returns near the start, its estimated position is off by, say, two meters. Its map is bent; the end of the loop doesn't meet the beginning.

Then it recognizes a place. The current scan matches a scan from the very start of the run — "I've been here before." This recognition (via scan context or a similar place-recognition descriptor) produces a loop-closure factor: a relative-pose constraint saying "pose 1000 and pose 5 are actually almost the same place." Adding that one factor to the graph and re-solving snaps the entire drifted trajectory back into global consistency. The two-meter gap closes, and the correction propagates backward through every pose in the loop, distributing the accumulated error smoothly across the whole path. A filter cannot do this — it threw pose 5 away long ago. Only a graph (or smoother) that kept the past as variables can reach back and fix it.

Watch it happen. Below, a robot drives a loop while its odometry drifts (the estimate peels away from the truth). Press Close loop to add the loop-closure factor — the drifted trajectory snaps back and the correction ripples through the whole path.

Loop closure — one factor snaps the drift away

A robot drives a loop. Its odometry estimate drifts away from the true loop (dashed) — the ends don't meet. Press Drive to accumulate drift, then Close loop to add a loop-closure factor tying the end pose back to the start. The graph re-solves and the corrected trajectory snaps into a clean closed loop, the error spread evenly along the path.

Drift rate 4
press Drive, then Close loop

The loop-closure correction, by the numbers

Let's quantify what "snapping back" does, with a tiny version. A robot drives a loop and, by the end, odometry estimates it returned to (2.0, 0.0) — but a loop closure recognizes it is truly back at the start, (0.0, 0.0). The loop-closure factor says "end ≈ start," so the end has a 2.0 m error to erase. If the loop has, say, 100 poses and the drift accumulated roughly uniformly, the smoother distributes the 2.0 m correction across them — about 2.0 / 100 = 0.02 m per pose — rather than yanking only the last pose (which would put a visible kink there). The result is a trajectory that closes and stays smooth, because the least-squares solve spreads the residual to minimize total squared error. This even distribution is the signature of a correct smoother: a kink at the closure point is a bug (Chapter 9).

The smoothing takeaway. iSAM2 makes lifelong factor-graph SLAM real-time by re-solving only the sub-graph a new measurement actually affects (via the Bayes tree), not the whole million-pose graph. Its superpower is loop closure: when the robot recognizes a place, one new factor ties present to past and re-solving propagates the correction backward through the whole trajectory, erasing accumulated drift — something a forward-only filter structurally cannot do, because it discarded the past. Keeping the past as variables is the price of, and the reason for, global consistency.

A robot's LiDAR odometry has drifted 2 m by the time it loops back to a recognized starting place. Why can a factor-graph smoother (iSAM2) fix this when a Kalman filter cannot?

Chapter 6: Degeneracy — When the Geometry Has Nothing to Say

We close the technical arc with the LiDAR's most insidious failure, the one that makes the IMU non-negotiable: degeneracy. We met it in Chapter 0 — a long, straight, featureless tunnel where the LiDAR can't tell how far the robot has slid forward. Now we make it precise: how does a system detect that it's in a degenerate scene, and what does it do about it? The answer is one of the most elegant uses of eigenvalues in robotics.

Recall from Chapter 2 that registration solves a least-squares problem: find the transform minimizing the point-to-plane residuals. That solve has, at its core, an information matrix (the AᵀA from the normal equations, sometimes called the Hessian) — a small matrix, one row/column per degree of freedom of the motion, that encodes how strongly the scan constrains each direction of motion. In a feature-rich scene (corners, walls in every orientation), this matrix is well-conditioned: every direction is constrained, the solve is confident.

In a degenerate scene, one direction is unconstrained. In a straight tunnel, the walls constrain sideways and vertical motion strongly, but along the tunnel axis there is nothing — sliding the cloud forward along the axis changes none of the point-to-plane residuals (the walls look identical). Mathematically, the information matrix has a near-zero eigenvalue, and its eigenvector points along the tunnel. That eigenvalue is the system telling you, quantitatively, "I have no information about motion in this direction."

Eigenvalues as a confidence meter, direction by direction. The eigenvalues of the registration information matrix are the "stiffness" of the solve along each principal direction. A large eigenvalue = "I'm sure how much you moved this way" (a wall pinning sideways motion). A near-zero eigenvalue = "I have no idea how much you moved this way" (the open tunnel axis). The eigenvector of the smallest eigenvalue points at the unconstrained direction. So you detect degeneracy by computing the eigenvalues every scan and watching for one to collapse toward zero — and the eigenvector tells you exactly which way the robot is geometrically blind.

Detecting it, by the numbers

Suppose a registration's information matrix has eigenvalues, in a 2-D (x along the tunnel, y across it) toy case:

λacross = 240   (strong — walls pin sideways motion)
λalong = 3   (near-zero — the tunnel axis is unconstrained)

The standard test is the condition number (ratio of largest to smallest eigenvalue) or simply a threshold on the smallest eigenvalue. Here λalong = 3 is far below a typical threshold (say 100), and the ratio 240/3 = 80 is huge — both scream "the along-tunnel direction is degenerate." The system now knows, this scan, that it cannot trust LiDAR for forward motion.

What you do about it: lean on the IMU exactly there

The fix is the complementary logic of the whole lesson, now applied surgically. Where the LiDAR is confident (the strong eigen-directions), trust the registration. Where it is degenerate (the weak eigen-direction), ignore the LiDAR and fall back on the IMU (or wheel odometry, or any other source) for that direction only. Concretely, you can project out the degenerate direction from the LiDAR update — let the laser correct sideways and vertical motion, but let the IMU carry forward motion through the tunnel until features reappear. This is sometimes called solution remapping (Zhang et al.) and it is precisely why a tightly-coupled IMU rescues LiDAR where a lone LiDAR drifts hopelessly down the axis.

And it connects straight back to Lesson 1's inverse-variance weighting: a degenerate direction is one where the LiDAR's effective variance has shot to infinity (zero information = infinite variance), so the optimal weight on the LiDAR in that direction slides to zero and the IMU takes over — automatically, if you've set up the information matrix correctly. The eigenvalue test is just the multi-dimensional version of "GPS variance → ∞ in the tunnel" from the very first lesson.

See it below. A robot drives down a tunnel; the registration's eigenvalues are shown as two bars. As it enters the tunnel, the along-axis eigenvalue collapses, the LiDAR-only estimate drifts down the axis, and the IMU-aided estimate stays locked to the truth.

Degeneracy — the tunnel collapses one eigenvalue

A robot drives left to right; the middle stretch is a featureless tunnel. The bars show the registration's across-axis and along-axis eigenvalues. In the tunnel the along-axis eigenvalue collapses (geometric blindness). The LiDAR-only estimate drifts along the axis; the IMU-aided estimate detects the collapse and leans on the IMU, staying on truth.

press Drive — toggle IMU aid off to watch LiDAR drift down the tunnel
How does a LiDAR-inertial system detect that it's in a geometrically degenerate scene (e.g. a straight tunnel), and what does it do?

Chapter 7: Use Cases & Real Products — LiDAR-Inertial in the Wild

Every lesson in this series ends with the same four chapters — Use Cases, Practical Application, Debugging, Connections — because a technique you can't point at in a shipped system is one you don't really own. LiDAR-inertial fusion and factor graphs are not academic curiosities; they are the localization-and-mapping backbone of an entire generation of robots. Here is where they run, and the open-source stacks that power them.

Autonomous driving & HD mapping. The high-definition maps that self-driving cars localize against are built by survey vehicles running LiDAR-inertial SLAM — a LiDAR (often a 64- or 128-beam spinning unit) fused with a high-grade IMU and GPS in a factor graph, producing centimeter-accurate maps over entire cities. At runtime the car localizes against that map by registering its live de-skewed scans, with the IMU bridging and de-skewing exactly as in this lesson.

Mobile robots & warehouses. Delivery robots, patrol robots, and warehouse AMRs use LiDAR-inertial odometry (frequently FAST-LIO2 or LIO-SAM directly) to navigate GPS-denied indoor spaces. The IMU is what lets them turn fast corners without smearing the map, and loop closure keeps multi-hour shifts globally consistent.

Drones & aerial mapping. A drone carrying a lightweight LiDAR + IMU can map a forest canopy, a quarry, or the inside of a building — FAST-LIO2's low latency is prized here because a fast-moving, agile drone demands tight, high-rate state estimation that a heavier graph might lag on.

Legged robots. Quadrupeds (Spot, ANYmal) and humanoids fuse LiDAR + IMU + leg odometry. The IMU is doubly essential here — not only for de-skew but because a walking robot's body lurches violently with each step, smearing scans badly; without aggressive de-skew the map is unusable.

Surveying & construction. Handheld and backpack LiDAR scanners (think GeoSLAM, NavVis, and the LiDAR mode in modern phones/tablets) build as-built models of buildings and sites. They are LiDAR-inertial SLAM in a box: walk through a building and the de-skewed, loop-closed factor graph hands you a millimeter-grade point-cloud model.

Search-and-rescue & subterranean. The DARPA Subterranean Challenge was won by LiDAR-inertial systems precisely because tunnels and mines are GPS-denied and degenerate — the exact conditions where IMU-aided degeneracy handling (Chapter 6) is the difference between a map and a lost robot.

The stacks you'll actually use

StackWhat it isFamily
LOAM / A-LOAMthe original LiDAR odometry; edge+planar features (Ch 2)LiDAR-only odometry
LIO-SAMfactor-graph LiDAR-inertial w/ GPS + loop closuregraph (loosely coupled)
FAST-LIO / FAST-LIO2tight iterated-filter LiDAR-inertial, ikd-Treefilter (tightly coupled)
GTSAM / iSAM2the factor-graph + incremental-smoothing librarysolver backend
Ceresgeneral nonlinear least-squares solver (Google)solver backend

Notice the layering. LOAM gave the world LiDAR odometry. LIO-SAM and FAST-LIO2 are the two LiDAR-inertial systems built on top — one graph, one filter — the exact pair from Chapter 4. Underneath the graph systems sits a solver backend, almost always GTSAM (which contains iSAM2) or Ceres, the same factor-graph machinery used for visual SLAM, bundle adjustment, and calibration. Learn the factor graph once and you have the key to all of them.

The deployable insight. If a robot maps a GPS-denied or self-similar space — indoors, underground, a fast drone, a lurching legged platform — it is almost certainly running LiDAR-inertial fusion, and almost certainly LIO-SAM (when global consistency and loop closure matter) or FAST-LIO2 (when latency is king), both standing on LOAM-style feature registration and a GTSAM/Ceres factor-graph or iterated-filter backend. The acronyms change by company; the de-skew → register → fuse-in-a-graph pipeline does not.

A search-and-rescue robot must map a long, straight, GPS-denied mine tunnel. Which property of LiDAR-inertial fusion is the decisive reason it works there?

Chapter 8: Practical Application — Choosing & Tuning a LiDAR-Inertial System

You now know de-skew, registration, factor graphs, the two architectures, smoothing, and degeneracy. This "now build it" chapter turns that into the decisions a real deployment forces on you — which architecture to pick, how to tune feature extraction and loop closure, when to add GPS, and how to fit it all in a compute budget.

Decision 1: factor graph or tight filter?

This is the first fork, and it follows directly from Chapter 4. Run these questions:

Do you need loop closure / global consistency / a reusable map?
Yes → factor graph (LIO-SAM) — loops & GPS are native factors
Are you fusing >2 sensor types (GPS, wheels, multiple LiDARs)?
Yes → factor graph — just add a factor type per sensor
Is the absolute priority lowest latency / agile motion / minimal moving parts?
Yes → tight filter (FAST-LIO2) — raw points, gain trick, ikd-Tree

The rule of thumb: FAST-LIO2 for the tightest, fastest pure odometry (a racing drone, a minimal stack); LIO-SAM for anything needing global consistency or multi-sensor fusion (a survey vehicle, a multi-hour warehouse shift, a robot with GPS). Many real systems run FAST-LIO-style odometry as a front-end and a factor graph as a back-end for loop closure — the best of both.

Decision 2: tuning feature extraction

LOAM-style feature extraction (Ch 2) has knobs that matter:

Decision 3: keyframes and loop closure

You don't add a graph node for every single scan — that's wasteful. You add a keyframe only when the robot has moved enough (e.g. >1 m or >10° since the last keyframe). Keyframes keep the graph sparse while still capturing the trajectory. For loop closure, the workhorse is scan context: a compact rotation-invariant descriptor of a scan that lets you quickly ask "have I seen a scan like this before?" against all past keyframes. When a candidate match is found, you verify it with ICP (Ch 2) before trusting it — because a false loop closure is catastrophic (Ch 9). Tune the matching threshold conservatively: a missed loop closure costs you some drift; a false one corrupts the whole map.

Decision 4: GPS factors for global consistency

Outdoors, even with loop closure, a LiDAR-inertial system slowly drifts in the global frame (its map is internally consistent but may be rotated/translated relative to the Earth). Adding GPS factors — absolute-position constraints on occasional keyframes — pins the trajectory to real-world coordinates and bounds global drift, exactly the complementary role GPS played in Lesson 1, now as one more factor in the graph. Weight them by GPS quality (use the reported covariance); de-weight or drop them in urban canyons where multipath makes GPS lie.

Decision 5: the compute & memory budget

Two structures dominate the budget. The map grows without bound if you keep every point — FAST-LIO2's ikd-Tree handles this by incrementally deleting points far from the robot, keeping a bounded local map. The graph grows with keyframes — iSAM2 keeps re-solving cheap, but for truly lifelong operation you eventually marginalize (summarize and drop) very old parts of the graph. Budget rule of thumb: a few hundred features per scan, voxel-downsampled clouds, keyframes every meter, a bounded local map — and you fit comfortably on an embedded CPU at sensor rate.

The reusable recipe. (1) Need loops / GPS / multi-sensor / a reusable map → factor graph (LIO-SAM); need lowest latency → tight filter (FAST-LIO2); often run both (filter front-end, graph back-end). (2) Extract a few hundred edge + planar features per scan, voxel-downsampled; set the curvature threshold to the scene. (3) Add keyframes by distance/angle, close loops with verified scan-context matches (conservative threshold). (4) Add GPS factors outdoors, weighted by quality. (5) Bound the map (ikd-Tree / marginalization) to fit the compute budget.

A survey vehicle must build a globally-consistent, reusable city map using LiDAR, IMU, and GPS, and will revisit streets (loop closure). Which architecture fits, and why?

Chapter 9: Debugging & Failure Modes — The Six Ways It Breaks

LiDAR-inertial systems fail in characteristic ways, and most failures are quiet — a slowly bending map, a map that suddenly tears in half — rather than loud crashes. This recurring chapter catalogs the classic failure modes, the symptom each shows, and the specific test that catches it before it ruins a map.

Failure 1 — uncorrected motion distortion (the smeared map)

If de-skew is off, mis-timed, or fed a bad IMU, every scan is captured smeared and the accumulated map looks fattened, doubled, or curved — straight walls become thick blurs, especially during fast turns.

Symptom & detection. Symptom: the map is sharp when the robot is still and smears the moment it moves or turns; walls thicken with speed. Detection: drive the same straight wall slowly then fast — if the wall fattens with speed, de-skew is broken. Check that LiDAR point timestamps and IMU timestamps share a clock (this is the time-sync of Lesson 15) and that the IMU is actually being integrated within the sweep. Fix: correct the time offset; verify per-point de-skew.

Failure 2 — degenerate scene drift (the tunnel)

In a tunnel, long corridor, open field, or featureless room, the registration is unconstrained along one direction (Ch 6) and the estimate drifts along that direction — the robot thinks it's standing still while it slides forward, or vice versa.

Symptom & detection. Symptom: position drifts specifically along the geometrically-poor axis; the map "telescopes" or compresses lengthwise. Detection: log the eigenvalues of the registration information matrix — a collapsing smallest eigenvalue is the alarm, and its eigenvector names the blind direction. Fix: detect degeneracy and lean on the IMU (or wheels) for the unconstrained direction; don't let a degenerate registration write a confident (wrong) update.

Failure 3 — bad IMU initialization

A factor graph needs the IMU's initial bias and the initial gravity direction to be roughly right, or preintegration is garbage from the first step. A robot that starts moving before the IMU is initialized, or that mis-estimates gravity, produces a trajectory that tilts or curves immediately.

Symptom & detection. Symptom: the trajectory tilts or drifts badly right from startup, even in a feature-rich scene; the "floor" in the map isn't level. Detection: keep the robot still for the first second or two and check that estimated biases settle and gravity points straight down. Fix: enforce a static initialization period; estimate gravity and biases before motion.

Failure 4 — false loop closure (the torn map)

The most catastrophic failure. A place-recognition descriptor wrongly decides "I've been here before" — two different corridors that happen to look alike — and adds a loop-closure factor between poses that are not the same place. Re-solving then violently warps the map to satisfy the false constraint, often tearing it in two.

Symptom & detection. Symptom: the map is fine, then suddenly folds, tears, or duplicates a region after a loop closure event. Detection: log every loop closure with its match score and the ICP fitness of the verification; a closure that warped the map but had a marginal verification score is your culprit. Fix: verify every candidate loop with ICP and a strict fitness threshold before adding it; use a robust kernel so one bad factor can't dominate the solve; reject geometrically implausible closures.

Failure 5 — extrinsic / time-sync errors

The IMU and LiDAR are rigidly mounted apart, with a fixed transform (the extrinsic) and ideally a shared clock. If the extrinsic calibration is wrong or the clocks are offset (Lesson 15's territory), de-skew applies the IMU's motion at the wrong place or the wrong time, smearing or warping subtly even at low speed.

Symptom & detection. Symptom: a persistent small smear or curvature that doesn't go to zero even when moving slowly, and that depends on rotation rate. Detection: rotate in place — an extrinsic/time-sync error shows up as motion-correlated blur during pure rotation. Fix: re-calibrate the LiDAR–IMU extrinsic and time offset (many systems estimate the time offset online).

Failure 6 — unbounded drift accumulation

Even with everything working, pure LiDAR-inertial odometry drifts slowly — without loop closures or GPS, a long traverse ends up off by a fraction of its length. This is expected, not a bug, but it becomes a problem if you assumed odometry alone would stay globally accurate.

Symptom & detection. Symptom: the map is locally crisp but globally bent — revisited places don't line up, the start and end of a loop don't meet. Detection: drive a known loop and measure the start/end gap. Fix: add loop closures (smoothing fixes it retroactively, Ch 5) and/or GPS factors (bounds global drift, Ch 8). If you need pure odometry, accept and budget for the residual drift.

The debugging discipline. Two tests catch most of it. (1) The same-wall test — drive a straight wall slow then fast; if it fattens, de-skew / time-sync is broken. (2) The known-loop test — drive a closed loop and measure the start/end gap; a large gap means drift you must close (loops/GPS), and a sudden tear after a closure means a false loop. Always log the registration eigenvalues (degeneracy alarm) and every loop closure's verification score (false-closure alarm). Most LiDAR-inertial bugs are a smeared map, a telescoping tunnel, or a torn map — each with a signature you can watch for.

A LiDAR-inertial map looks perfect, then suddenly folds and tears in half right after the robot passes a second identical-looking corridor. What's the most likely bug?

Chapter 10: Connections — Where This Sits in the Whole Map

This lesson is the conceptual high point of the series, because the factor graph is the frame that contains everything else. Let's place it among its neighbors, gather the references, and leave you a cheat-sheet.

How this connects to the rest of the series

The factor graph as the unifying idea

Step back and the whole series collapses into one picture. Every estimator you've met — the Bayes filter, the Kalman filter, the EKF/UKF, LiDAR-inertial SLAM, visual-inertial SLAM — is a factor graph being solved under different assumptions:

MethodGraph shapeHow it's solved
Kalman filtera chainforward-only, Gaussian summary of the past
EKF / iterated EKFa chain, nonlinearforward-only, re-linearized (FAST-LIO2)
Pose-graph SLAMchain + loop-closure edgesbatch or incremental smoothing
Full LiDAR-inertial SLAMposes + biases + GPS + loopsiSAM2 incremental (LIO-SAM)

Learn the factor graph and you have not learned one more method — you've learned the language the rest are dialects of. That is why Dellaert and Kaess titled their book Factor Graphs for Robot Perception: not "a technique," but the perception substrate itself.

The capstone insight. The robot's entire life — every scan, every IMU tick, every GPS fix, every remembered place — is one enormous bipartite graph of poses (variables) and measurements (factors), and "where am I, and what does the world look like?" is just the trajectory that minimizes the sum of all the squared residuals. De-skew prepares the LiDAR factor; registration computes it; preintegration packs the IMU factor; loop closure adds the long-range factor; iSAM2 re-solves only what changed; eigenvalues warn when a factor is blind. Every chapter of this lesson is one verb in that single sentence.

References

  1. Zhang, J. & Singh, S. "LOAM: Lidar Odometry and Mapping in Real-time." Robotics: Science and Systems (RSS), 2014.
  2. Shan, T., Englot, B., Meyers, D., Wang, W., Ratti, C. & Rus, D. "LIO-SAM: Tightly-coupled Lidar Inertial Odometry via Smoothing and Mapping." IROS, 2020. arXiv:2007.00258
  3. Xu, W., Cai, Y., He, D., Lin, J. & Zhang, F. "FAST-LIO2: Fast Direct LiDAR-inertial Odometry." IEEE Transactions on Robotics (T-RO), 2022. arXiv:2107.06829
  4. Kaess, M., Johannsson, H., Roberts, R., Ila, V., Leonard, J. & Dellaert, F. "iSAM2: Incremental Smoothing and Mapping Using the Bayes Tree." International Journal of Robotics Research (IJRR), 2012.
  5. Dellaert, F. & Kaess, M. "Factor Graphs for Robot Perception." Foundations and Trends in Robotics, 2017.
  6. Forster, C., Carlone, L., Dellaert, F. & Scaramuzza, D. "On-Manifold Preintegration for Real-Time Visual-Inertial Odometry." IEEE Transactions on Robotics, 2017. arXiv:1512.02363

Cheat-sheet

ConceptOne-line takeaway
De-skewuse the high-rate IMU to transform each point by its own timestamp's pose into one frame — un-bend the smeared sweep
Point-to-plane ICPminimize r = n·(p′−q): perpendicular distance into the surface, ignoring harmless sliding along it
LOAM featuresextract a few hundred edge (high-curvature) + planar (low-curvature) points; register only those
Factor graphvariables (poses) + factors (measurements = squared residuals); MAP = minimize the sum of all squared residuals
Kalman = chain grapha Kalman filter is a chain-structured factor graph solved forward-only
LIO-SAMfactor graph: LiDAR-odom + preintegrated-IMU + GPS + loop factors, smoothed by iSAM2 — global consistency
FAST-LIO2tight iterated Kalman filter on raw points; gain reformulation + ikd-Tree — lowest latency
IMU preintegrationsum many IMU samples into one reusable relative-motion factor, independent of initial pose/bias
iSAM2 / Bayes treere-solve only the sub-graph a new measurement affects — lifelong smoothing in real time
Loop closureone factor tying present to a recognized past pose; re-solving propagates the correction backward, erasing drift
Degeneracynear-zero eigenvalue of the registration information matrix = an unconstrained direction; lean on the IMU there

"What I cannot create, I do not understand." — Richard Feynman. You have now seen, derived, and simulated every piece of a LiDAR-inertial SLAM system; the factor graph is the single sentence that holds them together, and you can write it.

What is the single deepest connection this lesson draws to the rest of the series?