Detection is the easy half of tracking. You are only asked where the nuclei are in a single frame — no identity, no history, no divisions. And yet detection turned out to be where almost all of the score lives.

This post is the part of the project I am least able to take credit for, so let me put the conclusion at the top: the largest single improvement I made was deleting my detector and using someone else's.

The runtime budget rules everything

Before any accuracy discussion, the constraint that eliminated most options. The notebook re-runs over roughly 199 hidden volumes inside twelve hours. That leaves, very roughly, 3.6 minutes per volume for the entire pipeline — detection, linking, post-processing, writing output.

This single number decided more of the architecture than any accuracy result did.

What I tried first: a hand-written blob detector

Nuclei in this data are, visually, blobs. No texture, no distinguishing features — just roughly spherical bright regions. That is close to the textbook case for a multiscale Laplacian-of-Gaussian detector, which is old, boring, and has no learned parameters at all.

I implemented it as 3D convolutions in PyTorch so it ran on GPU, with the filter scales chosen in physical microns and non-maximum suppression done in an anisotropic neighbourhood — the same micron-not-voxel discipline the scorer needed.

It scored 0.7535 locally, 0.746 on the board, in well under the budget.

What happened when I tried the obvious deep models

The expectation is that a pretrained segmentation model beats a hand-tuned classical filter. On this data, the two I tried did not.

DetectorLocal edge-JaccardRuntime / volumeVerdict
Classical threshold + connected components0.066fastBaseline only
Multiscale LoG blob (mine)0.7535fastShipped
Cellpose-3D0.7245~85 minRejected — ~23× over budget
StarDist-3D (pretrained, out of the box)0.3978Rejected — worse than the blob

Cellpose was slightly less accurate than the blob detector and roughly twenty-three times over the entire per-volume budget on its own. StarDist out of the box scored about half.

It would be easy to read that as "deep learning is overrated here." That is the wrong lesson, and the next section is why. The right lesson is narrower: a pretrained model carries its training domain with it. These are anisotropic light-sheet volumes of densely packed embryonic nuclei. That is not what those checkpoints saw. The architecture was never the problem; the domain gap was.

What a U-Net does: encode down, decode back up, with skip connections carrying detail across.

The thing that actually worked, which I did not build

Partway through the competition, a support pack was published under CC0 containing a trained temporal 3D U-Net — a 3D U-Net with per-voxel attention across several consecutive frames, plus a shared backbone feeding both a detection head and an edge-scoring transformer.

Reading several consecutive frames at once: a nucleus ambiguous in one frame is often obvious with its neighbours.

That temporal window is the whole idea, and it is a good one. A nucleus that is ambiguous in one frame — dim, or overlapping a neighbour — is usually obvious once you can see the frames either side of it. The model gets to use the same context a human would.

Swapping it in:

DetectorLocalLeaderboardRuntime
My LoG blob + greedy linking0.75350.746fast
Public temporal U-Net + ILP linking0.94230.867~1.7 min

+0.113 on the leaderboard. For comparison, every idea of my own across the entire project added up to +0.014. The single biggest lever was adopting public work, and it was not close.

I want to state that plainly rather than bury it, for two reasons. The first is simple credit. The second is that it sets up the honest reading of my final score: reaching 0.867 was reproduction. It got me to the pack, not ahead of it. Public notebooks in this competition reach 0.902.

Detection output: nuclei found per frame, before any linking.

What I learned by trying to improve it, and failing

Having adopted the detector, the obvious next move was to train a better one. Several attempts, all negative, and the negatives are more useful than another table of wins:

A longer temporal window did not help. If two frames of context are good, four should be better. My first test said yes — but that test changed three variables at once. Re-running it as a proper matched comparison flipped the result: window-2 scored 0.9623 against window-4's 0.9599, and window-4 lost on four of five volumes. The pilot was confounded; the control was the experiment.

A longer-trained checkpoint did not help. A 350-epoch checkpoint scored 0.9439 against the 50-epoch model's 0.9423 — a difference well inside noise. The detector had converged long before I got to it.

Architecture swaps are a dead end here. Between StarDist's 0.3978 and Cellpose's runtime, the evidence says the remaining headroom is not in choosing a different architecture. It is in in-domain training — and doing that properly means a from-scratch run at full scale, which is where the last post ends up.

The shape of the result

Detection contributed +0.113. Everything else in the pipeline — linking strategy, graph repair, parameter tuning — contributed less than +0.02 combined.

That lopsidedness is worth internalising before optimising anything. It is very easy to spend a week on the clever part of a pipeline when the boring part in front of it holds an order of magnitude more score. I did spend that week, and the next two posts are what I found there. They are genuinely interesting — but they are the edge, not the lever.

Next: why scoring links one at a time poisons everything downstream.