patchworks 0.11.21__tar.gz → 1.1.0__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.
- {patchworks-0.11.21 → patchworks-1.1.0}/PKG-INFO +2 -2
- {patchworks-0.11.21 → patchworks-1.1.0}/pyproject.toml +1 -1
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_chunks.py +1 -1
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_merge.py +17 -4
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_relabel.py +34 -4
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/plugins/napari.py +38 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/plugins/ome_zarr.py +121 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/tests/test_core.py +56 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/tests/test_napari.py +36 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/tests/test_ome_zarr.py +28 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/scripts/merge.py +3 -1
- {patchworks-0.11.21 → patchworks-1.1.0}/.github/workflows/docs.yml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/.github/workflows/lint.yml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/.github/workflows/release.yml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/.gitignore +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/LICENSE +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/README.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/cliff.toml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/chunks.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/cluster.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/io.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/relabel.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/api/tile_process.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/assets/logo.png +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/custom.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/custom_method.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/stardist.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/getting_started.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/merging.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/performance.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/snakemake.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/guide/tiling.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/docs/index.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/mkdocs.yml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/__init__.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_core.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_distributed.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_io.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/_relations.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/tests/test_distributed.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/tests/test_relations.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/README.md +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/Snakefile +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/config/config.yaml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/config/multi.yaml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/pixi.toml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/rules/common.smk +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/rules/convert.smk +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/rules/merge.smk +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/rules/segment.smk +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/scripts/_pw.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/scripts/convert.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/scripts/run_multi.py +0 -0
- {patchworks-0.11.21 → patchworks-1.1.0}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Tiled processing of arbitrarily large images with globally consistent labels
|
|
5
5
|
Project-URL: Homepage, https://github.com/imcf/patchworks
|
|
6
6
|
Project-URL: Issues, https://github.com/imcf/patchworks/issues
|
|
@@ -8,7 +8,7 @@ Author-email: Laurent Guerard <laurent.guerard@unibas.ch>
|
|
|
8
8
|
License: GPL-3.0-only
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Keywords: bioimage,chunked,dask,image processing,segmentation,tiling,zarr
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Intended Audience :: Science/Research
|
|
13
13
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -19,7 +19,7 @@ keywords = [
|
|
|
19
19
|
"bioimage",
|
|
20
20
|
]
|
|
21
21
|
classifiers = [
|
|
22
|
-
"Development Status ::
|
|
22
|
+
"Development Status :: 5 - Production/Stable",
|
|
23
23
|
"Intended Audience :: Science/Research",
|
|
24
24
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
25
25
|
"Programming Language :: Python :: 3",
|
|
@@ -459,7 +459,8 @@ def merge_tile_labels(
|
|
|
459
459
|
stage_dir: Union[str, Path, None] = None,
|
|
460
460
|
keep_stage: bool = False,
|
|
461
461
|
progress: bool = False,
|
|
462
|
-
|
|
462
|
+
return_count: bool = False,
|
|
463
|
+
) -> Union["da.Array", tuple["da.Array", Union[int, None]]]:
|
|
463
464
|
"""Merge per-tile labels into a globally consistent label array.
|
|
464
465
|
|
|
465
466
|
Standalone merge step — use this when you already have per-tile labels
|
|
@@ -503,11 +504,21 @@ def merge_tile_labels(
|
|
|
503
504
|
Keep the temp stage zarr after merging. Default False.
|
|
504
505
|
progress:
|
|
505
506
|
Show a progress bar during the relabel step.
|
|
507
|
+
return_count:
|
|
508
|
+
Also return the exact object count. Only meaningful (non-``None``)
|
|
509
|
+
when ``sequential_labels=True``, which already computes it for free
|
|
510
|
+
while renumbering to ``1..N`` — otherwise no step here knows the
|
|
511
|
+
final count without an extra full scan, so the second element is
|
|
512
|
+
``None``. Useful to persist alongside the labels (e.g.
|
|
513
|
+
``write_labels(..., n_objects=...)``) so a downstream consumer with
|
|
514
|
+
the count can skip re-deriving the id set from the array itself.
|
|
506
515
|
|
|
507
516
|
Returns
|
|
508
517
|
-------
|
|
509
518
|
da.Array
|
|
510
|
-
Merged label array (int32) backed by ``write_to``.
|
|
519
|
+
Merged label array (int32) backed by ``write_to``. Or, when
|
|
520
|
+
``return_count=True``, a ``(labels, n_objects)`` tuple —
|
|
521
|
+
``n_objects`` is ``None`` unless ``sequential_labels=True``.
|
|
511
522
|
|
|
512
523
|
Examples
|
|
513
524
|
--------
|
|
@@ -600,9 +611,10 @@ def merge_tile_labels(
|
|
|
600
611
|
show_progress=progress,
|
|
601
612
|
)
|
|
602
613
|
|
|
614
|
+
n_objects = None
|
|
603
615
|
if sequential_labels:
|
|
604
616
|
logger.info("Relabelling to contiguous ids…")
|
|
605
|
-
relabel_sequential_zarr(effective_out, output_component)
|
|
617
|
+
n_objects = relabel_sequential_zarr(effective_out, output_component)
|
|
606
618
|
|
|
607
619
|
# -- Cleanup temp stage (only when we created it) --
|
|
608
620
|
if not isinstance(labeled, (str, Path)) and not keep_stage:
|
|
@@ -611,4 +623,5 @@ def merge_tile_labels(
|
|
|
611
623
|
shutil.rmtree(stage_path, ignore_errors=True)
|
|
612
624
|
logger.info("Removed stage store %s", stage_path)
|
|
613
625
|
|
|
614
|
-
|
|
626
|
+
result = da.from_zarr(effective_out, component=output_component)
|
|
627
|
+
return (result, n_objects) if return_count else result
|
|
@@ -33,7 +33,7 @@ def relabel_sequential_array(labels: np.ndarray) -> np.ndarray:
|
|
|
33
33
|
Examples
|
|
34
34
|
--------
|
|
35
35
|
>>> relabel_sequential_array(np.array([0, 500000, 500000, 7]))
|
|
36
|
-
array([0, 2, 2, 1])
|
|
36
|
+
array([0, 2, 2, 1], dtype=uint16)
|
|
37
37
|
"""
|
|
38
38
|
uniq = np.unique(labels)
|
|
39
39
|
max_label = int(uniq[-1])
|
|
@@ -53,11 +53,41 @@ def relabel_sequential_array(labels: np.ndarray) -> np.ndarray:
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def relabel_sequential_zarr(store_path: str, component: str = "labels") -> int:
|
|
56
|
-
"""Relabel a written label zarr to contiguous ids, in place.
|
|
56
|
+
"""Relabel a written label zarr to contiguous ids, in place.
|
|
57
57
|
|
|
58
58
|
Two-pass streaming algorithm — safe for arrays far larger than RAM.
|
|
59
|
-
Pass 1 collects unique ids (bounded memory: a set
|
|
60
|
-
lookup-table remap chunk by
|
|
59
|
+
Pass 1 collects unique ids (bounded memory: a Python ``set``, not the
|
|
60
|
+
voxels themselves). Pass 2 applies the lookup-table remap chunk by
|
|
61
|
+
chunk, writing back into the same store.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
store_path : str
|
|
66
|
+
Path to the zarr store containing the label array.
|
|
67
|
+
component : str, optional
|
|
68
|
+
Array name inside the store to relabel in place (default
|
|
69
|
+
``"labels"``).
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
int
|
|
74
|
+
Number of distinct objects (``N``); the array now holds ``1..N``
|
|
75
|
+
(background ``0`` unchanged).
|
|
76
|
+
|
|
77
|
+
Examples
|
|
78
|
+
--------
|
|
79
|
+
>>> import zarr
|
|
80
|
+
>>> root = zarr.open_group("staged.zarr", mode="w") # doctest: +SKIP
|
|
81
|
+
>>> root.create_array(
|
|
82
|
+
... "labels", shape=(4, 4), chunks=(4, 4), dtype="int32"
|
|
83
|
+
... )[:] = [
|
|
84
|
+
... [0, 500000, 500000, 0],
|
|
85
|
+
... [0, 0, 0, 7],
|
|
86
|
+
... [0, 0, 0, 0],
|
|
87
|
+
... [0, 0, 0, 0],
|
|
88
|
+
... ] # doctest: +SKIP
|
|
89
|
+
>>> relabel_sequential_zarr("staged.zarr") # doctest: +SKIP
|
|
90
|
+
2
|
|
61
91
|
"""
|
|
62
92
|
root = zarr.open_group(store_path, mode="r+")
|
|
63
93
|
z = root[component]
|
|
@@ -179,6 +179,41 @@ def _pyramid_calibration(
|
|
|
179
179
|
return scale, units
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
def _label_hint(path: Union[str, Path]) -> dict[str, Any]:
|
|
183
|
+
"""Read the known-object-count hint from a label group's zarr attrs.
|
|
184
|
+
|
|
185
|
+
``write_labels(..., n_objects=...)`` persists this when the labels were
|
|
186
|
+
renumbered to a contiguous ``1..N`` range (``sequential_labels=True``
|
|
187
|
+
during the merge) — the exact id set is then ``range(1, n_objects +
|
|
188
|
+
1)`` by construction, with no scan needed. Passed through as a Labels
|
|
189
|
+
layer's ``metadata`` so a downstream consumer (e.g.
|
|
190
|
+
napari-dask-ndmeasure) can use it instead of re-deriving the id set
|
|
191
|
+
from the array itself.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
path : str or Path
|
|
196
|
+
Label group path (e.g. ``f"{image}/labels/{name}"``).
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
dict
|
|
201
|
+
``{"n_objects": int, "sequential_labels": True}`` if the group has
|
|
202
|
+
the hint, else ``{}`` — safe to splat straight into
|
|
203
|
+
``metadata=``/merge into a bigger dict either way.
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
attrs = zarr.open_group(str(path), mode="r").attrs
|
|
207
|
+
except Exception:
|
|
208
|
+
return {}
|
|
209
|
+
if "n_objects" not in attrs:
|
|
210
|
+
return {}
|
|
211
|
+
return {
|
|
212
|
+
"n_objects": attrs["n_objects"],
|
|
213
|
+
"sequential_labels": attrs.get("sequential_labels", False),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
182
217
|
def _inner_label_names(store: Union[str, Path]) -> list[str]:
|
|
183
218
|
"""List label images registered under an OME-ZARR's ``labels/`` group.
|
|
184
219
|
|
|
@@ -351,12 +386,14 @@ def view_in_napari(
|
|
|
351
386
|
if _is_zarr(labels)
|
|
352
387
|
else (None, None)
|
|
353
388
|
)
|
|
389
|
+
metadata = _label_hint(labels) if _is_zarr(labels) else {}
|
|
354
390
|
viewer.add_labels(
|
|
355
391
|
lab,
|
|
356
392
|
name=labels_name,
|
|
357
393
|
multiscale=isinstance(lab, list),
|
|
358
394
|
scale=lab_scale,
|
|
359
395
|
units=lab_units,
|
|
396
|
+
metadata=metadata,
|
|
360
397
|
**label_kwargs,
|
|
361
398
|
)
|
|
362
399
|
elif _is_zarr(image):
|
|
@@ -377,6 +414,7 @@ def view_in_napari(
|
|
|
377
414
|
multiscale=True,
|
|
378
415
|
scale=lab_scale,
|
|
379
416
|
units=lab_units,
|
|
417
|
+
metadata=_label_hint(store),
|
|
380
418
|
**label_kwargs,
|
|
381
419
|
)
|
|
382
420
|
logger.info("auto-loaded labels/%s from %s", name, image)
|
|
@@ -953,10 +953,41 @@ def add_pyramid(
|
|
|
953
953
|
``multiscales`` metadata. Existing calibration is preserved; pass
|
|
954
954
|
*pixel_size* to set it.
|
|
955
955
|
|
|
956
|
+
Parameters
|
|
957
|
+
----------
|
|
958
|
+
group_path : str or Path
|
|
959
|
+
Zarr group containing the full-resolution array at *base*.
|
|
960
|
+
base : str, optional
|
|
961
|
+
Component name of the existing full-resolution level (default
|
|
962
|
+
``"0"``). Auto-detected from existing ``multiscales`` metadata if
|
|
963
|
+
present, overriding this.
|
|
964
|
+
axes : str, optional
|
|
965
|
+
One letter per axis, e.g. ``"zyx"``. ``None`` → inferred from
|
|
966
|
+
existing metadata, or from the array's dimensionality.
|
|
967
|
+
pixel_size : dict, tuple or None, optional
|
|
968
|
+
Physical voxel size in micrometers. ``None`` → read from the store's
|
|
969
|
+
existing calibration, if any.
|
|
970
|
+
n_levels : int, optional
|
|
971
|
+
Maximum number of levels including the existing full-resolution one
|
|
972
|
+
(default 5).
|
|
973
|
+
downscale : int, optional
|
|
974
|
+
Per-level X/Y downsampling factor (default 2).
|
|
975
|
+
chunks : tuple of int, optional
|
|
976
|
+
Chunk shape for the written levels. ``None`` → a bounded default.
|
|
977
|
+
shard : bool or tuple of int, optional
|
|
978
|
+
Sharding request (see :func:`to_ome_zarr`'s *shard*).
|
|
979
|
+
progress : bool, optional
|
|
980
|
+
Show a per-level dask progress bar (default ``True``).
|
|
981
|
+
|
|
956
982
|
Returns
|
|
957
983
|
-------
|
|
958
984
|
str
|
|
959
985
|
The path to the updated group.
|
|
986
|
+
|
|
987
|
+
Examples
|
|
988
|
+
--------
|
|
989
|
+
>>> add_pyramid("scan.zarr", n_levels=4) # doctest: +SKIP
|
|
990
|
+
'scan.zarr'
|
|
960
991
|
"""
|
|
961
992
|
if downscale < 2:
|
|
962
993
|
raise ValueError("downscale must be >= 2")
|
|
@@ -1014,6 +1045,7 @@ def register_labels(
|
|
|
1014
1045
|
chunks: Union[tuple[int, ...], None] = None,
|
|
1015
1046
|
shard: ShardSpec = False,
|
|
1016
1047
|
progress: bool = True,
|
|
1048
|
+
n_objects: Union[int, None] = None,
|
|
1017
1049
|
) -> str:
|
|
1018
1050
|
"""Pyramidalise and register an existing ``labels/<name>/0`` base level.
|
|
1019
1051
|
|
|
@@ -1023,10 +1055,46 @@ def register_labels(
|
|
|
1023
1055
|
``labels/.zattrs``, and inherits the parent image's pixel calibration
|
|
1024
1056
|
(unless *pixel_size* is given).
|
|
1025
1057
|
|
|
1058
|
+
Parameters
|
|
1059
|
+
----------
|
|
1060
|
+
image_store : str or Path
|
|
1061
|
+
OME-ZARR store path containing the image this label belongs to.
|
|
1062
|
+
name : str, optional
|
|
1063
|
+
Label image name under ``labels/`` (default ``"labels"``).
|
|
1064
|
+
axes : str, optional
|
|
1065
|
+
One letter per axis. ``None`` → inferred from the label array.
|
|
1066
|
+
pixel_size : dict, tuple or None, optional
|
|
1067
|
+
Physical voxel size in micrometers. ``None`` → inherited from the
|
|
1068
|
+
parent image's own calibration.
|
|
1069
|
+
n_levels : int, optional
|
|
1070
|
+
Maximum number of pyramid levels including full resolution
|
|
1071
|
+
(default 5).
|
|
1072
|
+
downscale : int, optional
|
|
1073
|
+
Per-level X/Y downsampling factor (default 2).
|
|
1074
|
+
chunks : tuple of int, optional
|
|
1075
|
+
Chunk shape for the written levels. ``None`` → a bounded default.
|
|
1076
|
+
shard : bool or tuple of int, optional
|
|
1077
|
+
Sharding request (see :func:`to_ome_zarr`'s *shard*).
|
|
1078
|
+
progress : bool, optional
|
|
1079
|
+
Show a per-level dask progress bar (default ``True``).
|
|
1080
|
+
n_objects : int or None, optional
|
|
1081
|
+
Exact non-background object count, if known (e.g. from
|
|
1082
|
+
:func:`patchworks.merge_tile_labels`'s ``return_count=True`` after
|
|
1083
|
+
``sequential_labels=True``, which means ``ids == range(1, n_objects
|
|
1084
|
+
+ 1)`` by construction). When given, written into the label group's
|
|
1085
|
+
attrs as ``n_objects``/``sequential_labels`` so a downstream reader
|
|
1086
|
+
(e.g. napari-dask-ndmeasure) can use the known id set instead of
|
|
1087
|
+
re-deriving it with a full-volume scan of its own.
|
|
1088
|
+
|
|
1026
1089
|
Returns
|
|
1027
1090
|
-------
|
|
1028
1091
|
str
|
|
1029
1092
|
Path to the label group (``image_store/labels/<name>``).
|
|
1093
|
+
|
|
1094
|
+
Examples
|
|
1095
|
+
--------
|
|
1096
|
+
>>> register_labels("scan.zarr", "cells") # doctest: +SKIP
|
|
1097
|
+
'scan.zarr/labels/cells'
|
|
1030
1098
|
"""
|
|
1031
1099
|
store = str(image_store)
|
|
1032
1100
|
group = f"{store}/labels/{name}"
|
|
@@ -1047,6 +1115,9 @@ def register_labels(
|
|
|
1047
1115
|
)
|
|
1048
1116
|
grp = zarr.open_group(group, mode="a")
|
|
1049
1117
|
grp.attrs["image-label"] = {"version": _NGFF_VERSION}
|
|
1118
|
+
if n_objects is not None:
|
|
1119
|
+
grp.attrs["n_objects"] = int(n_objects)
|
|
1120
|
+
grp.attrs["sequential_labels"] = True
|
|
1050
1121
|
|
|
1051
1122
|
labels_grp = zarr.open_group(f"{store}/labels", mode="a")
|
|
1052
1123
|
registered = list(labels_grp.attrs.get("labels", []))
|
|
@@ -1069,6 +1140,7 @@ def write_labels(
|
|
|
1069
1140
|
shard: ShardSpec = False,
|
|
1070
1141
|
progress: bool = True,
|
|
1071
1142
|
overwrite: bool = False,
|
|
1143
|
+
n_objects: Union[int, None] = None,
|
|
1072
1144
|
) -> str:
|
|
1073
1145
|
"""Store *labels* inside *image_store* under the NGFF ``labels/`` group.
|
|
1074
1146
|
|
|
@@ -1078,10 +1150,58 @@ def write_labels(
|
|
|
1078
1150
|
single OME-ZARR store. Calibration is inherited from the parent image
|
|
1079
1151
|
unless *pixel_size* is given.
|
|
1080
1152
|
|
|
1153
|
+
Parameters
|
|
1154
|
+
----------
|
|
1155
|
+
image_store : str or Path
|
|
1156
|
+
OME-ZARR store this label image belongs to.
|
|
1157
|
+
labels : da.Array or np.ndarray
|
|
1158
|
+
Integer label array (0 = background), same spatial shape as the
|
|
1159
|
+
image.
|
|
1160
|
+
name : str, optional
|
|
1161
|
+
Label image name under ``labels/`` (default ``"labels"``).
|
|
1162
|
+
axes : str, optional
|
|
1163
|
+
One letter per axis. ``None`` → inferred from *labels*'
|
|
1164
|
+
dimensionality.
|
|
1165
|
+
pixel_size : dict, tuple or None, optional
|
|
1166
|
+
Physical voxel size in micrometers. ``None`` → inherited from the
|
|
1167
|
+
parent image's own calibration.
|
|
1168
|
+
n_levels : int, optional
|
|
1169
|
+
Maximum number of pyramid levels including full resolution
|
|
1170
|
+
(default 5).
|
|
1171
|
+
downscale : int, optional
|
|
1172
|
+
Per-level X/Y downsampling factor (default 2).
|
|
1173
|
+
chunks : tuple of int, optional
|
|
1174
|
+
Chunk shape for the written levels. ``None`` → a bounded default.
|
|
1175
|
+
shard : bool or tuple of int, optional
|
|
1176
|
+
Sharding request (see :func:`to_ome_zarr`'s *shard*).
|
|
1177
|
+
progress : bool, optional
|
|
1178
|
+
Show a per-level dask progress bar (default ``True``).
|
|
1179
|
+
overwrite : bool, optional
|
|
1180
|
+
Replace an existing label image of the same *name* (default
|
|
1181
|
+
``False``).
|
|
1182
|
+
n_objects : int or None, optional
|
|
1183
|
+
Exact non-background object count, if known — forwarded to
|
|
1184
|
+
:func:`register_labels`; see its docstring for what this enables.
|
|
1185
|
+
|
|
1081
1186
|
Returns
|
|
1082
1187
|
-------
|
|
1083
1188
|
str
|
|
1084
1189
|
Path to the written label group (``image_store/labels/<name>``).
|
|
1190
|
+
|
|
1191
|
+
Examples
|
|
1192
|
+
--------
|
|
1193
|
+
>>> from patchworks import merge_tile_labels
|
|
1194
|
+
>>> merged, n = merge_tile_labels(
|
|
1195
|
+
... "stage.zarr",
|
|
1196
|
+
... input_component="staged",
|
|
1197
|
+
... write_to="merged.zarr",
|
|
1198
|
+
... sequential_labels=True,
|
|
1199
|
+
... return_count=True,
|
|
1200
|
+
... ) # doctest: +SKIP
|
|
1201
|
+
>>> write_labels(
|
|
1202
|
+
... "scan.zarr", merged, name="cells", n_objects=n
|
|
1203
|
+
... ) # doctest: +SKIP
|
|
1204
|
+
'scan.zarr/labels/cells'
|
|
1085
1205
|
"""
|
|
1086
1206
|
arr = labels if isinstance(labels, da.Array) else da.asarray(labels)
|
|
1087
1207
|
if axes is None:
|
|
@@ -1111,4 +1231,5 @@ def write_labels(
|
|
|
1111
1231
|
chunks=chunks,
|
|
1112
1232
|
shard=shard,
|
|
1113
1233
|
progress=progress,
|
|
1234
|
+
n_objects=n_objects,
|
|
1114
1235
|
)
|
|
@@ -148,6 +148,62 @@ def test_merge_tile_labels_standalone(tmp_path):
|
|
|
148
148
|
assert ids.size == 1, f"object split into {ids.size} labels, expected 1"
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def test_merge_tile_labels_return_count(tmp_path):
|
|
152
|
+
# sequential_labels=True already computes the exact object count while
|
|
153
|
+
# renumbering to 1..N — return_count=True surfaces it instead of
|
|
154
|
+
# discarding it, so a caller can persist it (e.g. write_labels'
|
|
155
|
+
# n_objects=) for a downstream consumer to skip re-deriving the id set.
|
|
156
|
+
import dask.array as da
|
|
157
|
+
|
|
158
|
+
from patchworks import merge_tile_labels
|
|
159
|
+
|
|
160
|
+
data = np.zeros((1, 16, 32), dtype="uint16")
|
|
161
|
+
data[0, 2:6, 2:6] = 1
|
|
162
|
+
data[0, 2:6, 10:14] = 1 # same tile-local label, different object
|
|
163
|
+
image = da.from_array(data, chunks=(1, 16, 16))
|
|
164
|
+
|
|
165
|
+
def fn(tile):
|
|
166
|
+
from skimage.measure import label
|
|
167
|
+
|
|
168
|
+
return label(tile > 0).astype("int32")
|
|
169
|
+
|
|
170
|
+
labeled = image.map_blocks(
|
|
171
|
+
fn, dtype="int32", meta=np.empty((0,) * image.ndim, dtype="int32")
|
|
172
|
+
)
|
|
173
|
+
out = str(tmp_path / "merged.zarr")
|
|
174
|
+
merged, n_objects = merge_tile_labels(
|
|
175
|
+
labeled, write_to=out, sequential_labels=True, return_count=True
|
|
176
|
+
)
|
|
177
|
+
arr = merged.compute()
|
|
178
|
+
ids = np.unique(arr[arr > 0])
|
|
179
|
+
assert n_objects == ids.size
|
|
180
|
+
assert n_objects == 2
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_merge_tile_labels_return_count_none_without_sequential(tmp_path):
|
|
184
|
+
import dask.array as da
|
|
185
|
+
|
|
186
|
+
from patchworks import merge_tile_labels
|
|
187
|
+
|
|
188
|
+
data = np.zeros((1, 16, 32), dtype="uint16")
|
|
189
|
+
data[0, 4:12, 8:24] = 1
|
|
190
|
+
image = da.from_array(data, chunks=(1, 16, 16))
|
|
191
|
+
|
|
192
|
+
def fn(tile):
|
|
193
|
+
from skimage.measure import label
|
|
194
|
+
|
|
195
|
+
return label(tile > 0).astype("int32")
|
|
196
|
+
|
|
197
|
+
labeled = image.map_blocks(
|
|
198
|
+
fn, dtype="int32", meta=np.empty((0,) * image.ndim, dtype="int32")
|
|
199
|
+
)
|
|
200
|
+
out = str(tmp_path / "merged.zarr")
|
|
201
|
+
merged, n_objects = merge_tile_labels(
|
|
202
|
+
labeled, write_to=out, sequential_labels=False, return_count=True
|
|
203
|
+
)
|
|
204
|
+
assert n_objects is None
|
|
205
|
+
|
|
206
|
+
|
|
151
207
|
def test_merge_transitive_three_tiles(tmp_path):
|
|
152
208
|
# A cell that spans 3 tiles (A→B→C) must be merged into one label even
|
|
153
209
|
# though A and C never directly touch. Transitivity via connected_components.
|
|
@@ -68,3 +68,39 @@ def test_inner_label_discovery_none(tmp_path):
|
|
|
68
68
|
np.zeros((8, 8, 8), "uint16"), tmp_path / "img.zarr", n_levels=1
|
|
69
69
|
)
|
|
70
70
|
assert nplugin._inner_label_names(store) == []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_label_hint_present_when_n_objects_written(tmp_path):
|
|
74
|
+
"""write_labels(..., n_objects=...) is readable back via _label_hint."""
|
|
75
|
+
from patchworks.plugins.ome_zarr import to_ome_zarr, write_labels
|
|
76
|
+
|
|
77
|
+
store = to_ome_zarr(
|
|
78
|
+
np.zeros((8, 8, 8), "uint16"), tmp_path / "scan.zarr", n_levels=1
|
|
79
|
+
)
|
|
80
|
+
write_labels(
|
|
81
|
+
store,
|
|
82
|
+
np.ones((8, 8, 8), "int32"),
|
|
83
|
+
name="cells",
|
|
84
|
+
n_levels=1,
|
|
85
|
+
n_objects=17,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
hint = nplugin._label_hint(f"{store}/labels/cells")
|
|
89
|
+
assert hint == {"n_objects": 17, "sequential_labels": True}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_label_hint_empty_without_n_objects(tmp_path):
|
|
93
|
+
"""No n_objects= at write time -> no hint, not a misleading default."""
|
|
94
|
+
from patchworks.plugins.ome_zarr import to_ome_zarr, write_labels
|
|
95
|
+
|
|
96
|
+
store = to_ome_zarr(
|
|
97
|
+
np.zeros((8, 8, 8), "uint16"), tmp_path / "scan.zarr", n_levels=1
|
|
98
|
+
)
|
|
99
|
+
write_labels(store, np.ones((8, 8, 8), "int32"), name="cells", n_levels=1)
|
|
100
|
+
|
|
101
|
+
assert nplugin._label_hint(f"{store}/labels/cells") == {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_label_hint_missing_store_returns_empty():
|
|
105
|
+
"""A path that doesn't exist (or isn't a label group) just yields {}."""
|
|
106
|
+
assert nplugin._label_hint("/no/such/store.zarr") == {}
|
|
@@ -130,6 +130,34 @@ def test_write_labels_into_store(tmp_path):
|
|
|
130
130
|
assert lg.attrs["image-label"]["version"]
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
def test_write_labels_n_objects_persisted(tmp_path):
|
|
134
|
+
"""n_objects lands in the label group's attrs for a downstream reader."""
|
|
135
|
+
store = to_ome_zarr(
|
|
136
|
+
np.zeros((8, 8, 8), "uint16"), tmp_path / "img.zarr", n_levels=2
|
|
137
|
+
)
|
|
138
|
+
labels = np.ones((8, 8, 8), dtype="int32")
|
|
139
|
+
|
|
140
|
+
group = write_labels(store, labels, name="cells", n_levels=2, n_objects=42)
|
|
141
|
+
|
|
142
|
+
lg = zarr.open_group(group, mode="r")
|
|
143
|
+
assert lg.attrs["n_objects"] == 42
|
|
144
|
+
assert lg.attrs["sequential_labels"] is True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_write_labels_no_n_objects_by_default(tmp_path):
|
|
148
|
+
"""Without n_objects=, no misleading count is written."""
|
|
149
|
+
store = to_ome_zarr(
|
|
150
|
+
np.zeros((8, 8, 8), "uint16"), tmp_path / "img.zarr", n_levels=2
|
|
151
|
+
)
|
|
152
|
+
labels = np.ones((8, 8, 8), dtype="int32")
|
|
153
|
+
|
|
154
|
+
group = write_labels(store, labels, name="cells", n_levels=2)
|
|
155
|
+
|
|
156
|
+
lg = zarr.open_group(group, mode="r")
|
|
157
|
+
assert "n_objects" not in lg.attrs
|
|
158
|
+
assert "sequential_labels" not in lg.attrs
|
|
159
|
+
|
|
160
|
+
|
|
133
161
|
def test_reuse_pyramid_ignored_for_arrays(tmp_path):
|
|
134
162
|
"""reuse_pyramid only affects .ims inputs; arrays still rebuild."""
|
|
135
163
|
out = to_ome_zarr(
|
|
@@ -28,13 +28,14 @@ merged_store = str(Path(work_dir) / label_name / "_merged.zarr")
|
|
|
28
28
|
default_workers = int(
|
|
29
29
|
os.environ.get("SLURM_CPUS_PER_TASK", os.cpu_count() or 4)
|
|
30
30
|
)
|
|
31
|
-
merged = merge_tile_labels(
|
|
31
|
+
merged, n_objects = merge_tile_labels(
|
|
32
32
|
stage_path(work_dir, label_name),
|
|
33
33
|
write_to=merged_store,
|
|
34
34
|
input_component="staged",
|
|
35
35
|
sequential_labels=cfg.get("sequential_labels", True),
|
|
36
36
|
n_workers=cfg.get("merge_workers", default_workers),
|
|
37
37
|
progress=False,
|
|
38
|
+
return_count=True,
|
|
38
39
|
)
|
|
39
40
|
group = write_labels(
|
|
40
41
|
image_store,
|
|
@@ -43,6 +44,7 @@ group = write_labels(
|
|
|
43
44
|
n_levels=int(cfg.get("pyramid_levels", 5)),
|
|
44
45
|
downscale=int(cfg.get("pyramid_downscale", 2)),
|
|
45
46
|
overwrite=True,
|
|
47
|
+
n_objects=n_objects,
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
shutil.rmtree(merged_store, ignore_errors=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|