Metadata-Version: 2.4
Name: pyrametric
Version: 0.0.1
Summary: Lazy, memory-bounded connected-components labeling and per-object measurement for OME-Zarr pyramids: label pyramids (from a mask), regionprops-like feature extraction, and label filtering (pyrops plugin; wraps tilewise-ccl)
Author-email: Bugra Oezdemir <bugraa.ozdemir@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/bugraoezdemir/pyrametric
Keywords: ome-zarr,ngff,connected-components,labeling,segmentation,regionprops,image-pyramid,bioimage,microscopy,out-of-core
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Image Processing
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: tilewise-ccl>=0.0.4
Requires-Dist: ome_zarr_pyramid>=0.0.7
Requires-Dist: scipy
Requires-Dist: numpy
Requires-Dist: dask
Requires-Dist: zarr
Provides-Extra: dyna
Requires-Dist: ome_zarr_pyramid[dyna]>=0.0.7; extra == "dyna"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pandas; extra == "dev"
Requires-Dist: dyna-zarr>=0.0.6; extra == "dev"
Dynamic: license-file

# 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`](../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`](../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

```bash
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).

```python
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 + propagate down the pyramid -> 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)
```

### With properties + an OME-Zarr label image

```python
# 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:

```python
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, build its multiscale, name it, then attach it to the image
label_pyr = label_pyramid(mask_pyr, properties=True).downscale().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:

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

---

## API

### `label_pyramid(pyramid, tile_shape=None, connectivity=2, n_workers=1, properties=False, verbose=False)`

Label a binary-mask `Pyramid`'s level 0 and propagate the labels down the
resolution levels (nearest-neighbour / stride downsampling — the correct choice
for categorical labels). 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](#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` → `(303,) * n_spatial`. |
| `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`. |

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](https://ngff.openmicroscopy.org/specifications/0.5/index.html#labels-metadata))
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).** Categorical labels must not be
  averaged; coarser levels are produced by striding, matching the source
  pyramid's per-level shapes and scales.
- **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.

## See also

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