Skip to content

feat(segment,track)!: segment and track write into the plate; labels and tracks.geff (#360, #361) - #413

Merged
ieivanov merged 49 commits into
mainfrom
feat/track-labels-geff
Oct 24, 2026
Merged

ieivanov merged 49 commits into
mainfrom
feat/track-labels-geff

Conversation

@ieivanov

@ieivanov ieivanov commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #360. Closes #361. Part of #363. Builds on #424 (biahub project): the PR is based on feat/project-cli until that merges.

In draft until royerlab/ultrack#290 merges and the tracking results are validated (see the comments below). The iohub side (#489, #496) is merged and pinned from main.

What changes

Segmentation moves out of biahub track into biahub segment, and both write into the plate they read — with #424 the projected plate 4-assemble/<ds>_proj.zarr for 2D work, or the assembled plate for 3D. The pipeline becomes assemble → project → segment → track → QC. Per position:

Output Written by Details
<fov>/labels/<model> biahub segment cellpose instance labels, one OME-NGFF label image per model, named after it (A549: labels/nuclei). uint32, shape (T, 1, Z, Y, X): the image's axes with one channel, with the plate's Z (the DCA array standard wants every array 5D; iohub refuses a label with more than one channel). Written through iohub's process_single_position(output_label=...) (Biohub/iohub#489), so --resume works as for every other step.
<fov>/labels/tracks_<model> biahub track the same objects after ultrack's re-segmentation and the sibling filter, painted with track_id (A549: labels/tracks_nuclei). Chunks (16,1,1,128,128) (1 MB: the DCA 512 KB MUST and 1 MB SHOULD); shards from iohub's shards="200MB" size target (Biohub/iohub#461), which on the A549 frame is one 16-timepoint chunk by the full plane; same for segment's.
<fov>/tracks.geff biahub track GEFF 1.3 (zarr v3) from ultrack's Tracker.to_geff. seg_id (= track_id) links nodes to labels/tracks_<model> via related_objects. Tracklet/lineage props are declared. Axes are pixel coordinates with physical scale.
<fov>/tracks_<row>_<col>_<fov>.csv biahub track Same tracks as before, kept for now for downstream readers.

biahub segment. -o and the separate label plate are gone: the labels go into the -i positions as label images, one per model, named after it. Segmentation strategies accumulate in a plate as differently named models, and a name is only rewritten by the model that made it: --init refuses a label image whose recorded settings (or shape) differ from the current model's, pointing at a new model name. --init creates the empty label images and warms the cellpose weights; output_ome_zarr_version is rejected, since the labels take the plate's version. nextflow/configs/a549/segment.yml takes the cellpose values tracking was validated with (cpsam_v2, diameter 80, min_size 2500), as one model nuclei.

biahub track. segmentation_method: labels (the default) names the label image to track, labels: <model>, with labels_sigma (ultrack's contour smoothing, formerly cellpose_config.labels_sigma); the output name is derived, tracks_<labels>. An optional output_label names it instead, for several tracking runs of one segmentation side by side; then the GEFF and CSV are tracks_<output_label>.geff / tracks_<output_label>_<fov>.csv, while the default stays tracks.geff / tracks_<fov>.csv, the names the downstream readers (VisCy#525, biocurator#997) expect. cellpose_config, segmentation_method: cellpose and target_channel on this path are rejected with a pointer to the segment config. foreground_contour stays as the legacy path and keeps target_channel and input_images. Blank frames in the CSV are filled with the nearest frame's labels, as the images were before, so a dropped timepoint does not break every track. All cellpose code leaves track.py; track needs no GPU and runs on the preempted partition with the other per-position steps: ultrack cannot resume, but a position now tracks in a minute or two, so a preemption costs that much and the retry covers it.

No Z placement. 2D work writes (T, Y, X) labels into the projected plate's one plane and 3D fills the stack; the plane-placement logic of earlier revisions (tracked_z_window, the z_window/z_index provenance) is gone, since #424 records the source window and focus plane on the projected plate itself. Each label image records its settings under biahub-segment / biahub-track (track's with the full resolved Ultrack config).

Sibling filter and the GEFF. min_sibling_length (#415) edits the tracks table and the labels after tracking; the GEFF is exported from that same table through ultrack's to_geff(tracks_df=...) (royerlab/ultrack#290), so the labels, tracks.geff and the CSV agree. biahub is pinned to the fork commit carrying #290 until it merges.

CLI details.

  • track --init checks the plate's Z against output_mode and that labels/<labels> exists in every position (so a pipeline missing its segment step fails in the init phase), then creates the empty tracked label images with iohub's create_empty_labels, which also records the settings on the label group.
  • track -o is optional: without it each worker builds its Ultrack database in a per-FOV scratch directory under its $TMPDIR and deletes it after export, and SLURM logs go to ./slurm_output; with it the databases are kept in {-o}/{row}_{col}_{fov}/. ultrack's own temporary zarr arrays used to leak into $TMPDIR too; Fix create_zarr deleting and then leaking its temporary store royerlab/ultrack#289 (in the pin) removes them.
  • TrackingSettings.output_ome_zarr_version is removed, since the labels take the plate's version.
  • The DCA label chunk sizing (label_layout) lives in biahub.utils.ngff, shared by both commands; the scaffolding itself is iohub's.

Nextflow.

  • New --segment_config (a549/segment.yml) between project and track: segment's init runs after project's and creates the label images, track's init after segment's. Segment runs on gpu_preempted with --resume, track on the cpu label (the preempted partition). Neither has a step directory: A549 runs end at 4-assemble (with <ds>.zarr and <ds>_proj.zarr in it) like neuromast runs. A run naming --track_config without --segment_config fails at track --init with the message above.
  • --qc_track_config / qc_track.yaml are removed; passing the param errors. The instance counts move to --qc_project_config (a549/qc_project.yaml) on the projected plate: instance_count.label: tracks_nuclei (imaging-qc#243) plus blank on the anchor channel. It is planned after the last of project/segment/track's inits and computed after the last of them runs; qc.yaml is pixel metrics on the assembled store and runs concurrently. Both stores are tabs of one report.
  • Cleanup: segment's resume markers live under the projected plate's .iohub-progress/ entry, which cleanup already removes; the label images are inside the deliverable plates and are never targets.
  • The reconstruct-with-nextflow skill: step lists, the run template (--segment_config), the schema notes and the pre-launch check script, which now checks segment.yml's channels against the projected plate, track.yml's labels against the segment models, and qc_project.yaml's label and anchor against both.

Dependencies

Testing

  • Unit tests on the branch (with the pinned iohub and ultrack): segment (label images in the input plate with resume, one image per model, strategies accumulating under different names, the settings and shape refusals), track (labels input, blank-frame filling, missing-label error at init, output_label naming the label and the tracks files, the legacy foreground+contour path, the GEFF's link to labels/tracks_<model>, the sibling filter, no GPU request), the settings rejections, and the A549 configs. nextflow lint passes.
  • Real data, end to end: on position C/2/000000 of 2026_08_11_A549_SEC61B_DENV_debug/v8's full-stack-mean projected plate, biahub segment (47 s, 1.6 GB on a GPU node) then biahub track (73 s, 9.4 GB, 16 CPUs) with the matched settings reproduce the earlier in-track run on the same plate exactly: 220 tracks, 58 divisions, 27.6 cells per frame in both, foreground IoU 0.9994 between the two tracked label images, every old object matched. The GEFF validates and points at ../labels/tracks_nuclei. Run script and configs: v8/run_segment_track.sh, configs/segment_projall.yml, configs/track_projall_labels.yml.
  • The 54-position comparison of the in-track revision against 5-track/ is in the comment below (matched run equal within solver noise; the current template keeps 72 % of the tracks and 59 % of the divisions).

Follow-ups

🤖 Generated with Claude Code

Tracking no longer creates a separate 5-track/<ds>.zarr plate holding the
labels as an image channel. It writes into each -i position of the
assembled plate (#360, #361):

- labels/<target_channel> as an OME-NGFF 0.5 label image: uint32, TZYX with
  the image's full Z, chunks (16,1,128,128) (1 MB, the DCA SHOULD), shards
  of 16 timepoints by the YX plane. 2D masks go in the middle plane of the
  tracked z-window (middle of the volume for a full-stack projection, the
  plane itself for a one-plane window, middle of a projected section); 3D
  masks fill the window. The track settings and z placement are recorded
  under `biahub-track` on the label image.
- tracks.geff via ultrack's Tracker.to_geff (zarr v3, seg_id = track_id,
  related_objects pointing at the labels, axes with units and scale).
- tracks_<row>_<col>_<fov>.csv, kept for downstream readers for now.

`track --init` creates the empty label images with
create_empty_plate(label_names=...). -o is now a working directory for the
Ultrack databases and SLURM logs. TrackingSettings drops
output_ome_zarr_version, since the labels take the input store's version.

Nextflow: --qc_track_config and qc_track.yaml are removed. a549/qc.yaml
counts instances in the label image (imaging-qc#243, `label:`), so QC is
planned after track --init and computed after tracking.

Pins: iohub dev/dca-labels (iohub#485, #486), the ieivanov/ultrack fork
(royerlab/ultrack#288), imaging-qc f5ae656, geff/geff-spec >= 1.3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ieivanov and others added 4 commits October 8, 2026 16:22
…abels field

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… fix

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each worker now builds its Ultrack database in a fresh scratch directory under
its own $TMPDIR and deletes it once the FOV is exported. `-o` becomes optional
and keeps the databases in {-o}/{row}_{col}_{fov} when given (e.g. to re-solve
with other weights); the SLURM logs go to {-o}/slurm_output, else
./slurm_output, since submitit's folder must be on a shared filesystem.

The scratch directory also holds ultrack's own temporary zarr arrays:
create_zarr backs its default store with a TemporaryDirectory it lets go out of
scope, so those directories were recreated by the first write and left in
$TMPDIR, which on Bruno outlives the job.

The full resolved Ultrack config (defaults included) is recorded under
`biahub-track.ultrack_config` on each label image, so provenance no longer
depends on config.toml beside the database. The worker returns nothing rather
than a label array inside the deleted scratch directory.

Nextflow passes no -o, so tracking has no step directory (no 5-track/, no
README, no chmod of the SQLite file) and takes no number; its real logs stay in
nextflow/slurm_output/track/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ieivanov and others added 5 commits October 9, 2026 13:16
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
create_zarr now deletes its temporary store with the array, so the per-FOV
temporary directory only needs to hold the Ultrack database again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ieivanov added a commit that referenced this pull request Oct 16, 2026
create_zarr's default temporary store was deleted on return and then
recreated by the first chunk write, never to be removed, so every
tracking job leaked orphan chunk directories into the worker's $TMPDIR.
royerlab/ultrack#289 ties the store's lifetime to the array. Not in a
PyPI release yet; same revision as #413.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ieivanov added a commit that referenced this pull request Oct 16, 2026
* feat(track): optional sibling filter for short false divisions

Add `min_sibling_length` to the track settings (default off). When set, at
each division drop a daughter track of that many frames or fewer and merge
the other daughter into the parent (ultrack filter_short_sibling_tracks,
same meaning), in the tracks CSV and the labels, before writing. The
relabeled copy is kept in memory (ultrack's default temporary directory is
never removed) and rows are sorted by track and time.

Most false divisions in the A549 benchmark against TrackEdit annotations
were 1-2-frame daughters (virtual-staining hallucinations, neighbours linked
as daughters). On the four annotated 2026_04_28 FOVs with the mantis-v2 A549
config, min_sibling_length 3 raised division F1 from 0.58 to 0.64 (precision
0.44 -> 0.55, recall 0.84 -> 0.77). Enabled with 3 in the mantis-v2 A549
track config; documented (commented out) in the example settings.

* docs(track): state when the sibling filter keeps a division

ultrack's filter_short_sibling_tracks only acts when exactly one daughter is
short and does not divide again; two short daughters keep the division. Say
so in the setting, the A549 pipeline config and the example, and report the
measured effect of the A549 config on the annotated 2026_04_28 FOVs next to
the benchmark range.

* feat(nextflow): A549 track config segments a 60-plane phase-focus window (#418)

The mantis-v2 A549 track config averaged all Z planes before cellpose, adding
out-of-focus signal. Use biahub's focus window instead: 60 planes around the
in-focus plane found on Phase3D (1/3 below, 2/3 above), with Phase3D loaded
only for focus finding. Sizes are unchanged: smaller cellpose/ultrack sizes
helped mock wells but added false divisions in infected wells.

Measured against TrackEdit annotations, with min_sibling_length 3:
- 2026_04_28 (4 FOVs, 78 divisions): division F1 0.63 -> 0.68 (mock 0.65 ->
  0.70, DENV 0.58 -> 0.63), fewer predicted divisions (107 -> 102) and
  fragmented tracks (396 -> 376).
- 2026_04_21 (5 FOVs, independent, 38 divisions): same mitotic-cell
  detection as all planes (0.95) and division recall (19/38), with fewer
  predicted divisions (92 -> 80) and tracks (422 -> 382).
A 40-plane window scored higher on 04_28 (whose annotations were curated
from a 40-plane run) but lost mitotic cells that drift in Z on 04_21 (0.89).
Peak memory unchanged (~15 GB); focus finding adds ~40% run time. A test
checks the config parses against the current schema.

* build(deps): pin ultrack to royerlab main at the #289 merge

create_zarr's default temporary store was deleted on return and then
recreated by the first chunk write, never to be removed, so every
tracking job leaked orphan chunk directories into the worker's $TMPDIR.
royerlab/ultrack#289 ties the store's lifetime to the array. Not in a
PyPI release yet; same revision as #413.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(track): drop the MemoryStore workaround in filter_short_siblings

With ultrack pinned past #289 the default store is removed with the
array, so the in-memory store is no longer needed to avoid a leak.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Ivan Ivanov <ivan.ivanov@czbiohub.org>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Add `biahub project`, which turns a 3D plate into a 2D (Z=1) plate with one
projection per output channel: max, mean, sum over a Z range or a window
around a plane, or one plane (`slice`). The plane may be fixed or the in-focus
plane found with the registration engine's focus criterion (FocusSettings
subclass: centre crop, NA, wavelength), once per position (median over T) or
per timepoint. Ranges are half-open like concatenate's Z_slice and relative
to `center` when one is set, so the A549 60-plane window is
`center: focus, range: [-20, 40]`. Unlisted input channels are dropped with a
warning. The focus plane of every timepoint is recorded in the output
position's `biahub-project` metadata.

Segment and track stop selecting and projecting Z-planes. Segment loses
`z_slice_2D`: a model without do_3D segments in 2D and needs a Z=1 input.
Track loses `z_slicing` and the focus window; `output_mode` is checked
against the input Z. Both raise, pointing at `biahub project`, when a 3D
input meets a 2D request. Old keys are rejected with a migration message.

Nextflow: a project step between assemble and track (`5-project`,
`6-track` for A549); track reads the projected plate when project runs.
The A549 project config reproduces the previous tracking input (mean over
the 60-plane focus window on Phase3D), measured on the 800x800 centre crop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ieivanov and others added 7 commits October 19, 2026 14:40
…ill checks it

The A549 projection config lists every channel of a typical assembled plate
under the pre-rename names project sees inside the pipeline: the in-focus
plane of BF and Phase3D, the full-stack max of the raw fluorescence channels,
and the mean of the virtual-stain predictions over the 60-plane focus window.
The focus is found on Phase3D per timepoint, as biahub#400 asks for inference.
The brightfield key is `BF`, matching reconstruct.yml; the fluorescence names
and an older `BF - Oblique` are per-dataset edits.

The reconstruct-with-nextflow check script also validates project.yml both
ways (a listed channel the plate lacks is a MISS, an assembled channel the
config omits is NOT PROJECTED) and track.yml against the projected plate. The
skill carries unprojected channels into the pre-run plan for the user to
decide, runs the rename on the projected plate too, and suggests the
`BF - Oblique` edit in both reconstruct.yml and project.yml when the raw
store names it so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…4 accumulation

NumPy already accumulates safely (float64 for integer input, pairwise float32
for float32 input), so mean and sum no longer force a float64 temporary. The
output plate is float32 whenever a channel uses max, mean or sum -- float32 in
and out is the common case -- and keeps the input dtype only when every
channel is a slice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The focus record under the output position's biahub-project metadata is
focus: {channel, per, z, z_per_t}; the focus_ prefix repeated the block name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`range` meant absolute planes without a center and offsets with one. The two
forms now have their own keys: `start`/`stop` are absolute, half-open like
concatenate's time_indices; `below`/`above` count planes around `center`,
selecting [center - below, center + above), so the A549 window is
`center: focus, below: 20, above: 40`. The validator refuses mixing them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The projected store is a deliverable like the assembled plate: cleanup
removes only its slurm_output/ and .iohub-progress/, as the wiring already
did; say so in the comment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… shards

ProjectionSettings takes the output chunk shape and shards ratio with
concatenate's names and semantics (shard = chunks * ratio, C not sharded) and
passes them to create_empty_plate; None keeps iohub's defaults.

The A549 config, following the Dynamic Cell Atlas array standard where a 2D
float32 plate can: one-frame 256 x 256 chunks, the grid the assembled and
tracking stores share, and shards of 16 timepoints by the full frame (the
standard's T-shard floor; ~150 MB each, 30 files per position instead of 400).
iohub writes one shard's timepoints per unit, so resume is 16 frames coarse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The focus pre-pass ran the timepoints one after another while the job's
other workers idled; with per-timepoint focus on a long timelapse it was the
serial part of the step. The timepoints are independent, so with more than
one worker they now run in a spawned ProcessPoolExecutor sized by the job's
num_workers (the count the projection itself uses), each worker reading its
own volume. One worker keeps the in-process loop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…>_proj.zarr

The projected plate is the 2D companion of the assembled plate, not a step
of its own: the pipeline now writes it to <N>-assemble/<DATASET>_proj.zarr,
so project has no step directory and track stays 5-track. Cleanup already
covers the shared directory's slurm_output/ and .iohub-progress/; the
slurm_output README notes that a directory can hold two steps' stores. The
skill docs, run template and config comments follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ieivanov
ieivanov requested a review from aofei-liu October 21, 2026 23:18
@tayllatheodoro

tayllatheodoro commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

@ieivanov 2026_08_11 has no tracking annotations, so I can't score your two stores directly. We did run the same comparison (old setup vs the template) on our annotated A549 datasets, and there the divisions the template drops are mostly false ones.

2026_04_28 (4 TrackEdit-curated FOVs, 2 mock + 2 DENV, 78 divisions; division matching within 2 frames / 10 µm):

division recall division precision division F1 real divisions found predicted divisions tracks (GT 287)
old: mean over all planes, no sibling filter 0.84 0.44 0.58 ~65 155 491
template: biahub project @ 88f363e (60-plane window, per-timepoint focus) + min_sibling_length: 3 0.79 0.59 0.67 ~61 104 378
  • The template keeps 77 % of the tracks and 67 % of the divisions, close to your 72 % / 59 % on 08_11.
  • Of the ~51 divisions it drops, ~4 were real and ~47 false. Most false divisions were a daughter that lives 1-3 frames: a virtual-staining hallucination for a frame, a neighbour linked as a daughter, or a lobed nucleus split for a frame. The sibling filter removes those.
  • Detection is unchanged (0.99), and the mock and DENV wells both improve.

2026_04_21 (5 independent DENV FOVs, 38 divisions from mitosis labels; the old run here already had the sibling filter, so this isolates the projection):

mitotic cells detected real divisions found predicted divisions tracks
old: mean over all planes 0.95 19 92 422
template projection 0.95 19 81 388

Same divisions found with fewer predicted, so the dropped ones were false there too.

The per-change breakdown is in #415 (sibling filter: F1 0.58 -> 0.63) and #418 (focus window: 0.63 -> 0.68); with #424's projection, tracking matches the old track-side window on both datasets.

To check 08_11 itself, I'll annotate a few of its FOVs, which also adds a dataset to the benchmark. In the meantime, the annotated results support deploying the template.

run_project asked for its init estimate verbatim on every retry, so a low
estimate failed all attempts with the same 137. Use retry_memory like the other
SLURM steps (fix/nextflow-retry-memory-cap carries the 244 GB cap).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aofei-liu

aofei-liu commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

hmm I think labels/ needs to output tczyx as well and this reads to me that it seems to be tzyx? so just change to (t, 1, z, y, x) I think

@ieivanov

Copy link
Copy Markdown
Collaborator Author

@aofei-liu good catch, I brought this up with the team here: https://github.com/chanzuckerberg/biocurator/issues/1103#issuecomment-6049741709

@srivarra tells me that labels shouldn't have a Channel dimension based on OME spec since labels are "channels" themselves. It makes more sense to have multiple labels rather than one "label" with multiple channels within in. With this reasoning, I think labels here should stay tzyx rather than (t, 1, z, y, x).

… float32

max picks an existing voxel, so promoting a max-only plate to float32
doubled its storage for nothing and would truncate uint32 labels above
2^24. A store has one dtype, so a plate with any mean/sum channel is
still float32 throughout and its max/slice channels are widened on write.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@aofei-liu

aofei-liu commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

@ieivanov I think the NGFF spec as written allows for both shapes:
"Each dimension of the label should be either the same as the corresponding dimension of the image, or 1 if that dimension of the label is irrelevant." So (t, 1, z, y, x) under a tczyx image is what the spec describes, and a missing c axis is also legal. I think dca chose to adopt (t, 1, z, y, x). it would probably require a broader discussion around the team on whether or not we want to also allow t, z, y, x. I believe that the ops pipeline also writes to t1zyx ?

ieivanov and others added 3 commits October 23, 2026 15:38
One pin for the whole chain: main now carries #489 (output_label,
create_empty_labels), #488, #461 and #491, so the 16cdc4f branch pin and
the dev/pyramid-shards combination branch retire.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Base automatically changed from feat/project-cli to main October 23, 2026 22:45
@ieivanov
ieivanov marked this pull request as ready for review October 23, 2026 22:49
@ieivanov

Copy link
Copy Markdown
Collaborator Author

@aofei-liu I did some digging on the label array shape, I agree with you, implementing now.

A label image now has the image's axes with one channel rather than TZYX:
the DCA array standard wants every array 5D, and OME-NGFF's own layout note
reads a label's dimensions against the image's. iohub creates and writes
them that way (czbiohub-sf/iohub, label channel axis); here the DCA label
chunks become (16, 1, 1, 128, 128), segment's shape check and track's label
reads and writes index the channel axis, and the tests and skill notes
follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ieivanov and others added 2 commits October 23, 2026 16:29
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ieivanov and others added 4 commits October 23, 2026 17:02
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`label_layout` passes iohub `label_shards="200MB"` (Biohub/iohub#461:
the largest chunk-aligned shard under the target, filled X, Y, Z then T, C
never grown) instead of a computed `label_shards_ratio`, which iohub has
deprecated. On the A549 projected plate the result is the same geometry as
before, one 16-timepoint chunk by the full frame; #430's segment labels use
the same constant, so the two branches agree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ieivanov
ieivanov merged commit db247c7 into main Oct 24, 2026
7 checks passed
@ieivanov
ieivanov deleted the feat/track-labels-geff branch October 24, 2026 00:39
ieivanov added a commit to Biohub/VisCy that referenced this pull request Oct 24, 2026
…plate labels

`biahub track` as merged (Biohub/biahub#413) still writes `tracks.geff`
by default, but a run that names its tracked label (`output_label`) writes
`tracks_<name>.geff` / `tracks_<name>_<fov>.csv`, and the FOV now also holds
the label images: `labels/<model>` from `biahub segment` and
`labels/tracks_<model>` from track, which the GEFF's `seg_id` indexes.

- `read_fov_tracks` reads exactly one `tracks_<name>.geff` when there is no
  `tracks.geff` (several raise), before the CSV fallback.
- airtable `prepare`: in-plate detection matches `tracks*.geff`; the copy step
  carries the GEFF(s), the CSV and `labels/` into the VAST FOV, since
  `biahub concatenate` copies image arrays only.
- Docs name the layout; tests cover the named GEFF and the labels include.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ieivanov added a commit that referenced this pull request Oct 26, 2026
main moved segmentation out of track (#413: `biahub segment` writes labels/<model>
into the plate; track reads `labels:` and writes labels/tracks_<labels>, tracks.geff
and the CSV into it) and Z projection into `biahub project` (#424), and added
pyramids (#430). The trackers of this PR are reworked on top of that:

- tracksdata, hoct and trackastra link the instance labels of `labels:` (the
  default segmentation_method: labels) instead of running cellpose in track;
  foreground_contour and output_mode 3D are refused for them.
- hoct and trackastra read the image the labels were segmented from: the first
  channel `biahub segment` recorded in the label image's biahub-segment
  provenance, or hoct_config/trackastra_config.image_channel when set (override,
  or for a label image segment did not write). --init checks it exists.
- Their outputs follow #413: labels/<tracked_label> (5D) and the CSV. They write
  no tracks.geff yet (ultrack's export reads its own database; #441) and remove a
  stale one from an earlier run.
- hoct and trackastra request a GPU when biahub submits them; ultrack and
  tracksdata stay on the CPU partition.
- pyproject: main's track extra plus tracksdata (git), hoct and trackastra[ilp],
  with the gurobipy fork by Python version; uv.lock regenerated from main's.
- Tests and the track-with-biahub skill moved to labels written into the plate
  (project -> segment -> one track run per tracker, side by side by output_label).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants