Skip to content

Scale auto-selection, skeleton radius/prune-only/morphology, dask scratch fix - #82

Merged
davidackerman merged 14 commits into
mainfrom
feature-updates
Jul 15, 2026
Merged

davidackerman merged 14 commits into
mainfrom
feature-updates

Conversation

@davidackerman

@davidackerman davidackerman commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator

@stuarteberg

Summary

Four independent improvements, bundled on one branch.

1. Auto-select scale level when reading a multiscale group path

  • Passing a bare multiscale group path (no /s0) to a reader now resolves to a concrete scale level instead of failing, and logs the choice.
    • No target voxel size → finest level (s0).
    • measure's raw_path → the level whose voxel size best matches the segmentation, preferring a finer level over upsampling.
  • Adds OME-NGFF 0.5 support (reads attributes.ome.multiscales, not only top-level multiscales).
  • Precomputed's existing default-to-scale-0 now logs too.
  • Read-only: output/write paths always name a scale, so they're untouched.

2. Per-vertex skeleton radius (opt-in)

  • New write_vertex_radius (default False). When enabled, the full/ (and pruned/) skeletons carry a float32 radius vertex attribute (declared in info) that neuroglancer can color by. The geometrically-simplified output stays attribute-free.

3. Skeletonize: prune_only output + generalized morphology

  • New prune_only (default False): the second output becomes a prune-only skeleton written to pruned/ (short terminal branches removed, every surviving original vertex + its exact radius kept, no geometric simplification). tolerance_nm is ignored in this mode.
  • Skeleton radius stats (radius_mean_nm/radius_std_nm) are now computed on the pruned skeleton, consistent with num_branches and longest_shortest_path_nm.
  • New morphological_operations: an ordered sequence applied to each object's mask before skeletonization — erosion/dilation/opening/closing (with iterations and connectivity rank 1/2/3) plus the existing corner-bridge removal ("6"/"18"). Supersedes the legacy erosion shorthand, which is mapped into the equivalent op list for backward compatibility. Radii are measured on the union of the original and processed masks, so shrinking ops keep true-object radii while growing ops reflect the grown/hole-filled structure.

4. Dask local-directory writability fix

  • set_local_directory now probes actual writability (creates/removes a temp subdir) instead of relying on os.makedirs(exist_ok=True), which silently succeeds on an existing-but-unwritable dir. This prevents committing to an unwritable /scratch/<user> and then crashing a worker on dask-scratch-space; it now falls through to /tmp/<user>.

Adds tests for scale resolution, dask local-directory selection, per-vertex radius, prune_only, and morphological operations.

ImageDataInterface now resolves a multiscale group path to a concrete
scale-level array instead of failing to open it:

- No target voxel size given -> finest level (s0), so a bare group path
  defaults to full resolution.
- A target voxel size given -> the level whose voxel size best matches
  (exact preferred, ties broken toward finer), minimizing resampling.

measure passes the input segmentation's voxel size as the target when
opening raw_path, so raw intensities line up with the segmentation grid.
The selected level is logged in every case (including precomputed's
existing default-to-scale-0).
When resolving a multiscale group against a target voxel size, pick the
coarsest level that is still at or finer than the target so the source is
only ever downsampled onto the target grid, never upsampled (which would
fabricate detail by repetition). Fall back to the finest level only when
every level is coarser than the target.
NGFF >=0.5 nests multiscales under an 'ome' key in the attrs
(attrs['ome']['multiscales']) rather than at the top level. Add a
get_multiscales() normalizer that reads either layout and route both the
new scale-resolution helper and the existing OME voxel-size/translation
readers through it, so group paths resolve and voxel sizes are read
correctly for 0.5 datasets.
The full neuroglancer skeletons now carry the EDT-sampled radius as a
float32 'radius' vertex attribute (declared in the full info file), so
neuroglancer can read and color by per-vertex thickness. Radii are
attached in the np.argwhere voxel order that produced the vertices, so
they stay aligned, and are set after prune/simplify so the simplified
skeletons remain geometry-only. The single seed-vertex fallback also
carries its radius.
…alse)

Per-vertex radius output is now opt-in via the write_vertex_radius
parameter (default False). When off, full skeletons declare no vertex
attributes and carry no radius bytes; when on, full skeletons get the
float32 radius attribute as before. Threaded through the worker and info
writer so the declaration and the encoded bytes stay consistent.
…ites)

Verify that passing a bare multiscale group path (no scale level) to the
skeletonize and contact-site readers auto-resolves to s0, confirming the
centralized ImageDataInterface resolution covers these tools.
set_local_directory used os.makedirs(exist_ok=True) to vet each candidate,
which succeeds even when the directory already exists but isn't writable
(e.g. /scratch/<user> left by another job). The code then committed to it
and a dask worker later crashed creating <dir>/dask-scratch-space with
PermissionError, never reaching the /tmp/<user> fallback.

Probe real writability by creating and removing a temp subdir inside each
candidate, so an existing-but-unwritable dir is skipped. Add an optional
candidate_dirs arg for testability and a regression test.
# Conflicts:
#	tests/operations/test_skeletonize.py
prune_only=True makes the second skeleton output a prune-only skeleton
(short terminal branches removed, every surviving original vertex kept --
no geometric simplification), written to a pruned/ directory instead of
simplified/. tolerance_nm is ignored in this mode.

Radii are now attached before prune(), which carries each surviving
node's exact original EDT radius through (graph_to_skeleton rebuilds
vertices and radii from the same node set), so the prune-only output
carries faithful per-vertex radii when write_vertex_radius is set -- the
full/prune-only info files declare the radius attribute, simplified does
not. The second output dir name is threaded through info, segment
properties, sharding, and emit.
radius_mean_nm/radius_std_nm now describe the pruned skeleton, consistent
with num_branches and longest_shortest_path_nm (also pruned). Radii are
attached before prune() unconditionally so the pruned radii are available
for stats regardless of write_vertex_radius (which still only controls
whether radii are written into the skeleton bytes). Falls back to the
full sampled radii if pruning dropped them.
Add a morphological_operations parameter: an ordered sequence of ops
applied to each object's mask before skeletonization. Supports erosion,
dilation, opening, and closing (with iterations + 6/18/26 connectivity),
plus the existing corner-bridge removal ('6'/'18'). Supersedes the legacy
erosion shorthand, which is mapped into the equivalent op list for
backward compatibility.

Radii are measured on the union of the original and processed masks, so
shrinking ops (erosion/opening) keep true-object radii while growing ops
(dilation/closing) reflect the grown/hole-filled structure. This lets you
fill holes (closing), drop thin protrusions/specks (opening), or thicken
thin structures (dilation) per object, addressing cases erosion alone
couldn't.
The morphological-op connectivity field now uses the same 1/2/3 rank
convention as the rest of the codebase (1=faces/6, 2=faces+edges/18,
3=faces+edges+corners/26) instead of a third 6/18/26 convention. Default
is 1 (faces), preserving the legacy erosion=True behavior (6-connectivity
cross) and matching the standard minimal structuring element for
erosion/dilation. The corner-bridge ops keep their 6/18 names, since
there the number is the connectivity being enforced (what
remove_unbridged_adjacencies accepts), not a structuring-element rank.
Group.create_dataset was removed in newer zarr (3.2.x on CI); switch the
test helper to create_array.
@davidackerman
davidackerman merged commit 4f4750c into main Jul 15, 2026
2 checks passed
@davidackerman
davidackerman deleted the feature-updates branch July 15, 2026 17:15
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.

1 participant