patchworks 2.0.0__tar.gz → 2.1.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-2.0.0 → patchworks-2.1.0}/PKG-INFO +1 -1
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/plugins/ome_zarr.md +4 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/dog.md +39 -9
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/custom_segmentation.md +16 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_distributed.py +33 -2
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/plugins/dog.py +84 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/plugins/ome_zarr.py +29 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_distributed.py +47 -0
- patchworks-2.1.0/tests/test_dog.py +109 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/config/config_cilia.yaml +8 -4
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/scripts/_pw.py +42 -1
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/scripts/prepare_tiles.py +10 -4
- patchworks-2.0.0/tests/test_dog.py +0 -49
- {patchworks-2.0.0 → patchworks-2.1.0}/.github/workflows/docs.yml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/.github/workflows/lint.yml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/.github/workflows/release.yml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/.gitignore +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/LICENSE +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/README.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/cliff.toml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/chunks.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/cluster.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/io.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/postprocess.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/relabel.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/api/tile_process.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/assets/logo.png +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/custom.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/custom_method.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/dog.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/stardist.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/getting_started.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/label_relations.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/measurements.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/merging.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/performance.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/snakemake.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/guide/tiling.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/docs/index.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/mkdocs.yml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/pyproject.toml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/__init__.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_chunks.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_core.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_io.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_merge.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/_relations.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_allocation.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_core.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_gpu.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_napari.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_occupancy.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_ome_zarr.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_postprocess.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/tests/test_relations.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/README.md +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/Snakefile +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/config/config.yaml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/config/multi.yaml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/pixi.toml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/rules/common.smk +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/rules/convert.smk +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/rules/merge.smk +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/rules/segment.smk +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/scripts/convert.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/scripts/merge.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/scripts/run_multi.py +0 -0
- {patchworks-2.0.0 → patchworks-2.1.0}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.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
|
|
@@ -25,6 +25,10 @@ resolution, matching anisotropic microscopy stacks.
|
|
|
25
25
|
|
|
26
26
|
::: patchworks.plugins.ome_zarr.register_labels
|
|
27
27
|
|
|
28
|
+
## read_pixel_size
|
|
29
|
+
|
|
30
|
+
::: patchworks.plugins.ome_zarr.read_pixel_size
|
|
31
|
+
|
|
28
32
|
## NGFF metadata layout
|
|
29
33
|
|
|
30
34
|
NGFF 0.4 is defined over zarr v2 and puts its keys at the top level; 0.5 is
|
|
@@ -65,18 +65,36 @@ Requires `cupy` (matching your CUDA version, e.g. `pip install cupy-cuda12x`)
|
|
|
65
65
|
|
|
66
66
|
## With deconvolution first
|
|
67
67
|
|
|
68
|
+
Let the voxel sizes come from the image rather than retyping them — the
|
|
69
|
+
lateral ones from X/Y, the axial ones from Z:
|
|
70
|
+
|
|
68
71
|
```python
|
|
72
|
+
from patchworks.plugins.ome_zarr import read_pixel_size
|
|
73
|
+
|
|
69
74
|
fn = dog_label_fn(
|
|
70
75
|
low_sigma=1.0, high_sigma=3.0, threshold=0.02,
|
|
71
|
-
decon_kwargs=dict(
|
|
72
|
-
|
|
73
|
-
dzpsf=z_scale, dzdata=z_scale,
|
|
74
|
-
wavelength=525, na=1.4, nimm=1.515,
|
|
75
|
-
),
|
|
76
|
+
decon_kwargs=dict(psf=psf, wavelength=525, na=1.4, nimm=1.515),
|
|
77
|
+
voxel_size=read_pixel_size(IMAGE), # -> dxdata/dzdata/dxpsf/dzpsf
|
|
76
78
|
)
|
|
77
79
|
result = tile_process(IMAGE, fn, tile_shape=(1, 1024, 1024), overlap=32)
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
Anything you set in `decon_kwargs` yourself wins, so a PSF sampled
|
|
83
|
+
differently from the data keeps its own sizes:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
decon_kwargs=dict(psf=psf, dxpsf=0.05, dzpsf=0.1, wavelength=525, ...)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
!!! warning "A wrong voxel size does not fail loudly"
|
|
90
|
+
Deconvolution given the wrong sampling still runs and still returns an
|
|
91
|
+
image — just a subtly wrong one. That is the reason to derive these from
|
|
92
|
+
the store's own calibration instead of keeping a second copy in a config
|
|
93
|
+
that can drift.
|
|
94
|
+
|
|
95
|
+
`read_pixel_size` returns `{}` for an uncalibrated store; then nothing is
|
|
96
|
+
filled in and you must supply the sizes yourself.
|
|
97
|
+
|
|
80
98
|
!!! note "Deconvolution always needs a GPU"
|
|
81
99
|
`pycudadecon` is CUDA-only, independent of `dog_label_fn`'s own `use_gpu`
|
|
82
100
|
flag (which only picks the backend for the blur/label steps). A SLURM job
|
|
@@ -146,15 +164,27 @@ custom:
|
|
|
146
164
|
threshold: 0.02
|
|
147
165
|
decon_kwargs:
|
|
148
166
|
psf: "/path/to/psf.tif"
|
|
149
|
-
dxpsf: 0.1
|
|
150
|
-
dxdata: 0.1
|
|
151
|
-
dzpsf: 0.2
|
|
152
|
-
dzdata: 0.2
|
|
153
167
|
wavelength: 525
|
|
154
168
|
na: 1.4
|
|
155
169
|
nimm: 1.515
|
|
156
170
|
```
|
|
157
171
|
|
|
172
|
+
No voxel sizes: the workflow reads `image.zarr`'s own calibration and fills
|
|
173
|
+
in `dxdata`/`dxpsf` from X/Y and `dzdata`/`dzpsf` from Z. Set any of them in
|
|
174
|
+
`decon_kwargs` to override — for instance a PSF sampled finer than the data:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
dxpsf: 0.05
|
|
178
|
+
dzpsf: 0.1
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
!!! tip "This works for your own methods too"
|
|
182
|
+
The injection is not DoG-specific. Any `custom` function that declares a
|
|
183
|
+
`voxel_size` parameter receives `{"z": .., "y": .., "x": ..}` from the
|
|
184
|
+
store, so a method needing physical units never has to keep a second copy
|
|
185
|
+
of the calibration in its config. If the store is uncalibrated the
|
|
186
|
+
workflow says so and passes nothing.
|
|
187
|
+
|
|
158
188
|
Run it exactly like a Cellpose config:
|
|
159
189
|
|
|
160
190
|
```bash
|
|
@@ -118,6 +118,22 @@ def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
|
|
|
118
118
|
|
|
119
119
|
Using a GPU? Just let your framework see it — nothing extra needed.
|
|
120
120
|
|
|
121
|
+
!!! tip "Ask for the voxel size instead of configuring it"
|
|
122
|
+
Declare a `voxel_size` parameter and the workflow passes the image's own
|
|
123
|
+
NGFF calibration, `{"z": .., "y": .., "x": ..}` in micrometers:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
def segment(tile, *, voxel_size=None, min_diameter_um=5.0):
|
|
127
|
+
min_px = min_diameter_um / voxel_size["x"] # µm -> pixels
|
|
128
|
+
...
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
That keeps physical parameters tied to the image rather than duplicated
|
|
132
|
+
in a config that can drift away from it. Setting `voxel_size` in
|
|
133
|
+
`custom.kwargs` overrides it; an uncalibrated store passes nothing and
|
|
134
|
+
logs a warning. This is how the DoG plugin gets its deconvolution voxel
|
|
135
|
+
sizes.
|
|
136
|
+
|
|
121
137
|
!!! tip "You do not need to modify patchworks"
|
|
122
138
|
`method: "custom"` imports any `(tile) -> labels` callable, so a new
|
|
123
139
|
method is a module of your own and a config block — nothing in the
|
|
@@ -12,6 +12,7 @@ it into that store. Stitch the result with
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import itertools
|
|
15
|
+
import logging
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Callable, Sequence, Union
|
|
17
18
|
|
|
@@ -21,10 +22,16 @@ import zarr
|
|
|
21
22
|
from ._io import zarr_compressor_kwargs
|
|
22
23
|
from ._relabel import relabel_sequential_array
|
|
23
24
|
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
24
27
|
Overlap = Union[int, Sequence[int]]
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
def normalize_overlap(
|
|
30
|
+
def normalize_overlap(
|
|
31
|
+
overlap: Overlap,
|
|
32
|
+
ndim: int,
|
|
33
|
+
tile_shape: "Sequence[int] | None" = None,
|
|
34
|
+
) -> tuple[int, ...]:
|
|
28
35
|
"""Expand an overlap spec to one halo width per axis.
|
|
29
36
|
|
|
30
37
|
A scalar applies the same halo to every axis (the historical behaviour).
|
|
@@ -33,12 +40,21 @@ def normalize_overlap(overlap: Overlap, ndim: int) -> tuple[int, ...]:
|
|
|
33
40
|
``76 x 1084 x 1084`` to keep ``16 x 1024 x 1024`` -- 5.3x more voxels than
|
|
34
41
|
it uses, nearly all of it in z.
|
|
35
42
|
|
|
43
|
+
With *tile_shape*, an axis only one voxel thick gets **no** halo. There is
|
|
44
|
+
no context to gather along an axis the tile does not span, and a 2-D
|
|
45
|
+
method handed the extra planes would read them as channels. This is the
|
|
46
|
+
``tile_shape: "auto"`` + ``do_3D: false`` case, where tiles come out one
|
|
47
|
+
plane thick: a z-overlap of 4 would otherwise read 9 planes per tile to
|
|
48
|
+
keep 1.
|
|
49
|
+
|
|
36
50
|
Parameters
|
|
37
51
|
----------
|
|
38
52
|
overlap : int or sequence of int
|
|
39
53
|
Halo width, shared or per-axis.
|
|
40
54
|
ndim : int
|
|
41
55
|
Number of axes the halo is applied to.
|
|
56
|
+
tile_shape : sequence of int, optional
|
|
57
|
+
Tile extent per axis. Used to drop halos an axis has no room for.
|
|
42
58
|
|
|
43
59
|
Returns
|
|
44
60
|
-------
|
|
@@ -55,6 +71,21 @@ def normalize_overlap(overlap: Overlap, ndim: int) -> tuple[int, ...]:
|
|
|
55
71
|
)
|
|
56
72
|
if any(o < 0 for o in values):
|
|
57
73
|
raise ValueError(f"overlap must be non-negative, got {values}")
|
|
74
|
+
|
|
75
|
+
if tile_shape is not None:
|
|
76
|
+
clipped = tuple(
|
|
77
|
+
0 if int(t) <= 1 else o for o, t in zip(values, tile_shape)
|
|
78
|
+
)
|
|
79
|
+
if clipped != values:
|
|
80
|
+
dropped = [
|
|
81
|
+
i for i, (a, b) in enumerate(zip(values, clipped)) if a != b
|
|
82
|
+
]
|
|
83
|
+
logger.info(
|
|
84
|
+
"dropping the halo on axis %s: the tile is 1 voxel thick "
|
|
85
|
+
"there, so there is no context to read.",
|
|
86
|
+
dropped,
|
|
87
|
+
)
|
|
88
|
+
values = clipped
|
|
58
89
|
return values
|
|
59
90
|
|
|
60
91
|
|
|
@@ -173,7 +204,7 @@ def stage_tile(
|
|
|
173
204
|
"""
|
|
174
205
|
shape = image.shape
|
|
175
206
|
sl = spatial_tiles(shape, tile_shape)[index]
|
|
176
|
-
halo = normalize_overlap(overlap, len(sl))
|
|
207
|
+
halo = normalize_overlap(overlap, len(sl), tile_shape=tile_shape)
|
|
177
208
|
expanded, trims = [], []
|
|
178
209
|
for s, dim, ov in zip(sl, shape, halo):
|
|
179
210
|
lo = max(0, s.start - ov)
|
|
@@ -34,6 +34,7 @@ docs/guide/custom_segmentation.md), use the
|
|
|
34
34
|
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
|
+
import logging
|
|
37
38
|
from functools import partial
|
|
38
39
|
from typing import Any, Callable
|
|
39
40
|
|
|
@@ -41,6 +42,8 @@ import numpy as np
|
|
|
41
42
|
|
|
42
43
|
from .._gpu import free_gpu_caches, retry_on_oom
|
|
43
44
|
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
44
47
|
|
|
45
48
|
def _require_cupy():
|
|
46
49
|
"""Raise an actionable ImportError if cupy is not installed.
|
|
@@ -77,6 +80,64 @@ def _require_pycudadecon():
|
|
|
77
80
|
) from exc
|
|
78
81
|
|
|
79
82
|
|
|
83
|
+
def decon_voxel_kwargs(
|
|
84
|
+
voxel_size: dict[str, float], psf_voxel_size: dict[str, float] | None = None
|
|
85
|
+
) -> dict[str, float]:
|
|
86
|
+
"""pycudadecon's voxel-size arguments, derived from a calibration.
|
|
87
|
+
|
|
88
|
+
``dxdata``/``dzdata`` describe the **image**, so they come straight from
|
|
89
|
+
its calibration. ``dxpsf``/``dzpsf`` describe the **PSF file**, which is
|
|
90
|
+
only the same when the PSF was acquired on the same system at the same
|
|
91
|
+
sampling -- pass *psf_voxel_size* when it was not.
|
|
92
|
+
|
|
93
|
+
Getting these wrong does not fail loudly: deconvolution just returns a
|
|
94
|
+
subtly wrong result, which is why deriving them beats retyping them.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
voxel_size : dict
|
|
99
|
+
Image calibration, e.g. from
|
|
100
|
+
:func:`patchworks.plugins.ome_zarr.read_pixel_size`. Needs ``x`` (or
|
|
101
|
+
``y``) for the lateral size and ``z`` for the axial one.
|
|
102
|
+
psf_voxel_size : dict, optional
|
|
103
|
+
The PSF's own calibration. Defaults to *voxel_size*.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
dict
|
|
108
|
+
``dxdata``/``dzdata``/``dxpsf``/``dzpsf``, omitting any the
|
|
109
|
+
calibration cannot supply.
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
--------
|
|
113
|
+
>>> decon_voxel_kwargs({"z": 0.2, "y": 0.1, "x": 0.1})
|
|
114
|
+
{'dxdata': 0.1, 'dzdata': 0.2, 'dxpsf': 0.1, 'dzpsf': 0.2}
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def _lateral(cal):
|
|
118
|
+
x, y = cal.get("x"), cal.get("y")
|
|
119
|
+
if x and y and abs(x - y) > 1e-9 * max(x, y):
|
|
120
|
+
logger.warning(
|
|
121
|
+
"anisotropic lateral calibration (x=%s, y=%s); pycudadecon "
|
|
122
|
+
"takes a single lateral size, using x.",
|
|
123
|
+
x,
|
|
124
|
+
y,
|
|
125
|
+
)
|
|
126
|
+
return x or y
|
|
127
|
+
|
|
128
|
+
psf_cal = psf_voxel_size if psf_voxel_size is not None else voxel_size
|
|
129
|
+
out: dict[str, float] = {}
|
|
130
|
+
for key, value in (
|
|
131
|
+
("dxdata", _lateral(voxel_size)),
|
|
132
|
+
("dzdata", voxel_size.get("z")),
|
|
133
|
+
("dxpsf", _lateral(psf_cal)),
|
|
134
|
+
("dzpsf", psf_cal.get("z")),
|
|
135
|
+
):
|
|
136
|
+
if value:
|
|
137
|
+
out[key] = float(value)
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
80
141
|
def dog_label_fn(
|
|
81
142
|
low_sigma: float | tuple[float, ...],
|
|
82
143
|
high_sigma: float | tuple[float, ...],
|
|
@@ -84,6 +145,7 @@ def dog_label_fn(
|
|
|
84
145
|
*,
|
|
85
146
|
use_gpu: bool = False,
|
|
86
147
|
decon_kwargs: dict[str, Any] | None = None,
|
|
148
|
+
voxel_size: dict[str, float] | None = None,
|
|
87
149
|
) -> Callable[[np.ndarray], np.ndarray]:
|
|
88
150
|
"""Return a ready-to-use DoG labeler for ``tile_process``.
|
|
89
151
|
|
|
@@ -108,6 +170,15 @@ def dog_label_fn(
|
|
|
108
170
|
PSF support when deconvolving, so edge tiles don't get truncated
|
|
109
171
|
context.
|
|
110
172
|
|
|
173
|
+
``dxdata``/``dzdata``/``dxpsf``/``dzpsf`` are filled in from
|
|
174
|
+
*voxel_size* when you leave them out, so the voxel sizes cannot drift
|
|
175
|
+
away from the image they describe. Anything you set explicitly wins.
|
|
176
|
+
voxel_size:
|
|
177
|
+
Physical voxel size as ``{"z": .., "y": .., "x": ..}``. The Snakemake
|
|
178
|
+
workflow passes the image's own calibration automatically; from the
|
|
179
|
+
API, :func:`patchworks.plugins.ome_zarr.read_pixel_size` reads it from
|
|
180
|
+
a store.
|
|
181
|
+
|
|
111
182
|
Returns
|
|
112
183
|
-------
|
|
113
184
|
Callable[[ndarray], ndarray]
|
|
@@ -117,6 +188,19 @@ def dog_label_fn(
|
|
|
117
188
|
_require_cupy()
|
|
118
189
|
if decon_kwargs is not None:
|
|
119
190
|
_require_pycudadecon()
|
|
191
|
+
if voxel_size:
|
|
192
|
+
# Derived values fill gaps only -- an explicit dxpsf for a PSF
|
|
193
|
+
# sampled differently from the data must not be overwritten.
|
|
194
|
+
derived = decon_voxel_kwargs(voxel_size)
|
|
195
|
+
missing = {
|
|
196
|
+
k: v for k, v in derived.items() if k not in decon_kwargs
|
|
197
|
+
}
|
|
198
|
+
if missing:
|
|
199
|
+
logger.info(
|
|
200
|
+
"decon voxel sizes taken from the image calibration: %s",
|
|
201
|
+
missing,
|
|
202
|
+
)
|
|
203
|
+
decon_kwargs = {**decon_kwargs, **missing}
|
|
120
204
|
cfg = {
|
|
121
205
|
"low_sigma": low_sigma,
|
|
122
206
|
"high_sigma": high_sigma,
|
|
@@ -743,6 +743,35 @@ def _write_multiscales(
|
|
|
743
743
|
write_ngff_attrs(zarr.open_group(group_path, mode="a"), multiscales=[entry])
|
|
744
744
|
|
|
745
745
|
|
|
746
|
+
def read_pixel_size(store: Union[str, Path]) -> PixelSize:
|
|
747
|
+
"""Physical voxel size recorded in an OME-ZARR's level-0 metadata.
|
|
748
|
+
|
|
749
|
+
The calibration the conversion carried over from the source file, as
|
|
750
|
+
``{"z": .., "y": .., "x": ..}`` in micrometers. Axes left at scale 1.0
|
|
751
|
+
(uncalibrated) are omitted, so an empty dict means the store carries no
|
|
752
|
+
usable calibration.
|
|
753
|
+
|
|
754
|
+
Use this instead of retyping voxel sizes into a config: a deconvolution
|
|
755
|
+
told the wrong voxel size produces a plausible-looking but wrong result.
|
|
756
|
+
|
|
757
|
+
Parameters
|
|
758
|
+
----------
|
|
759
|
+
store : str or Path
|
|
760
|
+
Path of the OME-ZARR group.
|
|
761
|
+
|
|
762
|
+
Returns
|
|
763
|
+
-------
|
|
764
|
+
dict
|
|
765
|
+
``{axis: size}`` for calibrated spatial axes.
|
|
766
|
+
|
|
767
|
+
Examples
|
|
768
|
+
--------
|
|
769
|
+
>>> read_pixel_size("scan.zarr") # doctest: +SKIP
|
|
770
|
+
{'z': 0.2, 'y': 0.1, 'x': 0.1}
|
|
771
|
+
"""
|
|
772
|
+
return _read_zarr_calibration(store, "")
|
|
773
|
+
|
|
774
|
+
|
|
746
775
|
def _read_zarr_calibration(store: Union[str, Path], axes: str) -> PixelSize:
|
|
747
776
|
"""Read level-0 spatial scale from an existing OME-ZARR, if any.
|
|
748
777
|
|
|
@@ -41,6 +41,53 @@ def test_normalize_overlap_scalar_and_per_axis():
|
|
|
41
41
|
normalize_overlap([-1, 0, 0], 3)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def test_no_halo_on_a_one_voxel_thick_axis():
|
|
45
|
+
"""A 1-plane tile must get no halo on that axis.
|
|
46
|
+
|
|
47
|
+
`tile_shape: "auto"` with `do_3D: false` produces z=1 tiles, so a z-overlap
|
|
48
|
+
of 4 would read 9 planes to keep 1 (9x) -- and hand a 2-D method a stack it
|
|
49
|
+
would read as channels.
|
|
50
|
+
"""
|
|
51
|
+
assert normalize_overlap([4, 30, 30], 3, tile_shape=(1, 8000, 8000)) == (
|
|
52
|
+
0,
|
|
53
|
+
30,
|
|
54
|
+
30,
|
|
55
|
+
)
|
|
56
|
+
# A tile with room keeps its halo.
|
|
57
|
+
assert normalize_overlap([4, 30, 30], 3, tile_shape=(16, 1024, 1024)) == (
|
|
58
|
+
4,
|
|
59
|
+
30,
|
|
60
|
+
30,
|
|
61
|
+
)
|
|
62
|
+
# Without a tile_shape nothing is clipped (the historical behaviour).
|
|
63
|
+
assert normalize_overlap([4, 30, 30], 3) == (4, 30, 30)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_single_plane_tiles_segment_without_a_z_halo(tmp_path):
|
|
67
|
+
"""End to end: z=1 tiles must not pull neighbouring planes into the tile."""
|
|
68
|
+
img = np.zeros((4, 32), "uint16")
|
|
69
|
+
img[1, 4:10] = 500 # only plane 1 has signal
|
|
70
|
+
tile = (1, 16)
|
|
71
|
+
|
|
72
|
+
stage = str(tmp_path / "stage.zarr")
|
|
73
|
+
create_stage(stage, img.shape, tile)
|
|
74
|
+
seen = []
|
|
75
|
+
|
|
76
|
+
def _recording(block):
|
|
77
|
+
seen.append(block.shape)
|
|
78
|
+
return _fn(block)
|
|
79
|
+
|
|
80
|
+
for i in range(len(spatial_tiles(img.shape, tile))):
|
|
81
|
+
stage_tile(img, _recording, stage, i, tile_shape=tile, overlap=[3, 2])
|
|
82
|
+
|
|
83
|
+
assert all(s[0] == 1 for s in seen), (
|
|
84
|
+
f"a z=1 tile was handed multiple planes: {seen}"
|
|
85
|
+
)
|
|
86
|
+
written = np.asarray(zarr.open_group(stage, mode="r")["staged"])
|
|
87
|
+
assert (written[0] == 0).all(), "plane 0 has no signal and must stay empty"
|
|
88
|
+
assert (written[1, 4:10] > 0).all()
|
|
89
|
+
|
|
90
|
+
|
|
44
91
|
def test_auto_overlap_anisotropy_shrinks_z():
|
|
45
92
|
"""A 20x coarser z step needs 20x fewer planes for the same distance."""
|
|
46
93
|
assert auto_overlap(30) == 30 # unchanged without voxel_size
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Self-contained tests for the dog plugin. No frameworks, no fixtures."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _make_blob_image(shape=(1, 64, 64)):
|
|
7
|
+
img = np.zeros(shape, dtype="float32")
|
|
8
|
+
img[0, 28:36, 28:36] = 1.0
|
|
9
|
+
return img
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_dog_label_fn_cpu_finds_blob():
|
|
13
|
+
from patchworks.plugins.dog import dog_label_fn
|
|
14
|
+
|
|
15
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
16
|
+
labels = fn(_make_blob_image())
|
|
17
|
+
|
|
18
|
+
assert labels.shape == (1, 64, 64)
|
|
19
|
+
assert labels.dtype == np.int32
|
|
20
|
+
assert labels.max() >= 1 # the blob was detected
|
|
21
|
+
assert labels[0, 0, 0] == 0 # background stays unlabeled
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_segment_adapter_matches_factory():
|
|
25
|
+
# method: "custom" calls segment(tile, **kwargs) directly — must match
|
|
26
|
+
# dog_label_fn(**kwargs)(tile) exactly.
|
|
27
|
+
from patchworks.plugins.dog import dog_label_fn, segment
|
|
28
|
+
|
|
29
|
+
kwargs = dict(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
30
|
+
img = _make_blob_image()
|
|
31
|
+
|
|
32
|
+
via_adapter = segment(img, **kwargs)
|
|
33
|
+
via_factory = dog_label_fn(**kwargs)(img)
|
|
34
|
+
|
|
35
|
+
np.testing.assert_array_equal(via_adapter, via_factory)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_dog_label_fn_with_tile_process():
|
|
39
|
+
import dask.array as da
|
|
40
|
+
|
|
41
|
+
from patchworks import tile_process
|
|
42
|
+
from patchworks.plugins.dog import dog_label_fn
|
|
43
|
+
|
|
44
|
+
arr = da.from_array(_make_blob_image((1, 64, 64)), chunks=(1, 64, 64))
|
|
45
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
46
|
+
result = tile_process(arr, fn, overlap=4).compute()
|
|
47
|
+
|
|
48
|
+
assert result.shape == (1, 64, 64)
|
|
49
|
+
assert result.max() >= 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_decon_voxel_kwargs_from_calibration():
|
|
53
|
+
"""Voxel sizes come from the image, not from retyped config values.
|
|
54
|
+
|
|
55
|
+
A deconvolution told the wrong voxel size does not fail -- it returns a
|
|
56
|
+
subtly wrong result -- so deriving them is the point.
|
|
57
|
+
"""
|
|
58
|
+
from patchworks.plugins.dog import decon_voxel_kwargs
|
|
59
|
+
|
|
60
|
+
assert decon_voxel_kwargs({"z": 0.2, "y": 0.1, "x": 0.1}) == {
|
|
61
|
+
"dxdata": 0.1,
|
|
62
|
+
"dzdata": 0.2,
|
|
63
|
+
"dxpsf": 0.1,
|
|
64
|
+
"dzpsf": 0.2,
|
|
65
|
+
}
|
|
66
|
+
# A PSF sampled differently from the data keeps its own sizes.
|
|
67
|
+
assert decon_voxel_kwargs(
|
|
68
|
+
{"z": 0.2, "y": 0.1, "x": 0.1}, {"z": 0.1, "y": 0.05, "x": 0.05}
|
|
69
|
+
) == {"dxdata": 0.1, "dzdata": 0.2, "dxpsf": 0.05, "dzpsf": 0.1}
|
|
70
|
+
# An uncalibrated axis is simply omitted rather than guessed.
|
|
71
|
+
assert decon_voxel_kwargs({"y": 0.1, "x": 0.1}) == {
|
|
72
|
+
"dxdata": 0.1,
|
|
73
|
+
"dxpsf": 0.1,
|
|
74
|
+
}
|
|
75
|
+
assert decon_voxel_kwargs({}) == {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_explicit_decon_kwargs_win_over_the_calibration(monkeypatch):
|
|
79
|
+
"""Anything set by hand must survive; only gaps are filled."""
|
|
80
|
+
import sys
|
|
81
|
+
import types
|
|
82
|
+
|
|
83
|
+
from patchworks.plugins import dog
|
|
84
|
+
|
|
85
|
+
captured = {}
|
|
86
|
+
|
|
87
|
+
def _decon(images, **kwargs):
|
|
88
|
+
captured.update(kwargs)
|
|
89
|
+
return images
|
|
90
|
+
|
|
91
|
+
monkeypatch.setitem(
|
|
92
|
+
sys.modules, "pycudadecon", types.SimpleNamespace(decon=_decon)
|
|
93
|
+
)
|
|
94
|
+
monkeypatch.setattr(dog, "_require_pycudadecon", lambda: None)
|
|
95
|
+
|
|
96
|
+
fn = dog.dog_label_fn(
|
|
97
|
+
low_sigma=1.0,
|
|
98
|
+
high_sigma=3.0,
|
|
99
|
+
threshold=0.5,
|
|
100
|
+
# dxpsf set by hand: the PSF was sampled finer than the data
|
|
101
|
+
decon_kwargs={"psf": "psf.tif", "dxpsf": 0.05},
|
|
102
|
+
voxel_size={"z": 0.2, "y": 0.1, "x": 0.1},
|
|
103
|
+
)
|
|
104
|
+
fn(np.zeros((4, 8, 8), "uint16"))
|
|
105
|
+
|
|
106
|
+
assert captured["dxpsf"] == 0.05, "an explicit value must not be replaced"
|
|
107
|
+
assert captured["dxdata"] == 0.1 # filled from the calibration
|
|
108
|
+
assert captured["dzdata"] == 0.2
|
|
109
|
+
assert captured["dzpsf"] == 0.2
|
|
@@ -47,10 +47,14 @@ custom:
|
|
|
47
47
|
# use_gpu: true # optional: also run the blur/label step on GPU
|
|
48
48
|
decon_kwargs:
|
|
49
49
|
psf: "/path/to/psf.tif"
|
|
50
|
-
dxpsf
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
# dxdata/dzdata (and dxpsf/dzpsf) are filled from image.zarr's own
|
|
51
|
+
# calibration -- the lateral size from X/Y, the axial one from Z -- so
|
|
52
|
+
# they cannot drift away from the image they describe. A wrong voxel
|
|
53
|
+
# size does not fail loudly; it just deconvolves incorrectly.
|
|
54
|
+
# Set any of them here only to override, e.g. when the PSF was sampled
|
|
55
|
+
# differently from the data:
|
|
56
|
+
# dxpsf: 0.05
|
|
57
|
+
# dzpsf: 0.1
|
|
54
58
|
wavelength: 525
|
|
55
59
|
na: 1.4
|
|
56
60
|
nimm: 1.515
|
|
@@ -131,6 +131,46 @@ def load_tiles_json(path):
|
|
|
131
131
|
return json.loads(Path(path).read_text())
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def _with_voxel_size(fn, kwargs, cfg):
|
|
135
|
+
"""Pass the image's calibration to a function that asks for it.
|
|
136
|
+
|
|
137
|
+
A method that needs physical units -- a deconvolution's voxel sizes, a
|
|
138
|
+
diameter in micrometers -- should not have them retyped into the config
|
|
139
|
+
where they can drift away from the image. Any custom function that
|
|
140
|
+
declares a ``voxel_size`` parameter gets ``{"z": .., "y": .., "x": ..}``
|
|
141
|
+
from the store's own NGFF calibration, unless the config sets it.
|
|
142
|
+
|
|
143
|
+
Returns *kwargs* unchanged when the function does not take it, the config
|
|
144
|
+
already supplied it, or the store carries no calibration.
|
|
145
|
+
"""
|
|
146
|
+
import inspect
|
|
147
|
+
|
|
148
|
+
if "voxel_size" in kwargs:
|
|
149
|
+
return kwargs
|
|
150
|
+
# A **kwargs passthrough names its real target (see dog.segment).
|
|
151
|
+
target = getattr(fn, "patchworks_kwargs_target", fn)
|
|
152
|
+
try:
|
|
153
|
+
params = inspect.signature(target).parameters
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
return kwargs
|
|
156
|
+
if "voxel_size" not in params:
|
|
157
|
+
return kwargs
|
|
158
|
+
|
|
159
|
+
from patchworks.plugins.ome_zarr import read_pixel_size
|
|
160
|
+
|
|
161
|
+
store = str(Path(cfg["work_dir"]) / "image.zarr")
|
|
162
|
+
calibration = read_pixel_size(store)
|
|
163
|
+
if not calibration:
|
|
164
|
+
logging.getLogger(__name__).warning(
|
|
165
|
+
"%s takes voxel_size but %s carries no calibration; set the "
|
|
166
|
+
"physical sizes in the config instead.",
|
|
167
|
+
getattr(target, "__name__", target),
|
|
168
|
+
store,
|
|
169
|
+
)
|
|
170
|
+
return kwargs
|
|
171
|
+
return {**kwargs, "voxel_size": calibration}
|
|
172
|
+
|
|
173
|
+
|
|
134
174
|
def _unknown_kwargs(fn, provided, *, skip=()) -> list[str]:
|
|
135
175
|
"""Keys in *provided* that *fn* would reject, ignoring *skip*.
|
|
136
176
|
|
|
@@ -314,7 +354,8 @@ def _build_method_fn(cfg):
|
|
|
314
354
|
importlib.import_module(spec["module"]),
|
|
315
355
|
spec.get("function", "segment"),
|
|
316
356
|
)
|
|
317
|
-
kwargs = spec.get("kwargs") or {}
|
|
357
|
+
kwargs = dict(spec.get("kwargs") or {})
|
|
358
|
+
kwargs = _with_voxel_size(fn, kwargs, cfg)
|
|
318
359
|
return partial(fn, **kwargs) if kwargs else fn
|
|
319
360
|
|
|
320
361
|
if method == "threshold":
|
|
@@ -85,7 +85,9 @@ if len(tile_shape) >= 3:
|
|
|
85
85
|
# A halo wider than the tile itself is not boundary context -- it means every
|
|
86
86
|
# tile re-reads and re-segments its neighbours. Catch it here rather than
|
|
87
87
|
# paying 5x the GPU time for results that get trimmed away.
|
|
88
|
-
overlap = normalize_overlap(
|
|
88
|
+
overlap = normalize_overlap(
|
|
89
|
+
cfg.get("overlap", 0), len(tile_shape), tile_shape=tile_shape
|
|
90
|
+
)
|
|
89
91
|
for axis, (ov, extent) in enumerate(zip(overlap, tile_shape)):
|
|
90
92
|
if ov >= extent:
|
|
91
93
|
raise ValueError(
|
|
@@ -93,9 +95,13 @@ for axis, (ov, extent) in enumerate(zip(overlap, tile_shape)):
|
|
|
93
95
|
f"would read past its neighbours. Use a per-axis overlap, e.g. "
|
|
94
96
|
f"overlap: {list(max(1, t // 4) for t in tile_shape)}"
|
|
95
97
|
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
# Clip the halo to the image the way stage_tile does, or a tile that already
|
|
99
|
+
# spans an axis reports a halo it will never actually read -- a tile covering
|
|
100
|
+
# the whole image would claim 5x while reading exactly 1x.
|
|
101
|
+
read = np.prod(
|
|
102
|
+
[min(t + 2 * o, s) for t, o, s in zip(tile_shape, overlap, image.shape)]
|
|
103
|
+
)
|
|
104
|
+
amplification = read / np.prod(tile_shape)
|
|
99
105
|
print(f"[patchworks] halo read amplification: {amplification:.2f}x")
|
|
100
106
|
|
|
101
107
|
tiles = spatial_tiles(image.shape, tile_shape)
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"""Self-contained tests for the dog plugin. No frameworks, no fixtures."""
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def _make_blob_image(shape=(1, 64, 64)):
|
|
7
|
-
img = np.zeros(shape, dtype="float32")
|
|
8
|
-
img[0, 28:36, 28:36] = 1.0
|
|
9
|
-
return img
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_dog_label_fn_cpu_finds_blob():
|
|
13
|
-
from patchworks.plugins.dog import dog_label_fn
|
|
14
|
-
|
|
15
|
-
fn = dog_label_fn(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
16
|
-
labels = fn(_make_blob_image())
|
|
17
|
-
|
|
18
|
-
assert labels.shape == (1, 64, 64)
|
|
19
|
-
assert labels.dtype == np.int32
|
|
20
|
-
assert labels.max() >= 1 # the blob was detected
|
|
21
|
-
assert labels[0, 0, 0] == 0 # background stays unlabeled
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_segment_adapter_matches_factory():
|
|
25
|
-
# method: "custom" calls segment(tile, **kwargs) directly — must match
|
|
26
|
-
# dog_label_fn(**kwargs)(tile) exactly.
|
|
27
|
-
from patchworks.plugins.dog import dog_label_fn, segment
|
|
28
|
-
|
|
29
|
-
kwargs = dict(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
30
|
-
img = _make_blob_image()
|
|
31
|
-
|
|
32
|
-
via_adapter = segment(img, **kwargs)
|
|
33
|
-
via_factory = dog_label_fn(**kwargs)(img)
|
|
34
|
-
|
|
35
|
-
np.testing.assert_array_equal(via_adapter, via_factory)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_dog_label_fn_with_tile_process():
|
|
39
|
-
import dask.array as da
|
|
40
|
-
|
|
41
|
-
from patchworks import tile_process
|
|
42
|
-
from patchworks.plugins.dog import dog_label_fn
|
|
43
|
-
|
|
44
|
-
arr = da.from_array(_make_blob_image((1, 64, 64)), chunks=(1, 64, 64))
|
|
45
|
-
fn = dog_label_fn(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
46
|
-
result = tile_process(arr, fn, overlap=4).compute()
|
|
47
|
-
|
|
48
|
-
assert result.shape == (1, 64, 64)
|
|
49
|
-
assert result.max() >= 1
|
|
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
|