patchworks 1.4.3__tar.gz → 2.0.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-1.4.3 → patchworks-2.0.0}/PKG-INFO +5 -2
- {patchworks-1.4.3 → patchworks-2.0.0}/README.md +4 -1
- patchworks-2.0.0/docs/api/chunks.md +19 -0
- patchworks-2.0.0/docs/api/io.md +19 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/plugins/ome_zarr.md +12 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/custom_segmentation.md +15 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/gpu_distributed.md +25 -2
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/merging.md +64 -12
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/ome_zarr_napari.md +32 -3
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/performance.md +12 -4
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/skip_empty.md +47 -6
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/snakemake.md +95 -27
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/tiling.md +29 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/__init__.py +30 -4
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_chunks.py +138 -18
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_cluster.py +3 -2
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_core.py +9 -8
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_distributed.py +68 -13
- patchworks-2.0.0/src/patchworks/_gpu.py +158 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_io.py +37 -3
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_merge.py +415 -67
- patchworks-2.0.0/src/patchworks/_occupancy.py +400 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_relations.py +3 -2
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/plugins/cellpose.py +86 -6
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/plugins/dog.py +45 -9
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/plugins/napari.py +4 -3
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/plugins/ome_zarr.py +232 -25
- patchworks-2.0.0/tests/test_allocation.py +78 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/tests/test_core.py +30 -0
- patchworks-2.0.0/tests/test_distributed.py +354 -0
- patchworks-2.0.0/tests/test_gpu.py +93 -0
- patchworks-2.0.0/tests/test_occupancy.py +234 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/tests/test_ome_zarr.py +125 -4
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/README.md +22 -7
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/config/config.yaml +17 -4
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/config/config_cilia.yaml +10 -1
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/config/config_cyto.yaml +7 -1
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/config/config_nuclei.yaml +7 -1
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/profile/slurm/config.yaml +29 -11
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/rules/common.smk +9 -4
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/rules/merge.smk +2 -1
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/rules/segment.smk +9 -3
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/scripts/_pw.py +130 -1
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/scripts/convert.py +10 -0
- patchworks-2.0.0/workflow/scripts/merge.py +120 -0
- patchworks-2.0.0/workflow/scripts/prepare_tiles.py +187 -0
- patchworks-2.0.0/workflow/scripts/run_multi.py +350 -0
- patchworks-2.0.0/workflow/scripts/segment_tile.py +55 -0
- patchworks-1.4.3/docs/api/chunks.md +0 -5
- patchworks-1.4.3/docs/api/io.md +0 -5
- patchworks-1.4.3/tests/test_distributed.py +0 -75
- patchworks-1.4.3/workflow/scripts/merge.py +0 -58
- patchworks-1.4.3/workflow/scripts/prepare_tiles.py +0 -64
- patchworks-1.4.3/workflow/scripts/run_multi.py +0 -194
- patchworks-1.4.3/workflow/scripts/segment_tile.py +0 -30
- {patchworks-1.4.3 → patchworks-2.0.0}/.github/workflows/docs.yml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/.github/workflows/lint.yml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/.github/workflows/release.yml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/.gitignore +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/LICENSE +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/cliff.toml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/cluster.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/plugins/dog.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/postprocess.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/relabel.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/api/tile_process.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/assets/logo.png +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/custom.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/custom_method.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/dog.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/dog.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/stardist.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/getting_started.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/label_relations.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/measurements.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/docs/index.md +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/mkdocs.yml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/pyproject.toml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_postprocess.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/tests/test_dog.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/tests/test_napari.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/tests/test_postprocess.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/tests/test_relations.py +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/Snakefile +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/config/multi.yaml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/pixi.toml +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/rules/convert.smk +0 -0
- {patchworks-1.4.3 → patchworks-2.0.0}/workflow/scripts/fetch_model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.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
|
|
@@ -376,9 +376,12 @@ tiles where the dask-image approach stalls.
|
|
|
376
376
|
| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------- |
|
|
377
377
|
| In-process Dask client | `FutureCancelledError: lost dependencies` | Detected at startup, raises immediately with fix instructions |
|
|
378
378
|
| 3-4× fn recompute during merge | Cellpose runs 3× per tile | Staging writes labels once, merge reads from disk |
|
|
379
|
-
| O(n²) sequential relabelling | Graph construction hangs at 1000+ tiles |
|
|
379
|
+
| O(n²) sequential relabelling | Graph construction hangs at 1000+ tiles | Folded into the merge's own LUT — no extra pass over the volume |
|
|
380
380
|
| Wrong overlap boundary | Output shape mismatch | Always uses `boundary="none"` |
|
|
381
381
|
| Persisting large arrays | Worker OOM | Never persists; keeps dask graph lazy and streams |
|
|
382
|
+
| Sizing work to the whole node | Job OOM-killed on a shared cluster node | Reads the SLURM/cgroup allocation, not `os.cpu_count()` |
|
|
383
|
+
| Rechunking a pyramid level | Threaded scheduler stockpiles intermediates | Levels stream one source chunk per task, bounded by construction |
|
|
384
|
+
| Isotropic halo on flat tiles | 5× the voxels read and segmented, then trimmed | `overlap` takes one width per axis |
|
|
382
385
|
|
|
383
386
|
---
|
|
384
387
|
|
|
@@ -286,9 +286,12 @@ tiles where the dask-image approach stalls.
|
|
|
286
286
|
| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------- |
|
|
287
287
|
| In-process Dask client | `FutureCancelledError: lost dependencies` | Detected at startup, raises immediately with fix instructions |
|
|
288
288
|
| 3-4× fn recompute during merge | Cellpose runs 3× per tile | Staging writes labels once, merge reads from disk |
|
|
289
|
-
| O(n²) sequential relabelling | Graph construction hangs at 1000+ tiles |
|
|
289
|
+
| O(n²) sequential relabelling | Graph construction hangs at 1000+ tiles | Folded into the merge's own LUT — no extra pass over the volume |
|
|
290
290
|
| Wrong overlap boundary | Output shape mismatch | Always uses `boundary="none"` |
|
|
291
291
|
| Persisting large arrays | Worker OOM | Never persists; keeps dask graph lazy and streams |
|
|
292
|
+
| Sizing work to the whole node | Job OOM-killed on a shared cluster node | Reads the SLURM/cgroup allocation, not `os.cpu_count()` |
|
|
293
|
+
| Rechunking a pyramid level | Threaded scheduler stockpiles intermediates | Levels stream one source chunk per task, bounded by construction |
|
|
294
|
+
| Isotropic halo on flat tiles | 5× the voxels read and segmented, then trimmed | `overlap` takes one width per axis |
|
|
292
295
|
|
|
293
296
|
---
|
|
294
297
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Tile sizing
|
|
2
|
+
|
|
3
|
+
::: patchworks.auto_tile_shape
|
|
4
|
+
|
|
5
|
+
::: patchworks.auto_tile_shape_cellpose
|
|
6
|
+
|
|
7
|
+
::: patchworks.auto_overlap
|
|
8
|
+
|
|
9
|
+
::: patchworks.normalize_overlap
|
|
10
|
+
|
|
11
|
+
## Cluster resource detection
|
|
12
|
+
|
|
13
|
+
On a shared node the machine's core count and free RAM say nothing about what
|
|
14
|
+
this job was granted. These read the allocation instead, and everything that
|
|
15
|
+
sizes a worker pool goes through them.
|
|
16
|
+
|
|
17
|
+
::: patchworks.cpu_allocation
|
|
18
|
+
|
|
19
|
+
::: patchworks.safe_worker_count
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# I/O helpers
|
|
2
|
+
|
|
3
|
+
::: patchworks.load_ome_zarr
|
|
4
|
+
|
|
5
|
+
## Deciding which tiles hold signal
|
|
6
|
+
|
|
7
|
+
`estimate_empty_tiles` is a fast **preview** — it samples a centred window per
|
|
8
|
+
tile, so it can miss signal at a tile's edge. `build_occupancy_map` +
|
|
9
|
+
`tile_occupancy` are **exact**: a brick maximum exceeds the threshold exactly
|
|
10
|
+
when some voxel in that brick does. Use the latter pair when the result is
|
|
11
|
+
used as a skip list. See [Skipping empty tiles](../guide/skip_empty.md).
|
|
12
|
+
|
|
13
|
+
::: patchworks.estimate_empty_tiles
|
|
14
|
+
|
|
15
|
+
::: patchworks.build_occupancy_map
|
|
16
|
+
|
|
17
|
+
::: patchworks.tile_occupancy
|
|
18
|
+
|
|
19
|
+
::: patchworks.auto_empty_threshold
|
|
@@ -24,3 +24,15 @@ resolution, matching anisotropic microscopy stacks.
|
|
|
24
24
|
## register_labels
|
|
25
25
|
|
|
26
26
|
::: patchworks.plugins.ome_zarr.register_labels
|
|
27
|
+
|
|
28
|
+
## NGFF metadata layout
|
|
29
|
+
|
|
30
|
+
NGFF 0.4 is defined over zarr v2 and puts its keys at the top level; 0.5 is
|
|
31
|
+
the zarr-v3 revision and nests them under `ome`. patchworks writes whichever
|
|
32
|
+
matches the store, and reads both.
|
|
33
|
+
|
|
34
|
+
::: patchworks.plugins.ome_zarr.ngff_version
|
|
35
|
+
|
|
36
|
+
::: patchworks.plugins.ome_zarr.read_ngff_attr
|
|
37
|
+
|
|
38
|
+
::: patchworks.plugins.ome_zarr.write_ngff_attrs
|
|
@@ -118,6 +118,18 @@ 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 "You do not need to modify patchworks"
|
|
122
|
+
`method: "custom"` imports any `(tile) -> labels` callable, so a new
|
|
123
|
+
method is a module of your own and a config block — nothing in the
|
|
124
|
+
package changes. It then inherits everything the pipeline does: empty
|
|
125
|
+
tiles are skipped, tiles are batched per job, labels are stitched across
|
|
126
|
+
boundaries and renumbered, and the result is written as a calibrated
|
|
127
|
+
pyramid.
|
|
128
|
+
|
|
129
|
+
(The `KNOWN_METHODS` list in `workflow/scripts/_pw.py` is only for
|
|
130
|
+
*built-in* shortcuts like `"cellpose"`. Adding to it is for methods that
|
|
131
|
+
ship with patchworks, not for your own.)
|
|
132
|
+
|
|
121
133
|
## Test it before you submit
|
|
122
134
|
|
|
123
135
|
Run your function on one real tile first — it catches shape/dtype bugs in
|
|
@@ -148,6 +160,9 @@ custom:
|
|
|
148
160
|
module: "my_seg" # import name
|
|
149
161
|
function: "segment" # default is "segment"
|
|
150
162
|
kwargs: # optional — forwarded as segment(tile, **kwargs)
|
|
163
|
+
# checked against your function's signature during
|
|
164
|
+
# `prepare`, so a typo fails on a cheap CPU job
|
|
165
|
+
# rather than in the first GPU job hours later
|
|
151
166
|
sigma: 1.5
|
|
152
167
|
```
|
|
153
168
|
|
|
@@ -73,8 +73,16 @@ FutureCancelledError: lost dependencies
|
|
|
73
73
|
## GPU memory sizing
|
|
74
74
|
|
|
75
75
|
When `use_gpu=True`, patchworks queries free GPU VRAM via `nvidia-ml-py`
|
|
76
|
-
(install: `pip install "patchworks[gpu]"`)
|
|
77
|
-
|
|
76
|
+
(install: `pip install "patchworks[gpu]"`) and keeps 20% of it as headroom,
|
|
77
|
+
because `info.free` is a point-in-time reading of a device you usually do not
|
|
78
|
+
own outright. `auto_tile_shape` then sizes each tile to at most half of that
|
|
79
|
+
budget; `auto_tile_shape_cellpose` uses Cellpose's own memory model instead
|
|
80
|
+
(roughly 20× the raw tile bytes, plus ~2 GiB for the model).
|
|
81
|
+
|
|
82
|
+
The device is resolved from `CUDA_VISIBLE_DEVICES`. This matters on
|
|
83
|
+
multi-GPU nodes: NVML enumerates **every** GPU regardless of `--gres=gpu:1`,
|
|
84
|
+
so querying index 0 unconditionally would read a different card's free memory
|
|
85
|
+
than the one your job was granted.
|
|
78
86
|
|
|
79
87
|
Without `nvidia-ml-py`, a conservative 8 GiB default is used with a warning.
|
|
80
88
|
Install it for accurate sizing on large-VRAM cards (A100, H100):
|
|
@@ -82,3 +90,18 @@ Install it for accurate sizing on large-VRAM cards (A100, H100):
|
|
|
82
90
|
```bash
|
|
83
91
|
pip install "patchworks[gpu]"
|
|
84
92
|
```
|
|
93
|
+
|
|
94
|
+
!!! tip "Sizing for a GPU you can't see"
|
|
95
|
+
The Snakemake workflow plans tiles in `prepare`, which runs on a CPU
|
|
96
|
+
node, so no GPU can be queried there. Set `gpu_memory_gb` in the config
|
|
97
|
+
to the segment GPU's VRAM (e.g. `24` for an RTX 4090) instead of relying
|
|
98
|
+
on the fallback.
|
|
99
|
+
|
|
100
|
+
## Surviving a shared GPU
|
|
101
|
+
|
|
102
|
+
An out-of-memory error on a shared device is often transient — a co-tenant
|
|
103
|
+
job grew, not a tile that doesn't fit. patchworks retries on the GPU with a
|
|
104
|
+
backoff rather than falling back to the CPU, where a single tile can take
|
|
105
|
+
over an hour. Before each backoff it releases its own device memory
|
|
106
|
+
(including any cached Cellpose model), since holding that is exactly what
|
|
107
|
+
starves the other job. Both torch and cupy OOM errors are recognised.
|
|
@@ -28,9 +28,9 @@ This is the same approach used by
|
|
|
28
28
|
|
|
29
29
|
### Step 1: stage
|
|
30
30
|
|
|
31
|
-
Each tile's labels are written to
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
Each tile's labels are written to zarr once. This is critical: without
|
|
32
|
+
staging, any downstream operation that reads the label array re-runs your
|
|
33
|
+
segmentation function. The merge internally reads labels multiple times.
|
|
34
34
|
|
|
35
35
|
```text
|
|
36
36
|
tile_process calls fn once per tile → staged zarr
|
|
@@ -38,14 +38,37 @@ tile_process calls fn once per tile → staged zarr
|
|
|
38
38
|
merge reads from staged zarr (no fn calls)
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
The Snakemake workflow goes further and stages **directly into**
|
|
42
|
+
`image.zarr/labels/<name>/0`, then has the merge rewrite that array in place
|
|
43
|
+
— saving a whole extra write of the volume plus the scratch store's disk. It
|
|
44
|
+
falls back to a separate store when the tile is larger than the label chunk
|
|
45
|
+
cap, since in place the chunking cannot be changed and level 0 has to stay
|
|
46
|
+
pageable for a viewer.
|
|
47
|
+
|
|
48
|
+
### Step 2: make the ids globally unique
|
|
49
|
+
|
|
50
|
+
Tiles write local `1..n`, which collide, so the boundary scan could not
|
|
51
|
+
otherwise tell two different objects apart. If each tile's label **count** is
|
|
52
|
+
known, this is just an exclusive cumulative sum — global id is
|
|
53
|
+
`offset[tile] + local`, computed in `O(n_tiles)` with no read of the volume
|
|
54
|
+
at all. `stage_tile` returns that count for exactly this purpose; pass the
|
|
55
|
+
counts as `label_counts=`.
|
|
56
|
+
|
|
57
|
+
Without counts, the merge falls back to streaming every chunk and renumbering
|
|
58
|
+
it in place — correct, but a full read **and write** of the volume.
|
|
59
|
+
|
|
60
|
+
### Step 3: boundary scan
|
|
42
61
|
|
|
43
62
|
Only the two voxels on either side of each tile boundary are read. For any
|
|
44
|
-
pair of touching non-zero labels `(a, b)`, they must be the same object.
|
|
63
|
+
pair of touching non-zero labels `(a, b)`, they must be the same object. The
|
|
64
|
+
per-tile offsets are applied here, on the fly.
|
|
45
65
|
|
|
46
|
-
I/O cost: `O(n_boundaries × face_area)`, not `O(full_volume)`.
|
|
66
|
+
I/O cost: `O(n_boundaries × face_area)`, not `O(full_volume)`. The columns
|
|
67
|
+
are read in parallel, and a boundary next to a chunk that holds no labels is
|
|
68
|
+
skipped outright — a pair needs a non-zero label on *both* sides, so it could
|
|
69
|
+
never produce one.
|
|
47
70
|
|
|
48
|
-
### Step
|
|
71
|
+
### Step 4: connected components
|
|
49
72
|
|
|
50
73
|
scipy sparse connected components on the touching pairs produces a relabeling
|
|
51
74
|
lookup table. All labels that transitively touch each other are mapped to the
|
|
@@ -53,12 +76,38 @@ same canonical label.
|
|
|
53
76
|
|
|
54
77
|
Cost: `O(n_touching_pairs)`.
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
With `sequential_labels=True` the contiguous renumbering is folded into this
|
|
80
|
+
same LUT. Because the id domain is dense by construction, the surviving ids
|
|
81
|
+
are exactly the distinct LUT values — a `np.unique` over an array the length
|
|
82
|
+
of the object count, with no scan of the volume.
|
|
83
|
+
|
|
84
|
+
### Step 5: parallel relabel
|
|
57
85
|
|
|
58
86
|
The LUT is applied to every tile in parallel via `multiprocessing.Pool`. The
|
|
59
87
|
LUT is shared via process initializer to avoid re-pickling it for every chunk
|
|
60
88
|
(LUTs can be hundreds of MB for dense label volumes).
|
|
61
89
|
|
|
90
|
+
Chunks whose tile wrote no labels are skipped entirely — not read, and not
|
|
91
|
+
written. Zarr never materialises an unwritten chunk and reads it back as the
|
|
92
|
+
fill value, so background regions cost neither I/O nor disk. On a sparse
|
|
93
|
+
image that is most of the volume.
|
|
94
|
+
|
|
95
|
+
When the merge's output *is* its input, this pass rewrites the array in
|
|
96
|
+
place. That is safe because the boundary scan (step 3) has already finished,
|
|
97
|
+
so nothing still needs the original ids.
|
|
98
|
+
|
|
99
|
+
Because an in-place merge destroys its own input, it records how far it got
|
|
100
|
+
on the array itself, and refuses to guess on a re-run:
|
|
101
|
+
|
|
102
|
+
| State found | What happens |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| nothing recorded | fresh tile-local ids — merge normally |
|
|
105
|
+
| `running` | a previous attempt died mid-relabel, so the array is part local and part global. **Refuses**: re-segment to rebuild it. |
|
|
106
|
+
| `done` | already merged — a no-op, so a failure *after* the relabel (the pyramid, say) can simply be retried |
|
|
107
|
+
|
|
108
|
+
Without that, a second pass would add each tile's offset to ids that are
|
|
109
|
+
already global, which can land two unrelated objects on the same id.
|
|
110
|
+
|
|
62
111
|
## Using the merge step standalone
|
|
63
112
|
|
|
64
113
|
You can call the merge step directly on any existing label array or zarr:
|
|
@@ -86,9 +135,10 @@ merged = merge_tile_labels(
|
|
|
86
135
|
|
|
87
136
|
## Sequential label numbering
|
|
88
137
|
|
|
89
|
-
By default, merged labels are globally unique but may be **gappy**
|
|
90
|
-
|
|
91
|
-
counting, `regionprops`, and measurement — the IDs just aren't
|
|
138
|
+
By default, merged labels are globally unique but may be **gappy** — boundary
|
|
139
|
+
merging fuses ids, leaving holes where the absorbed ones were. This is fine
|
|
140
|
+
for counting, `regionprops`, and measurement — the IDs just aren't
|
|
141
|
+
consecutive.
|
|
92
142
|
|
|
93
143
|
For contiguous 1..N numbering, use `sequential_labels=True`:
|
|
94
144
|
|
|
@@ -96,7 +146,9 @@ For contiguous 1..N numbering, use `sequential_labels=True`:
|
|
|
96
146
|
tile_process("image.zarr", fn, write_to="labels.zarr", sequential_labels=True)
|
|
97
147
|
```
|
|
98
148
|
|
|
99
|
-
This
|
|
149
|
+
This is free: it composes into the relabel LUT the merge already applies, so
|
|
150
|
+
it costs a `np.unique` over the object count rather than another pass over
|
|
151
|
+
the volume.
|
|
100
152
|
|
|
101
153
|
!!! warning "Do not use dask's built-in sequential relabel"
|
|
102
154
|
`dask_image.ndmeasure.merge_labels_across_chunk_boundaries` has a
|
|
@@ -22,9 +22,23 @@ natively. Pass `write_to="other.zarr"` to instead write a separate
|
|
|
22
22
|
single-resolution label store, or `output_component="cells"` to name the label
|
|
23
23
|
image.
|
|
24
24
|
|
|
25
|
-
The label pyramid is
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
The label pyramid is streamed chunk by chunk, so it stays OOM-safe even for
|
|
26
|
+
terabyte volumes. Control it with `pyramid_levels` and `pyramid_downscale`.
|
|
27
|
+
|
|
28
|
+
!!! note "NGFF version"
|
|
29
|
+
The metadata layout follows the zarr format being written: **NGFF 0.5**
|
|
30
|
+
(keys nested under an `ome` attribute) on zarr v3, **0.4** (keys at the
|
|
31
|
+
top level) on v2. Writing v3 data with 0.4's top-level layout — which
|
|
32
|
+
earlier versions did — matches neither revision, and a strict 0.5 reader
|
|
33
|
+
finds nothing there.
|
|
34
|
+
|
|
35
|
+
Reading accepts **both** layouts, so stores written by any patchworks
|
|
36
|
+
version still load. Use `read_ngff_attr()` if you need to inspect the
|
|
37
|
+
metadata yourself rather than indexing `attrs["multiscales"]` directly.
|
|
38
|
+
|
|
39
|
+
patchworks' own hints (`n_objects`, `sequential_labels`) are not NGFF
|
|
40
|
+
keys, so they stay at the top level where a consumer finds them without
|
|
41
|
+
knowing the layout.
|
|
28
42
|
|
|
29
43
|
## Why a pyramid?
|
|
30
44
|
|
|
@@ -107,6 +121,21 @@ input — see [Cluster usage](snakemake.md).
|
|
|
107
121
|
to_ome_zarr("scan.ims", "scan.zarr", reuse_pyramid=True)
|
|
108
122
|
```
|
|
109
123
|
|
|
124
|
+
### Axis names
|
|
125
|
+
|
|
126
|
+
For a file, axes come from the reader's own dimension metadata (bioio's
|
|
127
|
+
`dims.order`, Imaris, or the `sequence_pattern` groups) — not guessed. Only
|
|
128
|
+
non-spatial singleton axes are dropped, so a genuine `z=1` survives.
|
|
129
|
+
|
|
130
|
+
For a **bare array** there is nothing to read, so axes are inferred from the
|
|
131
|
+
number of dimensions (`yx`, `zyx`, `czyx`, `tczyx`). That is unambiguous up to
|
|
132
|
+
3-D; from 4-D the leading axis could be channel or time, and patchworks logs a
|
|
133
|
+
warning saying what it guessed. Pass `axes=` to settle it:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
to_ome_zarr(arr, "out.zarr", axes="tzyx") # not the czyx it would assume
|
|
137
|
+
```
|
|
138
|
+
|
|
110
139
|
### Pixel calibration
|
|
111
140
|
|
|
112
141
|
The physical voxel size is read from the input — bioio's `physical_pixel_sizes`,
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
`tile_process` is built so a run **adapts to whatever machine it lands on** and
|
|
4
4
|
can't run out of RAM/VRAM or freeze the box — without you tuning anything.
|
|
5
5
|
|
|
6
|
-
## Automatic,
|
|
6
|
+
## Automatic, allocation-aware concurrency
|
|
7
7
|
|
|
8
8
|
The staging step (running your `fn` once per tile to a temp store) and the
|
|
9
|
-
merge step are sized
|
|
9
|
+
merge step are sized automatically:
|
|
10
10
|
|
|
11
11
|
- **GPU** (`use_gpu=True`) → **one tile at a time**, so concurrent evaluations
|
|
12
12
|
can never exhaust VRAM.
|
|
@@ -14,8 +14,16 @@ merge step are sized to the host automatically:
|
|
|
14
14
|
from the tile size), and always **leaving one core free** so the machine
|
|
15
15
|
stays responsive — it never pins every core.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
"Available" means available **to this process**, not to the machine. On a
|
|
18
|
+
shared cluster node those differ wildly — a 32-core, 128 GB job on a 128-core,
|
|
19
|
+
512 GB node would otherwise size itself for the whole box and get OOM-killed
|
|
20
|
+
while its own accounting said it had room. patchworks takes the smallest of
|
|
21
|
+
`SLURM_MEM_PER_NODE`, `SLURM_MEM_PER_CPU × cpus`, the cgroup limit (the one
|
|
22
|
+
that actually triggers the kill) and `psutil`'s free RAM, and reads the core
|
|
23
|
+
count from `SLURM_CPUS_PER_TASK` or the process' CPU affinity mask.
|
|
24
|
+
|
|
25
|
+
Without any of those signals, a conservative default is used instead of
|
|
26
|
+
guessing high.
|
|
19
27
|
|
|
20
28
|
## Live progress dashboard (GPU runs)
|
|
21
29
|
|
|
@@ -37,7 +37,9 @@ tile_process(
|
|
|
37
37
|
)
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Two ways to decide
|
|
41
|
+
|
|
42
|
+
### `estimate_empty_tiles` — a fast preview
|
|
41
43
|
|
|
42
44
|
For each tile in the grid, only a small centred **sample window** is read
|
|
43
45
|
(default: 24×256×256 voxels). If the maximum value in that window exceeds
|
|
@@ -47,11 +49,50 @@ This is **bounded I/O**: the total data read is `n_tiles × sample_window`,
|
|
|
47
49
|
not the full image. For a 2200-tile image with the default window, this reads
|
|
48
50
|
≈ 30 MB instead of 250 GB — and it runs in seconds.
|
|
49
51
|
|
|
50
|
-
!!! warning "Approximate"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
the **full tile** max
|
|
54
|
-
|
|
52
|
+
!!! warning "Approximate — a preview, not a skip list"
|
|
53
|
+
Only the tile centre is inspected. On a `(16, 1024, 1024)` tile the
|
|
54
|
+
default window covers **6.25% of the tile's area**, so an object in the
|
|
55
|
+
outer ring is invisible. `tile_process` re-tests the **full tile** max
|
|
56
|
+
inline, so nothing is dropped in a real run — but anything that uses this
|
|
57
|
+
result *as* the skip list would drop those tiles for good.
|
|
58
|
+
|
|
59
|
+
### `build_occupancy_map` + `tile_occupancy` — exact
|
|
60
|
+
|
|
61
|
+
Reduces every `block`-sized brick of the image to its **maximum** and stores
|
|
62
|
+
that beside `image.zarr` (about 1/16384 of the image at the default 128 px
|
|
63
|
+
block). `tile_occupancy` then reduces the map over each tile's full footprint.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from patchworks import (
|
|
67
|
+
auto_empty_threshold, block_for_tile, build_occupancy_map, tile_occupancy
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Size the block from the tile, or every tile over-covers the same block and
|
|
71
|
+
# tests occupied — correct, but useless as a skip list.
|
|
72
|
+
build_occupancy_map("image.zarr", block=block_for_tile(TILE))
|
|
73
|
+
info = tile_occupancy(
|
|
74
|
+
"image.zarr", TILE, channel=0, threshold=auto_empty_threshold(img, 0, 0)
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The map is stored **beside** the image (`image.occupancy.zarr`), not inside
|
|
79
|
+
it: it is not an NGFF array, and a zarr hierarchy containing one cannot be
|
|
80
|
+
walked. It is rebuilt automatically if an existing map was built at a
|
|
81
|
+
different block.
|
|
82
|
+
|
|
83
|
+
This is **exact, not approximate**: `block_max > threshold` is true exactly
|
|
84
|
+
when some voxel in that block exceeds the threshold, so comparing pooled
|
|
85
|
+
maxima against a threshold derived from raw voxels answers the same question
|
|
86
|
+
as scanning every voxel. Max-pooling cannot lose a bright voxel.
|
|
87
|
+
|
|
88
|
+
The map is built once and shared by every segmentation reading that image, so
|
|
89
|
+
a three-config run pays for one pooling pass instead of three sampling passes.
|
|
90
|
+
This is what the Snakemake workflow uses, and it builds the map on first use
|
|
91
|
+
for stores converted before the map existed.
|
|
92
|
+
|
|
93
|
+
Skipping a tile pays off twice over: it is never segmented, **and** the merge
|
|
94
|
+
skips its chunk too — neither reading it nor writing zeros over it, so the
|
|
95
|
+
background never reaches disk at all.
|
|
55
96
|
|
|
56
97
|
## Threshold selection
|
|
57
98
|
|
|
@@ -57,9 +57,10 @@ channel: 0 # channel to segment (null = keep all)
|
|
|
57
57
|
level: 0 # pyramid level (0 = full resolution)
|
|
58
58
|
tile_shape: "auto" # "auto", or e.g. [16, 1024, 1024] (zyx)
|
|
59
59
|
gpu_memory_gb: null # for "auto" on SLURM: your segment GPU's VRAM
|
|
60
|
-
overlap: 30
|
|
60
|
+
overlap: [4, 30, 30] # halo ≈ one object diameter; scalar or [z,y,x]
|
|
61
61
|
skip_empty: true # skip background tiles
|
|
62
62
|
empty_threshold: null # null → Otsu
|
|
63
|
+
tiles_per_job: 4 # tiles per SLURM job (sequential, shared model)
|
|
63
64
|
|
|
64
65
|
# segmentation
|
|
65
66
|
method: "cellpose" # "cellpose" (GPU), "threshold" (no GPU), "custom"
|
|
@@ -102,6 +103,29 @@ sequential_labels: true # renumber labels to a contiguous 1..N
|
|
|
102
103
|
8 GiB. Harmless, but to size for the real GPU set `gpu_memory_gb:` to its
|
|
103
104
|
VRAM (e.g. `24`, `40`, `80`) — or just set `tile_shape` explicitly.
|
|
104
105
|
|
|
106
|
+
With `do_3D: true`, `"auto"` pins z to the **full** extent: no z tiling,
|
|
107
|
+
no z-boundary stitching, and Cellpose sees each object's whole depth. An
|
|
108
|
+
explicit z (like `[16, 1024, 1024]`) tiles in z instead. `prepare` logs
|
|
109
|
+
which regime it picked.
|
|
110
|
+
|
|
111
|
+
!!! tip "Use a per-axis `overlap`"
|
|
112
|
+
A scalar halo is applied to every axis. On a `[16, 1024, 1024]` tile,
|
|
113
|
+
`overlap: 30` reads `76 × 1084 × 1084` to keep `16 × 1024 × 1024` — 5.3×
|
|
114
|
+
the voxels it uses, nearly all of it wasted z. `[4, 30, 30]` brings that
|
|
115
|
+
to ~1.7×, i.e. roughly **3× less GPU time for identical results**.
|
|
116
|
+
`prepare` logs the amplification and refuses a halo wider than the tile.
|
|
117
|
+
|
|
118
|
+
!!! tip "Batch tiles per job with `tiles_per_job`"
|
|
119
|
+
Each job pays CUDA init plus a Cellpose weight load — tens of seconds —
|
|
120
|
+
before its first voxel. `tiles_per_job: N` segments N tiles sequentially
|
|
121
|
+
in one process, sharing one loaded model, which is most of the win on
|
|
122
|
+
fast tiles.
|
|
123
|
+
|
|
124
|
+
Tiles in a batch run **sequentially**, so job wall time is roughly
|
|
125
|
+
`N × per-tile time` and must stay inside the profile's `runtime`/QOS
|
|
126
|
+
ceiling. A retry re-runs the whole batch. Measure one tile with
|
|
127
|
+
`seff <jobid>` and size N from that; `1` restores one job per tile.
|
|
128
|
+
|
|
105
129
|
## 4. Dry-run (always do this first)
|
|
106
130
|
|
|
107
131
|
Check the plan without running anything:
|
|
@@ -126,7 +150,7 @@ image or a smoke test. `--rerun-triggers mtime` re-runs only steps whose output
|
|
|
126
150
|
is missing/stale — so upgrading patchworks won't redo the conversion (the SLURM
|
|
127
151
|
profile sets this for you).
|
|
128
152
|
|
|
129
|
-
## 5b. Run on SLURM (one GPU job per
|
|
153
|
+
## 5b. Run on SLURM (one GPU job per batch of tiles)
|
|
130
154
|
|
|
131
155
|
Edit `profile/slurm/config.yaml` for **your** cluster — partitions, account,
|
|
132
156
|
and the GPU request:
|
|
@@ -140,14 +164,15 @@ default-resources:
|
|
|
140
164
|
mem_mb: 16000
|
|
141
165
|
cpus_per_task: 4
|
|
142
166
|
runtime: 60
|
|
167
|
+
retries: 2 # resubmit a failed job, asking for more memory
|
|
143
168
|
set-resources:
|
|
144
169
|
segment: # the GPU step
|
|
145
170
|
slurm_partition: "gpu" # your GPU partition
|
|
146
171
|
slurm_extra: "'--gres=gpu:1'"
|
|
147
|
-
mem_mb: 32000
|
|
172
|
+
mem_mb: "attempt * 32000" # grows on each retry
|
|
148
173
|
runtime: 120
|
|
149
174
|
merge:
|
|
150
|
-
mem_mb:
|
|
175
|
+
mem_mb: "attempt * 64000"
|
|
151
176
|
runtime: 240
|
|
152
177
|
```
|
|
153
178
|
|
|
@@ -159,8 +184,14 @@ python -m snakemake --workflow-profile profile/slurm \
|
|
|
159
184
|
```
|
|
160
185
|
|
|
161
186
|
Snakemake submits `convert`, then `prepare`, then **one `segment` job per
|
|
162
|
-
non-empty
|
|
163
|
-
`merge`. Raise `jobs:` to use more GPUs.
|
|
187
|
+
batch of `tiles_per_job` non-empty tiles** (up to `jobs:` at once → that many
|
|
188
|
+
GPUs in parallel), then `merge`. Raise `jobs:` to use more GPUs.
|
|
189
|
+
|
|
190
|
+
!!! tip "Sizing memory"
|
|
191
|
+
Every step now sizes its own worker counts from what SLURM actually
|
|
192
|
+
granted (`SLURM_CPUS_PER_TASK`, `SLURM_MEM_PER_*`, the cgroup limit)
|
|
193
|
+
rather than the node's totals, and `merge` logs the budget it detected.
|
|
194
|
+
Compare that line against `seff <jobid>` when tuning `mem_mb`.
|
|
164
195
|
|
|
165
196
|
!!! note "GPU request flag"
|
|
166
197
|
Clusters differ. `--gres=gpu:1` is common; some need `--gpus=1` or a
|
|
@@ -211,8 +242,8 @@ result. Keep `mtime` and reruns happen only when an output is missing or stale.
|
|
|
211
242
|
|
|
212
243
|
## Running two segmentations (e.g. nuclei + cytoplasm)
|
|
213
244
|
|
|
214
|
-
Every path the workflow writes — `tiles.json`,
|
|
215
|
-
|
|
245
|
+
Every path the workflow writes — `tiles.json`, per-batch `seg/` markers, the
|
|
246
|
+
cached model, `labels.done` — lives under `work_dir/<label_name>/`, so
|
|
216
247
|
running the workflow **twice with two configs against the same `work_dir`**
|
|
217
248
|
never collides: each run gets its own private subdirectory, and both reuse
|
|
218
249
|
the *same* already-converted `image.zarr` (conversion never re-runs).
|
|
@@ -243,18 +274,22 @@ cellpose:
|
|
|
243
274
|
do_3D: true
|
|
244
275
|
```
|
|
245
276
|
|
|
246
|
-
Run them
|
|
247
|
-
concurrently
|
|
277
|
+
Run them as two independent SLURM submissions — they touch disjoint files, so
|
|
278
|
+
they can run concurrently. Give each its own `--directory`, because
|
|
279
|
+
Snakemake's lock lives in the working directory, not in the config:
|
|
248
280
|
|
|
249
281
|
```bash
|
|
250
|
-
snakemake --workflow-profile profile/slurm --configfile config/config_nuclei.yaml
|
|
251
|
-
|
|
282
|
+
snakemake --workflow-profile profile/slurm --configfile config/config_nuclei.yaml \
|
|
283
|
+
--directory /scratch/results/nuclei_labels/.snakemake
|
|
284
|
+
snakemake --workflow-profile profile/slurm --configfile config/config_cyto.yaml \
|
|
285
|
+
--directory /scratch/results/cyto_labels/.snakemake
|
|
252
286
|
```
|
|
253
287
|
|
|
254
288
|
!!! tip "One command for several segmentations + relations"
|
|
255
289
|
`config/multi.yaml` lists any number of segmentation configs plus which
|
|
256
|
-
pairs to relate afterward; `pixi run multi` (or `multi-slurm`)
|
|
257
|
-
|
|
290
|
+
pairs to relate afterward; `pixi run multi` (or `multi-slurm`) converts
|
|
291
|
+
once, then runs every config **concurrently** with its own state
|
|
292
|
+
directory, and writes a workbook per pair — see *One command: multiple
|
|
258
293
|
segmentations + relations* below for the config format.
|
|
259
294
|
|
|
260
295
|
Both land side by side in the same store:
|
|
@@ -276,10 +311,10 @@ workflow's own automation is below.
|
|
|
276
311
|
|
|
277
312
|
### One command: multiple segmentations + relations
|
|
278
313
|
|
|
279
|
-
`scripts/run_multi.py` (wired up as `pixi run multi`)
|
|
280
|
-
|
|
281
|
-
configured relation — one command instead of juggling several
|
|
282
|
-
calls and a separate Python step.
|
|
314
|
+
`scripts/run_multi.py` (wired up as `pixi run multi`) drives the above: it
|
|
315
|
+
converts once, runs every segmentation config **concurrently**, then computes
|
|
316
|
+
and saves every configured relation — one command instead of juggling several
|
|
317
|
+
`snakemake` calls and a separate Python step.
|
|
283
318
|
|
|
284
319
|
```yaml
|
|
285
320
|
# config/multi.yaml
|
|
@@ -299,10 +334,22 @@ pixi run multi # run locally
|
|
|
299
334
|
pixi run multi-slurm # submit every segmentation to SLURM
|
|
300
335
|
```
|
|
301
336
|
|
|
302
|
-
|
|
303
|
-
`label_relations` has
|
|
304
|
-
|
|
305
|
-
|
|
337
|
+
Before anything is submitted, the script checks that every listed config
|
|
338
|
+
shares one `work_dir` (so `label_relations` has a single `image.zarr` to read
|
|
339
|
+
both label groups from), that `tile_shape` and `level` are identical (so the
|
|
340
|
+
label arrays share a chunk layout), and that `label_name` is unique — two
|
|
341
|
+
configs sharing one would silently overwrite each other's outputs. `relations`
|
|
342
|
+
is optional; omit it to just run segmentations without a relation step.
|
|
343
|
+
|
|
344
|
+
The configs then run at the same time, each with its own state directory, so
|
|
345
|
+
the GPU partition stays busy instead of idling through every config's
|
|
346
|
+
`prepare` and multi-hour `merge` in turn. A config that fails does **not**
|
|
347
|
+
abort the others; you get a per-config status and a non-zero exit.
|
|
348
|
+
|
|
349
|
+
!!! warning "`jobs:` is per config"
|
|
350
|
+
The profile's `jobs:` caps one Snakemake process. Running three configs
|
|
351
|
+
concurrently can therefore have 3× that many jobs in flight — lower it if
|
|
352
|
+
that would exceed your cluster quota.
|
|
306
353
|
|
|
307
354
|
Each `output:` is an Excel workbook (`openpyxl`, part of the `workflow`
|
|
308
355
|
extra) with two sheets:
|
|
@@ -422,13 +469,34 @@ from patchworks import (
|
|
|
422
469
|
)
|
|
423
470
|
from patchworks.plugins.ome_zarr import write_labels
|
|
424
471
|
|
|
472
|
+
TILE = (16, 1024, 1024)
|
|
425
473
|
img = load_ome_zarr("image.zarr", channel=0)
|
|
426
|
-
tiles = spatial_tiles(img.shape, tile_shape=
|
|
427
|
-
create_stage("stage.zarr", img.shape,
|
|
428
|
-
|
|
474
|
+
tiles = spatial_tiles(img.shape, tile_shape=TILE)
|
|
475
|
+
create_stage("stage.zarr", img.shape, TILE)
|
|
476
|
+
|
|
477
|
+
# (distribute these across jobs:) each returns how many labels it wrote
|
|
478
|
+
counts = {}
|
|
429
479
|
for i in range(len(tiles)):
|
|
430
|
-
|
|
480
|
+
counts[i] = stage_tile(
|
|
481
|
+
img, my_fn, "stage.zarr", i, tile_shape=TILE, overlap=[4, 30, 30]
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Handing those counts to the merge lets it derive each tile's global id range
|
|
485
|
+
# by a cumulative sum, instead of streaming the whole store to renumber it.
|
|
431
486
|
merged = merge_tile_labels("stage.zarr", input_component="staged",
|
|
432
|
-
write_to="merged.zarr", sequential_labels=True
|
|
487
|
+
write_to="merged.zarr", sequential_labels=True,
|
|
488
|
+
label_counts=counts)
|
|
433
489
|
write_labels("image.zarr", merged, name="cells")
|
|
434
490
|
```
|
|
491
|
+
|
|
492
|
+
The workflow goes two steps further. It stages **into**
|
|
493
|
+
`image.zarr/labels/cells/0` in the first place, then has the merge rewrite
|
|
494
|
+
that array **in place** (`write_to=` and `input_component=`/
|
|
495
|
+
`output_component=` all pointing at it) and calls `register_labels` to add
|
|
496
|
+
the pyramid. That removes both the scratch store and the full-volume copy
|
|
497
|
+
`write_labels` would otherwise do — three passes over the label volume become
|
|
498
|
+
two.
|
|
499
|
+
|
|
500
|
+
It falls back to the separate store above when the tile is larger than the
|
|
501
|
+
label chunk cap, because in place the chunking cannot be changed and level 0
|
|
502
|
+
has to stay pageable for a viewer.
|