Skip to main content

pyrametric

Lazy, memory-bounded connected-components labeling and per-object measurement for OME-Zarr pyramids, with OME-NGFF label-image output.

pyrametric is the OME-Zarr-aware layer over tilewise-ccl: it labels the level-0 binary mask of a Pyramid, propagates the labels down the resolution levels, measures per-object features, and writes the result as a spec-compliant NGFF label image (labels/<name>/ with image-label metadata). It works on already-segmented masks — thresholding/segmentation lives in pyrops. It provides:

  • label_pyramid — label a mask Pyramid → a lazy, writable label Pyramid (optionally with a per-object properties table).
  • extract_features / ObjectFeatures — regionprops-like per-object measurement (area, bbox, moments, physical units, ...) over a label pyramid, computed lazily and memory-bounded (validated against skimage in 2D and 3D).
  • write_label_pyramid — write a label pyramid as an OME-Zarr label image (colors, properties, source back-reference; default labels/<name>/ location).
  • label_array — re-exported from tilewise-ccl for array-level labeling.

The heavy lifting (fast tile-wise connected components, memory-bounded, exact, returning a lazy dask array) lives in tilewise-ccl; this package adds the pyramid, axis-handling, measurement, and NGFF I/O concerns on top.


Installation

pip install pyrametric
# or, from a checkout:
pip install -e .

Depends on tilewise-ccl, ome_zarr_pyramid, plus numpy/scipy/dask/zarr.


Quick start

The input's level 0 must already be a binary mask (background 0 + one foreground value) — there is no thresholding here (binarize beforehand).

from ome_zarr_pyramid.core.io import IO
from pyrametric import label_pyramid, write_label_pyramid

io = IO()
mask_pyr = io.read_pyramid("mask.zarr")           # level 0 = a binary mask

# label level 0 -> a lazy, writable label Pyramid
label_pyr = label_pyramid(mask_pyr, connectivity=2, n_workers=8)

# ...write it as a plain multiscale array
io.write_pyramid(label_pyr, "labels.zarr", overwrite=True)

The result mirrors mask.zarr's resolution levels — you do not ask for them, and nothing is downsampled until the write. See Downscaling to change the depth.

With properties + an OME-Zarr label image

# also measure per-object properties (area + bounding box) - FREE from the labeling
# metadata pass; they are stamped onto the returned pyramid's image-label metadata
label_pyr = label_pyramid(mask_pyr, properties=True, n_workers=8)

# write a spec-compliant label image under the source's labels/ group:
#   mask.zarr/labels/cells/   (multiscales + image-label: colors, properties, source)
write_label_pyramid(label_pyr, source="mask.zarr", name="cells")

Attaching labels to the source image, written together

Pyramid.add_image_label attaches a label pyramid to its source image pyramid, so the two travel together and a single write_pyramid emits the image plus its labels/<name>/ sub-group in one OME-Zarr store — the common "image + its segmentation, side by side" layout that viewers open as one dataset:

image_pyr = io.read_pyramid("image.zarr")     # the intensity image
mask_pyr  = io.read_pyramid("mask.zarr")       # its binary mask (segment/threshold upstream)

# label the mask, name it, then attach it to the image. The label pyramid mirrors
# the source's levels automatically - add `.downscale(n_layers=3)` to override.
label_pyr = label_pyramid(mask_pyr, properties=True).rename("cells")
combined  = image_pyr.add_image_label(label_pyr, name="cells")

io.write_pyramid(combined, "image_with_labels.zarr", overwrite=True)

Labeling a sub-region

To label only part of the image, select it first with Pyramid.isel (any axis, int/slice/list) — the label matches that sub-region's geometry:

label_pyr = label_pyramid(mask_pyr.isel(c=1, z=slice(100, 150)))

Downscaling

label_pyramid takes no downscaling parameters. A label pyramid is downsampled the same way a raw one is — with Pyramid.downscale() — so the two behave alike:

labels = label_pyramid(mask_pyr)                      # mirrors mask.zarr's levels
labels = label_pyramid(mask_pyr).downscale(n_layers=5)          # exactly 5 levels
labels = label_pyramid(mask_pyr).downscale(min_dimension_size=128)  # coarser stop

min_dimension_size stops once the largest dimension would fall below it, so a volume already smaller than the threshold stays single-level.

Three things are handled for you:

Levels mirror the source. With no .downscale() call, the label pyramid gets the same number of levels as the mask it came from, at the same shapes, so the two line up voxel-for-voxel in a viewer.

Per-axis factors are derived from the source's own levels, not assumed. If the mask was built plane-wise (z=1, y=2, x=2) or with an irregular progression (2x to level 1, 5x to level 2), the labels follow it exactly. Only when there is nothing to derive from — a single-level source — does the default apply: isotropic 2x on the spatial axes (z=2, y=2, x=2; t/c stay 1), the same default a raw pyramid uses. When the source's levels cannot be reproduced by any integer factor, pyrametric warns and builds a fresh plan rather than silently shifting a level by a voxel.

Downsampling is nearest-neighbour (downscale_method="simple", the default for both label_pyramid and .downscale()), so a coarse level only ever contains ids that are really present at level 0. Do not override it on a label pyramid: averaging label ids invents objects that do not exist — the mean of ids 4 and 8 is 6, a different object — so downscale_method="mean"/"median" will corrupt the labels. It is accepted, not blocked; the correctness is on you if you change it.

Nothing is computed until the write: .downscale() records a plan, and the writer expands it progressively from disk. Level 0 is labelled exactly once, however many levels you ask for — so labels.nlayers reads 1 until it is written.


API

label_pyramid(pyramid, tile_shape=None, connectivity=2, n_workers=1, properties=False, verbose=False, backend="auto")

Label a binary-mask Pyramid's level 0. The result mirrors the source's resolution levels — it takes no downscaling parameters. To choose a different depth, call .downscale() on the result, exactly as you would for a raw pyramid (see Downscaling).

The coarser levels are recorded as a deferred plan, not built here: the writer expands them progressively from disk, so level 0 is labelled exactly once. Building them eagerly cost one additional full re-labelling of level 0 per level (measured 1x / 2x / 3x / 4x for 1..4 levels). Downsampling is nearest-neighbour (stride), the only correct choice for categorical labels, and the per-axis factors are derived from the source pyramid's own levels so the labels stay aligned with their image.

Because the levels are a plan, label_pyr.nlayers reads 1 until the pyramid is written; the store then contains the planned levels.

To label a sub-region, select it first with Pyramid.isel (e.g. mask.isel(c=1, z=slice(100, 150))).

Returns a lazy label Pyramid (dtype int32 unless the object count exceeds int32), mirroring the source's axes, units, per-level shapes and scales. With properties=True the same pyramid additionally carries its OME image-label metadata (per-object colors + an area/bbox properties table), so it is write-ready.

Parameter Type Default Description
pyramid Pyramid — Source pyramid; level 0 is a binary mask (not validated — see Notes). Axes may be any subset of tczyx.
tile_shape sequence of int None Tile size for tilewise-ccl, one entry per spatial axis. None derives one from the array: a ~256 MiB working set snapped to the storage chunks.
connectivity int 2 Spatial scipy.ndimage connectivity (2D: 1=4-, 2=8-conn; 3D: 1=6-, 2=18-, 3=26-conn).
n_workers int 1 Threads for each labeling metadata pass.
properties bool False Also measure per-object area + bbox (free from the labeling pass) and stamp them onto the returned pyramid's image-label metadata (colors + properties). The return type is unchanged — always a Pyramid.
verbose bool False Forwarded to tilewise-ccl.label_array.
backend "auto" | "dask" | "dyna" "auto" Array backend. "auto" uses the memory-bounded, dask-free dyna path when the pyramid's layers are zarr-backed, and falls back to dask otherwise. Labels are identical either way.

When properties=True, label_pyramid measures each object's area (voxel count) and bbox (half-open bounding box, per axis, in the label's own coordinates) and stamps them onto the returned pyramid's image-label metadata (alongside deterministic per-object colors), so it is write-ready. For richer per-object measurement (intensity / physical / shape features, filtering) pass the label pyramid to extract_features(...).

