patchworks 1.3.1__tar.gz → 1.3.2__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.2}/PKG-INFO +1 -1
  2. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/dog.md +3 -3
  3. patchworks-1.3.2/docs/guide/custom_segmentation.md +154 -0
  4. patchworks-1.3.2/docs/guide/label_relations.md +44 -0
  5. patchworks-1.3.2/docs/guide/measurements.md +48 -0
  6. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/ome_zarr_napari.md +1 -1
  7. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/snakemake.md +19 -223
  8. {patchworks-1.3.1 → patchworks-1.3.2}/mkdocs.yml +4 -1
  9. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/dog.py +1 -1
  10. {patchworks-1.3.1 → patchworks-1.3.2}/.github/workflows/docs.yml +0 -0
  11. {patchworks-1.3.1 → patchworks-1.3.2}/.github/workflows/lint.yml +0 -0
  12. {patchworks-1.3.1 → patchworks-1.3.2}/.github/workflows/release.yml +0 -0
  13. {patchworks-1.3.1 → patchworks-1.3.2}/.gitignore +0 -0
  14. {patchworks-1.3.1 → patchworks-1.3.2}/.markdownlint-cli2.yaml +0 -0
  15. {patchworks-1.3.1 → patchworks-1.3.2}/LICENSE +0 -0
  16. {patchworks-1.3.1 → patchworks-1.3.2}/README.md +0 -0
  17. {patchworks-1.3.1 → patchworks-1.3.2}/cliff.toml +0 -0
  18. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/chunks.md +0 -0
  19. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/cluster.md +0 -0
  20. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/io.md +0 -0
  21. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/merge_tile_labels.md +0 -0
  22. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/cellpose.md +0 -0
  23. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/dog.md +0 -0
  24. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/napari.md +0 -0
  25. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/ome_zarr.md +0 -0
  26. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/postprocess.md +0 -0
  27. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/relabel.md +0 -0
  28. {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/tile_process.md +0 -0
  29. {patchworks-1.3.1 → patchworks-1.3.2}/docs/assets/logo.png +0 -0
  30. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_2d.md +0 -0
  31. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_2d.py +0 -0
  32. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_3d.md +0 -0
  33. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_3d.py +0 -0
  34. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/custom.md +0 -0
  35. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/custom_method.py +0 -0
  36. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/dog.py +0 -0
  37. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/standalone_merge.md +0 -0
  38. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/stardist.md +0 -0
  39. {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/stardist_2d.py +0 -0
  40. {patchworks-1.3.1 → patchworks-1.3.2}/docs/getting_started.md +0 -0
  41. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/gpu_distributed.md +0 -0
  42. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/merging.md +0 -0
  43. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/performance.md +0 -0
  44. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/pitfalls.md +0 -0
  45. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/skip_empty.md +0 -0
  46. {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/tiling.md +0 -0
  47. {patchworks-1.3.1 → patchworks-1.3.2}/docs/index.md +0 -0
  48. {patchworks-1.3.1 → patchworks-1.3.2}/pyproject.toml +0 -0
  49. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/__init__.py +0 -0
  50. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_chunks.py +0 -0
  51. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_cluster.py +0 -0
  52. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_core.py +0 -0
  53. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_distributed.py +0 -0
  54. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_io.py +0 -0
  55. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_merge.py +0 -0
  56. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_postprocess.py +0 -0
  57. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_relabel.py +0 -0
  58. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_relations.py +0 -0
  59. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/__init__.py +0 -0
  60. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/cellpose.py +0 -0
  61. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/napari.py +0 -0
  62. {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/ome_zarr.py +0 -0
  63. {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_core.py +0 -0
  64. {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_distributed.py +0 -0
  65. {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_dog.py +0 -0
  66. {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_napari.py +0 -0
  67. {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_ome_zarr.py +0 -0
  68. {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_postprocess.py +0 -0
  69. {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_relations.py +0 -0
  70. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/README.md +0 -0
  71. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/Snakefile +0 -0
  72. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config.yaml +0 -0
  73. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config_cilia.yaml +0 -0
  74. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config_cyto.yaml +0 -0
  75. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config_nuclei.yaml +0 -0
  76. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/multi.yaml +0 -0
  77. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/pixi.toml +0 -0
  78. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/profile/slurm/config.yaml +0 -0
  79. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/common.smk +0 -0
  80. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/convert.smk +0 -0
  81. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/merge.smk +0 -0
  82. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/segment.smk +0 -0
  83. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/_pw.py +0 -0
  84. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/convert.py +0 -0
  85. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/fetch_model.py +0 -0
  86. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/merge.py +0 -0
  87. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/prepare_tiles.py +0 -0
  88. {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/run_multi.py +0 -0
  89. {patchworks-1.3.1 → patchworks-1.3.2}/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.2
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,154 @@
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
+ ## Real example: StarDist 3-D, with model caching
78
+
79
+ Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
80
+ own process so this matters less, but for local runs one process segments many
81
+ tiles — cache the model at module level (or with `functools.lru_cache`):
82
+
83
+ ```python
84
+ # stardist_seg.py
85
+ import numpy as np
86
+
87
+ _MODEL = None
88
+
89
+ def _model():
90
+ global _MODEL
91
+ if _MODEL is None: # loaded once per worker process
92
+ from stardist.models import StarDist3D
93
+ _MODEL = StarDist3D.from_pretrained("3D_demo")
94
+ return _MODEL
95
+
96
+ def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
97
+ from csbdeep.utils import normalize
98
+
99
+ labels, _ = _model().predict_instances(
100
+ normalize(tile), prob_thresh=prob_thresh
101
+ )
102
+ return labels.astype("int32")
103
+ ```
104
+
105
+ Using a GPU? Just let your framework see it — nothing extra needed.
106
+
107
+ ## Test it before you submit
108
+
109
+ Run your function on one real tile first — it catches shape/dtype bugs in
110
+ seconds instead of after a queue wait (or a long local run). Output must be
111
+ integer, same shape, `0` for background:
112
+
113
+ ```python
114
+ from patchworks import load_ome_zarr
115
+ from my_seg import segment
116
+
117
+ img = load_ome_zarr("results/image.zarr", channel=0, level=0)
118
+ tile = img[:, :512, :512].compute() # a small spatial block
119
+ out = segment(tile)
120
+
121
+ assert out.shape == tile.shape, (out.shape, tile.shape)
122
+ assert out.dtype.kind in "iu" # integer labels, not a float mask
123
+ print("objects in tile:", int(out.max()))
124
+ ```
125
+
126
+ ## Wiring it into the cluster workflow
127
+
128
+ Point the config at your module and function:
129
+
130
+ ```yaml
131
+ method: "custom"
132
+ label_name: "my_labels"
133
+ custom:
134
+ module: "my_seg" # import name
135
+ function: "segment" # default is "segment"
136
+ kwargs: # optional — forwarded as segment(tile, **kwargs)
137
+ sigma: 1.5
138
+ ```
139
+
140
+ Same for the StarDist example above:
141
+
142
+ ```yaml
143
+ method: "custom"
144
+ label_name: "stardist"
145
+ custom:
146
+ module: "stardist_seg"
147
+ function: "segment"
148
+ kwargs:
149
+ prob_thresh: 0.5
150
+ ```
151
+
152
+ For getting the module importable on a compute node, dependency/GPU
153
+ checklist, and troubleshooting, see [Custom functions on the
154
+ 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
@@ -80,15 +80,9 @@ sequential_labels: true # renumber labels to a contiguous 1..N
80
80
 
81
81
  !!! tip "Growing labels after segmentation"
82
82
  `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.
83
+ regardless of `method`. `0` (default) disables it. See [Growing labels
84
+ afterwards](custom_segmentation.md#growing-labels-afterwards-dilation)
85
+ for how it works and the equivalent direct-API call.
92
86
 
93
87
  !!! tip "Tile size vs runtime"
94
88
  `tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
@@ -266,39 +260,11 @@ results/image.zarr/labels/cyto_labels/
266
260
  Different segmentations of the same image can use different `channel` and
267
261
  `cellpose:` settings freely, but keep `tile_shape`/`level` the same across
268
262
  configs — the label arrays then share the exact same chunk layout, which
269
- `label_relations()` (below) requires.
263
+ [`label_relations()`](label_relations.md) requires.
270
264
 
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
- ```
265
+ See [Relating labels across segmentations](label_relations.md) for what
266
+ `label_relations()` returns and how to save it yourself — the cluster
267
+ workflow's own automation is below.
302
268
 
303
269
  ### One command: multiple segmentations + relations
304
270
 
@@ -338,9 +304,9 @@ extra) with two sheets:
338
304
  | `<a>` | every non-background `a` label, **including unmatched ones** | `<a>_id`, `<b>_id` (blank if unmatched), `overlap_voxels`, `overlap_fraction` (0 if unmatched) |
339
305
  | `<b>` | every non-background `b` label, **including ones with zero matches** | `<b>_id`, `<a>_count`, `total_overlap_voxels` |
340
306
 
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",
307
+ Unlike calling [`label_relations()`](label_relations.md) directly (which
308
+ only returns matched `a` labels), the workbook always covers every object in
309
+ both segmentations, so counts (e.g. "how many nuclei have no matching cell",
344
310
  "how many cells have zero cilia") aren't silently dropped.
345
311
 
346
312
  Both lists are ordinary lists, so 3+ segmentations work the same way — add
@@ -358,189 +324,19 @@ cyto_labels` and `cilia_labels -> nuclei_labels`) so you can use whichever
358
324
  fits a given dataset. See `config/config_cilia.yaml`. Its deconvolution step
359
325
  needs `pip install "patchworks[dog]"` in the segment jobs' environment.
360
326
 
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]`.
327
+ ## Measurements
374
328
 
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.
329
+ See [Measurements](measurements.md) for computing area/centroid/intensity
330
+ stats on a whole label store, interactively in napari or headless/scripted
331
+ `skimage.measure.regionprops` alone doesn't scale to a store this size.
409
332
 
410
333
  ## Custom segmentation function
411
334
 
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
- ```
335
+ Not using Cellpose? See [Custom segmentation function](custom_segmentation.md)
336
+ for the function contract, examples (including label dilation), and how to
337
+ test it before submitting. The rest of this section covers what's specific to
338
+ running it **on the cluster**: getting the module importable and the
339
+ checklist below.
544
340
 
545
341
  ### Make it importable on the cluster
546
342
 
@@ -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",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes