patchworks 0.11.17__tar.gz → 0.11.18__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.11.17 → patchworks-0.11.18}/PKG-INFO +3 -3
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/snakemake.md +123 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/pyproject.toml +4 -3
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/__init__.py +2 -0
- patchworks-0.11.18/src/patchworks/_relations.py +142 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/plugins/napari.py +41 -12
- patchworks-0.11.18/tests/test_relations.py +43 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/README.md +11 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/Snakefile +1 -1
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/config/config.yaml +4 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/rules/common.smk +16 -5
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/rules/merge.smk +1 -1
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/rules/segment.smk +1 -1
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/scripts/_pw.py +10 -4
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/scripts/merge.py +6 -5
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/scripts/prepare_tiles.py +4 -2
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/scripts/segment_tile.py +2 -1
- {patchworks-0.11.17 → patchworks-0.11.18}/.github/workflows/docs.yml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/.github/workflows/lint.yml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/.github/workflows/release.yml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/.gitignore +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/.markdownlint-cli2.yaml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/LICENSE +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/README.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/cliff.toml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/chunks.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/cluster.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/io.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/plugins/napari.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/relabel.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/api/tile_process.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/assets/logo.png +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/custom.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/custom_method.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/standalone_merge.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/stardist.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/examples/stardist_2d.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/getting_started.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/merging.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/performance.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/pitfalls.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/skip_empty.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/guide/tiling.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/docs/index.md +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/mkdocs.yml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/_chunks.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/_cluster.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/_core.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/_distributed.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/_io.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/_merge.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/_relabel.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/tests/test_core.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/tests/test_distributed.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/tests/test_napari.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/tests/test_ome_zarr.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/pixi.toml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/rules/convert.smk +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/scripts/convert.py +0 -0
- {patchworks-0.11.17 → patchworks-0.11.18}/workflow/scripts/fetch_model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.18
|
|
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
|
|
@@ -34,7 +34,7 @@ Requires-Dist: glasbey; extra == 'all'
|
|
|
34
34
|
Requires-Dist: imaris-ims-file-reader; extra == 'all'
|
|
35
35
|
Requires-Dist: ipykernel<7; extra == 'all'
|
|
36
36
|
Requires-Dist: lxml-html-clean; extra == 'all'
|
|
37
|
-
Requires-Dist: napari[all]>=0.
|
|
37
|
+
Requires-Dist: napari[all]>=0.7.1; extra == 'all'
|
|
38
38
|
Requires-Dist: numpy<2.5; extra == 'all'
|
|
39
39
|
Requires-Dist: nvidia-ml-py; extra == 'all'
|
|
40
40
|
Requires-Dist: psutil; extra == 'all'
|
|
@@ -74,7 +74,7 @@ Provides-Extra: napari
|
|
|
74
74
|
Requires-Dist: glasbey; extra == 'napari'
|
|
75
75
|
Requires-Dist: ipykernel<7; extra == 'napari'
|
|
76
76
|
Requires-Dist: lxml-html-clean; extra == 'napari'
|
|
77
|
-
Requires-Dist: napari[all]>=0.
|
|
77
|
+
Requires-Dist: napari[all]>=0.7.1; extra == 'napari'
|
|
78
78
|
Requires-Dist: numpy<2.5; extra == 'napari'
|
|
79
79
|
Provides-Extra: workflow
|
|
80
80
|
Requires-Dist: snakemake-executor-plugin-slurm; extra == 'workflow'
|
|
@@ -194,6 +194,129 @@ also re-runs a step when its **code, params or software environment** change —
|
|
|
194
194
|
so upgrading patchworks would re-do the conversion and overwrite an existing
|
|
195
195
|
result. Keep `mtime` and reruns happen only when an output is missing or stale.
|
|
196
196
|
|
|
197
|
+
## Running two segmentations (e.g. nuclei + cytoplasm)
|
|
198
|
+
|
|
199
|
+
Every path the workflow writes — `tiles.json`, `stage.zarr`, per-tile `seg/`,
|
|
200
|
+
the cached model, `labels.done` — lives under `work_dir/<label_name>/`, so
|
|
201
|
+
running the workflow **twice with two configs against the same `work_dir`**
|
|
202
|
+
never collides: each run gets its own private subdirectory, and both reuse
|
|
203
|
+
the *same* already-converted `image.zarr` (conversion never re-runs).
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
# config/config_nuclei.yaml
|
|
207
|
+
input: "/data/scan.ims"
|
|
208
|
+
work_dir: "/scratch/results"
|
|
209
|
+
label_name: "nuclei_labels"
|
|
210
|
+
channel: 1 # nuclear stain channel
|
|
211
|
+
tile_shape: [16, 1024, 1024]
|
|
212
|
+
cellpose:
|
|
213
|
+
model: "nuclei"
|
|
214
|
+
diameter: 15
|
|
215
|
+
do_3D: true
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
# config/config_cyto.yaml
|
|
220
|
+
input: "/data/scan.ims"
|
|
221
|
+
work_dir: "/scratch/results" # same work_dir — image.zarr is reused
|
|
222
|
+
label_name: "cyto_labels"
|
|
223
|
+
channel: 0 # cytoplasm/membrane channel
|
|
224
|
+
tile_shape: [16, 1024, 1024] # keep this identical across configs — see below
|
|
225
|
+
cellpose:
|
|
226
|
+
model: "cyto3"
|
|
227
|
+
diameter: 30
|
|
228
|
+
do_3D: true
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Run them one after another (or as two independent SLURM submissions, even
|
|
232
|
+
concurrently — they touch disjoint files):
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
snakemake --workflow-profile profile/slurm --configfile config/config_nuclei.yaml
|
|
236
|
+
snakemake --workflow-profile profile/slurm --configfile config/config_cyto.yaml
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Both land side by side in the same store:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
results/image.zarr/labels/nuclei_labels/
|
|
243
|
+
results/image.zarr/labels/cyto_labels/
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
!!! tip "Keep `tile_shape` (and `level`) identical across configs"
|
|
247
|
+
Different segmentations of the same image can use different `channel` and
|
|
248
|
+
`cellpose:` settings freely, but keep `tile_shape`/`level` the same across
|
|
249
|
+
configs — the label arrays then share the exact same chunk layout, which
|
|
250
|
+
`label_relations()` (below) requires.
|
|
251
|
+
|
|
252
|
+
### Relating labels across segmentations
|
|
253
|
+
|
|
254
|
+
Once you have two segmentations of the same image (e.g. nuclei inside cells),
|
|
255
|
+
`label_relations()` maps each label in one to the label it overlaps most in
|
|
256
|
+
the other — by streaming both arrays chunk by chunk, so it scales to
|
|
257
|
+
hundreds of thousands of objects without loading anything fully into RAM:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
import dask.array as da
|
|
261
|
+
from patchworks import label_relations
|
|
262
|
+
|
|
263
|
+
nuclei = da.from_zarr("results/image.zarr", component="labels/nuclei_labels/0")
|
|
264
|
+
cells = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
|
|
265
|
+
|
|
266
|
+
table = label_relations(nuclei, cells)
|
|
267
|
+
table[2]
|
|
268
|
+
# {'match': 3, 'overlap_voxels': 4821, 'overlap_fraction': 0.94}
|
|
269
|
+
# -> nucleus 2 belongs to cell 3, 94% of its voxels fall inside it
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Save it as a table:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
import csv
|
|
276
|
+
|
|
277
|
+
with open("nuclei_to_cell.csv", "w", newline="") as f:
|
|
278
|
+
w = csv.writer(f)
|
|
279
|
+
w.writerow(["nucleus_id", "cell_id", "overlap_voxels", "overlap_fraction"])
|
|
280
|
+
for nucleus_id, m in table.items():
|
|
281
|
+
w.writerow([nucleus_id, m["match"], m["overlap_voxels"], m["overlap_fraction"]])
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Measurements (fast, whole-volume regionprops)
|
|
285
|
+
|
|
286
|
+
`skimage.measure.regionprops` needs the full labelled + intensity array in
|
|
287
|
+
RAM — fine for one tile, not for a hundred-thousand-object OME-ZARR. Use
|
|
288
|
+
[`dask-image`](https://image.dask.org)'s `ndmeasure`, which computes directly
|
|
289
|
+
on the dask/zarr-backed arrays, chunk-parallel, without materializing the
|
|
290
|
+
volume:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
pip install dask-image
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
import dask.array as da
|
|
298
|
+
from dask_image.ndmeasure import area, center_of_mass, mean, standard_deviation
|
|
299
|
+
|
|
300
|
+
labels = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
|
|
301
|
+
image = da.from_zarr("results/image.zarr", component="0")[0] # channel 0, level 0
|
|
302
|
+
|
|
303
|
+
ids = da.unique(labels[labels > 0]).compute()
|
|
304
|
+
areas = area(image, labels, ids).compute() # voxel counts
|
|
305
|
+
means = mean(image, labels, ids).compute() # mean intensity
|
|
306
|
+
stds = standard_deviation(image, labels, ids).compute()
|
|
307
|
+
centroids = center_of_mass(image, labels, ids).compute() # voxel coords (z, y, x)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Multiply `areas` by the voxel's physical volume and `centroids` by the pixel
|
|
311
|
+
size (both read straight from the OME-ZARR's own `multiscales` metadata) to
|
|
312
|
+
get µm-scale measurements.
|
|
313
|
+
|
|
314
|
+
For interactively inspecting individual cells by clicking in the viewer
|
|
315
|
+
(not all objects at once), the
|
|
316
|
+
[napari-skimage-regionprops](https://github.com/haesleinhuepf/napari-skimage-regionprops)
|
|
317
|
+
plugin's table widget works well — point it at a cropped region rather than
|
|
318
|
+
the full volume, since it loads its input fully into memory.
|
|
319
|
+
|
|
197
320
|
## Custom segmentation function
|
|
198
321
|
|
|
199
322
|
Not using Cellpose? Run **your own** per-tile function — no need to edit the
|
|
@@ -59,15 +59,16 @@ bioio = [
|
|
|
59
59
|
# imaris reads .ims files natively (HDF5, no JVM) for OME-ZARR conversion.
|
|
60
60
|
imaris = ["imaris-ims-file-reader"]
|
|
61
61
|
# napari enables the interactive viewer plugin.
|
|
62
|
-
# - napari >= 0.
|
|
63
|
-
#
|
|
62
|
+
# - napari >= 0.7.1: adds locked_data_level (manual multiscale resolution pin
|
|
63
|
+
# for 2D *and* 3D) — before this, 3D view always showed the coarsest
|
|
64
|
+
# pyramid level unconditionally, with no way to change it.
|
|
64
65
|
# - numpy < 2.5: napari/numba don't support numpy 2.5 yet.
|
|
65
66
|
# - ipykernel < 7: napari-console requires ipykernel < 7.
|
|
66
67
|
# - lxml-html-clean: napari's notebook_display imports lxml.html.clean, split
|
|
67
68
|
# into a separate package in lxml >= 5.2 (else ImportError on Viewer()).
|
|
68
69
|
# - glasbey: distinct high-contrast label LUTs for view_in_napari.
|
|
69
70
|
napari = [
|
|
70
|
-
"napari[all]>=0.
|
|
71
|
+
"napari[all]>=0.7.1",
|
|
71
72
|
"numpy<2.5",
|
|
72
73
|
"ipykernel<7",
|
|
73
74
|
"lxml-html-clean",
|
|
@@ -36,6 +36,7 @@ from ._distributed import create_stage, spatial_tiles, stage_tile
|
|
|
36
36
|
from ._io import estimate_empty_tiles, load_ome_zarr
|
|
37
37
|
from ._merge import merge_tile_labels
|
|
38
38
|
from ._relabel import relabel_sequential_array, relabel_sequential_zarr
|
|
39
|
+
from ._relations import label_relations
|
|
39
40
|
|
|
40
41
|
try:
|
|
41
42
|
__version__ = _pkg_version("patchworks")
|
|
@@ -52,6 +53,7 @@ __all__ = [
|
|
|
52
53
|
"make_local_cluster",
|
|
53
54
|
"relabel_sequential_array",
|
|
54
55
|
"relabel_sequential_zarr",
|
|
56
|
+
"label_relations",
|
|
55
57
|
"spatial_tiles",
|
|
56
58
|
"create_stage",
|
|
57
59
|
"stage_tile",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Relate two label images by voxel overlap (e.g. nucleus -> containing cell)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
import dask.array as da
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _as_dask(source: Union["da.Array", str, Path], component: str) -> "da.Array":
|
|
18
|
+
if isinstance(source, (str, Path)):
|
|
19
|
+
return da.from_zarr(str(source), component=component)
|
|
20
|
+
return source
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _chunk_pairs(a_block: np.ndarray, b_block: np.ndarray) -> np.ndarray:
|
|
24
|
+
"""Non-background ``(a_id, b_id) -> voxel count`` rows for one chunk pair."""
|
|
25
|
+
mask = (a_block > 0) & (b_block > 0)
|
|
26
|
+
if not mask.any():
|
|
27
|
+
return np.empty((0, 3), dtype=np.int64)
|
|
28
|
+
pairs = np.stack([a_block[mask], b_block[mask]], axis=1).astype(np.int64)
|
|
29
|
+
uniq, counts = np.unique(pairs, axis=0, return_counts=True)
|
|
30
|
+
return np.concatenate([uniq, counts[:, None]], axis=1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def label_relations(
|
|
34
|
+
a: Union["da.Array", str, Path],
|
|
35
|
+
b: Union["da.Array", str, Path],
|
|
36
|
+
*,
|
|
37
|
+
a_component: str = "labels",
|
|
38
|
+
b_component: str = "labels",
|
|
39
|
+
n_workers: int | None = None,
|
|
40
|
+
) -> dict[int, dict[str, float]]:
|
|
41
|
+
"""Map each label in *a* to its best-overlapping label in *b*.
|
|
42
|
+
|
|
43
|
+
For every non-background label in *a* (e.g. a nucleus segmentation),
|
|
44
|
+
finds the label in *b* (e.g. a cell/cytoplasm segmentation of the same
|
|
45
|
+
image) it shares the most voxels with. Streams both arrays chunk by
|
|
46
|
+
chunk — memory is bounded by the number of distinct label pairs (one row
|
|
47
|
+
per touching (a, b) pair per chunk), not by volume size.
|
|
48
|
+
|
|
49
|
+
*a* and *b* must be two segmentations of the **same image** (identical
|
|
50
|
+
shape and chunk layout) — e.g. two runs of the Snakemake workflow with
|
|
51
|
+
different ``label_name``/``cellpose:`` config but the same ``tile_shape``.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
a, b : da.Array, str or Path
|
|
56
|
+
Two label arrays of identical shape and chunking. A path is read via
|
|
57
|
+
``dask.array.from_zarr(path, component=...)``.
|
|
58
|
+
a_component, b_component : str, optional
|
|
59
|
+
Zarr array name inside *a*/*b* when they're store paths (default
|
|
60
|
+
``"labels"``).
|
|
61
|
+
n_workers : int or None, optional
|
|
62
|
+
Parallel chunk workers. Default ``min(4, cpu_count)``.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
dict
|
|
67
|
+
``{a_label: {"match": b_label, "overlap_voxels": int,
|
|
68
|
+
"overlap_fraction": float}}`` — one entry per *a* label that touches
|
|
69
|
+
at least one non-background *b* voxel. ``overlap_fraction`` is the
|
|
70
|
+
matched voxel count over *a* label's total voxel count (1.0 = fully
|
|
71
|
+
contained). Labels in *a* with zero overlap are omitted.
|
|
72
|
+
|
|
73
|
+
Examples
|
|
74
|
+
--------
|
|
75
|
+
>>> from patchworks import label_relations
|
|
76
|
+
>>> table = label_relations(
|
|
77
|
+
... "scan.zarr/labels/nuclei", "scan.zarr/labels/cells"
|
|
78
|
+
... ) # doctest: +SKIP
|
|
79
|
+
>>> table[2] # nucleus 2 sits inside cell 3 # doctest: +SKIP
|
|
80
|
+
{'match': 3, 'overlap_voxels': 4821, 'overlap_fraction': 0.94}
|
|
81
|
+
"""
|
|
82
|
+
a = _as_dask(a, a_component)
|
|
83
|
+
b = _as_dask(b, b_component)
|
|
84
|
+
if a.shape != b.shape:
|
|
85
|
+
raise ValueError(f"shape mismatch: a={a.shape} b={b.shape}")
|
|
86
|
+
if a.chunks != b.chunks:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"a and b must share the same chunk layout "
|
|
89
|
+
f"(a={a.chunks} b={b.chunks}); rechunk one to match the other, "
|
|
90
|
+
"e.g. b = b.rechunk(a.chunks)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
n_blocks = a.numblocks
|
|
94
|
+
total = int(np.prod(n_blocks))
|
|
95
|
+
nw = n_workers if n_workers is not None else min(4, os.cpu_count() or 1)
|
|
96
|
+
|
|
97
|
+
def _one(flat_idx: int) -> np.ndarray:
|
|
98
|
+
idx = np.unravel_index(flat_idx, n_blocks)
|
|
99
|
+
return _chunk_pairs(np.asarray(a.blocks[idx]), np.asarray(b.blocks[idx]))
|
|
100
|
+
|
|
101
|
+
with ThreadPoolExecutor(max_workers=nw) as ex:
|
|
102
|
+
parts = list(ex.map(_one, range(total)))
|
|
103
|
+
|
|
104
|
+
rows = [p for p in parts if p.size]
|
|
105
|
+
if not rows:
|
|
106
|
+
return {}
|
|
107
|
+
all_pairs = np.concatenate(rows, axis=0)
|
|
108
|
+
|
|
109
|
+
# Merge duplicate (a_id, b_id) rows across chunks (a label can span
|
|
110
|
+
# several chunks) by sorting on a combined key and summing runs.
|
|
111
|
+
key = all_pairs[:, 0] * (int(all_pairs[:, 1].max()) + 1) + all_pairs[:, 1]
|
|
112
|
+
order = np.argsort(key, kind="stable")
|
|
113
|
+
all_pairs, key = all_pairs[order], key[order]
|
|
114
|
+
starts = np.concatenate([[0], np.flatnonzero(np.diff(key)) + 1])
|
|
115
|
+
merged_counts = np.add.reduceat(all_pairs[:, 2], starts)
|
|
116
|
+
merged = np.stack(
|
|
117
|
+
[all_pairs[starts, 0], all_pairs[starts, 1], merged_counts], axis=1
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
a_totals: dict[int, int] = {}
|
|
121
|
+
for a_id, _, count in merged:
|
|
122
|
+
a_id = int(a_id)
|
|
123
|
+
a_totals[a_id] = a_totals.get(a_id, 0) + int(count)
|
|
124
|
+
|
|
125
|
+
best: dict[int, tuple[int, int]] = {}
|
|
126
|
+
for a_id, b_id, count in merged:
|
|
127
|
+
a_id, b_id, count = int(a_id), int(b_id), int(count)
|
|
128
|
+
cur = best.get(a_id)
|
|
129
|
+
if cur is None or count > cur[1]:
|
|
130
|
+
best[a_id] = (b_id, count)
|
|
131
|
+
|
|
132
|
+
logger.info(
|
|
133
|
+
"label_relations: %d a-labels matched across %d chunks", len(best), total
|
|
134
|
+
)
|
|
135
|
+
return {
|
|
136
|
+
a_id: {
|
|
137
|
+
"match": b_id,
|
|
138
|
+
"overlap_voxels": count,
|
|
139
|
+
"overlap_fraction": count / a_totals[a_id],
|
|
140
|
+
}
|
|
141
|
+
for a_id, (b_id, count) in best.items()
|
|
142
|
+
}
|
|
@@ -142,12 +142,14 @@ def _resolve_image(
|
|
|
142
142
|
return source
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
def
|
|
146
|
-
|
|
145
|
+
def _pyramid_calibration(
|
|
146
|
+
path: Union[str, Path], ndim: int
|
|
147
|
+
) -> tuple[list[float], list[str]] | tuple[None, None]:
|
|
148
|
+
"""Read the level-0 physical scale and units from an OME-ZARR's metadata.
|
|
147
149
|
|
|
148
|
-
Without this, napari shows every axis with an implicit scale of 1
|
|
149
|
-
volume with anisotropic voxels (e.g. z coarser than
|
|
150
|
-
wrong aspect ratio and no real-world units.
|
|
150
|
+
Without this, napari shows every axis with an implicit scale of 1 and
|
|
151
|
+
"pixel" units, so a volume with anisotropic voxels (e.g. z coarser than
|
|
152
|
+
x/y) renders with the wrong aspect ratio and no real-world units.
|
|
151
153
|
|
|
152
154
|
Parameters
|
|
153
155
|
----------
|
|
@@ -161,15 +163,20 @@ def _pyramid_scale(path: Union[str, Path], ndim: int) -> list[float] | None:
|
|
|
161
163
|
|
|
162
164
|
Returns
|
|
163
165
|
-------
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
tuple
|
|
167
|
+
``(scale, units)`` — physical size and unit name per axis — or
|
|
168
|
+
``(None, None)`` if the store carries no calibration (napari then
|
|
169
|
+
falls back to its uncalibrated pixel default).
|
|
167
170
|
"""
|
|
168
171
|
from .ome_zarr import _base_scale, _default_axes, _read_zarr_calibration
|
|
169
172
|
|
|
170
173
|
axes = _default_axes(ndim)
|
|
171
174
|
pixel_size = _read_zarr_calibration(str(path), axes)
|
|
172
|
-
|
|
175
|
+
if not pixel_size:
|
|
176
|
+
return None, None
|
|
177
|
+
scale = _base_scale(axes, pixel_size)
|
|
178
|
+
units = ["micrometer"] * len(scale)
|
|
179
|
+
return scale, units
|
|
173
180
|
|
|
174
181
|
|
|
175
182
|
def _inner_label_names(store: Union[str, Path]) -> list[str]:
|
|
@@ -271,6 +278,18 @@ def view_in_napari(
|
|
|
271
278
|
napari.Viewer
|
|
272
279
|
The viewer instance (useful when ``show=False``).
|
|
273
280
|
|
|
281
|
+
Notes
|
|
282
|
+
-----
|
|
283
|
+
In 3-D view (the cube icon, or ``viewer.dims.ndisplay = 3``), napari
|
|
284
|
+
always shows the **coarsest** pyramid level — there is no automatic
|
|
285
|
+
zoom-based switching in 3-D, only in 2-D. To pin a specific resolution
|
|
286
|
+
(needs ``napari>=0.7.1``)::
|
|
287
|
+
|
|
288
|
+
viewer.layers["labels"].locked_data_level = 0 # full resolution
|
|
289
|
+
viewer.layers["labels"].locked_data_level = None # back to coarsest
|
|
290
|
+
|
|
291
|
+
A widget for this is also in the layer controls panel in napari>=0.7.1.
|
|
292
|
+
|
|
274
293
|
Examples
|
|
275
294
|
--------
|
|
276
295
|
>>> view_in_napari("scan.zarr") # auto-loads scan.zarr/labels/* # doctest: +SKIP
|
|
@@ -279,13 +298,16 @@ def view_in_napari(
|
|
|
279
298
|
|
|
280
299
|
img = _resolve_image(image, channel)
|
|
281
300
|
img_ndim = img[0].ndim if isinstance(img, list) else img.ndim
|
|
282
|
-
img_scale
|
|
301
|
+
img_scale, img_units = (
|
|
302
|
+
_pyramid_calibration(image, img_ndim) if _is_zarr(image) else (None, None)
|
|
303
|
+
)
|
|
283
304
|
viewer = napari.Viewer()
|
|
284
305
|
viewer.add_image(
|
|
285
306
|
img,
|
|
286
307
|
name=image_name,
|
|
287
308
|
multiscale=isinstance(img, list),
|
|
288
309
|
scale=img_scale,
|
|
310
|
+
units=img_units,
|
|
289
311
|
**add_image_kwargs,
|
|
290
312
|
)
|
|
291
313
|
|
|
@@ -322,12 +344,17 @@ def view_in_napari(
|
|
|
322
344
|
if labels is not None:
|
|
323
345
|
lab = _resolve_labels(labels, labels_component)
|
|
324
346
|
lab_ndim = lab[0].ndim if isinstance(lab, list) else lab.ndim
|
|
325
|
-
lab_scale
|
|
347
|
+
lab_scale, lab_units = (
|
|
348
|
+
_pyramid_calibration(labels, lab_ndim)
|
|
349
|
+
if _is_zarr(labels)
|
|
350
|
+
else (None, None)
|
|
351
|
+
)
|
|
326
352
|
viewer.add_labels(
|
|
327
353
|
lab,
|
|
328
354
|
name=labels_name,
|
|
329
355
|
multiscale=isinstance(lab, list),
|
|
330
356
|
scale=lab_scale,
|
|
357
|
+
units=lab_units,
|
|
331
358
|
**label_kwargs,
|
|
332
359
|
)
|
|
333
360
|
elif _is_zarr(image):
|
|
@@ -341,11 +368,13 @@ def view_in_napari(
|
|
|
341
368
|
store = f"{image}/labels/{name}"
|
|
342
369
|
levels = _multiscale_levels(store, None)
|
|
343
370
|
lab = [lvl.astype("int32") for lvl in levels]
|
|
371
|
+
lab_scale, lab_units = _pyramid_calibration(store, lab[0].ndim)
|
|
344
372
|
viewer.add_labels(
|
|
345
373
|
lab,
|
|
346
374
|
name=name,
|
|
347
375
|
multiscale=True,
|
|
348
|
-
scale=
|
|
376
|
+
scale=lab_scale,
|
|
377
|
+
units=lab_units,
|
|
349
378
|
**label_kwargs,
|
|
350
379
|
)
|
|
351
380
|
logger.info("auto-loaded labels/%s from %s", name, image)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import dask.array as da
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from patchworks import label_relations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_label_relations_majority_overlap():
|
|
9
|
+
# nucleus 1 sits fully inside cell 10; nucleus 2 straddles cells 20/21
|
|
10
|
+
# with more voxels in 20 -> should match 20, not 21.
|
|
11
|
+
a = np.zeros((4, 10), dtype=np.int32)
|
|
12
|
+
a[0:2, 0:2] = 1
|
|
13
|
+
a[0:2, 4:8] = 2
|
|
14
|
+
b = np.zeros((4, 10), dtype=np.int32)
|
|
15
|
+
b[0:3, 0:3] = 10
|
|
16
|
+
b[0:2, 4:6] = 20 # 4 voxels overlap with label 2
|
|
17
|
+
b[0:2, 6:8] = 21 # 4 voxels overlap with label 2 -> tie, but let's skew it
|
|
18
|
+
b[0:2, 6:7] = 20 # tip the tie: label 20 now has 6 voxels vs 21's 2
|
|
19
|
+
|
|
20
|
+
table = label_relations(da.from_array(a, chunks=(2, 5)), da.from_array(b, chunks=(2, 5)))
|
|
21
|
+
|
|
22
|
+
assert table[1]["match"] == 10
|
|
23
|
+
assert table[1]["overlap_voxels"] == 4
|
|
24
|
+
assert table[1]["overlap_fraction"] == 1.0
|
|
25
|
+
|
|
26
|
+
assert table[2]["match"] == 20
|
|
27
|
+
assert table[2]["overlap_voxels"] == 6
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_label_relations_no_overlap_omitted():
|
|
31
|
+
a = np.zeros((2, 2), dtype=np.int32)
|
|
32
|
+
a[0, 0] = 1
|
|
33
|
+
b = np.zeros((2, 2), dtype=np.int32) # all background, no overlap anywhere
|
|
34
|
+
|
|
35
|
+
table = label_relations(da.from_array(a, chunks=(2, 2)), da.from_array(b, chunks=(2, 2)))
|
|
36
|
+
assert table == {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_label_relations_chunk_mismatch_raises():
|
|
40
|
+
a = da.zeros((4, 4), chunks=(2, 4), dtype=np.int32)
|
|
41
|
+
b = da.zeros((4, 4), chunks=(4, 4), dtype=np.int32)
|
|
42
|
+
with pytest.raises(ValueError, match="chunk layout"):
|
|
43
|
+
label_relations(a, b)
|
|
@@ -75,6 +75,17 @@ The GPU request lives in `profile/slurm/config.yaml` under
|
|
|
75
75
|
`set-resources: segment:` (`--gres=gpu:1`). Raise `jobs:` to use more GPUs at
|
|
76
76
|
once.
|
|
77
77
|
|
|
78
|
+
## Multiple segmentations (nuclei + cytoplasm, …)
|
|
79
|
+
|
|
80
|
+
Every intermediate path is namespaced under `work_dir/<label_name>/`, so
|
|
81
|
+
running the workflow **twice with two configs** (different `label_name`,
|
|
82
|
+
`channel`, `cellpose:`) against the **same `work_dir`** is safe — both reuse
|
|
83
|
+
the one converted `image.zarr` and land side by side in
|
|
84
|
+
`image.zarr/labels/<label_name>/`. See the
|
|
85
|
+
[full guide](https://imcf.one/patchworks/guide/snakemake/#running-two-segmentations-eg-nuclei--cytoplasm)
|
|
86
|
+
for the two-config recipe and `patchworks.label_relations()` for mapping one
|
|
87
|
+
segmentation onto the other (e.g. nucleus → containing cell).
|
|
88
|
+
|
|
78
89
|
## Output
|
|
79
90
|
|
|
80
91
|
`<work_dir>/image.zarr` — the image plus `labels/<label_name>/` (multi-scale,
|
|
@@ -30,6 +30,10 @@ empty_threshold: null # null → Otsu; or a number
|
|
|
30
30
|
|
|
31
31
|
# ---- segmentation -----------------------------------------------------------
|
|
32
32
|
method: "cellpose" # "cellpose" (GPU), "threshold" (no GPU; testing), "custom"
|
|
33
|
+
# Also namespaces this run's intermediate files under work_dir/<label_name>/,
|
|
34
|
+
# so a second segmentation (different label_name, e.g. nuclei vs cytoplasm)
|
|
35
|
+
# can safely target the same work_dir — see docs/guide/snakemake.md
|
|
36
|
+
# "Running two segmentations".
|
|
33
37
|
label_name: "cellpose_labels"
|
|
34
38
|
cellpose:
|
|
35
39
|
model: "nuclei"
|
|
@@ -7,8 +7,17 @@ IMAGE = f"{WORK}/image.zarr"
|
|
|
7
7
|
# directory) lets Snakemake skip conversion when the store already exists and
|
|
8
8
|
# avoids wiping the whole store on a re-run (same trick as imcf/sopa).
|
|
9
9
|
IMAGE_OK = f"{IMAGE}/zarr.json"
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
# Everything below is per-segmentation, namespaced under WORK/<label_name>/, so
|
|
12
|
+
# running the workflow twice with two configs (different label_name, e.g.
|
|
13
|
+
# "nuclei_labels" and "cell_labels") against the *same* work_dir never
|
|
14
|
+
# collides — each gets its own tiles/stage/seg/model/labels.done, and both
|
|
15
|
+
# read the *same* already-converted image.zarr. See docs/guide/snakemake.md
|
|
16
|
+
# "Running two segmentations" for the two-config recipe.
|
|
17
|
+
LABEL_NAME = config.get("label_name", "labels")
|
|
18
|
+
RUN = f"{WORK}/{LABEL_NAME}"
|
|
19
|
+
TILES = f"{RUN}/tiles.json"
|
|
20
|
+
STAGE = f"{RUN}/stage.zarr"
|
|
12
21
|
# Completion sentinel for the stage store. Tracking a touch()ed marker instead
|
|
13
22
|
# of directory(STAGE) keeps Snakemake from deleting/recreating the store on a
|
|
14
23
|
# re-run and avoids directory-mtime quirks (same touch() discipline as sopa).
|
|
@@ -17,16 +26,18 @@ STAGE_OK = f"{STAGE}.done"
|
|
|
17
26
|
|
|
18
27
|
# Logs: one shared file for the sequential CPU steps (convert/prepare/merge),
|
|
19
28
|
# one file per tile for the GPU segment jobs.
|
|
20
|
-
LOGS = f"{
|
|
29
|
+
LOGS = f"{RUN}/logs"
|
|
21
30
|
STEPLOG = f"{LOGS}/steps.log"
|
|
22
31
|
|
|
23
32
|
# Marker that the segmentation model is cached locally. Produced by a local
|
|
24
33
|
# rule (runs on the networked submit host) so offline GPU nodes never download.
|
|
25
|
-
|
|
34
|
+
# Namespaced per-run too: two configs using different models must each fetch
|
|
35
|
+
# their own, rather than the second silently reusing the first's marker.
|
|
36
|
+
MODEL_OK = f"{RUN}/model.ready"
|
|
26
37
|
|
|
27
38
|
|
|
28
39
|
def occupied_done(wildcards):
|
|
29
40
|
"""Per-tile markers for the occupied tiles (resolved after the checkpoint)."""
|
|
30
41
|
tiles = checkpoints.prepare.get().output.tiles
|
|
31
42
|
occupied = json.loads(Path(tiles).read_text())["occupied"]
|
|
32
|
-
return [f"{
|
|
43
|
+
return [f"{RUN}/seg/{i}.done" for i in occupied]
|
|
@@ -87,20 +87,26 @@ def open_image(work_dir, channel, level):
|
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def stage_path(work_dir):
|
|
91
|
-
"""Path of the staged-labels store.
|
|
90
|
+
def stage_path(work_dir, label_name):
|
|
91
|
+
"""Path of the staged-labels store for one segmentation run.
|
|
92
|
+
|
|
93
|
+
Namespaced under ``<work_dir>/<label_name>/`` so two segmentations
|
|
94
|
+
(different ``label_name``) can target the same ``work_dir`` without
|
|
95
|
+
colliding — see docs/guide/snakemake.md "Running two segmentations".
|
|
92
96
|
|
|
93
97
|
Parameters
|
|
94
98
|
----------
|
|
95
99
|
work_dir : str or Path
|
|
96
100
|
Workflow output directory.
|
|
101
|
+
label_name : str
|
|
102
|
+
This run's ``label_name`` (from config).
|
|
97
103
|
|
|
98
104
|
Returns
|
|
99
105
|
-------
|
|
100
106
|
str
|
|
101
|
-
``<work_dir>/stage.zarr``.
|
|
107
|
+
``<work_dir>/<label_name>/stage.zarr``.
|
|
102
108
|
"""
|
|
103
|
-
return str(Path(work_dir) / "stage.zarr")
|
|
109
|
+
return str(Path(work_dir) / label_name / "stage.zarr")
|
|
104
110
|
|
|
105
111
|
|
|
106
112
|
def load_tiles_json(path):
|
|
@@ -17,8 +17,9 @@ from _pw import stage_path, start_log
|
|
|
17
17
|
start_log(snakemake.log[0]) # noqa: F821
|
|
18
18
|
cfg = snakemake.config # noqa: F821
|
|
19
19
|
work_dir = cfg["work_dir"]
|
|
20
|
+
label_name = cfg.get("label_name", "labels")
|
|
20
21
|
image_store = str(Path(work_dir) / "image.zarr")
|
|
21
|
-
merged_store = str(Path(work_dir) / "_merged.zarr")
|
|
22
|
+
merged_store = str(Path(work_dir) / label_name / "_merged.zarr")
|
|
22
23
|
|
|
23
24
|
# merge_tile_labels defaults to min(4, cpu_count) workers, so it ignores
|
|
24
25
|
# whatever cpus_per_task the "merge" rule was actually allocated in the SLURM
|
|
@@ -26,7 +27,7 @@ merged_store = str(Path(work_dir) / "_merged.zarr")
|
|
|
26
27
|
# the cores it's paying for; merge_workers: in config.yaml can still override.
|
|
27
28
|
default_workers = int(os.environ.get("SLURM_CPUS_PER_TASK", os.cpu_count() or 4))
|
|
28
29
|
merged = merge_tile_labels(
|
|
29
|
-
stage_path(work_dir),
|
|
30
|
+
stage_path(work_dir, label_name),
|
|
30
31
|
write_to=merged_store,
|
|
31
32
|
input_component="staged",
|
|
32
33
|
sequential_labels=cfg.get("sequential_labels", True),
|
|
@@ -36,18 +37,18 @@ merged = merge_tile_labels(
|
|
|
36
37
|
group = write_labels(
|
|
37
38
|
image_store,
|
|
38
39
|
merged,
|
|
39
|
-
name=
|
|
40
|
+
name=label_name,
|
|
40
41
|
n_levels=int(cfg.get("pyramid_levels", 5)),
|
|
41
42
|
downscale=int(cfg.get("pyramid_downscale", 2)),
|
|
42
43
|
overwrite=True,
|
|
43
44
|
)
|
|
44
45
|
|
|
45
46
|
shutil.rmtree(merged_store, ignore_errors=True)
|
|
46
|
-
shutil.rmtree(stage_path(work_dir), ignore_errors=True)
|
|
47
|
+
shutil.rmtree(stage_path(work_dir, label_name), ignore_errors=True)
|
|
47
48
|
# Also drop the checkpoint's completion sentinel (stage.zarr.done): the
|
|
48
49
|
# "prepare" rule's stage=touch(STAGE_OK) output must not outlive the store it
|
|
49
50
|
# claims exists, or a future rerun (e.g. re-segmenting for new labels) skips
|
|
50
51
|
# "prepare" and "segment" tries to open a stage.zarr that's already gone.
|
|
51
|
-
Path(f"{stage_path(work_dir)}.done").unlink(missing_ok=True)
|
|
52
|
+
Path(f"{stage_path(work_dir, label_name)}.done").unlink(missing_ok=True)
|
|
52
53
|
print(f"[patchworks] labels written to {group}")
|
|
53
54
|
open(snakemake.output[0], "w").close() # noqa: F821
|
|
@@ -16,6 +16,8 @@ from _pw import open_image, stage_path, start_log
|
|
|
16
16
|
start_log(snakemake.log[0]) # noqa: F821
|
|
17
17
|
cfg = snakemake.config # noqa: F821
|
|
18
18
|
work_dir = cfg["work_dir"]
|
|
19
|
+
label_name = cfg.get("label_name", "labels")
|
|
20
|
+
Path(work_dir, label_name).mkdir(parents=True, exist_ok=True)
|
|
19
21
|
image = open_image(work_dir, cfg["channel"], cfg["level"])
|
|
20
22
|
|
|
21
23
|
ts = cfg.get("tile_shape", "auto")
|
|
@@ -46,9 +48,9 @@ if cfg.get("skip_empty", True):
|
|
|
46
48
|
occ = info["occupancy"].ravel() # row-major, matches spatial_tiles
|
|
47
49
|
occupied = [i for i in range(len(tiles)) if occ[i]]
|
|
48
50
|
|
|
49
|
-
create_stage(stage_path(work_dir), image.shape, tile_shape)
|
|
51
|
+
create_stage(stage_path(work_dir, label_name), image.shape, tile_shape)
|
|
50
52
|
|
|
51
|
-
Path(work_dir, "tiles.json").write_text(
|
|
53
|
+
Path(work_dir, label_name, "tiles.json").write_text(
|
|
52
54
|
json.dumps(
|
|
53
55
|
{
|
|
54
56
|
"tile_shape": list(tile_shape),
|
|
@@ -12,6 +12,7 @@ start_log(snakemake.log[0]) # noqa: F821
|
|
|
12
12
|
cfg = snakemake.config # noqa: F821
|
|
13
13
|
index = int(snakemake.wildcards.index) # noqa: F821
|
|
14
14
|
work_dir = cfg["work_dir"]
|
|
15
|
+
label_name = cfg.get("label_name", "labels")
|
|
15
16
|
|
|
16
17
|
manifest = load_tiles_json(snakemake.input.tiles) # noqa: F821
|
|
17
18
|
image = open_image(work_dir, cfg["channel"], cfg["level"])
|
|
@@ -19,7 +20,7 @@ image = open_image(work_dir, cfg["channel"], cfg["level"])
|
|
|
19
20
|
stage_tile(
|
|
20
21
|
image,
|
|
21
22
|
build_fn(cfg),
|
|
22
|
-
stage_path(work_dir),
|
|
23
|
+
stage_path(work_dir, label_name),
|
|
23
24
|
index,
|
|
24
25
|
tile_shape=tuple(manifest["tile_shape"]),
|
|
25
26
|
overlap=int(manifest["overlap"]),
|
|
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
|