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.
Files changed (89) hide show
  1. {patchworks-1.3.3 → patchworks-1.4.0}/PKG-INFO +14 -1
  2. {patchworks-1.3.3 → patchworks-1.4.0}/README.md +13 -0
  3. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/dog.md +3 -0
  4. {patchworks-1.3.3 → patchworks-1.4.0}/docs/getting_started.md +13 -0
  5. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/custom_segmentation.md +7 -2
  6. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/ome_zarr.py +149 -8
  7. {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_ome_zarr.py +29 -0
  8. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config.yaml +7 -1
  9. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/convert.py +1 -0
  10. {patchworks-1.3.3 → patchworks-1.4.0}/.github/workflows/docs.yml +0 -0
  11. {patchworks-1.3.3 → patchworks-1.4.0}/.github/workflows/lint.yml +0 -0
  12. {patchworks-1.3.3 → patchworks-1.4.0}/.github/workflows/release.yml +0 -0
  13. {patchworks-1.3.3 → patchworks-1.4.0}/.gitignore +0 -0
  14. {patchworks-1.3.3 → patchworks-1.4.0}/.markdownlint-cli2.yaml +0 -0
  15. {patchworks-1.3.3 → patchworks-1.4.0}/LICENSE +0 -0
  16. {patchworks-1.3.3 → patchworks-1.4.0}/cliff.toml +0 -0
  17. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/chunks.md +0 -0
  18. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/cluster.md +0 -0
  19. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/io.md +0 -0
  20. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/merge_tile_labels.md +0 -0
  21. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/cellpose.md +0 -0
  22. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/dog.md +0 -0
  23. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/napari.md +0 -0
  24. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/plugins/ome_zarr.md +0 -0
  25. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/postprocess.md +0 -0
  26. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/relabel.md +0 -0
  27. {patchworks-1.3.3 → patchworks-1.4.0}/docs/api/tile_process.md +0 -0
  28. {patchworks-1.3.3 → patchworks-1.4.0}/docs/assets/logo.png +0 -0
  29. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_2d.md +0 -0
  30. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_2d.py +0 -0
  31. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_3d.md +0 -0
  32. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/cellpose_3d.py +0 -0
  33. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/custom.md +0 -0
  34. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/custom_method.py +0 -0
  35. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/dog.py +0 -0
  36. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/standalone_merge.md +0 -0
  37. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/stardist.md +0 -0
  38. {patchworks-1.3.3 → patchworks-1.4.0}/docs/examples/stardist_2d.py +0 -0
  39. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/gpu_distributed.md +0 -0
  40. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/label_relations.md +0 -0
  41. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/measurements.md +0 -0
  42. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/merging.md +0 -0
  43. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/ome_zarr_napari.md +0 -0
  44. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/performance.md +0 -0
  45. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/pitfalls.md +0 -0
  46. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/skip_empty.md +0 -0
  47. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/snakemake.md +0 -0
  48. {patchworks-1.3.3 → patchworks-1.4.0}/docs/guide/tiling.md +0 -0
  49. {patchworks-1.3.3 → patchworks-1.4.0}/docs/index.md +0 -0
  50. {patchworks-1.3.3 → patchworks-1.4.0}/mkdocs.yml +0 -0
  51. {patchworks-1.3.3 → patchworks-1.4.0}/pyproject.toml +0 -0
  52. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/__init__.py +0 -0
  53. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_chunks.py +0 -0
  54. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_cluster.py +0 -0
  55. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_core.py +0 -0
  56. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_distributed.py +0 -0
  57. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_io.py +0 -0
  58. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_merge.py +0 -0
  59. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_postprocess.py +0 -0
  60. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_relabel.py +0 -0
  61. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/_relations.py +0 -0
  62. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/__init__.py +0 -0
  63. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/cellpose.py +0 -0
  64. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/dog.py +0 -0
  65. {patchworks-1.3.3 → patchworks-1.4.0}/src/patchworks/plugins/napari.py +0 -0
  66. {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_core.py +0 -0
  67. {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_distributed.py +0 -0
  68. {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_dog.py +0 -0
  69. {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_napari.py +0 -0
  70. {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_postprocess.py +0 -0
  71. {patchworks-1.3.3 → patchworks-1.4.0}/tests/test_relations.py +0 -0
  72. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/README.md +0 -0
  73. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/Snakefile +0 -0
  74. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config_cilia.yaml +0 -0
  75. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config_cyto.yaml +0 -0
  76. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/config_nuclei.yaml +0 -0
  77. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/config/multi.yaml +0 -0
  78. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/pixi.toml +0 -0
  79. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/profile/slurm/config.yaml +0 -0
  80. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/common.smk +0 -0
  81. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/convert.smk +0 -0
  82. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/merge.smk +0 -0
  83. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/rules/segment.smk +0 -0
  84. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/_pw.py +0 -0
  85. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/fetch_model.py +0 -0
  86. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/merge.py +0 -0
  87. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/prepare_tiles.py +0 -0
  88. {patchworks-1.3.3 → patchworks-1.4.0}/workflow/scripts/run_multi.py +0 -0
  89. {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.3
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) # needs cupy installed
81
+ fn = dilate_labels(fn, iterations=2, use_gpu=True)
82
82
  ```
83
83
 
84
- On the cluster, this is the `dilate_gpu: true` config key.
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, or an existing
17
- OME-ZARR's scale transform and written into the NGFF ``coordinate
18
- Transformations`` so the µm/pixel sizing is preserved. Pass ``pixel_size=`` to
19
- override or to supply it for bare arrays.
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; anything else uses
777
- bioio.
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, or any image format readable by bioio (CZI, LIF, ND2, OME-TIFF, …).
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 an .zarr
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