patchworks 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patchworks-0.4.0/.github/workflows/lint.yml +23 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/.github/workflows/release.yml +3 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/PKG-INFO +70 -26
- {patchworks-0.2.0 → patchworks-0.4.0}/README.md +52 -25
- {patchworks-0.2.0 → patchworks-0.4.0}/cliff.toml +7 -4
- patchworks-0.4.0/docs/api/plugins/napari.md +7 -0
- patchworks-0.4.0/docs/api/plugins/ome_zarr.md +26 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/cellpose_2d.md +6 -5
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/cellpose_2d.py +4 -2
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/cellpose_3d.md +3 -2
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/cellpose_3d.py +9 -3
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/custom.md +23 -10
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/custom_method.py +1 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/standalone_merge.md +6 -2
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/stardist.md +5 -2
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/examples/stardist_2d.py +3 -1
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/getting_started.md +14 -10
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/guide/gpu_distributed.md +17 -11
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/guide/merging.md +4 -4
- patchworks-0.4.0/docs/guide/ome_zarr_napari.md +114 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/guide/pitfalls.md +8 -7
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/guide/skip_empty.md +12 -7
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/index.md +13 -2
- patchworks-0.4.0/mkdocs.yml +87 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/pyproject.toml +37 -31
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/__init__.py +7 -1
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/_chunks.py +20 -5
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/_cluster.py +5 -1
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/_core.py +111 -32
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/_io.py +24 -7
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/_merge.py +65 -18
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/_relabel.py +9 -3
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/plugins/cellpose.py +5 -1
- patchworks-0.4.0/src/patchworks/plugins/napari.py +167 -0
- patchworks-0.4.0/src/patchworks/plugins/ome_zarr.py +459 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/tests/test_core.py +37 -15
- patchworks-0.4.0/tests/test_napari.py +41 -0
- patchworks-0.4.0/tests/test_ome_zarr.py +95 -0
- patchworks-0.2.0/mkdocs.yml +0 -84
- {patchworks-0.2.0 → patchworks-0.4.0}/.github/workflows/docs.yml +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/.gitignore +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/api/chunks.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/api/cluster.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/api/io.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/api/relabel.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/api/tile_process.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/docs/guide/tiling.md +0 -0
- {patchworks-0.2.0 → patchworks-0.4.0}/src/patchworks/plugins/__init__.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ruff:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: ruff check
|
|
16
|
+
uses: astral-sh/ruff-action@v3
|
|
17
|
+
with:
|
|
18
|
+
args: "check"
|
|
19
|
+
|
|
20
|
+
- name: ruff format --check
|
|
21
|
+
uses: astral-sh/ruff-action@v3
|
|
22
|
+
with:
|
|
23
|
+
args: "format --check"
|
|
@@ -22,6 +22,9 @@ jobs:
|
|
|
22
22
|
with:
|
|
23
23
|
config: cliff.toml
|
|
24
24
|
args: --latest --strip header
|
|
25
|
+
env:
|
|
26
|
+
# Lets git-cliff resolve commit authors to GitHub handles/avatars.
|
|
27
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
25
28
|
|
|
26
29
|
- name: Create GitHub release
|
|
27
30
|
uses: softprops/action-gh-release@v2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -22,10 +22,25 @@ Requires-Dist: numpy>=1.24
|
|
|
22
22
|
Requires-Dist: scipy>=1.9
|
|
23
23
|
Requires-Dist: zarr>=2.14
|
|
24
24
|
Provides-Extra: all
|
|
25
|
+
Requires-Dist: bioio; extra == 'all'
|
|
26
|
+
Requires-Dist: bioio-bioformats; extra == 'all'
|
|
27
|
+
Requires-Dist: bioio-czi; extra == 'all'
|
|
28
|
+
Requires-Dist: bioio-lif; extra == 'all'
|
|
29
|
+
Requires-Dist: bioio-nd2; extra == 'all'
|
|
30
|
+
Requires-Dist: bioio-ome-tiff; extra == 'all'
|
|
31
|
+
Requires-Dist: bioio-tifffile; extra == 'all'
|
|
25
32
|
Requires-Dist: nvidia-ml-py; extra == 'all'
|
|
26
33
|
Requires-Dist: psutil; extra == 'all'
|
|
27
34
|
Requires-Dist: scikit-image; extra == 'all'
|
|
28
35
|
Requires-Dist: tqdm; extra == 'all'
|
|
36
|
+
Provides-Extra: bioio
|
|
37
|
+
Requires-Dist: bioio; extra == 'bioio'
|
|
38
|
+
Requires-Dist: bioio-bioformats; extra == 'bioio'
|
|
39
|
+
Requires-Dist: bioio-czi; extra == 'bioio'
|
|
40
|
+
Requires-Dist: bioio-lif; extra == 'bioio'
|
|
41
|
+
Requires-Dist: bioio-nd2; extra == 'bioio'
|
|
42
|
+
Requires-Dist: bioio-ome-tiff; extra == 'bioio'
|
|
43
|
+
Requires-Dist: bioio-tifffile; extra == 'bioio'
|
|
29
44
|
Provides-Extra: cellpose
|
|
30
45
|
Requires-Dist: cellpose>=3.0; extra == 'cellpose'
|
|
31
46
|
Provides-Extra: dev
|
|
@@ -42,10 +57,17 @@ Requires-Dist: nvidia-ml-py; extra == 'gpu'
|
|
|
42
57
|
Provides-Extra: io
|
|
43
58
|
Requires-Dist: psutil; extra == 'io'
|
|
44
59
|
Requires-Dist: tqdm; extra == 'io'
|
|
60
|
+
Provides-Extra: napari
|
|
61
|
+
Requires-Dist: napari[all]; extra == 'napari'
|
|
45
62
|
Description-Content-Type: text/markdown
|
|
46
63
|
|
|
47
64
|
# patchworks
|
|
48
65
|
|
|
66
|
+
[](https://pypi.org/project/patchworks/)
|
|
67
|
+
[](https://pypi.org/project/patchworks/)
|
|
68
|
+
[](https://opensource.org/licenses/MIT)
|
|
69
|
+
[](https://imcf.one/patchworks/)
|
|
70
|
+
|
|
49
71
|
> Tiled processing of arbitrarily large images — any image, any function.
|
|
50
72
|
|
|
51
73
|
```
|
|
@@ -75,9 +97,15 @@ Optional extras:
|
|
|
75
97
|
```bash
|
|
76
98
|
pip install "patchworks[gpu]" # GPU VRAM querying (nvidia-ml-py)
|
|
77
99
|
pip install "patchworks[cellpose]" # Cellpose plugin
|
|
78
|
-
pip install "patchworks[
|
|
100
|
+
pip install "patchworks[bioio]" # convert any image format to OME-ZARR
|
|
101
|
+
pip install "patchworks[napari]" # interactive napari viewer plugin
|
|
102
|
+
pip install "patchworks[all]" # Everything (except napari GUI)
|
|
79
103
|
```
|
|
80
104
|
|
|
105
|
+
> `bioio` reads CZI/LIF/ND2/OME-TIFF/… The `[bioio]` extra bundles the common
|
|
106
|
+
> native readers (`bioio-nd2`, `bioio-ome-tiff`, `bioio-czi`, `bioio-tifffile`,
|
|
107
|
+
> `bioio-lif`) plus `bioio-bioformats`, the Bio-Formats catch-all reader (JVM).
|
|
108
|
+
|
|
81
109
|
---
|
|
82
110
|
|
|
83
111
|
## Quick start — 5 lines
|
|
@@ -85,11 +113,14 @@ pip install "patchworks[all]" # Everything
|
|
|
85
113
|
```python
|
|
86
114
|
from patchworks import tile_process
|
|
87
115
|
|
|
116
|
+
|
|
88
117
|
def my_fn(tile):
|
|
89
118
|
from skimage.filters import threshold_otsu
|
|
90
119
|
from skimage.measure import label
|
|
120
|
+
|
|
91
121
|
return label(tile > threshold_otsu(tile)).astype("int32")
|
|
92
122
|
|
|
123
|
+
|
|
93
124
|
result = tile_process("image.zarr", my_fn, compute=True)
|
|
94
125
|
```
|
|
95
126
|
|
|
@@ -107,10 +138,11 @@ from patchworks.plugins.cellpose import cellpose_fn
|
|
|
107
138
|
fn = cellpose_fn("cyto3", gpu=True, diameter=30)
|
|
108
139
|
|
|
109
140
|
tile_process(
|
|
110
|
-
"image.zarr",
|
|
141
|
+
"image.zarr",
|
|
142
|
+
fn,
|
|
111
143
|
tile_shape=(1, 2048, 2048), # one z-slice per tile
|
|
112
|
-
overlap=20,
|
|
113
|
-
write_to="labels.zarr",
|
|
144
|
+
overlap=20, # gives boundary cells enough context
|
|
145
|
+
write_to="labels.zarr", # stream directly to disk — no RAM accumulation
|
|
114
146
|
progress=True,
|
|
115
147
|
)
|
|
116
148
|
```
|
|
@@ -125,15 +157,22 @@ from patchworks import tile_process
|
|
|
125
157
|
|
|
126
158
|
model = StarDist2D.from_pretrained("2D_versatile_fluo")
|
|
127
159
|
|
|
160
|
+
|
|
128
161
|
def stardist_fn(tile):
|
|
129
162
|
img = tile[0] if tile.ndim == 3 and tile.shape[0] == 1 else tile
|
|
130
163
|
norm = img.astype("float32") / (img.max() or 1)
|
|
131
164
|
labels, _ = model.predict_instances(norm)
|
|
132
165
|
return labels.astype("int32")[None] if tile.ndim == 3 else labels.astype("int32")
|
|
133
166
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
|
|
168
|
+
tile_process(
|
|
169
|
+
"image.zarr",
|
|
170
|
+
stardist_fn,
|
|
171
|
+
tile_shape=(1, 1024, 1024),
|
|
172
|
+
overlap=32,
|
|
173
|
+
write_to="labels.zarr",
|
|
174
|
+
progress=True,
|
|
175
|
+
)
|
|
137
176
|
```
|
|
138
177
|
|
|
139
178
|
---
|
|
@@ -146,11 +185,13 @@ from scipy.ndimage import gaussian_filter
|
|
|
146
185
|
from skimage.measure import label
|
|
147
186
|
from patchworks import tile_process
|
|
148
187
|
|
|
188
|
+
|
|
149
189
|
def my_custom_fn(tile: np.ndarray) -> np.ndarray:
|
|
150
190
|
smoothed = gaussian_filter(tile.astype("float32"), sigma=1.5)
|
|
151
191
|
binary = smoothed > smoothed.mean()
|
|
152
192
|
return label(binary).astype("int32")
|
|
153
193
|
|
|
194
|
+
|
|
154
195
|
tile_process("image.zarr", my_custom_fn, tile_shape=(1, 512, 512))
|
|
155
196
|
```
|
|
156
197
|
|
|
@@ -174,11 +215,14 @@ from patchworks import estimate_empty_tiles, tile_process
|
|
|
174
215
|
info = estimate_empty_tiles("image.zarr", tile_shape=(120, 697, 697))
|
|
175
216
|
print(f"{info['empty_fraction']:.0%} tiles are background — will be skipped")
|
|
176
217
|
|
|
177
|
-
tile_process(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
tile_process(
|
|
219
|
+
"image.zarr",
|
|
220
|
+
fn,
|
|
221
|
+
tile_shape=(120, 697, 697),
|
|
222
|
+
skip_empty=True,
|
|
223
|
+
empty_threshold=info["threshold"],
|
|
224
|
+
write_to="labels.zarr",
|
|
225
|
+
)
|
|
182
226
|
```
|
|
183
227
|
|
|
184
228
|
### Distributed cluster for GPU
|
|
@@ -190,7 +234,8 @@ client, cluster = make_local_cluster(use_gpu=True)
|
|
|
190
234
|
try:
|
|
191
235
|
tile_process("image.zarr", fn, write_to="labels.zarr", progress=True)
|
|
192
236
|
finally:
|
|
193
|
-
client.close()
|
|
237
|
+
client.close()
|
|
238
|
+
cluster.close()
|
|
194
239
|
```
|
|
195
240
|
|
|
196
241
|
### Contiguous label numbering
|
|
@@ -198,9 +243,7 @@ finally:
|
|
|
198
243
|
```python
|
|
199
244
|
# Labels are globally unique by default, but may be gappy (block-encoded IDs).
|
|
200
245
|
# sequential_labels=True does a linear relabel O(voxels) — not O(n_tiles²).
|
|
201
|
-
tile_process("image.zarr", fn,
|
|
202
|
-
write_to="labels.zarr",
|
|
203
|
-
sequential_labels=True)
|
|
246
|
+
tile_process("image.zarr", fn, write_to="labels.zarr", sequential_labels=True)
|
|
204
247
|
```
|
|
205
248
|
|
|
206
249
|
### Use only the merge step (bring your own tiling)
|
|
@@ -215,8 +258,9 @@ from patchworks import merge_tile_labels
|
|
|
215
258
|
|
|
216
259
|
# Your own tiling + segmentation
|
|
217
260
|
image = da.from_zarr("image.zarr").rechunk((1, 1024, 1024))
|
|
218
|
-
labeled = image.map_blocks(
|
|
219
|
-
|
|
261
|
+
labeled = image.map_blocks(
|
|
262
|
+
my_segment_fn, dtype="int32", meta=np.empty((0,) * image.ndim, dtype="int32")
|
|
263
|
+
)
|
|
220
264
|
|
|
221
265
|
merged = merge_tile_labels(labeled, write_to="labels.zarr", progress=True)
|
|
222
266
|
```
|
|
@@ -257,13 +301,13 @@ tiles where the dask-image approach stalls.
|
|
|
257
301
|
|
|
258
302
|
## Known pitfalls (and how patchworks avoids them)
|
|
259
303
|
|
|
260
|
-
| Pitfall
|
|
261
|
-
|
|
262
|
-
| In-process Dask client
|
|
263
|
-
| 3-4× fn recompute during merge | Cellpose runs 3× per tile
|
|
264
|
-
| O(n²) sequential relabelling
|
|
265
|
-
| Wrong overlap boundary
|
|
266
|
-
| Persisting large arrays
|
|
304
|
+
| Pitfall | Symptom | How patchworks handles it |
|
|
305
|
+
| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------- |
|
|
306
|
+
| In-process Dask client | `FutureCancelledError: lost dependencies` | Detected at startup, raises immediately with fix instructions |
|
|
307
|
+
| 3-4× fn recompute during merge | Cellpose runs 3× per tile | Staging writes labels once, merge reads from disk |
|
|
308
|
+
| O(n²) sequential relabelling | Graph construction hangs at 1000+ tiles | Linear post-pass O(voxels) via `np.unique` + LUT |
|
|
309
|
+
| Wrong overlap boundary | Output shape mismatch | Always uses `boundary="none"` |
|
|
310
|
+
| Persisting large arrays | Worker OOM | Never persists; keeps dask graph lazy and streams |
|
|
267
311
|
|
|
268
312
|
---
|
|
269
313
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# patchworks
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/patchworks/)
|
|
4
|
+
[](https://pypi.org/project/patchworks/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://imcf.one/patchworks/)
|
|
7
|
+
|
|
3
8
|
> Tiled processing of arbitrarily large images — any image, any function.
|
|
4
9
|
|
|
5
10
|
```
|
|
@@ -29,9 +34,15 @@ Optional extras:
|
|
|
29
34
|
```bash
|
|
30
35
|
pip install "patchworks[gpu]" # GPU VRAM querying (nvidia-ml-py)
|
|
31
36
|
pip install "patchworks[cellpose]" # Cellpose plugin
|
|
32
|
-
pip install "patchworks[
|
|
37
|
+
pip install "patchworks[bioio]" # convert any image format to OME-ZARR
|
|
38
|
+
pip install "patchworks[napari]" # interactive napari viewer plugin
|
|
39
|
+
pip install "patchworks[all]" # Everything (except napari GUI)
|
|
33
40
|
```
|
|
34
41
|
|
|
42
|
+
> `bioio` reads CZI/LIF/ND2/OME-TIFF/… The `[bioio]` extra bundles the common
|
|
43
|
+
> native readers (`bioio-nd2`, `bioio-ome-tiff`, `bioio-czi`, `bioio-tifffile`,
|
|
44
|
+
> `bioio-lif`) plus `bioio-bioformats`, the Bio-Formats catch-all reader (JVM).
|
|
45
|
+
|
|
35
46
|
---
|
|
36
47
|
|
|
37
48
|
## Quick start — 5 lines
|
|
@@ -39,11 +50,14 @@ pip install "patchworks[all]" # Everything
|
|
|
39
50
|
```python
|
|
40
51
|
from patchworks import tile_process
|
|
41
52
|
|
|
53
|
+
|
|
42
54
|
def my_fn(tile):
|
|
43
55
|
from skimage.filters import threshold_otsu
|
|
44
56
|
from skimage.measure import label
|
|
57
|
+
|
|
45
58
|
return label(tile > threshold_otsu(tile)).astype("int32")
|
|
46
59
|
|
|
60
|
+
|
|
47
61
|
result = tile_process("image.zarr", my_fn, compute=True)
|
|
48
62
|
```
|
|
49
63
|
|
|
@@ -61,10 +75,11 @@ from patchworks.plugins.cellpose import cellpose_fn
|
|
|
61
75
|
fn = cellpose_fn("cyto3", gpu=True, diameter=30)
|
|
62
76
|
|
|
63
77
|
tile_process(
|
|
64
|
-
"image.zarr",
|
|
78
|
+
"image.zarr",
|
|
79
|
+
fn,
|
|
65
80
|
tile_shape=(1, 2048, 2048), # one z-slice per tile
|
|
66
|
-
overlap=20,
|
|
67
|
-
write_to="labels.zarr",
|
|
81
|
+
overlap=20, # gives boundary cells enough context
|
|
82
|
+
write_to="labels.zarr", # stream directly to disk — no RAM accumulation
|
|
68
83
|
progress=True,
|
|
69
84
|
)
|
|
70
85
|
```
|
|
@@ -79,15 +94,22 @@ from patchworks import tile_process
|
|
|
79
94
|
|
|
80
95
|
model = StarDist2D.from_pretrained("2D_versatile_fluo")
|
|
81
96
|
|
|
97
|
+
|
|
82
98
|
def stardist_fn(tile):
|
|
83
99
|
img = tile[0] if tile.ndim == 3 and tile.shape[0] == 1 else tile
|
|
84
100
|
norm = img.astype("float32") / (img.max() or 1)
|
|
85
101
|
labels, _ = model.predict_instances(norm)
|
|
86
102
|
return labels.astype("int32")[None] if tile.ndim == 3 else labels.astype("int32")
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
|
|
105
|
+
tile_process(
|
|
106
|
+
"image.zarr",
|
|
107
|
+
stardist_fn,
|
|
108
|
+
tile_shape=(1, 1024, 1024),
|
|
109
|
+
overlap=32,
|
|
110
|
+
write_to="labels.zarr",
|
|
111
|
+
progress=True,
|
|
112
|
+
)
|
|
91
113
|
```
|
|
92
114
|
|
|
93
115
|
---
|
|
@@ -100,11 +122,13 @@ from scipy.ndimage import gaussian_filter
|
|
|
100
122
|
from skimage.measure import label
|
|
101
123
|
from patchworks import tile_process
|
|
102
124
|
|
|
125
|
+
|
|
103
126
|
def my_custom_fn(tile: np.ndarray) -> np.ndarray:
|
|
104
127
|
smoothed = gaussian_filter(tile.astype("float32"), sigma=1.5)
|
|
105
128
|
binary = smoothed > smoothed.mean()
|
|
106
129
|
return label(binary).astype("int32")
|
|
107
130
|
|
|
131
|
+
|
|
108
132
|
tile_process("image.zarr", my_custom_fn, tile_shape=(1, 512, 512))
|
|
109
133
|
```
|
|
110
134
|
|
|
@@ -128,11 +152,14 @@ from patchworks import estimate_empty_tiles, tile_process
|
|
|
128
152
|
info = estimate_empty_tiles("image.zarr", tile_shape=(120, 697, 697))
|
|
129
153
|
print(f"{info['empty_fraction']:.0%} tiles are background — will be skipped")
|
|
130
154
|
|
|
131
|
-
tile_process(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
tile_process(
|
|
156
|
+
"image.zarr",
|
|
157
|
+
fn,
|
|
158
|
+
tile_shape=(120, 697, 697),
|
|
159
|
+
skip_empty=True,
|
|
160
|
+
empty_threshold=info["threshold"],
|
|
161
|
+
write_to="labels.zarr",
|
|
162
|
+
)
|
|
136
163
|
```
|
|
137
164
|
|
|
138
165
|
### Distributed cluster for GPU
|
|
@@ -144,7 +171,8 @@ client, cluster = make_local_cluster(use_gpu=True)
|
|
|
144
171
|
try:
|
|
145
172
|
tile_process("image.zarr", fn, write_to="labels.zarr", progress=True)
|
|
146
173
|
finally:
|
|
147
|
-
client.close()
|
|
174
|
+
client.close()
|
|
175
|
+
cluster.close()
|
|
148
176
|
```
|
|
149
177
|
|
|
150
178
|
### Contiguous label numbering
|
|
@@ -152,9 +180,7 @@ finally:
|
|
|
152
180
|
```python
|
|
153
181
|
# Labels are globally unique by default, but may be gappy (block-encoded IDs).
|
|
154
182
|
# sequential_labels=True does a linear relabel O(voxels) — not O(n_tiles²).
|
|
155
|
-
tile_process("image.zarr", fn,
|
|
156
|
-
write_to="labels.zarr",
|
|
157
|
-
sequential_labels=True)
|
|
183
|
+
tile_process("image.zarr", fn, write_to="labels.zarr", sequential_labels=True)
|
|
158
184
|
```
|
|
159
185
|
|
|
160
186
|
### Use only the merge step (bring your own tiling)
|
|
@@ -169,8 +195,9 @@ from patchworks import merge_tile_labels
|
|
|
169
195
|
|
|
170
196
|
# Your own tiling + segmentation
|
|
171
197
|
image = da.from_zarr("image.zarr").rechunk((1, 1024, 1024))
|
|
172
|
-
labeled = image.map_blocks(
|
|
173
|
-
|
|
198
|
+
labeled = image.map_blocks(
|
|
199
|
+
my_segment_fn, dtype="int32", meta=np.empty((0,) * image.ndim, dtype="int32")
|
|
200
|
+
)
|
|
174
201
|
|
|
175
202
|
merged = merge_tile_labels(labeled, write_to="labels.zarr", progress=True)
|
|
176
203
|
```
|
|
@@ -211,13 +238,13 @@ tiles where the dask-image approach stalls.
|
|
|
211
238
|
|
|
212
239
|
## Known pitfalls (and how patchworks avoids them)
|
|
213
240
|
|
|
214
|
-
| Pitfall
|
|
215
|
-
|
|
216
|
-
| In-process Dask client
|
|
217
|
-
| 3-4× fn recompute during merge | Cellpose runs 3× per tile
|
|
218
|
-
| O(n²) sequential relabelling
|
|
219
|
-
| Wrong overlap boundary
|
|
220
|
-
| Persisting large arrays
|
|
241
|
+
| Pitfall | Symptom | How patchworks handles it |
|
|
242
|
+
| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------- |
|
|
243
|
+
| In-process Dask client | `FutureCancelledError: lost dependencies` | Detected at startup, raises immediately with fix instructions |
|
|
244
|
+
| 3-4× fn recompute during merge | Cellpose runs 3× per tile | Staging writes labels once, merge reads from disk |
|
|
245
|
+
| O(n²) sequential relabelling | Graph construction hangs at 1000+ tiles | Linear post-pass O(voxels) via `np.unique` + LUT |
|
|
246
|
+
| Wrong overlap boundary | Output shape mismatch | Always uses `boundary="none"` |
|
|
247
|
+
| Persisting large arrays | Worker OOM | Never persists; keeps dask graph lazy and streams |
|
|
221
248
|
|
|
222
249
|
---
|
|
223
250
|
|
|
@@ -8,12 +8,11 @@ body = """
|
|
|
8
8
|
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | split(pat="\n") | first }}
|
|
9
9
|
{% endfor %}
|
|
10
10
|
{% endfor %}\
|
|
11
|
-
{%
|
|
12
|
-
{% if contributors | length > 0 %}
|
|
11
|
+
{% if github.contributors | length > 0 %}
|
|
13
12
|
### 👥 Contributors
|
|
14
13
|
|
|
15
|
-
{% for
|
|
16
|
-
|
|
14
|
+
{% for contributor in github.contributors | sort(attribute="username") %}\
|
|
15
|
+
* @{{ contributor.username }}
|
|
17
16
|
{% endfor %}
|
|
18
17
|
{% endif %}\
|
|
19
18
|
"""
|
|
@@ -47,3 +46,7 @@ commit_parsers = [
|
|
|
47
46
|
filter_commits = true
|
|
48
47
|
tag_pattern = "v[0-9].*"
|
|
49
48
|
sort_commits = "oldest"
|
|
49
|
+
|
|
50
|
+
[remote.github]
|
|
51
|
+
owner = "imcf"
|
|
52
|
+
repo = "patchworks"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# napari viewer plugin
|
|
2
|
+
|
|
3
|
+
Open an OME-ZARR image and overlay the labels produced by
|
|
4
|
+
[`tile_process`](../tile_process.md) as a napari *Labels* layer in a single
|
|
5
|
+
call. Requires the optional `napari` extra (`pip install "patchworks[napari]"`).
|
|
6
|
+
|
|
7
|
+
::: patchworks.plugins.napari.view_in_napari
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# OME-ZARR conversion plugin
|
|
2
|
+
|
|
3
|
+
Write any array or image file to a pyramidal OME-ZARR store, add resolution
|
|
4
|
+
levels to an existing store, or store a label image inside an OME-ZARR under
|
|
5
|
+
the NGFF `labels/` group. Uses only the core dependencies for arrays and
|
|
6
|
+
`.zarr` inputs; reading other file formats needs the optional `bioio` extra
|
|
7
|
+
(`pip install "patchworks[bioio]"`).
|
|
8
|
+
|
|
9
|
+
Pyramids downsample **X and Y only** — `Z` (and channel/time) are kept at full
|
|
10
|
+
resolution, matching anisotropic microscopy stacks.
|
|
11
|
+
|
|
12
|
+
## to_ome_zarr
|
|
13
|
+
|
|
14
|
+
::: patchworks.plugins.ome_zarr.to_ome_zarr
|
|
15
|
+
|
|
16
|
+
## add_pyramid
|
|
17
|
+
|
|
18
|
+
::: patchworks.plugins.ome_zarr.add_pyramid
|
|
19
|
+
|
|
20
|
+
## write_labels
|
|
21
|
+
|
|
22
|
+
::: patchworks.plugins.ome_zarr.write_labels
|
|
23
|
+
|
|
24
|
+
## register_labels
|
|
25
|
+
|
|
26
|
+
::: patchworks.plugins.ome_zarr.register_labels
|
|
@@ -18,8 +18,8 @@ from patchworks.plugins.cellpose import cellpose_fn
|
|
|
18
18
|
|
|
19
19
|
IMAGE = "image.zarr"
|
|
20
20
|
OUTPUT = "labels.zarr"
|
|
21
|
-
CHANNEL = 0
|
|
22
|
-
DIAMETER = 30
|
|
21
|
+
CHANNEL = 0 # channel to segment
|
|
22
|
+
DIAMETER = 30 # expected cell diameter in pixels
|
|
23
23
|
|
|
24
24
|
# 1. Create the Cellpose function
|
|
25
25
|
fn = cellpose_fn("cyto3", gpu=True, diameter=DIAMETER)
|
|
@@ -31,10 +31,11 @@ print(f"{info['empty_fraction']:.0%} of slices are background")
|
|
|
31
31
|
# 3. Run
|
|
32
32
|
tile_fn = partial(auto_tile_shape_cellpose, diameter=DIAMETER, use_gpu=True)
|
|
33
33
|
tile_process(
|
|
34
|
-
IMAGE,
|
|
34
|
+
IMAGE,
|
|
35
|
+
fn,
|
|
35
36
|
channel=CHANNEL,
|
|
36
|
-
tile_shape=tile_fn,
|
|
37
|
-
overlap=20,
|
|
37
|
+
tile_shape=tile_fn, # auto-sized for GPU VRAM
|
|
38
|
+
overlap=20, # 20-voxel halo for boundary cells
|
|
38
39
|
skip_empty=True,
|
|
39
40
|
empty_threshold=info["threshold"],
|
|
40
41
|
write_to=OUTPUT,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Each z-slice is processed independently. Tiles overlap by 20 voxels so cells
|
|
4
4
|
near tile boundaries are fully visible to Cellpose.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from functools import partial
|
|
7
8
|
|
|
8
9
|
from patchworks import auto_tile_shape_cellpose, tile_process
|
|
@@ -19,11 +20,12 @@ fn = cellpose_fn("cyto3", gpu=True, diameter=DIAMETER)
|
|
|
19
20
|
tile_fn = partial(auto_tile_shape_cellpose, diameter=DIAMETER, use_gpu=True)
|
|
20
21
|
|
|
21
22
|
tile_process(
|
|
22
|
-
IMAGE,
|
|
23
|
+
IMAGE,
|
|
24
|
+
fn,
|
|
23
25
|
channel=CHANNEL,
|
|
24
26
|
tile_shape=tile_fn,
|
|
25
27
|
overlap=20,
|
|
26
|
-
skip_empty=True,
|
|
28
|
+
skip_empty=True, # skip background slices
|
|
27
29
|
write_to=OUTPUT,
|
|
28
30
|
progress=True,
|
|
29
31
|
)
|
|
@@ -18,7 +18,7 @@ from patchworks.plugins.cellpose import cellpose_fn
|
|
|
18
18
|
IMAGE = "image.zarr"
|
|
19
19
|
OUTPUT = "labels_3d.zarr"
|
|
20
20
|
CHANNEL = 0
|
|
21
|
-
DIAMETER = 20
|
|
21
|
+
DIAMETER = 20 # pixels
|
|
22
22
|
ANISOTROPY = 3.0 # z_spacing / xy_spacing
|
|
23
23
|
|
|
24
24
|
fn = cellpose_fn(
|
|
@@ -45,7 +45,8 @@ print("Dashboard:", client.dashboard_link)
|
|
|
45
45
|
|
|
46
46
|
try:
|
|
47
47
|
tile_process(
|
|
48
|
-
IMAGE,
|
|
48
|
+
IMAGE,
|
|
49
|
+
fn,
|
|
49
50
|
channel=CHANNEL,
|
|
50
51
|
tile_shape=tile_fn,
|
|
51
52
|
overlap=10,
|
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
Each tile contains the full z extent. Cellpose do_3D=True runs segmentation on
|
|
4
4
|
xy, xz, and yz planes and takes a 3-D consensus.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from functools import partial
|
|
7
8
|
|
|
8
|
-
from patchworks import
|
|
9
|
+
from patchworks import (
|
|
10
|
+
auto_tile_shape_cellpose,
|
|
11
|
+
make_local_cluster,
|
|
12
|
+
tile_process,
|
|
13
|
+
)
|
|
9
14
|
from patchworks.plugins.cellpose import cellpose_fn
|
|
10
15
|
|
|
11
16
|
IMAGE = "image.zarr"
|
|
12
17
|
OUTPUT = "labels_3d.zarr"
|
|
13
18
|
CHANNEL = 0
|
|
14
|
-
DIAMETER = 20
|
|
19
|
+
DIAMETER = 20 # pixels
|
|
15
20
|
ANISOTROPY = 3.0 # z-spacing / xy-spacing
|
|
16
21
|
|
|
17
22
|
fn = cellpose_fn(
|
|
@@ -35,7 +40,8 @@ print("Dashboard:", client.dashboard_link)
|
|
|
35
40
|
|
|
36
41
|
try:
|
|
37
42
|
tile_process(
|
|
38
|
-
IMAGE,
|
|
43
|
+
IMAGE,
|
|
44
|
+
fn,
|
|
39
45
|
channel=CHANNEL,
|
|
40
46
|
tile_shape=tile_fn,
|
|
41
47
|
overlap=10,
|
|
@@ -11,10 +11,12 @@ from skimage.filters import threshold_otsu
|
|
|
11
11
|
from skimage.measure import label
|
|
12
12
|
from patchworks import tile_process
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
def threshold_fn(tile: np.ndarray) -> np.ndarray:
|
|
15
16
|
thr = threshold_otsu(tile)
|
|
16
17
|
return label(tile > thr).astype("int32")
|
|
17
18
|
|
|
19
|
+
|
|
18
20
|
result = tile_process("image.zarr", threshold_fn, compute=True)
|
|
19
21
|
```
|
|
20
22
|
|
|
@@ -27,17 +29,22 @@ from skimage.morphology import remove_small_objects
|
|
|
27
29
|
from skimage.measure import label
|
|
28
30
|
from patchworks import tile_process
|
|
29
31
|
|
|
32
|
+
|
|
30
33
|
def smooth_and_label(tile: np.ndarray) -> np.ndarray:
|
|
31
34
|
smoothed = gaussian_filter(tile.astype("float32"), sigma=1.5)
|
|
32
35
|
binary = smoothed > smoothed.mean()
|
|
33
36
|
cleaned = remove_small_objects(binary, min_size=100)
|
|
34
37
|
return label(cleaned).astype("int32")
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
|
|
40
|
+
tile_process(
|
|
41
|
+
"image.zarr",
|
|
42
|
+
smooth_and_label,
|
|
43
|
+
tile_shape=(1, 512, 512),
|
|
44
|
+
overlap=16,
|
|
45
|
+
write_to="labels.zarr",
|
|
46
|
+
progress=True,
|
|
47
|
+
)
|
|
41
48
|
```
|
|
42
49
|
|
|
43
50
|
## PyTorch model
|
|
@@ -51,6 +58,7 @@ from patchworks import tile_process
|
|
|
51
58
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
52
59
|
model = MySegmentationModel().to(device).eval()
|
|
53
60
|
|
|
61
|
+
|
|
54
62
|
@torch.no_grad()
|
|
55
63
|
def torch_fn(tile: np.ndarray) -> np.ndarray:
|
|
56
64
|
t = torch.from_numpy(tile.astype("float32")).unsqueeze(0).unsqueeze(0).to(device)
|
|
@@ -58,11 +66,15 @@ def torch_fn(tile: np.ndarray) -> np.ndarray:
|
|
|
58
66
|
pred = logits.argmax(1).squeeze(0).cpu().numpy()
|
|
59
67
|
return pred.astype("int32")
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
|
|
70
|
+
tile_process(
|
|
71
|
+
"image.zarr",
|
|
72
|
+
torch_fn,
|
|
73
|
+
tile_shape=(1, 512, 512),
|
|
74
|
+
use_gpu=True,
|
|
75
|
+
write_to="labels.zarr",
|
|
76
|
+
progress=True,
|
|
77
|
+
)
|
|
66
78
|
```
|
|
67
79
|
|
|
68
80
|
## Any array input, not just zarr
|
|
@@ -79,6 +91,7 @@ result = tile_process(arr, my_fn, compute=True)
|
|
|
79
91
|
# From tifffile
|
|
80
92
|
import tifffile
|
|
81
93
|
import dask.array as da
|
|
94
|
+
|
|
82
95
|
arr = da.from_array(tifffile.imread("image.tif", aszarr=True))
|
|
83
96
|
result = tile_process(arr, my_fn, compute=True)
|
|
84
97
|
```
|