pyrametric 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrametric-0.0.1/LICENSE +21 -0
- pyrametric-0.0.1/PKG-INFO +219 -0
- pyrametric-0.0.1/README.md +182 -0
- pyrametric-0.0.1/pyproject.toml +64 -0
- pyrametric-0.0.1/setup.cfg +4 -0
- pyrametric-0.0.1/src/pyrametric/__init__.py +26 -0
- pyrametric-0.0.1/src/pyrametric/object_features.py +952 -0
- pyrametric-0.0.1/src/pyrametric/pyramid.py +288 -0
- pyrametric-0.0.1/src/pyrametric/write.py +121 -0
- pyrametric-0.0.1/src/pyrametric.egg-info/PKG-INFO +219 -0
- pyrametric-0.0.1/src/pyrametric.egg-info/SOURCES.txt +15 -0
- pyrametric-0.0.1/src/pyrametric.egg-info/dependency_links.txt +1 -0
- pyrametric-0.0.1/src/pyrametric.egg-info/requires.txt +14 -0
- pyrametric-0.0.1/src/pyrametric.egg-info/top_level.txt +1 -0
- pyrametric-0.0.1/tests/test_label_pyramid.py +163 -0
- pyrametric-0.0.1/tests/test_object_features.py +254 -0
- pyrametric-0.0.1/tests/test_write_scenarios.py +137 -0
pyrametric-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bugra Oezdemir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrametric
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
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)
|
|
5
|
+
Author-email: Bugra Oezdemir <bugraa.ozdemir@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bugraoezdemir/pyrametric
|
|
8
|
+
Keywords: ome-zarr,ngff,connected-components,labeling,segmentation,regionprops,image-pyramid,bioimage,microscopy,out-of-core
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: tilewise-ccl>=0.0.4
|
|
25
|
+
Requires-Dist: ome_zarr_pyramid>=0.0.7
|
|
26
|
+
Requires-Dist: scipy
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: dask
|
|
29
|
+
Requires-Dist: zarr
|
|
30
|
+
Provides-Extra: dyna
|
|
31
|
+
Requires-Dist: ome_zarr_pyramid[dyna]>=0.0.7; extra == "dyna"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pandas; extra == "dev"
|
|
35
|
+
Requires-Dist: dyna-zarr>=0.0.6; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# pyrametric
|
|
39
|
+
|
|
40
|
+
Lazy, memory-bounded **connected-components labeling and per-object
|
|
41
|
+
measurement** for OME-Zarr pyramids, with OME-NGFF label-image output.
|
|
42
|
+
|
|
43
|
+
`pyrametric` is the OME-Zarr-aware layer over [`tilewise-ccl`](../tilewise-ccl): it
|
|
44
|
+
labels the level-0 binary mask of a `Pyramid`, propagates the labels down the
|
|
45
|
+
resolution levels, measures per-object features, and writes the result as a
|
|
46
|
+
spec-compliant NGFF **label image** (`labels/<name>/` with `image-label`
|
|
47
|
+
metadata). It works on already-segmented masks — thresholding/segmentation
|
|
48
|
+
lives in [`pyrops`](../pyrops). It provides:
|
|
49
|
+
|
|
50
|
+
- **`label_pyramid`** — label a mask `Pyramid` → a lazy, writable label
|
|
51
|
+
`Pyramid` (optionally with a per-object properties table).
|
|
52
|
+
- **`extract_features`** / **`ObjectFeatures`** — regionprops-like per-object
|
|
53
|
+
measurement (area, bbox, moments, physical units, ...) over a label pyramid,
|
|
54
|
+
computed lazily and memory-bounded (validated against skimage in 2D and 3D).
|
|
55
|
+
- **`write_label_pyramid`** — write a label pyramid as an OME-Zarr label image
|
|
56
|
+
(colors, properties, `source` back-reference; default `labels/<name>/`
|
|
57
|
+
location).
|
|
58
|
+
- **`label_array`** — re-exported from `tilewise-ccl` for array-level labeling.
|
|
59
|
+
|
|
60
|
+
The heavy lifting (fast tile-wise connected components, memory-bounded, exact,
|
|
61
|
+
returning a lazy dask array) lives in `tilewise-ccl`; this package adds the
|
|
62
|
+
pyramid, axis-handling, measurement, and NGFF I/O concerns on top.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pyrametric
|
|
70
|
+
# or, from a checkout:
|
|
71
|
+
pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Depends on `tilewise-ccl`, `ome_zarr_pyramid`, plus `numpy`/`scipy`/`dask`/`zarr`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
The input's level 0 must already be a **binary mask** (background 0 + one
|
|
81
|
+
foreground value) — there is no thresholding here (binarize beforehand).
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from ome_zarr_pyramid.core.io import IO
|
|
85
|
+
from pyrametric import label_pyramid, write_label_pyramid
|
|
86
|
+
|
|
87
|
+
io = IO()
|
|
88
|
+
mask_pyr = io.read_pyramid("mask.zarr") # level 0 = a binary mask
|
|
89
|
+
|
|
90
|
+
# label + propagate down the pyramid -> a lazy, writable label Pyramid
|
|
91
|
+
label_pyr = label_pyramid(mask_pyr, connectivity=2, n_workers=8)
|
|
92
|
+
|
|
93
|
+
# ...write it as a plain multiscale array,
|
|
94
|
+
io.write_pyramid(label_pyr, "labels.zarr", overwrite=True)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### With properties + an OME-Zarr label image
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# also measure per-object properties (area + bounding box) - FREE from the labeling
|
|
101
|
+
# metadata pass; they are stamped onto the returned pyramid's image-label metadata
|
|
102
|
+
label_pyr = label_pyramid(mask_pyr, properties=True, n_workers=8)
|
|
103
|
+
|
|
104
|
+
# write a spec-compliant label image under the source's labels/ group:
|
|
105
|
+
# mask.zarr/labels/cells/ (multiscales + image-label: colors, properties, source)
|
|
106
|
+
write_label_pyramid(label_pyr, source="mask.zarr", name="cells")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Attaching labels to the source image, written together
|
|
110
|
+
|
|
111
|
+
`Pyramid.add_image_label` attaches a label pyramid to its source **image** pyramid,
|
|
112
|
+
so the two travel together and a single `write_pyramid` emits the image plus its
|
|
113
|
+
`labels/<name>/` sub-group in one OME-Zarr store — the common "image + its
|
|
114
|
+
segmentation, side by side" layout that viewers open as one dataset:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
image_pyr = io.read_pyramid("image.zarr") # the intensity image
|
|
118
|
+
mask_pyr = io.read_pyramid("mask.zarr") # its binary mask (segment/threshold upstream)
|
|
119
|
+
|
|
120
|
+
# label the mask, build its multiscale, name it, then attach it to the image
|
|
121
|
+
label_pyr = label_pyramid(mask_pyr, properties=True).downscale().rename("cells")
|
|
122
|
+
combined = image_pyr.add_image_label(label_pyr, name="cells")
|
|
123
|
+
|
|
124
|
+
io.write_pyramid(combined, "image_with_labels.zarr", overwrite=True)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Labeling a sub-region
|
|
128
|
+
|
|
129
|
+
To label only part of the image, select it first with `Pyramid.isel` (any axis,
|
|
130
|
+
int/slice/list) — the label matches that sub-region's geometry:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
label_pyr = label_pyramid(mask_pyr.isel(c=1, z=slice(100, 150)))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## API
|
|
139
|
+
|
|
140
|
+
### `label_pyramid(pyramid, tile_shape=None, connectivity=2, n_workers=1, properties=False, verbose=False)`
|
|
141
|
+
|
|
142
|
+
Label a binary-mask `Pyramid`'s level 0 and propagate the labels down the
|
|
143
|
+
resolution levels (nearest-neighbour / stride downsampling — the correct choice
|
|
144
|
+
for categorical labels). To label a sub-region, select it first with
|
|
145
|
+
`Pyramid.isel` (e.g. `mask.isel(c=1, z=slice(100, 150))`).
|
|
146
|
+
|
|
147
|
+
**Returns** a lazy label `Pyramid` (dtype int32 unless the object count exceeds
|
|
148
|
+
int32), mirroring the source's axes, units, per-level shapes and scales. With
|
|
149
|
+
`properties=True` the same pyramid additionally carries its OME `image-label`
|
|
150
|
+
metadata (per-object colors + an area/bbox properties table), so it is write-ready.
|
|
151
|
+
|
|
152
|
+
| Parameter | Type | Default | Description |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `pyramid` | `Pyramid` | — | Source pyramid; level 0 is a **binary mask** (not validated — see [Notes](#notes)). Axes may be any subset of `tczyx`. |
|
|
155
|
+
| `tile_shape` | sequence of int | `None` | Tile size for `tilewise-ccl`, one entry per **spatial** axis. `None` → `(303,) * n_spatial`. |
|
|
156
|
+
| `connectivity` | int | `2` | Spatial `scipy.ndimage` connectivity (2D: 1=4-, 2=8-conn; 3D: 1=6-, 2=18-, 3=26-conn). |
|
|
157
|
+
| `n_workers` | int | `1` | Threads for each labeling metadata pass. |
|
|
158
|
+
| `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`. |
|
|
159
|
+
| `verbose` | bool | `False` | Forwarded to `tilewise-ccl.label_array`. |
|
|
160
|
+
|
|
161
|
+
When `properties=True`, `label_pyramid` measures each object's **`area`** (voxel
|
|
162
|
+
count) and **`bbox`** (half-open bounding box, per axis, in the label's own
|
|
163
|
+
coordinates) and stamps them onto the returned pyramid's `image-label` metadata
|
|
164
|
+
(alongside deterministic per-object `colors`), so it is write-ready. For richer
|
|
165
|
+
per-object measurement (intensity / physical / shape features, filtering) pass the
|
|
166
|
+
label pyramid to `extract_features(...)`.
|
|
167
|
+
|
|
168
|
+
### `write_label_pyramid(label_pyramid, source=None, name="labels_0", output=None, write_colors=True, write_props=True, overwrite=False, **write_kwargs)`
|
|
169
|
+
|
|
170
|
+
Write a label pyramid as an OME-Zarr **label image**, serialising the `image-label`
|
|
171
|
+
metadata already on the pyramid (built by `label_pyramid(..., properties=True)` or
|
|
172
|
+
`Pyramid.set_image_label`). This writer only persists it — it does not measure.
|
|
173
|
+
Returns the path written.
|
|
174
|
+
|
|
175
|
+
| Parameter | Type | Default | Description |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `label_pyramid` | `Pyramid` | — | The label pyramid, carrying its `image-label` metadata. |
|
|
178
|
+
| `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 = "../../"`. |
|
|
179
|
+
| `name` | str | `"labels_0"` | Label-image name (the `labels/<name>` subgroup). |
|
|
180
|
+
| `output` | str \| Path | `None` | Explicit output path; overrides the default `labels/<name>` location (written standalone). |
|
|
181
|
+
| `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). |
|
|
182
|
+
| `write_props` | bool | `True` | Include the pyramid's `properties` table (`label-value`, `area (pixels)`, `object-coordinates`); `False` drops it at write time. |
|
|
183
|
+
| `overwrite` | bool | `False` | Overwrite an existing label image. |
|
|
184
|
+
| `**write_kwargs` | | | Forwarded to `IO.write_labels` / `write_pyramid` (backend, workers, ...). |
|
|
185
|
+
|
|
186
|
+
The emitted `image-label` metadata (OME-NGFF
|
|
187
|
+
[labels spec](https://ngff.openmicroscopy.org/specifications/0.5/index.html#labels-metadata))
|
|
188
|
+
contains `version`, `colors` (`{label-value, rgba}`), `properties`
|
|
189
|
+
(`{label-value, "area (pixels)", "object-coordinates": {axis: [start, stop]}}`),
|
|
190
|
+
and `source`. Both NGFF 0.4 and 0.5 layouts are written correctly.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Notes
|
|
195
|
+
|
|
196
|
+
- **Binary mask required, not validated.** The level-0 array must be a binary
|
|
197
|
+
mask; this is the caller's responsibility. Validating it would force a full
|
|
198
|
+
scan of a potentially huge array, so it is deliberately skipped. (Re-labeling
|
|
199
|
+
an already-labeled dataset will be possible later, once `tilewise-ccl` grows a
|
|
200
|
+
relabel function.)
|
|
201
|
+
- **Downsampling is nearest-neighbour (stride).** Categorical labels must not be
|
|
202
|
+
averaged; coarser levels are produced by striding, matching the source
|
|
203
|
+
pyramid's per-level shapes and scales.
|
|
204
|
+
- **Batch axes give globally-unique ids.** When multiple `(t, c)` volumes are
|
|
205
|
+
labeled, each is labeled independently and its ids are offset so the whole
|
|
206
|
+
output is a single, globally-unique label image.
|
|
207
|
+
- **Colors/properties at scale.** For very large object counts, writing explicit
|
|
208
|
+
per-object colors and properties bloats the metadata (pyrametric warns) — pass
|
|
209
|
+
`write_colors=False` / `write_props=False` to `write_label_pyramid` to drop them.
|
|
210
|
+
Viewers such as napari auto-randomise label colors when no `colors` list is
|
|
211
|
+
present, so skipping them is usually fine.
|
|
212
|
+
- **Memory.** Labeling and property computation are memory-bounded by tile size
|
|
213
|
+
(see `tilewise-ccl`); the only object-count-proportional cost is the in-memory
|
|
214
|
+
properties table.
|
|
215
|
+
|
|
216
|
+
## See also
|
|
217
|
+
|
|
218
|
+
- [`tilewise-ccl`](../tilewise-ccl) — the storage-agnostic array-level engine
|
|
219
|
+
(`label_array`), with its own README and benchmarks.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# pyrametric
|
|
2
|
+
|
|
3
|
+
Lazy, memory-bounded **connected-components labeling and per-object
|
|
4
|
+
measurement** for OME-Zarr pyramids, with OME-NGFF label-image output.
|
|
5
|
+
|
|
6
|
+
`pyrametric` is the OME-Zarr-aware layer over [`tilewise-ccl`](../tilewise-ccl): it
|
|
7
|
+
labels the level-0 binary mask of a `Pyramid`, propagates the labels down the
|
|
8
|
+
resolution levels, measures per-object features, and writes the result as a
|
|
9
|
+
spec-compliant NGFF **label image** (`labels/<name>/` with `image-label`
|
|
10
|
+
metadata). It works on already-segmented masks — thresholding/segmentation
|
|
11
|
+
lives in [`pyrops`](../pyrops). It provides:
|
|
12
|
+
|
|
13
|
+
- **`label_pyramid`** — label a mask `Pyramid` → a lazy, writable label
|
|
14
|
+
`Pyramid` (optionally with a per-object properties table).
|
|
15
|
+
- **`extract_features`** / **`ObjectFeatures`** — regionprops-like per-object
|
|
16
|
+
measurement (area, bbox, moments, physical units, ...) over a label pyramid,
|
|
17
|
+
computed lazily and memory-bounded (validated against skimage in 2D and 3D).
|
|
18
|
+
- **`write_label_pyramid`** — write a label pyramid as an OME-Zarr label image
|
|
19
|
+
(colors, properties, `source` back-reference; default `labels/<name>/`
|
|
20
|
+
location).
|
|
21
|
+
- **`label_array`** — re-exported from `tilewise-ccl` for array-level labeling.
|
|
22
|
+
|
|
23
|
+
The heavy lifting (fast tile-wise connected components, memory-bounded, exact,
|
|
24
|
+
returning a lazy dask array) lives in `tilewise-ccl`; this package adds the
|
|
25
|
+
pyramid, axis-handling, measurement, and NGFF I/O concerns on top.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pyrametric
|
|
33
|
+
# or, from a checkout:
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Depends on `tilewise-ccl`, `ome_zarr_pyramid`, plus `numpy`/`scipy`/`dask`/`zarr`.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
The input's level 0 must already be a **binary mask** (background 0 + one
|
|
44
|
+
foreground value) — there is no thresholding here (binarize beforehand).
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from ome_zarr_pyramid.core.io import IO
|
|
48
|
+
from pyrametric import label_pyramid, write_label_pyramid
|
|
49
|
+
|
|
50
|
+
io = IO()
|
|
51
|
+
mask_pyr = io.read_pyramid("mask.zarr") # level 0 = a binary mask
|
|
52
|
+
|
|
53
|
+
# label + propagate down the pyramid -> a lazy, writable label Pyramid
|
|
54
|
+
label_pyr = label_pyramid(mask_pyr, connectivity=2, n_workers=8)
|
|
55
|
+
|
|
56
|
+
# ...write it as a plain multiscale array,
|
|
57
|
+
io.write_pyramid(label_pyr, "labels.zarr", overwrite=True)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### With properties + an OME-Zarr label image
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# also measure per-object properties (area + bounding box) - FREE from the labeling
|
|
64
|
+
# metadata pass; they are stamped onto the returned pyramid's image-label metadata
|
|
65
|
+
label_pyr = label_pyramid(mask_pyr, properties=True, n_workers=8)
|
|
66
|
+
|
|
67
|
+
# write a spec-compliant label image under the source's labels/ group:
|
|
68
|
+
# mask.zarr/labels/cells/ (multiscales + image-label: colors, properties, source)
|
|
69
|
+
write_label_pyramid(label_pyr, source="mask.zarr", name="cells")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Attaching labels to the source image, written together
|
|
73
|
+
|
|
74
|
+
`Pyramid.add_image_label` attaches a label pyramid to its source **image** pyramid,
|
|
75
|
+
so the two travel together and a single `write_pyramid` emits the image plus its
|
|
76
|
+
`labels/<name>/` sub-group in one OME-Zarr store — the common "image + its
|
|
77
|
+
segmentation, side by side" layout that viewers open as one dataset:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
image_pyr = io.read_pyramid("image.zarr") # the intensity image
|
|
81
|
+
mask_pyr = io.read_pyramid("mask.zarr") # its binary mask (segment/threshold upstream)
|
|
82
|
+
|
|
83
|
+
# label the mask, build its multiscale, name it, then attach it to the image
|
|
84
|
+
label_pyr = label_pyramid(mask_pyr, properties=True).downscale().rename("cells")
|
|
85
|
+
combined = image_pyr.add_image_label(label_pyr, name="cells")
|
|
86
|
+
|
|
87
|
+
io.write_pyramid(combined, "image_with_labels.zarr", overwrite=True)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Labeling a sub-region
|
|
91
|
+
|
|
92
|
+
To label only part of the image, select it first with `Pyramid.isel` (any axis,
|
|
93
|
+
int/slice/list) — the label matches that sub-region's geometry:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
label_pyr = label_pyramid(mask_pyr.isel(c=1, z=slice(100, 150)))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## API
|
|
102
|
+
|
|
103
|
+
### `label_pyramid(pyramid, tile_shape=None, connectivity=2, n_workers=1, properties=False, verbose=False)`
|
|
104
|
+
|
|
105
|
+
Label a binary-mask `Pyramid`'s level 0 and propagate the labels down the
|
|
106
|
+
resolution levels (nearest-neighbour / stride downsampling — the correct choice
|
|
107
|
+
for categorical labels). To label a sub-region, select it first with
|
|
108
|
+
`Pyramid.isel` (e.g. `mask.isel(c=1, z=slice(100, 150))`).
|
|
109
|
+
|
|
110
|
+
**Returns** a lazy label `Pyramid` (dtype int32 unless the object count exceeds
|
|
111
|
+
int32), mirroring the source's axes, units, per-level shapes and scales. With
|
|
112
|
+
`properties=True` the same pyramid additionally carries its OME `image-label`
|
|
113
|
+
metadata (per-object colors + an area/bbox properties table), so it is write-ready.
|
|
114
|
+
|
|
115
|
+
| Parameter | Type | Default | Description |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `pyramid` | `Pyramid` | — | Source pyramid; level 0 is a **binary mask** (not validated — see [Notes](#notes)). Axes may be any subset of `tczyx`. |
|
|
118
|
+
| `tile_shape` | sequence of int | `None` | Tile size for `tilewise-ccl`, one entry per **spatial** axis. `None` → `(303,) * n_spatial`. |
|
|
119
|
+
| `connectivity` | int | `2` | Spatial `scipy.ndimage` connectivity (2D: 1=4-, 2=8-conn; 3D: 1=6-, 2=18-, 3=26-conn). |
|
|
120
|
+
| `n_workers` | int | `1` | Threads for each labeling metadata pass. |
|
|
121
|
+
| `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`. |
|
|
122
|
+
| `verbose` | bool | `False` | Forwarded to `tilewise-ccl.label_array`. |
|
|
123
|
+
|
|
124
|
+
When `properties=True`, `label_pyramid` measures each object's **`area`** (voxel
|
|
125
|
+
count) and **`bbox`** (half-open bounding box, per axis, in the label's own
|
|
126
|
+
coordinates) and stamps them onto the returned pyramid's `image-label` metadata
|
|
127
|
+
(alongside deterministic per-object `colors`), so it is write-ready. For richer
|
|
128
|
+
per-object measurement (intensity / physical / shape features, filtering) pass the
|
|
129
|
+
label pyramid to `extract_features(...)`.
|
|
130
|
+
|
|
131
|
+
### `write_label_pyramid(label_pyramid, source=None, name="labels_0", output=None, write_colors=True, write_props=True, overwrite=False, **write_kwargs)`
|
|
132
|
+
|
|
133
|
+
Write a label pyramid as an OME-Zarr **label image**, serialising the `image-label`
|
|
134
|
+
metadata already on the pyramid (built by `label_pyramid(..., properties=True)` or
|
|
135
|
+
`Pyramid.set_image_label`). This writer only persists it — it does not measure.
|
|
136
|
+
Returns the path written.
|
|
137
|
+
|
|
138
|
+
| Parameter | Type | Default | Description |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| `label_pyramid` | `Pyramid` | — | The label pyramid, carrying its `image-label` metadata. |
|
|
141
|
+
| `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 = "../../"`. |
|
|
142
|
+
| `name` | str | `"labels_0"` | Label-image name (the `labels/<name>` subgroup). |
|
|
143
|
+
| `output` | str \| Path | `None` | Explicit output path; overrides the default `labels/<name>` location (written standalone). |
|
|
144
|
+
| `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). |
|
|
145
|
+
| `write_props` | bool | `True` | Include the pyramid's `properties` table (`label-value`, `area (pixels)`, `object-coordinates`); `False` drops it at write time. |
|
|
146
|
+
| `overwrite` | bool | `False` | Overwrite an existing label image. |
|
|
147
|
+
| `**write_kwargs` | | | Forwarded to `IO.write_labels` / `write_pyramid` (backend, workers, ...). |
|
|
148
|
+
|
|
149
|
+
The emitted `image-label` metadata (OME-NGFF
|
|
150
|
+
[labels spec](https://ngff.openmicroscopy.org/specifications/0.5/index.html#labels-metadata))
|
|
151
|
+
contains `version`, `colors` (`{label-value, rgba}`), `properties`
|
|
152
|
+
(`{label-value, "area (pixels)", "object-coordinates": {axis: [start, stop]}}`),
|
|
153
|
+
and `source`. Both NGFF 0.4 and 0.5 layouts are written correctly.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Notes
|
|
158
|
+
|
|
159
|
+
- **Binary mask required, not validated.** The level-0 array must be a binary
|
|
160
|
+
mask; this is the caller's responsibility. Validating it would force a full
|
|
161
|
+
scan of a potentially huge array, so it is deliberately skipped. (Re-labeling
|
|
162
|
+
an already-labeled dataset will be possible later, once `tilewise-ccl` grows a
|
|
163
|
+
relabel function.)
|
|
164
|
+
- **Downsampling is nearest-neighbour (stride).** Categorical labels must not be
|
|
165
|
+
averaged; coarser levels are produced by striding, matching the source
|
|
166
|
+
pyramid's per-level shapes and scales.
|
|
167
|
+
- **Batch axes give globally-unique ids.** When multiple `(t, c)` volumes are
|
|
168
|
+
labeled, each is labeled independently and its ids are offset so the whole
|
|
169
|
+
output is a single, globally-unique label image.
|
|
170
|
+
- **Colors/properties at scale.** For very large object counts, writing explicit
|
|
171
|
+
per-object colors and properties bloats the metadata (pyrametric warns) — pass
|
|
172
|
+
`write_colors=False` / `write_props=False` to `write_label_pyramid` to drop them.
|
|
173
|
+
Viewers such as napari auto-randomise label colors when no `colors` list is
|
|
174
|
+
present, so skipping them is usually fine.
|
|
175
|
+
- **Memory.** Labeling and property computation are memory-bounded by tile size
|
|
176
|
+
(see `tilewise-ccl`); the only object-count-proportional cost is the in-memory
|
|
177
|
+
properties table.
|
|
178
|
+
|
|
179
|
+
## See also
|
|
180
|
+
|
|
181
|
+
- [`tilewise-ccl`](../tilewise-ccl) — the storage-agnostic array-level engine
|
|
182
|
+
(`label_array`), with its own README and benchmarks.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyrametric"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "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)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
# 3.11+ to match ome_zarr_pyramid and tilewise-ccl.
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Bugra Oezdemir", email = "bugraa.ozdemir@gmail.com"},
|
|
15
|
+
]
|
|
16
|
+
# Kept in sync with the GitHub repository topics.
|
|
17
|
+
keywords = [
|
|
18
|
+
"ome-zarr", "ngff", "connected-components", "labeling", "segmentation",
|
|
19
|
+
"regionprops", "image-pyramid", "bioimage", "microscopy", "out-of-core",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Science/Research",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Topic :: Scientific/Engineering",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
33
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
34
|
+
]
|
|
35
|
+
dependencies = [
|
|
36
|
+
# The labeling engine. >=0.0.4: 0.0.3 carried the whole-tile guard but pinned
|
|
37
|
+
# dyna-zarr>=0.0.5, which does NOT align an integer-indexed axis - so `labels[z]`
|
|
38
|
+
# raised "needs whole tiles" on a fresh install. 0.0.4 raises that pin to >=0.0.6.
|
|
39
|
+
"tilewise-ccl>=0.0.4",
|
|
40
|
+
"ome_zarr_pyramid>=0.0.7",
|
|
41
|
+
"scipy",
|
|
42
|
+
"numpy",
|
|
43
|
+
"dask",
|
|
44
|
+
"zarr",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
# pyrametric never imports dyna_zarr itself - it reaches the memory-bounded backend
|
|
49
|
+
# through ome_zarr_pyramid, so this just forwards that package's own extra.
|
|
50
|
+
dyna = ["ome_zarr_pyramid[dyna]>=0.0.7"]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=7.0",
|
|
53
|
+
# `ObjectFeatures.to_frame()` returns a DataFrame; the dyna extra lets the
|
|
54
|
+
# backend="dyna" tests run instead of falling back to dask.
|
|
55
|
+
"pandas",
|
|
56
|
+
"dyna-zarr>=0.0.6",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://github.com/bugraoezdemir/pyrametric"
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.packages.find]
|
|
63
|
+
where = ["src"]
|
|
64
|
+
exclude = ["tests*"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""pyrametric: connected-components labeling for OME-Zarr pyramids.
|
|
2
|
+
|
|
3
|
+
The array-level labeling algorithm lives in the standalone, storage-agnostic
|
|
4
|
+
`tilewise-ccl` package; `label_array` is re-exported here for convenience.
|
|
5
|
+
|
|
6
|
+
`label_pyramid` is the OME-Zarr-aware layer: it labels a `Pyramid`'s level-0
|
|
7
|
+
binary mask and propagates the labels down the multiscale levels, returning a
|
|
8
|
+
lazy label `Pyramid` aligned to the source.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.0.1"
|
|
12
|
+
__author__ = "Bugra Oezdemir"
|
|
13
|
+
|
|
14
|
+
from tilewise_ccl import label_array
|
|
15
|
+
|
|
16
|
+
from pyrametric.object_features import ObjectFeatures, extract_features
|
|
17
|
+
from pyrametric.pyramid import label_pyramid
|
|
18
|
+
from pyrametric.write import write_label_pyramid
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"label_array",
|
|
22
|
+
"label_pyramid",
|
|
23
|
+
"write_label_pyramid",
|
|
24
|
+
"extract_features",
|
|
25
|
+
"ObjectFeatures", # filtering is now ObjectFeatures.filter_by_size / .filter_by_feature
|
|
26
|
+
]
|