patchworks 1.3.0__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.0 → patchworks-1.3.2}/PKG-INFO +1 -1
  2. {patchworks-1.3.0 → 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.0 → patchworks-1.3.2}/docs/guide/ome_zarr_napari.md +10 -0
  7. {patchworks-1.3.0 → patchworks-1.3.2}/docs/guide/snakemake.md +19 -210
  8. {patchworks-1.3.0 → patchworks-1.3.2}/mkdocs.yml +4 -1
  9. {patchworks-1.3.0 → patchworks-1.3.2}/pyproject.toml +6 -5
  10. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/plugins/dog.py +1 -1
  11. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/plugins/napari.py +2 -2
  12. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/plugins/ome_zarr.py +4 -2
  13. {patchworks-1.3.0 → patchworks-1.3.2}/.github/workflows/docs.yml +0 -0
  14. {patchworks-1.3.0 → patchworks-1.3.2}/.github/workflows/lint.yml +0 -0
  15. {patchworks-1.3.0 → patchworks-1.3.2}/.github/workflows/release.yml +0 -0
  16. {patchworks-1.3.0 → patchworks-1.3.2}/.gitignore +0 -0
  17. {patchworks-1.3.0 → patchworks-1.3.2}/.markdownlint-cli2.yaml +0 -0
  18. {patchworks-1.3.0 → patchworks-1.3.2}/LICENSE +0 -0
  19. {patchworks-1.3.0 → patchworks-1.3.2}/README.md +0 -0
  20. {patchworks-1.3.0 → patchworks-1.3.2}/cliff.toml +0 -0
  21. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/chunks.md +0 -0
  22. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/cluster.md +0 -0
  23. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/io.md +0 -0
  24. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/merge_tile_labels.md +0 -0
  25. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/plugins/cellpose.md +0 -0
  26. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/plugins/dog.md +0 -0
  27. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/plugins/napari.md +0 -0
  28. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/plugins/ome_zarr.md +0 -0
  29. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/postprocess.md +0 -0
  30. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/relabel.md +0 -0
  31. {patchworks-1.3.0 → patchworks-1.3.2}/docs/api/tile_process.md +0 -0
  32. {patchworks-1.3.0 → patchworks-1.3.2}/docs/assets/logo.png +0 -0
  33. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/cellpose_2d.md +0 -0
  34. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/cellpose_2d.py +0 -0
  35. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/cellpose_3d.md +0 -0
  36. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/cellpose_3d.py +0 -0
  37. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/custom.md +0 -0
  38. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/custom_method.py +0 -0
  39. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/dog.py +0 -0
  40. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/standalone_merge.md +0 -0
  41. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/stardist.md +0 -0
  42. {patchworks-1.3.0 → patchworks-1.3.2}/docs/examples/stardist_2d.py +0 -0
  43. {patchworks-1.3.0 → patchworks-1.3.2}/docs/getting_started.md +0 -0
  44. {patchworks-1.3.0 → patchworks-1.3.2}/docs/guide/gpu_distributed.md +0 -0
  45. {patchworks-1.3.0 → patchworks-1.3.2}/docs/guide/merging.md +0 -0
  46. {patchworks-1.3.0 → patchworks-1.3.2}/docs/guide/performance.md +0 -0
  47. {patchworks-1.3.0 → patchworks-1.3.2}/docs/guide/pitfalls.md +0 -0
  48. {patchworks-1.3.0 → patchworks-1.3.2}/docs/guide/skip_empty.md +0 -0
  49. {patchworks-1.3.0 → patchworks-1.3.2}/docs/guide/tiling.md +0 -0
  50. {patchworks-1.3.0 → patchworks-1.3.2}/docs/index.md +0 -0
  51. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/__init__.py +0 -0
  52. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_chunks.py +0 -0
  53. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_cluster.py +0 -0
  54. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_core.py +0 -0
  55. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_distributed.py +0 -0
  56. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_io.py +0 -0
  57. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_merge.py +0 -0
  58. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_postprocess.py +0 -0
  59. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_relabel.py +0 -0
  60. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/_relations.py +0 -0
  61. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/plugins/__init__.py +0 -0
  62. {patchworks-1.3.0 → patchworks-1.3.2}/src/patchworks/plugins/cellpose.py +0 -0
  63. {patchworks-1.3.0 → patchworks-1.3.2}/tests/test_core.py +0 -0
  64. {patchworks-1.3.0 → patchworks-1.3.2}/tests/test_distributed.py +0 -0
  65. {patchworks-1.3.0 → patchworks-1.3.2}/tests/test_dog.py +0 -0
  66. {patchworks-1.3.0 → patchworks-1.3.2}/tests/test_napari.py +0 -0
  67. {patchworks-1.3.0 → patchworks-1.3.2}/tests/test_ome_zarr.py +0 -0
  68. {patchworks-1.3.0 → patchworks-1.3.2}/tests/test_postprocess.py +0 -0
  69. {patchworks-1.3.0 → patchworks-1.3.2}/tests/test_relations.py +0 -0
  70. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/README.md +0 -0
  71. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/Snakefile +0 -0
  72. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/config/config.yaml +0 -0
  73. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/config/config_cilia.yaml +0 -0
  74. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/config/config_cyto.yaml +0 -0
  75. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/config/config_nuclei.yaml +0 -0
  76. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/config/multi.yaml +0 -0
  77. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/pixi.toml +0 -0
  78. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/profile/slurm/config.yaml +0 -0
  79. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/rules/common.smk +0 -0
  80. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/rules/convert.smk +0 -0
  81. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/rules/merge.smk +0 -0
  82. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/rules/segment.smk +0 -0
  83. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/scripts/_pw.py +0 -0
  84. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/scripts/convert.py +0 -0
  85. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/scripts/fetch_model.py +0 -0
  86. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/scripts/merge.py +0 -0
  87. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/scripts/prepare_tiles.py +0 -0
  88. {patchworks-1.3.0 → patchworks-1.3.2}/workflow/scripts/run_multi.py +0 -0
  89. {patchworks-1.3.0 → 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.0
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.
@@ -152,6 +152,16 @@ just one channel instead: `view_in_napari("scan.zarr", channel=0)`.
152
152
  napari ships in `patchworks[all]`, or install just it with
153
153
  `pip install "patchworks[napari]"`.
154
154
 
155
+ !!! tip "Measuring all the objects"
156
+ Once labels are loaded, use
157
+ [napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)'s
158
+ "Measure" dock widget for area/centroid/intensity stats — it works
159
+ out-of-core straight off the Labels layer's backing dask/zarr array, so
160
+ it scales to the same huge label images `tile_process` writes, unlike
161
+ plain `skimage.measure.regionprops`. Bundled in `patchworks[napari]`. See
162
+ [Measurements](measurements.md)
163
+ for the non-interactive/headless equivalent.
164
+
155
165
  ## End-to-end
156
166
 
157
167
  ```python
@@ -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,176 +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. Use
365
- [`dask-image`](https://image.dask.org)'s `ndmeasure`, which computes directly
366
- on the dask/zarr-backed arrays, chunk-parallel, without materializing the
367
- volume:
368
-
369
- ```bash
370
- pip install dask-image
371
- ```
372
-
373
- ```python
374
- import dask.array as da
375
- from dask_image.ndmeasure import area, center_of_mass, mean, standard_deviation
376
-
377
- labels = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
378
- image = da.from_zarr("results/image.zarr", component="0")[0] # channel 0, level 0
379
-
380
- ids = da.unique(labels[labels > 0]).compute()
381
- areas = area(image, labels, ids).compute() # voxel counts
382
- means = mean(image, labels, ids).compute() # mean intensity
383
- stds = standard_deviation(image, labels, ids).compute()
384
- centroids = center_of_mass(image, labels, ids).compute() # voxel coords (z, y, x)
385
- ```
327
+ ## Measurements
386
328
 
387
- Multiply `areas` by the voxel's physical volume and `centroids` by the pixel
388
- size (both read straight from the OME-ZARR's own `multiscales` metadata) to
389
- get µm-scale measurements.
390
-
391
- For interactively inspecting individual cells by clicking in the viewer
392
- (not all objects at once), the
393
- [napari-skimage-regionprops](https://github.com/haesleinhuepf/napari-skimage-regionprops)
394
- plugin's table widget works well — point it at a cropped region rather than
395
- the full volume, since it loads its input fully into memory.
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.
396
332
 
397
333
  ## Custom segmentation function
398
334
 
399
- Not using Cellpose? Run **your own** per-tile function — no need to edit the
400
- package. You write one function; patchworks handles everything around it
401
- (tiling, halos, skipping empty tiles, the zarr-native merge, global relabelling,
402
- resume, logs).
403
-
404
- ### The contract
405
-
406
- Your function is called **once per tile**:
407
-
408
- ```python
409
- labels = segment(tile) # plus any kwargs you configure
410
- ```
411
-
412
- | | What you get / must return |
413
- | --- | --- |
414
- | **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`). |
415
- | **Return** | An integer **label** array (not a boolean mask), **same shape** as `tile`. `0` = background; each object a distinct positive integer. |
416
- | **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`). |
417
- | **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. |
418
-
419
- That is the whole interface. Anything that turns an image tile into a label
420
- image works: classic image processing, StarDist, a trained model, an external
421
- binary you shell out to, …
422
-
423
- ### Minimal example (no GPU, no deps beyond scikit-image)
424
-
425
- ```python
426
- # my_seg.py
427
- import numpy as np
428
- from skimage.measure import label
429
-
430
- def segment(tile: np.ndarray, sigma: float = 2.0) -> np.ndarray:
431
- """Threshold + connected components. Returns int32 labels (0 = bg)."""
432
- from skimage.filters import gaussian, threshold_otsu
433
-
434
- smooth = gaussian(tile, sigma=sigma, preserve_range=True)
435
- thr = threshold_otsu(smooth) if smooth.max() > smooth.min() else np.inf
436
- return label(smooth > thr).astype("int32")
437
- ```
438
-
439
- ```yaml
440
- method: "custom"
441
- label_name: "my_labels"
442
- custom:
443
- module: "my_seg" # import name (see "Make it importable")
444
- function: "segment" # default is "segment"
445
- kwargs: # optional — forwarded as segment(tile, **kwargs)
446
- sigma: 1.5
447
- ```
448
-
449
- ### Growing labels afterwards (dilation)
450
-
451
- To grow every label by a few pixels after segmentation — any method, not
452
- just `custom` — set `dilate: N` in the config (see the tip above), or wrap
453
- your function directly with
454
- [`patchworks.dilate_labels`](../api/postprocess.md) when calling the API
455
- yourself:
456
-
457
- ```python
458
- from patchworks import tile_process, dilate_labels
459
- from patchworks.plugins.dog import dog_label_fn
460
-
461
- fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
462
- fn = dilate_labels(fn, iterations=2) # grow each label by 2 px, then run
463
- result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048),
464
- overlap=8, write_to="labels.zarr")
465
- ```
466
-
467
- `dilate_labels` wraps any `(tile) -> labels` function — the same contract
468
- described above — so it works with `dog_label_fn`, `cellpose_fn`, or your
469
- own `segment`. It dilates each tile's labels before the halo is trimmed and
470
- tiles are merged, so `overlap` must still cover the dilation amount.
471
-
472
- ### Real example: StarDist 3-D, with model caching
473
-
474
- Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
475
- own process so this matters less, but for local runs one process segments many
476
- tiles — cache the model at module level (or with `functools.lru_cache`):
477
-
478
- ```python
479
- # stardist_seg.py
480
- import numpy as np
481
-
482
- _MODEL = None
483
-
484
- def _model():
485
- global _MODEL
486
- if _MODEL is None: # loaded once per worker process
487
- from stardist.models import StarDist3D
488
- _MODEL = StarDist3D.from_pretrained("3D_demo")
489
- return _MODEL
490
-
491
- def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
492
- from csbdeep.utils import normalize
493
-
494
- labels, _ = _model().predict_instances(
495
- normalize(tile), prob_thresh=prob_thresh
496
- )
497
- return labels.astype("int32")
498
- ```
499
-
500
- ```yaml
501
- method: "custom"
502
- label_name: "stardist"
503
- custom:
504
- module: "stardist_seg"
505
- function: "segment"
506
- kwargs:
507
- prob_thresh: 0.5
508
- ```
509
-
510
- Using a GPU? The segment jobs already hold one (the `gres: "gpu:1"` request),
511
- so just let your framework see it — nothing extra in the config.
512
-
513
- ### Test it before you submit
514
-
515
- Run your function on one real tile first — it catches shape/dtype bugs in
516
- seconds instead of after a queue wait. Output must be integer, same shape, `0`
517
- for background:
518
-
519
- ```python
520
- from patchworks import load_ome_zarr
521
- from my_seg import segment
522
-
523
- img = load_ome_zarr("results/image.zarr", channel=0, level=0)
524
- tile = img[:, :512, :512].compute() # a small spatial block
525
- out = segment(tile)
526
-
527
- assert out.shape == tile.shape, (out.shape, tile.shape)
528
- assert out.dtype.kind in "iu" # integer labels, not a float mask
529
- print("objects in tile:", int(out.max()))
530
- ```
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.
531
340
 
532
341
  ### Make it importable on the cluster
533
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
@@ -72,11 +72,12 @@ imaris = ["imaris-ims-file-reader"]
72
72
  # - lxml-html-clean: napari's notebook_display imports lxml.html.clean, split
73
73
  # into a separate package in lxml >= 5.2 (else ImportError on Viewer()).
74
74
  # - glasbey: distinct high-contrast label LUTs for view_in_napari.
75
- # - napari-chunked-regionprops: out-of-core regionprops-style measurements
76
- # (area/centroid/intensity stats) for huge Labels layers, straight off
77
- # their backing dask/zarr arrays the "Measure" dock widget. Formerly
78
- # napari-dask-ndmeasure; renamed when its engine dropped dask_image.ndmeasure
79
- # for a chunk-local map/merge that scales with chunk count, not object count.
75
+ # - napari-chunked-regionprops (https://github.com/imcf/napari-chunked-regionprops):
76
+ # out-of-core regionprops-style measurements (area/centroid/intensity
77
+ # stats) for huge Labels layers, straight off their backing dask/zarr
78
+ # arrays the "Measure" dock widget. Formerly napari-dask-ndmeasure;
79
+ # renamed when its engine dropped dask_image.ndmeasure for a chunk-local
80
+ # map/merge that scales with chunk count, not object count.
80
81
  # - pyqt6 < 6.10: PyQt6-Qt6 6.10.2's Windows wheel fails to import (`DLL load
81
82
  # failed while importing QtWidgets: The specified procedure could not be
82
83
  # found`) — confirmed unrelated to napari/qtpy, reproduces on a bare
@@ -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",
@@ -187,8 +187,8 @@ def _label_hint(path: Union[str, Path]) -> dict[str, Any]:
187
187
  during the merge) — the exact id set is then ``range(1, n_objects +
188
188
  1)`` by construction, with no scan needed. Passed through as a Labels
189
189
  layer's ``metadata`` so a downstream consumer (e.g.
190
- napari-chunked-regionprops) can use it instead of re-deriving the id
191
- set from the array itself.
190
+ napari-chunked-regionprops, https://github.com/imcf/napari-chunked-regionprops)
191
+ can use it instead of re-deriving the id set from the array itself.
192
192
 
193
193
  Parameters
194
194
  ----------
@@ -1083,8 +1083,10 @@ def register_labels(
1083
1083
  ``sequential_labels=True``, which means ``ids == range(1, n_objects
1084
1084
  + 1)`` by construction). When given, written into the label group's
1085
1085
  attrs as ``n_objects``/``sequential_labels`` so a downstream reader
1086
- (e.g. napari-chunked-regionprops) can use the known id set instead
1087
- of re-deriving it with a full-volume scan of its own.
1086
+ (e.g. napari-chunked-regionprops,
1087
+ https://github.com/imcf/napari-chunked-regionprops) can use the
1088
+ known id set instead of re-deriving it with a full-volume scan of
1089
+ its own.
1088
1090
 
1089
1091
  Returns
1090
1092
  -------
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes