Skip to content

Add Trackastra and Ultrack cell-tracking workers#150

Open
arjunrajlab wants to merge 3 commits into
masterfrom
claude/trackastra-ultrack-worker-31umz7
Open

Add Trackastra and Ultrack cell-tracking workers#150
arjunrajlab wants to merge 3 commits into
masterfrom
claude/trackastra-ultrack-worker-31umz7

Conversation

@arjunrajlab

@arjunrajlab arjunrajlab commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds two new NimbusImage annotation workers that link existing tagged polygon segmentations across time into lineage tracks and upload the result as parent–child connections — the same integration model as connect_timelapse. Both capture cell divisions as one parent linked to two daughters. You segment first (Cellpose, Stardist, etc.), tag the objects, then run one of these to build the lineage.

Shared pipeline: load tagged polygons → group by XY/Z → rasterize each time frame into an integer label mask → run the tracker → map tracks back to the original annotation IDs → create connections.

  • Trackastra (workers/annotations/trackastra/) — transformer-based tracking on PyTorch (GPU with CPU fallback). Also loads the chosen intensity channel per frame, runs Trackastra.track, and maps the returned track graph back to annotations by each node's (time, label). Modes: greedy (default, allows divisions) and greedy_nodiv.
  • Ultrack (workers/annotations/ultrack/) — global-optimization tracking on CPU (open-source COIN-OR CBC solver, no Gurobi required). Runs Ultrack on the label stack and maps tracks back to annotations by nearest mask centroid, deriving division links from parent_track_id. Exposes max linking distance and an allow-divisions toggle.

Both connection sets always carry a descriptive tag (Trackastra / Ultrack) so they can be filtered and managed as a group in the UI.

What's included per worker

entrypoint.py, Dockerfile, environment.yml, a WORKERNAME.md doc, and a pytest suite. Trackastra also bakes its pretrained general_2d model into the image. Both are registered in build_machine_learning_workers.sh and REGISTRY.md (annotation workers 26 → 28).

Testing

  • 22 unit tests (12 trackastra + 10 ultrack) pass locally against real numpy / scikit-image / networkx / pandas with the tracker libraries mocked. They cover rasterization, graph/tracks→connections mapping, division handling, time-ordering, nearest-centroid matching, default connection tagging, and end-to-end compute.
  • The third-party APIs were verified against the current Trackastra and Ultrack sources (Trackastra.track returns (graph, masks); node attributes time/label; Ultrack track(labels=…, overwrite=True) and to_tracks_layer(include_parents=True) with track_id/t/y/x/parent_track_id; config attribute paths).

Not yet exercised (require a GPU/Docker build and live data, unavailable in the authoring environment): the Docker image builds themselves and an end-to-end tracking run against real NimbusImage data.

Code review incorporated

  • Dropped Trackastra's ilp mode — the ILP solver stack (ilpy + SCIP/Gurobi) is not installed, so selecting it would crash at runtime.
  • Both workers always stamp a descriptive connection tag in addition to any configured output tags.
  • Removed the inaccurate annotationShape="polygon" Docker label (these produce connections, not polygons).
  • Removed error-swallowing try/except around Ultrack config assignment so real misconfiguration surfaces.
  • Collapsed a double tracks_df.iterrows() pass into one and dropped an unused variable.
  • Reverted an unrelated Cellpose-SAM row that the REGISTRY.md regeneration swept in.
  • Documented the tracking limitations.

Limitations

Each frame is rasterized to a single label image, so objects are expected to be non-overlapping instance segmentations within the image bounds; overlapping or out-of-bounds objects may be dropped. Ultrack's map-back is nearest-centroid with no distance cutoff, so it assumes distinct per-frame centroids. "Forbid divisions" is a strong solver penalty, not a hard constraint.

🤖 Generated with Claude Code

https://claude.ai/code/session_0185y3NmemLJQdz5pY1yYQPQ

Both workers link existing tagged polygon segmentations across time into
lineage tracks and upload the result as parent-child connections (like
connect_timelapse), handling cell divisions as one parent to two daughters.

- trackastra: transformer-based tracking (PyTorch, GPU with CPU fallback);
  rasterizes tagged polygons + loads channel intensity per frame, runs
  Trackastra.track, maps the track graph back to annotations by (time, label).
- ultrack: global-optimization tracking (CPU, CBC solver); rasterizes tagged
  polygons, runs Ultrack on the label stack, maps tracks back by nearest mask
  centroid, and derives division links from parent_track_id.

Each worker has an entrypoint, Dockerfile, environment.yml, docs, and a pytest
suite (20 tests total) covering rasterization, graph/tracks-to-connections
mapping, divisions, and end-to-end compute with the tracker mocked. Registered
in build_machine_learning_workers.sh and REGISTRY.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185y3NmemLJQdz5pY1yYQPQ

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db848ef451

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

},
'Tracking mode': {
'type': 'select',
'items': ['greedy', 'greedy_nodiv', 'ilp'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the unsupported ILP tracking option

If a user selects ilp, the shipped Trackastra image only installs base trackastra (workers/annotations/trackastra/Dockerfile uses pip install trackastra) and does not install the optional ILP/solver dependencies required by Trackastra's ILP path, so model.track(..., mode='ilp') will fail at runtime instead of creating connections. Either install the ILP extras and solver in the image or omit this choice from the worker interface.

Useful? React with 👍 / 👎.

Comment on lines +222 to +223
if parent_track_id is None or parent_track_id < 0:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip zero parent IDs for founder tracks

When Ultrack emits parent_track_id == 0 for root tracks (the comment above says some versions use this), this condition treats zero as a real parent. If a track with ID 0 exists, founder tracks can be connected as division children of track 0, corrupting the lineage; normalize 0 to -1 before storing it or skip <= 0 here if zero means no parent.

Useful? React with 👍 / 👎.

claude added 2 commits July 7, 2026 05:05
- trackastra: drop the "ilp" tracking mode from the interface; the ILP
  solver stack (ilpy + SCIP/Gurobi) is not installed in the image, so
  selecting it would crash at runtime. Only greedy modes are exposed.
- Both workers now always stamp a descriptive connection tag ("Trackastra" /
  "Ultrack") in addition to any configured output tags, so tracking
  connections are identifiable and filterable in the UI (matching the
  convention of connect_timelapse / sam2_video).
- Remove the misleading annotationShape="polygon" Docker label from both
  workers; they create parent-child connections, not polygon annotations.
- ultrack: remove the broad try/except around config assignments so a real
  misconfiguration surfaces instead of silently ignoring user settings; the
  field names are correct against the current Ultrack API.
- ultrack: collapse the double tracks_df.iterrows() pass into one and drop the
  unused has_z variable.
- REGISTRY.md: revert an unrelated Cellpose-SAM row change that the registry
  regeneration swept in, keeping this PR focused.
- Document tracking limitations (non-overlapping instance masks, out-of-bounds
  polygons, nearest-centroid mapping, soft division penalty) in both docs.
- Add tests asserting the default descriptive connection tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185y3NmemLJQdz5pY1yYQPQ
Codex flagged that the division-link guard skipped only parent_track_id < 0,
so a founder track whose parent_track_id is 0 (a "no parent" sentinel some
Ultrack versions emit) would be wired as a child of track 0 if such a track
existed, corrupting the lineage. Ultrack track ids are positive, so anything
<= 0 means no parent. Fix the guard and add a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185y3NmemLJQdz5pY1yYQPQ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants