Skip to main content

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.

Final score per graded run
00.250.500.751
  • 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.

t = 0t = 1t = 2t = 3division
Figure 1. What a prediction is. Each panel is one timepoint of the recording, each blob a detected cell, drawn smaller and fainter the deeper it sits in the volume, and each line a link to the same cell in the next timepoint; hover a cell to follow its track. One cell drifts out of the volume and one enters it, and where one cell becomes two the parent carries two outgoing links, which is the division the metric scores separately.

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.

README.md127 lines
# 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`.
Figure 2. The agent's workspace at /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 JJ for the link Jaccard on a recording, NpredN_{\mathrm{pred}} for the cells predicted and NestN_{\mathrm{est}} for the estimated true cell count carried in that recording's metadata,

J=TPTP+FP+FNJadj=max ⁣(0,  J(10.1NpredNestNest))J = \frac{\mathrm{TP}}{\mathrm{TP} + \mathrm{FP} + \mathrm{FN}} \qquad J_{\mathrm{adj}} = \max\!\left(0,\; J \left(1 - 0.1\,\frac{N_{\mathrm{pred}} - N_{\mathrm{est}}}{N_{\mathrm{est}}}\right)\right)

so flooding the volume with detections costs score even though the extra cells are never false positives. The per-recording JadjJ_{\mathrm{adj}} 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

S=Jadj+0.1Jdivision.S = J_{\mathrm{adj}} + 0.1 \cdot J_{\mathrm{division}}.

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.