patchworks 0.11.7__tar.gz → 0.11.9__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 (70) hide show
  1. {patchworks-0.11.7 → patchworks-0.11.9}/.gitignore +2 -0
  2. {patchworks-0.11.7 → patchworks-0.11.9}/PKG-INFO +11 -3
  3. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/snakemake.md +140 -0
  4. {patchworks-0.11.7 → patchworks-0.11.9}/pyproject.toml +14 -1
  5. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/plugins/napari.py +25 -2
  6. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/config/config.yaml +11 -4
  7. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/profile/slurm/config.yaml +6 -3
  8. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/scripts/_pw.py +19 -2
  9. patchworks-0.11.7/workflow/.snakemake/iocache/latest.pkl +0 -0
  10. {patchworks-0.11.7 → patchworks-0.11.9}/.github/workflows/docs.yml +0 -0
  11. {patchworks-0.11.7 → patchworks-0.11.9}/.github/workflows/lint.yml +0 -0
  12. {patchworks-0.11.7 → patchworks-0.11.9}/.github/workflows/release.yml +0 -0
  13. {patchworks-0.11.7 → patchworks-0.11.9}/.markdownlint-cli2.yaml +0 -0
  14. {patchworks-0.11.7 → patchworks-0.11.9}/README.md +0 -0
  15. {patchworks-0.11.7 → patchworks-0.11.9}/cliff.toml +0 -0
  16. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/chunks.md +0 -0
  17. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/cluster.md +0 -0
  18. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/io.md +0 -0
  19. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/merge_tile_labels.md +0 -0
  20. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/plugins/cellpose.md +0 -0
  21. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/plugins/napari.md +0 -0
  22. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/plugins/ome_zarr.md +0 -0
  23. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/relabel.md +0 -0
  24. {patchworks-0.11.7 → patchworks-0.11.9}/docs/api/tile_process.md +0 -0
  25. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/cellpose_2d.md +0 -0
  26. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/cellpose_2d.py +0 -0
  27. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/cellpose_3d.md +0 -0
  28. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/cellpose_3d.py +0 -0
  29. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/custom.md +0 -0
  30. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/custom_method.py +0 -0
  31. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/standalone_merge.md +0 -0
  32. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/stardist.md +0 -0
  33. {patchworks-0.11.7 → patchworks-0.11.9}/docs/examples/stardist_2d.py +0 -0
  34. {patchworks-0.11.7 → patchworks-0.11.9}/docs/getting_started.md +0 -0
  35. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/gpu_distributed.md +0 -0
  36. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/merging.md +0 -0
  37. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/ome_zarr_napari.md +0 -0
  38. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/performance.md +0 -0
  39. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/pitfalls.md +0 -0
  40. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/skip_empty.md +0 -0
  41. {patchworks-0.11.7 → patchworks-0.11.9}/docs/guide/tiling.md +0 -0
  42. {patchworks-0.11.7 → patchworks-0.11.9}/docs/index.md +0 -0
  43. {patchworks-0.11.7 → patchworks-0.11.9}/mkdocs.yml +0 -0
  44. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/__init__.py +0 -0
  45. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/_chunks.py +0 -0
  46. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/_cluster.py +0 -0
  47. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/_core.py +0 -0
  48. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/_distributed.py +0 -0
  49. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/_io.py +0 -0
  50. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/_merge.py +0 -0
  51. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/_relabel.py +0 -0
  52. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/plugins/__init__.py +0 -0
  53. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/plugins/cellpose.py +0 -0
  54. {patchworks-0.11.7 → patchworks-0.11.9}/src/patchworks/plugins/ome_zarr.py +0 -0
  55. {patchworks-0.11.7 → patchworks-0.11.9}/tests/test_core.py +0 -0
  56. {patchworks-0.11.7 → patchworks-0.11.9}/tests/test_distributed.py +0 -0
  57. {patchworks-0.11.7 → patchworks-0.11.9}/tests/test_napari.py +0 -0
  58. {patchworks-0.11.7 → patchworks-0.11.9}/tests/test_ome_zarr.py +0 -0
  59. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/README.md +0 -0
  60. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/Snakefile +0 -0
  61. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/pixi.toml +0 -0
  62. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/rules/common.smk +0 -0
  63. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/rules/convert.smk +0 -0
  64. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/rules/merge.smk +0 -0
  65. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/rules/segment.smk +0 -0
  66. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/scripts/convert.py +0 -0
  67. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/scripts/fetch_model.py +0 -0
  68. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/scripts/merge.py +0 -0
  69. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/scripts/prepare_tiles.py +0 -0
  70. {patchworks-0.11.7 → patchworks-0.11.9}/workflow/scripts/segment_tile.py +0 -0
@@ -7,3 +7,5 @@ build/
7
7
  site/
8
8
  .pytest_cache/
9
9
  *.log
10
+ .snakemake/
11
+ .pixi/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchworks
3
- Version: 0.11.7
3
+ Version: 0.11.9
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
@@ -29,8 +29,12 @@ Requires-Dist: bioio-lif; extra == 'all'
29
29
  Requires-Dist: bioio-nd2; extra == 'all'
30
30
  Requires-Dist: bioio-ome-tiff; extra == 'all'
31
31
  Requires-Dist: bioio-tifffile; extra == 'all'
32
+ Requires-Dist: glasbey; extra == 'all'
32
33
  Requires-Dist: imaris-ims-file-reader; extra == 'all'
33
- Requires-Dist: napari[all]; extra == 'all'
34
+ Requires-Dist: ipykernel<7; extra == 'all'
35
+ Requires-Dist: lxml-html-clean; extra == 'all'
36
+ Requires-Dist: napari[all]>=0.5.5; extra == 'all'
37
+ Requires-Dist: numpy<2.5; extra == 'all'
34
38
  Requires-Dist: nvidia-ml-py; extra == 'all'
35
39
  Requires-Dist: psutil; extra == 'all'
36
40
  Requires-Dist: scikit-image; extra == 'all'
@@ -62,7 +66,11 @@ Provides-Extra: io
62
66
  Requires-Dist: psutil; extra == 'io'
63
67
  Requires-Dist: tqdm; extra == 'io'
64
68
  Provides-Extra: napari
65
- Requires-Dist: napari[all]; extra == 'napari'
69
+ Requires-Dist: glasbey; extra == 'napari'
70
+ Requires-Dist: ipykernel<7; extra == 'napari'
71
+ Requires-Dist: lxml-html-clean; extra == 'napari'
72
+ Requires-Dist: napari[all]>=0.5.5; extra == 'napari'
73
+ Requires-Dist: numpy<2.5; extra == 'napari'
66
74
  Provides-Extra: workflow
67
75
  Requires-Dist: snakemake-executor-plugin-slurm; extra == 'workflow'
68
76
  Requires-Dist: snakemake>=8; extra == 'workflow'
@@ -194,6 +194,146 @@ also re-runs a step when its **code, params or software environment** change —
194
194
  so upgrading patchworks would re-do the conversion and overwrite an existing
195
195
  result. Keep `mtime` and reruns happen only when an output is missing or stale.
196
196
 
