patchworks 1.3.4__tar.gz → 1.4.1__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.4 → patchworks-1.4.1}/PKG-INFO +3 -1
- {patchworks-1.3.4 → patchworks-1.4.1}/README.md +2 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/dog.md +3 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/ome_zarr_napari.md +54 -6
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/plugins/ome_zarr.py +182 -8
- {patchworks-1.3.4 → patchworks-1.4.1}/tests/test_ome_zarr.py +66 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/config/config.yaml +7 -1
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/scripts/_pw.py +3 -1
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/scripts/convert.py +1 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/.github/workflows/docs.yml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/.github/workflows/lint.yml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/.github/workflows/release.yml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/.gitignore +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/.markdownlint-cli2.yaml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/LICENSE +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/cliff.toml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/chunks.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/cluster.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/io.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/plugins/dog.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/plugins/napari.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/postprocess.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/relabel.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/api/tile_process.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/assets/logo.png +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/custom.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/custom_method.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/dog.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/standalone_merge.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/stardist.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/examples/stardist_2d.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/getting_started.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/label_relations.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/measurements.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/merging.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/performance.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/pitfalls.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/skip_empty.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/snakemake.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/guide/tiling.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/docs/index.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/mkdocs.yml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/pyproject.toml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/__init__.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_chunks.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_cluster.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_core.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_distributed.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_io.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_merge.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_postprocess.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_relabel.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/_relations.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/tests/test_core.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/tests/test_distributed.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/tests/test_dog.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/tests/test_napari.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/tests/test_postprocess.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/tests/test_relations.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/README.md +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/Snakefile +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/config/multi.yaml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/pixi.toml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/rules/common.smk +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/rules/convert.smk +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/rules/merge.smk +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/rules/segment.smk +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/scripts/merge.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/workflow/scripts/run_multi.py +0 -0
- {patchworks-1.3.4 → patchworks-1.4.1}/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.1
|
|
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
|
|
@@ -143,6 +143,8 @@ pip install "patchworks[all]" # Everything, incl. the napari viewer
|
|
|
143
143
|
> `[imaris]` adds native `.ims` support (HDF5, no JVM). Physical pixel
|
|
144
144
|
> calibration is read from the input and written into the OME-ZARR.
|
|
145
145
|
|
|
146
|
+
<!-- -->
|
|
147
|
+
|
|
146
148
|
> **`cupy` is never installed automatically**, unlike Cellpose's GPU support
|
|
147
149
|
> (which comes for free via PyTorch's self-contained CUDA wheels). Any
|
|
148
150
|
> `use_gpu=True`/`dilate_gpu: true` option (the `dog` plugin, `dilate_labels`)
|
|
@@ -53,6 +53,8 @@ pip install "patchworks[all]" # Everything, incl. the napari viewer
|
|
|
53
53
|
> `[imaris]` adds native `.ims` support (HDF5, no JVM). Physical pixel
|
|
54
54
|
> calibration is read from the input and written into the OME-ZARR.
|
|
55
55
|
|
|
56
|
+
<!-- -->
|
|
57
|
+
|
|
56
58
|
> **`cupy` is never installed automatically**, unlike Cellpose's GPU support
|
|
57
59
|
> (which comes for free via PyTorch's self-contained CUDA wheels). Any
|
|
58
60
|
> `use_gpu=True`/`dilate_gpu: true` option (the `dog` plugin, `dilate_labels`)
|
|
@@ -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
|
|
@@ -39,9 +39,10 @@ existed.
|
|
|
39
39
|
## Convert any image to OME-ZARR
|
|
40
40
|
|
|
41
41
|
`to_ome_zarr` accepts a dask/NumPy array, an existing `.zarr` store, an
|
|
42
|
-
**Imaris `.ims`** file,
|
|
43
|
-
[bioio](https://github.com/bioio-devs/bioio) (CZI, LIF, ND2, OME-TIFF, …)
|
|
44
|
-
inputs are read
|
|
42
|
+
**Imaris `.ims`** file, **any format** readable by
|
|
43
|
+
[bioio](https://github.com/bioio-devs/bioio) (CZI, LIF, ND2, OME-TIFF, …), or
|
|
44
|
+
a **folder of single-plane TIFFs** (see below). File inputs are read
|
|
45
|
+
**lazily**.
|
|
45
46
|
|
|
46
47
|
```python
|
|
47
48
|
from patchworks.plugins.ome_zarr import to_ome_zarr
|
|
@@ -50,6 +51,51 @@ to_ome_zarr("scan.czi", "scan.zarr", n_levels=5) # via bioio
|
|
|
50
51
|
to_ome_zarr("scan.ims", "scan.zarr") # Imaris, native HDF5
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
### A folder of single-plane TIFFs
|
|
55
|
+
|
|
56
|
+
Some acquisitions/stitching tools save **one TIFF per Z/C plane** instead of a
|
|
57
|
+
single multi-page file, e.g.:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
sample_T0_Z000_C0_V0.tif
|
|
61
|
+
sample_T0_Z000_C1_V0.tif
|
|
62
|
+
sample_T0_Z001_C0_V0.tif
|
|
63
|
+
sample_T0_Z001_C1_V0.tif
|
|
64
|
+
...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pass `sequence_pattern=` and let `source` be a glob over the folder instead of
|
|
68
|
+
one file path. The pattern is a regex whose **named groups** map to axis
|
|
69
|
+
labels — one file per group of indices:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
to_ome_zarr(
|
|
73
|
+
"sample/*.tif",
|
|
74
|
+
"sample.zarr",
|
|
75
|
+
sequence_pattern=r"_T(?P<T>\d+)_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+",
|
|
76
|
+
shard=True, # recommended for large sequences, see Sharding below
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You don't need to order the named groups to match how the axes should come
|
|
81
|
+
out: the result is always reordered to patchworks' `c, z, y, x` convention —
|
|
82
|
+
required for `channel=` selection (`load_ome_zarr`/`tile_process` always read
|
|
83
|
+
the channel from axis 0) — and a constant axis like `T0` above (one value, no
|
|
84
|
+
real time series) is dropped automatically, the same way the bioio/Imaris
|
|
85
|
+
readers already drop a singleton time axis.
|
|
86
|
+
|
|
87
|
+
Each file becomes exactly **one dask chunk**, decoded lazily on access — no
|
|
88
|
+
data is duplicated or eagerly loaded, so this scales to huge (multi-TB)
|
|
89
|
+
sequences (built on `tifffile.TiffSequence`, the same mechanism Cellpose's
|
|
90
|
+
own distributed pipeline uses). Pixel calibration is read automatically from
|
|
91
|
+
the first file's own metadata — see [Pixel calibration](#pixel-calibration)
|
|
92
|
+
below.
|
|
93
|
+
|
|
94
|
+
Available on the cluster too: the Snakemake `convert` rule reads a
|
|
95
|
+
`sequence_pattern:` key from the config (`input:` then being the glob), so a
|
|
96
|
+
folder-of-TIFFs conversion runs through the same SLURM profile as any other
|
|
97
|
+
input — see [Cluster usage](snakemake.md).
|
|
98
|
+
|
|
53
99
|
!!! note "Imaris pyramids: rebuild (default) or reuse"
|
|
54
100
|
`.ims` files carry their own resolution pyramid. By default `to_ome_zarr`
|
|
55
101
|
reads only the **full-resolution** level and **builds a fresh NGFF pyramid**
|
|
@@ -64,9 +110,11 @@ to_ome_zarr("scan.ims", "scan.zarr") # Imaris, native HDF5
|
|
|
64
110
|
### Pixel calibration
|
|
65
111
|
|
|
66
112
|
The physical voxel size is read from the input — bioio's `physical_pixel_sizes`,
|
|
67
|
-
the Imaris resolution metadata,
|
|
68
|
-
|
|
69
|
-
|
|
113
|
+
the Imaris resolution metadata, an existing OME-ZARR's scale, or (for a TIFF
|
|
114
|
+
sequence) the first file's own ImageJ metadata (`spacing`/`unit`) or
|
|
115
|
+
`XResolution`/`YResolution` tags — and written into the NGFF
|
|
116
|
+
`coordinateTransformations` (in micrometers), so calibration is preserved
|
|
117
|
+
regardless of input. Override or supply it for bare arrays with
|
|
70
118
|
`pixel_size={"z": 2.0, "y": 0.32, "x": 0.32}`.
|
|
71
119
|
|
|
72
120
|
### Won't OOM
|
|
@@ -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,146 @@ 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 — the order they appear in
|
|
705
|
+
the *pattern* (not the filename) doesn't matter, the result is
|
|
706
|
+
always reordered to patchworks' ``tczyx`` convention and any
|
|
707
|
+
singleton non-spatial axis (e.g. a constant ``T0``) is dropped, same
|
|
708
|
+
as the bioio/Imaris readers, so a real channel axis always ends up
|
|
709
|
+
first (required by :func:`patchworks.load_ome_zarr`).
|
|
710
|
+
|
|
711
|
+
Returns
|
|
712
|
+
-------
|
|
713
|
+
tuple
|
|
714
|
+
``(array, axes, pixel_size)`` — a lazy dask array, its axes string
|
|
715
|
+
(parsed axes, canonicalised, + trailing ``"yx"``) and a
|
|
716
|
+
``{axis: micrometers}`` calibration dict read from the first file.
|
|
717
|
+
"""
|
|
718
|
+
try:
|
|
719
|
+
import tifffile
|
|
720
|
+
except ImportError as exc:
|
|
721
|
+
raise ImportError(
|
|
722
|
+
"reading a folder of TIFFs requires tifffile. Install it with:\n"
|
|
723
|
+
" pip install tifffile\n"
|
|
724
|
+
"(already pulled in by patchworks[bioio] via bioio-tifffile)."
|
|
725
|
+
) from exc
|
|
726
|
+
|
|
727
|
+
ts = tifffile.TiffSequence(pattern, pattern=sequence_pattern)
|
|
728
|
+
arr = da.from_zarr(zarr.open(store=ts.aszarr()))
|
|
729
|
+
full_axes = ts.axes.lower() + "yx"
|
|
730
|
+
|
|
731
|
+
# Canonicalise to patchworks' tczyx axis order regardless of the order
|
|
732
|
+
# sequence_pattern's groups happen to appear in (unrecognised axes sort
|
|
733
|
+
# last, before y/x).
|
|
734
|
+
order = sorted(
|
|
735
|
+
range(len(full_axes)),
|
|
736
|
+
key=lambda i: (
|
|
737
|
+
_DEFAULT_ORDER.index(full_axes[i])
|
|
738
|
+
if full_axes[i] in _DEFAULT_ORDER
|
|
739
|
+
else len(_DEFAULT_ORDER)
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
arr = arr.transpose(order)
|
|
743
|
+
ordered_axes = "".join(full_axes[i] for i in order)
|
|
744
|
+
|
|
745
|
+
# Drop singleton non-spatial axes (e.g. a constant T), matching
|
|
746
|
+
# _open_bioio/_open_imaris, so a real channel axis lands at position 0.
|
|
747
|
+
keep = [
|
|
748
|
+
i
|
|
749
|
+
for i, a in enumerate(ordered_axes)
|
|
750
|
+
if a in _SPATIAL_AXES or arr.shape[i] > 1
|
|
751
|
+
]
|
|
752
|
+
index = tuple(slice(None) if i in keep else 0 for i in range(arr.ndim))
|
|
753
|
+
arr = arr[index]
|
|
754
|
+
axes = "".join(ordered_axes[i] for i in keep)
|
|
755
|
+
|
|
756
|
+
pixel_size = _tiff_pixel_size(ts[0])
|
|
757
|
+
logger.info(
|
|
758
|
+
"tifffile sequence opened %s files as %s %s cal=%s",
|
|
759
|
+
len(ts),
|
|
760
|
+
axes,
|
|
761
|
+
arr.shape,
|
|
762
|
+
pixel_size,
|
|
763
|
+
)
|
|
764
|
+
return arr, axes, pixel_size
|
|
765
|
+
|
|
766
|
+
|
|
623
767
|
def _open_imaris(path: str, level: int = 0) -> tuple[da.Array, str, PixelSize]:
|
|
624
768
|
"""Open one Imaris ``.ims`` resolution level lazily.
|
|
625
769
|
|
|
@@ -769,12 +913,14 @@ def _to_dask(
|
|
|
769
913
|
source: Union[da.Array, np.ndarray, str, Path],
|
|
770
914
|
axes: Union[str, None],
|
|
771
915
|
scene: int,
|
|
916
|
+
sequence_pattern: Union[str, None] = None,
|
|
772
917
|
) -> tuple[da.Array, str, PixelSize]:
|
|
773
918
|
"""Resolve *source* into a lazy ``(array, axes, pixel_size)`` triple.
|
|
774
919
|
|
|
775
920
|
Dispatches by type: dask/NumPy arrays pass through; ``.zarr`` paths use the
|
|
776
|
-
OME-ZARR loader; ``.ims`` paths use the Imaris reader;
|
|
777
|
-
|
|
921
|
+
OME-ZARR loader; ``.ims`` paths use the Imaris reader; a *sequence_pattern*
|
|
922
|
+
treats *source* as a glob over a folder of single-plane TIFFs; anything
|
|
923
|
+
else uses bioio.
|
|
778
924
|
|
|
779
925
|
Parameters
|
|
780
926
|
----------
|
|
@@ -784,6 +930,10 @@ def _to_dask(
|
|
|
784
930
|
Explicit axes, or ``None`` to infer them.
|
|
785
931
|
scene : int
|
|
786
932
|
Scene index for bioio inputs.
|
|
933
|
+
sequence_pattern : str or None
|
|
934
|
+
When given, *source* is a glob pattern over a folder of single-plane
|
|
935
|
+
TIFFs and this is the filename-parsing regex; see
|
|
936
|
+
:func:`_open_tiff_sequence`.
|
|
787
937
|
|
|
788
938
|
Returns
|
|
789
939
|
-------
|
|
@@ -796,6 +946,9 @@ def _to_dask(
|
|
|
796
946
|
return da.asarray(source), axes or _default_axes(source.ndim), {}
|
|
797
947
|
|
|
798
948
|
path = str(source)
|
|
949
|
+
if sequence_pattern is not None:
|
|
950
|
+
arr, detected, ps = _open_tiff_sequence(path, sequence_pattern)
|
|
951
|
+
return arr, axes or detected, ps
|
|
799
952
|
if path.endswith(".zarr"):
|
|
800
953
|
arr = load_ome_zarr(source, channel=None)
|
|
801
954
|
ax = axes or _default_axes(arr.ndim)
|
|
@@ -815,6 +968,7 @@ def to_ome_zarr(
|
|
|
815
968
|
axes: Union[str, None] = None,
|
|
816
969
|
pixel_size: Union[PixelSize, tuple, None] = None,
|
|
817
970
|
scene: int = 0,
|
|
971
|
+
sequence_pattern: Union[str, None] = None,
|
|
818
972
|
n_levels: int = 5,
|
|
819
973
|
downscale: int = 2,
|
|
820
974
|
chunks: Union[tuple[int, ...], None] = None,
|
|
@@ -826,7 +980,8 @@ def to_ome_zarr(
|
|
|
826
980
|
"""Write *source* as a pyramidal, calibrated OME-ZARR store.
|
|
827
981
|
|
|
828
982
|
*source* may be a dask/NumPy array, a ``.zarr`` store, an Imaris ``.ims``
|
|
829
|
-
file,
|
|
983
|
+
file, any image format readable by bioio (CZI, LIF, ND2, OME-TIFF, …), or
|
|
984
|
+
(with *sequence_pattern* set) a glob over a folder of single-plane TIFFs.
|
|
830
985
|
File inputs are read lazily; the pyramid is built level-by-level from disk
|
|
831
986
|
with bounded chunks, so the full volume never needs to fit in RAM. Only
|
|
832
987
|
``x``/``y`` are downsampled; ``z`` (and channel/time) stay full-resolution.
|
|
@@ -847,6 +1002,18 @@ def to_ome_zarr(
|
|
|
847
1002
|
arrays.
|
|
848
1003
|
scene : int, optional
|
|
849
1004
|
Scene index for multi-scene bioio files.
|
|
1005
|
+
sequence_pattern : str, optional
|
|
1006
|
+
When given, *source* is treated as a glob pattern over a folder of
|
|
1007
|
+
single-plane TIFFs (e.g. ``"folder/*.tif"``) instead of a single
|
|
1008
|
+
file, and this is the regex parsing each file name into axis labels
|
|
1009
|
+
and indices via named groups, e.g.
|
|
1010
|
+
``r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+"``. Each file
|
|
1011
|
+
becomes exactly one chunk, read lazily on access (no data
|
|
1012
|
+
duplicated) — see :func:`tifffile.TiffSequence`. The result is
|
|
1013
|
+
always reordered to patchworks' ``czyx`` convention regardless of
|
|
1014
|
+
the order named groups appear in the pattern, and any singleton
|
|
1015
|
+
non-spatial axis (e.g. a constant ``T0``) is dropped, so a real
|
|
1016
|
+
channel axis always ends up first.
|
|
850
1017
|
n_levels : int, optional
|
|
851
1018
|
Maximum number of pyramid levels including full resolution.
|
|
852
1019
|
downscale : int, optional
|
|
@@ -881,6 +1048,13 @@ def to_ome_zarr(
|
|
|
881
1048
|
>>> from patchworks.plugins.ome_zarr import to_ome_zarr
|
|
882
1049
|
>>> to_ome_zarr("scan.ims", "scan.zarr", n_levels=4)
|
|
883
1050
|
'scan.zarr'
|
|
1051
|
+
>>> to_ome_zarr(
|
|
1052
|
+
... "ZT18_Male4_Left/*.tif",
|
|
1053
|
+
... "ZT18_Male4_Left.zarr",
|
|
1054
|
+
... sequence_pattern=r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+",
|
|
1055
|
+
... shard=True,
|
|
1056
|
+
... ) # doctest: +SKIP
|
|
1057
|
+
'ZT18_Male4_Left.zarr'
|
|
884
1058
|
"""
|
|
885
1059
|
if downscale < 2:
|
|
886
1060
|
raise ValueError("downscale must be >= 2")
|
|
@@ -908,7 +1082,7 @@ def to_ome_zarr(
|
|
|
908
1082
|
exc,
|
|
909
1083
|
)
|
|
910
1084
|
|
|
911
|
-
arr, axes, detected = _to_dask(source, axes, scene)
|
|
1085
|
+
arr, axes, detected = _to_dask(source, axes, scene, sequence_pattern)
|
|
912
1086
|
if len(axes) != arr.ndim:
|
|
913
1087
|
raise ValueError(
|
|
914
1088
|
f"axes {axes!r} has {len(axes)} entries but array is {arr.ndim}-D"
|
|
@@ -79,6 +79,72 @@ 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
|
+
|
|
85
|
+
The filename pattern lists Z before C, but the output must still come
|
|
86
|
+
out channel-first (patchworks' tczyx convention), since load_ome_zarr /
|
|
87
|
+
tile_process hard-assume axis 0 is the channel axis.
|
|
88
|
+
"""
|
|
89
|
+
tifffile = pytest.importorskip("tifffile")
|
|
90
|
+
n_z, n_c, size = 3, 2, 8
|
|
91
|
+
for z in range(n_z):
|
|
92
|
+
for c in range(n_c):
|
|
93
|
+
img = np.full((size, size), z * 10 + c, dtype="uint16")
|
|
94
|
+
tifffile.imwrite(
|
|
95
|
+
tmp_path / f"sample_Z{z:03d}_C{c}_V0.tif",
|
|
96
|
+
img,
|
|
97
|
+
resolution=(20000.0, 20000.0),
|
|
98
|
+
resolutionunit="CENTIMETER",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
out = tmp_path / "out.zarr"
|
|
102
|
+
to_ome_zarr(
|
|
103
|
+
str(tmp_path / "*.tif"),
|
|
104
|
+
out,
|
|
105
|
+
sequence_pattern=r"_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+",
|
|
106
|
+
n_levels=1,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
result = np.asarray(load_ome_zarr(out, channel=None))
|
|
110
|
+
assert result.shape == (n_c, n_z, size, size) # channel-first, not z-first
|
|
111
|
+
# each plane's constant value encodes its (z, c) position.
|
|
112
|
+
assert (
|
|
113
|
+
result[:, :, 0, 0]
|
|
114
|
+
== [[z * 10 + c for z in range(n_z)] for c in range(n_c)]
|
|
115
|
+
).all()
|
|
116
|
+
assert _level_scale(out, 0) == pytest.approx([1.0, 1.0, 0.5, 0.5])
|
|
117
|
+
|
|
118
|
+
# per-channel selection picks the right plane regardless of pattern order.
|
|
119
|
+
ch1 = np.asarray(load_ome_zarr(out, channel=1))
|
|
120
|
+
assert ch1.shape == (n_z, size, size)
|
|
121
|
+
assert (ch1[:, 0, 0] == [z * 10 + 1 for z in range(n_z)]).all()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_tiff_sequence_drops_singleton_time_axis(tmp_path):
|
|
125
|
+
"""A constant T in the pattern is dropped, keeping channel at axis 0."""
|
|
126
|
+
tifffile = pytest.importorskip("tifffile")
|
|
127
|
+
n_z, n_c, size = 2, 3, 8
|
|
128
|
+
for z in range(n_z):
|
|
129
|
+
for c in range(n_c):
|
|
130
|
+
img = np.full((size, size), z * 10 + c, dtype="uint16")
|
|
131
|
+
tifffile.imwrite(tmp_path / f"sample_T0_Z{z:03d}_C{c}_V0.tif", img)
|
|
132
|
+
|
|
133
|
+
out = tmp_path / "out.zarr"
|
|
134
|
+
to_ome_zarr(
|
|
135
|
+
str(tmp_path / "*.tif"),
|
|
136
|
+
out,
|
|
137
|
+
sequence_pattern=r"_T(?P<T>\d+)_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+",
|
|
138
|
+
n_levels=1,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
result = np.asarray(load_ome_zarr(out, channel=None))
|
|
142
|
+
assert result.shape == (n_c, n_z, size, size) # no leftover T axis
|
|
143
|
+
ch2 = np.asarray(load_ome_zarr(out, channel=2))
|
|
144
|
+
assert ch2.shape == (n_z, size, size)
|
|
145
|
+
assert (ch2[:, 0, 0] == [z * 10 + 2 for z in range(n_z)]).all()
|
|
146
|
+
|
|
147
|
+
|
|
82
148
|
def test_axes_length_mismatch(tmp_path):
|
|
83
149
|
with pytest.raises(ValueError):
|
|
84
150
|
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
|
|
|
@@ -152,7 +152,9 @@ def build_fn(cfg):
|
|
|
152
152
|
if dilate:
|
|
153
153
|
from patchworks import dilate_labels
|
|
154
154
|
|
|
155
|
-
fn = dilate_labels(
|
|
155
|
+
fn = dilate_labels(
|
|
156
|
+
fn, iterations=dilate, use_gpu=cfg.get("dilate_gpu", False)
|
|
157
|
+
)
|
|
156
158
|
|
|
157
159
|
return fn
|
|
158
160
|
|
|
@@ -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
|