write_label_pyramid(label_pyramid, source=None, name="labels_0", output=None, write_colors=True, write_props=True, overwrite=False, **write_kwargs)

Write a label pyramid as an OME-Zarr label image, serialising the image-label metadata already on the pyramid (built by label_pyramid(..., properties=True) or Pyramid.set_image_label). This writer only persists it — it does not measure. Returns the path written.

Parameter Type Default Description
label_pyramid Pyramid — The label pyramid, carrying its image-label metadata.
source str | Path | Pyramid None The source image the labels belong to. Required when output is None: labels go to <source>/labels/<name>/, registered in the source's labels group, with image-label.source.image = "../../".
name str "labels_0" Label-image name (the labels/<name> subgroup).
output str | Path None Explicit output path; overrides the default labels/<name> location (written standalone).
write_colors bool True Include the pyramid's colors in the written metadata; False drops that key at write time (a lighter write for a very large object count).
write_props bool True Include the pyramid's properties table (label-value, area (pixels), object-coordinates); False drops it at write time.
overwrite bool False Overwrite an existing label image.
**write_kwargs Forwarded to IO.write_labels / write_pyramid (backend, workers, ...).

The emitted image-label metadata (OME-NGFF labels spec) contains version, colors ({label-value, rgba}), properties ({label-value, "area (pixels)", "object-coordinates": {axis: [start, stop]}}), and source. Both NGFF 0.4 and 0.5 layouts are written correctly.


Notes

  • Binary mask required, not validated. The level-0 array must be a binary mask; this is the caller's responsibility. Validating it would force a full scan of a potentially huge array, so it is deliberately skipped. (Re-labeling an already-labeled dataset will be possible later, once tilewise-ccl grows a relabel function.)
  • Downsampling is nearest-neighbour (stride), and deferred. Categorical labels must not be averaged, so coarser levels are produced by striding, with the per-axis factor taken from the source pyramid. They are RECORDED as a plan and expanded by the writer from disk, so level 0 is labelled once no matter how many levels you ask for.
  • Batch axes give globally-unique ids. When multiple (t, c) volumes are labeled, each is labeled independently and its ids are offset so the whole output is a single, globally-unique label image.
  • Colors/properties at scale. For very large object counts, writing explicit per-object colors and properties bloats the metadata (pyrametric warns) — pass write_colors=False / write_props=False to write_label_pyramid to drop them. Viewers such as napari auto-randomise label colors when no colors list is present, so skipping them is usually fine.
  • Memory. Labeling and property computation are memory-bounded by tile size (see tilewise-ccl); the only object-count-proportional cost is the in-memory properties table.
  • Backends. label_pyramid and extract_features both take backend="auto"|"dask"|"dyna". "auto" prefers dyna when the pyramid's layers are zarr-backed and falls back to dask silently; a dask-backed pyramid is dask-only, and asking for "dyna" there raises rather than guessing. The results are identical - it is purely an execution choice.

See also

  • tilewise-ccl — the storage-agnostic array-level engine (label_array), with its own README and benchmarks.

Metadata

Release files for pyrametric 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyrametric 0.0.2
File Size Uploaded
pyrametric-0.0.2.tar.gz 40.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyrametric 0.0.2
File Interpreter ABI Platform
pyrametric-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 72.0 kB

Release files / pyrametric-0.0.2.tar.gz

Download URL pyrametric-0.0.2.tar.gz
Size 40.7 kB
Tags Source
SHA-256 checksum
How to use checksums
4a113be1d60eac91194055b086a1f72a1e3d01a841f2b62452b32ca77de78814
BLAKE2b-256 checksum
How to use checksums
6a9c70884c01a252b24ff0705a3981b850d5a111fed92920d80c14283971a6d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Release files / pyrametric-0.0.2-py3-none-any.whl

Download URL pyrametric-0.0.2-py3-none-any.whl
Size 31.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
28ba65190a699f0e7e76ffe3ec18a28f9ddb761ad88ec4b30920173f241d4b7d
BLAKE2b-256 checksum
How to use checksums
d9c8636db33c21368f058729382d5e49198009c607b4877a4e5b2c92b5ccda6e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.2 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page