197
+ ## Custom segmentation function
198
+
199
+ Not using Cellpose? Run **your own** per-tile function — no need to edit the
200
+ package. You write one function; patchworks handles everything around it
201
+ (tiling, halos, skipping empty tiles, the zarr-native merge, global relabelling,
202
+ resume, logs).
203
+
204
+ ### The contract
205
+
206
+ Your function is called **once per tile**:
207
+
208
+ ```python
209
+ labels = segment(tile) # plus any kwargs you configure
210
+ ```
211
+
212
+ | | What you get / must return |
213
+ | --- | --- |
214
+ | **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`). |
215
+ | **Return** | An integer **label** array (not a boolean mask), **same shape** as `tile`. `0` = background; each object a distinct positive integer. |
216
+ | **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`). |
217
+ | **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. |
218
+
219
+ That is the whole interface. Anything that turns an image tile into a label
220
+ image works: classic image processing, StarDist, a trained model, an external
221
+ binary you shell out to, …
222
+
223
+ ### Minimal example (no GPU, no deps beyond scikit-image)
224
+
225
+ ```python
226
+ # my_seg.py
227
+ import numpy as np
228
+ from skimage.measure import label
229
+
230
+ def segment(tile: np.ndarray, sigma: float = 2.0) -> np.ndarray:
231
+ """Threshold + connected components. Returns int32 labels (0 = bg)."""
232
+ from skimage.filters import gaussian, threshold_otsu
233
+
234
+ smooth = gaussian(tile, sigma=sigma, preserve_range=True)
235
+ thr = threshold_otsu(smooth) if smooth.max() > smooth.min() else np.inf
236
+ return label(smooth > thr).astype("int32")
237
+ ```
238
+
239
+ ```yaml
240
+ method: "custom"
241
+ label_name: "my_labels"
242
+ custom:
243
+ module: "my_seg" # import name (see "Make it importable")
244
+ function: "segment" # default is "segment"
245
+ kwargs: # optional — forwarded as segment(tile, **kwargs)
246
+ sigma: 1.5
247
+ ```
248
+
249
+ ### Real example: StarDist 3-D, with model caching
250
+
251
+ Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
252
+ own process so this matters less, but for local runs one process segments many
253
+ tiles — cache the model at module level (or with `functools.lru_cache`):
254
+
255
+ ```python
256
+ # stardist_seg.py
257
+ import numpy as np
258
+
259
+ _MODEL = None
260
+
261
+ def _model():
262
+ global _MODEL
263
+ if _MODEL is None: # loaded once per worker process
264
+ from stardist.models import StarDist3D
265
+ _MODEL = StarDist3D.from_pretrained("3D_demo")
266
+ return _MODEL
267
+
268
+ def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
269
+ from csbdeep.utils import normalize
270
+
271
+ labels, _ = _model().predict_instances(
272
+ normalize(tile), prob_thresh=prob_thresh
273
+ )
274
+ return labels.astype("int32")
275
+ ```
276
+
277
+ ```yaml
278
+ method: "custom"
279
+ label_name: "stardist"
280
+ custom:
281
+ module: "stardist_seg"
282
+ function: "segment"
283
+ kwargs:
284
+ prob_thresh: 0.5
285
+ ```
286
+
287
+ Using a GPU? The segment jobs already hold one (the `gres: "gpu:1"` request),
288
+ so just let your framework see it — nothing extra in the config.
289
+
290
+ ### Test it before you submit
291
+
292
+ Run your function on one real tile first — it catches shape/dtype bugs in
293
+ seconds instead of after a queue wait. Output must be integer, same shape, `0`
294
+ for background:
295
+
296
+ ```python
297
+ from patchworks import load_ome_zarr
298
+ from my_seg import segment
299
+
300
+ img = load_ome_zarr("results/image.zarr", channel=0, level=0)
301
+ tile = img[:, :512, :512].compute() # a small spatial block
302
+ out = segment(tile)
303
+
304
+ assert out.shape == tile.shape, (out.shape, tile.shape)
305
+ assert out.dtype.kind in "iu" # integer labels, not a float mask
306
+ print("objects in tile:", int(out.max()))
307
+ ```
308
+
309
+ ### Make it importable on the cluster
310
+
311
+ The segment job runs `import <module>`, so the module must be on the path. Pick
312
+ one:
313
+
314
+ 1. **Drop the file in `workflow/scripts/`** — Snakemake adds the script dir to
315
+ `sys.path`, so `module: "my_seg"` just works. Simplest for a single file.
316
+ 2. **Install it** into the run env (`pip install -e .`, `pixi add --pypi …`),
317
+ then use its import name. Best for a real package with dependencies.
318
+ 3. **Set `PYTHONPATH`** to the file's directory before launching Snakemake.
319
+
320
+ ### Cluster checklist
321
+
322
+ - **Dependencies:** the env that runs the **segment** jobs must have everything
323
+ your function imports (`pip`/`pixi add` it). A missing import or any crash
324
+ shows up in `logs/segment/<index>.log`, not the (empty) SLURM log.
325
+ - **Offline GPU nodes:** the built-in `fetch_model` prefetch covers Cellpose
326
+ only. If your function downloads weights/data on first use, fetch them once on
327
+ the **login node** (network access) so they land in shared `$HOME`; otherwise
328
+ the segment jobs fail with `Network is unreachable`. See *Troubleshooting*.
329
+ - **Memory / walltime:** tune `segment:` in `profile/slurm/config.yaml` for your
330
+ model (`mem_mb`, `runtime`) just as for Cellpose.
331
+ - Everything else — tiling, halos, empty-tile skipping, the zarr-native merge,
332
+ resume, and per-tile logs — is identical to a Cellpose run.
333
+
334
+ For full control (your own tiling/merge loop instead of the bundled rules), call
335
+ the public API directly — see *How it works* below.
336
+
197
337
  ## pixi (instead of conda)
198
338
 
199
339
  Conda is **not** required — Snakemake runs in whatever environment launches it.
