patchworks 1.3.1__tar.gz → 1.3.3__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.
Files changed (89) hide show
  1. {patchworks-1.3.1 → patchworks-1.3.3}/PKG-INFO +1 -1
  2. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/dog.md +3 -3
  3. patchworks-1.3.3/docs/guide/custom_segmentation.md +163 -0
  4. patchworks-1.3.3/docs/guide/label_relations.md +44 -0
  5. patchworks-1.3.3/docs/guide/measurements.md +48 -0
  6. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/ome_zarr_napari.md +1 -1
  7. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/snakemake.md +27 -223
  8. {patchworks-1.3.1 → patchworks-1.3.3}/mkdocs.yml +4 -1
  9. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/plugins/dog.py +1 -1
  10. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/config/config.yaml +2 -1
  11. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/config/config_cilia.yaml +2 -1
  12. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/scripts/_pw.py +5 -2
  13. {patchworks-1.3.1 → patchworks-1.3.3}/.github/workflows/docs.yml +0 -0
  14. {patchworks-1.3.1 → patchworks-1.3.3}/.github/workflows/lint.yml +0 -0
  15. {patchworks-1.3.1 → patchworks-1.3.3}/.github/workflows/release.yml +0 -0
  16. {patchworks-1.3.1 → patchworks-1.3.3}/.gitignore +0 -0
  17. {patchworks-1.3.1 → patchworks-1.3.3}/.markdownlint-cli2.yaml +0 -0
  18. {patchworks-1.3.1 → patchworks-1.3.3}/LICENSE +0 -0
  19. {patchworks-1.3.1 → patchworks-1.3.3}/README.md +0 -0
  20. {patchworks-1.3.1 → patchworks-1.3.3}/cliff.toml +0 -0
  21. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/chunks.md +0 -0
  22. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/cluster.md +0 -0
  23. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/io.md +0 -0
  24. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/merge_tile_labels.md +0 -0
  25. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/plugins/cellpose.md +0 -0
  26. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/plugins/dog.md +0 -0
  27. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/plugins/napari.md +0 -0
  28. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/plugins/ome_zarr.md +0 -0
  29. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/postprocess.md +0 -0
  30. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/relabel.md +0 -0
  31. {patchworks-1.3.1 → patchworks-1.3.3}/docs/api/tile_process.md +0 -0
  32. {patchworks-1.3.1 → patchworks-1.3.3}/docs/assets/logo.png +0 -0
  33. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/cellpose_2d.md +0 -0
  34. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/cellpose_2d.py +0 -0
  35. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/cellpose_3d.md +0 -0
  36. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/cellpose_3d.py +0 -0
  37. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/custom.md +0 -0
  38. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/custom_method.py +0 -0
  39. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/dog.py +0 -0
  40. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/standalone_merge.md +0 -0
  41. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/stardist.md +0 -0
  42. {patchworks-1.3.1 → patchworks-1.3.3}/docs/examples/stardist_2d.py +0 -0
  43. {patchworks-1.3.1 → patchworks-1.3.3}/docs/getting_started.md +0 -0
  44. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/gpu_distributed.md +0 -0
  45. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/merging.md +0 -0
  46. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/performance.md +0 -0
  47. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/pitfalls.md +0 -0
  48. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/skip_empty.md +0 -0
  49. {patchworks-1.3.1 → patchworks-1.3.3}/docs/guide/tiling.md +0 -0
  50. {patchworks-1.3.1 → patchworks-1.3.3}/docs/index.md +0 -0
  51. {patchworks-1.3.1 → patchworks-1.3.3}/pyproject.toml +0 -0
  52. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/__init__.py +0 -0
  53. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_chunks.py +0 -0
  54. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_cluster.py +0 -0
  55. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_core.py +0 -0
  56. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_distributed.py +0 -0
  57. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_io.py +0 -0
  58. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_merge.py +0 -0
  59. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_postprocess.py +0 -0
  60. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_relabel.py +0 -0
  61. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/_relations.py +0 -0
  62. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/plugins/__init__.py +0 -0
  63. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/plugins/cellpose.py +0 -0
  64. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/plugins/napari.py +0 -0
  65. {patchworks-1.3.1 → patchworks-1.3.3}/src/patchworks/plugins/ome_zarr.py +0 -0
  66. {patchworks-1.3.1 → patchworks-1.3.3}/tests/test_core.py +0 -0
  67. {patchworks-1.3.1 → patchworks-1.3.3}/tests/test_distributed.py +0 -0
  68. {patchworks-1.3.1 → patchworks-1.3.3}/tests/test_dog.py +0 -0
  69. {patchworks-1.3.1 → patchworks-1.3.3}/tests/test_napari.py +0 -0
  70. {patchworks-1.3.1 → patchworks-1.3.3}/tests/test_ome_zarr.py +0 -0
  71. {patchworks-1.3.1 → patchworks-1.3.3}/tests/test_postprocess.py +0 -0
  72. {patchworks-1.3.1 → patchworks-1.3.3}/tests/test_relations.py +0 -0
  73. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/README.md +0 -0
  74. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/Snakefile +0 -0
  75. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/config/config_cyto.yaml +0 -0
  76. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/config/config_nuclei.yaml +0 -0
  77. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/config/multi.yaml +0 -0
  78. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/pixi.toml +0 -0
  79. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/profile/slurm/config.yaml +0 -0
  80. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/rules/common.smk +0 -0
  81. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/rules/convert.smk +0 -0
  82. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/rules/merge.smk +0 -0
  83. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/rules/segment.smk +0 -0
  84. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/scripts/convert.py +0 -0
  85. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/scripts/fetch_model.py +0 -0
  86. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/scripts/merge.py +0 -0
  87. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/scripts/prepare_tiles.py +0 -0
  88. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/scripts/run_multi.py +0 -0
  89. {patchworks-1.3.1 → patchworks-1.3.3}/workflow/scripts/segment_tile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchworks
