neu-lib¶
The vocabulary the neu-suite packages share: boxes and grid arithmetic, the voxel-to-nanometre frame, and the two geometry types meshes and skeletons are carried in.
numpy only. Nothing here reads a store, opens a canvas, or imports anything else —
which is the whole reason it is its own distribution rather than part of neu-vol. Every
tier of the suite can name these types without inheriting an I/O stack, and this builds
on Python 3.11 as well as 3.12 (the rest of the suite is pinned to 3.12 by vol2mesh and
dvidutils, both conda-only). If something here ever needs to open a store, it belongs
in neu-vol instead.
from neu_lib import (BBox, Frame, Mesh, Piece, ScaleInfo, Skeleton, align_box,
box_predicate, mask_predicate, skeleton_tube, to_xyz)
name |
what it is |
|---|---|
|
a half-open box |
|
the same arithmetic on plain tuples, per axis |
|
voxel indices → physical nm, per axis, with an origin |
|
an array and the frame that says where it is |
|
the single zyx→xyz conversion, for a renderer |
|
vertices and faces, nm, zyx |
|
vertices and an edge list (not polylines), nm, zyx, with |
|
one pyramid level: index, shape, key, and the |
|
build the |
|
a skeleton as a solid tube, one truncated cone per edge |
Piece: an array and where it is¶
ScaleInfo pairs a shape with a Frame and answers “what does a voxel at this level
mean”. Piece pairs the data with one and answers “where is this box, and what is in
it”:
piece.shape # (364, 244, 244)
piece.bbox # BBox in the frame's voxels — where it sits in its parent
piece.bounds_nm # the same box in nm, as floats
piece.crop(box) # a sub-piece, its origin shifted to match
It also carries kind (what the voxels mean — image / probability /
segmentation, which decides whether coarsening may average or must take a mode) and
name (what it is called; neu_vol.read_piece sets both from the source).
It exists because every place that read a crop used to carry the array here and the voxel size and origin there, and each such pair is a chance to drop the origin — which puts the data at nm zero instead of on top of what it came from, silently.
Transforming one¶
out = piece.apply(gaussian_filter, sigma=2) # frame, kind, name survive
out = piece.apply(threshold, kind="segmentation") # ...unless you say otherwise
out = piece.copy(); out.array[mask] = 0 # or edit a copy in place
apply calls fn(array, *args, **kwargs) and returns a new piece. Two guards, and they
are the same shape — each is an observable signal that a transform did something the
metadata cannot follow:
A changed spatial shape needs
frame=. Shape and frame are two halves of one statement about where the voxels are: a 2× downsample halves the shape and doubles the voxel size, so carrying the old frame through would place the result at half its real size. The factor is not guessed — real pyramids are anisotropic, and a crop changes the origin instead of the size.A changed dtype needs
kind=, one of the three, or"same"if the meaning is unchanged, orNoneif it is no longer known. Nothing can detect a change of meaning —(a > 0.5).astype("float32")changes neither shape nor dtype — but a dtype change is the signal that is available, and it catches the two that occur: a probability map thresholded to a mask, and a mask labelled. Full dtype equality, notdtype.kind, becauseuint8 → uint32is exactly the connected-components case.
copy(**changes) is the same thing without a function, and it copies the array —
dataclasses.replace would re-pair the same one, so an in-place edit on what looks like a
copy would reach into the original and into whatever that was a view of.
One coordinate space, and it is the frame’s voxels. bbox and crop both speak it,
not array indices: a piece read from voxel 7819 of a level reports lo = 7819, because
that is the number that places it back or compares it with a sibling. Array indices are
always 0..shape and answer nothing.
bbox is integers, bounds_nm is floats, and they are named apart on purpose. BBox
has integer bounds and every grid operation on it assumes that, so a box in nanometres
cannot be one without truncating.
Frame.voxel_box(bounds_nm) is the conversion between frames, and the reason nanometres
are the shared model space — two levels of one volume, or a crop and its parent, have
different voxel sizes and different origins, so no voxel box transfers between them:
gt.bbox.lo # (7819, 8979, 3479) at 8 nm
level2.voxel_box(gt.bounds_nm).lo # (1954, 2244, 869) at 32 nm
It grows the box outward by default, so the result contains what was asked for rather than dropping a face when the levels do not divide evenly.
Two conventions, both load-bearing¶
Both fail silently when broken, which is why they are stated here rather than left to each caller.
Physical nanometres are the one model space, expressed per axis. A geometry object
never carries an integer level, and a factor is never derived from 2 ** level. Real
pyramids are anisotropic — halving x and y while leaving z alone is ordinary — so a level
index means nothing without the source’s own voxel sizes. Reading those belongs to
whatever layer can open a store: neu_vol.read_scales returns a ScaleInfo per level,
each already carrying its Frame, so nothing downstream rebuilds a transform from a
bare voxel size — which is how a level’s origin got dropped for as long as it did.
Boxes are half-open, [lo, hi). A hi already sitting on a block boundary must not
move; ceiling it anyway grows every correctly-sized box by a whole block. The same rule is
why BBox.from_points adds one to its maximum — a box built from points must contain
them, and getting it wrong drops the far face on every axis while looking fine.
zyx in memory, xyz at the boundary. Every array here is zyx, and to_xyz is the only
conversion. Reversed, geometry mirrors through the z=x diagonal, which reads as plausible
data in the wrong place rather than as an error.
Regions¶
Skeleton.crop(inside) and .exclude(inside) take a callable — given an (N, 3)
array of zyx nm points, which are in the region — and that is the whole interface. Keeping
it a plain function is what lets a caller supply a region this package has never heard of:
a mesh hull, a distance field, a polygon.
box_predicate and mask_predicate cover the two ordinary cases, and union composes
them. union matters more than it looks: excluding a soma and a nucleus is two masks,
and merging them into one array would force them to share a resolution and an origin,
which they need not — a compartment read in a small box around the thing that localises it
has its own frame.
A point outside a region’s own extent is outside the region, not an error. A skeleton normally runs past any one ROI or compartment mask, and that is the question being asked.
Which grid?¶
align_box takes a block shape and does integer arithmetic per axis. It deliberately does
not know which grid you mean, and there are several — the write unit (the chunk, or the
shard where a level is sharded), the pyramid’s cumulative factor, or the per-axis LCM of
a source and destination chunking. Resolving that needs a volume, so it lives in neu-vol;
see neu-vol align-bbox.
Install¶
pip install -e ./neu-lib
Within the suite’s shared conda environment use --no-deps, so pip does not re-resolve the
conda-provided numpy:
pip install --no-deps -e ./neu-lib
Tests¶
python -m pytest -q
No fixtures, no store, no temp directories — every test is arrays and tuples, which is the property worth keeping.
License¶
©2026 The Simons Foundation, Inc.
Licensed under the Apache License, Version 2.0. See CONTRIBUTING.md for how contributions are licensed.