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.
Files changed (50) hide show
  1. {patchworks-0.6.0 → patchworks-0.7.0}/PKG-INFO +1 -1
  2. {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/ome_zarr_napari.md +15 -4
  3. {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/performance.md +16 -0
  4. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_core.py +28 -1
  5. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/napari.py +27 -7
  6. {patchworks-0.6.0 → patchworks-0.7.0}/tests/test_napari.py +29 -0
  7. {patchworks-0.6.0 → patchworks-0.7.0}/.github/workflows/docs.yml +0 -0
  8. {patchworks-0.6.0 → patchworks-0.7.0}/.github/workflows/lint.yml +0 -0
  9. {patchworks-0.6.0 → patchworks-0.7.0}/.github/workflows/release.yml +0 -0
  10. {patchworks-0.6.0 → patchworks-0.7.0}/.gitignore +0 -0
  11. {patchworks-0.6.0 → patchworks-0.7.0}/README.md +0 -0
  12. {patchworks-0.6.0 → patchworks-0.7.0}/cliff.toml +0 -0
  13. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/chunks.md +0 -0
  14. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/cluster.md +0 -0
  15. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/io.md +0 -0
  16. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/merge_tile_labels.md +0 -0
  17. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/plugins/cellpose.md +0 -0
  18. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/plugins/napari.md +0 -0
  19. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/plugins/ome_zarr.md +0 -0
  20. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/relabel.md +0 -0
  21. {patchworks-0.6.0 → patchworks-0.7.0}/docs/api/tile_process.md +0 -0
  22. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_2d.md +0 -0
  23. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_2d.py +0 -0
  24. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_3d.md +0 -0
  25. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/cellpose_3d.py +0 -0
  26. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/custom.md +0 -0
  27. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/custom_method.py +0 -0
  28. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/standalone_merge.md +0 -0
  29. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/stardist.md +0 -0
  30. {patchworks-0.6.0 → patchworks-0.7.0}/docs/examples/stardist_2d.py +0 -0
  31. {patchworks-0.6.0 → patchworks-0.7.0}/docs/getting_started.md +0 -0
  32. {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/gpu_distributed.md +0 -0
  33. {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/merging.md +0 -0
  34. {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/pitfalls.md +0 -0
  35. {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/skip_empty.md +0 -0
  36. {patchworks-0.6.0 → patchworks-0.7.0}/docs/guide/tiling.md +0 -0
  37. {patchworks-0.6.0 → patchworks-0.7.0}/docs/index.md +0 -0
  38. {patchworks-0.6.0 → patchworks-0.7.0}/mkdocs.yml +0 -0
  39. {patchworks-0.6.0 → patchworks-0.7.0}/pyproject.toml +0 -0
  40. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/__init__.py +0 -0
  41. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_chunks.py +0 -0
  42. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_cluster.py +0 -0
  43. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_io.py +0 -0
  44. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_merge.py +0 -0
  45. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/_relabel.py +0 -0
  46. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/__init__.py +0 -0
  47. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/cellpose.py +0 -0
  48. {patchworks-0.6.0 → patchworks-0.7.0}/src/patchworks/plugins/ome_zarr.py +0 -0
  49. {patchworks-0.6.0 → patchworks-0.7.0}/tests/test_core.py +0 -0
  50. {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.6.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
- # one store holding both image and labels/<name>:
103
- view_in_napari("scan.zarr", labels="scan.zarr/labels/labels")
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", labels="scan.zarr/labels/labels")
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
- if _active is None:
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
- >>> tile_process("scan.zarr", fn, write_to="labels.zarr")
21
- >>> to_ome_zarr("scan.zarr", "scan_pyramid.zarr") # optional, for speed
22
- >>>
23
- >>> view_in_napari("scan_pyramid.zarr", labels="labels.zarr")
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; ``None`` shows the image only.
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", labels="labels.zarr") # doctest: +SKIP
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