patchworks 1.0.0__tar.gz → 1.1.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.
- {patchworks-1.0.0 → patchworks-1.1.1}/PKG-INFO +3 -3
- {patchworks-1.0.0 → patchworks-1.1.1}/pyproject.toml +5 -3
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_merge.py +17 -4
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/plugins/napari.py +38 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/plugins/ome_zarr.py +26 -3
- {patchworks-1.0.0 → patchworks-1.1.1}/tests/test_core.py +56 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/tests/test_napari.py +36 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/tests/test_ome_zarr.py +28 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/scripts/merge.py +3 -1
- {patchworks-1.0.0 → patchworks-1.1.1}/.github/workflows/docs.yml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/.github/workflows/lint.yml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/.github/workflows/release.yml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/.gitignore +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/.markdownlint-cli2.yaml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/LICENSE +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/README.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/cliff.toml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/chunks.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/cluster.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/io.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/plugins/napari.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/relabel.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/api/tile_process.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/assets/logo.png +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/custom.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/custom_method.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/standalone_merge.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/stardist.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/examples/stardist_2d.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/getting_started.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/merging.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/performance.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/pitfalls.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/skip_empty.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/snakemake.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/guide/tiling.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/docs/index.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/mkdocs.yml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/__init__.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_chunks.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_cluster.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_core.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_distributed.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_io.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_relabel.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/_relations.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/tests/test_distributed.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/tests/test_relations.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/README.md +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/Snakefile +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/config/config.yaml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/config/multi.yaml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/pixi.toml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/rules/common.smk +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/rules/convert.smk +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/rules/merge.smk +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/rules/segment.smk +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/scripts/_pw.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/scripts/convert.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/scripts/run_multi.py +0 -0
- {patchworks-1.0.0 → patchworks-1.1.1}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
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
|
|
@@ -34,7 +34,7 @@ Requires-Dist: glasbey; extra == 'all'
|
|
|
34
34
|
Requires-Dist: imaris-ims-file-reader; extra == 'all'
|
|
35
35
|
Requires-Dist: ipykernel<7; extra == 'all'
|
|
36
36
|
Requires-Dist: lxml-html-clean; extra == 'all'
|
|
37
|
-
Requires-Dist: napari-
|
|
37
|
+
Requires-Dist: napari-chunked-regionprops>=0.1.0; extra == 'all'
|
|
38
38
|
Requires-Dist: napari[all]>=0.7.1; extra == 'all'
|
|
39
39
|
Requires-Dist: numpy<2.5; extra == 'all'
|
|
40
40
|
Requires-Dist: nvidia-ml-py; extra == 'all'
|
|
@@ -75,7 +75,7 @@ Provides-Extra: napari
|
|
|
75
75
|
Requires-Dist: glasbey; extra == 'napari'
|
|
76
76
|
Requires-Dist: ipykernel<7; extra == 'napari'
|
|
77
77
|
Requires-Dist: lxml-html-clean; extra == 'napari'
|
|
78
|
-
Requires-Dist: napari-
|
|
78
|
+
Requires-Dist: napari-chunked-regionprops>=0.1.0; extra == 'napari'
|
|
79
79
|
Requires-Dist: napari[all]>=0.7.1; extra == 'napari'
|
|
80
80
|
Requires-Dist: numpy<2.5; extra == 'napari'
|
|
81
81
|
Provides-Extra: workflow
|
|
@@ -67,16 +67,18 @@ imaris = ["imaris-ims-file-reader"]
|
|
|
67
67
|
# - lxml-html-clean: napari's notebook_display imports lxml.html.clean, split
|
|
68
68
|
# into a separate package in lxml >= 5.2 (else ImportError on Viewer()).
|
|
69
69
|
# - glasbey: distinct high-contrast label LUTs for view_in_napari.
|
|
70
|
-
# - napari-
|
|
70
|
+
# - napari-chunked-regionprops: out-of-core regionprops-style measurements
|
|
71
71
|
# (area/centroid/intensity stats) for huge Labels layers, straight off
|
|
72
|
-
# their backing dask/zarr arrays — the "Measure" dock widget.
|
|
72
|
+
# their backing dask/zarr arrays — the "Measure" dock widget. Formerly
|
|
73
|
+
# napari-dask-ndmeasure; renamed when its engine dropped dask_image.ndmeasure
|
|
74
|
+
# for a chunk-local map/merge that scales with chunk count, not object count.
|
|
73
75
|
napari = [
|
|
74
76
|
"napari[all]>=0.7.1",
|
|
75
77
|
"numpy<2.5",
|
|
76
78
|
"ipykernel<7",
|
|
77
79
|
"lxml-html-clean",
|
|
78
80
|
"glasbey",
|
|
79
|
-
"napari-
|
|
81
|
+
"napari-chunked-regionprops>=0.1.0",
|
|
80
82
|
]
|
|
81
83
|
# workflow runs the Snakemake pipeline (per-tile SLURM jobs across GPUs).
|
|
82
84
|
workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm"]
|
|
@@ -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
|
|
@@ -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-chunked-regionprops) can use it instead of re-deriving the id
|
|
191
|
+
set 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)
|
|
@@ -1045,6 +1045,7 @@ def register_labels(
|
|
|
1045
1045
|
chunks: Union[tuple[int, ...], None] = None,
|
|
1046
1046
|
shard: ShardSpec = False,
|
|
1047
1047
|
progress: bool = True,
|
|
1048
|
+
n_objects: Union[int, None] = None,
|
|
1048
1049
|
) -> str:
|
|
1049
1050
|
"""Pyramidalise and register an existing ``labels/<name>/0`` base level.
|
|
1050
1051
|
|
|
@@ -1076,6 +1077,14 @@ def register_labels(
|
|
|
1076
1077
|
Sharding request (see :func:`to_ome_zarr`'s *shard*).
|
|
1077
1078
|
progress : bool, optional
|
|
1078
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-chunked-regionprops) can use the known id set instead
|
|
1087
|
+
of re-deriving it with a full-volume scan of its own.
|
|
1079
1088
|
|
|
1080
1089
|
Returns
|
|
1081
1090
|
-------
|
|
@@ -1106,6 +1115,9 @@ def register_labels(
|
|
|
1106
1115
|
)
|
|
1107
1116
|
grp = zarr.open_group(group, mode="a")
|
|
1108
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
|
|
1109
1121
|
|
|
1110
1122
|
labels_grp = zarr.open_group(f"{store}/labels", mode="a")
|
|
1111
1123
|
registered = list(labels_grp.attrs.get("labels", []))
|
|
@@ -1128,6 +1140,7 @@ def write_labels(
|
|
|
1128
1140
|
shard: ShardSpec = False,
|
|
1129
1141
|
progress: bool = True,
|
|
1130
1142
|
overwrite: bool = False,
|
|
1143
|
+
n_objects: Union[int, None] = None,
|
|
1131
1144
|
) -> str:
|
|
1132
1145
|
"""Store *labels* inside *image_store* under the NGFF ``labels/`` group.
|
|
1133
1146
|
|
|
@@ -1166,6 +1179,9 @@ def write_labels(
|
|
|
1166
1179
|
overwrite : bool, optional
|
|
1167
1180
|
Replace an existing label image of the same *name* (default
|
|
1168
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.
|
|
1169
1185
|
|
|
1170
1186
|
Returns
|
|
1171
1187
|
-------
|
|
@@ -1175,10 +1191,16 @@ def write_labels(
|
|
|
1175
1191
|
Examples
|
|
1176
1192
|
--------
|
|
1177
1193
|
>>> from patchworks import merge_tile_labels
|
|
1178
|
-
>>> merged = merge_tile_labels(
|
|
1179
|
-
... "stage.zarr",
|
|
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
|
|
1180
1203
|
... ) # doctest: +SKIP
|
|
1181
|
-
>>> write_labels("scan.zarr", merged, name="cells") # doctest: +SKIP
|
|
1182
1204
|
'scan.zarr/labels/cells'
|
|
1183
1205
|
"""
|
|
1184
1206
|
arr = labels if isinstance(labels, da.Array) else da.asarray(labels)
|
|
@@ -1209,4 +1231,5 @@ def write_labels(
|
|
|
1209
1231
|
chunks=chunks,
|
|
1210
1232
|
shard=shard,
|
|
1211
1233
|
progress=progress,
|
|
1234
|
+
n_objects=n_objects,
|
|
1212
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
|
|
File without changes
|
|
File without changes
|