patchworks 3.1.0__tar.gz → 3.3.0__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 (169) hide show
  1. {patchworks-3.1.0 → patchworks-3.3.0}/.github/workflows/test.yml +2 -4
  2. {patchworks-3.1.0 → patchworks-3.3.0}/PKG-INFO +13 -2
  3. {patchworks-3.1.0 → patchworks-3.3.0}/README.md +8 -0
  4. patchworks-3.3.0/docs/api/plugins/careamics.md +11 -0
  5. patchworks-3.3.0/docs/api/plugins/plantseg.md +11 -0
  6. patchworks-3.3.0/docs/api/plugins/watershed.md +11 -0
  7. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/dog.md +18 -0
  8. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/cli.md +20 -1
  9. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/custom_segmentation.md +8 -0
  10. patchworks-3.3.0/docs/guide/membrane_cells.md +283 -0
  11. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/review.md +7 -3
  12. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/snakemake.md +67 -18
  13. {patchworks-3.1.0 → patchworks-3.3.0}/mkdocs.yml +4 -0
  14. {patchworks-3.1.0 → patchworks-3.3.0}/pyproject.toml +9 -1
  15. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_distributed.py +40 -2
  16. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_merge.py +253 -18
  17. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_occupancy.py +1 -1
  18. patchworks-3.3.0/src/patchworks/_relations.py +329 -0
  19. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_review.py +43 -7
  20. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_tables.py +187 -36
  21. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_volume_filter.py +44 -8
  22. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/cli.py +91 -1
  23. patchworks-3.3.0/src/patchworks/plugins/careamics.py +381 -0
  24. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/plugins/dog.py +39 -1
  25. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/plugins/napari.py +22 -0
  26. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/plugins/ome_zarr.py +176 -8
  27. patchworks-3.3.0/src/patchworks/plugins/plantseg.py +750 -0
  28. patchworks-3.3.0/src/patchworks/plugins/watershed.py +403 -0
  29. patchworks-3.3.0/tests/test_careamics.py +170 -0
  30. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_distributed.py +163 -0
  31. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_dog.py +17 -0
  32. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_napari.py +27 -0
  33. patchworks-3.3.0/tests/test_ngff_conformance.py +177 -0
  34. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_ome_zarr.py +4 -3
  35. patchworks-3.3.0/tests/test_plantseg.py +369 -0
  36. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_position.py +90 -7
  37. patchworks-3.3.0/tests/test_pw.py +662 -0
  38. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_relations.py +59 -1
  39. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_run_multi.py +775 -21
  40. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_volume_filter.py +35 -0
  41. patchworks-3.3.0/tests/test_watershed.py +124 -0
  42. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/config/config.yaml +28 -2
  43. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/config/config_cilia.yaml +5 -0
  44. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/config/config_cyto.yaml +12 -0
  45. patchworks-3.3.0/workflow/config/config_cyto_plantseg.yaml +51 -0
  46. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/config/multi.yaml +5 -2
  47. patchworks-3.3.0/workflow/config/multi_plantseg.yaml +33 -0
  48. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/pixi.toml +91 -8
  49. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/rules/common.smk +3 -1
  50. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/_pw.py +385 -9
  51. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/export_iso.py +35 -3
  52. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/merge.py +87 -28
  53. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/prepare_tiles.py +17 -2
  54. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/relate.py +124 -65
  55. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/run_multi.py +547 -62
  56. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/segment_tile.py +17 -2
  57. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/view.py +13 -1
  58. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/viewer/pixi.toml +12 -4
  59. patchworks-3.1.0/src/patchworks/_relations.py +0 -194
  60. patchworks-3.1.0/tests/test_pw.py +0 -278
  61. {patchworks-3.1.0 → patchworks-3.3.0}/.github/min-versions.txt +0 -0
  62. {patchworks-3.1.0 → patchworks-3.3.0}/.github/workflows/docs.yml +0 -0
  63. {patchworks-3.1.0 → patchworks-3.3.0}/.github/workflows/lint.yml +0 -0
  64. {patchworks-3.1.0 → patchworks-3.3.0}/.github/workflows/release.yml +0 -0
  65. {patchworks-3.1.0 → patchworks-3.3.0}/.gitignore +0 -0
  66. {patchworks-3.1.0 → patchworks-3.3.0}/.markdownlint-cli2.yaml +0 -0
  67. {patchworks-3.1.0 → patchworks-3.3.0}/.pre-commit-config.yaml +0 -0
  68. {patchworks-3.1.0 → patchworks-3.3.0}/LICENSE +0 -0
  69. {patchworks-3.1.0 → patchworks-3.3.0}/benchmarks/bench.py +0 -0
  70. {patchworks-3.1.0 → patchworks-3.3.0}/benchmarks/compare.py +0 -0
  71. {patchworks-3.1.0 → patchworks-3.3.0}/cliff.toml +0 -0
  72. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/chunks.md +0 -0
  73. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/cluster.md +0 -0
  74. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/io.md +0 -0
  75. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/merge_tile_labels.md +0 -0
  76. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/plugins/cellpose.md +0 -0
  77. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/plugins/dog.md +0 -0
  78. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/plugins/napari.md +0 -0
  79. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/plugins/ome_zarr.md +0 -0
  80. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/postprocess.md +0 -0
  81. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/provenance.md +0 -0
  82. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/relabel.md +0 -0
  83. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/review.md +0 -0
  84. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/seams.md +0 -0
  85. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/tile_process.md +0 -0
  86. {patchworks-3.1.0 → patchworks-3.3.0}/docs/api/volume_filter.md +0 -0
  87. {patchworks-3.1.0 → patchworks-3.3.0}/docs/assets/logo.png +0 -0
  88. {patchworks-3.1.0 → patchworks-3.3.0}/docs/assets/review_panel.png +0 -0
  89. {patchworks-3.1.0 → patchworks-3.3.0}/docs/assets/review_position.png +0 -0
  90. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/cellpose_2d.md +0 -0
  91. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/cellpose_2d.py +0 -0
  92. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/cellpose_3d.md +0 -0
  93. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/cellpose_3d.py +0 -0
  94. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/custom.md +0 -0
  95. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/custom_method.py +0 -0
  96. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/dog.py +0 -0
  97. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/standalone_merge.md +0 -0
  98. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/stardist.md +0 -0
  99. {patchworks-3.1.0 → patchworks-3.3.0}/docs/examples/stardist_2d.py +0 -0
  100. {patchworks-3.1.0 → patchworks-3.3.0}/docs/getting_started.md +0 -0
  101. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/gpu_distributed.md +0 -0
  102. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/label_relations.md +0 -0
  103. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/launcher.md +0 -0
  104. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/measurements.md +0 -0
  105. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/merging.md +0 -0
  106. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/ome_zarr_napari.md +0 -0
  107. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/performance.md +0 -0
  108. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/pitfalls.md +0 -0
  109. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/skip_empty.md +0 -0
  110. {patchworks-3.1.0 → patchworks-3.3.0}/docs/guide/tiling.md +0 -0
  111. {patchworks-3.1.0 → patchworks-3.3.0}/docs/index.md +0 -0
  112. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/__init__.py +0 -0
  113. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_autotune.py +0 -0
  114. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_chunks.py +0 -0
  115. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_cluster.py +0 -0
  116. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_core.py +0 -0
  117. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_gpu.py +0 -0
  118. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_io.py +0 -0
  119. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_notify.py +0 -0
  120. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_postprocess.py +0 -0
  121. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_progress.py +0 -0
  122. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_provenance.py +0 -0
  123. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_relabel.py +0 -0
  124. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/_seams.py +0 -0
  125. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/plugins/__init__.py +0 -0
  126. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/plugins/cellpose.py +0 -0
  127. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/plugins/review.py +0 -0
  128. {patchworks-3.1.0 → patchworks-3.3.0}/src/patchworks/py.typed +0 -0
  129. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/0.4/image.schema +0 -0
  130. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/0.4/label.schema +0 -0
  131. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/0.4/ome.schema +0 -0
  132. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/_version.schema +0 -0
  133. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/image.schema +0 -0
  134. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/label.schema +0 -0
  135. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/ome.schema +0 -0
  136. {patchworks-3.1.0 → patchworks-3.3.0}/tests/ngff_schemas/README.md +0 -0
  137. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_allocation.py +0 -0
  138. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_autotune.py +0 -0
  139. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_cellpose.py +0 -0
  140. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_cli.py +0 -0
  141. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_core.py +0 -0
  142. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_gpu.py +0 -0
  143. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_launcher.py +0 -0
  144. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_notify.py +0 -0
  145. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_occupancy.py +0 -0
  146. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_postprocess.py +0 -0
  147. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_progress.py +0 -0
  148. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_remote.py +0 -0
  149. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_review.py +0 -0
  150. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_review_napari.py +0 -0
  151. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_seams.py +0 -0
  152. {patchworks-3.1.0 → patchworks-3.3.0}/tests/test_tables.py +0 -0
  153. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/README.md +0 -0
  154. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/Snakefile +0 -0
  155. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/config/common.yaml +0 -0
  156. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/config/config_nuclei.yaml +0 -0
  157. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/launcher/README.md +0 -0
  158. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/launcher/app.py +0 -0
  159. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/launcher/clusters.yaml +0 -0
  160. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/launcher/launcher_core.py +0 -0
  161. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/launcher/requirements.txt +0 -0
  162. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/profile/slurm/config.yaml +0 -0
  163. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/rules/convert.smk +0 -0
  164. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/rules/merge.smk +0 -0
  165. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/rules/segment.smk +0 -0
  166. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/build_occupancy.py +0 -0
  167. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/convert.py +0 -0
  168. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/fetch_model.py +0 -0
  169. {patchworks-3.1.0 → patchworks-3.3.0}/workflow/scripts/reshard_store.py +0 -0
