patchworks 2.5.0__tar.gz → 2.5.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-2.5.0 → patchworks-2.5.1}/PKG-INFO +1 -1
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_distributed.py +16 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/plugins/dog.py +64 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_dog.py +53 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/.github/workflows/docs.yml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/.github/workflows/lint.yml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/.github/workflows/release.yml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/.gitignore +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/LICENSE +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/README.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/cliff.toml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/chunks.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/cluster.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/io.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/postprocess.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/relabel.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/api/tile_process.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/assets/logo.png +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/custom.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/custom_method.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/dog.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/dog.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/stardist.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/getting_started.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/label_relations.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/measurements.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/merging.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/performance.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/snakemake.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/guide/tiling.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/docs/index.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/mkdocs.yml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/pyproject.toml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/__init__.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_chunks.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_core.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_io.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_merge.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_notify.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_progress.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/_relations.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_allocation.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_core.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_distributed.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_gpu.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_napari.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_notify.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_occupancy.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_ome_zarr.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_postprocess.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_progress.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_relations.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/tests/test_run_multi.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/README.md +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/Snakefile +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/config/common.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/config/config.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/config/multi.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/pixi.toml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/rules/common.smk +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/rules/convert.smk +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/rules/merge.smk +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/rules/segment.smk +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/_pw.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/build_occupancy.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/convert.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/merge.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/run_multi.py +0 -0
- {patchworks-2.5.0 → patchworks-2.5.1}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -213,6 +213,22 @@ def stage_tile(
|
|
|
213
213
|
trims.append((s.start - lo, hi - s.stop))
|
|
214
214
|
block = np.asarray(image[tuple(expanded)])
|
|
215
215
|
out = np.asarray(fn(block))
|
|
216
|
+
if out.shape != block.shape:
|
|
217
|
+
# Caught here rather than 6 frames deep in zarr's codec pipeline as
|
|
218
|
+
# "could not broadcast input array from shape (13,1020,1020) into
|
|
219
|
+
# shape (14,1024,1024)", which says nothing about which function is
|
|
220
|
+
# at fault. A segmentation function must label the voxels it was
|
|
221
|
+
# given: the halo trim and the destination slice are both computed
|
|
222
|
+
# from the tile's geometry, so a different shape has no defined
|
|
223
|
+
# placement.
|
|
224
|
+
name = getattr(fn, "__name__", type(fn).__name__)
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"segmentation function {name!r} returned shape {out.shape} for "
|
|
227
|
+
f"a tile of shape {block.shape} (tile {index}). It must return "
|
|
228
|
+
"one label per input voxel. Some deconvolution backends crop "
|
|
229
|
+
"their output -- pad or centre it back to the input shape before "
|
|
230
|
+
"returning."
|
|
231
|
+
)
|
|
216
232
|
sel = tuple(
|
|
217
233
|
slice(left, out.shape[i] - right)
|
|
218
234
|
for i, (left, right) in enumerate(trims)
|
|
@@ -238,6 +238,61 @@ def _run(block: np.ndarray, dog_dict: dict[str, Any]) -> np.ndarray:
|
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
|
|
241
|
+
def _restore_shape(arr: np.ndarray, shape: tuple[int, ...]) -> np.ndarray:
|
|
242
|
+
"""Centre *arr* back into an array of *shape*, cropping or edge-padding.
|
|
243
|
+
|
|
244
|
+
Deconvolution must not change the field of view: patchworks writes the
|
|
245
|
+
result into a destination slice derived from the tile's geometry, so one
|
|
246
|
+
label per input voxel is required.
|
|
247
|
+
|
|
248
|
+
Centring is the right correction for a symmetric crop, which is what
|
|
249
|
+
apodisation produces. The discrepancies observed are small (a voxel in z,
|
|
250
|
+
a few in x/y) and land inside the halo, which is discarded anyway -- so
|
|
251
|
+
the labels that survive the trim are unaffected. It is logged at WARNING
|
|
252
|
+
with the exact shapes so a larger, non-symmetric crop cannot pass
|
|
253
|
+
silently.
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
arr : np.ndarray
|
|
258
|
+
The deconvolved volume.
|
|
259
|
+
shape : tuple of int
|
|
260
|
+
The shape it must be returned at (the input tile's).
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
np.ndarray
|
|
265
|
+
An array of exactly *shape*.
|
|
266
|
+
|
|
267
|
+
Examples
|
|
268
|
+
--------
|
|
269
|
+
>>> import numpy as np
|
|
270
|
+
>>> _restore_shape(np.ones((13, 1020)), (14, 1024)).shape
|
|
271
|
+
(14, 1024)
|
|
272
|
+
"""
|
|
273
|
+
logger.warning(
|
|
274
|
+
"deconvolution returned %s for a %s input; re-centring to the input "
|
|
275
|
+
"shape. patchworks needs one label per input voxel. A large or "
|
|
276
|
+
"asymmetric difference here would shift labels -- check the PSF and "
|
|
277
|
+
"voxel sizes if this is more than a few voxels.",
|
|
278
|
+
arr.shape,
|
|
279
|
+
shape,
|
|
280
|
+
)
|
|
281
|
+
# Crop first, so an axis that grew is handled before padding the rest.
|
|
282
|
+
crop = tuple(
|
|
283
|
+
slice((a - s) // 2, (a - s) // 2 + s) if a > s else slice(None)
|
|
284
|
+
for a, s in zip(arr.shape, shape)
|
|
285
|
+
)
|
|
286
|
+
arr = arr[crop]
|
|
287
|
+
pad = tuple(
|
|
288
|
+
((s - a) // 2, s - a - (s - a) // 2) if a < s else (0, 0)
|
|
289
|
+
for a, s in zip(arr.shape, shape)
|
|
290
|
+
)
|
|
291
|
+
if any(lo or hi for lo, hi in pad):
|
|
292
|
+
arr = np.pad(arr, pad, mode="edge")
|
|
293
|
+
return arr
|
|
294
|
+
|
|
295
|
+
|
|
241
296
|
def _segment_once(
|
|
242
297
|
block: np.ndarray, dog_dict: dict[str, Any], use_gpu: bool
|
|
243
298
|
) -> np.ndarray:
|
|
@@ -253,7 +308,16 @@ def _segment_once(
|
|
|
253
308
|
# in the per-tile timing that tile_process logs.
|
|
254
309
|
from pycudadecon import decon
|
|
255
310
|
|
|
311
|
+
before = img.shape
|
|
256
312
|
img = decon(images=img, **decon_kwargs)
|
|
313
|
+
if img.shape != before:
|
|
314
|
+
# cudaDecon returns a slightly smaller volume for some input
|
|
315
|
+
# sizes (e.g. (14,1024,1024) -> (13,1020,1020) on an edge tile).
|
|
316
|
+
# patchworks needs one label per input voxel: the halo trim and
|
|
317
|
+
# the destination slice are both derived from the tile geometry,
|
|
318
|
+
# so a shrunken result has nowhere to go and used to surface as
|
|
319
|
+
# an unreadable broadcast error from inside zarr.
|
|
320
|
+
img = _restore_shape(img, before)
|
|
257
321
|
|
|
258
322
|
if use_gpu:
|
|
259
323
|
import cupy as cp
|
|
@@ -107,3 +107,56 @@ def test_explicit_decon_kwargs_win_over_the_calibration(monkeypatch):
|
|
|
107
107
|
assert captured["dxdata"] == 0.1 # filled from the calibration
|
|
108
108
|
assert captured["dzdata"] == 0.2
|
|
109
109
|
assert captured["dzpsf"] == 0.2
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_restore_shape_recentres_a_cropped_decon():
|
|
113
|
+
"""cudaDecon can hand back a smaller volume than it was given.
|
|
114
|
+
|
|
115
|
+
Observed on a real edge tile: (14, 1024, 1024) in, (13, 1020, 1020) out.
|
|
116
|
+
patchworks needs one label per input voxel, so the field of view has to be
|
|
117
|
+
restored before the DoG step.
|
|
118
|
+
"""
|
|
119
|
+
from patchworks.plugins.dog import _restore_shape
|
|
120
|
+
|
|
121
|
+
arr = np.arange(13 * 1020 * 1020, dtype="float32").reshape(13, 1020, 1020)
|
|
122
|
+
out = _restore_shape(arr, (14, 1024, 1024))
|
|
123
|
+
assert out.shape == (14, 1024, 1024)
|
|
124
|
+
# The original content is preserved, centred, not resampled.
|
|
125
|
+
assert np.array_equal(out[0:13, 2:1022, 2:1022], arr)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_restore_shape_handles_growth_and_exact_fit():
|
|
129
|
+
"""It must be a no-op when shapes already match, and crop when larger."""
|
|
130
|
+
from patchworks.plugins.dog import _restore_shape
|
|
131
|
+
|
|
132
|
+
same = np.ones((4, 8, 8), dtype="float32")
|
|
133
|
+
assert _restore_shape(same, (4, 8, 8)).shape == (4, 8, 8)
|
|
134
|
+
bigger = np.ones((6, 12, 12), dtype="float32")
|
|
135
|
+
assert _restore_shape(bigger, (4, 8, 8)).shape == (4, 8, 8)
|
|
136
|
+
# And a mix: one axis short, one long.
|
|
137
|
+
mixed = np.ones((2, 12), dtype="float32")
|
|
138
|
+
assert _restore_shape(mixed, (4, 8)).shape == (4, 8)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_stage_tile_rejects_a_shape_changing_function(tmp_path):
|
|
142
|
+
"""A wrong-shaped return must name the culprit, not blow up inside zarr.
|
|
143
|
+
|
|
144
|
+
This used to surface as "could not broadcast input array from shape
|
|
145
|
+
(13,1020,1020) into shape (14,1024,1024)" six frames deep in zarr's codec
|
|
146
|
+
pipeline, which says nothing about which function misbehaved.
|
|
147
|
+
"""
|
|
148
|
+
import dask.array as da
|
|
149
|
+
import pytest
|
|
150
|
+
|
|
151
|
+
from patchworks import create_stage, stage_tile
|
|
152
|
+
|
|
153
|
+
image = da.zeros((8, 32, 32), chunks=(4, 16, 16), dtype="uint16")
|
|
154
|
+
stage = str(tmp_path / "stage.zarr")
|
|
155
|
+
create_stage(stage, image.shape, (4, 16, 16))
|
|
156
|
+
|
|
157
|
+
def crops(block):
|
|
158
|
+
"""Stand-in for a deconvolution backend that trims its output."""
|
|
159
|
+
return np.zeros(tuple(s - 1 for s in block.shape), dtype="int32")
|
|
160
|
+
|
|
161
|
+
with pytest.raises(ValueError, match="one label per input voxel"):
|
|
162
|
+
stage_tile(image, crops, stage, 0, tile_shape=(4, 16, 16), overlap=2)
|
|
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
|
|
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
|