patchworks 2.6.11__tar.gz → 2.7.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.
- {patchworks-2.6.11 → patchworks-2.7.0}/PKG-INFO +2 -1
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/ome_zarr.md +12 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/snakemake.md +138 -7
- {patchworks-2.6.11 → patchworks-2.7.0}/pyproject.toml +10 -1
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_chunks.py +29 -2
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_io.py +15 -4
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/cellpose.py +27 -1
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/ome_zarr.py +440 -162
- patchworks-2.7.0/tests/ngff_schemas/0.4/image.schema +233 -0
- patchworks-2.7.0/tests/ngff_schemas/0.4/label.schema +77 -0
- patchworks-2.7.0/tests/ngff_schemas/0.4/ome.schema +17 -0
- patchworks-2.7.0/tests/ngff_schemas/0.5/_version.schema +10 -0
- patchworks-2.7.0/tests/ngff_schemas/0.5/image.schema +268 -0
- patchworks-2.7.0/tests/ngff_schemas/0.5/label.schema +91 -0
- patchworks-2.7.0/tests/ngff_schemas/0.5/ome.schema +33 -0
- patchworks-2.7.0/tests/ngff_schemas/README.md +16 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_allocation.py +57 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_cellpose.py +24 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_ome_zarr.py +328 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_pw.py +101 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_run_multi.py +75 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/common.yaml +6 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config.yaml +14 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/common.smk +9 -2
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/_pw.py +76 -6
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/build_occupancy.py +5 -1
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/convert.py +8 -2
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/merge.py +28 -1
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/prepare_tiles.py +12 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/run_multi.py +1 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/.github/workflows/docs.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/.github/workflows/lint.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/.github/workflows/release.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/.gitignore +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/LICENSE +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/README.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/cliff.toml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/chunks.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/cluster.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/io.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/postprocess.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/relabel.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/tile_process.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/volume_filter.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/assets/logo.png +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/custom.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/custom_method.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/dog.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/dog.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/stardist.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/getting_started.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/label_relations.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/measurements.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/merging.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/performance.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/tiling.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/docs/index.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/mkdocs.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/__init__.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_core.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_distributed.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_merge.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_notify.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_progress.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_relations.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_volume_filter.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_core.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_distributed.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_dog.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_gpu.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_napari.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_notify.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_occupancy.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_postprocess.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_progress.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_relations.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_volume_filter.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/README.md +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/Snakefile +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/multi.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/pixi.toml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/convert.smk +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/merge.smk +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/segment.smk +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/relate.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/segment_tile.py +0 -0
- {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.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
|
|
@@ -57,6 +57,7 @@ Requires-Dist: cellpose<4,>=3.0; extra == 'cellpose3'
|
|
|
57
57
|
Provides-Extra: cellpose4
|
|
58
58
|
Requires-Dist: cellpose>=4; extra == 'cellpose4'
|
|
59
59
|
Provides-Extra: dev
|
|
60
|
+
Requires-Dist: jsonschema>=4.18; extra == 'dev'
|
|
60
61
|
Requires-Dist: psutil; extra == 'dev'
|
|
61
62
|
Requires-Dist: pytest; extra == 'dev'
|
|
62
63
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
@@ -25,6 +25,10 @@ resolution, matching anisotropic microscopy stacks.
|
|
|
25
25
|
|
|
26
26
|
::: patchworks.plugins.ome_zarr.register_labels
|
|
27
27
|
|
|
28
|
+
## reshard_level
|
|
29
|
+
|
|
30
|
+
::: patchworks.plugins.ome_zarr.reshard_level
|
|
31
|
+
|
|
28
32
|
## read_pixel_size
|
|
29
33
|
|
|
30
34
|
::: patchworks.plugins.ome_zarr.read_pixel_size
|
|
@@ -37,6 +41,14 @@ matches the store, and reads both.
|
|
|
37
41
|
|
|
38
42
|
::: patchworks.plugins.ome_zarr.ngff_version
|
|
39
43
|
|
|
44
|
+
Every writer takes an `ngff_version=` keyword: `"auto"` (default) follows the
|
|
45
|
+
installed zarr — 0.5 on v3, 0.4 on v2 — and `"0.4"` pins the older, zarr-v2
|
|
46
|
+
layout. Writing into an existing store always follows *that store's* format,
|
|
47
|
+
so a label pyramid added later can never disagree with the image it sits in.
|
|
48
|
+
NGFF 0.6 is released but not written: RFC-5 replaces `axes` with
|
|
49
|
+
`coordinateSystems` and requires `input`/`output` on every coordinate
|
|
50
|
+
transformation, and no reader supports it yet.
|
|
51
|
+
|
|
40
52
|
::: patchworks.plugins.ome_zarr.read_ngff_attr
|
|
41
53
|
|
|
42
54
|
::: patchworks.plugins.ome_zarr.write_ngff_attrs
|
|
@@ -50,7 +50,9 @@ work_dir: "/scratch/results" # everything is written here
|
|
|
50
50
|
# conversion (input → pyramidal OME-ZARR)
|
|
51
51
|
reuse_pyramid: true # .ims: copy its own pyramid (fast)
|
|
52
52
|
convert_chunks: null # null → bounded auto chunks; or [c,z,y,x]
|
|
53
|
-
shard: false # true → pack chunks into shards (fewer files)
|
|
53
|
+
shard: false # true → pack chunks into shards (fewer files);
|
|
54
|
+
# covers the image and the label pyramids
|
|
55
|
+
ngff_version: "auto" # OME-ZARR version: "auto" (0.5), or "0.4"
|
|
54
56
|
|
|
55
57
|
# tiling
|
|
56
58
|
channel: 0 # channel to segment, 0-based (null = keep all)
|
|
@@ -83,6 +85,8 @@ cellpose:
|
|
|
83
85
|
pyramid_levels: 5
|
|
84
86
|
pyramid_downscale: 2
|
|
85
87
|
sequential_labels: true # renumber labels to a contiguous 1..N
|
|
88
|
+
shard_labels: false # true → also reshard label level 0 after the
|
|
89
|
+
# merge (one extra pass; see tip below)
|
|
86
90
|
```
|
|
87
91
|
|
|
88
92
|
!!! tip "Growing labels after segmentation"
|
|
@@ -98,6 +102,77 @@ sequential_labels: true # renumber labels to a contiguous 1..N
|
|
|
98
102
|
labels afterwards](custom_segmentation.md#growing-labels-afterwards-dilation)
|
|
99
103
|
for how it works and the equivalent direct-API call.
|
|
100
104
|
|
|
105
|
+
!!! tip "What `shard: true` covers, and `shard_labels` for label level 0"
|
|
106
|
+
Without sharding, one chunk is one file, and a fine-chunked level 0 can
|
|
107
|
+
run to ~950k of them — painful on a shared filesystem at write time and
|
|
108
|
+
on every read after. `shard: true` packs chunks into far fewer files
|
|
109
|
+
without changing the chunking or the memory profile.
|
|
110
|
+
|
|
111
|
+
`shard: true` applies to **the converted image (all levels)** and to
|
|
112
|
+
**the label pyramid levels (1..N)**. It cannot apply to **label level
|
|
113
|
+
0** *while that level is being written*: a shard has to be written whole
|
|
114
|
+
by a single writer, while level 0 is filled one chunk at a time by
|
|
115
|
+
concurrent `segment` jobs (or by the merge's own worker pool). Two
|
|
116
|
+
writers doing a read-modify-write on the same shard file silently lose
|
|
117
|
+
each other's chunks.
|
|
118
|
+
|
|
119
|
+
Once the merge has finished, though, nothing is writing it anymore, so a
|
|
120
|
+
single-threaded pass *can* rewrite it sharded. That is `shard_labels`:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
shard: true
|
|
124
|
+
shard_labels: true # or an explicit shape, e.g. [16, 512, 512]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`true` reuses whatever spec `shard` carries; a list overrides it.
|
|
128
|
+
`shard: false` does not veto it — an unsharded image with sharded labels
|
|
129
|
+
is a valid combination. It is opt-in because it costs one extra full
|
|
130
|
+
read+write of level 0, and because it is the level with the most chunks
|
|
131
|
+
it is also the one worth paying for. The array's attributes (including
|
|
132
|
+
the merge's own completion marker) are carried across, so a later rerun
|
|
133
|
+
still sees the merge as done.
|
|
134
|
+
|
|
135
|
+
Check whether the file count is actually a problem for your data first —
|
|
136
|
+
a `(126, 34000, 28500)` image at the `(16, 1024, 1024)` label chunk cap
|
|
137
|
+
is 7,616 files per label group, well under the 200,000 at which the
|
|
138
|
+
conversion starts warning. On scicore that is fine; on a filesystem with
|
|
139
|
+
a tighter inode or per-directory budget it may not be.
|
|
140
|
+
|
|
141
|
+
!!! tip "Which OME-ZARR version gets written (`ngff_version`)"
|
|
142
|
+
OME-ZARR is versioned, and the version decides the **zarr format** as
|
|
143
|
+
well as the metadata layout — the two are not separate choices. NGFF 0.4
|
|
144
|
+
is specified over zarr v2 and puts `multiscales`, `labels` and
|
|
145
|
+
`image-label` at the top level of the store's attributes; 0.5 is the
|
|
146
|
+
zarr-v3 revision and nests them under an `ome` key.
|
|
147
|
+
|
|
148
|
+
`ngff_version: "auto"` (the default) follows the installed zarr, which on
|
|
149
|
+
any current environment means **0.5**. Pin `"0.4"` only when a downstream
|
|
150
|
+
tool still cannot read 0.5:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
ngff_version: "0.4"
|
|
154
|
+
shard: false # required: zarr v2 has no sharding codec
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
That is a real trade-off, not a formality — 0.4 means a zarr-v2 store, and
|
|
158
|
+
sharding is a zarr-v3 feature, so `shard`/`shard_labels` stop doing
|
|
159
|
+
anything. The workflow refuses the combination rather than silently
|
|
160
|
+
ignoring it. The store's own root file changes too (`.zgroup` instead of
|
|
161
|
+
`zarr.json`), which the rules account for.
|
|
162
|
+
|
|
163
|
+
Writing into an **existing** store always follows that store's format,
|
|
164
|
+
whatever this key says: the label pyramid is written by `merge`, long
|
|
165
|
+
after `convert` made the image, and a store with v2 arrays and v3
|
|
166
|
+
metadata is one no reader can open.
|
|
167
|
+
|
|
168
|
+
**What about 0.6?** It was released on 14 September 2026 and patchworks
|
|
169
|
+
does not write it. It is not a version bump but a different metadata
|
|
170
|
+
document: RFC-5 replaces a multiscale's `axes` with `coordinateSystems`
|
|
171
|
+
and requires an `input`/`output` pair on every coordinate transformation.
|
|
172
|
+
Nothing reads it yet either — ome-zarr-py, and so napari, still default
|
|
173
|
+
to 0.5. Setting `ngff_version: "0.6"` is rejected with that explanation
|
|
174
|
+
rather than writing a store you could not open.
|
|
175
|
+
|
|
101
176
|
!!! tip "Dropping objects by size with `min_volume`/`max_volume`"
|
|
102
177
|
`min_volume: N` drops any object smaller than `N` µm³; `max_volume: N`
|
|
103
178
|
drops any object larger than `N` µm³ (e.g. several objects merged into
|
|
@@ -112,15 +187,60 @@ sequential_labels: true # renumber labels to a contiguous 1..N
|
|
|
112
187
|
[Filtering by size after merge](merging.md#filtering-by-size-after-merge)
|
|
113
188
|
for the equivalent direct-API call.
|
|
114
189
|
|
|
190
|
+
!!! warning "`model:` names are version-specific — check which Cellpose you have"
|
|
191
|
+
Cellpose 3 and 4 have **disjoint** model names: v3 has `cyto3`,
|
|
192
|
+
`nuclei`, …; v4 replaced them all with the `cpsam` family (`cpsam`,
|
|
193
|
+
`cpsam_v2`, `cpdino`, …). Neither version *raises* on a name it doesn't
|
|
194
|
+
know — v4 logs a warning and quietly loads its default (`cpsam_v2`)
|
|
195
|
+
instead. A `cyto3` left in a config against a v4 install therefore
|
|
196
|
+
segments every tile with a model you did not choose, and the only
|
|
197
|
+
evidence is one line in the job log.
|
|
198
|
+
|
|
199
|
+
patchworks now rejects an unavailable name in `prepare`, on the cheap
|
|
200
|
+
CPU job, rather than letting it through. Check what you have with
|
|
201
|
+
`python -c "import cellpose; print(cellpose.version)"`, then either pick
|
|
202
|
+
a name that install offers, point `model:` at a custom-trained model's
|
|
203
|
+
path, or install the version you want — the workflow ships `cellpose3`
|
|
204
|
+
and `cellpose4` pixi environments (`pixi install -e cellpose3`) for
|
|
205
|
+
exactly this.
|
|
206
|
+
|
|
115
207
|
!!! tip "3-D anisotropy is derived automatically"
|
|
116
208
|
Cellpose's `do_3D` assumes isotropic voxels unless told otherwise —
|
|
117
209
|
without an `anisotropy`, a real (anisotropic) dataset gets objects
|
|
118
|
-
fragmented or distorted across z. `segment`
|
|
210
|
+
fragmented or distorted across z. `segment` derives it from
|
|
119
211
|
`image.zarr`'s own calibration (`z` voxel size ÷ lateral voxel size)
|
|
120
212
|
whenever `do_3D: true` and `cellpose.anisotropy` isn't set explicitly, so
|
|
121
213
|
there's usually nothing to configure. Set `anisotropy:` yourself in the
|
|
122
214
|
`cellpose:` block to override it.
|
|
123
215
|
|
|
216
|
+
Note it is not just a hint to the model: Cellpose *resizes* the tile to
|
|
217
|
+
`z × anisotropy` planes before the net runs. `tile_shape: "auto"`
|
|
218
|
+
budgets for that resized tile, so a 2.2× anisotropy buys a
|
|
219
|
+
correspondingly smaller tile rather than an out-of-memory job.
|
|
220
|
+
|
|
221
|
+
!!! warning "The physically-correct anisotropy is not always the best one"
|
|
222
|
+
Upsampling z by the true ratio can produce **ring artifacts** and costs
|
|
223
|
+
runtime proportional to the ratio. A contributor on
|
|
224
|
+
[cellpose#1408](https://github.com/MouseLand/cellpose/pull/1408) reports
|
|
225
|
+
that `anisotropy: 1` avoids the rings entirely and is much faster, at
|
|
226
|
+
the cost of boundaries being off by a few pixels in the top/bottom
|
|
227
|
+
z-planes, where a cell's cross-section changes fastest — and recommends
|
|
228
|
+
pairing it with light z-only flow smoothing:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
cellpose:
|
|
232
|
+
do_3D: true
|
|
233
|
+
anisotropy: 1 # overrides the derived value
|
|
234
|
+
flow3D_smooth: [1, 0, 0] # z, y, x -- smooth z only
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This is a genuine trade-off, not a strictly better setting, and it is
|
|
238
|
+
worth testing both on your own data rather than taking either on faith.
|
|
239
|
+
Reach for it especially if 3-D results look ringed or fragmented along
|
|
240
|
+
z: that is the symptom this addresses. `flow3D_smooth` accepts a scalar
|
|
241
|
+
on older Cellpose and a `[z, y, x]` list from the version that merged
|
|
242
|
+
that PR onward.
|
|
243
|
+
|
|
124
244
|
!!! tip "Tile size vs runtime"
|
|
125
245
|
`tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
|
|
126
246
|
more (faster) jobs; very large 3-D tiles are slow. Keep `do_3D: false` (2-D
|
|
@@ -317,6 +437,7 @@ input: "/data/scan.ims"
|
|
|
317
437
|
work_dir: "/scratch/results"
|
|
318
438
|
tile_shape: [16, 1024, 1024]
|
|
319
439
|
shard: false # true → far fewer files, same chunks
|
|
440
|
+
ngff_version: "auto" # convert reads it, so it belongs here
|
|
320
441
|
tiles_per_job: 4
|
|
321
442
|
```
|
|
322
443
|
|
|
@@ -402,11 +523,21 @@ snakemake --workflow-profile profile/slurm --configfile config/common.yaml confi
|
|
|
402
523
|
```
|
|
403
524
|
|
|
404
525
|
!!! warning "Conversion settings belong in the shared file"
|
|
405
|
-
`convert` runs **once**, from the first config only. A `shard`, `input
|
|
406
|
-
`
|
|
407
|
-
nothing logs that it was dropped.
|
|
408
|
-
|
|
409
|
-
the configs by hand, keep them in
|
|
526
|
+
`convert` runs **once**, from the first config only. A `shard`, `input`,
|
|
527
|
+
`convert_chunks`, `sequence_pattern` or `reuse_pyramid` set on the second
|
|
528
|
+
config is therefore never read, and nothing logs that it was dropped.
|
|
529
|
+
`run_multi` refuses to start when those keys disagree across configs and
|
|
530
|
+
tells you which one — but if you drive the configs by hand, keep them in
|
|
531
|
+
`common.yaml`.
|
|
532
|
+
|
|
533
|
+
`ngff_version` is in that list: it decides the store's zarr format, so a
|
|
534
|
+
second config disagreeing about it would describe a store that is not the
|
|
535
|
+
one on disk.
|
|
536
|
+
|
|
537
|
+
`merge` runs once **per config**, so the keys it reads —
|
|
538
|
+
`pyramid_levels`, `pyramid_downscale`, `sequential_labels`,
|
|
539
|
+
`min_volume`/`max_volume` and `shard_labels` — may legitimately differ
|
|
540
|
+
between them, and are only in `common.yaml` for convenience.
|
|
410
541
|
|
|
411
542
|
Splitting the configs is optional: a self-contained config still works,
|
|
412
543
|
and `common:` can simply be left out of `multi.yaml`.
|
|
@@ -96,7 +96,16 @@ napari = [
|
|
|
96
96
|
# openpyxl -> scripts/run_multi.py writes label_relations() output as an
|
|
97
97
|
# Excel workbook (per-object + per-container sheets), not a plain CSV.
|
|
98
98
|
workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm", "openpyxl"]
|
|
99
|
-
|
|
99
|
+
# jsonschema validates what we write against the vendored official
|
|
100
|
+
# OME-NGFF schemas (tests/ngff_schemas/); without it that one test skips.
|
|
101
|
+
dev = [
|
|
102
|
+
"pytest",
|
|
103
|
+
"pytest-cov",
|
|
104
|
+
"scikit-image",
|
|
105
|
+
"psutil",
|
|
106
|
+
"tqdm",
|
|
107
|
+
"jsonschema>=4.18",
|
|
108
|
+
]
|
|
100
109
|
docs = ["mkdocs-material>=9.0", "mkdocstrings[python]>=0.24"]
|
|
101
110
|
all = [
|
|
102
111
|
"patchworks[io,gpu,bioio,imaris,napari]",
|
|
@@ -380,6 +380,7 @@ def auto_tile_shape_cellpose(
|
|
|
380
380
|
model_memory_bytes: int = 2 * 1024**3,
|
|
381
381
|
cellpose_memory_factor: int = 20,
|
|
382
382
|
n_channels: int = 1,
|
|
383
|
+
anisotropy: float | None = None,
|
|
383
384
|
verbose: bool = False,
|
|
384
385
|
) -> tuple[int, ...]:
|
|
385
386
|
"""Cellpose-optimised tile shape.
|
|
@@ -426,6 +427,13 @@ def auto_tile_shape_cellpose(
|
|
|
426
427
|
Channels each tile carries (default 1). Above 1 the per-voxel cost
|
|
427
428
|
scales with it, so the tile shrinks accordingly -- e.g. the workflow's
|
|
428
429
|
``nuclei_channel`` hands Cellpose a cyto+nuclei pair.
|
|
430
|
+
anisotropy:
|
|
431
|
+
The ``anisotropy`` Cellpose will be given (``do_3D`` only). It is not
|
|
432
|
+
merely a hint to the model: Cellpose resizes the tile to
|
|
433
|
+
``z * anisotropy`` planes before the net runs, so a 2.2x anisotropy
|
|
434
|
+
costs 2.2x the z it was handed. Budgeting against the unscaled ``z``
|
|
435
|
+
under-counts by exactly that factor. ``None`` (the default) assumes
|
|
436
|
+
isotropic, i.e. no resize.
|
|
429
437
|
verbose:
|
|
430
438
|
Log the chosen shape and memory estimates.
|
|
431
439
|
|
|
@@ -441,6 +449,18 @@ def auto_tile_shape_cellpose(
|
|
|
441
449
|
(1, 2048, 2048)
|
|
442
450
|
"""
|
|
443
451
|
n_workers = n_workers or cpu_allocation()
|
|
452
|
+
# Cellpose resizes the tile before the net runs -- by `rescale`
|
|
453
|
+
# (= 30 / diameter) on every axis, and by `anisotropy` on z as well -- so
|
|
454
|
+
# the array it actually holds is bigger than the one it was handed, and a
|
|
455
|
+
# budget computed from the unresized tile under-counts by that factor. A
|
|
456
|
+
# diameter half the model's 30 px means a 2x upsample per axis: 8x the
|
|
457
|
+
# voxels, enough to turn a comfortable tile into an OOM.
|
|
458
|
+
#
|
|
459
|
+
# Both only ever *shrink* the tile. A predicted downsample (diameter > 30)
|
|
460
|
+
# would license a bigger one, but these are a safety margin against a
|
|
461
|
+
# rough memory model, not a measurement to spend headroom on.
|
|
462
|
+
rescale = max(1.0, 30.0 / diameter) if diameter else 1.0
|
|
463
|
+
z_resize = max(1.0, anisotropy or 1.0)
|
|
444
464
|
# A tile holds n_channels planes per voxel (e.g. Cellpose's
|
|
445
465
|
# cyto+nuclei pair), so the per-voxel cost -- and every budget
|
|
446
466
|
# derived from it below -- scales with them.
|
|
@@ -467,7 +487,8 @@ def auto_tile_shape_cellpose(
|
|
|
467
487
|
min_tile = int(4 * diameter) if diameter is not None else 1
|
|
468
488
|
|
|
469
489
|
if n_spatial == 2 or not do_3D:
|
|
470
|
-
|
|
490
|
+
# Two axes resized, so the cost per configured pixel is rescale**2.
|
|
491
|
+
max_pixels_2d = max(1, int(max_raw_bytes // (itemsize * rescale**2)))
|
|
471
492
|
tile_side = max(min_tile, int(max_pixels_2d**0.5))
|
|
472
493
|
if n_spatial == 2:
|
|
473
494
|
y, x = shape[-2], shape[-1]
|
|
@@ -477,7 +498,13 @@ def auto_tile_shape_cellpose(
|
|
|
477
498
|
chunk_spatial = [1, min(y, tile_side), min(x, tile_side)]
|
|
478
499
|
else:
|
|
479
500
|
z, y, x = shape[-3], shape[-2], shape[-1]
|
|
480
|
-
|
|
501
|
+
# All three axes are resized: z by anisotropy * rescale, y/x by
|
|
502
|
+
# rescale each -- so one configured voxel costs
|
|
503
|
+
# anisotropy * rescale**3 of them.
|
|
504
|
+
effective_z = z * z_resize * rescale**3
|
|
505
|
+
max_pixels_per_slice = max(
|
|
506
|
+
1, int((max_raw_bytes // 3) // (effective_z * itemsize))
|
|
507
|
+
)
|
|
481
508
|
tile_side = max(min_tile, int(max_pixels_per_slice**0.5))
|
|
482
509
|
chunk_spatial = [z, min(y, tile_side), min(x, tile_side)]
|
|
483
510
|
|
|
@@ -18,27 +18,38 @@ _ZARR_V3 = int(zarr.__version__.split(".")[0]) >= 3
|
|
|
18
18
|
_ZARR_V3 = int(zarr.__version__.split(".")[0]) >= 3
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def zarr_compressor_kwargs() -> dict:
|
|
21
|
+
def zarr_compressor_kwargs(zarr_format: int = 3) -> dict:
|
|
22
22
|
"""Keyword arguments pinning the compression codec for a new array.
|
|
23
23
|
|
|
24
24
|
zstd is already zarr v3's default, but relying on a library default means
|
|
25
25
|
the stores patchworks writes change silently if that default ever moves.
|
|
26
26
|
Labels in particular are highly compressible, so this is worth stating.
|
|
27
27
|
|
|
28
|
+
The codec *object* depends on the format of the array being written, not
|
|
29
|
+
on the installed zarr: zarr-python 3 can write a zarr-v2 array (which is
|
|
30
|
+
what NGFF 0.4 needs), and a v2 array rejects ``zarr.codecs.ZstdCodec`` --
|
|
31
|
+
it wants the numcodecs one.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
zarr_format : int, optional
|
|
36
|
+
Format of the array about to be created, 2 or 3 (default 3).
|
|
37
|
+
|
|
28
38
|
Returns
|
|
29
39
|
-------
|
|
30
40
|
dict
|
|
31
|
-
``compressors=``/``compressor=`` as
|
|
41
|
+
``compressors=``/``compressor=`` as that combination expects, or
|
|
32
42
|
empty if the codec cannot be built (then the default applies).
|
|
33
43
|
"""
|
|
34
44
|
try:
|
|
35
|
-
if _ZARR_V3:
|
|
45
|
+
if _ZARR_V3 and zarr_format != 2:
|
|
36
46
|
from zarr.codecs import ZstdCodec
|
|
37
47
|
|
|
38
48
|
return {"compressors": (ZstdCodec(level=1),)}
|
|
39
49
|
import numcodecs
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
codec = numcodecs.Zstd(level=1)
|
|
52
|
+
return {"compressors": (codec,)} if _ZARR_V3 else {"compressor": codec}
|
|
42
53
|
except Exception: # pragma: no cover - depends on the installed zarr
|
|
43
54
|
logger.debug("could not pin a compressor; using zarr's default")
|
|
44
55
|
return {}
|
|
@@ -253,6 +253,26 @@ def _make_config(
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
|
|
256
|
+
def available_models() -> list[str]:
|
|
257
|
+
"""Pretrained model names the installed Cellpose actually accepts.
|
|
258
|
+
|
|
259
|
+
The name set changed completely between major versions -- v3 ships
|
|
260
|
+
``cyto3``/``nuclei``/… , v4 replaced them with ``cpsam``-family names --
|
|
261
|
+
and neither version raises on an unknown one. v4 falls back to its
|
|
262
|
+
default model with only a log line, so a v3 name in a v4 environment
|
|
263
|
+
segments everything with the wrong model and nothing fails. Checking the
|
|
264
|
+
name against this list turns that into a config error instead.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
list of str
|
|
269
|
+
Model names, or an empty list when Cellpose isn't installed or
|
|
270
|
+
doesn't publish them (in which case no name can be rejected).
|
|
271
|
+
"""
|
|
272
|
+
names = getattr(_cellpose_models, "MODEL_NAMES", None)
|
|
273
|
+
return list(names) if names else []
|
|
274
|
+
|
|
275
|
+
|
|
256
276
|
def _get_model(cellpose_dict: dict[str, Any]) -> Any:
|
|
257
277
|
"""Return a worker-local cached Cellpose model.
|
|
258
278
|
|
|
@@ -272,8 +292,14 @@ def _get_model(cellpose_dict: dict[str, Any]) -> Any:
|
|
|
272
292
|
gpu = cellpose_dict.get("gpu", False)
|
|
273
293
|
model_type = cellpose_dict["model"]
|
|
274
294
|
if _CELLPOSE_V4:
|
|
295
|
+
# v4 renamed this: `model_type=` is accepted but explicitly
|
|
296
|
+
# ignored ("not used in v4.0.1+"), leaving pretrained_model at
|
|
297
|
+
# its "cpsam_v2" default -- so passing the configured name there
|
|
298
|
+
# silently segmented *every* config with the same default model,
|
|
299
|
+
# whatever `model:` said, with only a logger warning to show for
|
|
300
|
+
# it. `available_models()` rejects an unusable name up front.
|
|
275
301
|
_model_cache[key] = _cellpose_models.CellposeModel(
|
|
276
|
-
|
|
302
|
+
pretrained_model=model_type, gpu=gpu
|
|
277
303
|
)
|
|
278
304
|
else:
|
|
279
305
|
_model_cache[key] = _cellpose_models.Cellpose(
|