Skip to content

Commit 1b49540

Browse files
committed
merge dev into main
Adds the method-agnostic dilate_labels post-segmentation step (API, Snakemake config wiring, docs).
2 parents 7da85fc + eb22f28 commit 1b49540

10 files changed

Lines changed: 233 additions & 2 deletions

File tree

docs/api/postprocess.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# dilate_labels
2+
3+
::: patchworks.dilate_labels

docs/examples/dog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ result = tile_process(IMAGE, fn, tile_shape=(1, 1024, 1024), overlap=32)
8181
so edge tiles keep enough context (a plain intensity/threshold halo is
8282
too thin).
8383

84+
## Growing the labels afterwards
85+
86+
DoG spots/threads are often thin — grow each label by a few pixels with
87+
[`dilate_labels`](../api/postprocess.md):
88+
89+
```python
90+
from patchworks import tile_process, dilate_labels
91+
from patchworks.plugins.dog import dog_label_fn
92+
93+
fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
94+
fn = dilate_labels(fn, iterations=2)
95+
tile_process(IMAGE, fn, tile_shape=(1, 1024, 1024), overlap=8, write_to=OUTPUT)
96+
```
97+
98+
On the cluster, set `dilate: 2` in the YAML config instead — it applies to
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).
101+
84102
## Using it in the Snakemake workflow
85103

86104
No dedicated wiring needed — `patchworks.plugins.dog` exposes a `segment(tile, **kwargs)`

docs/guide/snakemake.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ skip_empty: true # skip background tiles
6262
empty_threshold: null # null → Otsu
6363

