patchworks 1.4.0__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.
Files changed (89) hide show
  1. {patchworks-1.4.0 → patchworks-1.4.1}/PKG-INFO +3 -1
  2. {patchworks-1.4.0 → patchworks-1.4.1}/README.md +2 -0
  3. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/ome_zarr_napari.md +54 -6
  4. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/plugins/ome_zarr.py +40 -7
  5. {patchworks-1.4.0 → patchworks-1.4.1}/tests/test_ome_zarr.py +40 -3
  6. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/scripts/_pw.py +3 -1
  7. {patchworks-1.4.0 → patchworks-1.4.1}/.github/workflows/docs.yml +0 -0
  8. {patchworks-1.4.0 → patchworks-1.4.1}/.github/workflows/lint.yml +0 -0
  9. {patchworks-1.4.0 → patchworks-1.4.1}/.github/workflows/release.yml +0 -0
  10. {patchworks-1.4.0 → patchworks-1.4.1}/.gitignore +0 -0
  11. {patchworks-1.4.0 → patchworks-1.4.1}/.markdownlint-cli2.yaml +0 -0
  12. {patchworks-1.4.0 → patchworks-1.4.1}/LICENSE +0 -0
  13. {patchworks-1.4.0 → patchworks-1.4.1}/cliff.toml +0 -0
  14. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/chunks.md +0 -0
  15. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/cluster.md +0 -0
  16. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/io.md +0 -0
  17. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/merge_tile_labels.md +0 -0
  18. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/plugins/cellpose.md +0 -0
  19. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/plugins/dog.md +0 -0
  20. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/plugins/napari.md +0 -0
  21. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/plugins/ome_zarr.md +0 -0
  22. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/postprocess.md +0 -0
  23. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/relabel.md +0 -0
  24. {patchworks-1.4.0 → patchworks-1.4.1}/docs/api/tile_process.md +0 -0
  25. {patchworks-1.4.0 → patchworks-1.4.1}/docs/assets/logo.png +0 -0
  26. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/cellpose_2d.md +0 -0
  27. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/cellpose_2d.py +0 -0
  28. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/cellpose_3d.md +0 -0
  29. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/cellpose_3d.py +0 -0
  30. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/custom.md +0 -0
  31. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/custom_method.py +0 -0
  32. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/dog.md +0 -0
  33. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/dog.py +0 -0
  34. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/standalone_merge.md +0 -0
  35. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/stardist.md +0 -0
  36. {patchworks-1.4.0 → patchworks-1.4.1}/docs/examples/stardist_2d.py +0 -0
  37. {patchworks-1.4.0 → patchworks-1.4.1}/docs/getting_started.md +0 -0
  38. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/custom_segmentation.md +0 -0
  39. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/gpu_distributed.md +0 -0
  40. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/label_relations.md +0 -0
  41. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/measurements.md +0 -0
  42. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/merging.md +0 -0
  43. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/performance.md +0 -0
  44. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/pitfalls.md +0 -0
  45. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/skip_empty.md +0 -0
  46. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/snakemake.md +0 -0
  47. {patchworks-1.4.0 → patchworks-1.4.1}/docs/guide/tiling.md +0 -0
  48. {patchworks-1.4.0 → patchworks-1.4.1}/docs/index.md +0 -0
  49. {patchworks-1.4.0 → patchworks-1.4.1}/mkdocs.yml +0 -0
  50. {patchworks-1.4.0 → patchworks-1.4.1}/pyproject.toml +0 -0
  51. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/__init__.py +0 -0
  52. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_chunks.py +0 -0
  53. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_cluster.py +0 -0
  54. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_core.py +0 -0
  55. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_distributed.py +0 -0
  56. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_io.py +0 -0
  57. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_merge.py +0 -0
  58. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_postprocess.py +0 -0
  59. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_relabel.py +0 -0
  60. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/_relations.py +0 -0
  61. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/plugins/__init__.py +0 -0
  62. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/plugins/cellpose.py +0 -0
  63. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/plugins/dog.py +0 -0
  64. {patchworks-1.4.0 → patchworks-1.4.1}/src/patchworks/plugins/napari.py +0 -0
  65. {patchworks-1.4.0 → patchworks-1.4.1}/tests/test_core.py +0 -0
  66. {patchworks-1.4.0 → patchworks-1.4.1}/tests/test_distributed.py +0 -0
  67. {patchworks-1.4.0 → patchworks-1.4.1}/tests/test_dog.py +0 -0
  68. {patchworks-1.4.0 → patchworks-1.4.1}/tests/test_napari.py +0 -0
  69. {patchworks-1.4.0 → patchworks-1.4.1}/tests/test_postprocess.py +0 -0
  70. {patchworks-1.4.0 → patchworks-1.4.1}/tests/test_relations.py +0 -0
  71. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/README.md +0 -0
  72. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/Snakefile +0 -0
  73. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/config/config.yaml +0 -0
  74. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/config/config_cilia.yaml +0 -0
  75. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/config/config_cyto.yaml +0 -0
  76. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/config/config_nuclei.yaml +0 -0
  77. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/config/multi.yaml +0 -0
  78. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/pixi.toml +0 -0
  79. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/profile/slurm/config.yaml +0 -0
  80. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/rules/common.smk +0 -0
  81. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/rules/convert.smk +0 -0
  82. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/rules/merge.smk +0 -0
  83. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/rules/segment.smk +0 -0
  84. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/scripts/convert.py +0 -0
  85. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/scripts/fetch_model.py +0 -0
  86. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/scripts/merge.py +0 -0
  87. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/scripts/prepare_tiles.py +0 -0
  88. {patchworks-1.4.0 → patchworks-1.4.1}/workflow/scripts/run_multi.py +0 -0
  89. {patchworks-1.4.0 → 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.4.0
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`)
@@ -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, or **any format** readable by
43
- [bioio](https://github.com/bioio-devs/bioio) (CZI, LIF, ND2, OME-TIFF, …). File
44
- inputs are read **lazily**.
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, or an existing OME-ZARR's scale and written
68
- into the NGFF `coordinateTransformations` (in micrometers), so calibration is
69
- preserved regardless of input. Override or supply it for bare arrays with
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
@@ -701,15 +701,19 @@ def _open_tiff_sequence(
701
701
  sequence_pattern : str
702
702
  Regular expression parsing each file name into axis labels and
703
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.
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`).
706
710
 
707
711
  Returns
708
712
  -------
709
713
  tuple
710
714
  ``(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.
715
+ (parsed axes, canonicalised, + trailing ``"yx"``) and a
716
+ ``{axis: micrometers}`` calibration dict read from the first file.
713
717
  """
714
718
  try:
715
719
  import tifffile
@@ -722,7 +726,33 @@ def _open_tiff_sequence(
722
726
 
723
727
  ts = tifffile.TiffSequence(pattern, pattern=sequence_pattern)
724
728
  arr = da.from_zarr(zarr.open(store=ts.aszarr()))
725
- axes = ts.axes.lower() + "yx"
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
+
726
756
  pixel_size = _tiff_pixel_size(ts[0])
727
757
  logger.info(
728
758
  "tifffile sequence opened %s files as %s %s cal=%s",
@@ -979,8 +1009,11 @@ def to_ome_zarr(
979
1009
  and indices via named groups, e.g.
980
1010
  ``r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+"``. Each file
981
1011
  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.
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.
984
1017
  n_levels : int, optional
985
1018
  Maximum number of pyramid levels including full resolution.
986
1019
  downscale : int, optional
@@ -80,7 +80,12 @@ def test_non_spatial_axis_not_downsampled(tmp_path):
80
80
 
81
81
 
82
82
  def test_tiff_sequence_conversion(tmp_path):
83
- """A folder of single-plane TIFFs is wrapped lazily and converted."""
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
+ """
84
89
  tifffile = pytest.importorskip("tifffile")
85
90
  n_z, n_c, size = 3, 2, 8
86
91
  for z in range(n_z):
@@ -102,11 +107,43 @@ def test_tiff_sequence_conversion(tmp_path):
102
107
  )
103
108
 
104
109
  result = np.asarray(load_ome_zarr(out, channel=None))
105
- assert result.shape == (n_z, n_c, size, size)
110
+ assert result.shape == (n_c, n_z, size, size) # channel-first, not z-first
106
111
  # 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()
112
+ assert (
113
+ result[:, :, 0, 0]
114
+ == [[z * 10 + c for z in range(n_z)] for c in range(n_c)]
115
+ ).all()
108
116
  assert _level_scale(out, 0) == pytest.approx([1.0, 1.0, 0.5, 0.5])
109
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
+
110
147
 
111
148
  def test_axes_length_mismatch(tmp_path):
112
149
  with pytest.raises(ValueError):
@@ -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(fn, iterations=dilate, use_gpu=cfg.get("dilate_gpu", False))
155
+ fn = dilate_labels(
156
+ fn, iterations=dilate, use_gpu=cfg.get("dilate_gpu", False)
157
+ )
156
158
 
157
159
  return fn
158
160
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes