Cell tracking in 3D time-lapse microscopy
Detect, link and division-track every cell in volumetric fluorescence recordings of a developing specimen, scored on a held-out specimen the training data never shows.
- Claude Opus 5
- GPT-5.6 Sol
- Gemini 3.7 Flash
- Kimi K3
- Grok 4.6
Tracking cells through 3D time-lapse microscopy is difficult because thousands of similar cells move, deform, and divide in dense, noisy recordings. This task provides annotated volumetric recordings of a developing specimen, the official tracking metric, one L40S, and four hours. The submitted tracker is graded on a recording of a different specimen, so nothing about the held-out volume is in the training data.
The deliverable is three connected predictions rather than one: detect every cell, link it to the same cell in adjacent timepoints, and identify parent-child links when a cell divides. Getting detection right and linking wrong scores almost nothing, and the reference baseline, local-maxima detection followed by nearest-neighbour linking, is deliberately the obvious thing that does not work well.
The task
The training recordings come with per-timepoint cell detections and links, including divisions; the held-out recording has none. The deliverable is a predictions CSV listing every detected cell per timepoint with its integer voxel centroid, and every temporal link between them, with a division expressed as a node with two outgoing links.
Environment
The agent works in an isolated container with no general internet access, on a
GPU with four hours on the clock. It has the annotated training recordings, an
unlabelled test recording, and the official metric shipped as
tools/trackmetric, the exact scoring code the grader runs, so it can carve
its own folds out of the training data and score them locally as often as it
likes. Structural validation is free, and both local validation and scored
submissions are unlimited, so the constraint is the clock rather than a budget.
What it cannot see is the split: the held-out recording is divided into a public
part whose score submit returns and a private part that is scored on every
submission and never shown.
# Working in this environment
Everything lives under `/app`:
```
/app/lintrack/ the dataset (your private copy), data dictionary in lintrack/README.md
/app/tools/ trackmetric/ (official metric), validate.py, score_local.py, submit, clock
/app/src/ your workspace: code, checkpoints, predictions
/app/experiments/ your lab notebook lives here (history.jsonl)
/app/submissions/ your submission record, managed by `submit`, never edit by hand
```
The environment is offline. A GPU is available; torch, scipy, scikit-image,
zarr, polars, geff, and tracksdata are preinstalled.
## Predictions format
One CSV describing a tracking graph for every test dataset. Header:
```
id,dataset,row_type,node_id,t,z,y,x,source_id,target_id
0,e101,node,1,0,32,128,128,-1,-1
1,e101,node,2,1,33,130,125,-1,-1
2,e101,edge,-1,-1,-1,-1,-1,1,2
```
- Node rows (`row_type=node`): a cell detection with `node_id` and integer
voxel centroid `t,z,y,x`; set `source_id` and `target_id` to `-1`.
- Edge rows (`row_type=edge`): a temporal link from `source_id` to
`target_id` (node ids of the SAME dataset, target one timepoint after
source); set `node_id,t,z,y,x` to `-1`.
- `id` is a required throwaway index (consecutive integers over the file).
- `dataset` must match the test folder names without the `.zarr` extension,
and every test dataset must appear.
- A division is a node with two outgoing edges.
- `/app/lintrack/sample_submission.csv` is a valid (empty-graph) submission.
## Scoring locally (free, unlimited)
`/app/tools/trackmetric/` is the exact official scoring code. Score any
fold you carve from `train/` with `score_local.py`:
```
python3 /app/tools/score_local.py --submission src/fold_preds.csv \
--gt-dir /app/lintrack/train --zarr-dir /app/lintrack/train
```
It scores every dataset present in both the CSV and `--gt-dir` and prints
`cv_score: <float>` plus per-sample detail. The score is
`adjusted_edge_jaccard + 0.1 x division_jaccard`:
- Predicted nodes are matched to ground-truth nodes per timepoint by optimal
assignment on physical centroid distance (max 7.0 micrometers; the voxel
scale is z=1.625, y=x=0.40625 micrometers, stored in each zarr's metadata).
- A predicted edge is a true positive when both endpoints match ground-truth
nodes joined by a ground-truth edge. The ground truth is SPARSE: not every
cell is annotated, and predicted nodes or edges without nearby annotations
are simply ignored, not penalized as false positives.
- The edge Jaccard TP/(TP+FP+FN) is scaled by a penalty on over-predicting
the TOTAL node count: `max(0, J * (1 - 0.1 * (N_pred - N_est) / N_est))`
where `N_est` is the estimated true cell count stored in each ground-truth
store's metadata (`estimated_number_of_nodes`). Per-sample values are
weight-averaged by sample size; the division Jaccard is micro-averaged.
- Scores can exceed 1.0 (predicting fewer nodes than `N_est` makes the
penalty factor a bonus; the achievable range is bounded by edge recall).
Check a predictions file is structurally gradable:
```
python3 /app/task/validate.py src/predictions.csv
```
Hard-fails with a pointed message on a wrong header, missing datasets,
duplicate node ids, or edges referencing unknown nodes. A file that passes
here cannot be rejected by the leaderboard for structural reasons.
## Submitting
```
/app/tools/submit --file src/predictions.csv --code src/ --name track-v2 --notes "assoc thresh 5um"
```
- Validates first (a rejection costs nothing), then registers the submission
and waits; on success it prints your **public** score.
- `--code` is required: that directory is zipped and sealed alongside your
predictions for reproducibility. It is archived, not executed.
- The private part of the test set is scored on every submission but never
shown; it decides your final result.
- You have unlimited scored submissions and unlimited local validation and cross-validation. Invalid and rejected attempts never count against any
limit.
- `submit --list` shows all your submissions and public scores.
## Experiment discipline
`/app/src` is initialized as a git repo. Conventions:
- one experimental idea per commit; the checked-out commit is always your
best-so-far; `git reset --hard HEAD~1` rejects a failed idea
- `.gitignore` (preseeded) keeps weights/logs/results.tsv untracked, so
resets never delete trained artifacts or your experiment record
- print `cv_score: <float>` as the last line of every evaluation so results
are one grep away; append every attempt to `/app/src/results.tsv`
## Time and long-running commands
- `/app/tools/clock` prints elapsed and remaining time (a safe lower
bound). Check it whenever you are deciding what to attempt next.
- Training runs longer than a couple of minutes: launch them in the
background (`nohup python train.py > train.log &`) and poll the log, or
give the shell tool an explicit long timeout. Do not let a default
2-minute timeout chop a 20-minute training into a dozen polls.
## Lab notebook
Append one line per experiment to `/app/experiments/history.jsonl`:
```json
{"exp": "unet-assoc-v1", "hypothesis": "learned affinities beat distance linking", "cv": 0.41, "public": 0.39, "verdict": "keep"}
```
## Rules
- Predictions must come from models applied to the provided volumes.
- Don't create or modify anything in `/app/submissions/` by hand.
- `train/` ground truth is yours; test ground truth exists nowhere in this
environment.
- Do not create absolute symlinks inside `/app`./app, with the files it is given. The official metric ships in tools/trackmetric, so it can carve folds out of the training recordings and score itself against exactly the code the grader runs. The recordings are in the container but omitted here, and the task instruction is delivered as the agent's prompt rather than as a file.Evaluation
The leaderboard grades the submitted predictions file directly, on the public recordings for the score the agent sees and the private ones for the score that counts. There is no normalization in the grader: the number the board returns is the raw tracking score.
Predicted cells are matched to ground-truth cells one timepoint at a time by optimal assignment on physical centroid distance, with a match rejected past 7 micrometres. A predicted link is a true positive when both of its endpoints match ground-truth cells that are themselves joined by a ground-truth link, so detection and linking are not scored separately: a perfect detection with the wrong link earns nothing.
One property of the annotation shapes everything else. The ground truth is sparse: not every cell in the volume is labelled, so a predicted cell with no annotation near it is ignored rather than counted as a false positive. That removes the usual penalty for over-detecting, and the metric replaces it with an explicit one on the total count. Writing for the link Jaccard on a recording, for the cells predicted and for the estimated true cell count carried in that recording's metadata,
so flooding the volume with detections costs score even though the extra cells are never false positives. The per-recording are averaged across the split weighted by sample size, divisions are scored as their own micro-averaged Jaccard over the pooled counts, and the two combine as
Two consequences are worth stating. Because the division term is added rather than blended, the score can exceed 1. And because the node penalty becomes a bonus when a tracker predicts fewer cells than the metadata estimates, a conservative tracker is rewarded slightly, with the achievable range bounded by link recall rather than by 1.