3
- Version: 1.3.1
3
+ Version: 1.3.3
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
@@ -97,12 +97,12 @@ tile_process(IMAGE, fn, tile_shape=(1, 1024, 1024), overlap=8, write_to=OUTPUT)
97
97
 
98
98
  On the cluster, set `dilate: 2` in the YAML config instead — it applies to
99
99
  `method: "custom"` (this plugin) the same way it does for `cellpose`/
100
- `threshold`, see [Growing labels after segmentation](../guide/snakemake.md#growing-labels-afterwards-dilation).
100
+ `threshold`, see [Growing labels afterwards](../guide/custom_segmentation.md#growing-labels-afterwards-dilation).
101
101
 
102
102
  ## Using it in the Snakemake workflow
103
103
 
104
104
  No dedicated wiring needed — `patchworks.plugins.dog` exposes a `segment(tile, **kwargs)`
105
- adapter for the documented [`"custom"` method](../guide/snakemake.md#custom-segmentation-function):
105
+ adapter for the documented [`"custom"` method](../guide/custom_segmentation.md):
106
106
 
107
107
  ```yaml
108
108
  method: "custom"
@@ -185,6 +185,6 @@ Checklist specific to this config:
185
185
 
186
186
  Segment the cell body with Cellpose and the cilia with `dog_label_fn` as two
187
187
  separate `tile_process` runs (same image, same `tile_shape`), then use
188
- [`label_relations`](../guide/snakemake.md#relating-labels-across-segmentations)
188
+ [`label_relations`](../guide/label_relations.md)
189
189
  to map each cilium to the cell it belongs to — see
190
190
  `workflow/config/multi.yaml` for the same thing wired up as a cluster job.
@@ -0,0 +1,163 @@
1
+ # Custom segmentation function
2
+
3
+ Not using Cellpose? Run **your own** per-tile function — no need to edit the
4
+ package. You write one function; patchworks handles everything around it
5
+ (tiling, halos, skipping empty tiles, the zarr-native merge, global
6
+ relabelling, resume, logs).
7
+
8
+ This applies the same whether you call the API directly (`tile_process`) or
9
+ run via the [Snakemake cluster workflow](snakemake.md) — see [Wiring it into
10
+ the cluster workflow](#wiring-it-into-the-cluster-workflow) below for the
11
+ config side.
12
+
13
+ ## The contract
14
+
15
+ Your function is called **once per tile**:
16
+
17
+ ```python
18
+ labels = segment(tile) # plus any kwargs you configure
19
+ ```
20
+
21
+ | | What you get / must return |
22
+ | --- | --- |
23
+ | **Input `tile`** | A NumPy array of **one** tile, with the overlap halo already included. The channel and pyramid level are already selected, so it is purely spatial: `(z, y, x)` for a 3-D run, `(y, x)` for 2-D. Dtype is the image's (e.g. `uint16`). |
24
+ | **Return** | An integer **label** array (not a boolean mask), **same shape** as `tile`. `0` = background; each object a distinct positive integer. |
25
+ | **Labels** | Only need to be unique **within the tile**. Don't try to make them globally unique — the merge step stitches objects across tile borders and renumbers everything to a contiguous `1..N` (`sequential_labels: true`). |
26
+ | **Shape** | Must match the input exactly — patchworks trims the halo off your output, so a wrong shape is an error. Don't crop or resize inside the function. |
27
+
28
+ That is the whole interface. Anything that turns an image tile into a label
29
+ image works: classic image processing, StarDist, a trained model, an external
30
+ binary you shell out to, …
31
+
32
+ ## Minimal example (no GPU, no deps beyond scikit-image)
33
+
34
+ ```python
35
+ # my_seg.py
36
+ import numpy as np
37
+ from skimage.measure import label
38
+
39
+ def segment(tile: np.ndarray, sigma: float = 2.0) -> np.ndarray:
40
+ """Threshold + connected components. Returns int32 labels (0 = bg)."""
41
+ from skimage.filters import gaussian, threshold_otsu
42
+
43
+ smooth = gaussian(tile, sigma=sigma, preserve_range=True)
44
+ thr = threshold_otsu(smooth) if smooth.max() > smooth.min() else np.inf
45
+ return label(smooth > thr).astype("int32")
46
+ ```
47
+
48
+ ```python
49
+ from patchworks import tile_process
50
+ from my_seg import segment
51
+
52
+ tile_process("image.zarr", segment, write_to="labels.zarr")
53
+ ```
54
+
55
+ ## Growing labels afterwards (dilation)
56
+
57
+ To grow every label by a few pixels after segmentation, wrap your function
58
+ with [`patchworks.dilate_labels`](../api/postprocess.md):
59
+
60
+ ```python
61
+ from patchworks import tile_process, dilate_labels
62
+ from patchworks.plugins.dog import dog_label_fn
63
+
64
+ fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
65
+ fn = dilate_labels(fn, iterations=2) # grow each label by 2 px, then run
66
+ result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048),
67
+ overlap=8, write_to="labels.zarr")
68
+ ```
69
+
70
+ `dilate_labels` wraps any `(tile) -> labels` function — the same contract
71
+ above — so it works with `dog_label_fn`, `cellpose_fn`, or your own
72
+ `segment`. It dilates each tile's labels before the halo is trimmed and
73
+ tiles are merged, so `overlap` must still cover the dilation amount. On the
74
+ cluster, set `dilate: N` in the config instead — see [Configure the
75
+ run](snakemake.md#3-configure-the-run).
76
+
77
+ By default the dilation itself runs on CPU (scipy), independent of whatever
78
+ backend `fn` used — pass `use_gpu=True` to dilate via cupy instead:
79
+
80
+ ```python
81
+ fn = dilate_labels(fn, iterations=2, use_gpu=True) # needs cupy installed
82
+ ```
83
+
84
+ On the cluster, this is the `dilate_gpu: true` config key.
85
+
86
+ ## Real example: StarDist 3-D, with model caching
87
+
88
+ Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
89
+ own process so this matters less, but for local runs one process segments many
90
+ tiles — cache the model at module level (or with `functools.lru_cache`):
91
+
92
+ ```python
93
+ # stardist_seg.py
94
+ import numpy as np
95
+
96
+ _MODEL = None
97
+
98
+ def _model():
99
+ global _MODEL
100
+ if _MODEL is None: # loaded once per worker process
101
+ from stardist.models import StarDist3D
102
+ _MODEL = StarDist3D.from_pretrained("3D_demo")
103
+ return _MODEL
104
+
105
+ def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
106
+ from csbdeep.utils import normalize
107
+
108
+ labels, _ = _model().predict_instances(
109
+ normalize(tile), prob_thresh=prob_thresh
110
+ )
111
+ return labels.astype("int32")
112
+ ```
113
+
114
+ Using a GPU? Just let your framework see it — nothing extra needed.
115
+
116
+ ## Test it before you submit
117
+
118
+ Run your function on one real tile first — it catches shape/dtype bugs in
119
+ seconds instead of after a queue wait (or a long local run). Output must be
120
+ integer, same shape, `0` for background:
121
+
122
+ ```python
123
+ from patchworks import load_ome_zarr
124
+ from my_seg import segment
125
+
126
+ img = load_ome_zarr("results/image.zarr", channel=0, level=0)
127
+ tile = img[:, :512, :512].compute() # a small spatial block
128
+ out = segment(tile)
129
+
130
+ assert out.shape == tile.shape, (out.shape, tile.shape)
131
+ assert out.dtype.kind in "iu" # integer labels, not a float mask
132
+ print("objects in tile:", int(out.max()))
133
+ ```
134
+
135
+ ## Wiring it into the cluster workflow
136
+
137
+ Point the config at your module and function:
138
+
139
+ ```yaml
140
+ method: "custom"
141
+ label_name: "my_labels"
142
+ custom:
143
+ module: "my_seg" # import name
144
+ function: "segment" # default is "segment"
145
+ kwargs: # optional — forwarded as segment(tile, **kwargs)
146
+ sigma: 1.5
147
+ ```
148
+
149
+ Same for the StarDist example above:
150
+
151
+ ```yaml
152
+ method: "custom"
153
+ label_name: "stardist"
154
+ custom:
155
+ module: "stardist_seg"
156
+ function: "segment"
157
+ kwargs:
158
+ prob_thresh: 0.5
159
+ ```
160
+
161
+ For getting the module importable on a compute node, dependency/GPU
162
+ checklist, and troubleshooting, see [Custom functions on the
163
+ cluster](snakemake.md#custom-segmentation-function) in the cluster guide.
@@ -0,0 +1,44 @@
1
+ # Relating labels across segmentations
2
+
3
+ Once you have two segmentations of the same image (e.g. nuclei inside cells),
4
+ `label_relations()` maps each label in one to the label it overlaps most in
5
+ the other — by streaming both arrays chunk by chunk, so it scales to
6
+ hundreds of thousands of objects without loading anything fully into RAM.
7
+
8
+ Both label arrays must share the exact same chunk layout — same
9
+ `tile_shape`/pyramid `level` when they were produced.
10
+
11
+ ```python
12
+ import dask.array as da
13
+ from patchworks import label_relations
14
+
15
+ nuclei = da.from_zarr("results/image.zarr", component="labels/nuclei_labels/0")
16
+ cells = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
17
+
18
+ table = label_relations(nuclei, cells)
19
+ table[2]
20
+ # {'match': 3, 'overlap_voxels': 4821, 'overlap_fraction': 0.94}
21
+ # -> nucleus 2 belongs to cell 3, 94% of its voxels fall inside it
22
+ ```
23
+
24
+ `table` only contains matched `a` labels (nuclei with at least one
25
+ overlapping voxel in `cells`) — unmatched labels and full per-`b` coverage
26
+ need a bit more bookkeeping (the [cluster workflow's `run_multi.py`
27
+ script](snakemake.md#one-command-multiple-segmentations--relations) does
28
+ this for you and writes it as a two-sheet workbook).
29
+
30
+ Save it as a table yourself:
31
+
32
+ ```python
33
+ import csv
34
+
35
+ with open("nuclei_to_cell.csv", "w", newline="") as f:
36
+ w = csv.writer(f)
37
+ w.writerow(["nucleus_id", "cell_id", "overlap_voxels", "overlap_fraction"])
38
+ for nucleus_id, m in table.items():
39
+ w.writerow([nucleus_id, m["match"], m["overlap_voxels"], m["overlap_fraction"]])
40
+ ```
41
+
42
+ On the cluster, producing the two label stores in the first place is a
43
+ matter of running the workflow twice against the same `work_dir` — see
44
+ [Running two segmentations](snakemake.md#running-two-segmentations-eg-nuclei--cytoplasm).
@@ -0,0 +1,48 @@
1
+ # Measurements (fast, whole-volume regionprops)
2
+
3
+ `skimage.measure.regionprops` needs the full labelled + intensity array in
4
+ RAM — fine for one tile, not for a hundred-thousand-object OME-ZARR.
5
+
6
+ ## Interactively, in napari
7
+
8
+ [napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)
9
+ is built for this — its "Measure" dock widget computes area/centroid/intensity
10
+ stats directly off a Labels layer's dask/zarr-backed array, out-of-core, and
11
+ scales with chunk count rather than object count. It's the best fit for
12
+ measuring *every* object in a store this size, not just a cropped region —
13
+ see [View image + labels in napari](ome_zarr_napari.md#view-image--labels-in-napari).
14
+ Bundled in `patchworks[napari]`.
15
+
16
+ For interactively inspecting individual cells by clicking in the viewer (not
17
+ all objects at once), the
18
+ [napari-skimage-regionprops](https://github.com/haesleinhuepf/napari-skimage-regionprops)
19
+ plugin's table widget also works well — point it at a cropped region rather
20
+ than the full volume, since it loads its input fully into memory.
21
+
22
+ ## Headless / scripted
23
+
24
+ Use [`dask-image`](https://image.dask.org)'s `ndmeasure`, which computes
25
+ directly on the dask/zarr-backed arrays, chunk-parallel, without
26
+ materializing the volume:
27
+
28
+ ```bash
29
+ pip install dask-image
30
+ ```
31
+
32
+ ```python
33
+ import dask.array as da
34
+ from dask_image.ndmeasure import area, center_of_mass, mean, standard_deviation
35
+
36
+ labels = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
37
+ image = da.from_zarr("results/image.zarr", component="0")[0] # channel 0, level 0
38
+
39
+ ids = da.unique(labels[labels > 0]).compute()
40
+ areas = area(image, labels, ids).compute() # voxel counts
41
+ means = mean(image, labels, ids).compute() # mean intensity
42
+ stds = standard_deviation(image, labels, ids).compute()
43
+ centroids = center_of_mass(image, labels, ids).compute() # voxel coords (z, y, x)
44
+ ```
45
+
46
+ Multiply `areas` by the voxel's physical volume and `centroids` by the pixel
47
+ size (both read straight from the OME-ZARR's own `multiscales` metadata) to
48
+ get µm-scale measurements.
@@ -159,7 +159,7 @@ just one channel instead: `view_in_napari("scan.zarr", channel=0)`.
159
159
  out-of-core straight off the Labels layer's backing dask/zarr array, so
160
160
  it scales to the same huge label images `tile_process` writes, unlike
161
161
  plain `skimage.measure.regionprops`. Bundled in `patchworks[napari]`. See
162
- [Measurements](snakemake.md#measurements-fast-whole-volume-regionprops)
162
+ [Measurements](measurements.md)
163
163
  for the non-interactive/headless equivalent.
164
164
 
165
165
  ## End-to-end
@@ -65,6 +65,7 @@ empty_threshold: null # null → Otsu
65
65
  method: "cellpose" # "cellpose" (GPU), "threshold" (no GPU), "custom"
66
66
  label_name: "cellpose" # name under image.zarr/labels/
67
67
  dilate: 0 # optional: pixels to grow labels by, any method
68
+ dilate_gpu: false # dilate via cupy instead of scipy (needs a GPU)
68
69
  cellpose:
69
70
  model: "cyto3"
70
71
  diameter: 30
@@ -80,15 +81,16 @@ sequential_labels: true # renumber labels to a contiguous 1..N
80
81
 
81
82
  !!! tip "Growing labels after segmentation"
82
83
  `dilate: N` grows every label by `N` pixels once segmentation finishes,
83
- regardless of `method` (`cellpose`, `threshold`, or `custom`). It runs
84
- per-tile, before the overlap halo is trimmed and tiles are merged, so
85
- dilated labels still stitch correctly across tile boundaries just make
86
- sure `overlap` covers the dilation amount plus the usual object-diameter
87
- halo. `0` (default) disables it. Under the hood this wraps whatever
88
- segmentation function `method` builds with
89
- [`patchworks.dilate_labels`](../api/postprocess.md); see [Custom
90
- segmentation function](#custom-segmentation-function) below for using it
91
- directly from Python instead of via config.
84
+ regardless of `method`. `0` (default) disables it. Runs on CPU (scipy)
85
+ by default; set `dilate_gpu: true` to dilate via cupy instead that
86
+ needs `cupy` installed in the segment job's environment (matching your
87
+ CUDA version, e.g. `pip install cupy-cuda12x`) and a GPU allocated for
88
+ that job (`set-resources: segment:` in `profile/slurm/config.yaml`,
89
+ same as for a GPU `method`). It's independent of whatever `method`
90
+ itself runs on — you can dilate on GPU even with `method: "threshold"`
91
+ (CPU), or on CPU even with `method: "cellpose"` (GPU). See [Growing
92
+ labels afterwards](custom_segmentation.md#growing-labels-afterwards-dilation)
93
+ for how it works and the equivalent direct-API call.
92
94
 
93
95
  !!! tip "Tile size vs runtime"
94
96
  `tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
@@ -266,39 +268,11 @@ results/image.zarr/labels/cyto_labels/
266
268
  Different segmentations of the same image can use different `channel` and
267
269
  `cellpose:` settings freely, but keep `tile_shape`/`level` the same across
268
270
  configs — the label arrays then share the exact same chunk layout, which
269
- `label_relations()` (below) requires.
271
+ [`label_relations()`](label_relations.md) requires.
270
272
 
271
- ### Relating labels across segmentations
272
-
273
- Once you have two segmentations of the same image (e.g. nuclei inside cells),
274
- `label_relations()` maps each label in one to the label it overlaps most in
275
- the other — by streaming both arrays chunk by chunk, so it scales to
276
- hundreds of thousands of objects without loading anything fully into RAM:
277
-
278
- ```python
279
- import dask.array as da
280
- from patchworks import label_relations
281
-
282
- nuclei = da.from_zarr("results/image.zarr", component="labels/nuclei_labels/0")
283
- cells = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
284
-
285
- table = label_relations(nuclei, cells)
286
- table[2]
287
- # {'match': 3, 'overlap_voxels': 4821, 'overlap_fraction': 0.94}
288
- # -> nucleus 2 belongs to cell 3, 94% of its voxels fall inside it
289
- ```
290
-
291
- Save it as a table:
292
-
293
- ```python
294
- import csv
295
-
296
- with open("nuclei_to_cell.csv", "w", newline="") as f:
297
- w = csv.writer(f)
298
- w.writerow(["nucleus_id", "cell_id", "overlap_voxels", "overlap_fraction"])
299
- for nucleus_id, m in table.items():
300
- w.writerow([nucleus_id, m["match"], m["overlap_voxels"], m["overlap_fraction"]])
301
- ```
273
+ See [Relating labels across segmentations](label_relations.md) for what
274
+ `label_relations()` returns and how to save it yourself — the cluster
275
+ workflow's own automation is below.
302
276
 
303
277
  ### One command: multiple segmentations + relations
304
278
 
@@ -338,9 +312,9 @@ extra) with two sheets:
338
312
  | `<a>` | every non-background `a` label, **including unmatched ones** | `<a>_id`, `<b>_id` (blank if unmatched), `overlap_voxels`, `overlap_fraction` (0 if unmatched) |