@@ -57,7 +57,20 @@ bioio = [
57
57
  # imaris reads .ims files natively (HDF5, no JVM) for OME-ZARR conversion.
58
58
  imaris = ["imaris-ims-file-reader"]
59
59
  # napari enables the interactive viewer plugin.
60
- napari = ["napari[all]"]
60
+ # - napari >= 0.5: NumPy 2.0 compatible (older 0.4.x uses np.array(copy=False),
61
+ # which raises under NumPy 2 -> ValidationError on Viewer()).
62
+ # - numpy < 2.5: napari/numba don't support numpy 2.5 yet.
63
+ # - ipykernel < 7: napari-console requires ipykernel < 7.
64
+ # - lxml-html-clean: napari's notebook_display imports lxml.html.clean, split
65
+ # into a separate package in lxml >= 5.2 (else ImportError on Viewer()).
66
+ # - glasbey: distinct high-contrast label LUTs for view_in_napari.
67
+ napari = [
68
+ "napari[all]>=0.5.5",
69
+ "numpy<2.5",
70
+ "ipykernel<7",
71
+ "lxml-html-clean",
72
+ "glasbey",
73
+ ]
61
74
  # workflow runs the Snakemake pipeline (per-tile SLURM jobs across GPUs).
62
75
  workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm"]
63
76
  dev = ["pytest", "pytest-cov", "scikit-image", "psutil", "tqdm"]
@@ -199,6 +199,7 @@ def view_in_napari(
199
199
  labels_component: str = "labels",
200
200
  image_name: str = "image",
201
201
  labels_name: str = "labels",
202
+ glasbey: bool = True,
202
203
  show: bool = True,
203
204
  **add_image_kwargs: Any,
204
205
  ):
@@ -223,6 +224,11 @@ def view_in_napari(
223
224
  matching ``tile_process``'s ``output_component``).
224
225
  image_name, labels_name : str, optional
225
226
  Layer names shown in napari.
227
+ glasbey : bool, optional
228
+ Colour the labels with a glasbey palette (many distinct, high-contrast
229
+ colours, tuned to read on the dark canvas) instead of napari's default.
230
+ Default ``True``. Needs the ``glasbey`` package (ships with
231
+ ``patchworks[napari]``).
226
232
  show : bool, optional
227
233
  Start the napari event loop (blocking). Set ``False`` in scripts/tests
228
234
  that manage the loop themselves.
@@ -250,9 +256,24 @@ def view_in_napari(
250
256
  **add_image_kwargs,
251
257
  )
252
258
 
259
+ label_kwargs: dict[str, Any] = {}
260
+ if glasbey:
261
+ try:
262
+ import glasbey as _glasbey
263
+
264
+ # bias lighter so colours read on napari's dark canvas
265
+ label_kwargs["colormap"] = _glasbey.create_palette(
266
+ 256, lightness_bounds=(40, 100)
267
+ )
268
+ except ImportError:
269
+ logger.warning(
270
+ "glasbey not installed; using napari's default label colours "
271
+ "(pip install glasbey, or it ships with patchworks[napari])."
272
+ )
273
+
253
274
  if labels is not None:
254
275
  lab = _resolve_labels(labels, labels_component)
255
- viewer.add_labels(lab, name=labels_name)
276
+ viewer.add_labels(lab, name=labels_name, **label_kwargs)
256
277
  elif _is_zarr(image):
257
278
  # No labels given → auto-overlay every label image stored inside the
258
279
  # OME-ZARR under labels/<name>/ (the default place tile_process writes
@@ -260,7 +281,9 @@ def view_in_napari(
260
281
  for name in _inner_label_names(image):
261
282
  levels = _multiscale_levels(f"{image}/labels/{name}", None)
262
283
  lab = [lvl.astype("int32") for lvl in levels]
263
- viewer.add_labels(lab if len(lab) > 1 else lab[0], name=name)
284
+ viewer.add_labels(
285
+ lab if len(lab) > 1 else lab[0], name=name, **label_kwargs
286
+ )
264
287
  logger.info("auto-loaded labels/%s from %s", name, image)
265
288
 
266
289
  if show:
@@ -12,7 +12,7 @@ work_dir: "/path/to/results" # everything is written under here
12
12
  # image.zarr/labels/<name>/ — the segmentation (multi-scale), in the image
13
13
 
14
14
  # ---- conversion -------------------------------------------------------------
15
- reuse_pyramid: true # for .ims: copy its own pyramid (fast); else rebuild
15
+ reuse_pyramid: false # for .ims: copy its own pyramid (fast); else rebuild
16
16
  convert_chunks: null # null → patchworks' bounded auto chunks; or [c,z,y,x]
17
17
  shard: false # true → pack chunks into shards (zarr v3; fewer files)
18
18
  # Re-running reuses an existing image.zarr automatically (skips conversion).
@@ -29,10 +29,10 @@ skip_empty: true # skip background tiles
29
29
  empty_threshold: null # null → Otsu; or a number
30
30
 
31
31
  # ---- segmentation -----------------------------------------------------------
32
- method: "cellpose" # "cellpose" (GPU) or "threshold" (simple, no GPU; testing)
33
- label_name: "cellpose"
32
+ method: "cellpose" # "cellpose" (GPU), "threshold" (no GPU; testing), "custom"
33
+ label_name: "cellpose_labels"
34
34
  cellpose:
35
- model: "cyto3"
35
+ model: "nuclei"
36
36
  diameter: 30
37
37
  do_3D: true
38
38
  gpu: true
@@ -40,6 +40,13 @@ cellpose:
40
40
  # flow_threshold: 0.4
41
41
  # cellprob_threshold: 0.0
42
42
 
43
+ # Your own per-tile function (method: "custom"). See the "custom function"
44
+ # section of docs/guide/snakemake.md.
45
+ # custom:
46
+ # module: "my_seg" # importable on the cluster (workflow/scripts/, PYTHONPATH, or pip-installed)
47
+ # function: "segment" # def segment(tile: np.ndarray) -> np.ndarray (int32 labels)
48
+ # kwargs: {} # optional extra keyword args
49
+
43
50
  # ---- pyramid for the labels -------------------------------------------------
44
51
  pyramid_levels: 5
45
52
  pyramid_downscale: 2
@@ -33,11 +33,14 @@ set-resources:
33
33
  mem_mb: 32000
34
34
  runtime: 120
35
35
  segment:
36
- # one GPU per tile — this is what spreads Cellpose across GPUs
36
+ # one GPU per tile — this is what spreads Cellpose across GPUs.
37
+ # Use the plugin's native `gres` resource → emits --gres=gpu:1, which is
38
+ # how scicore binds a CUDA device. (The `gpu:` resource emits --gpus and
39
+ # leaves the job without a device.)
37
40
  slurm_partition: "rtx4090"
38
- gpu: 1
41
+ gres: "gpu:1"
39
42
  qos: "rtx4090-6hours" # scicore: <partition>-<duration> QOS
40
- mem_mb: 32000 # plenty — a tile used ~1G; the failure was a Python error
43
+ mem_mb: 32000 # plenty — a tile used ~1G
41
44
  cpus_per_task: 4
42
45
  runtime: 360 # 6 hours — must match the QOS, NOT 120 (=2h → killed early)
43
46
  merge:
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import json
11
11
  import logging
12
12
  import sys
13
+ from functools import partial
13
14
  from pathlib import Path
14
15
 
15
16
  from patchworks import load_ome_zarr
@@ -124,8 +125,9 @@ def build_fn(cfg):
124
125
  Parameters
125
126
  ----------
126
127
  cfg : dict
127
- Snakemake config. ``method`` selects ``"cellpose"`` (default) or a
128
- simple ``"threshold"`` (handy for testing / no-GPU runs).
128
+ Snakemake config. ``method`` selects ``"cellpose"`` (default), a simple
129
+ ``"threshold"`` (testing / no-GPU), or ``"custom"`` to import your own
130
+ function (``cfg["custom"] = {module, function, kwargs}``).
129
131
 
130
132
  Returns
131
133
  -------
@@ -133,6 +135,21 @@ def build_fn(cfg):
133
135
  ``(ndarray) -> ndarray`` returning integer labels.
134
136
  """
135
137
  method = cfg.get("method", "cellpose")
138
+ if method == "custom":
139
+ # Import a user-provided function, e.g.
140
+ # custom: {module: my_seg, function: segment, kwargs: {...}}
141
+ # The module must be importable on the cluster (a file in
142
+ # workflow/scripts/, on PYTHONPATH, or an installed package).
143
+ import importlib
144
+
145
+ spec = cfg["custom"]
146
+ fn = getattr(
147
+ importlib.import_module(spec["module"]),
148
+ spec.get("function", "segment"),
149
+ )
150
+ kwargs = spec.get("kwargs") or {}
151
+ return partial(fn, **kwargs) if kwargs else fn
152
+
136
153
  if method == "threshold":
137
154
 
138
155
  def fn(tile):
File without changes
File without changes
File without changes
File without changes
File without changes