neu-morpho¶
Segment morphology from segmentation volumes: multi-resolution Draco-encoded
meshes (via vol2mesh), written in neuroglancer-precomputed format, and
measurement of published meshes and skeletons.
Orchestration across segments:
blockrun(dask local/SLURM + resumable manifest).Segmentation-array I/O:
neu-vol(read precomputed / zarr / … regions and crop views).Meshes:
vol2mesh(marching cubes → simplify → Draco → multi-resolution neuroglancer mesh).Skeletons are made by
neu-skel, a separate package: GPL-licensed, because its tracers build onkimimaroanddijkstra3d.neu-skel runadds skeletons to a work dir this package’srunhas indexed, writing them into the same volume, metrics DB and run summary. Everything that reads or measures skeletons —neu_vol.readback,neu-morpho measure skeletons, path distances — is here and needs no GPL code.
Large segments — whose binary mask over a big bounding box would OOM (the failure mode this package is designed around) — are meshed chunked and stitched rather than materialized whole.
Environment¶
One conda environment covers this repo and the two below it, each installed
editable. blockrun and neu-vol must be sibling directories.
conda activate neu-env
pip install --no-deps -e ../blockrun -e ../neu-vol -e .
python -m pytest -q
Python is pinned to 3.12: vol2mesh and dvidutils are only built for py312
on flyem-forge, and they are conda-only (no PyPI equivalent), so the conda
environment — not pip — has to provide them. The
neu-suite README describes how to
build the shared environment.
Running it¶
Installing the package provides the neu-morpho command (equivalently
python -m neu_morpho):
neu-morpho run ... # the pipeline
neu-morpho progress <work-dir> # live per-stage counts
neu-morpho run-report <work-dir> # self-contained HTML summary
neu-morpho help [COMMAND] # what --help prints, as a subcommand
run drives the stages against a dask cluster — local or SLURM, chosen by
--config. Every stage is idempotent, so re-running the same command resumes.
The stages¶
--stages takes a comma-separated subset, run in this order. The default is
index,mesh. Skeletons are a separate command, neu-skel run, against the same work
dir.
stage |
what it does |
|---|---|
|
Copies the ROI’s labels out as a precomputed volume, so the meshes and skeletons can be viewed against the segmentation they came from. Off by default — it is pure I/O and usually the most expensive stage. |
|
Scans the volume once and records every body’s bounding box and voxel count in a SQLite metrics DB. |
|
Multi-resolution Draco meshes, one per body. |
index is what makes a size filter possible. It records every body’s voxel count
and bounding box in the metrics DB, and --min-voxels / --limit-bodies build the
allowlist from those counts; without the DB there is no way to say “only bodies above N
voxels”. mesh and neu-skel’s skel work block by block and read no bounding box, so
with an explicit --allowlist (or none) they run without it. It writes no volume data.
Once the DB exists you can re-run the later stages without it: --stages mesh is the
normal way to redo meshing after a parameter change. mesh runs in two
phases — fragments per (body, block), then one output assembled per body — so a re-run
of only stage 2 reuses the fragments already on disk instead of recomputing them.
# 0. look at the pyramid and pick scales from real metadata (no cluster, no writes)
neu-morpho run --src /path/to/seg --describe
# 1. one small cube, in-process — the fastest way to see it work end to end
neu-morpho run --src ... \
--dst /path/to/morpho/segmentation --work-dir /path/to/morpho \
--serial --roi 0,0,0,512,2048,2048 --roi-scale 2 --stages index,mesh
# 2. the same cube on SLURM, surviving logout, publishing to s3
nohup env PYTHONUNBUFFERED=1 neu-morpho run --src ... \
--dst s3://bucket/sample3/segmentation --work-dir /path/to/morpho \
--config dask-slurm-example --config ~/my-site.yaml --workers 48 \
--roi 0,0,0,512,2048,2048 --roi-scale 2 --stages index,mesh > run.log 2>&1 &
squeue -u "$USER"
# 3. widen or drop --roi for the whole volume; step 2's work is reused
PYTHONUNBUFFERED=1 is the console-script equivalent of python -u; without it the
log lags a long run in 8 KB blocks. Keep that log off a shared network filesystem such
as CephFS: unbuffered, every line is its own write, and once another node reads the file
(tail -f) each one becomes a synchronous round trip. A local disk or your home directory
is fine.
--roi-scale is required whenever --roi is given: the same six numbers name a
box four times smaller per pyramid level, and the wrong one silently processes the
wrong region instead of failing.
The same run from Python, where plan checks the arguments against the source and
counts blocks without writing anything:
from neu_morpho import pipeline
p = pipeline.plan(src, "/path/to/morpho/segmentation", work_dir="/path/to/morpho",
roi=(0, 0, 0, 512, 2048, 2048), roi_scale=2)
summary = pipeline.run(p) # in-process; pass client= to run on a dask cluster
Cluster config¶
--config takes a bundled template name or a path, and is repeatable,
deep-merged left to right. The templates (dask-local, used by default, and
dask-slurm-example) ship with blockrun, next to start_dask, so every
consumer shares one set. An overlay carries only the keys that differ:
neu-morpho run --config dask-slurm-example --config ~/my-site.yaml ...
# my-site.yaml — everything else comes from the template
jobqueue:
slurm:
account: my-account
queue: my-partition
log-directory: /path/with/room
Unrecognised keys raise rather than merging silently, and the effective merged config is written into the run’s work dir. Site-specific configs and dataset allowlists are deliberately not in this repo.
Two destinations: --dst and --work-dir¶
--dst is the published volume — the precomputed labels with meshes and
skeletons written inside it, so a single neuroglancer layer carries all three.
It may be a local path or an s3:// URL.
precomputed://file:///path/to/morpho/segmentation
precomputed://s3://bucket/sample3/segmentation
That volume’s info gets "mesh": "mesh" and "skeletons": "skeleton" (the
precomputed spec’s subdirectory-naming keys), and the copy carries voxel_offset
so it lands at its true global position rather than at the origin.
--work-dir is everything else — metrics.db, manifests, failure logs and
the stage-1 fragment stores. It must be an ordinary filesystem path: it holds a
sqlite DB, appended JSONL and a fragment store read back with file I/O, none of
which work over an object store. It defaults to the parent of a local --dst,
and is required when --dst is remote.
--dstchanged meaning. It used to be the run root, with the volume at<dst>/segmentation. It is now the volume itself. An existing invocation passing--dst /path/to/morphowill publish to.../morphodirectly rather than.../morpho/segmentation. Add/segmentationto keep the old layout — the work dir then defaults to the old run root.
Because bookkeeping no longer lives inside the output, a manifest can outlive the
data it describes: clear --dst and a resumed run would skip every task and
report success having written nothing. Each stage refuses to resume when its
manifest records completed work but its output info is gone, naming
--no-resume as the explicit override.
A resume also keeps to the bodies stage 1 was run for. A completed block is not
revisited, so a body added to the allowlist afterwards would get nothing; the run
refuses instead. Mesh or skeletonize new bodies with a new --work-dir. Blocks are
recorded by index, so a resume also refuses a different --block or scale, whose block
(0, 0, 0) is a different region, and different --smoothing, whose finished blocks were
meshed without it; change any of them in a new --work-dir, or with --no-resume.
--dry-run reports the plan (scales, voxel sizes, block counts per stage)
without touching anything — worth running before any large job.
The ROI filters blocks on the global grid, it does not re-tile. A block’s index, its region and its nm coordinates are the same numbers in a small run and in the full run, so widening the ROI later reuses the fragments and manifest entries already on disk instead of redoing them. The caveat: a body straddling the ROI edge is built only from the blocks inside it, so it stays truncated until its neighbours are run.
Scales are integers; voxel sizes are read from the source. --mesh-scale 2
looks up that level’s actual resolution in the precomputed info (or the zarr
OME metadata) rather than assuming 2**scale. Real pyramids are often
anisotropic — z frequently isn’t downsampled at all — and that assumption is
exactly what misaligns skeletons against meshes.
Size the cluster to one block, not one body. That is the whole point of the
block-first design: peak memory is a single block’s mask and its distance
transform regardless of how large the bodies in it are, so a constraint pinning
jobs to fat nodes buys nothing and only shrinks the pool of nodes the job is
eligible for. Read the sizing comments in blockrun’s dask-slurm-example
template before changing --block.
When something fails¶
Task granularity differs per stage, and so does what a failure costs — the asymmetry is deliberate:
stage |
one task is |
on failure |
|---|---|---|
|
one block |
raise, abort the stage |
|
one body |
record |
Stage 2 isolates, because bodies are independent: a bad one costs exactly
that body, is recorded in the manifest, is logged with its traceback to
<work-dir>/failures.{mesh,skel}.jsonl, and is retried on the next run. One
pathological body cannot kill hour 11 of a 50k-body run.
Stage 1 does not, and that is the point. Stage 2 aggregates across blocks, so a silently skipped block does not leave a hole in one block — it truncates every body passing through it and erases outright any body lying wholly inside it, while the output still looks complete. Stage 2 cannot distinguish “this body had 3 fragments” from “it had 4 and one block died”. A crash you notice; a truncated neuron you may not.
Isolation is right for one odd body and wrong for a broken environment — 40k
identical failures are one data point, not 40k. So a stage also aborts on a
systemic exception (MemoryError, ImportError, and OSError with
ENOSPC / EDQUOT / EROFS, all of which recur for every remaining task by
construction) or after max_consecutive_failures (default 10, one success
resets, 0 disables). Ctrl-C always stops a run immediately. The CLI logs failed
body ids per stage and exits non-zero, so a scripted pipeline cannot mistake
a partial result for a clean one. --allow-body-failures makes it exit 0 when per-body
failures are all that went wrong, for a pipeline that should carry on and retry them
later; an aborted stage still exits non-zero.
One caveat on the metrics¶
mesh_area_nm2 is measured on the decimated mesh, since stage 1 simplifies
before writing fragments (decimation_fraction defaults to 0.1). It is a sound
relative size measure across bodies; do not read it as an absolute membrane
area without checking the bias against decimation_fraction=1.0 on a sample.
License¶
©2026 The Simons Foundation, Inc.
Licensed under the Apache License, Version 2.0. See CONTRIBUTING.md for how contributions are licensed.