Skip to content

feat(processing): pack labelmap inputs by multiplicity and label count - #964

Open
PaulHax wants to merge 13 commits into
segmentation-integrationfrom
processing-labelmap-packing
Open

PaulHax wants to merge 13 commits into
segmentation-integrationfrom
processing-labelmap-packing

Conversation

@PaulHax

@PaulHax PaulHax commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Builds on #963.

Renames the segmentation result instruction, chooses the labelmap bit depth from the label count, and packs processing labelmap inputs according to the input multiplicity the task declares.

Commits:

  • refactor: rename segmentation result instruction
  • feat: choose labelmap bit depth from label count
  • feat: pack processing labelmaps according to input multiplicity

The commits after the first three are fixes, performance work, docs and tests for processing and for the segmentation code that depends on the packing in this change. Each is its own commit with its own message.

@PaulHax
PaulHax added this pull request to stack #965 September 21, 2026 20:08
@netlify

netlify Bot commented Sep 21, 2026

Copy link
Copy Markdown

Deploy Preview for volview-dev ready!

Name Link
🔨 Latest commit a0419ef
🔍 Latest deploy log https://app.netlify.com/projects/volview-dev/deploys/6ab1ac2f3c4c3e00085efaab
😎 Deploy Preview https://deploy-preview-964--volview-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Composed labelmaps use unsigned 8-bit voxels for up to 255 labels and unsigned
16-bit voxels for 256 through 65535. An imported 16-bit labelmap splits into
byte masks in one sweep. Fractional, NaN and infinite voxel values are excluded
during import.
A labelmap input stages the overlap-free parts of a segmentation, one file
each. An input that accepts multiple files receives every mask across as many
files as overlap requires. A single-file input receives the non-conflicting
subset that fits one file, starting from a segment the image holds, and the
user is told which masks were left out. No mask is clipped.

An empty segmentation is left unbound, an unconfirmed preview survives a task
that stages no labelmap, and the overlap sweep no longer reruns on every
segment selection.
Every segment actor is drawn at an opacity just under 1, so vtk.js puts it in
the order-independent translucent pass with depth writes off. Overlapping
segments therefore blend to the same colour whichever one comes first in the
registry, and no amount of reordering makes one occlude the other.

The README's "Ordering and overlap" section, the reorder handle's tooltip and
the eyedropper comment all told the user that registry order decides what is
drawn on top. They now describe what the order really controls: the sidebar,
shortcuts, selection and picking, and flattened export precedence, and they
say plainly that overlaps blend in the slice view.

A new spec reads the README section and scans the segmentation and component
sources so the claim cannot come back.
A result's segment list names the bins its run looked for; the labelmap carries
only the ones it filled. convertImageToLabelmap merged the wire descriptions
onto the descriptors the decode found in the voxels, so a declared value with no
voxels and no header block created nothing, and a run that found no spleen read
exactly like a run that never looked for one.

Append the declared values the decode did not cover, guarding 0 for background.
The seg.nrrd header path already does this, so both import paths now keep the
same segments. They join the list at the decode rather than at the split,
because the import pairs the masks the split returns with the descriptors it
decoded by position.

The declared empty gets a mask whose extent covers nothing: it draws nothing and
survives the state file, which already restores a mask that covers nothing as
one that covers nothing.

The existing assertion that pinned the dropping is rewritten to pin the new
behaviour, and now asserts the apply succeeded rather than discarding it.


A segment that is declared both in the header and on the wire, or across several
components, is declared once.
…file

A saved labelmap read straight from the archive was decoded with no name to
fall back on, so its segments came back as 'Segment 1', 'Segment 2' while the
same labelmap converted live, or restored through a loaded dataset, was named
after the file it arrived in.

The decode now takes a base name from the caller when no loaded image can give
one, and the restore passes the labelmap's own name. The parity spec compares
the archive-backed catalog with the live one whole, rather than pinning the
name that said nothing.
The save dialog listened for Enter on the window, so any keystroke
reached it: opening the format menu with the keyboard and pressing Enter
to choose an option also started the export, with the format the select
had not applied yet. Nothing checked whether a save was already running
either, and one keystroke arrives twice when the form submits as well,
so a second run composed the same masks again and downloaded the archive
a second time.

The listener now sits on the dialog's card, so it hears the keystrokes
that belong to this dialog and not the ones an overlay above it owns,
and the save returns early while one is in flight.

A new spec mounts the dialog attached to the document and stops the save
where it hands its parts over, so it can count the runs one keystroke
starts.
…describe

A legacy manifest can describe fewer values than its labelmap carries. Restore split out
only the described ones and dropped the rest. The others now get a default segment, as a
live import of the same labelmap gives them.
Auto-loading skipped any job result whose declared intent the applier could
not route, and nothing reached the user: the completion toast had already
announced the results, and the Jobs panel still offered the file as a plain
download.

The skip now surfaces a warning naming the result and pointing at the download
that is still available. An intent name this client does not know gets the
version wording. A known name whose payload fails the strict shape union says
the result does not carry the payload that intent requires. Results that
declare no intent stay quiet, since there is nothing to route for them.

The contract README describes the intent vocabulary version accordingly.
A 0.2.0 producer still emits the earlier intent name. The client reads it
as the current one, so the producer can move to the new name afterwards.
A segmentation result is recognized as already applied through the `source`
the producer stamps on it, and that field is optional on the wire. A backend
that omits it left the masks with no receipt, so after a reload the re-adopted
job's Load button imported every mask again, arriving as Tumor (2), Tumor (3)
and so on.

The client now mints the key when the result carries none, from the provider
and job it submitted plus the result row's id. Nothing new goes on the wire:
the minted key is scene provenance, stored on the mask binding and restored
like a producer's own, and a producer-sent source still wins. A case applies a
source-less result, hands its receipt back as a restore would, and shows the
second apply converts nothing.


Annotation results get the same minted key, so a re-applied annotation result
is recognized without a wire source too.
@PaulHax
PaulHax force-pushed the processing-labelmap-packing branch from 30b6191 to a0419ef Compare September 21, 2026 22:14
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.

Simplify the segmentation workflow Communicate to the user when segmentation series is being loaded

1 participant