Here is a way to fail at cell tracking.
You find every single cell. Every nucleus, in every frame, perfectly. And you still score close to zero.
Because nobody asked you where the cells are. They asked which cell became which. And the answer to that is not made of cells. It is made of the connections between them.
That sentence is the whole of this series. Everything after it is a consequence — including both of the things I got wrong.
The competition
The CZ Biohub cell tracking challenge gives you light-sheet microscopy recordings of a developing zebrafish embryo and asks for the lineage: which nucleus in this frame is the same nucleus as that one in the next, and where a cell divided into two. It runs as a Kaggle code competition — your notebook is re-run on roughly 199 hidden volumes, inside twelve hours, with no internet access. Whatever you build has to be fast enough to survive that.
This series is a working log of getting to 0.881 on the public leaderboard over about three weeks, what actually moved the number, and the two experiments that moved it the wrong way. I'll be specific throughout about which parts were mine and which were not, because that distinction turns out to matter for the last post.
What the metric actually counts
The score is adjusted_edge_jaccard + 0.1 · division_jaccard. Strip the names away and it means: mostly, you are graded on edges — the link between a cell in frame t and the same cell in frame t+1 — with a small bonus for correctly spotting divisions.
Two details in that definition end up driving every design decision I made:
Nodes are matched by proximity, not identity. A predicted nucleus counts as the ground-truth one if its centroid lands within about 7 µm. Inside that radius, closer is not better. This sounds like a technicality. It is the reason one of my later "improvements" scored exactly zero.
Only consecutive frames count. An edge from t to t+1 scores. An edge from t to t+2 — jumping over a frame where you missed the cell — scores nothing at all. You cannot skip a hole in your own detections. You have to fill it.
You are graded on a fraction of what you can see
This is a real recording: a zebrafish embryo, a hundred timepoints, tens of thousands of nuclei. But the ground truth is sparse. Only about 6% of the cells are labelled — roughly two thousand, out of the thirty thousand you can plainly see.
In a single frame that ratio is startling: eighteen labelled cells among more than a hundred obviously-visible ones. You are graded on a fraction of what is in front of you, and you have no way of knowing in advance which fraction.
The practical consequence is that you cannot naively score yourself. If you compute a normal tracking metric against sparse labels, every correct detection of an unlabelled cell looks like a false positive, and your local number collapses in a way the leaderboard does not. Building a scorer that understood sparsity was the first real work of the project, and it's the next post.
Depth is not like width
One more property of the data, because it shapes where the errors live. Look at the volume face-on and the nuclei are round. Look at the same volume from the side and they go blocky.
The microscope samples depth about four times more coarsely than width — the same 104 µm of tissue, a quarter of the detail. This is anisotropy, and it is not a rendering artefact; it is the actual resolution of the data. Every distance you compute, every neighbourhood you search, has to be expressed in physical microns rather than voxels, or it silently means something different along z than it does along x.
Depth is where the detector is least sure. It is worth knowing that before you start trusting it.
Why the combinatorics are brutal
So: connections. How hard can that be?
Each frame holds around three hundred cells, and any one of them could connect to any of three hundred in the next. Across the whole recording, that is roughly nine million possible links. About thirty thousand are real.
One in three hundred.
And you cannot tell them apart by looking. These are featureless blobs — no texture, no colour, no shape to distinguish one from another. They all look identical. They drift between frames. They divide, so one becomes two. And every so often the detector blinks and a cell simply is not there for a frame.
That last failure mode sounds minor. It is the single most expensive thing that happens in the whole pipeline, and it gets its own post.
What's in the rest of this series
Six parts, each landing one idea:
| Part | The idea |
|---|---|
| 1 · this one | The score is about connections, not cells |
| 2 | Build a scorer you trust before you build a model |
| 3 | Finding cells is the easy half — and the biggest win wasn't mine |
| 4 | Scoring links one at a time is a trap |
| 5 | One missed cell costs two links and one lineage |
| 6 | Two failures, two different axes — and where the ceiling is |
A note on honesty before we start. The detector and the linker at the heart of this pipeline are public work, not mine — I'll say so plainly when we get there. My own contribution is a graph post-processing stage worth +0.014, and a local scorer that predicted the leaderboard to within 0.008. Public notebooks in this competition reach 0.902, so 0.881 sits at the public floor rather than ahead of it.
That framing is not modesty. It is the reason the final post is worth reading: when you know exactly which part you built, you know exactly which part to distrust.