patchworks 0.6.0__tar.gz → 0.7.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.6.0 → patchworks-0.7.0}/PKG-INFO +1 -1
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/ome_zarr_napari.md +15 -4
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/performance.md +16 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_core.py +28 -1
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/napari.py +27 -7
- {patchworks-0.6.0 → patchworks-0.7.0}/tests/test_napari.py +29 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/.github/workflows/docs.yml +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/.github/workflows/lint.yml +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/.github/workflows/release.yml +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/.gitignore +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/README.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/cliff.toml +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/chunks.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/cluster.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/io.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/relabel.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/tile_process.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/custom.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/custom_method.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/stardist.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/getting_started.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/merging.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/tiling.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/docs/index.md +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/mkdocs.yml +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/pyproject.toml +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/__init__.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_chunks.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_io.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_merge.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/tests/test_core.py +0 -0
- {patchworks-0.6.0 → patchworks-0.7.0}/tests/test_ome_zarr.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -50,6 +50,13 @@ to_ome_zarr("scan.czi", "scan.zarr", n_levels=5) # via bioio
|
|
|
50
50
|
to_ome_zarr("scan.ims", "scan.zarr") # Imaris, native HDF5
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
!!! note "Imaris pyramids are rebuilt, not reused"
|
|
54
|
+
`.ims` files carry their own resolution pyramid, but `to_ome_zarr` reads
|
|
55
|
+
only the **full-resolution** level and **builds a fresh NGFF pyramid** from
|
|
56
|
+
it. This guarantees a consistent pyramid (XY-only, nearest-neighbour,
|
|
57
|
+
calibrated) rather than inheriting Imaris's own downsampling scheme. It
|
|
58
|
+
costs some extra compute, but the build is lazy and OOM-safe.
|
|
59
|
+
|
|
53
60
|
### Pixel calibration
|
|
54
61
|
|
|
55
62
|
The physical voxel size is read from the input — bioio's `physical_pixel_sizes`,
|
|
@@ -96,13 +103,17 @@ write_labels("scan.zarr", my_labels, name="nuclei")
|
|
|
96
103
|
layer in one call. OME-ZARR pyramids are handed to napari as a lazy multi-scale
|
|
97
104
|
list, so even huge stores open instantly and only on-screen data is fetched.
|
|
98
105
|
|
|
106
|
+
Because `tile_process` writes labels **into** the store by default, you usually
|
|
107
|
+
need no `labels=` argument at all — `view_in_napari` auto-loads every label
|
|
108
|
+
image found under `scan.zarr/labels/`:
|
|
109
|
+
|
|
99
110
|
```python
|
|
100
111
|
from patchworks.plugins.napari import view_in_napari
|
|
101
112
|
|
|
102
|
-
#
|
|
103
|
-
view_in_napari("scan.zarr"
|
|
113
|
+
# auto-loads scan.zarr/labels/* as Labels layers:
|
|
114
|
+
view_in_napari("scan.zarr")
|
|
104
115
|
|
|
105
|
-
# or a separate plain label store written with write_to=:
|
|
116
|
+
# or point at a separate plain label store written with write_to=:
|
|
106
117
|
view_in_napari("scan.zarr", labels="labels.zarr")
|
|
107
118
|
```
|
|
108
119
|
|
|
@@ -120,7 +131,7 @@ from patchworks.plugins.napari import view_in_napari
|
|
|
120
131
|
tile_process("scan.zarr", fn, progress=True)
|
|
121
132
|
|
|
122
133
|
# 2. inspect image + labels together, straight from the one store
|
|
123
|
-
view_in_napari("scan.zarr"
|
|
134
|
+
view_in_napari("scan.zarr") # labels auto-loaded from scan.zarr/labels/
|
|
124
135
|
```
|
|
125
136
|
|
|
126
137
|
Plugging in a different segmentation method is just swapping `fn` — any
|
|
@@ -17,6 +17,22 @@ merge step are sized to the host automatically:
|
|
|
17
17
|
The RAM figure is read live via `psutil`; without it, a conservative default is
|
|
18
18
|
used instead of guessing high.
|
|
19
19
|
|
|
20
|
+
## Live progress dashboard (GPU runs)
|
|
21
|
+
|
|
22
|
+
A single-GPU run still gets a **Dask dashboard**: patchworks spins up a tiny
|
|
23
|
+
1-worker / 1-thread in-process cluster, which keeps GPU evaluations serial (no
|
|
24
|
+
VRAM contention) while exposing the dashboard so you can watch tiles stream
|
|
25
|
+
through. The URL is logged at the start of staging:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
INFO:patchworks._core:Dask dashboard for this run: http://127.0.0.1:8787/status
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This needs `distributed` (and `bokeh` for the UI) installed; if they are
|
|
32
|
+
missing, patchworks logs a warning and falls back to the threaded scheduler
|
|
33
|
+
(no dashboard, same result). A cluster you start yourself
|
|
34
|
+
(`make_local_cluster`) is used as-is instead.
|
|
35
|
+
|
|
20
36
|
## Overriding the worker count
|
|
21
37
|
|
|
22
38
|
```python
|
|
@@ -332,7 +332,31 @@ def tile_process(
|
|
|
332
332
|
import dask as _dask
|
|
333
333
|
|
|
334
334
|
_tile_nbytes = int(np.prod(labeled.chunksize)) * labeled.dtype.itemsize
|
|
335
|
-
|
|
335
|
+
_temp_cluster = None
|
|
336
|
+
_temp_client = None
|
|
337
|
+
if _active is None and use_gpu:
|
|
338
|
+
# Single-GPU runs still get a live Dask dashboard: a 1-worker /
|
|
339
|
+
# 1-thread in-process cluster keeps GPU evals serial (no VRAM
|
|
340
|
+
# contention) while exposing the dashboard for progress.
|
|
341
|
+
try:
|
|
342
|
+
from dask.distributed import Client, LocalCluster
|
|
343
|
+
|
|
344
|
+
_temp_cluster = LocalCluster(
|
|
345
|
+
n_workers=1, threads_per_worker=1, processes=False
|
|
346
|
+
)
|
|
347
|
+
_temp_client = Client(_temp_cluster)
|
|
348
|
+
logger.info(
|
|
349
|
+
"Dask dashboard for this run: %s",
|
|
350
|
+
_temp_client.dashboard_link,
|
|
351
|
+
)
|
|
352
|
+
except Exception as exc: # no distributed/bokeh → threaded fallback
|
|
353
|
+
logger.warning(
|
|
354
|
+
"Could not start a dashboard cluster (%s); "
|
|
355
|
+
"falling back to the threaded scheduler.",
|
|
356
|
+
exc,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if _distributed_client() is None:
|
|
336
360
|
_workers = (
|
|
337
361
|
max_workers
|
|
338
362
|
if max_workers is not None
|
|
@@ -363,6 +387,9 @@ def tile_process(
|
|
|
363
387
|
logger.info("Staging tiles to %s …", stage_path)
|
|
364
388
|
with _sched_ctx:
|
|
365
389
|
_stage_to_zarr(labeled, stage_path, "staged", progress)
|
|
390
|
+
if _temp_client is not None:
|
|
391
|
+
_temp_client.close()
|
|
392
|
+
_temp_cluster.close()
|
|
366
393
|
labeled = da.from_zarr(stage_path, component="staged")
|
|
367
394
|
|
|
368
395
|
# NB: no post-staging skip-count pass here — counting skipped tiles by
|
|
@@ -14,13 +14,12 @@ napari is an optional, GUI-heavy dependency. Install it with
|
|
|
14
14
|
Usage
|
|
15
15
|
-----
|
|
16
16
|
>>> from patchworks import tile_process
|
|
17
|
-
>>> from patchworks.plugins.ome_zarr import to_ome_zarr
|
|
18
17
|
>>> from patchworks.plugins.napari import view_in_napari
|
|
19
18
|
>>>
|
|
20
|
-
>>>
|
|
21
|
-
>>>
|
|
22
|
-
>>>
|
|
23
|
-
>>> view_in_napari("
|
|
19
|
+
>>> # labels are written into scan.zarr/labels/ by default …
|
|
20
|
+
>>> tile_process("scan.zarr", fn)
|
|
21
|
+
>>> # … so the viewer finds and overlays them with no labels= argument:
|
|
22
|
+
>>> view_in_napari("scan.zarr")
|
|
24
23
|
"""
|
|
25
24
|
|
|
26
25
|
from __future__ import annotations
|
|
@@ -86,6 +85,15 @@ def _resolve_image(
|
|
|
86
85
|
return source
|
|
87
86
|
|
|
88
87
|
|
|
88
|
+
def _inner_label_names(store: Union[str, Path]) -> list[str]:
|
|
89
|
+
"""Names registered under an OME-ZARR's NGFF ``labels/`` group, if any."""
|
|
90
|
+
try:
|
|
91
|
+
grp = zarr.open_group(f"{store}/labels", mode="r")
|
|
92
|
+
except Exception:
|
|
93
|
+
return []
|
|
94
|
+
return list(grp.attrs.get("labels", []))
|
|
95
|
+
|
|
96
|
+
|
|
89
97
|
def _resolve_labels(
|
|
90
98
|
source: Union[da.Array, str, Path], component: str
|
|
91
99
|
) -> Union[da.Array, list[da.Array]]:
|
|
@@ -123,7 +131,10 @@ def view_in_napari(
|
|
|
123
131
|
labels : da.Array, str, Path or None
|
|
124
132
|
Label array to overlay. A plain ``.zarr`` store written by
|
|
125
133
|
``tile_process`` is read from its ``labels_component``; an OME-ZARR
|
|
126
|
-
pyramid is shown multi-scale
|
|
134
|
+
pyramid is shown multi-scale. ``None`` (default) **auto-loads** every
|
|
135
|
+
label image stored inside the OME-ZARR under ``labels/<name>/`` — the
|
|
136
|
+
place ``tile_process`` writes them by default — each as its own Labels
|
|
137
|
+
layer. (Falls back to image-only if there are none.)
|
|
127
138
|
channel : int or None, optional
|
|
128
139
|
Channel to display from the image (``None`` keeps all channels).
|
|
129
140
|
labels_component : str, optional
|
|
@@ -145,7 +156,7 @@ def view_in_napari(
|
|
|
145
156
|
|
|
146
157
|
Examples
|
|
147
158
|
--------
|
|
148
|
-
>>> view_in_napari("scan.zarr"
|
|
159
|
+
>>> view_in_napari("scan.zarr") # auto-loads scan.zarr/labels/* # doctest: +SKIP
|
|
149
160
|
"""
|
|
150
161
|
napari = _require_napari()
|
|
151
162
|
|
|
@@ -161,6 +172,15 @@ def view_in_napari(
|
|
|
161
172
|
if labels is not None:
|
|
162
173
|
lab = _resolve_labels(labels, labels_component)
|
|
163
174
|
viewer.add_labels(lab, name=labels_name)
|
|
175
|
+
elif _is_zarr(image):
|
|
176
|
+
# No labels given → auto-overlay every label image stored inside the
|
|
177
|
+
# OME-ZARR under labels/<name>/ (the default place tile_process writes
|
|
178
|
+
# them), each as its own multi-scale Labels layer.
|
|
179
|
+
for name in _inner_label_names(image):
|
|
180
|
+
levels = _multiscale_levels(f"{image}/labels/{name}", None)
|
|
181
|
+
lab = [lvl.astype("int32") for lvl in levels]
|
|
182
|
+
viewer.add_labels(lab if len(lab) > 1 else lab[0], name=name)
|
|
183
|
+
logger.info("auto-loaded labels/%s from %s", name, image)
|
|
164
184
|
|
|
165
185
|
if show:
|
|
166
186
|
napari.run()
|
|
@@ -39,3 +39,32 @@ def test_require_napari_message(monkeypatch):
|
|
|
39
39
|
nplugin._require_napari()
|
|
40
40
|
else:
|
|
41
41
|
assert nplugin._require_napari() is napari
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_inner_label_discovery(tmp_path):
|
|
45
|
+
"""Labels written into a store are discoverable for auto-overlay."""
|
|
46
|
+
import numpy as np
|
|
47
|
+
|
|
48
|
+
from patchworks.plugins.ome_zarr import to_ome_zarr, write_labels
|
|
49
|
+
|
|
50
|
+
store = to_ome_zarr(
|
|
51
|
+
np.zeros((8, 8, 8), "uint16"), tmp_path / "scan.zarr", n_levels=2
|
|
52
|
+
)
|
|
53
|
+
write_labels(store, np.ones((8, 8, 8), "int32"), name="cells", n_levels=2)
|
|
54
|
+
|
|
55
|
+
assert nplugin._inner_label_names(store) == ["cells"]
|
|
56
|
+
levels = nplugin._multiscale_levels(f"{store}/labels/cells", None)
|
|
57
|
+
assert len(levels) == 2
|
|
58
|
+
assert levels[1].shape == (8, 4, 4) # Z preserved, XY downsampled
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_inner_label_discovery_none(tmp_path):
|
|
62
|
+
"""A store without labels yields an empty list (image-only view)."""
|
|
63
|
+
import numpy as np
|
|
64
|
+
|
|
65
|
+
from patchworks.plugins.ome_zarr import to_ome_zarr
|
|
66
|
+
|
|
67
|
+
store = to_ome_zarr(
|
|
68
|
+
np.zeros((8, 8, 8), "uint16"), tmp_path / "img.zarr", n_levels=1
|
|
69
|
+
)
|
|
70
|
+
assert nplugin._inner_label_names(store) == []
|
|
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
|