patchworks 1.3.3__tar.gz → 1.4.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-1.3.3 → patchworks-1.4.0}/PKG-INFO +14 -1
- {patchworks-1.3.3 → patchworks-1.4.0}/README.md +13 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/dog.md +3 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/getting_started.md +13 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/custom_segmentation.md +7 -2
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/ome_zarr.py +149 -8
- {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_ome_zarr.py +29 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config.yaml +7 -1
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/convert.py +1 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/.github/workflows/docs.yml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/.github/workflows/lint.yml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/.github/workflows/release.yml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/.gitignore +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/LICENSE +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/cliff.toml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/chunks.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/cluster.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/io.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/dog.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/postprocess.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/relabel.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/tile_process.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/assets/logo.png +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/custom.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/custom_method.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/dog.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/stardist.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/label_relations.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/measurements.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/merging.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/performance.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/snakemake.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/tiling.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/docs/index.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/mkdocs.yml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/pyproject.toml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/__init__.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_chunks.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_core.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_distributed.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_io.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_merge.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_postprocess.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_relations.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_core.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_distributed.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_dog.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_napari.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_postprocess.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_relations.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/README.md +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/Snakefile +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/multi.yaml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/pixi.toml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/common.smk +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/convert.smk +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/merge.smk +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/segment.smk +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/_pw.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/merge.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/run_multi.py +0 -0
- {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -130,6 +130,7 @@ pip install "patchworks[gpu]" # GPU VRAM querying (nvidia-ml-py)
|
|
|
130
130
|
pip install "patchworks[cellpose]" # Cellpose plugin (>=3.0, v3 or v4)
|
|
131
131
|
pip install "patchworks[cellpose3]" # Cellpose plugin, pinned to v3.x
|
|
132
132
|
pip install "patchworks[cellpose4]" # Cellpose plugin, pinned to v4+
|
|
133
|
+
pip install "patchworks[dog]" # deconvolution + DoG plugin (pycudadecon)
|
|
133
134
|
pip install "patchworks[bioio]" # convert any image format to OME-ZARR
|
|
134
135
|
pip install "patchworks[imaris]" # convert Imaris .ims files to OME-ZARR
|
|
135
136
|
pip install "patchworks[napari]" # interactive napari viewer plugin
|
|
@@ -142,6 +143,14 @@ pip install "patchworks[all]" # Everything, incl. the napari viewer
|
|
|
142
143
|
> `[imaris]` adds native `.ims` support (HDF5, no JVM). Physical pixel
|
|
143
144
|
> calibration is read from the input and written into the OME-ZARR.
|
|
144
145
|
|
|
146
|
+
> **`cupy` is never installed automatically**, unlike Cellpose's GPU support
|
|
147
|
+
> (which comes for free via PyTorch's self-contained CUDA wheels). Any
|
|
148
|
+
> `use_gpu=True`/`dilate_gpu: true` option (the `dog` plugin, `dilate_labels`)
|
|
149
|
+
> needs `cupy` installed separately, matching your CUDA version — e.g.
|
|
150
|
+
> `pip install cupy-cuda12x`. Not bundled because cupy ships one wheel per
|
|
151
|
+
> CUDA major version; a generic pin would resolve to the wrong build (or fail
|
|
152
|
+
> to resolve) depending on the machine.
|
|
153
|
+
|
|
145
154
|
---
|
|
146
155
|
|
|
147
156
|
## Quick start — 5 lines
|
|
@@ -397,10 +406,14 @@ Optional:
|
|
|
397
406
|
- `tqdm` — progress bars
|
|
398
407
|
- `cellpose` — Cellpose plugin, v3 or v4 (`patchworks[cellpose]`);
|
|
399
408
|
pin with `[cellpose3]` or `[cellpose4]`
|
|
409
|
+
- `pycudadecon` — deconvolution step of the `dog` plugin (`patchworks[dog]`)
|
|
400
410
|
- `bioio` + readers — convert CZI/LIF/ND2/OME-TIFF/… to OME-ZARR
|
|
401
411
|
(`patchworks[bioio]`)
|
|
402
412
|
- `imaris-ims-file-reader` — convert Imaris `.ims` (`patchworks[imaris]`)
|
|
403
413
|
- `napari` — interactive viewer plugin (`patchworks[napari]`)
|
|
414
|
+
- `cupy` — **install manually**, matching your CUDA version (e.g.
|
|
415
|
+
`pip install cupy-cuda12x`); not offered as an extra since it isn't one
|
|
416
|
+
generic pin. Needed for `use_gpu=True`/`dilate_gpu: true`.
|
|
404
417
|
|
|
405
418
|
---
|
|
406
419
|
|
|
@@ -40,6 +40,7 @@ pip install "patchworks[gpu]" # GPU VRAM querying (nvidia-ml-py)
|
|
|
40
40
|
pip install "patchworks[cellpose]" # Cellpose plugin (>=3.0, v3 or v4)
|
|
41
41
|
pip install "patchworks[cellpose3]" # Cellpose plugin, pinned to v3.x
|
|
42
42
|
pip install "patchworks[cellpose4]" # Cellpose plugin, pinned to v4+
|
|
43
|
+
pip install "patchworks[dog]" # deconvolution + DoG plugin (pycudadecon)
|
|
43
44
|
pip install "patchworks[bioio]" # convert any image format to OME-ZARR
|
|
44
45
|
pip install "patchworks[imaris]" # convert Imaris .ims files to OME-ZARR
|
|
45
46
|
pip install "patchworks[napari]" # interactive napari viewer plugin
|
|
@@ -52,6 +53,14 @@ pip install "patchworks[all]" # Everything, incl. the napari viewer
|
|
|
52
53
|
> `[imaris]` adds native `.ims` support (HDF5, no JVM). Physical pixel
|
|
53
54
|
> calibration is read from the input and written into the OME-ZARR.
|
|
54
55
|
|
|
56
|
+
> **`cupy` is never installed automatically**, unlike Cellpose's GPU support
|
|
57
|
+
> (which comes for free via PyTorch's self-contained CUDA wheels). Any
|
|
58
|
+
> `use_gpu=True`/`dilate_gpu: true` option (the `dog` plugin, `dilate_labels`)
|
|
59
|
+
> needs `cupy` installed separately, matching your CUDA version — e.g.
|
|
60
|
+
> `pip install cupy-cuda12x`. Not bundled because cupy ships one wheel per
|
|
61
|
+
> CUDA major version; a generic pin would resolve to the wrong build (or fail
|
|
62
|
+
> to resolve) depending on the machine.
|
|
63
|
+
|
|
55
64
|
---
|
|
56
65
|
|
|
57
66
|
## Quick start — 5 lines
|
|
@@ -307,10 +316,14 @@ Optional:
|
|
|
307
316
|
- `tqdm` — progress bars
|
|
308
317
|
- `cellpose` — Cellpose plugin, v3 or v4 (`patchworks[cellpose]`);
|
|
309
318
|
pin with `[cellpose3]` or `[cellpose4]`
|
|
319
|
+
- `pycudadecon` — deconvolution step of the `dog` plugin (`patchworks[dog]`)
|
|
310
320
|
- `bioio` + readers — convert CZI/LIF/ND2/OME-TIFF/… to OME-ZARR
|
|
311
321
|
(`patchworks[bioio]`)
|
|
312
322
|
- `imaris-ims-file-reader` — convert Imaris `.ims` (`patchworks[imaris]`)
|
|
313
323
|
- `napari` — interactive viewer plugin (`patchworks[napari]`)
|
|
324
|
+
- `cupy` — **install manually**, matching your CUDA version (e.g.
|
|
325
|
+
`pip install cupy-cuda12x`); not offered as an extra since it isn't one
|
|
326
|
+
generic pin. Needed for `use_gpu=True`/`dilate_gpu: true`.
|
|
314
327
|
|
|
315
328
|
---
|
|
316
329
|
|
|
@@ -6,6 +6,9 @@ label the connected components. CPU (scipy) by default, GPU (cupy) optional.
|
|
|
6
6
|
Optionally deconvolve each tile first with
|
|
7
7
|
[pycudadecon](https://github.com/tlambert03/pycudadecon).
|
|
8
8
|
|
|
9
|
+
> Cilia DoG + deconvolution approach courtesy of
|
|
10
|
+
> [angelo-angonezi](https://github.com/angelo-angonezi).
|
|
11
|
+
|
|
9
12
|
## Installation
|
|
10
13
|
|
|
11
14
|
`dog_label_fn` itself only needs patchworks' core deps (scipy). The
|
|
@@ -36,12 +36,25 @@ patchworks can be installed from PyPI on all operating systems, for Python ≥ 3
|
|
|
36
36
|
pip install "patchworks[cellpose]"
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
=== "With deconvolution + DoG plugin"
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install "patchworks[dog]"
|
|
43
|
+
```
|
|
44
|
+
|
|
39
45
|
=== "Everything"
|
|
40
46
|
|
|
41
47
|
```bash
|
|
42
48
|
pip install "patchworks[all]"
|
|
43
49
|
```
|
|
44
50
|
|
|
51
|
+
!!! note "cupy is always a manual install"
|
|
52
|
+
`use_gpu=True` (the `dog` plugin, `dilate_labels`) needs `cupy`, but it's
|
|
53
|
+
never pulled in automatically — unlike Cellpose, which gets GPU support
|
|
54
|
+
for free via PyTorch's self-contained CUDA wheels, `cupy` ships one wheel
|
|
55
|
+
per CUDA major version. Install the one matching your CUDA version
|
|
56
|
+
yourself, e.g. `pip install cupy-cuda12x`.
|
|
57
|
+
|
|
45
58
|
---
|
|
46
59
|
|
|
47
60
|
## The one function you need
|
|
@@ -78,10 +78,15 @@ By default the dilation itself runs on CPU (scipy), independent of whatever
|
|
|
78
78
|
backend `fn` used — pass `use_gpu=True` to dilate via cupy instead:
|
|
79
79
|
|
|
80
80
|
```python
|
|
81
|
-
fn = dilate_labels(fn, iterations=2, use_gpu=True)
|
|
81
|
+
fn = dilate_labels(fn, iterations=2, use_gpu=True)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Needs `cupy` installed **manually**, matching your CUDA version (e.g.
|
|
85
|
+
`pip install cupy-cuda12x`) — it's never installed automatically by
|
|
86
|
+
patchworks, unlike Cellpose's GPU support (which comes for free via
|
|
87
|
+
PyTorch's self-contained CUDA wheels); cupy ships one wheel per CUDA major
|
|
88
|
+
version, so there's no single generic pin that works everywhere. On the
|
|
89
|
+
cluster, this is the `dilate_gpu: true` config key.
|
|
85
90
|
|
|
86
91
|
## Real example: StarDist 3-D, with model caching
|
|
87
92
|
|
|
@@ -9,14 +9,18 @@ Input handling, in order:
|
|
|
9
9
|
* a ``.zarr`` path → read through :func:`patchworks.load_ome_zarr`;
|
|
10
10
|
* a ``.ims`` (Imaris) path → read lazily with ``imaris-ims-file-reader``
|
|
11
11
|
(HDF5, no JVM); install with ``pip install "patchworks[imaris]"``;
|
|
12
|
+
* with ``sequence_pattern=`` set, a glob over a folder of single-plane TIFFs
|
|
13
|
+
→ each file becomes one chunk of a lazy dask array via
|
|
14
|
+
:func:`tifffile.TiffSequence`, no data duplicated;
|
|
12
15
|
* any other path (CZI, LIF, ND2, OME-TIFF, …) → opened lazily with
|
|
13
16
|
`bioio <https://github.com/bioio-devs/bioio>`_.
|
|
14
17
|
|
|
15
18
|
Pixel calibration (physical voxel size) is read from the input — bioio's
|
|
16
|
-
``physical_pixel_sizes``, the Imaris resolution metadata,
|
|
17
|
-
OME-ZARR's scale transform
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
``physical_pixel_sizes``, the Imaris resolution metadata, an existing
|
|
20
|
+
OME-ZARR's scale transform, or (for a TIFF sequence) the first file's own
|
|
21
|
+
ImageJ/resolution tags — and written into the NGFF ``coordinateTransformations``
|
|
22
|
+
so the µm/pixel sizing is preserved. Pass ``pixel_size=`` to override or to
|
|
23
|
+
supply it for bare arrays.
|
|
20
24
|
|
|
21
25
|
Downsampling uses strided (nearest-neighbour) subsampling — the correct,
|
|
22
26
|
label-preserving choice — and only on **X and Y**; ``z`` (and channel/time)
|
|
@@ -620,6 +624,116 @@ def _open_bioio(path: str, scene: int) -> tuple[da.Array, str, PixelSize]:
|
|
|
620
624
|
return arr, axes, pixel_size
|
|
621
625
|
|
|
622
626
|
|
|
627
|
+
_UNIT_TO_UM = {
|
|
628
|
+
"micron": 1.0,
|
|
629
|
+
"um": 1.0,
|
|
630
|
+
"µm": 1.0,
|
|
631
|
+
"mm": 1000.0,
|
|
632
|
+
"cm": 10000.0,
|
|
633
|
+
"centimeter": 10000.0,
|
|
634
|
+
"inch": 25400.0,
|
|
635
|
+
}
|
|
636
|
+
# TIFF ResolutionUnit tag values (RESUNIT): 2 == INCH, 3 == CENTIMETER.
|
|
637
|
+
_RESUNIT_TO_UM = {2: 25400.0, 3: 10000.0}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _tiff_pixel_size(path: str) -> PixelSize:
|
|
641
|
+
"""Read physical voxel size from a TIFF file's own metadata.
|
|
642
|
+
|
|
643
|
+
Z comes from ImageJ metadata (``spacing`` + ``unit``), if present. X/Y
|
|
644
|
+
come from the page's ``XResolution``/``YResolution`` tags (pixels per
|
|
645
|
+
unit); the unit itself is taken from the ``ResolutionUnit`` tag when set
|
|
646
|
+
(plain TIFFs), or falls back to ImageJ metadata's ``unit`` — ImageJ
|
|
647
|
+
itself writes ``ResolutionUnit=NONE`` and keeps the unit as a string in
|
|
648
|
+
its metadata instead. Unrecognized units are ignored (treated as
|
|
649
|
+
uncalibrated).
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
path : str
|
|
654
|
+
Path of one TIFF file in the sequence.
|
|
655
|
+
|
|
656
|
+
Returns
|
|
657
|
+
-------
|
|
658
|
+
dict
|
|
659
|
+
``{axis: micrometers}`` for whichever axes could be determined
|
|
660
|
+
(empty if none).
|
|
661
|
+
"""
|
|
662
|
+
import tifffile
|
|
663
|
+
|
|
664
|
+
pixel_size: PixelSize = {}
|
|
665
|
+
with tifffile.TiffFile(path) as tif:
|
|
666
|
+
ij = tif.imagej_metadata or {}
|
|
667
|
+
spacing = ij.get("spacing")
|
|
668
|
+
if spacing:
|
|
669
|
+
factor = _UNIT_TO_UM.get(str(ij.get("unit", "micron")).lower(), 1.0)
|
|
670
|
+
pixel_size["z"] = float(spacing) * factor
|
|
671
|
+
|
|
672
|
+
page = tif.pages[0]
|
|
673
|
+
unit_tag = page.tags.get("ResolutionUnit")
|
|
674
|
+
um_per_unit = _RESUNIT_TO_UM.get(
|
|
675
|
+
unit_tag.value if unit_tag else None
|
|
676
|
+
) or _UNIT_TO_UM.get(str(ij.get("unit", "")).lower())
|
|
677
|
+
if um_per_unit is not None:
|
|
678
|
+
for axis, tag_name in (("y", "YResolution"), ("x", "XResolution")):
|
|
679
|
+
tag = page.tags.get(tag_name)
|
|
680
|
+
if tag and tag.value[0]:
|
|
681
|
+
num, den = tag.value
|
|
682
|
+
pixel_size[axis] = um_per_unit / (num / den)
|
|
683
|
+
return pixel_size
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _open_tiff_sequence(
|
|
687
|
+
pattern: str, sequence_pattern: str
|
|
688
|
+
) -> tuple[da.Array, str, PixelSize]:
|
|
689
|
+
"""Open a folder of single-plane TIFFs as one lazy dask array.
|
|
690
|
+
|
|
691
|
+
Each file becomes exactly one chunk, decoded on access — no data is
|
|
692
|
+
duplicated or eagerly loaded. See
|
|
693
|
+
:func:`tifffile.TiffSequence`/``ZarrFileSequenceStore`` (the same
|
|
694
|
+
mechanism used by Cellpose's own distributed pipeline,
|
|
695
|
+
``cellpose.contrib.distributed_segmentation.wrap_folder_of_tiffs``).
|
|
696
|
+
|
|
697
|
+
Parameters
|
|
698
|
+
----------
|
|
699
|
+
pattern : str
|
|
700
|
+
Glob pattern matching all the files, e.g. ``"folder/*.tif"``.
|
|
701
|
+
sequence_pattern : str
|
|
702
|
+
Regular expression parsing each file name into axis labels and
|
|
703
|
+
indices, e.g. ``r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+"``.
|
|
704
|
+
Named groups become axis labels directly; axis order follows the
|
|
705
|
+
order groups appear in the pattern.
|
|
706
|
+
|
|
707
|
+
Returns
|
|
708
|
+
-------
|
|
709
|
+
tuple
|
|
710
|
+
``(array, axes, pixel_size)`` — a lazy dask array, its axes string
|
|
711
|
+
(parsed axes + trailing ``"yx"``) and a ``{axis: micrometers}``
|
|
712
|
+
calibration dict read from the first file.
|
|
713
|
+
"""
|
|
714
|
+
try:
|
|
715
|
+
import tifffile
|
|
716
|
+
except ImportError as exc:
|
|
717
|
+
raise ImportError(
|
|
718
|
+
"reading a folder of TIFFs requires tifffile. Install it with:\n"
|
|
719
|
+
" pip install tifffile\n"
|
|
720
|
+
"(already pulled in by patchworks[bioio] via bioio-tifffile)."
|
|
721
|
+
) from exc
|
|
722
|
+
|
|
723
|
+
ts = tifffile.TiffSequence(pattern, pattern=sequence_pattern)
|
|
724
|
+
arr = da.from_zarr(zarr.open(store=ts.aszarr()))
|
|
725
|
+
axes = ts.axes.lower() + "yx"
|
|
726
|
+
pixel_size = _tiff_pixel_size(ts[0])
|
|
727
|
+
logger.info(
|
|
728
|
+
"tifffile sequence opened %s files as %s %s cal=%s",
|
|
729
|
+
len(ts),
|
|
730
|
+
axes,
|
|
731
|
+
arr.shape,
|
|
732
|
+
pixel_size,
|
|
733
|
+
)
|
|
734
|
+
return arr, axes, pixel_size
|
|
735
|
+
|
|
736
|
+
|
|
623
737
|
def _open_imaris(path: str, level: int = 0) -> tuple[da.Array, str, PixelSize]:
|
|
624
738
|
"""Open one Imaris ``.ims`` resolution level lazily.
|
|
625
739
|
|
|
@@ -769,12 +883,14 @@ def _to_dask(
|
|
|
769
883
|
source: Union[da.Array, np.ndarray, str, Path],
|
|
770
884
|
axes: Union[str, None],
|
|
771
885
|
scene: int,
|
|
886
|
+
sequence_pattern: Union[str, None] = None,
|
|
772
887
|
) -> tuple[da.Array, str, PixelSize]:
|
|
773
888
|
"""Resolve *source* into a lazy ``(array, axes, pixel_size)`` triple.
|
|
774
889
|
|
|
775
890
|
Dispatches by type: dask/NumPy arrays pass through; ``.zarr`` paths use the
|
|
776
|
-
OME-ZARR loader; ``.ims`` paths use the Imaris reader;
|
|
777
|
-
|
|
891
|
+
OME-ZARR loader; ``.ims`` paths use the Imaris reader; a *sequence_pattern*
|
|
892
|
+
treats *source* as a glob over a folder of single-plane TIFFs; anything
|
|
893
|
+
else uses bioio.
|
|
778
894
|
|
|
779
895
|
Parameters
|
|
780
896
|
----------
|
|
@@ -784,6 +900,10 @@ def _to_dask(
|
|
|
784
900
|
Explicit axes, or ``None`` to infer them.
|
|
785
901
|
scene : int
|
|
786
902
|
Scene index for bioio inputs.
|
|
903
|
+
sequence_pattern : str or None
|
|
904
|
+
When given, *source* is a glob pattern over a folder of single-plane
|
|
905
|
+
TIFFs and this is the filename-parsing regex; see
|
|
906
|
+
:func:`_open_tiff_sequence`.
|
|
787
907
|
|
|
788
908
|
Returns
|
|
789
909
|
-------
|
|
@@ -796,6 +916,9 @@ def _to_dask(
|
|
|
796
916
|
return da.asarray(source), axes or _default_axes(source.ndim), {}
|
|
797
917
|
|
|
798
918
|
path = str(source)
|
|
919
|
+
if sequence_pattern is not None:
|
|
920
|
+
arr, detected, ps = _open_tiff_sequence(path, sequence_pattern)
|
|
921
|
+
return arr, axes or detected, ps
|
|
799
922
|
if path.endswith(".zarr"):
|
|
800
923
|
arr = load_ome_zarr(source, channel=None)
|
|
801
924
|
ax = axes or _default_axes(arr.ndim)
|
|
@@ -815,6 +938,7 @@ def to_ome_zarr(
|
|
|
815
938
|
axes: Union[str, None] = None,
|
|
816
939
|
pixel_size: Union[PixelSize, tuple, None] = None,
|
|
817
940
|
scene: int = 0,
|
|
941
|
+
sequence_pattern: Union[str, None] = None,
|
|
818
942
|
n_levels: int = 5,
|
|
819
943
|
downscale: int = 2,
|
|
820
944
|
chunks: Union[tuple[int, ...], None] = None,
|
|
@@ -826,7 +950,8 @@ def to_ome_zarr(
|
|
|
826
950
|
"""Write *source* as a pyramidal, calibrated OME-ZARR store.
|
|
827
951
|
|
|
828
952
|
*source* may be a dask/NumPy array, a ``.zarr`` store, an Imaris ``.ims``
|
|
829
|
-
file,
|
|
953
|
+
file, any image format readable by bioio (CZI, LIF, ND2, OME-TIFF, …), or
|
|
954
|
+
(with *sequence_pattern* set) a glob over a folder of single-plane TIFFs.
|
|
830
955
|
File inputs are read lazily; the pyramid is built level-by-level from disk
|
|
831
956
|
with bounded chunks, so the full volume never needs to fit in RAM. Only
|
|
832
957
|
``x``/``y`` are downsampled; ``z`` (and channel/time) stay full-resolution.
|
|
@@ -847,6 +972,15 @@ def to_ome_zarr(
|
|
|
847
972
|
arrays.
|
|
848
973
|
scene : int, optional
|
|
849
974
|
Scene index for multi-scene bioio files.
|
|
975
|
+
sequence_pattern : str, optional
|
|
976
|
+
When given, *source* is treated as a glob pattern over a folder of
|
|
977
|
+
single-plane TIFFs (e.g. ``"folder/*.tif"``) instead of a single
|
|
978
|
+
file, and this is the regex parsing each file name into axis labels
|
|
979
|
+
and indices via named groups, e.g.
|
|
980
|
+
``r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+"``. Each file
|
|
981
|
+
becomes exactly one chunk, read lazily on access (no data
|
|
982
|
+
duplicated) — see :func:`tifffile.TiffSequence`. Axis order follows
|
|
983
|
+
the order named groups appear in the pattern.
|
|
850
984
|
n_levels : int, optional
|
|
851
985
|
Maximum number of pyramid levels including full resolution.
|
|
852
986
|
downscale : int, optional
|
|
@@ -881,6 +1015,13 @@ def to_ome_zarr(
|
|
|
881
1015
|
>>> from patchworks.plugins.ome_zarr import to_ome_zarr
|
|
882
1016
|
>>> to_ome_zarr("scan.ims", "scan.zarr", n_levels=4)
|
|
883
1017
|
'scan.zarr'
|
|
1018
|
+
>>> to_ome_zarr(
|
|
1019
|
+
... "ZT18_Male4_Left/*.tif",
|
|
1020
|
+
... "ZT18_Male4_Left.zarr",
|
|
1021
|
+
... sequence_pattern=r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+",
|
|
1022
|
+
... shard=True,
|
|
1023
|
+
... ) # doctest: +SKIP
|
|
1024
|
+
'ZT18_Male4_Left.zarr'
|
|
884
1025
|
"""
|
|
885
1026
|
if downscale < 2:
|
|
886
1027
|
raise ValueError("downscale must be >= 2")
|
|
@@ -908,7 +1049,7 @@ def to_ome_zarr(
|
|
|
908
1049
|
exc,
|
|
909
1050
|
)
|
|
910
1051
|
|
|
911
|
-
arr, axes, detected = _to_dask(source, axes, scene)
|
|
1052
|
+
arr, axes, detected = _to_dask(source, axes, scene, sequence_pattern)
|
|
912
1053
|
if len(axes) != arr.ndim:
|
|
913
1054
|
raise ValueError(
|
|
914
1055
|
f"axes {axes!r} has {len(axes)} entries but array is {arr.ndim}-D"
|
|
@@ -79,6 +79,35 @@ def test_non_spatial_axis_not_downsampled(tmp_path):
|
|
|
79
79
|
assert load_ome_zarr(out, channel=None, level=1).shape == (3, 8, 8)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def test_tiff_sequence_conversion(tmp_path):
|
|
83
|
+
"""A folder of single-plane TIFFs is wrapped lazily and converted."""
|
|
84
|
+
tifffile = pytest.importorskip("tifffile")
|
|
85
|
+
n_z, n_c, size = 3, 2, 8
|
|
86
|
+
for z in range(n_z):
|
|
87
|
+
for c in range(n_c):
|
|
88
|
+
img = np.full((size, size), z * 10 + c, dtype="uint16")
|
|
89
|
+
tifffile.imwrite(
|
|
90
|
+
tmp_path / f"sample_Z{z:03d}_C{c}_V0.tif",
|
|
91
|
+
img,
|
|
92
|
+
resolution=(20000.0, 20000.0),
|
|
93
|
+
resolutionunit="CENTIMETER",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
out = tmp_path / "out.zarr"
|
|
97
|
+
to_ome_zarr(
|
|
98
|
+
str(tmp_path / "*.tif"),
|
|
99
|
+
out,
|
|
100
|
+
sequence_pattern=r"_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+",
|
|
101
|
+
n_levels=1,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result = np.asarray(load_ome_zarr(out, channel=None))
|
|
105
|
+
assert result.shape == (n_z, n_c, size, size)
|
|
106
|
+
# each plane's constant value encodes its (z, c) position.
|
|
107
|
+
assert (result[:, :, 0, 0] == [[z * 10 + c for c in range(n_c)] for z in range(n_z)]).all()
|
|
108
|
+
assert _level_scale(out, 0) == pytest.approx([1.0, 1.0, 0.5, 0.5])
|
|
109
|
+
|
|
110
|
+
|
|
82
111
|
def test_axes_length_mismatch(tmp_path):
|
|
83
112
|
with pytest.raises(ValueError):
|
|
84
113
|
to_ome_zarr(
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
# GPUs run in parallel; the merge is a single CPU job.
|
|
6
6
|
|
|
7
7
|
# ---- input / output ---------------------------------------------------------
|
|
8
|
-
input: "/path/to/scan.ims" # .ims, .czi, .lif, .nd2, ome-tiff, or
|
|
8
|
+
input: "/path/to/scan.ims" # .ims, .czi, .lif, .nd2, ome-tiff, .zarr, or a
|
|
9
|
+
# glob (e.g. "/path/*.tif") when sequence_pattern
|
|
10
|
+
# is set below
|
|
9
11
|
work_dir: "/path/to/results" # everything is written under here
|
|
10
12
|
# Final outputs (under work_dir):
|
|
11
13
|
# image.zarr — converted, pyramidal OME-ZARR
|
|
@@ -15,6 +17,10 @@ work_dir: "/path/to/results" # everything is written under here
|
|
|
15
17
|
reuse_pyramid: false # for .ims: copy its own pyramid (fast); else rebuild
|
|
16
18
|
convert_chunks: null # null → patchworks' bounded auto chunks; or [c,z,y,x]
|
|
17
19
|
shard: false # true → pack chunks into shards (zarr v3; fewer files)
|
|
20
|
+
# For a folder of single-plane TIFFs (input: a glob), set the filename regex
|
|
21
|
+
# below; named groups become axes, e.g. one file per Z/C plane:
|
|
22
|
+
# sequence_pattern: '_T(?P<T>\d+)_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+'
|
|
23
|
+
sequence_pattern: null
|
|
18
24
|
# Re-running reuses an existing image.zarr automatically (skips conversion).
|
|
19
25
|
# To force a rebuild: delete image.zarr, or `snakemake --forcerun convert`.
|
|
20
26
|
|
|
@@ -16,6 +16,7 @@ chunks = cfg.get("convert_chunks")
|
|
|
16
16
|
to_ome_zarr(
|
|
17
17
|
cfg["input"],
|
|
18
18
|
str(snakemake.output[0]).removesuffix("/zarr.json"), # noqa: F821
|
|
19
|
+
sequence_pattern=cfg.get("sequence_pattern"),
|
|
19
20
|
chunks=tuple(chunks) if chunks else None,
|
|
20
21
|
shard=bool(cfg.get("shard", False)),
|
|
21
22
|
reuse_pyramid=bool(cfg.get("reuse_pyramid", False)),
|
|
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
|