6464
# segmentation
65-
method: "cellpose" # "cellpose" (GPU) or "threshold" (no GPU)
65+
method: "cellpose" # "cellpose" (GPU), "threshold" (no GPU), "custom"
6666
label_name: "cellpose" # name under image.zarr/labels/
67+
dilate: 0 # optional: pixels to grow labels by, any method
6768
cellpose:
6869
model: "cyto3"
6970
diameter: 30
@@ -77,6 +78,18 @@ pyramid_downscale: 2
7778
sequential_labels: true # renumber labels to a contiguous 1..N
7879
```
7980
81+
!!! tip "Growing labels after segmentation"
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.
92+
8093
!!! tip "Tile size vs runtime"
8194
`tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
8295
more (faster) jobs; very large 3-D tiles are slow. Keep `do_3D: false` (2-D
@@ -433,6 +446,29 @@ custom:
433446
sigma: 1.5
434447
```
435448

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+
436472
### Real example: StarDist 3-D, with model caching
437473

438474
Heavy models must be loaded **once**, not per tile. On SLURM each tile is its

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ nav:
5454
- API Reference:
5555
- tile_process: api/tile_process.md
5656
- merge_tile_labels: api/merge_tile_labels.md
57+
- dilate_labels: api/postprocess.md
5758
- Tile sizing: api/chunks.md
5859
- I/O helpers: api/io.md
5960
- Relabelling: api/relabel.md

src/patchworks/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from ._distributed import create_stage, spatial_tiles, stage_tile
3636
from ._io import estimate_empty_tiles, load_ome_zarr
3737
from ._merge import merge_tile_labels
38+
from ._postprocess import dilate_labels
3839
from ._relabel import relabel_sequential_array, relabel_sequential_zarr
3940
from ._relations import label_relations
4041

@@ -57,4 +58,5 @@
5758
"spatial_tiles",
5859
"create_stage",
5960
"stage_tile",
61+
"dilate_labels",
6062
]

src/patchworks/_postprocess.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Generic post-segmentation wrappers for patchworks.
2+
3+
These wrap any ``fn(tile) -> labels`` callable (a plugin, a custom function,
4+
whatever ``method`` in the Snakemake workflow builds) so the same
5+
post-processing applies regardless of which segmentation method produced the
6+
labels.
7+
8+
Usage
9+
-----
10+
>>> from patchworks import tile_process, dilate_labels
11+
>>> from patchworks.plugins.dog import dog_label_fn
12+
>>>
13+
>>> fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
14+
>>> fn = dilate_labels(fn, iterations=2)
15+
>>> result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048),
16+
... overlap=8, write_to="labels.zarr")
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from functools import partial
22+
from typing import Callable
23+
24+
import numpy as np
25+
26+
27+
def dilate_labels(
28+
fn: Callable[[np.ndarray], np.ndarray],
29+
iterations: int = 1,
30+
*,
31+
use_gpu: bool = False,
32+
) -> Callable[[np.ndarray], np.ndarray]:
33+
"""Wrap a segmentation callable to grow its labels after each tile.
34+
35+
Applies a single-pass grey dilation to whatever ``fn`` returns, before
36+
``tile_process``/``stage_tile`` trim the overlap halo and merge across
37+
tile boundaries — so dilated labels still stitch correctly at tile
38+
edges.
39+
40+
Parameters
41+
----------
42+
fn : Callable[[np.ndarray], np.ndarray]
43+
Any segmentation function with the ``tile_process``/``stage_tile``
44+
contract (one tile in, integer label array out).
45+
iterations : int, optional
46+
Pixels to grow each label by (grey-dilation footprint size
47+
``2 * iterations + 1``, single pass). Default 1. Values ``<= 0``
48+
disable dilation — ``fn`` is returned unwrapped.
49+
use_gpu : bool, optional
50+
Dilate via cupyx instead of scipy. Independent of whatever backend
51+
``fn`` itself uses internally.
52+
53+
Returns
54+
-------
55+
Callable[[np.ndarray], np.ndarray]
56+
Picklable function ready for ``tile_process``/``stage_tile``. If
57+
``iterations <= 0``, this is ``fn`` itself.
58+
"""
59+
if iterations <= 0:
60+
return fn
61+
return partial(_run, fn=fn, iterations=iterations, use_gpu=use_gpu)
62+
63+
64+
def _run(
65+
block: np.ndarray,
66+
fn: Callable[[np.ndarray], np.ndarray],
67+
iterations: int,
68+
use_gpu: bool,
69+
) -> np.ndarray:
70+
"""Run ``fn`` on ``block``, then grow the resulting labels.
71+
72+
Parameters
73+
----------
74+
block : np.ndarray
75+
One image tile.
76+
fn : Callable[[np.ndarray], np.ndarray]
77+
Segmentation function to run first.
78+
iterations : int
79+
Pixels to grow each label by.
80+
use_gpu : bool
81+
Dilate via cupyx instead of scipy.
82+
83+
Returns
84+
-------
85+
np.ndarray
86+
Dilated integer label array, same shape as ``fn``'s output.
87+
"""
88+
labels = fn(block)
89+
size = 2 * iterations + 1
90+
91+
if use_gpu:
92+
import cupy as cp
93+
from cupyx.scipy.ndimage import grey_dilation
94+
95+
labels = cp.asnumpy(grey_dilation(cp.asarray(labels), size=size))
96+
else:
97+
from scipy.ndimage import grey_dilation
98+
99+
labels = grey_dilation(labels, size=size)
100+
101+
return labels

tests/test_postprocess.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Self-contained tests for the dilate_labels post-processing wrapper."""
2+
3+
import pickle
4+
5+
import numpy as np
6+
7+
8+
def _make_blob_labels(shape=(1, 64, 64)):
9+
labels = np.zeros(shape, dtype="int32")
10+
labels[0, 28:36, 28:36] = 1
11+
return labels
12+
13+
14+
def test_dilate_labels_grows_mask():
15+
from patchworks import dilate_labels
16+
17+
fn = lambda tile: _make_blob_labels(tile.shape) # noqa: E731
18+
plain = fn(np.zeros((1, 64, 64)))
19+
dilated = dilate_labels(fn, iterations=2)(np.zeros((1, 64, 64)))
20+
21+
assert (dilated > 0).sum() > (plain > 0).sum()
22+
23+
24+
def test_dilate_labels_zero_iterations_is_noop():
25+
from patchworks import dilate_labels
26+
27+
fn = lambda tile: _make_blob_labels(tile.shape) # noqa: E731
28+
29+
assert dilate_labels(fn, iterations=0) is fn
30+
31+
32+
def test_dilate_labels_picklable():
33+
from patchworks.plugins.dog import dog_label_fn
34+
35+
from patchworks import dilate_labels
36+
37+
fn = dilate_labels(
38+
dog_label_fn(low_sigma=1.0, high_sigma=4.0, threshold=0.01),
39+
iterations=2,
40+
)
41+
pickle.loads(pickle.dumps(fn))

workflow/config/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ empty_threshold: null # null → Otsu; or a number
3030

3131
# ---- segmentation -----------------------------------------------------------
3232
method: "cellpose" # "cellpose" (GPU), "threshold" (no GPU; testing), "custom"
33+
# dilate: 2 # optional: pixels to grow labels by after segmentation, any method
3334
# Also namespaces this run's intermediate files under work_dir/<label_name>/,
3435
# so a second segmentation (different label_name, e.g. nuclei vs cytoplasm)
3536
# can safely target the same work_dir — see docs/guide/snakemake.md

workflow/config/config_cilia.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ skip_empty: true
2525
empty_threshold: null
2626

2727
method: "custom"
28+
# dilate: 2 # optional: pixels to grow labels by after segmentation
2829
label_name: "cilia_labels"
2930
custom:
3031
module: "patchworks.plugins.dog"

workflow/scripts/_pw.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,34 @@ def build_fn(cfg):
133133
cfg : dict
134134
Snakemake config. ``method`` selects ``"cellpose"`` (default), a simple
135135
``"threshold"`` (testing / no-GPU), or ``"custom"`` to import your own
136-
function (``cfg["custom"] = {module, function, kwargs}``).
136+
function (``cfg["custom"] = {module, function, kwargs}``). Optional
137+
``cfg["dilate"]``: int, pixels to grow labels by after segmentation
138+
(via ``patchworks.dilate_labels``), applied regardless of ``method``.
139+
Omitted/0 disables dilation.
140+
141+
Returns
142+
-------
143+
callable
144+
``(ndarray) -> ndarray`` returning integer labels.
145+
"""
146+
fn = _build_method_fn(cfg)
147+
148+
dilate = cfg.get("dilate")
149+
if dilate:
150+
from patchworks import dilate_labels
151+
152+
fn = dilate_labels(fn, iterations=dilate)
153+
154+
return fn
155+
156+
157+
def _build_method_fn(cfg):
158+
"""Build the per-tile segmentation function for ``cfg["method"]``.
159+
160+
Parameters
161+
----------
162+
cfg : dict
163+
Snakemake config, see :func:`build_fn`.
137164
138165
Returns
139166
-------

0 commit comments

Comments
 (0)