Skip to content

Commit 6929c60

Browse files
lguerardclaude
andcommitted
refactor(workflow): ♻️ keep the method list in one place
The validator repeated the dispatcher's list of built-in methods, so adding one meant editing two places that could silently disagree. Both now read KNOWN_METHODS. Also adds the missing test for relabel_sequential_zarr: the merge folds the sequential renumbering into its own LUT now, so nothing internal exercised that function any more, even though it stays public API for label stores written by other pipelines. Documents that a new segmentation method needs no change to patchworks at all -- method: "custom" imports any (tile) -> labels callable and inherits the empty-tile skipping, per-job batching, boundary stitching and pyramid -- and that custom.kwargs are checked against the target's signature in `prepare`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent cfac007 commit 6929c60

3 files changed

Lines changed: 55 additions & 6 deletions

File tree

docs/guide/custom_segmentation.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
118118

119119
Using a GPU? Just let your framework see it — nothing extra needed.
120120

121+
!!! tip "You do not need to modify patchworks"
122+
`method: "custom"` imports any `(tile) -> labels` callable, so a new
123+
method is a module of your own and a config block — nothing in the
124+
package changes. It then inherits everything the pipeline does: empty
125+
tiles are skipped, tiles are batched per job, labels are stitched across
126+
boundaries and renumbered, and the result is written as a calibrated
127+
pyramid.
128+
129+
(The `KNOWN_METHODS` list in `workflow/scripts/_pw.py` is only for
130+
*built-in* shortcuts like `"cellpose"`. Adding to it is for methods that
131+
ship with patchworks, not for your own.)
132+
121133
## Test it before you submit
122134

123135
Run your function on one real tile first — it catches shape/dtype bugs in
@@ -148,6 +160,9 @@ custom:
148160
module: "my_seg" # import name
149161
function: "segment" # default is "segment"
150162
kwargs: # optional — forwarded as segment(tile, **kwargs)
163+
# checked against your function's signature during
164+
# `prepare`, so a typo fails on a cheap CPU job
165+
# rather than in the first GPU job hours later
151166
sigma: 1.5
152167
```
153168

tests/test_core.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,36 @@ def test_relabel_sequential_array():
290290
assert np.all(uniq == np.arange(1, len(uniq) + 1))
291291

292292

293+
def test_relabel_sequential_zarr(tmp_path):
294+
"""The standalone zarr relabeller stays correct on its own.
295+
296+
The merge folds this into its LUT now, so nothing internal exercises it —
297+
but it is public API for label stores written by other pipelines.
298+
"""
299+
import zarr
300+
301+
from patchworks import relabel_sequential_zarr
302+
303+
store = str(tmp_path / "labels.zarr")
304+
root = zarr.open_group(store, mode="w")
305+
root.create_array(name="labels", shape=(4, 8), chunks=(4, 4), dtype="i4")
306+
data = np.zeros((4, 8), dtype="int32")
307+
data[0, :3] = 500 # gappy ids spanning both chunks
308+
data[1, 5:] = 7
309+
data[3, 2] = 100000
310+
root["labels"][:] = data
311+
312+
n = relabel_sequential_zarr(store, "labels")
313+
out = np.asarray(zarr.open_group(store, mode="r")["labels"])
314+
315+
assert n == 3, f"three distinct objects, got {n}"
316+
assert set(np.unique(out).tolist()) == {0, 1, 2, 3}, "must be contiguous"
317+
# Same voxels keep the same id; background stays background.
318+
assert len(set(out[0, :3].tolist())) == 1
319+
assert len(set(out[1, 5:].tolist())) == 1
320+
assert out[2, 0] == 0
321+
322+
293323
def test_estimate_empty_tiles():
294324
import dask.array as da
295325

workflow/scripts/_pw.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515

1616
from patchworks import load_ome_zarr
1717

18+
# Segmentation methods `_build_method_fn` can dispatch. Adding one means
19+
# teaching that function to build it and adding the name here; the validator
20+
# reads this list so the two cannot drift apart. Most new methods need
21+
# neither: `method: "custom"` already imports any `(tile) -> labels` callable.
22+
KNOWN_METHODS = ("cellpose", "threshold", "custom")
23+
1824

1925
class _Tee:
2026
"""Write to several streams at once (e.g. the SLURM log and a file)."""
@@ -181,11 +187,9 @@ def validate_config(cfg) -> None:
181187
problems.append(f"tile_shape must be positive integers; got {ts!r}")
182188

183189
method = cfg.get("method", "cellpose")
184-
if method not in ("cellpose", "threshold", "custom"):
185-
problems.append(
186-
f'method must be "cellpose", "threshold" or "custom"; got '
187-
f"{method!r}"
188-
)
190+
if method not in KNOWN_METHODS:
191+
listed = ", ".join(f'"{m}"' for m in KNOWN_METHODS)
192+
problems.append(f"method must be one of {listed}; got {method!r}")
189193

190194
if method == "cellpose":
191195
cp = cfg.get("cellpose")
@@ -297,7 +301,7 @@ def _build_method_fn(cfg):
297301
callable
298302
``(ndarray) -> ndarray`` returning integer labels.
299303
"""
300-
method = cfg.get("method", "cellpose")
304+
method = cfg.get("method", "cellpose") # see KNOWN_METHODS
301305
if method == "custom":
302306
# Import a user-provided function, e.g.
303307
# custom: {module: my_seg, function: segment, kwargs: {...}}

0 commit comments

Comments
 (0)