339
313
  | `<b>` | every non-background `b` label, **including ones with zero matches** | `<b>_id`, `<a>_count`, `total_overlap_voxels` |
340
314
 
341
- Unlike calling `label_relations()` directly (which only returns matched `a`
342
- labels see below), the workbook always covers every object in both
343
- segmentations, so counts (e.g. "how many nuclei have no matching cell",
315
+ Unlike calling [`label_relations()`](label_relations.md) directly (which
316
+ only returns matched `a` labels), the workbook always covers every object in
317
+ both segmentations, so counts (e.g. "how many nuclei have no matching cell",
344
318
  "how many cells have zero cilia") aren't silently dropped.
345
319
 
346
320
  Both lists are ordinary lists, so 3+ segmentations work the same way — add
@@ -358,189 +332,19 @@ cyto_labels` and `cilia_labels -> nuclei_labels`) so you can use whichever
358
332
  fits a given dataset. See `config/config_cilia.yaml`. Its deconvolution step
359
333
  needs `pip install "patchworks[dog]"` in the segment jobs' environment.
360
334
 
361
- ## Measurements (fast, whole-volume regionprops)
362
-
363
- `skimage.measure.regionprops` needs the full labelled + intensity array in
364
- RAM — fine for one tile, not for a hundred-thousand-object OME-ZARR.
365
-
366
- **Interactively, in napari**, this is what
367
- [napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)
368
- is for — its "Measure" dock widget computes area/centroid/intensity stats
369
- directly off a Labels layer's dask/zarr-backed array, out-of-core, and scales
370
- with chunk count rather than object count. It's the best fit for measuring
371
- *every* object in a store this size, not just a cropped region — see
372
- [View image + labels in napari](ome_zarr_napari.md#view-image--labels-in-napari).
373
- Bundled in `patchworks[napari]`.
335
+ ## Measurements
374
336
 
375
- **Headless/scripted**, use [`dask-image`](https://image.dask.org)'s
376
- `ndmeasure`, which computes directly on the dask/zarr-backed arrays,
377
- chunk-parallel, without materializing the volume:
378
-
379
- ```bash
380
- pip install dask-image
381
- ```
382
-
383
- ```python
384
- import dask.array as da
385
- from dask_image.ndmeasure import area, center_of_mass, mean, standard_deviation
386
-
387
- labels = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
388
- image = da.from_zarr("results/image.zarr", component="0")[0] # channel 0, level 0
389
-
390
- ids = da.unique(labels[labels > 0]).compute()
391
- areas = area(image, labels, ids).compute() # voxel counts
392
- means = mean(image, labels, ids).compute() # mean intensity
393
- stds = standard_deviation(image, labels, ids).compute()
394
- centroids = center_of_mass(image, labels, ids).compute() # voxel coords (z, y, x)
395
- ```
396
-
397
- Multiply `areas` by the voxel's physical volume and `centroids` by the pixel
398
- size (both read straight from the OME-ZARR's own `multiscales` metadata) to
399
- get µm-scale measurements.
400
-
401
- For interactively inspecting individual cells by clicking in the viewer
402
- (not all objects at once), the
403
- [napari-skimage-regionprops](https://github.com/haesleinhuepf/napari-skimage-regionprops)
404
- plugin's table widget works well — point it at a cropped region rather than
405
- the full volume, since it loads its input fully into memory. For measuring
406
- *all* objects at once, use
407
- [napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)
408
- instead (see above) — it doesn't have this in-memory limitation.
337
+ See [Measurements](measurements.md) for computing area/centroid/intensity
338
+ stats on a whole label store, interactively in napari or headless/scripted
339
+ `skimage.measure.regionprops` alone doesn't scale to a store this size.
409
340
 
410
341
  ## Custom segmentation function
411
342
 
412
- Not using Cellpose? Run **your own** per-tile function — no need to edit the
413
- package. You write one function; patchworks handles everything around it
414
- (tiling, halos, skipping empty tiles, the zarr-native merge, global relabelling,
415
- resume, logs).
416
-
417
- ### The contract
418
-
419
- Your function is called **once per tile**:
420
-
421
- ```python
422
- labels = segment(tile) # plus any kwargs you configure
423
- ```
424
-
425
- | | What you get / must return |
426
- | --- | --- |
427
- | **Input `tile`** | A NumPy array of **one** tile, with the overlap halo already included. The channel and pyramid level from the config are already selected, so it is purely spatial: `(z, y, x)` for a 3-D run, `(y, x)` for 2-D. Dtype is the image's (e.g. `uint16`). |
428
- | **Return** | An integer **label** array (not a boolean mask), **same shape** as `tile`. `0` = background; each object a distinct positive integer. |
429
- | **Labels** | Only need to be unique **within the tile**. Don't try to make them globally unique — the merge step stitches objects across tile borders and renumbers everything to a contiguous `1..N` (`sequential_labels: true`). |
430
- | **Shape** | Must match the input exactly — patchworks trims the halo off your output, so a wrong shape is an error. Don't crop or resize inside the function. |
431
-
432
- That is the whole interface. Anything that turns an image tile into a label
433
- image works: classic image processing, StarDist, a trained model, an external
434
- binary you shell out to, …
435
-
436
- ### Minimal example (no GPU, no deps beyond scikit-image)
437
-
438
- ```python
439
- # my_seg.py
440
- import numpy as np
441
- from skimage.measure import label
442
-
443
- def segment(tile: np.ndarray, sigma: float = 2.0) -> np.ndarray:
444
- """Threshold + connected components. Returns int32 labels (0 = bg)."""
445
- from skimage.filters import gaussian, threshold_otsu
446
-
447
- smooth = gaussian(tile, sigma=sigma, preserve_range=True)
448
- thr = threshold_otsu(smooth) if smooth.max() > smooth.min() else np.inf
449
- return label(smooth > thr).astype("int32")
450
- ```
451
-
452
- ```yaml
453
- method: "custom"
454
- label_name: "my_labels"
455
- custom:
456
- module: "my_seg" # import name (see "Make it importable")
457
- function: "segment" # default is "segment"
458
- kwargs: # optional — forwarded as segment(tile, **kwargs)
459
- sigma: 1.5
460
- ```
461
-
462
- ### Growing labels afterwards (dilation)
463
-
464
- To grow every label by a few pixels after segmentation — any method, not
465
- just `custom` — set `dilate: N` in the config (see the tip above), or wrap
466
- your function directly with
467
- [`patchworks.dilate_labels`](../api/postprocess.md) when calling the API
468
- yourself:
469
-
470
- ```python
471
- from patchworks import tile_process, dilate_labels
472
- from patchworks.plugins.dog import dog_label_fn
473
-
474
- fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
475
- fn = dilate_labels(fn, iterations=2) # grow each label by 2 px, then run
476
- result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048),
477
- overlap=8, write_to="labels.zarr")
478
- ```
479
-
480
- `dilate_labels` wraps any `(tile) -> labels` function — the same contract
481
- described above — so it works with `dog_label_fn`, `cellpose_fn`, or your
482
- own `segment`. It dilates each tile's labels before the halo is trimmed and
483
- tiles are merged, so `overlap` must still cover the dilation amount.
484
-
485
- ### Real example: StarDist 3-D, with model caching
486
-
487
- Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
488
- own process so this matters less, but for local runs one process segments many
489
- tiles — cache the model at module level (or with `functools.lru_cache`):
490
-
491
- ```python
492
- # stardist_seg.py
493
- import numpy as np
494
-
495
- _MODEL = None
496
-
497
- def _model():
498
- global _MODEL
499
- if _MODEL is None: # loaded once per worker process
500
- from stardist.models import StarDist3D
501
- _MODEL = StarDist3D.from_pretrained("3D_demo")
502
- return _MODEL
503
-
504
- def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
505
- from csbdeep.utils import normalize
506
-
507
- labels, _ = _model().predict_instances(
508
- normalize(tile), prob_thresh=prob_thresh
509
- )
510
- return labels.astype("int32")
511
- ```
512
-
513
- ```yaml
514
- method: "custom"
515
- label_name: "stardist"
516
- custom:
517
- module: "stardist_seg"
518
- function: "segment"
519
- kwargs:
520
- prob_thresh: 0.5
521
- ```
522
-
523
- Using a GPU? The segment jobs already hold one (the `gres: "gpu:1"` request),
524
- so just let your framework see it — nothing extra in the config.
525
-
526
- ### Test it before you submit
527
-
528
- Run your function on one real tile first — it catches shape/dtype bugs in
529
- seconds instead of after a queue wait. Output must be integer, same shape, `0`
530
- for background:
531
-
532
- ```python
533
- from patchworks import load_ome_zarr
534
- from my_seg import segment
535
-
536
- img = load_ome_zarr("results/image.zarr", channel=0, level=0)
537
- tile = img[:, :512, :512].compute() # a small spatial block
538
- out = segment(tile)
539
-
540
- assert out.shape == tile.shape, (out.shape, tile.shape)
541
- assert out.dtype.kind in "iu" # integer labels, not a float mask
542
- print("objects in tile:", int(out.max()))
543
- ```
343
+ Not using Cellpose? See [Custom segmentation function](custom_segmentation.md)
344
+ for the function contract, examples (including label dilation), and how to
345
+ test it before submitting. The rest of this section covers what's specific to
346
+ running it **on the cluster**: getting the module importable and the
347
+ checklist below.
544
348
 
545
349
  ### Make it importable on the cluster
546
350
 
@@ -41,8 +41,11 @@ nav:
41
41
  - Empty tile skipping: guide/skip_empty.md
42
42
  - GPU & distributed: guide/gpu_distributed.md
43
43
  - Performance & memory: guide/performance.md
44
- - Cluster workflow (Snakemake): guide/snakemake.md
44
+ - Custom segmentation function: guide/custom_segmentation.md
45
+ - Relating labels across segmentations: guide/label_relations.md
46
+ - Measurements: guide/measurements.md
45
47
  - OME-ZARR & napari: guide/ome_zarr_napari.md
48
+ - Cluster workflow (Snakemake): guide/snakemake.md
46
49
  - Pitfalls: guide/pitfalls.md
47
50
  - Examples:
48
51
  - Cellpose 2-D: examples/cellpose_2d.md
@@ -25,7 +25,7 @@ With deconvolution first (widen ``overlap`` to cover the PSF support):
25
25
  >>> result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048), overlap=32)
26
26
 
27
27
  For the Snakemake workflow's ``method: "custom"`` (see
28
- docs/guide/snakemake.md "Custom segmentation function"), use the
28
+ docs/guide/custom_segmentation.md), use the
29
29
  :func:`segment` adapter instead of the factory directly:
30
30
 
31
31
  >>> # custom: {module: "patchworks.plugins.dog", function: "segment",
@@ -30,7 +30,8 @@ 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
- # dilate: 2 # optional: pixels to grow labels by after segmentation, any method
33
+ # dilate: 2 # optional: pixels to grow labels by after segmentation, any method
34
+ # dilate_gpu: true # optional: dilate via cupy instead of scipy, needs a GPU
34
35
  # Also namespaces this run's intermediate files under work_dir/<label_name>/,
35
36
  # so a second segmentation (different label_name, e.g. nuclei vs cytoplasm)
36
37
  # can safely target the same work_dir — see docs/guide/snakemake.md
@@ -25,7 +25,8 @@ skip_empty: true
25
25
  empty_threshold: null
26
26
 
27
27
  method: "custom"
28
- # dilate: 2 # optional: pixels to grow labels by after segmentation
28
+ # dilate: 2 # optional: pixels to grow labels by after segmentation
29
+ # dilate_gpu: true # optional: dilate via cupy instead of scipy, needs a GPU
29
30
  label_name: "cilia_labels"
30
31
  custom:
31
32
  module: "patchworks.plugins.dog"
@@ -136,7 +136,10 @@ def build_fn(cfg):
136
136
  function (``cfg["custom"] = {module, function, kwargs}``). Optional
137
137
  ``cfg["dilate"]``: int, pixels to grow labels by after segmentation
138
138
  (via ``patchworks.dilate_labels``), applied regardless of ``method``.
139
- Omitted/0 disables dilation.
139
+ Omitted/0 disables dilation. ``cfg["dilate_gpu"]``: bool, dilate via
140
+ cupyx instead of scipy (default ``False``); only takes effect when
141
+ ``dilate`` is set, and needs a GPU allocated for the segment job
142
+ (independent of whether ``method`` itself uses one).
140
143
 
141
144
  Returns
142
145
  -------
@@ -149,7 +152,7 @@ def build_fn(cfg):
149
152
  if dilate:
150
153
  from patchworks import dilate_labels
151
154
 
152
- fn = dilate_labels(fn, iterations=dilate)
155
+ fn = dilate_labels(fn, iterations=dilate, use_gpu=cfg.get("dilate_gpu", False))
153
156
 
154
157
  return fn
155
158
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes