patchworks 2.1.1__tar.gz → 2.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {patchworks-2.1.1 → patchworks-2.1.2}/PKG-INFO +1 -1
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/plugins/ome_zarr.py +195 -23
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_ome_zarr.py +161 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/.github/workflows/docs.yml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/.github/workflows/lint.yml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/.github/workflows/release.yml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/.gitignore +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/LICENSE +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/README.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/cliff.toml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/chunks.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/cluster.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/io.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/postprocess.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/relabel.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/api/tile_process.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/assets/logo.png +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/custom.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/custom_method.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/dog.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/dog.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/stardist.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/getting_started.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/label_relations.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/measurements.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/merging.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/performance.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/snakemake.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/guide/tiling.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/docs/index.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/mkdocs.yml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/pyproject.toml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/__init__.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_chunks.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_core.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_distributed.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_io.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_merge.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/_relations.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_allocation.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_core.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_distributed.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_dog.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_gpu.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_napari.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_occupancy.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_postprocess.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/tests/test_relations.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/README.md +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/Snakefile +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/config/config.yaml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/config/multi.yaml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/pixi.toml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/rules/common.smk +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/rules/convert.smk +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/rules/merge.smk +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/rules/segment.smk +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/scripts/_pw.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/scripts/convert.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/scripts/merge.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/scripts/run_multi.py +0 -0
- {patchworks-2.1.1 → patchworks-2.1.2}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
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
|
|
@@ -49,6 +49,7 @@ import glob
|
|
|
49
49
|
import logging
|
|
50
50
|
import math
|
|
51
51
|
from concurrent.futures import ThreadPoolExecutor
|
|
52
|
+
from contextlib import nullcontext as _nullcontext
|
|
52
53
|
from itertools import product as _iproduct
|
|
53
54
|
from pathlib import Path
|
|
54
55
|
from typing import Union
|
|
@@ -171,22 +172,41 @@ def _strides(axes: str, downscale: int) -> tuple[int, ...]:
|
|
|
171
172
|
return tuple(downscale if a in _DOWNSAMPLE_AXES else 1 for a in axes)
|
|
172
173
|
|
|
173
174
|
|
|
174
|
-
def _default_chunks(
|
|
175
|
+
def _default_chunks(
|
|
176
|
+
shape: tuple[int, ...],
|
|
177
|
+
axes: str,
|
|
178
|
+
source_chunks: Union[tuple[int, ...], None] = None,
|
|
179
|
+
) -> tuple[int, ...]:
|
|
175
180
|
"""Bounded chunk shape so writing a level never blows up RAM.
|
|
176
181
|
|
|
182
|
+
With *source_chunks*, the result never spans more than one source chunk on
|
|
183
|
+
any axis. That matters when the source's own granularity is coarse: a
|
|
184
|
+
folder of stitched TIFFs gives one chunk per **file**, so a plane can be
|
|
185
|
+
gigabytes. Grouping 16 of those into one output chunk (the ``z`` cap) means
|
|
186
|
+
holding 16 whole planes to write a 32 MB chunk -- tens of GB for the
|
|
187
|
+
smallest unit of work, which is how conversion gets OOM-killed.
|
|
188
|
+
|
|
189
|
+
Splitting a source chunk is fine (that is a slice of something already
|
|
190
|
+
read); combining several is not.
|
|
191
|
+
|
|
177
192
|
Parameters
|
|
178
193
|
----------
|
|
179
194
|
shape : tuple of int
|
|
180
195
|
Array shape.
|
|
181
196
|
axes : str
|
|
182
197
|
One letter per axis (selects the per-axis cap).
|
|
198
|
+
source_chunks : tuple of int, optional
|
|
199
|
+
The input array's own chunking.
|
|
183
200
|
|
|
184
201
|
Returns
|
|
185
202
|
-------
|
|
186
203
|
tuple of int
|
|
187
|
-
Per-axis chunk size, capped by ``_CHUNK_CAP
|
|
204
|
+
Per-axis chunk size, capped by ``_CHUNK_CAP`` and by *source_chunks*.
|
|
188
205
|
"""
|
|
189
|
-
|
|
206
|
+
caps = [min(s, _CHUNK_CAP.get(a, s)) for s, a in zip(shape, axes)]
|
|
207
|
+
if source_chunks is not None:
|
|
208
|
+
caps = [min(c, max(1, int(sc))) for c, sc in zip(caps, source_chunks)]
|
|
209
|
+
return tuple(caps)
|
|
190
210
|
|
|
191
211
|
|
|
192
212
|
ShardSpec = Union[bool, tuple[int, ...]]
|
|
@@ -446,6 +466,70 @@ def _stream_strided_level(
|
|
|
446
466
|
pass
|
|
447
467
|
|
|
448
468
|
|
|
469
|
+
# One chunk is one file without sharding. A shared cluster filesystem starts
|
|
470
|
+
# to hurt well before a million of them, and the cost lands on every later
|
|
471
|
+
# read too, not just the write.
|
|
472
|
+
_CHUNK_COUNT_WARN = 200_000
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _warn_if_many_chunks(
|
|
476
|
+
shape: tuple[int, ...], chunks: tuple[int, ...], shard: ShardSpec
|
|
477
|
+
) -> None:
|
|
478
|
+
"""Warn when a level will be written as a very large number of files.
|
|
479
|
+
|
|
480
|
+
A coarse source forces fine output chunks (see :func:`_default_chunks`),
|
|
481
|
+
and fine chunks mean many files. Sharding packs them into far fewer
|
|
482
|
+
without changing the chunking, so the fix is cheap -- but only if someone
|
|
483
|
+
notices before the store is written.
|
|
484
|
+
"""
|
|
485
|
+
if shard:
|
|
486
|
+
return
|
|
487
|
+
count = int(np.prod([math.ceil(s / c) for s, c in zip(shape, chunks)]))
|
|
488
|
+
if count < _CHUNK_COUNT_WARN:
|
|
489
|
+
return
|
|
490
|
+
logger.warning(
|
|
491
|
+
"level 0 will be %s chunks, i.e. that many files: the source's own "
|
|
492
|
+
"granularity forced chunks of %s. Set shard=True (config: "
|
|
493
|
+
"shard: true) to pack them into far fewer files -- same chunking, "
|
|
494
|
+
"same memory, and kinder to a shared filesystem.",
|
|
495
|
+
f"{count:,}",
|
|
496
|
+
chunks,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _bounded_scheduler(arr: da.Array):
|
|
501
|
+
"""Cap dask's threads by the memory one **source** chunk costs.
|
|
502
|
+
|
|
503
|
+
Dask keeps a source chunk alive while every task that reads it runs, so
|
|
504
|
+
peak memory tracks the *input* granularity, not the output's. A folder of
|
|
505
|
+
stitched TIFFs gives one chunk per file -- gigabytes each -- and the
|
|
506
|
+
default worker count is the machine's core count, so 32 threads x a 3.6 GB
|
|
507
|
+
plane is over 100 GB before anything is written.
|
|
508
|
+
|
|
509
|
+
Sizing from the source chunk against the job's real budget keeps that
|
|
510
|
+
bounded. Returns a context manager; a no-op when a distributed client is
|
|
511
|
+
driving, since it schedules with its own memory awareness.
|
|
512
|
+
"""
|
|
513
|
+
import dask
|
|
514
|
+
|
|
515
|
+
from .._chunks import cpu_allocation, safe_worker_count
|
|
516
|
+
from .._cluster import _distributed_client
|
|
517
|
+
|
|
518
|
+
if _distributed_client() is not None:
|
|
519
|
+
return _nullcontext()
|
|
520
|
+
chunk_nbytes = int(np.prod(arr.chunksize)) * arr.dtype.itemsize
|
|
521
|
+
# Two live copies per worker: the source chunk plus the slice being written.
|
|
522
|
+
workers = min(
|
|
523
|
+
cpu_allocation(), safe_worker_count(chunk_nbytes, fn_overhead=2)
|
|
524
|
+
)
|
|
525
|
+
logger.info(
|
|
526
|
+
"writing with %d dask worker(s): source chunks are %.2f GB each",
|
|
527
|
+
workers,
|
|
528
|
+
chunk_nbytes / 1024**3,
|
|
529
|
+
)
|
|
530
|
+
return dask.config.set(scheduler="threads", num_workers=workers)
|
|
531
|
+
|
|
532
|
+
|
|
449
533
|
def _progress_ctx(progress: bool, label: str):
|
|
450
534
|
"""Return a progress-bar context manager.
|
|
451
535
|
|
|
@@ -648,7 +732,12 @@ def _write_pyramid(
|
|
|
648
732
|
strides = _strides(axes, downscale)
|
|
649
733
|
|
|
650
734
|
if write_base:
|
|
651
|
-
|
|
735
|
+
# Cap by the source's own chunking: combining several source chunks
|
|
736
|
+
# into one output chunk can mean holding gigabytes to write megabytes.
|
|
737
|
+
base_chunks = chunks or _default_chunks(
|
|
738
|
+
arr.shape, axes, source_chunks=arr.chunksize
|
|
739
|
+
)
|
|
740
|
+
_warn_if_many_chunks(arr.shape, base_chunks, shard)
|
|
652
741
|
_to_zarr_level(
|
|
653
742
|
arr.rechunk(base_chunks), group_path, base_name, shard, progress
|
|
654
743
|
)
|
|
@@ -872,16 +961,75 @@ _UNIT_TO_UM = {
|
|
|
872
961
|
_RESUNIT_TO_UM = {2: 25400.0, 3: 10000.0}
|
|
873
962
|
|
|
874
963
|
|
|
964
|
+
def _ome_xml_pixel_size(xml: str) -> PixelSize:
|
|
965
|
+
"""Voxel size from an OME-XML ``Pixels`` element.
|
|
966
|
+
|
|
967
|
+
Stitching and acquisition software commonly writes OME-XML into
|
|
968
|
+
``ImageDescription`` and nothing else -- no ImageJ block, and resolution
|
|
969
|
+
tags left at their defaults. Readers that only look at the TIFF tags then
|
|
970
|
+
report an uncalibrated image even though the size is right there.
|
|
971
|
+
|
|
972
|
+
Parameters
|
|
973
|
+
----------
|
|
974
|
+
xml : str
|
|
975
|
+
The OME-XML document.
|
|
976
|
+
|
|
977
|
+
Returns
|
|
978
|
+
-------
|
|
979
|
+
dict
|
|
980
|
+
``{axis: micrometers}`` for whichever axes carry a size.
|
|
981
|
+
"""
|
|
982
|
+
from xml.etree import ElementTree
|
|
983
|
+
|
|
984
|
+
try:
|
|
985
|
+
root = ElementTree.fromstring(xml)
|
|
986
|
+
except ElementTree.ParseError:
|
|
987
|
+
return {}
|
|
988
|
+
# The OME namespace is versioned, so match on the local tag name.
|
|
989
|
+
pixels = next(
|
|
990
|
+
(el for el in root.iter() if el.tag.rsplit("}", 1)[-1] == "Pixels"),
|
|
991
|
+
None,
|
|
992
|
+
)
|
|
993
|
+
if pixels is None:
|
|
994
|
+
return {}
|
|
995
|
+
out: PixelSize = {}
|
|
996
|
+
for axis in "zyx":
|
|
997
|
+
value = pixels.get(f"PhysicalSize{axis.upper()}")
|
|
998
|
+
if not value:
|
|
999
|
+
continue
|
|
1000
|
+
unit = pixels.get(f"PhysicalSize{axis.upper()}Unit", "µm")
|
|
1001
|
+
factor = _UNIT_TO_UM.get(str(unit).lower())
|
|
1002
|
+
if factor is None:
|
|
1003
|
+
logger.warning(
|
|
1004
|
+
"OME-XML PhysicalSize%s has unit %r, which is not recognised; "
|
|
1005
|
+
"ignoring it.",
|
|
1006
|
+
axis.upper(),
|
|
1007
|
+
unit,
|
|
1008
|
+
)
|
|
1009
|
+
continue
|
|
1010
|
+
try:
|
|
1011
|
+
out[axis] = float(value) * factor
|
|
1012
|
+
except ValueError:
|
|
1013
|
+
continue
|
|
1014
|
+
return out
|
|
1015
|
+
|
|
1016
|
+
|
|
875
1017
|
def _tiff_pixel_size(path: str) -> PixelSize:
|
|
876
1018
|
"""Read physical voxel size from a TIFF file's own metadata.
|
|
877
1019
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1020
|
+
Checks every place the size is commonly written, most explicit first:
|
|
1021
|
+
|
|
1022
|
+
1. **OME-XML** ``PhysicalSizeX/Y/Z`` (+ their units) in
|
|
1023
|
+
``ImageDescription`` — what stitched and OME-TIFF output usually
|
|
1024
|
+
carries, and the only one of the three that can give ``z`` for a
|
|
1025
|
+
sequence of single planes.
|
|
1026
|
+
2. **ImageJ metadata** — ``spacing`` (+ ``unit``) for ``z``.
|
|
1027
|
+
3. **Resolution tags** — ``XResolution``/``YResolution`` in pixels per
|
|
1028
|
+
unit, the unit from ``ResolutionUnit`` (plain TIFFs) or from ImageJ's
|
|
1029
|
+
``unit`` string (ImageJ writes ``ResolutionUnit=NONE``).
|
|
1030
|
+
|
|
1031
|
+
Earlier sources win per axis, so a file carrying both keeps the explicit
|
|
1032
|
+
OME value. Unrecognized units are ignored rather than guessed at.
|
|
885
1033
|
|
|
886
1034
|
Parameters
|
|
887
1035
|
----------
|
|
@@ -898,9 +1046,18 @@ def _tiff_pixel_size(path: str) -> PixelSize:
|
|
|
898
1046
|
|
|
899
1047
|
pixel_size: PixelSize = {}
|
|
900
1048
|
with tifffile.TiffFile(path) as tif:
|
|
1049
|
+
description = tif.pages[0].tags.get("ImageDescription")
|
|
1050
|
+
xml = tif.ome_metadata or (
|
|
1051
|
+
description.value
|
|
1052
|
+
if description and "<OME" in str(description.value)
|
|
1053
|
+
else None
|
|
1054
|
+
)
|
|
1055
|
+
if xml:
|
|
1056
|
+
pixel_size.update(_ome_xml_pixel_size(str(xml)))
|
|
1057
|
+
|
|
901
1058
|
ij = tif.imagej_metadata or {}
|
|
902
1059
|
spacing = ij.get("spacing")
|
|
903
|
-
if spacing:
|
|
1060
|
+
if spacing and "z" not in pixel_size:
|
|
904
1061
|
factor = _UNIT_TO_UM.get(str(ij.get("unit", "micron")).lower(), 1.0)
|
|
905
1062
|
pixel_size["z"] = float(spacing) * factor
|
|
906
1063
|
|
|
@@ -911,6 +1068,8 @@ def _tiff_pixel_size(path: str) -> PixelSize:
|
|
|
911
1068
|
) or _UNIT_TO_UM.get(str(ij.get("unit", "")).lower())
|
|
912
1069
|
if um_per_unit is not None:
|
|
913
1070
|
for axis, tag_name in (("y", "YResolution"), ("x", "XResolution")):
|
|
1071
|
+
if axis in pixel_size:
|
|
1072
|
+
continue
|
|
914
1073
|
tag = page.tags.get(tag_name)
|
|
915
1074
|
if tag and tag.value[0]:
|
|
916
1075
|
num, den = tag.value
|
|
@@ -1014,6 +1173,18 @@ def _open_tiff_sequence(
|
|
|
1014
1173
|
arr.shape,
|
|
1015
1174
|
pixel_size,
|
|
1016
1175
|
)
|
|
1176
|
+
missing = [a for a in axes if a in _SPATIAL_AXES and a not in pixel_size]
|
|
1177
|
+
if missing:
|
|
1178
|
+
logger.warning(
|
|
1179
|
+
"no voxel size for %s in %s -- checked OME-XML PhysicalSize*, "
|
|
1180
|
+
"ImageJ spacing/unit, and the resolution tags. The store will be "
|
|
1181
|
+
"uncalibrated on %s, which also leaves anything deriving physical "
|
|
1182
|
+
"units from it (e.g. deconvolution voxel sizes) with nothing to "
|
|
1183
|
+
"read. Pass pixel_size= to supply it.",
|
|
1184
|
+
"/".join(missing),
|
|
1185
|
+
Path(ts[0]).name,
|
|
1186
|
+
"/".join(missing),
|
|
1187
|
+
)
|
|
1017
1188
|
return arr, axes, pixel_size
|
|
1018
1189
|
|
|
1019
1190
|
|
|
@@ -1361,17 +1532,18 @@ def to_ome_zarr(
|
|
|
1361
1532
|
|
|
1362
1533
|
out = str(out_path)
|
|
1363
1534
|
zarr.open_group(out, mode="w" if overwrite else "w-")
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1535
|
+
with _bounded_scheduler(arr):
|
|
1536
|
+
datasets = _write_pyramid(
|
|
1537
|
+
arr,
|
|
1538
|
+
axes,
|
|
1539
|
+
out,
|
|
1540
|
+
n_levels=n_levels,
|
|
1541
|
+
downscale=downscale,
|
|
1542
|
+
chunks=chunks,
|
|
1543
|
+
base_scale=base_scale,
|
|
1544
|
+
shard=shard,
|
|
1545
|
+
progress=progress,
|
|
1546
|
+
)
|
|
1375
1547
|
_write_multiscales(out, axes, datasets, Path(out).stem, calibrated=bool(ps))
|
|
1376
1548
|
return out
|
|
1377
1549
|
|
|
@@ -415,3 +415,164 @@ def test_glob_without_sequence_pattern_says_so(tmp_path):
|
|
|
415
415
|
with pytest.raises(Exception) as exc:
|
|
416
416
|
to_ome_zarr(str(tmp_path / "scan.ims"), tmp_path / "o2.zarr")
|
|
417
417
|
assert "sequence_pattern" not in str(exc.value)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_base_chunks_never_group_source_chunks(tmp_path):
|
|
421
|
+
"""One output chunk must not require several source chunks.
|
|
422
|
+
|
|
423
|
+
A folder of stitched TIFFs gives one dask chunk per file, so a plane can
|
|
424
|
+
be gigabytes. The z cap of 16 would group sixteen of them into one output
|
|
425
|
+
chunk -- ~58 GB held to write 32 MB, against a 64 GB job. Splitting a
|
|
426
|
+
source chunk is fine; combining several is not.
|
|
427
|
+
"""
|
|
428
|
+
from patchworks.plugins.ome_zarr import _default_chunks
|
|
429
|
+
|
|
430
|
+
shape = (4, 126, 45961, 42072)
|
|
431
|
+
# one chunk per file: (c=1, z=1, whole plane)
|
|
432
|
+
assert _default_chunks(
|
|
433
|
+
shape, "czyx", source_chunks=(1, 1, 45961, 42072)
|
|
434
|
+
) == (
|
|
435
|
+
1,
|
|
436
|
+
1,
|
|
437
|
+
1024,
|
|
438
|
+
1024,
|
|
439
|
+
)
|
|
440
|
+
# a normally-chunked source is unaffected by the cap
|
|
441
|
+
assert _default_chunks(shape, "czyx", source_chunks=shape) == (
|
|
442
|
+
1,
|
|
443
|
+
16,
|
|
444
|
+
1024,
|
|
445
|
+
1024,
|
|
446
|
+
)
|
|
447
|
+
# and omitting it keeps the previous behaviour
|
|
448
|
+
assert _default_chunks(shape, "czyx") == (1, 16, 1024, 1024)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def test_tiff_sequence_keeps_one_plane_per_chunk(tmp_path):
|
|
452
|
+
"""End to end: a per-file source must not be z-grouped on write."""
|
|
453
|
+
tifffile = pytest.importorskip("tifffile")
|
|
454
|
+
n_z, n_c, size = 4, 2, 8
|
|
455
|
+
for z in range(n_z):
|
|
456
|
+
for c in range(n_c):
|
|
457
|
+
tifffile.imwrite(
|
|
458
|
+
tmp_path / f"s_Z{z:03d}_C{c}_V0.tif",
|
|
459
|
+
np.full((size, size), z * 10 + c, "uint16"),
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
out = tmp_path / "seq.zarr"
|
|
463
|
+
to_ome_zarr(
|
|
464
|
+
str(tmp_path / "*.tif"),
|
|
465
|
+
out,
|
|
466
|
+
sequence_pattern=r"_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+",
|
|
467
|
+
n_levels=1,
|
|
468
|
+
progress=False,
|
|
469
|
+
)
|
|
470
|
+
level0 = zarr.open_array(str(out) + "/0", mode="r")
|
|
471
|
+
assert level0.chunks[1] == 1, (
|
|
472
|
+
f"z was grouped into {level0.chunks[1]} planes per chunk; each source "
|
|
473
|
+
"chunk is a whole file, so that multiplies the read"
|
|
474
|
+
)
|
|
475
|
+
# and the data still round-trips
|
|
476
|
+
result = np.asarray(load_ome_zarr(out, channel=None))
|
|
477
|
+
assert result.shape == (n_c, n_z, size, size)
|
|
478
|
+
assert (
|
|
479
|
+
result[:, :, 0, 0]
|
|
480
|
+
== [[z * 10 + c for z in range(n_z)] for c in range(n_c)]
|
|
481
|
+
).all()
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _ome_xml(x, y, z, unit="um"):
|
|
485
|
+
return (
|
|
486
|
+
'<?xml version="1.0"?>'
|
|
487
|
+
'<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06">'
|
|
488
|
+
f'<Image><Pixels PhysicalSizeX="{x}" PhysicalSizeXUnit="{unit}"'
|
|
489
|
+
f' PhysicalSizeY="{y}" PhysicalSizeYUnit="{unit}"'
|
|
490
|
+
f' PhysicalSizeZ="{z}" PhysicalSizeZUnit="{unit}"/></Image></OME>'
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_pixel_size_from_ome_xml(tmp_path):
|
|
495
|
+
"""OME-XML PhysicalSize* must be read, not just the resolution tags.
|
|
496
|
+
|
|
497
|
+
Stitched output commonly writes OME-XML into ImageDescription and leaves
|
|
498
|
+
the TIFF tags at their defaults, so a tag-only reader calls a perfectly
|
|
499
|
+
calibrated image uncalibrated -- and it is the only source that can give
|
|
500
|
+
z for a sequence of single planes.
|
|
501
|
+
"""
|
|
502
|
+
tifffile = pytest.importorskip("tifffile")
|
|
503
|
+
from patchworks.plugins.ome_zarr import _tiff_pixel_size
|
|
504
|
+
|
|
505
|
+
path = tmp_path / "ome.tif"
|
|
506
|
+
tifffile.imwrite(
|
|
507
|
+
path,
|
|
508
|
+
np.zeros((8, 8), "uint16"),
|
|
509
|
+
description=_ome_xml(0.325, 0.325, 1.5),
|
|
510
|
+
)
|
|
511
|
+
assert _tiff_pixel_size(str(path)) == pytest.approx(
|
|
512
|
+
{"z": 1.5, "y": 0.325, "x": 0.325}
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# mm is converted, not taken at face value
|
|
516
|
+
mm = tmp_path / "mm.tif"
|
|
517
|
+
tifffile.imwrite(
|
|
518
|
+
mm,
|
|
519
|
+
np.zeros((8, 8), "uint16"),
|
|
520
|
+
description=_ome_xml(0.001, 0.001, 0.002, "mm"),
|
|
521
|
+
)
|
|
522
|
+
assert _tiff_pixel_size(str(mm)) == pytest.approx(
|
|
523
|
+
{"z": 2.0, "y": 1.0, "x": 1.0}
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def test_ome_xml_wins_over_the_resolution_tags(tmp_path):
|
|
528
|
+
"""The explicit source wins per axis when a file carries both."""
|
|
529
|
+
tifffile = pytest.importorskip("tifffile")
|
|
530
|
+
from patchworks.plugins.ome_zarr import _tiff_pixel_size
|
|
531
|
+
|
|
532
|
+
path = tmp_path / "both.tif"
|
|
533
|
+
tifffile.imwrite(
|
|
534
|
+
path,
|
|
535
|
+
np.zeros((8, 8), "uint16"),
|
|
536
|
+
description=_ome_xml(0.325, 0.325, 1.5),
|
|
537
|
+
resolution=(20000.0, 20000.0), # would say 0.5 µm
|
|
538
|
+
resolutionunit="CENTIMETER",
|
|
539
|
+
)
|
|
540
|
+
got = _tiff_pixel_size(str(path))
|
|
541
|
+
assert got["x"] == pytest.approx(0.325), "OME-XML must win over the tag"
|
|
542
|
+
assert got["z"] == pytest.approx(1.5)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_uncalibrated_tiff_reports_nothing(tmp_path):
|
|
546
|
+
"""No metadata gives an empty dict rather than an invented default."""
|
|
547
|
+
tifffile = pytest.importorskip("tifffile")
|
|
548
|
+
from patchworks.plugins.ome_zarr import _tiff_pixel_size
|
|
549
|
+
|
|
550
|
+
path = tmp_path / "bare.tif"
|
|
551
|
+
tifffile.imwrite(path, np.zeros((8, 8), "uint16"))
|
|
552
|
+
assert _tiff_pixel_size(str(path)) == {}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_many_chunks_suggests_sharding(caplog):
|
|
556
|
+
"""A coarse source forces fine chunks; fine chunks mean many files.
|
|
557
|
+
|
|
558
|
+
That trade-off is invisible until a shared filesystem is carrying a
|
|
559
|
+
million of them, so it has to be said at write time -- with the remedy.
|
|
560
|
+
"""
|
|
561
|
+
import logging
|
|
562
|
+
|
|
563
|
+
from patchworks.plugins.ome_zarr import _warn_if_many_chunks
|
|
564
|
+
|
|
565
|
+
real = (4, 126, 45961, 42072) # ~950k chunks at (1, 1, 1024, 1024)
|
|
566
|
+
with caplog.at_level(logging.WARNING, logger="patchworks.plugins.ome_zarr"):
|
|
567
|
+
_warn_if_many_chunks(real, (1, 1, 1024, 1024), shard=False)
|
|
568
|
+
assert "shard: true" in caplog.text
|
|
569
|
+
assert "952,560" in caplog.text
|
|
570
|
+
|
|
571
|
+
# Sharding already asked for, or a modest chunk count: nothing to say.
|
|
572
|
+
caplog.clear()
|
|
573
|
+
with caplog.at_level(logging.WARNING, logger="patchworks.plugins.ome_zarr"):
|
|
574
|
+
_warn_if_many_chunks(real, (1, 1, 1024, 1024), shard=True)
|
|
575
|
+
_warn_if_many_chunks(
|
|
576
|
+
(4, 126, 1024, 1024), (1, 16, 1024, 1024), shard=False
|
|
577
|
+
)
|
|
578
|
+
assert caplog.text == ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|