Every filter so far quietly assumed you already knew which measurement updates which state. In the real world you get a cloud of detections each frame — some real, some clutter, some missing — and you must decide the correspondence before you can fuse. Get it wrong and the filter fuses garbage. This is the hard, underappreciated half of fusion.
Go back through every lesson in this series so far. The Kalman filter took a measurement z and folded it into the state. The EKF and UKF did the same with nonlinear sensors. The particle filter reweighted particles against z. In every single case, the moment a measurement arrived, the filter knew what it was a measurement of. The GPS reading was the car's position. The range to the landmark was the range to that landmark. Nobody ever asked: are you sure?
That question is the whole subject of this lesson. In the real world a sensor does not hand you one clean, pre-labeled measurement. A radar sweep returns a set of blips. A camera detector fires a list of bounding boxes. A LiDAR frame yields a cloud of clustered returns. You are tracking maybe five aircraft, or twelve pedestrians, or eight cells under a microscope — several targets at once — and each frame you receive a basket of detections with no labels on them. Before you can run a single Kalman update, you must answer: which measurement came from which target?
Why is it hard? Three independent headaches arrive together, and any one of them alone would already be annoying:
Put concretely: you maintain N tracks (each is a Kalman prediction — a predicted measurement plus an innovation covariance S that says how uncertain it is) and you receive M measurements this frame. You must build a correspondence between the two sets, knowing that some of the M are clutter, some of the N produced no measurement, and the rest pair up in some unknown way. Only after you fix that correspondence can you fuse.
The instinct is irresistible: for each track, grab the nearest measurement by ordinary Euclidean distance. It feels obviously right, and it is exactly what breaks first. Picture two pedestrians, A on the left and B on the right, crossing toward each other. Track A predicts its target at x = 0; track B predicts at x = 8 (meters). Two measurements arrive: z1 = 1 and z2 = 5.
Greedy nearest-neighbor processes track A first: its nearest measurement is z1 (distance 1) — fine. Then track B: its nearest remaining measurement is z2 (distance 3). That happened to work. But now move them closer: track A at x = 0, track B at x = 2, measurements z1 = 1.6 and z2 = 0.5. Greedy A grabs z2 (distance 0.5, its nearest), leaving track B stuck with z1 (distance 0.6). But the globally best pairing was A→z1 and B→z2 — A made a locally-greedy choice that forced B into a worse one. One greedy pick poisoned the whole frame.
Below, watch the core scene this whole lesson lives in: two tracks, each with a predicted position and an uncertainty gate, surrounded by a scatter of measurements — some real, some clutter. Notice immediately that "closest" is not obvious: a measurement can be physically near one track yet sit outside its uncertainty region while comfortably inside another's.
Two tracks sit with their predicted positions (crosses) and their elliptical uncertainty gates. Around them are measurements — some are real returns, some are clutter. Press New frame to redraw the cloud. The question for the rest of the lesson: which dot belongs to which cross?
Some measurements clearly belong to a track; some are clutter sitting in empty space; some are ambiguous, lying where two gates overlap. The rest of this lesson is a ladder of increasingly clever ways to resolve exactly that picture: gate out the impossible (Ch 1), assign greedily (Ch 2), assign optimally per frame (Ch 3), refuse to commit and weight everything (Ch 4–5), or defer the decision across many frames (Ch 6).
Before we can decide which measurement belongs to a track, we should throw away the measurements that couldn't possibly belong to it. A radar tracking a jet at 30,000 feet should not even consider a blip that appeared two miles away — the jet cannot have teleported. This pre-filter is called validation gating: around each track's prediction we draw a region, and only measurements that fall inside the region are candidates. Everything outside is rejected before any assignment math runs. Gating is the cheap first move that makes everything after it tractable.
But "draw a region" needs a shape and a size, and getting those right is the whole art. The naive choice — a circle of fixed radius — is wrong for the reason Chapter 0 flagged: a track is not equally uncertain in every direction. A car moving fast along a road is very sure of its lateral position (it stays in its lane) but quite unsure of how far it has driven forward this frame. Its uncertainty is a stretched ellipse, long in the direction of travel, thin across it. A circular gate would reject a perfectly plausible forward measurement while accepting an implausible sideways one.
Recall from the Kalman filter that each frame the track makes a predicted measurement — where it expects its target to show up — call it ẑ (z-hat). When a real measurement z arrives, the difference is the innovation (also called the residual):
where ν (nu) is the innovation: how far the measurement landed from where the track expected it. The track also knows how surprised it should be by a given innovation, captured in the innovation covariance S — a matrix that combines the track's own prediction uncertainty with the sensor's measurement noise. A large S in some direction means "I'm very unsure along here, so a big innovation in this direction is not alarming." A small S means "I'm confident here, so even a small surprise is suspicious."
The Mahalanobis distance squared measures how far the innovation is from zero, scaled by the uncertainty in each direction:
Read it piece by piece. ν is the innovation vector (the raw miss). S−1 is the inverse of the innovation covariance — it divides the miss by the variance in each direction, so a miss along an uncertain axis is shrunk (forgiven) and a miss along a confident axis is amplified (penalized). The νT … ν sandwich just turns the whole thing into a single non-negative number. The result d² is "squared distance in units of standard deviations," and it is dimensionless — a 2 m miss and a 2 rad miss become comparable because each is divided by its own uncertainty.
Now the magic. If the measurement truly came from this track, the innovation ν is zero-mean Gaussian with covariance S (that is exactly the Kalman filter's assumption). A known fact of statistics: for such an innovation, d² follows a chi-square distribution with degrees of freedom equal to the measurement dimension m. So d² is not just a number — it is a number whose distribution we know precisely, which lets us set a principled threshold:
where the gate threshold γ (gamma) is read off the chi-square distribution. We pick a gate probability PG — the fraction of true measurements we want to keep (say 99%) — and set γ to the chi-square value that captures that probability mass. Choosing PG = 0.99 means "I am willing to discard 1% of genuine measurements as the price of a tight gate that rejects most clutter." The chi-square table converts that choice into a number:
| Measurement dim. m | PG = 0.95 | PG = 0.99 | PG = 0.999 |
|---|---|---|---|
| 1 (e.g. range only) | 3.84 | 6.63 | 10.83 |
| 2 (e.g. x,y position) | 5.99 | 9.21 | 13.82 |
| 3 (e.g. x,y,z) | 7.81 | 11.34 | 16.27 |
These are the values that show up in real tracker code as a single constant. A 2-D position tracker that wants to keep 99% of true detections sets γ = 9.21, and any measurement with d² below 9.21 is gated in. (For the geometrically minded: the gate is the ellipse νT S−1 ν = γ, and its area grows with γ and with the size of S.)
Let's gate a single track by hand. The track predicts its target at ẑ = (10, 5) meters. Its innovation covariance — built from the prediction and the sensor noise — comes out diagonal for simplicity:
So the track is twice as uncertain in x as in y — its gate is an ellipse stretched along x. Because S is diagonal, its inverse is trivial: S−1 = [ 0.25 0 ; 0 1 ] (just reciprocate the diagonal). Now three measurements arrive. We want a 95% gate, so for m = 2 we use γ = 5.99. Compute d² for each:
| Meas. | z = (x, y) | ν = z − ẑ | d² = νx²/4 + νy²/1 | Verdict (γ = 5.99) |
|---|---|---|---|---|
| z1 | (13, 5) | (3, 0) | 9/4 + 0 = 2.25 | INSIDE — 3 m off in x, but x is uncertain |
| z2 | (10, 7.5) | (0, 2.5) | 0 + 6.25/1 = 6.25 | OUTSIDE — only 2.5 m off, but in confident y |
| z3 | (12, 6) | (2, 1) | 4/4 + 1/1 = 2.0 | INSIDE |
Stop and savor the punchline in rows 1 and 2. Measurement z1 is a full 3 meters away from the prediction and gets accepted; z2 is only 2.5 meters away and gets rejected. A Euclidean gate would have done the exact opposite. The Mahalanobis distance knows that 3 m along the uncertain x-axis (1.5 sigmas) is more plausible than 2.5 m along the confident y-axis (2.5 sigmas). This single example is why no serious tracker uses raw distance.
Play with the gate below. Drag the gate-size slider (which sets γ via PG) and watch the ellipse grow and shrink, validating or rejecting each measurement. Notice how an elongated S makes the gate an ellipse, not a circle, and how the same measurement flips from rejected to validated as γ grows.
A single track sits at its prediction with innovation covariance S (an ellipse, wider in x than y). Measurements that fall inside the gate are validated; those outside are rejected. Drag gate PG to resize γ; drag S elongation to stretch the ellipse. Watch a measurement flip as the gate grows.
The Mahalanobis gate is about six lines of numpy. The whole point is that the gate test and the d² that feeds later association are the same computation:
python import numpy as np def gate(z, z_hat, S, gamma): # z, z_hat: (m,) vectors; S: (m,m) innovation covariance; gamma: chi-square threshold nu = z - z_hat # the innovation (residual) d2 = nu @ np.linalg.inv(S) @ nu # Mahalanobis distance squared return d2 < gamma, d2 # (inside? , the squared distance for later use) z_hat = np.array([10.0, 5.0]) S = np.array([[4.0, 0.0], [0.0, 1.0]]) # wide in x, tight in y gamma = 5.99 # 2-D, P_G = 0.95 gate(np.array([13.0, 5.0]), z_hat, S, gamma) # -> (True, 2.25) 3 m off in x -> INSIDE gate(np.array([10.0, 7.5]), z_hat, S, gamma) # -> (False, 6.25) 2.5 m off in y -> OUTSIDE
Run the two calls in your head against the table above — they reproduce it exactly. The reason we return d2 alongside the boolean is that the very same number becomes the cost in the assignment problem (Chapter 3) and feeds the association likelihood (Chapter 4). Gating, cost, and probability all spring from one quadratic form: νT S−1 ν.
Gating gave us, for each track, a short list of validated candidate measurements. Now we must commit: pick one measurement per track to feed the Kalman update. The simplest possible rule is nearest neighbor (NN): assign each track the validated measurement with the smallest Mahalanobis distance d². It is one line of code, it is fast, and it is the right starting point precisely because seeing how it breaks motivates everything that follows.
Two flavors exist, and the difference matters more than its name suggests. Greedy NN walks the tracks one at a time: each track grabs its nearest still-available measurement, which is then removed from the pool so no two tracks can claim the same one. Global NN (GNN), the subject of Chapter 3, instead finds the single assignment of all tracks to all measurements that minimizes the total distance. Greedy is local and order-dependent; global is optimal and order-independent.
Let's make the Chapter 0 trap concrete with numbers and watch greedy NN actively pick the worse pairing. Two tracks, two measurements, and for clarity we use plain 1-D distance (the logic is identical with Mahalanobis). Predictions and measurements:
The four pairwise distances:
| z1 = 1.6 | z2 = 0.5 | |
|---|---|---|
| track A (0.0) | |0.0 − 1.6| = 1.6 | |0.0 − 0.5| = 0.5 |
| track B (2.0) | |2.0 − 1.6| = 0.4 | |2.0 − 0.5| = 1.5 |
Greedy, processing A first: A's nearest is z2 (0.5), so A→z2, and z2 is removed. B is now forced onto the only survivor, z1, at distance 0.4… wait, B→z1 is fine here. The total greedy cost is 0.5 + 0.4 = 0.9. But check the other assignment: A→z1 (1.6) and B→z2 (1.5), total 3.1 — worse. So in this instance greedy and global agree, by luck. Now nudge the numbers: make z2 = 0.5 but z1 = 0.6. Greedy-A still grabs z2 (0.5), forcing B onto z1 at cost |2.0 − 0.6| = 1.4, total 1.9. The global optimum is A→z1 (0.6) and B→z2 (1.5), total 2.1 — hmm, greedy actually won again.
The clean, unambiguous counterexample needs the tracks' nearest choices to collide. Set track A at 0.0, track B at 1.0, with z1 = 0.9 and z2 = 1.1:
| z1 = 0.9 | z2 = 1.1 | |
|---|---|---|
| track A (0.0) | 0.9 | 1.1 |
| track B (1.0) | 0.1 | 0.1 |
Process B first (greedy is order-dependent — that is the whole problem). B's nearest is a tie at 0.1; say it takes z1. Now A is forced onto z2 at cost 1.1. Greedy total = 0.1 + 1.1 = 1.2. The global optimum assigns B→z2 (0.1) and A→z1 (0.9), total 1.0. Greedy lost by 0.2 because B selfishly grabbed the measurement A needed more. The lesson: greedy NN's answer depends on the order you process tracks, and a track that grabs first can rob a track that needed that measurement more.
When is NN actually fine? When targets are well-separated relative to their uncertainty and clutter is sparse — a single aircraft on a clear radar, a lone car on an empty road. NN is the workhorse for easy scenes and the right default until a scene gets crowded. The famous SORT tracker (Chapter 8) is, at heart, a gated nearest-neighbor (well, Hungarian) assignment over Kalman predictions — proof that the simplest idea, applied carefully, still ships in production.
Greedy NN failed because it decided track by track. The fix is to decide all tracks at once: find the one assignment of measurements to tracks that minimizes the total cost, with the constraint that each track gets at most one measurement and each measurement goes to at most one track. This is the classic assignment problem of combinatorial optimization, and it has a beautiful exact solution — the Hungarian algorithm (also called Kuhn–Munkres), or its faster cousin the auction algorithm. Applying it to tracking gives Global Nearest Neighbor (GNN).
The input is an N×M cost matrix C, where Cij is the cost of assigning track i to measurement j. What should the cost be? We want the assignment that is most probable, and the probability of a measurement given a track is the Gaussian likelihood. Maximizing a product of probabilities is the same as minimizing a sum of negative log-likelihoods — and the negative log-likelihood of a Gaussian is, up to constants, exactly the Mahalanobis distance from Chapter 1:
where dij² is the Mahalanobis distance from track i's prediction to measurement j, and the ln|2πSi| term (the log-determinant of the covariance) accounts for the fact that a track with a large gate is intrinsically less surprised by any measurement — it normalizes across tracks of different uncertainty. For a first pass you can drop the constant and just use Cij = dij². Gated-out pairs get cost = ∞ (or a large number) so the optimizer never selects them.
Three tracks, three gated measurements. Here is the cost matrix (using d² as the cost; lower is better):
| z1 | z2 | z3 | |
|---|---|---|---|
| track A | 1 | 9 | 4 |
| track B | 4 | 2 | 8 |
| track C | 7 | 3 | 2 |
There are 3! = 6 possible complete assignments. Because it's small, we can enumerate and find the optimum, then see how greedy gets it wrong:
| Assignment | Total cost |
|---|---|
| A→z1, B→z2, C→z3 | 1 + 2 + 2 = 5 ← optimal |
| A→z1, B→z3, C→z2 | 1 + 8 + 3 = 12 |
| A→z2, B→z1, C→z3 | 9 + 4 + 2 = 15 |
| A→z2, B→z3, C→z1 | 9 + 8 + 7 = 24 |
| A→z3, B→z1, C→z2 | 4 + 4 + 3 = 11 |
| A→z3, B→z2, C→z1 | 4 + 2 + 7 = 13 |
The optimum is A→z1, B→z2, C→z3 with total cost 5. Now run greedy by global-minimum-first (a common heuristic): the single smallest cell in the matrix is A→z1 = 1; take it, cross out row A and column z1. The smallest remaining cell is C→z3 = 2; take it, cross out row C and column z3. Only B and z2 remain: B→z2 = 2. Total = 5 — greedy got it right this time. But change one cell: set C→z3 = 6 (instead of 2). The new optimal is still A→z1, B→z2, C→z3 = 1+2+6 = 9, OR A→z3(4), B→z2(2), C→z1(7) = 13, OR A→z1(1), B→z3(8), C→z2(3) = 12 — so 9 is still best. Greedy: smallest cell A→z1=1, then smallest remaining is B→z2=2, then C→z3=6, total 9. Still matches.
The honest takeaway: greedy frequently matches the optimum on small clean matrices, which is exactly why it is so tempting and so dangerous — it works until the day it doesn't. The Hungarian algorithm guarantees the optimum in O(n³) time regardless of the matrix, so a production tracker uses it and never has to wonder. Below you can build your own cost matrix and watch the optimal GNN assignment light up against the greedy one; find a matrix where they diverge.
A 3×3 cost matrix (tracks × measurements). The optimal GNN assignment (minimum total cost, found by exhaustive search = what Hungarian computes) is outlined in green; the greedy assignment (smallest-cell-first) in orange. Click any cell to cycle its cost and hunt for a matrix where greedy ≠ optimal.
Real frames are not square. You may have more measurements than tracks (extra = clutter or new targets) or more tracks than measurements (some targets were missed). The standard trick is to pad the cost matrix: add dummy "no-detection" rows/columns with a fixed cost equal to the cost of declaring a miss or a false alarm. A track assigned to its dummy column means "this track was not detected this frame" (coast it on the prediction); a measurement assigned to its dummy row means "this measurement is clutter / a new target" (start a tentative new track). The Hungarian algorithm handles the padded matrix exactly as before — misses and clutter become just more cells to optimize over.
With scipy's linear_sum_assignment (an efficient Hungarian/Jonker–Volgenant solver), GNN is a few lines. Note how gated-out pairs are set to a large cost so the optimizer avoids them:
python import numpy as np from scipy.optimize import linear_sum_assignment def gnn_assign(tracks, meas, gamma=9.21, big=1e6): N, M = len(tracks), len(meas) C = np.full((N, M), big) # default: gated out for i, (z_hat, S) in enumerate(tracks): Sinv = np.linalg.inv(S) for j, z in enumerate(meas): nu = z - z_hat d2 = nu @ Sinv @ nu # Mahalanobis distance squared if d2 < gamma: # only fill gated-in pairs C[i, j] = 0.5 * d2 + 0.5 * np.log(np.linalg.det(2 * np.pi * S)) rows, cols = linear_sum_assignment(C) # Hungarian: optimal total cost # keep only assignments that were actually gated-in (cost < big) pairs = [(r, c) for r, c in zip(rows, cols) if C[r, c] < big] return pairs # list of (track_index, meas_index)
The compact mental model: fill a cost matrix with negative-log-likelihoods, gate out the impossible, hand it to Hungarian, read off the optimal pairing. Everything downstream — the Kalman update for each matched track, coasting the unmatched tracks, spawning new tracks from unmatched measurements — follows mechanically from the pairs this returns.
GNN's flaw is that it commits. When two measurements are both plausible for a track, GNN must choose one and throw the other away — and if it chooses wrong, it has fed an impostor into the Kalman update. But why choose at all? When you are genuinely uncertain which measurement is right, the honest move is to not decide — to use all the gated measurements, each weighted by how probable it is. This is Probabilistic Data Association (PDA), introduced by Bar-Shalom and Tse in 1975, and it is the single most important robustness idea in classical tracking.
The shift in mindset is profound. NN and GNN ask "which measurement is the right one?" PDA asks "what is the probability that each measurement is the right one?" — and then forms a single combined update by blending all of them in proportion to those probabilities. There is no hard decision to get wrong. A measurement that is probably clutter contributes a little; a measurement that is probably the target contributes a lot; and crucially, the possibility that none of them is the target (the target was missed and all gated measurements are clutter) is given its own weight too.
For a single track with m gated measurements z1, …, zm, PDA computes an association probability βi for each: the probability that measurement i is the true target-originated one. It also computes β0: the probability that none of the gated measurements came from the target (i.e. the target went undetected this frame and every gated measurement is clutter). These weights sum to 1:
To build them we need two pieces of physics. First, each measurement i has a likelihood of having come from the target — the Gaussian evaluated at its innovation:
where di² = νiT S−1 νi is the now-familiar Mahalanobis distance. A measurement near the prediction (small d²) has high likelihood; a measurement near the gate edge has low likelihood. Second, we need the rate at which clutter produces measurements: the clutter density λ (false alarms per unit volume per scan), and the sensor's detection probability PD (chance the target was detected at all) and gate probability PG (chance a detected target's measurement fell in the gate). The PDA weights are then:
Don't let the symbols scare you — the structure is simple. Every β has the same denominator (a normalizer so they sum to 1). The numerator of each βi is "the target was detected (PD) and produced a measurement that looks like zi (Li)." The numerator of β0 is "the clutter explanation": λ (clutter rate) times (1 − PDPG) (the chance the target was missed or its measurement fell outside the gate). The denominator just adds those competing explanations. The whole formula is a competition between "a real measurement explains this" and "clutter explains this."
Concrete numbers. A track has two measurements in its gate with Mahalanobis distances d1² = 1.0 (close, probably the target) and d2² = 4.0 (farther, probably clutter). Sensor parameters: PD = 0.9, PG = 0.99 (so PDPG = 0.891), clutter density λ = 0.02. For the likelihoods, take the normalizing constant √(|2πS|) = 10 (it cancels in the ratio anyway, but we carry it for honesty). Compute the likelihoods:
Now the shared denominator. The clutter term is λ(1 − PDPG) = 0.02 × (1 − 0.891) = 0.02 × 0.109 = 0.00218. The target term is PD(L1 + L2) = 0.9 × (0.06065 + 0.01353) = 0.9 × 0.07418 = 0.06676. Denominator = 0.00218 + 0.06676 = 0.06894. The weights:
| Weight | Numerator | β = num / 0.06894 | Meaning |
|---|---|---|---|
| β1 | 0.9 × 0.06065 = 0.05459 | 0.792 | z1 is very likely the target |
| β2 | 0.9 × 0.01353 = 0.01218 | 0.177 | z2 contributes modestly |
| β0 | 0.00218 | 0.032 | small chance both are clutter |
Sanity check: 0.792 + 0.177 + 0.032 = 1.001 (rounding). The closer measurement z1 gets 79% of the weight, the farther z2 gets 18%, and there's a small 3% chance neither is real. Now — and this is the elegant part — PDA fuses these into a single combined innovation by taking the β-weighted average of all the individual innovations:
The Kalman update then uses this blended innovation ν̅ instead of any single measurement's innovation. Because β0 doesn't multiply any innovation, a large β0 shrinks the effective update — the track barely moves, i.e. it coasts. PDA also inflates the updated covariance to account for the association uncertainty (the "spread of the innovations" term), so the filter honestly reports that it is less sure when the association was ambiguous. The net effect: an ambiguous frame nudges the track gently toward the consensus of plausible measurements, while widening its uncertainty, rather than yanking it onto one possibly-wrong pick.
Drag a measurement nearer to or farther from the track's prediction below and watch its β bar respond in real time. Push both measurements to the gate edge and watch β0 swell — PDA losing confidence in all of them and choosing to coast.
A track with two gated measurements. Drag z1 and z2 closer to or farther from the prediction (cross). The bars show β1, β2, and the "no valid measurement" weight β0. Push both far out (toward the gate edge) and β0 grows — PDA decides to coast.
The whole β computation is a likelihood, a normalizer, and a divide:
python import numpy as np def pda_betas(d2, S, P_D=0.9, P_G=0.99, lam=0.02): # d2: array of Mahalanobis distances-squared for the gated measurements norm = np.sqrt(np.linalg.det(2 * np.pi * S)) L = np.exp(-0.5 * d2) / norm # Gaussian likelihood of each measurement clutter = lam * (1 - P_D * P_G) # the "all clutter / missed" explanation denom = clutter + P_D * L.sum() beta = P_D * L / denom # beta_1 .. beta_m beta0 = clutter / denom # beta_0: no valid measurement return beta0, beta # sums to 1 S = np.eye(2) * 2.0 pda_betas(np.array([1.0, 4.0]), S) # -> beta0~0.03, beta~[0.79, 0.18] (close meas dominates) # the combined innovation that feeds the Kalman update: # nu_bar = sum_i beta[i] * nu[i] (then update covariance is inflated)
The exact numbers shift with the chosen normalizer, but the ratio — close measurement dominates, far one contributes a little, β0 small unless everything is implausible — matches the hand calculation. Swap the closer measurement out to the gate edge and you'll watch β0 climb as the filter loses faith in every candidate.
Plain PDA reasons about one track in isolation. That is fine when targets are far apart, but it has a fatal blind spot the moment two targets come close: nothing stops two tracks from both heavily weighting the same measurement. If track A and track B both have measurement z1 in their gates, single-target PDA lets A give z1 a big β and B give z1 a big β, independently. Both tracks then get pulled toward z1, the two tracks drift together, and you get track coalescence — two tracks collapsing into one. Joint Probabilistic Data Association (JPDA), from Fortmann, Bar-Shalom and Scheffe (1983), fixes this by reasoning about all tracks jointly.
JPDA's key idea: instead of computing each track's association probabilities independently, enumerate the joint association events — complete, consistent assignments of measurements to tracks that obey the physical constraints. The two constraints that single-target PDA ignored:
JPDA lists every feasible joint event (every way to legally hand out the gated measurements to the tracks, with the rest declared clutter or misses), scores each event by its probability, and then marginalizes: track A's probability of being associated with z1 is the sum of the probabilities of all joint events in which A→z1. Those marginal βA,1 values are then used exactly like PDA's — a weighted-innovation update per track — but now the weights are coupled: the constraint that z1 can't go to both means heavy weight on A→z1 automatically suppresses the weight on B→z1. The two tracks can no longer both claim the same return.
Two tracks (A, B), and suppose track A has gated measurements {z1, z2} while track B has gated {z1} only (z1 is the contested one). The feasible joint events — respecting "each measurement to at most one track, each track at most one measurement" — are:
| Event | A gets | B gets | Notes |
|---|---|---|---|
| E1 | z1 | — (miss) | z2 & B's slot unused / clutter |
| E2 | z2 | z1 | both tracks detected, no conflict |
| E3 | z2 | — (miss) | z1 declared clutter |
| E4 | — (miss) | z1 | A missed |
| E5 | — (miss) | — (miss) | everything is clutter |
Notice what is absent: there is no event where A→z1 and B→z1 — that would violate the one-measurement-one-source rule, so JPDA simply never considers it. To get B's marginal probability of owning z1, sum the probabilities of the events where B→z1 (E2 and E4). To get A's marginal for z1, sum the events where A→z1 (only E1). Because the contested z1 appears for A in fewer feasible events (A also has z2 as an alternative), the coupling naturally pushes A toward z2 and lets B keep z1 — resolving the competition that plain PDA could not.
Each event's probability is a product of the per-assignment likelihoods (the same Gaussian Li from Chapter 4) times the clutter/miss factors for the unassigned slots, normalized over all feasible events. The arithmetic is just PDA's likelihoods, multiplied across a consistent assignment, summed over the legal events — conceptually a small extension, computationally the crux of the problem.
The number of feasible joint events explodes with the number of tracks and measurements. With T tracks and a dense cluster of M measurements all mutually gated, the count of legal assignments grows super-exponentially — enumerating them exactly becomes intractable in heavy clutter. This is why production JPDA uses approximations: cheap approximations of the marginal β's (e.g. "cheap JPDA" / JPDAF variants), m-best enumeration (compute only the highest-probability events), or clustering (only tracks whose gates actually overlap need to be reasoned about jointly — isolated tracks fall back to plain PDA). The art of practical JPDA is keeping the joint reasoning confined to the small clusters where it actually matters.
Every method so far decides this frame. NN/GNN commit hard; PDA/JPDA commit soft; but all of them collapse the ambiguity by the end of the scan and move on, never to reconsider. Multiple Hypothesis Tracking (MHT), introduced by Donald Reid in 1979, makes the boldest move of all: don't decide yet. When an association is ambiguous, MHT keeps both interpretations alive as competing hypotheses, carries them forward, and lets future frames disambiguate. The insight is that a measurement that is ambiguous now often becomes obvious two frames later — so the optimal strategy is to wait for the evidence rather than guess early.
MHT maintains a tree of association hypotheses. The root is the initial state. Each frame, every leaf hypothesis branches into all the ways the new measurements could be associated under that hypothesis — this measurement extends track A, or starts a new track, or is clutter; that measurement extends track B, or is a missed detection, and so on. Each branch carries a likelihood score (the same Gaussian likelihoods times clutter/miss factors). Over time, the correct interpretation accumulates high likelihood while wrong ones accumulate low likelihood — and the past decision is made in hindsight, when it is easy, rather than in the moment, when it is hard.
The tree's danger is obvious: if every ambiguous measurement doubles the number of leaves, the hypothesis count explodes exponentially with time. Within a handful of frames you'd have millions of hypotheses. MHT is only practical because of aggressive pruning and merging:
There are two classic formulations. Hypothesis-oriented MHT (HOMHT), Reid's original, enumerates global hypotheses directly. Track-oriented MHT (TOMHT), the modern workhorse, instead maintains a set of candidate track trees and reconstructs the best global hypothesis each frame by solving a (large) assignment / maximum-weighted-independent-set problem — which is far more memory-efficient and is what most real MHT trackers use today.
The hypothesis tree below grows as ambiguous frames arrive and gets pruned back when you apply N-scan / K-best limits. Add ambiguous frames to watch the branching explode, then prune to watch it collapse to the surviving best hypotheses.
Each ambiguous frame branches every surviving hypothesis. Watch the leaf count explode. Then drag keep K-best and N-scan depth to prune: low-likelihood branches (faded) are deleted, the survivors (bright) carry forward. This is why MHT is tractable at all.
Time to put the whole ladder to the test in one live, multi-frame tracker. Two real targets cross paths in a field of clutter, with occasional missed detections. You choose the association method — hard nearest-neighbor or soft JPDA-style probabilistic association — crank up the clutter and miss rate, and watch what happens to the two tracks. This is the payoff: you will see NN swap tracks and get captured by clutter while the probabilistic method rides through the same chaos.
What you're watching: two true targets move along smooth crossing paths. Each frame the simulator emits a measurement for each target (with probability PD, else a miss) plus a scatter of clutter. Two tracks try to follow the targets, gating and associating each frame. The good outcome is each track staying glued to its own target; the failure is a track swap (the tracks trade targets at the crossing) or a clutter capture (a track wanders off onto a false alarm).
Two targets cross. Track 1 and track 2 follow them. Choose the method, set the clutter and miss rate, then Run. Watch hard NN swap or get captured under heavy clutter while the probabilistic method holds. The status reports each track's error.
Don't just watch; falsify. Run this sequence and confirm each prediction with your own eyes:
There is no quiz for the showcase — running the four-step experiment above is the test. If you can predict, before pressing Run, whether NN or JPDA will hold under a given clutter level, you understand the chapter.
Every Engineermaxxing lesson in this series ends with the same four chapters — Use Cases, Practical Application, Debugging, Connections — because a concept you can't point at in a shipped product is a concept you don't really own yet. Data association is everywhere a machine tracks more than one thing at once, and once you can name a product's association method, you can predict exactly how it will fail in a crowd.
Data association was invented for radar. An air-traffic control radar sweeps the sky and returns dozens of blips per scan — aircraft, weather, ground clutter, birds. The system must maintain a stable track per aircraft through clutter, missed sweeps, and crossing flight paths. This is the canonical home of JPDA and MHT: Bar-Shalom's and Reid's methods came directly out of this problem. Naval sonar and defense / missile-tracking systems are the same problem underwater and in the air — dense targets, heavy clutter, hard real-time deadlines — and they overwhelmingly use MHT-class trackers because a track swap on a threat is unacceptable. The vocabulary you learned (gate, β, hypothesis tree) is literally the vocabulary of these systems' design docs.
A self-driving car runs data association continuously and at large scale. Each frame, the perception stack detects pedestrians, cyclists, and vehicles — often dozens of objects — and must keep a consistent track ID on each one across time and across sensors. This is multi-object tracking (MOT), and the dominant paradigm is tracking-by-detection: an object detector fires bounding boxes each frame, and a data-association stage links this frame's detections to existing tracks. The association cost mixes Mahalanobis distance (from a Kalman predict) with appearance and IoU (intersection-over-union of boxes). Track fusion across modalities — matching a radar track to a LiDAR track to a camera track of the same physical car — is itself a data-association problem one level up.
The same algorithms reappear far from radar:
| Domain | What's tracked | Method | Why that choice |
|---|---|---|---|
| Air-traffic radar | aircraft | MHT / JPDA | dense, crossing, clutter; a swap is unacceptable |
| Self-driving MOT | cars, people | GNN (SORT) / + appearance (DeepSORT) | real-time, many objects, occlusion re-ID |
| Naval sonar / defense | contacts, threats | MHT | heavy clutter, low PD, high stakes |
| Sports / pedestrian | players, people | JPDA / MHT + re-ID | constant crossings and collisions |
| Cell / microscopy | cells, particles | GNN / MHT (for division) | hundreds of near-identical targets |
You know the methods; now we turn them into the half-dozen knobs you actually set when a tracker lands on your desk. This recurring "now build it" chapter walks the concrete decisions — gate probability, clutter density, cost-matrix construction, method choice, track confirmation, and compute budget — in the order you'd make them.
The gate threshold γ comes from PG via the chi-square table (Chapter 1). The trade is direct: a high PG (e.g. 0.999) means a big gate that almost never drops a true measurement — but admits more clutter, which slows the assignment and risks clutter capture. A low PG (e.g. 0.95) means a tight gate that rejects most clutter — but occasionally drops a true measurement, breaking the track. The standard starting point is PG = 0.99 (for 2-D, γ = 9.21); tighten it in heavy clutter, loosen it if you see tracks dropping on good targets. Crucially, the gate scales with S, so a track that is uncertain (large S, e.g. just after a missed detection) automatically gets a bigger gate — the chi-square threshold is on the normalized distance, not meters.
PDA and JPDA need the clutter density λ (false alarms per unit area per scan). Two ways to get it: measure it (count, over many frames, how many gated measurements turn out to be clutter, divided by gate area) or estimate it adaptively from the current scene. λ controls the β0 weight: set λ too low and PDA over-trusts measurements (it thinks clutter is rare, so any gated return is probably real — clutter capture); set it too high and PDA under-trusts everything (β0 dominates, the track coasts forever and lags reality). Getting λ roughly right matters more than getting it exactly right — it is a robustness knob, not a precision knob.
For GNN, the cost is the negative-log-likelihood (Chapter 3). In practice you assemble it from parts and tune their weights:
The motion term is your gated Mahalanobis distance. In MOT you add an appearance distance (cosine distance between learned embeddings, à la DeepSORT) and maybe a shape/size term. The weights wa, ws are tuned on a validation set. Gated-out pairs get a sentinel large cost so the Hungarian solver never picks them, and you pad with dummy rows/columns (Chapter 3) for misses and new tracks, where the dummy cost encodes "how bad is it to declare a miss / spawn a new track."
A practical decision rule:
A single unmatched measurement might be a new target — or clutter. You don't want to spawn a permanent track on every false alarm, nor ignore real new targets. The standard answer is a track-management state machine with an M-of-N confirmation rule: a new measurement starts a tentative track; if it gets associated in at least M of the next N frames (say 3-of-5), it is confirmed (promoted to a real track); otherwise it is deleted as clutter. Symmetrically, a confirmed track that goes unassociated (coasting on prediction) for too many consecutive frames is deleted (its target left or died). These birth/confirm/coast/delete transitions are as important as the association math — they are what keep clutter from accreting into phantom tracks and what let tracks survive temporary occlusion.
Real trackers run in real time, so the method's cost matters. GNN: O(n³) Hungarian on a gated (sparse) matrix — cheap, milliseconds for dozens of tracks. JPDA: enumerating joint events is the cost; keep it tractable by clustering (only jointly-reason over tracks whose gates actually overlap) so most of the scene falls back to cheap per-track PDA. MHT: the priciest by far — budget for the tree, the pruning, and the global-hypothesis solve; tune N-scan and K-best down until you fit the deadline. The universal trick is the same: gate hard and cluster, so the expensive reasoning only runs on the small, genuinely-ambiguous knots of the scene.
Association bugs are rarely loud crashes — they are wrong pairings that the downstream filter dutifully fuses, so the symptom shows up in the estimate, one step removed from the cause. This recurring chapter catalogs the classic failures, the symptom each presents, and the test that exposes it. The unifying diagnostic, threaded through all of them, is innovation monitoring — watching the very d² / NIS that gating already computes.
The signature multi-target bug. When two targets pass close, their measurements overlap both gates, and a hard tracker assigns track 1 to target 2's measurement (and vice-versa) — a track swap. The IDs are now wrong forever. Its cousin, coalescence, is when two tracks merge onto the same target (both claim the same measurement — classic single-target PDA failure from Chapter 5) and one target goes untracked.
A confirmed track suddenly dies, then a new track is born nearby a frame later — you broke one continuous track into two fragments. Almost always the gate was too tight: the target maneuvered (or the sensor noise spiked), its true measurement fell outside the gate, the track found nothing to associate, coasted, and got deleted — while the orphaned measurement spawned a fresh track.
The opposite: phantom tracks appear in empty space. Either the gate is so loose that clutter routinely associates and accretes into tracks, or the M-of-N confirmation is too lenient and one-off false alarms get promoted.
This is the deep consequence the whole lesson has been warning about. A single wrong hard association feeds a measurement from the wrong target (or from clutter) into the Kalman update. The filter, trusting the pairing absolutely, pulls the state toward the impostor. Now the next frame's prediction is off, so the gate centers on the wrong place, making the next mis-association more likely — a positive feedback loop. Left unchecked, the track diverges: the estimate runs away from truth and the reported covariance stays falsely small (the filter remains confident while being completely wrong).
| Check | How | Catches |
|---|---|---|
| NIS in chi-square band? | Plot d² per track vs γ over time | wrong associations, divergence, overconfidence |
| ID-switch count (IDSW) | Log track↔truth ID through crossings | track swaps / coalescence (Failure 1) |
| Track fragmentation rate | Count deaths-then-rebirths on one target | broken tracks, gate too tight (Failure 2) |
| Tracks vs true target count | Compare confirmed-track count to ground truth | spurious tracks, gate too loose (Failure 3) |
| Coast duration before delete | How many frames a track coasts before deletion | premature deletion vs phantom persistence |
You now hold the missing half of fusion — the part the estimation lessons assumed away. Before naming what comes next, here is everything in one place: the cheat-sheet to screenshot, the motto, the real sources, and the cross-links.
Screenshot this. It is the entire lesson collapsed into a single lookup:
| Method | Decision | Scope | Handles clutter? | Cost | Best for |
|---|---|---|---|---|---|
| NN | hard | per track, greedy | poorly | trivial | sparse, separated targets |
| GNN | hard | all tracks, optimal/frame | via gating | O(n³) | real-time MOT (SORT) |
| PDA | soft (β) | single track | yes (β0) | low | one target in clutter |
| JPDA | soft (β) | all tracks jointly | yes | moderate–high | close targets + clutter |
| MHT | deferred | tree over frames | best | high | dense, low PD, no-swap |
This was Lesson 12 — the bridge between the single-target estimation lessons (4–11) and the multi-sensor, multi-target world that the rest of the series lives in. Every filter you learned before assumed association was solved; you now solve it. The recurring chapter structure (gating → methods → showcase → Use Cases, Practical, Debugging, Connections) repeats across the series so you always know where you are.
These lessons supply the machinery this one builds on or feeds into: