patchworks 2.6.7__tar.gz → 2.6.8__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 (105) hide show
  1. {patchworks-2.6.7 → patchworks-2.6.8}/PKG-INFO +1 -1
  2. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/ome_zarr_napari.md +14 -0
  3. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/plugins/cellpose.py +61 -1
  4. patchworks-2.6.8/tests/test_cellpose.py +49 -0
  5. patchworks-2.6.8/tests/test_pw.py +57 -0
  6. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_run_multi.py +26 -0
  7. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/pixi.toml +14 -1
  8. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/_pw.py +6 -0
  9. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/relate.py +1 -3
  10. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/run_multi.py +3 -1
  11. patchworks-2.6.8/workflow/scripts/view.py +47 -0
  12. {patchworks-2.6.7 → patchworks-2.6.8}/.github/workflows/docs.yml +0 -0
  13. {patchworks-2.6.7 → patchworks-2.6.8}/.github/workflows/lint.yml +0 -0
  14. {patchworks-2.6.7 → patchworks-2.6.8}/.github/workflows/release.yml +0 -0
  15. {patchworks-2.6.7 → patchworks-2.6.8}/.gitignore +0 -0
  16. {patchworks-2.6.7 → patchworks-2.6.8}/.markdownlint-cli2.yaml +0 -0
  17. {patchworks-2.6.7 → patchworks-2.6.8}/LICENSE +0 -0
  18. {patchworks-2.6.7 → patchworks-2.6.8}/README.md +0 -0
  19. {patchworks-2.6.7 → patchworks-2.6.8}/cliff.toml +0 -0
  20. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/chunks.md +0 -0
  21. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/cluster.md +0 -0
  22. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/io.md +0 -0
  23. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/merge_tile_labels.md +0 -0
  24. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/plugins/cellpose.md +0 -0
  25. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/plugins/dog.md +0 -0
  26. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/plugins/napari.md +0 -0
  27. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/plugins/ome_zarr.md +0 -0
  28. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/postprocess.md +0 -0
  29. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/relabel.md +0 -0
  30. {patchworks-2.6.7 → patchworks-2.6.8}/docs/api/tile_process.md +0 -0
  31. {patchworks-2.6.7 → patchworks-2.6.8}/docs/assets/logo.png +0 -0
  32. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/cellpose_2d.md +0 -0
  33. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/cellpose_2d.py +0 -0
  34. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/cellpose_3d.md +0 -0
  35. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/cellpose_3d.py +0 -0
  36. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/custom.md +0 -0
  37. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/custom_method.py +0 -0
  38. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/dog.md +0 -0
  39. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/dog.py +0 -0
  40. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/standalone_merge.md +0 -0
  41. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/stardist.md +0 -0
  42. {patchworks-2.6.7 → patchworks-2.6.8}/docs/examples/stardist_2d.py +0 -0
  43. {patchworks-2.6.7 → patchworks-2.6.8}/docs/getting_started.md +0 -0
  44. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/custom_segmentation.md +0 -0
  45. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/gpu_distributed.md +0 -0
  46. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/label_relations.md +0 -0
  47. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/measurements.md +0 -0
  48. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/merging.md +0 -0
  49. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/performance.md +0 -0
  50. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/pitfalls.md +0 -0
  51. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/skip_empty.md +0 -0
  52. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/snakemake.md +0 -0
  53. {patchworks-2.6.7 → patchworks-2.6.8}/docs/guide/tiling.md +0 -0
  54. {patchworks-2.6.7 → patchworks-2.6.8}/docs/index.md +0 -0
  55. {patchworks-2.6.7 → patchworks-2.6.8}/mkdocs.yml +0 -0
  56. {patchworks-2.6.7 → patchworks-2.6.8}/pyproject.toml +0 -0
  57. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/__init__.py +0 -0
  58. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_chunks.py +0 -0
  59. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_cluster.py +0 -0
  60. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_core.py +0 -0
  61. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_distributed.py +0 -0
  62. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_gpu.py +0 -0
  63. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_io.py +0 -0
  64. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_merge.py +0 -0
  65. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_notify.py +0 -0
  66. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_occupancy.py +0 -0
  67. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_postprocess.py +0 -0
  68. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_progress.py +0 -0
  69. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_relabel.py +0 -0
  70. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/_relations.py +0 -0
  71. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/plugins/__init__.py +0 -0
  72. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/plugins/dog.py +0 -0
  73. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/plugins/napari.py +0 -0
  74. {patchworks-2.6.7 → patchworks-2.6.8}/src/patchworks/plugins/ome_zarr.py +0 -0
  75. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_allocation.py +0 -0
  76. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_core.py +0 -0
  77. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_distributed.py +0 -0
  78. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_dog.py +0 -0
  79. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_gpu.py +0 -0
  80. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_napari.py +0 -0
  81. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_notify.py +0 -0
  82. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_occupancy.py +0 -0
  83. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_ome_zarr.py +0 -0
  84. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_postprocess.py +0 -0
  85. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_progress.py +0 -0
  86. {patchworks-2.6.7 → patchworks-2.6.8}/tests/test_relations.py +0 -0
  87. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/README.md +0 -0
  88. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/Snakefile +0 -0
  89. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/config/common.yaml +0 -0
  90. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/config/config.yaml +0 -0
  91. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/config/config_cilia.yaml +0 -0
  92. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/config/config_cyto.yaml +0 -0
  93. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/config/config_nuclei.yaml +0 -0
  94. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/config/multi.yaml +0 -0
  95. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/profile/slurm/config.yaml +0 -0
  96. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/rules/common.smk +0 -0
  97. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/rules/convert.smk +0 -0
  98. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/rules/merge.smk +0 -0
  99. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/rules/segment.smk +0 -0
  100. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/build_occupancy.py +0 -0
  101. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/convert.py +0 -0
  102. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/fetch_model.py +0 -0
  103. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/merge.py +0 -0
  104. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/prepare_tiles.py +0 -0
  105. {patchworks-2.6.7 → patchworks-2.6.8}/workflow/scripts/segment_tile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchworks
3
- Version: 2.6.7
3
+ Version: 2.6.8
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
@@ -229,6 +229,20 @@ just one channel instead: `view_in_napari("scan.zarr", channel=0)`.
229
229
  napari ships in `patchworks[all]`, or install just it with
230
230
  `pip install "patchworks[napari]"`.
231
231
 
232
+ !!! tip "From the cluster workflow"
233
+ `workflow/pixi.toml` has a separate `viewer` environment (napari's Qt/GUI
234
+ dependencies are kept out of the default headless workflow env on
235
+ purpose):
236
+
237
+ ```bash
238
+ pixi install -e viewer
239
+ pixi run -e viewer napari /path/to/work_dir/image.zarr
240
+ ```
241
+
242
+ This wraps `view_in_napari` with its `labels=` argument left at the
243
+ default, so every label group in the store loads automatically — needs a
244
+ display (`ssh -X`, or a remote-desktop/VNC session).
245
+
232
246
  !!! tip "Measuring all the objects"
233
247
  Once labels are loaded, use
234
248
  [napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)'s
@@ -75,6 +75,40 @@ def _require_cellpose():
75
75
  )
76
76
 
77
77
 
78
+ def cellpose_anisotropy(voxel_size: dict[str, float]) -> float | None:
79
+ """Cellpose's ``anisotropy`` (z voxel size / lateral voxel size) from a calibration.
80
+
81
+ ``do_3D`` assumes isotropic voxels unless told otherwise: without this,
82
+ Cellpose builds its 3-D flow-field consensus across z-planes it thinks
83
+ are spaced the same as the xy pixels, which for real data rarely holds
84
+ and fragments or distorts objects across z. Getting this wrong does not
85
+ fail loudly -- it just produces a subtly (or not so subtly) wrong
86
+ segmentation, which is why deriving it beats retyping it.
87
+
88
+ Parameters
89
+ ----------
90
+ voxel_size : dict
91
+ Image calibration, e.g. from
92
+ :func:`patchworks.plugins.ome_zarr.read_pixel_size`. Needs ``z`` and
93
+ ``x`` (or ``y``) for the lateral size.
94
+
95
+ Returns
96
+ -------
97
+ float or None
98
+ ``z / lateral``, or None if the calibration lacks what is needed.
99
+
100
+ Examples
101
+ --------
102
+ >>> cellpose_anisotropy({"z": 0.24, "y": 0.10833, "x": 0.10833})
103
+ 2.215452783162559
104
+ """
105
+ lateral = voxel_size.get("x") or voxel_size.get("y")
106
+ z = voxel_size.get("z")
107
+ if not lateral or not z:
108
+ return None
109
+ return z / lateral
110
+
111
+
78
112
  def cellpose_fn(
79
113
  model: str = "cyto3",
80
114
  *,
@@ -83,6 +117,7 @@ def cellpose_fn(
83
117
  do_3D: bool = False,
84
118
  channels: list[int] | None = None,
85
119
  channel_axis: int | None = None,
120
+ voxel_size: dict[str, float] | None = None,
86
121
  **cellpose_kwargs: Any,
87
122
  ) -> Callable[[np.ndarray], np.ndarray]:
88
123
  """Return a ready-to-use Cellpose function for ``tile_process``.
@@ -107,6 +142,15 @@ def cellpose_fn(
107
142
  channel_axis:
108
143
  *Cellpose 4 only.* Index of the channel axis in the input array.
109
144
  ``None`` → greyscale input.
145
+ voxel_size:
146
+ Physical voxel size as ``{"z": .., "y": .., "x": ..}``. When
147
+ ``do_3D`` is set and ``anisotropy`` is not already in
148
+ ``cellpose_kwargs``, it is derived from this via
149
+ :func:`cellpose_anisotropy` -- an explicit ``anisotropy=`` always
150
+ wins. The Snakemake workflow passes the image's own calibration
151
+ automatically; from the API,
152
+ :func:`patchworks.plugins.ome_zarr.read_pixel_size` reads it from a
153
+ store.
110
154
  **cellpose_kwargs:
111
155
  Extra kwargs forwarded to ``model.eval()``
112
156
  (e.g. ``flow_threshold``, ``cellprob_threshold``, ``anisotropy``).
@@ -128,15 +172,31 @@ def cellpose_fn(
128
172
  >>> fn = cellpose_fn("nuclei", diameter=15)
129
173
  >>> result = tile_process("image.zarr", fn, channel=1)
130
174
 
131
- 3-D with anisotropy:
175
+ 3-D with an explicit anisotropy:
132
176
 
133
177
  >>> fn = cellpose_fn("cyto3", gpu=True, do_3D=True, anisotropy=3.0, diameter=20)
134
178
  >>> from functools import partial
135
179
  >>> from patchworks import auto_tile_shape_cellpose, tile_process
136
180
  >>> tile_fn = partial(auto_tile_shape_cellpose, do_3D=True, use_gpu=True, diameter=20)
137
181
  >>> result = tile_process("image.zarr", fn, tile_shape=tile_fn, overlap=10)
182
+
183
+ 3-D with anisotropy derived from the image's own calibration:
184
+
185
+ >>> fn = cellpose_fn("cyto3", gpu=True, do_3D=True, diameter=20,
186
+ ... voxel_size={"z": 0.24, "y": 0.10833, "x": 0.10833})
138
187
  """
139
188
  _require_cellpose()
189
+ if do_3D and voxel_size and "anisotropy" not in cellpose_kwargs:
190
+ anisotropy = cellpose_anisotropy(voxel_size)
191
+ if anisotropy is not None:
192
+ logger.info(
193
+ "anisotropy derived from image calibration: %.4g "
194
+ "(z=%.4g, lateral=%.4g)",
195
+ anisotropy,
196
+ voxel_size.get("z"),
197
+ voxel_size.get("x") or voxel_size.get("y"),
198
+ )
199
+ cellpose_kwargs = {**cellpose_kwargs, "anisotropy": anisotropy}
140
200
  cfg = _make_config(
141
201
  model, gpu, channels, channel_axis, diameter, do_3D, **cellpose_kwargs
142
202
  )
@@ -0,0 +1,49 @@
1
+ """Tests for the cellpose plugin's anisotropy handling.
2
+
3
+ cellpose itself is not a test dependency (heavy, GPU-oriented), and
4
+ cellpose_fn() calls _require_cellpose() as its very first line, so it can't
5
+ be exercised end-to-end here. cellpose_anisotropy() is a pure function with
6
+ no cellpose import at all, and is where the actual math lives -- that's
7
+ what's covered directly. The wiring that fills cellpose_fn's voxel_size
8
+ parameter in from the image's calibration (workflow/scripts/_pw.py's
9
+ _with_voxel_size) is covered in tests/test_pw.py.
10
+ """
11
+
12
+ import inspect
13
+
14
+
15
+ def test_cellpose_anisotropy_from_calibration():
16
+ from patchworks.plugins.cellpose import cellpose_anisotropy
17
+
18
+ calibration = {"z": 0.24, "y": 0.10833, "x": 0.10833}
19
+ assert cellpose_anisotropy(calibration) == 0.24 / 0.10833
20
+
21
+
22
+ def test_cellpose_anisotropy_falls_back_to_y_when_x_is_missing():
23
+ from patchworks.plugins.cellpose import cellpose_anisotropy
24
+
25
+ assert cellpose_anisotropy({"z": 0.2, "y": 0.1}) == 2.0
26
+
27
+
28
+ def test_cellpose_anisotropy_missing_calibration_returns_none():
29
+ from patchworks.plugins.cellpose import cellpose_anisotropy
30
+
31
+ assert cellpose_anisotropy({}) is None
32
+ assert cellpose_anisotropy({"z": 0.2}) is None # no lateral size at all
33
+ assert cellpose_anisotropy({"x": 0.1}) is None # no z size
34
+
35
+
36
+ def test_cellpose_fn_declares_a_voxel_size_parameter():
37
+ """workflow/scripts/_pw.py's _with_voxel_size() finds this by signature
38
+
39
+ inspection to decide whether to fill it in -- if this parameter were
40
+ ever renamed, that wiring would silently stop working rather than error.
41
+ """
42
+ from patchworks.plugins.cellpose import cellpose_fn
43
+
44
+ params = inspect.signature(cellpose_fn).parameters
45
+ assert "voxel_size" in params
46
+ # anisotropy itself goes through **cellpose_kwargs (it's a cellpose
47
+ # model.eval() argument, not a patchworks-specific one) -- only the raw
48
+ # calibration is a named parameter.
49
+ assert "anisotropy" not in params
@@ -0,0 +1,57 @@
1
+ """Tests for workflow/scripts/_pw.py's config-to-segmentation-function wiring."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(
7
+ 0, str(Path(__file__).resolve().parents[1] / "workflow" / "scripts")
8
+ )
9
+
10
+ import numpy as np # noqa: E402
11
+
12
+
13
+ def test_with_voxel_size_fills_in_cellposes_calibration(tmp_path):
14
+ """The same auto-fill a custom function's voxel_size gets (see
15
+
16
+ _with_voxel_size's docstring) must also reach cellpose_fn, or do_3D
17
+ silently assumes isotropic voxels -- fragmenting objects across z for
18
+ any real (anisotropic) calibration. This checks the actual calibration
19
+ read + injection against a real store, not just that the parameter
20
+ exists (see test_cellpose.py for that).
21
+ """
22
+ from _pw import _with_voxel_size
23
+ from patchworks.plugins.cellpose import cellpose_fn
24
+ from patchworks.plugins.ome_zarr import to_ome_zarr
25
+
26
+ arr = np.zeros((4, 8, 8), dtype="uint16")
27
+ to_ome_zarr(
28
+ arr,
29
+ str(tmp_path / "image.zarr"),
30
+ axes="zyx",
31
+ pixel_size={"z": 0.24, "y": 0.10833, "x": 0.10833},
32
+ n_levels=1,
33
+ progress=False,
34
+ )
35
+ cfg = {"work_dir": str(tmp_path)}
36
+
37
+ kwargs = _with_voxel_size(cellpose_fn, {}, cfg)
38
+
39
+ assert kwargs["voxel_size"]["z"] == 0.24
40
+ assert kwargs["voxel_size"]["x"] == 0.10833
41
+
42
+
43
+ def test_with_voxel_size_never_overrides_an_explicit_value(tmp_path):
44
+ """An explicit voxel_size must win, and win *cheaply*: no image.zarr
45
+
46
+ exists in work_dir at all here, so if this read past the early return it
47
+ would raise, not just return the wrong value.
48
+ """
49
+ from _pw import _with_voxel_size
50
+ from patchworks.plugins.cellpose import cellpose_fn
51
+
52
+ cfg = {"work_dir": str(tmp_path)}
53
+ explicit = {"z": 1.0, "y": 1.0, "x": 1.0}
54
+
55
+ kwargs = _with_voxel_size(cellpose_fn, {"voxel_size": explicit}, cfg)
56
+
57
+ assert kwargs["voxel_size"] == explicit
@@ -188,6 +188,32 @@ def test_relate_script_has_the_real_bookkeeping():
188
188
  assert "openpyxl" in src
189
189
 
190
190
 
191
+ def test_view_script_loads_every_label_by_default():
192
+ """view.py must not override labels=, or auto-load stops working.
193
+
194
+ view_in_napari's labels=None default is what auto-loads every label
195
+ group under <image>/labels/<name>/ as its own layer -- passing an
196
+ explicit labels= here would silently drop that and show only one.
197
+ """
198
+ src = (_workflow_dir() / "scripts" / "view.py").read_text()
199
+ assert "from patchworks.plugins.napari import view_in_napari" in src
200
+ assert "labels=" not in src
201
+
202
+
203
+ def test_viewer_is_a_separate_opt_in_pixi_environment():
204
+ """napari's Qt/GUI deps must stay out of the default headless env.
205
+
206
+ Adding them to the default `[pypi-dependencies]` would pull heavy GUI
207
+ dependencies into every SLURM job's environment for a feature only used
208
+ interactively.
209
+ """
210
+ src = (_workflow_dir() / "pixi.toml").read_text()
211
+ default_deps = src.split("[pypi-dependencies]")[1].split("[feature")[0]
212
+ assert "napari" not in default_deps
213
+ assert 'viewer = { features = ["viewer"] }' in src
214
+ assert 'extras = ["napari"]' in src
215
+
216
+
191
217
  def test_relate_writes_its_own_log():
192
218
  """relate.py runs via srun, not a Snakemake rule -- nothing else wires up
193
219
 
@@ -6,6 +6,9 @@
6
6
  # pixi run multi # run several segmentations + relate their labels (local)
7
7
  # pixi run multi-slurm # same, on SLURM
8
8
  # pixi run multi-dry # dry-run every segmentation in config/multi.yaml
9
+ # pixi install -e viewer && pixi run -e viewer napari <image.zarr>
10
+ # # open a store in napari, every label auto-loaded
11
+ # # (needs a display -- ssh -X or remote-desktop/VNC)
9
12
  #
10
13
  # On a cluster, put this project on a shared filesystem the compute nodes can
11
14
  # read: the SLURM jobs re-launch snakemake from this env's interpreter.
@@ -31,11 +34,21 @@ cellpose = ">=3.0,<4"
31
34
  [feature.cp4.pypi-dependencies]
32
35
  cellpose = ">=4"
33
36
 
34
- # Named environments: pixi install -e cellpose3 / -e cellpose4
37
+ # napari pulls in a lot of Qt/GUI dependencies that don't belong in the
38
+ # default headless SLURM environment, so it's a separate opt-in feature --
39
+ # pixi install -e viewer, not part of the default `pixi install`.
40
+ [feature.viewer.pypi-dependencies]
41
+ patchworks = { version = "*", extras = ["napari"] }
42
+
43
+ [feature.viewer.tasks]
44
+ napari = "python scripts/view.py"
45
+
46
+ # Named environments: pixi install -e cellpose3 / -e cellpose4 / -e viewer
35
47
  # The base patchworks[cellpose] (>=3.0) is further constrained by each feature.
36
48
  [environments]
37
49
  cellpose3 = { features = ["cp3"], solve-group = "cp3" }
38
50
  cellpose4 = { features = ["cp4"], solve-group = "cp4" }
51
+ viewer = { features = ["viewer"] }
39
52
 
40
53
  [tasks]
41
54
  # --rerun-triggers mtime: don't redo finished steps (e.g. the conversion) just
@@ -425,6 +425,12 @@ def _build_method_fn(cfg):
425
425
  # Cellpose where they are. setdefault so an explicit
426
426
  # cellpose.channel_axis in the config still wins.
427
427
  extra.setdefault("channel_axis", 0)
428
+ # do_3D without anisotropy assumes isotropic voxels, which fragments
429
+ # objects across z for any real (anisotropic) calibration -- fill it
430
+ # in from the image's own calibration the same way a custom
431
+ # function's voxel_size gets filled in, unless the config already
432
+ # set anisotropy explicitly.
433
+ extra = _with_voxel_size(cellpose_fn, extra, cfg)
428
434
  return cellpose_fn(
429
435
  cp.get("model", "cyto3"),
430
436
  gpu=cp.get("gpu", True),
@@ -155,9 +155,7 @@ def run_relations(
155
155
  )
156
156
 
157
157
  ws_b = wb.create_sheet(title=b_name[:31])
158
- ws_b.append(
159
- [f"{b_name}_id", f"{a_name}_count", "total_overlap_voxels"]
160
- )
158
+ ws_b.append([f"{b_name}_id", f"{a_name}_count", "total_overlap_voxels"])
161
159
  for b_id in b_ids:
162
160
  agg = per_b[b_id]
163
161
  ws_b.append([b_id, agg["count"], agg["overlap_voxels"]])
@@ -356,7 +356,9 @@ def _resolve_shared_tile_shape(
356
356
  gpu_memory=gpu_bytes,
357
357
  n_channels=n_channels,
358
358
  )
359
- candidates.append(tuple(int(x) for x in sizer(image.shape, image.dtype)))
359
+ candidates.append(
360
+ tuple(int(x) for x in sizer(image.shape, image.dtype))
361
+ )
360
362
 
361
363
  tile_shape = min(candidates, key=lambda t: int(np.prod(t)))
362
364
  print(
@@ -0,0 +1,47 @@
1
+ """Open an OME-ZARR store in napari, auto-loading every label group.
2
+
3
+ Usage:
4
+ pixi run -e viewer napari /path/to/image.zarr
5
+ pixi run -e viewer napari /path/to/image.zarr --channel 1
6
+
7
+ Needs the `viewer` pixi environment (`pixi install -e viewer`) -- napari's
8
+ Qt/GUI dependencies live there, not in the default headless workflow env --
9
+ and a display: an X11-forwarded SSH session (`ssh -X`) or a remote-desktop/
10
+ VNC session. It cannot open over a plain headless SSH session.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+
17
+ from patchworks.plugins.napari import view_in_napari
18
+
19
+
20
+ def main() -> None:
21
+ parser = argparse.ArgumentParser(description=__doc__)
22
+ parser.add_argument(
23
+ "image", help="OME-ZARR store to open, e.g. work_dir/image.zarr"
24
+ )
25
+ parser.add_argument(
26
+ "--channel",
27
+ type=int,
28
+ default=None,
29
+ help="show only this channel (default: every channel)",
30
+ )
31
+ parser.add_argument(
32
+ "--no-glasbey",
33
+ action="store_true",
34
+ help="use napari's default label colours instead of glasbey",
35
+ )
36
+ args = parser.parse_args()
37
+
38
+ # Leaving the labels argument at its default auto-loads every label
39
+ # group under <image>/labels/<name>/ as its own Labels layer -- exactly
40
+ # what's wanted here, so there is nothing further to wire up.
41
+ view_in_napari(
42
+ args.image, channel=args.channel, glasbey=not args.no_glasbey
43
+ )
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
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