patchworks 2.6.11__tar.gz → 2.6.12__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.6.12}/PKG-INFO +1 -1
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/snakemake.md +46 -1
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_chunks.py +14 -1
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/cellpose.py +27 -1
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_allocation.py +29 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_cellpose.py +24 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_pw.py +41 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/_pw.py +38 -6
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/prepare_tiles.py +12 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/.github/workflows/docs.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/.github/workflows/lint.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/.github/workflows/release.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/.gitignore +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/LICENSE +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/README.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/cliff.toml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/chunks.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/cluster.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/io.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/postprocess.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/relabel.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/tile_process.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/api/volume_filter.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/assets/logo.png +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/custom.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/custom_method.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/dog.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/dog.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/stardist.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/getting_started.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/label_relations.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/measurements.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/merging.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/performance.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/guide/tiling.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/docs/index.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/mkdocs.yml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/pyproject.toml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/__init__.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_core.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_distributed.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_io.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_merge.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_notify.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_progress.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_relations.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/_volume_filter.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_core.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_distributed.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_dog.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_gpu.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_napari.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_notify.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_occupancy.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_ome_zarr.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_postprocess.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_progress.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_relations.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_run_multi.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/tests/test_volume_filter.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/README.md +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/Snakefile +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/common.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/config/multi.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/pixi.toml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/common.smk +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/convert.smk +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/merge.smk +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/rules/segment.smk +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/build_occupancy.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/convert.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/merge.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/relate.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/run_multi.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/segment_tile.py +0 -0
- {patchworks-2.6.11 → patchworks-2.6.12}/workflow/scripts/view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.12
|
|
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
|
|
@@ -112,15 +112,60 @@ sequential_labels: true # renumber labels to a contiguous 1..N
|
|
|
112
112
|
[Filtering by size after merge](merging.md#filtering-by-size-after-merge)
|
|
113
113
|
for the equivalent direct-API call.
|
|
114
114
|
|
|
115
|
+
!!! warning "`model:` names are version-specific — check which Cellpose you have"
|
|
116
|
+
Cellpose 3 and 4 have **disjoint** model names: v3 has `cyto3`,
|
|
117
|
+
`nuclei`, …; v4 replaced them all with the `cpsam` family (`cpsam`,
|
|
118
|
+
`cpsam_v2`, `cpdino`, …). Neither version *raises* on a name it doesn't
|
|
119
|
+
know — v4 logs a warning and quietly loads its default (`cpsam_v2`)
|
|
120
|
+
instead. A `cyto3` left in a config against a v4 install therefore
|
|
121
|
+
segments every tile with a model you did not choose, and the only
|
|
122
|
+
evidence is one line in the job log.
|
|
123
|
+
|
|
124
|
+
patchworks now rejects an unavailable name in `prepare`, on the cheap
|
|
125
|
+
CPU job, rather than letting it through. Check what you have with
|
|
126
|
+
`python -c "import cellpose; print(cellpose.version)"`, then either pick
|
|
127
|
+
a name that install offers, point `model:` at a custom-trained model's
|
|
128
|
+
path, or install the version you want — the workflow ships `cellpose3`
|
|
129
|
+
and `cellpose4` pixi environments (`pixi install -e cellpose3`) for
|
|
130
|
+
exactly this.
|
|
131
|
+
|
|
115
132
|
!!! tip "3-D anisotropy is derived automatically"
|
|
116
133
|
Cellpose's `do_3D` assumes isotropic voxels unless told otherwise —
|
|
117
134
|
without an `anisotropy`, a real (anisotropic) dataset gets objects
|
|
118
|
-
fragmented or distorted across z. `segment`
|
|
135
|
+
fragmented or distorted across z. `segment` derives it from
|
|
119
136
|
`image.zarr`'s own calibration (`z` voxel size ÷ lateral voxel size)
|
|
120
137
|
whenever `do_3D: true` and `cellpose.anisotropy` isn't set explicitly, so
|
|
121
138
|
there's usually nothing to configure. Set `anisotropy:` yourself in the
|
|
122
139
|
`cellpose:` block to override it.
|
|
123
140
|
|
|
141
|
+
Note it is not just a hint to the model: Cellpose *resizes* the tile to
|
|
142
|
+
`z × anisotropy` planes before the net runs. `tile_shape: "auto"`
|
|
143
|
+
budgets for that resized tile, so a 2.2× anisotropy buys a
|
|
144
|
+
correspondingly smaller tile rather than an out-of-memory job.
|
|
145
|
+
|
|
146
|
+
!!! warning "The physically-correct anisotropy is not always the best one"
|
|
147
|
+
Upsampling z by the true ratio can produce **ring artifacts** and costs
|
|
148
|
+
runtime proportional to the ratio. A contributor on
|
|
149
|
+
[cellpose#1408](https://github.com/MouseLand/cellpose/pull/1408) reports
|
|
150
|
+
that `anisotropy: 1` avoids the rings entirely and is much faster, at
|
|
151
|
+
the cost of boundaries being off by a few pixels in the top/bottom
|
|
152
|
+
z-planes, where a cell's cross-section changes fastest — and recommends
|
|
153
|
+
pairing it with light z-only flow smoothing:
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
cellpose:
|
|
157
|
+
do_3D: true
|
|
158
|
+
anisotropy: 1 # overrides the derived value
|
|
159
|
+
flow3D_smooth: [1, 0, 0] # z, y, x -- smooth z only
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This is a genuine trade-off, not a strictly better setting, and it is
|
|
163
|
+
worth testing both on your own data rather than taking either on faith.
|
|
164
|
+
Reach for it especially if 3-D results look ringed or fragmented along
|
|
165
|
+
z: that is the symptom this addresses. `flow3D_smooth` accepts a scalar
|
|
166
|
+
on older Cellpose and a `[z, y, x]` list from the version that merged
|
|
167
|
+
that PR onward.
|
|
168
|
+
|
|
124
169
|
!!! tip "Tile size vs runtime"
|
|
125
170
|
`tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
|
|
126
171
|
more (faster) jobs; very large 3-D tiles are slow. Keep `do_3D: false` (2-D
|
|
@@ -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
|
|
|
@@ -477,7 +485,12 @@ def auto_tile_shape_cellpose(
|
|
|
477
485
|
chunk_spatial = [1, min(y, tile_side), min(x, tile_side)]
|
|
478
486
|
else:
|
|
479
487
|
z, y, x = shape[-3], shape[-2], shape[-1]
|
|
480
|
-
|
|
488
|
+
# Cellpose resizes z by `anisotropy` before the net runs, so the tile
|
|
489
|
+
# it actually holds is that much deeper than the one handed to it.
|
|
490
|
+
effective_z = z * max(1.0, anisotropy or 1.0)
|
|
491
|
+
max_pixels_per_slice = max(
|
|
492
|
+
1, int((max_raw_bytes // 3) // (effective_z * itemsize))
|
|
493
|
+
)
|
|
481
494
|
tile_side = max(min_tile, int(max_pixels_per_slice**0.5))
|
|
482
495
|
chunk_spatial = [z, min(y, tile_side), min(x, tile_side)]
|
|
483
496
|
|
|
@@ -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(
|
|
@@ -117,3 +117,32 @@ def test_gpu_tile_sizing_is_bounded_by_the_host_allocation(monkeypatch):
|
|
|
117
117
|
"the 1 GiB host grant must shrink the tile below what the same "
|
|
118
118
|
"24 GiB GPU would otherwise allow"
|
|
119
119
|
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_auto_tile_shape_cellpose_budgets_for_the_anisotropy_resize():
|
|
123
|
+
"""Cellpose resizes a do_3D tile to z * anisotropy planes before the net
|
|
124
|
+
|
|
125
|
+
runs, so budgeting against the unscaled z hands the GPU a tile that is
|
|
126
|
+
`anisotropy` times bigger than the estimate. Auto-deriving anisotropy
|
|
127
|
+
turned that from dormant into live, so the sizer has to know about it.
|
|
128
|
+
"""
|
|
129
|
+
from patchworks import auto_tile_shape_cellpose
|
|
130
|
+
|
|
131
|
+
kwargs = dict(
|
|
132
|
+
shape=(64, 4096, 4096),
|
|
133
|
+
dtype="uint16",
|
|
134
|
+
do_3D=True,
|
|
135
|
+
use_gpu=True,
|
|
136
|
+
gpu_memory=8 * 1024**3,
|
|
137
|
+
available_memory=64 * 1024**3,
|
|
138
|
+
)
|
|
139
|
+
isotropic = auto_tile_shape_cellpose(**kwargs)
|
|
140
|
+
anisotropic = auto_tile_shape_cellpose(**kwargs, anisotropy=2.215)
|
|
141
|
+
|
|
142
|
+
# z is pinned to the full extent either way; the cost is paid in y/x.
|
|
143
|
+
assert anisotropic[0] == isotropic[0]
|
|
144
|
+
assert anisotropic[1] < isotropic[1]
|
|
145
|
+
assert anisotropic[2] < isotropic[2]
|
|
146
|
+
# An anisotropy at or below 1 cannot *grow* the budget.
|
|
147
|
+
assert auto_tile_shape_cellpose(**kwargs, anisotropy=1.0) == isotropic
|
|
148
|
+
assert auto_tile_shape_cellpose(**kwargs, anisotropy=0.5) == isotropic
|
|
@@ -47,3 +47,27 @@ def test_cellpose_fn_declares_a_voxel_size_parameter():
|
|
|
47
47
|
# model.eval() argument, not a patchworks-specific one) -- only the raw
|
|
48
48
|
# calibration is a named parameter.
|
|
49
49
|
assert "anisotropy" not in params
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_available_models_is_a_list():
|
|
53
|
+
"""Empty when Cellpose isn't installed -- then no name can be rejected."""
|
|
54
|
+
from patchworks.plugins.cellpose import available_models
|
|
55
|
+
|
|
56
|
+
assert isinstance(available_models(), list)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_v4_gets_the_model_name_as_pretrained_model():
|
|
60
|
+
"""v4 accepts `model_type=` and then ignores it ("not used in v4.0.1+"),
|
|
61
|
+
|
|
62
|
+
leaving pretrained_model at its default -- so passing the configured name
|
|
63
|
+
there segmented every config with the same default model whatever
|
|
64
|
+
`model:` said. The name has to reach `pretrained_model=` on v4.
|
|
65
|
+
"""
|
|
66
|
+
import inspect
|
|
67
|
+
|
|
68
|
+
from patchworks.plugins import cellpose as cp
|
|
69
|
+
|
|
70
|
+
src = inspect.getsource(cp._get_model)
|
|
71
|
+
v4_branch = src.split("if _CELLPOSE_V4:")[1].split("else:")[0]
|
|
72
|
+
assert "pretrained_model=model_type" in v4_branch
|
|
73
|
+
assert "model_type=model_type" not in v4_branch
|
|
@@ -121,3 +121,44 @@ def test_validate_config_rejects_max_volume_at_or_below_min_volume():
|
|
|
121
121
|
validate_config(
|
|
122
122
|
{"method": "threshold", "min_volume": 500.0, "max_volume": 5.0}
|
|
123
123
|
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_validate_config_rejects_a_model_the_install_does_not_have(
|
|
127
|
+
monkeypatch,
|
|
128
|
+
):
|
|
129
|
+
"""Cellpose does not raise on an unknown model name -- it logs and loads
|
|
130
|
+
|
|
131
|
+
its default. A v3 name against a v4 install therefore segments every
|
|
132
|
+
tile with a model nobody chose, silently. That has to fail in prepare.
|
|
133
|
+
"""
|
|
134
|
+
import pytest
|
|
135
|
+
from _pw import validate_config
|
|
136
|
+
|
|
137
|
+
import patchworks.plugins.cellpose as cp
|
|
138
|
+
|
|
139
|
+
monkeypatch.setattr(cp, "available_models", lambda: ["cpsam", "cpsam_v2"])
|
|
140
|
+
with pytest.raises(ValueError, match="cyto3"):
|
|
141
|
+
validate_config({"method": "cellpose", "cellpose": {"model": "cyto3"}})
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_validate_config_accepts_a_model_the_install_has(monkeypatch):
|
|
145
|
+
from _pw import validate_config
|
|
146
|
+
|
|
147
|
+
import patchworks.plugins.cellpose as cp
|
|
148
|
+
|
|
149
|
+
monkeypatch.setattr(cp, "available_models", lambda: ["cpsam", "cpsam_v2"])
|
|
150
|
+
validate_config({"method": "cellpose", "cellpose": {"model": "cpsam"}})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_validate_config_leaves_a_custom_model_path_alone(
|
|
154
|
+
monkeypatch, tmp_path
|
|
155
|
+
):
|
|
156
|
+
"""A path is a custom-trained model; no name list can vouch for it."""
|
|
157
|
+
from _pw import validate_config
|
|
158
|
+
|
|
159
|
+
import patchworks.plugins.cellpose as cp
|
|
160
|
+
|
|
161
|
+
monkeypatch.setattr(cp, "available_models", lambda: ["cpsam"])
|
|
162
|
+
custom = tmp_path / "my_model.pth"
|
|
163
|
+
custom.write_text("")
|
|
164
|
+
validate_config({"method": "cellpose", "cellpose": {"model": str(custom)}})
|
|
@@ -322,29 +322,61 @@ def validate_config(cfg) -> None:
|
|
|
322
322
|
|
|
323
323
|
def _cellpose_problems(cp: dict) -> list[str]:
|
|
324
324
|
"""Unknown keys in the ``cellpose:`` block, checked against model.eval."""
|
|
325
|
+
problems = _cellpose_model_problems(cp)
|
|
325
326
|
known = ("model", "diameter", "do_3D", "gpu")
|
|
326
327
|
extra = {k: v for k, v in cp.items() if k not in known}
|
|
327
328
|
if not extra:
|
|
328
|
-
return
|
|
329
|
+
return problems
|
|
329
330
|
try:
|
|
330
331
|
from patchworks.plugins.cellpose import _get_model
|
|
331
332
|
except ImportError:
|
|
332
|
-
return
|
|
333
|
+
return problems
|
|
333
334
|
try:
|
|
334
335
|
model = _get_model({"model": cp.get("model", "cyto3"), "gpu": False})
|
|
335
336
|
except Exception:
|
|
336
337
|
# Weights unavailable here (fetch_model runs separately); skip rather
|
|
337
338
|
# than fail a config that may be perfectly fine.
|
|
338
|
-
return
|
|
339
|
+
return problems
|
|
339
340
|
unknown = _unknown_kwargs(
|
|
340
341
|
model.eval, extra, skip=("channels", "channel_axis")
|
|
341
342
|
)
|
|
342
343
|
if unknown:
|
|
343
|
-
|
|
344
|
+
problems.append(
|
|
344
345
|
f"unknown cellpose: key(s) {unknown} -- these are forwarded to "
|
|
345
346
|
"model.eval(), which does not accept them"
|
|
346
|
-
|
|
347
|
-
return
|
|
347
|
+
)
|
|
348
|
+
return problems
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _cellpose_model_problems(cp: dict) -> list[str]:
|
|
352
|
+
"""Reject a model name the installed Cellpose would silently substitute.
|
|
353
|
+
|
|
354
|
+
Cellpose 4 renamed the pretrained models outright, and does not raise on
|
|
355
|
+
an unknown name -- it logs and loads its default instead. A v3 name
|
|
356
|
+
(``cyto3``) left in a config against a v4 install therefore segments
|
|
357
|
+
every tile with the wrong model, silently. A path is left alone: that is
|
|
358
|
+
a custom-trained model, which no name list can vouch for.
|
|
359
|
+
"""
|
|
360
|
+
name = cp.get("model", "cyto3")
|
|
361
|
+
if not isinstance(name, str) or Path(name).exists():
|
|
362
|
+
return []
|
|
363
|
+
try:
|
|
364
|
+
from patchworks.plugins.cellpose import available_models
|
|
365
|
+
except ImportError:
|
|
366
|
+
return []
|
|
367
|
+
names = available_models()
|
|
368
|
+
if not names or name in names:
|
|
369
|
+
return []
|
|
370
|
+
listed = ", ".join(f'"{n}"' for n in names)
|
|
371
|
+
return [
|
|
372
|
+
f"cellpose.model {name!r} is not available in the installed Cellpose "
|
|
373
|
+
f"(it offers {listed}). Cellpose would fall back to its default "
|
|
374
|
+
"model and log a warning rather than fail, so every tile would be "
|
|
375
|
+
"segmented with a model you did not choose. Pick one of the above, "
|
|
376
|
+
"give a path to a custom-trained model, or install the Cellpose "
|
|
377
|
+
'major version that has it (the workflow ships "cellpose3" and '
|
|
378
|
+
'"cellpose4" pixi environments).'
|
|
379
|
+
]
|
|
348
380
|
|
|
349
381
|
|
|
350
382
|
def _custom_problems(spec) -> list[str]:
|
|
@@ -50,6 +50,17 @@ if ts == "auto":
|
|
|
50
50
|
n_channels = 2 if cfg.get("nuclei_channel") is not None else 1
|
|
51
51
|
if method == "cellpose":
|
|
52
52
|
cp = cfg["cellpose"]
|
|
53
|
+
# The same anisotropy segment will derive (or was given). Cellpose
|
|
54
|
+
# resizes the tile to z * anisotropy planes, so the sizer has to
|
|
55
|
+
# budget for the resized tile, not the one it hands over.
|
|
56
|
+
anisotropy = cp.get("anisotropy")
|
|
57
|
+
if anisotropy is None and cp.get("do_3D", False):
|
|
58
|
+
from patchworks.plugins.cellpose import cellpose_anisotropy
|
|
59
|
+
from patchworks.plugins.ome_zarr import read_pixel_size
|
|
60
|
+
|
|
61
|
+
anisotropy = cellpose_anisotropy(
|
|
62
|
+
read_pixel_size(str(Path(work_dir) / "image.zarr"))
|
|
63
|
+
)
|
|
53
64
|
sizer = partial(
|
|
54
65
|
auto_tile_shape_cellpose,
|
|
55
66
|
do_3D=cp.get("do_3D", False),
|
|
@@ -57,6 +68,7 @@ if ts == "auto":
|
|
|
57
68
|
diameter=cp.get("diameter"),
|
|
58
69
|
gpu_memory=gpu_bytes,
|
|
59
70
|
n_channels=n_channels,
|
|
71
|
+
anisotropy=anisotropy,
|
|
60
72
|
)
|
|
61
73
|
else:
|
|
62
74
|
# cfg["cellpose"] used to be read unconditionally here, so a DoG or
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|