@@ -32,11 +32,9 @@ jobs:
32
32
  run: pytest -q --doctest-modules src
33
33
 
34
34
  other-platforms:
35
- # Not yet verified on macOS or Windows (no CI ran there before), so this
36
- # job reports without failing the build; promote it into the matrix
37
- # above once it is green.
35
+ # Green on macOS and Windows since 2026-10-07: a failure there now fails
36
+ # the build like any other.
38
37
  runs-on: ${{ matrix.os }}
39
- continue-on-error: true
40
38
  strategy:
41
39
  fail-fast: false
42
40
  matrix:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchworks
3
- Version: 3.1.0
3
+ Version: 3.3.0
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
@@ -52,6 +52,8 @@ Requires-Dist: bioio-lif; extra == 'bioio'
52
52
  Requires-Dist: bioio-nd2; extra == 'bioio'
53
53
  Requires-Dist: bioio-ome-tiff; extra == 'bioio'
54
54
  Requires-Dist: bioio-tifffile; extra == 'bioio'
55
+ Provides-Extra: careamics
56
+ Requires-Dist: careamics>=0.3; extra == 'careamics'
55
57
  Provides-Extra: cellpose
56
58
  Requires-Dist: cellpose>=3.0; extra == 'cellpose'
57
59
  Provides-Extra: cellpose3
@@ -99,7 +101,8 @@ Requires-Dist: pandas>=2.0; extra == 'review'
99
101
  Provides-Extra: workflow
100
102
  Requires-Dist: openpyxl; extra == 'workflow'
101
103
  Requires-Dist: pandas>=2.0; extra == 'workflow'
102
- Requires-Dist: snakemake-executor-plugin-slurm; extra == 'workflow'
104
+ Requires-Dist: scikit-image; extra == 'workflow'
105
+ Requires-Dist: snakemake-executor-plugin-slurm>=2.2; extra == 'workflow'
103
106
  Requires-Dist: snakemake>=8; extra == 'workflow'
104
107
  Description-Content-Type: text/markdown
105
108
 
@@ -154,6 +157,7 @@ pip install "patchworks[cellpose]" # Cellpose plugin (>=3.0, v3 or v4)
154
157
  pip install "patchworks[cellpose3]" # Cellpose plugin, pinned to v3.x
155
158
  pip install "patchworks[cellpose4]" # Cellpose plugin, pinned to v4+
156
159
  pip install "patchworks[dog]" # deconvolution + DoG plugin (pycudadecon)
160
+ pip install "patchworks[careamics]" # Noise2Void denoising before segmenting
157
161
  pip install "patchworks[bioio]" # convert any image format to OME-ZARR
158
162
  pip install "patchworks[imaris]" # convert Imaris .ims files to OME-ZARR
159
163
  pip install "patchworks[napari]" # interactive napari viewer plugin
@@ -457,6 +461,13 @@ Optional:
457
461
  - `cellpose` — Cellpose plugin, v3 or v4 (`patchworks[cellpose]`);
458
462
  pin with `[cellpose3]` or `[cellpose4]`
459
463
  - `pycudadecon` — deconvolution step of the `dog` plugin (`patchworks[dog]`)
464
+ - `careamics` — denoise tiles before segmenting them (Noise2Void),
465
+ `patchworks[careamics]`
466
+ - `plant-seg` — PlantSeg plugin (boundary U-Net + GASP/multicut), from
467
+ conda-forge only (`conda install -c conda-forge plant-seg`, or the
468
+ workflow's `pixi install -e plantseg`). The nuclei-seeded watershed plugin
469
+ needs only scikit-image. See the
470
+ [membrane cells guide](https://imcf.one/patchworks/guide/membrane_cells/).
460
471
  - `bioio` + readers — convert CZI/LIF/ND2/OME-TIFF/… to OME-ZARR
461
472
  (`patchworks[bioio]`)
462
473
  - `imaris-ims-file-reader` — convert Imaris `.ims` (`patchworks[imaris]`)
@@ -49,6 +49,7 @@ pip install "patchworks[cellpose]" # Cellpose plugin (>=3.0, v3 or v4)
49
49
  pip install "patchworks[cellpose3]" # Cellpose plugin, pinned to v3.x
50
50
  pip install "patchworks[cellpose4]" # Cellpose plugin, pinned to v4+
51
51
  pip install "patchworks[dog]" # deconvolution + DoG plugin (pycudadecon)
52
+ pip install "patchworks[careamics]" # Noise2Void denoising before segmenting
52
53
  pip install "patchworks[bioio]" # convert any image format to OME-ZARR
53
54
  pip install "patchworks[imaris]" # convert Imaris .ims files to OME-ZARR
54
55
  pip install "patchworks[napari]" # interactive napari viewer plugin
@@ -352,6 +353,13 @@ Optional:
352
353
  - `cellpose` — Cellpose plugin, v3 or v4 (`patchworks[cellpose]`);
353
354
  pin with `[cellpose3]` or `[cellpose4]`
354
355
  - `pycudadecon` — deconvolution step of the `dog` plugin (`patchworks[dog]`)
356
+ - `careamics` — denoise tiles before segmenting them (Noise2Void),
357
+ `patchworks[careamics]`
358
+ - `plant-seg` — PlantSeg plugin (boundary U-Net + GASP/multicut), from
359
+ conda-forge only (`conda install -c conda-forge plant-seg`, or the
360
+ workflow's `pixi install -e plantseg`). The nuclei-seeded watershed plugin
361
+ needs only scikit-image. See the
362
+ [membrane cells guide](https://imcf.one/patchworks/guide/membrane_cells/).
355
363
  - `bioio` + readers — convert CZI/LIF/ND2/OME-TIFF/… to OME-ZARR
356
364
  (`patchworks[bioio]`)
357
365
  - `imaris-ims-file-reader` — convert Imaris `.ims` (`patchworks[imaris]`)
@@ -0,0 +1,11 @@
1
+ # CAREamics denoising plugin
2
+
3
+ See [Denoising first](../../guide/membrane_cells.md#denoising-first-careamics).
4
+
5
+ ::: patchworks.plugins.careamics.denoise_fn
6
+
7
+ ::: patchworks.plugins.careamics.denoise
8
+
9
+ ::: patchworks.plugins.careamics.train_n2v
10
+
11
+ ::: patchworks.plugins.careamics.training_crops
@@ -0,0 +1,11 @@
1
+ # PlantSeg plugin
2
+
3
+ See [Cells from a membrane stain](../../guide/membrane_cells.md#plantseg).
4
+
5
+ ::: patchworks.plugins.plantseg.plantseg_fn
6
+
7
+ ::: patchworks.plugins.plantseg.fetch_model
8
+
9
+ ::: patchworks.plugins.plantseg.available_models
10
+
11
+ ::: patchworks.plugins.plantseg.rescale_factors
@@ -0,0 +1,11 @@
1
+ # Nuclei-seeded watershed plugin
2
+
3
+ See [Cells from a membrane stain](../../guide/membrane_cells.md).
4
+
5
+ ::: patchworks.plugins.watershed.watershed_fn
6
+
7
+ ::: patchworks.plugins.watershed.nuclei_seeds
8
+
9
+ ::: patchworks.plugins.watershed.foreground_mask
10
+
11
+ ::: patchworks.plugins.watershed.seeded_watershed
@@ -54,6 +54,24 @@ directly to the DoG image — start near the DoG's typical peak value on a
54
54
  known-positive region and adjust from there; there's no auto (Otsu-style)
55
55
  option, since the DoG image isn't bimodal the way a raw intensity image is.
56
56
 
57
+ ## Thin, oblique objects: `connectivity`
58
+
59
+ The thresholded voxels are joined into objects across shared **faces** by
60
+ default. A cilium lying obliquely is a staircase of voxels touching only
61
+ along edges or at corners, and comes out as a row of fragments.
62
+ `connectivity=2` also joins voxels sharing an edge, `connectivity=3` (3-D)
63
+ also a corner:
64
+
65
+ ```python
66
+ fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02, connectivity=3)
67
+ ```
68
+
69
+ The merge has to join tiles the same way, or objects crossing a tile
70
+ boundary diagonally are split there: the workflow does this by itself; with
71
+ the API, pass the same value to `merge_tile_labels(..., connectivity=3)`.
72
+ The merged result is then exactly that of labelling the whole image at once,
73
+ whatever the tile size.
74
+
57
75
  ## GPU
58
76
 
59
77
  ```python
@@ -25,8 +25,18 @@ patchworks tables scan.zarr --relate nuclei:cells
25
25
 
26
26
  # Look at the likely mistakes one by one and fix them (napari)
27
27
  patchworks review scan.zarr --expect cells:nuclei=1
28
+
29
+ # Bring a store written by an older patchworks up to the OME-Zarr spec
30
+ patchworks fix-metadata scan.zarr
28
31
  ```
29
32
 
33
+ `patchworks fix-metadata` changes metadata only, in place: it gives every
34
+ OME-Zarr 0.5 array its `dimension_names` (required by 0.5; versions before
35
+ this one wrote none, which strict readers such as `ome-zarr-models` refuse),
36
+ and drops surplus coarse levels from a label image with more pyramid levels
37
+ than its image (the spec requires the same number). Running it twice
38
+ changes nothing the second time.
39
+
30
40
  `patchworks review` without a window: `--summary` (counts and error
31
41
  estimate), `--export DIR --format csv|xlsx|parquet` (corrected tables),
32
42
  `--workbooks DIR` (relation workbooks), `--write-labels NAME` (a label image
@@ -46,7 +56,16 @@ gives a child touching no parent the nearest one. See
46
56
  | `custom` | any importable function | `--fn module:function`, `--fn-kwargs '{"k": 1}'` |
47
57
 
48
58
  Cellpose's anisotropy and the DoG plugin's voxel size are read from the
49
- store's own calibration, at the level being segmented.
59
+ store's own calibration, at the level being segmented -- as is `voxel_size`
60
+ for a custom function that takes one, such as the PlantSeg and watershed
61
+ plugins: `--method custom --fn patchworks.plugins.plantseg:segment
62
+ --fn-kwargs '{"segmentation": "gasp"}' --stitch iou` (`iou`: these fill
63
+ space, so neighbouring cells touch at every seam).
64
+
65
+ `--denoise MODEL` denoises every tile with a CAREamics model before any
66
+ method segments it; `patchworks denoise-train STORE --channel 0 --out
67
+ n2v.ckpt` trains one (Noise2Void, no ground truth). See
68
+ [Cells from a membrane stain](membrane_cells.md).
50
69
 
51
70
  Tiling and stitching take the same options as
52
71
  [`tile_process`](../api/tile_process.md): `--tile-shape` (`z,y,x`, `auto` or
@@ -29,6 +29,14 @@ That is the whole interface. Anything that turns an image tile into a label
29
29
  image works: classic image processing, StarDist, a trained model, an external
30
30
  binary you shell out to, …
31
31
 
32
+ Ready-made ones ship with patchworks: `patchworks.plugins.dog` (spots,
33
+ cilia), and for cells from a membrane stain `patchworks.plugins.watershed`
34
+ (nuclei-seeded watershed) and `patchworks.plugins.plantseg` (PlantSeg) --
35
+ see [Cells from a membrane stain](membrane_cells.md). With
36
+ `nuclei_channel` set, the tile is `(2, z, y, x)`: `[channel,
37
+ nuclei_channel]` on the first axis. Any method can also run on
38
+ [denoised tiles](membrane_cells.md#denoising-first-careamics).
39
+
32
40
  ## Minimal example (no GPU, no deps beyond scikit-image)
33
41
 
34
42
  ```python
@@ -0,0 +1,283 @@
1
+ # Cells from a membrane stain
2
+
3
+ Epithelia, organoids and tissues are often imaged with a **membrane** (or
4
+ cortex) marker plus a **nuclear** dye. Cellpose struggles there in a
5
+ recognisable way: Cellpose 4 (`cpsam`) reads every channel it is given
6
+ without a "cytoplasm" or "nucleus" role, so with a bright nuclear channel
7
+ next to a faint membrane it segments the **nuclei**; and on the membrane
8
+ alone, a wall that is faint in places merges two cells. Patchworks ships
9
+ three plugins for this case, plus an optional denoising step in front of any
10
+ method.
11
+
12
+ | | Needs | Cells come from | Best when |
13
+ | --- | --- | --- | --- |
14
+ | [Nuclei-seeded watershed](#nuclei-seeded-watershed) | scikit-image | the membrane, flooded from the nuclei | every cell has one nucleus; start here |
15
+ | [PlantSeg](#plantseg) | `plant-seg` (conda-forge) | a boundary U-Net, then GASP / multicut | the membrane is uneven or noisy |
16
+ | [PlantSeg + nuclei](#plantseg) | same | the U-Net's boundaries and the nuclei | both of the above |
17
+ | Cellpose, membrane only | cellpose | Cellpose | `nuclei_channel: null`, `do_3D: true` |
18
+ | [Denoising first](#denoising-first-careamics) | `careamics` | any of these, on denoised tiles | noisy acquisitions |
19
+
20
+ All of them are ordinary [custom functions](custom_segmentation.md): one
21
+ config block, the same tiling, merge, tables and review as any other run.
22
+
23
+ ## Nuclei-seeded watershed
24
+
25
+ The nuclei are the easy part of the image: bright, compact, separated. They
26
+ say how many cells there are and where; flooding the membrane image from
27
+ them grows each nucleus out to its cell's walls. A cell can then not be
28
+ split (one seed each) or merged with its neighbour (two seeds never join),
29
+ even across a gap in the wall.
30
+
31
+ ```yaml
32
+ channel: 0 # membrane
33
+ nuclei_channel: 1 # stacked onto each tile as [membrane, nuclei]
34
+ method: "custom"
35
+ label_name: "cyto_labels"
36
+ stitch: "iou" # required: see below
37
+ custom:
38
+ module: "patchworks.plugins.watershed"
39
+ kwargs:
40
+ nuclei_min_size: 200 # voxels: specks smaller than a nucleus
41
+ foreground: "otsu" # stop at the tissue edge
42
+ # max_radius_um: 15 # or: no further than this from the nucleus
43
+ # nuclei_threshold: 800 # intensity, if Otsu misses dim nuclei
44
+ ```
45
+
46
+ `stitch: "iou"` is required with this plugin and with PlantSeg, and
47
+ `prepare` refuses the config without it. They give every voxel to some
48
+ cell, so neighbouring cells touch at every tile seam, and the default
49
+ `"touch"` stitching would join each such pair: on a test grid of 12 cells
50
+ cut by six tiles, 5 came out. `"iou"` joins two pieces only where both tiles
51
+ agree on their overlap, and gives the 12. (From the command line, pass
52
+ `--stitch iou` yourself: nothing checks it there.)
53
+
54
+ Two nuclei touching each other give one seed, so one cell for two: raise
55
+ `nuclei_threshold` if that happens -- or grow the cells from nuclei you have
56
+ already segmented, with `seed_labels` instead of `nuclei_channel` (see
57
+ [the example below](#how-seed_labels-works)). Each tile's halo (`overlap`) must hold a
58
+ whole cell, as for any method.
59
+
60
+ ## PlantSeg
61
+
62
+ [PlantSeg](https://github.com/kreshuklab/plant-seg) predicts cell
63
+ boundaries with a 3-D U-Net trained on membrane stains, then partitions the
64
+ boundary map into cells (supervoxels by a distance-transform watershed,
65
+ merged by GASP, mutex watershed or multicut). A wall the U-Net sees at all
66
+ gets closed by the partitioning, where Cellpose would merge across it.
67
+
68
+ PlantSeg is on conda-forge only, so it has its own pixi environment:
69
+
70
+ ```bash
71
+ export CONDA_OVERRIDE_CUDA=12.0 # login node without a GPU: see below
72
+ pixi install -e plantseg
73
+ pixi run -e plantseg plantseg-fetch generic_confocal_3D_unet # once, with internet
74
+ pixi run -e plantseg multi-slurm
75
+ ```
76
+
77
+ ```yaml
78
+ method: "custom"
79
+ stitch: "iou" # required, as for the watershed
80
+ custom:
81
+ module: "patchworks.plugins.plantseg"
82
+ kwargs:
83
+ model: "generic_confocal_3D_unet" # or generic_light_sheet_3D_unet, ...
84
+ segmentation: "gasp" # gasp | mutex_ws | multicut | dt_watershed
85
+ beta: 0.6 # lower merges more, higher splits more
86
+ foreground: "otsu" # a boundary U-Net puts cells everywhere
87
+ ```
88
+
89
+ With `nuclei_channel` set, two more `segmentation` modes use the nuclei:
90
+
91
+ - `"nuclei_watershed"` -- the U-Net's boundary map flooded from the nuclei:
92
+ the watershed above, on a much cleaner boundary image.
93
+ - `"lifted_multicut"` -- PlantSeg's lifted multicut: supervoxels in one
94
+ nucleus pulled together, in different nuclei pushed apart.
95
+
96
+ Each tile is resampled to the voxel size the model was trained at (from the
97
+ image's own calibration, `rescale: true`), which matters more than any other
98
+ setting for a pretrained U-Net; the prediction is resampled back.
99
+
100
+ ## Example: Cellpose nuclei + PlantSeg cells in one run
101
+
102
+ The workflow ships this pairing ready to edit: Cellpose segments the nuclei,
103
+ then PlantSeg grows the cells from **exactly those nuclei** -- one cell per
104
+ nucleus Cellpose found -- and the two are related. Three files, next to
105
+ `config/multi.yaml`:
106
+
107
+ ```yaml
108
+ # config/multi_plantseg.yaml
109
+ common: config/common.yaml # input, work_dir, tile_shape, level: shared
110
+
111
+ segmentations:
112
+ - config/config_nuclei.yaml # Cellpose "nuclei" model on channel 1
113
+ - config/config_cyto_plantseg.yaml # PlantSeg on channel 0, seeded by nuclei_labels
114
+
115
+ relations:
116
+ - a: nuclei_labels
117
+ b: cyto_labels
118
+ output: nuclei_to_cyto.xlsx
119
+
120
+ review: # a sanity check: one nucleus per cell
121
+ expect:
122
+ cyto_labels:
123
+ nuclei_labels: 1
124
+ ```
125
+
126
+ ```yaml
127
+ # config/config_nuclei.yaml (unchanged)
128
+ channel: 1
129
+ overlap: [4, 30, 30]
130
+ method: "cellpose"
131
+ label_name: "nuclei_labels"
132
+ cellpose:
133
+ model: "nuclei"
134
+ diameter: 15
135
+ do_3D: true
136
+ gpu: true
137
+ ```
138
+
139
+ ```yaml
140
+ # config/config_cyto_plantseg.yaml
141
+ channel: 0 # membrane
142
+ seed_labels: "nuclei_labels" # each tile becomes [membrane, nuclei labels]
143
+ overlap: [4, 40, 40] # the halo must hold a whole cell
144
+ stitch: "iou" # cells touch at every seam: join on agreement only
145
+ method: "custom"
146
+ label_name: "cyto_labels"
147
+ custom:
148
+ module: "patchworks.plugins.plantseg"
149
+ function: "segment"
150
+ kwargs:
151
+ model: "generic_confocal_3D_unet" # generic_light_sheet_3D_unet for light-sheet
152
+ segmentation: "nuclei_watershed" # U-Net boundaries flooded from the nuclei
153
+ foreground: "otsu" # keep the tissue only
154
+ # max_radius_um: 15
155
+ ```
156
+
157
+ Set `input` and `work_dir` in `config/common.yaml`, then, from `workflow/`:
158
+
159
+ ```bash
160
+ pixi install -e plantseg
161
+ pixi run -e plantseg plantseg-fetch generic_confocal_3D_unet # once, with internet
162
+ pixi run -e plantseg multi-plantseg-dry # check the plan
163
+ pixi run -e plantseg multi-plantseg-slurm # submit
164
+ ```
165
+
166
+ Your own multi config with a PlantSeg segmentation in it runs the same
167
+ way, from the same environment:
168
+
169
+ ```bash
170
+ pixi run -e plantseg multi-slurm --config /path/to/my_multi.yaml
171
+ ```
172
+
173
+ !!! note "`Virtual package '__cuda' does not match` on a login node"
174
+
175
+ The `plantseg` environment holds a CUDA build of PyTorch, and pixi
176
+ refuses to install or run an environment needing CUDA on a machine with
177
+ no GPU driver -- a login node. Only the GPU jobs need CUDA, and they
178
+ start the environment's Python directly, without pixi. Tell pixi on the
179
+ login node that a driver is there:
180
+
181
+ ```bash
182
+ export CONDA_OVERRIDE_CUDA=12.0 # any 12.x; put it in ~/.bashrc
183
+ ```
184
+
185
+ It must be set for every `pixi run -e plantseg ...` there, not only for
186
+ the install. Check the GPU nodes' driver supports CUDA 12 (`nvidia-smi`
187
+ in a GPU job prints "CUDA Version: 12.x" or later).
188
+
189
+ The `-e plantseg` matters: every job runs in the environment the command
190
+ was started from, and the default one has no PlantSeg. `run_multi` checks
191
+ this before converting anything and says which environment to use (as it
192
+ does for `denoise:`, which needs `-e careamics`).
193
+
194
+ The `plantseg` environment is the default one plus PlantSeg and cupy, so
195
+ the Cellpose run comes from it too, as does a cilia config with the DoG
196
+ plugin's `use_gpu: true` (cupy) and deconvolution. Afterwards, `nuclei_to_cyto.xlsx` gives
197
+ each nucleus its cell, and `pixi run -e viewer review <work_dir>/image.zarr`
198
+ lists any cell not holding exactly one nucleus.
199
+
200
+ ### How `seed_labels` works
201
+
202
+ - **Order.** `run_multi` starts the cells' config only once the config
203
+ producing `nuclei_labels` has finished; everything else listed (cilia,
204
+ say) runs alongside. If the nuclei fail, the cells are skipped, not run
205
+ without seeds. Two configs seeding each other are refused up front. A
206
+ dry run (`-n`) waits for nothing.
207
+ - **Tiles.** The nuclei label image is stacked onto the membrane as each
208
+ tile's second channel, halo included, so neighbouring tiles see the same
209
+ nuclei and a cell crossing a seam is grown from the same nucleus on both
210
+ sides. Both runs must use the same `level`, which `run_multi` already
211
+ enforces; the plugin is told `seeds: "labels"` automatically.
212
+ - **Seeds as given.** Two touching nuclei that Cellpose split stay two
213
+ cells; a dim nucleus Cellpose found still gets its cell. Cellpose's
214
+ mistakes carry over the same way: a nucleus split in two makes two
215
+ cells. Correcting the nuclei first (`patchworks review`, then
216
+ `--write-labels nuclei_labels` and `seed_labels: nuclei_labels_reviewed`)
217
+ gives the cells the corrected nuclei.
218
+ - **On its own**, outside `run_multi`, the cells' config needs
219
+ `labels/nuclei_labels` already in `image.zarr`: `prepare` checks, and
220
+ stops with a message rather than segmenting without seeds.
221
+ - **Re-segmenting the nuclei** does not re-run the cells by itself: delete
222
+ `<work_dir>/cyto_labels` and `image.zarr/labels/cyto_labels` to grow them
223
+ again from the new nuclei.
224
+
225
+ `nuclei_channel: 1` instead of `seed_labels` makes the plugin find the
226
+ nuclei itself, in the nuclear stain (Otsu per tile, `nuclei_*` options),
227
+ independently of Cellpose -- both segmentations then run at the same time.
228
+
229
+ Without PlantSeg, the same works with the plain
230
+ [nuclei-seeded watershed](#nuclei-seeded-watershed): set `module:
231
+ "patchworks.plugins.watershed"` with only the `foreground` and
232
+ `max_radius_um` keys, and use the default environment (`pixi run
233
+ multi-slurm` with this pair listed in `config/multi.yaml`).
234
+
235
+ ## Cellpose: membrane only, in 3-D
236
+
237
+ If you stay with Cellpose on such images:
238
+
239
+ - Give it **only the membrane** (`nuclei_channel: null`): `cpsam` then has
240
+ nothing brighter to lock onto.
241
+ - `do_3D: true` combines the three orthogonal views, so a wall faint in one
242
+ plane is found in the others. `stitch_threshold` instead joins
243
+ independent 2-D masks across z and fixes nothing in the masks
244
+ themselves. A GPU (`gpu: true`) only changes the speed.
245
+
246
+ ## Denoising first (CAREamics)
247
+
248
+ Noise breaks segmentations: a wall lost in the noise merges two cells.
249
+ [Noise2Void](https://careamics.github.io) learns to remove the noise from
250
+ the image itself (no clean ground truth, no annotation), so a model trained
251
+ once on a few crops of the store denoises every tile before any method sees
252
+ it.
253
+
254
+ ```bash
255
+ pixi install -e careamics
256
+ # on a GPU node: the brightest crops of the channel, ~30 epochs
257
+ pixi run -e careamics denoise-train <work_dir>/image.zarr --channel 0 --out n2v_membrane.ckpt
258
+ pixi run -e careamics denoise-train <work_dir>/image.zarr --channel 1 --out n2v_nuclei.ckpt
259
+ ```
260
+
261
+ then, in the segmentation config, with any `method`:
262
+
263
+ ```yaml
264
+ denoise:
265
+ model: "/path/to/n2v_membrane.ckpt" # .ckpt, or a BioImage.IO .zip
266
+ nuclei_model: "/path/to/n2v_nuclei.ckpt" # optional, for nuclei_channel
267
+ # tile_size: [16, 256, 256] # CAREamics' tiling inside a tile (VRAM)
268
+ ```
269
+
270
+ and run from the `careamics` environment (`plantseg-careamics` for both).
271
+ The model paths are checked during `prepare`; the denoised image is not
272
+ stored, only used for segmenting. From the command line, `patchworks segment
273
+ ... --denoise n2v_membrane.ckpt` does the same.
274
+
275
+ Look at a denoised tile before a full run:
276
+
277
+ ```python
278
+ from patchworks import load_ome_zarr
279
+ from patchworks.plugins.careamics import denoise
280
+
281
+ tile = load_ome_zarr("image.zarr", channel=0)[20:44, 1000:1512, 1000:1512]
282
+ clean = denoise(tile.compute(), model="n2v_membrane.ckpt")
283
+ ```
@@ -223,9 +223,13 @@ image.zarr/labels/cilia_labels/
223
223
  So it travels with the labels, including in a zip bundle, and it is
224
224
  replaced whenever the labels are. A table computed from older labels is
225
225
  recognised and ignored, never shown against the wrong segmentation.
226
- Measuring costs one read of the labels, after the merge. Add intensity
227
- columns with `table_channels: [0, 2]`, or turn tables off with
228
- `object_table: false` (see the [workflow config](snakemake.md)).
226
+ The table costs no extra read of the labels: each segment job measures
227
+ its tiles' objects as it writes them, and the merge adds those sums up per
228
+ merged object (an object cut by tile boundaries gets exactly the values it
229
+ would have measured whole). Intensity columns (`table_channels: [0, 2]`)
230
+ need the image, so with them the merged labels are measured once instead.
231
+ Turn tables off with `object_table: false` (see the
232
+ [workflow config](snakemake.md)).
229
233
 
230
234
  For a store that has none, for example a run made before tables existed,
231
235
  or labels from elsewhere:
@@ -363,8 +363,12 @@ shard_labels: false # true → also reshard label level 0 after the
363
363
  one blob). Set either, both, or neither (`null`, the default, disables
364
364
  each). Both run once on the **fully merged** image — not per tile, where
365
365
  an object crossing a tile boundary would look smaller or larger than it
366
- really is. Runs after `merge` and before the pyramid is built, so every
367
- pyramid level reflects the filtered result, and needs `image.zarr` to
366
+ really is. The sizes come from what the segment jobs measured per tile,
367
+ added up per merged object, and the filter is applied in the merge's
368
+ own relabelling pass, so it costs no pass of its own over the image
369
+ (runs whose tiles were segmented by an older version filter the merged
370
+ labels in a separate pass instead). Every pyramid level reflects the
371
+ filtered result. It needs `image.zarr` to
368
372
  carry a pixel size (the same calibration deconvolution's voxel sizes and
369
373
  Cellpose's `anisotropy` are derived from — see the tip below); an
370
374
  uncalibrated store raises rather than silently skipping the filter. See
@@ -650,6 +654,14 @@ running the workflow **twice with two configs against the same `work_dir`**
650
654
  never collides: each run gets its own private subdirectory, and both reuse
651
655
  the *same* already-converted `image.zarr` (conversion never re-runs).
652
656
 
657
+ `labels.done` is what makes Snakemake call a segmentation finished, while
658
+ the labels themselves are in `image.zarr/labels/<label_name>`. Deleting only
659
+ the latter leaves the run "done" with nothing re-making it: `run_multi`
660
+ refuses that up front and names the `work_dir/<label_name>` folder to remove
661
+ for a fresh segmentation. It also refuses a relation naming a label image
662
+ that no listed config makes and the store does not hold, and skips (with a
663
+ message) a relation whose labels are still missing once everything has run.
664
+
653
665
  Most of what those configs contain is identical — the input, the `work_dir`,
654
666
  the tiling, everything `convert` reads. Put it in **one** shared file and let
655
667
  each config carry only what actually differs. Snakemake merges several
@@ -696,6 +708,14 @@ cellpose:
696
708
  usually improves cytoplasm segmentation. Both indices are 0-based, like
697
709
  `channel`.
698
710
 
711
+ !!! tip "Cellpose 4 and a bright nuclear channel"
712
+
713
+ `cpsam` gives the two channels no roles, so next to a faint membrane it
714
+ may segment the nuclei instead of the cells. Give it the membrane alone
715
+ (`nuclei_channel: null`), or use the nuclei the other way round: as
716
+ seeds, with the nuclei-seeded watershed or PlantSeg plugins. See
717
+ [Cells from a membrane stain](membrane_cells.md).
718
+
699
719
  Only the `segment` step reads it. The pair is stacked on a leading axis that
700
720
  is *carried* into each tile rather than tiled, so the tile geometry, the
701
721
  occupancy map and the staged labels are byte-for-byte what a single-channel
@@ -802,9 +822,10 @@ results/image.zarr/labels/cyto_labels/
802
822
  rule, so it doesn't get a `log:` directive for free. Standalone (or
803
823
  under plain `multi`), it writes to `<work_dir>/logs/relate.log`
804
824
  (override with `relate.py --log`), the same tee-to-file-and-stdout
805
- behaviour as the other steps. Under `multi-slurm`, where each pair is
806
- its own concurrent job, `run_multi.py` points each one at its own file
807
- instead — `<work_dir>/logs/relate/<a>_to_<b>.log` — so concurrent pairs
825
+ behaviour as the other steps. Under `multi-slurm`, where each parent's
826
+ relations are one concurrent job, `run_multi.py` points each job at its
827
+ own file instead — `<work_dir>/logs/relate/to_<parent>.log`, or
828
+ `<a>_to_<b>.log` for a parent with a single child — so concurrent jobs
808
829
  don't interleave into one log; check there instead of scrolling back
809
830
  through `srun`'s live output.
810
831
 
@@ -833,6 +854,11 @@ relations:
833
854
  output: nuclei_to_cyto.xlsx # written into work_dir
834
855
  ```
835
856
 
857
+ A second example, `config/multi_plantseg.yaml`, pairs Cellpose nuclei with
858
+ PlantSeg cells seeded from the nuclei (`pixi run -e plantseg
859
+ multi-plantseg-slurm`); see [Cells from a membrane
860
+ stain](membrane_cells.md#example-cellpose-nuclei-plantseg-cells-in-one-run).
861
+
836
862
  `common:` is optional — leave it out and each config must be self-contained,
837
863
  as before. With it, changing the input path or turning on `shard` is a
838
864
  one-line edit in one file instead of the same edit repeated per config.
@@ -843,6 +869,19 @@ pixi run multi # run locally
843
869
  pixi run multi-slurm # submit every segmentation to SLURM
844
870
  ```
845
871
 
872
+ They read `config/multi.yaml` unless given another with `--config`:
873
+
874
+ ```bash
875
+ pixi run multi-slurm --config /data/run42/multi.yaml
876
+ pixi run multi-dry --config my_multi.yaml # relative to where you run pixi
877
+ ```
878
+
879
+ A relative `--config` is looked for in the directory you run `pixi` from,
880
+ then in `workflow/`. The `common:` and `segmentations:` paths inside it are
881
+ looked for next to the multi config first, then in `workflow/` -- so a run's
882
+ configs can live together in a folder of their own, beside the data, while
883
+ the shipped `config/multi.yaml` keeps working as it is.
884
+
846
885
  Before anything is submitted, the script checks that every listed config
847
886
  shares one `work_dir` (so `label_relations` has a single `image.zarr` to read
848
887
  both label groups from), that `tile_shape` and `level` are identical (so the
@@ -880,7 +919,7 @@ abort the others; you get a per-config status and a non-zero exit.
880
919
  # config/multi.yaml
881
920
  relate:
882
921
  qos: "1day"
883
- time: 720 # minutes, per pair; must stay under that QOS's MaxWall
922
+ time: 720 # minutes, per job (one per parent); under the QOS's MaxWall
884
923
  ```
885
924
 
886
925
  A `--relate-*` flag overrides the block for that one key; anything the
@@ -889,18 +928,23 @@ abort the others; you get a per-config status and a non-zero exit.
889
928
  default you meant to replace. Under plain `multi` (no `--profile`), relations
890
929
  still run locally, in-process, one after another, as before.
891
930
 
892
- Each pair logs its shape, chunk count and object count before it starts,
893
- then a progress line roughly once a minute (`label_relations: 412/3,600
894
- (11%) after 7m, ~55m left`), so a long relation is distinguishable from a
895
- hung one in `logs/relate/<a>_to_<b>.log`.
896
-
897
- Because every pair gets its own job, one running long no longer starves
898
- the others out of a shared time budget, and a pair that gets killed no
899
- longer takes an already-finished sibling's workbook down with it.
900
- `relate.py` also skips a pair whose `.xlsx` is already newer than both
901
- labels' merge marker, so **re-running the exact same `multi-slurm`
902
- command only recomputes what's still missing or stale** — delete a
903
- specific `.xlsx` yourself to force just that one to recompute.
931
+ The relations are grouped by parent: one job reads each parent label
932
+ image once for all its children (e.g. nuclei, cilia and the other cell
933
+ segmentation against `cyto_labels`), and only the chunks some child has
934
+ labels in. Each job logs its images' shape, chunking and object counts
935
+ before it starts, then a progress line roughly once a minute
936
+ (`label_relations: 412/3,600 (11%) after 7m, ~55m left`), so a long
937
+ relation is distinguishable from a hung one in `logs/relate/`.
938
+
939
+ Because each parent gets its own job, one running long does not starve
940
+ the others out of a shared time budget. `relate.py` skips a pair whose
941
+ workbook (the `.xlsx`, or the two `.csv` files a sheet too long for
942
+ Excel is written as) is already newer than both labels' merge marker,
943
+ and rewrites a missing workbook from the object tables when they
944
+ already hold the relation for the current labels, so **re-running the
945
+ exact same `multi-slurm` command only recomputes what's still missing
946
+ or stale** — delete a specific workbook to force just that one. The
947
+ bundle is likewise left alone when nothing in the store changed.
904
948
 
905
949
  !!! tip "After a killed run"
906
950
  Snakemake only releases its lock on a clean exit, so a run that was killed
@@ -1075,6 +1119,11 @@ pixi run go # run locally (8 cores)
1075
1119
  pixi run slurm # submit to SLURM (edit profile/slurm/config.yaml first)
1076
1120
  ```
1077
1121
 
1122
+ Optional environments add methods: `-e plantseg` (PlantSeg, from
1123
+ conda-forge), `-e careamics` (the `denoise:` step and `pixi run denoise-train`),
1124
+ `-e plantseg-careamics` for both -- see
1125
+ [Cells from a membrane stain](membrane_cells.md).
1126
+
1078
1127
  `pixi run …` activates the env, so the rule scripts execute in that env — do
1079
1128
  **not** pass `--use-conda`. On a cluster, keep the `workflow/` directory on a
1080
1129
  shared filesystem the compute nodes can read: the SLURM executor re-launches