- support some sequences of files in tifread
PyTest / pytest (3.10) (push) Successful in 15s
PyTest / pytest (3.11) (push) Successful in 12s
PyTest / pytest (3.12) (push) Successful in 12s
PyTest / pytest (3.13) (push) Successful in 12s

This commit is contained in:
w.pomp
2026-07-27 18:00:32 +02:00
parent d9806244d3
commit e32b7ea810
3 changed files with 83 additions and 28 deletions
+1 -1
View File
@@ -192,4 +192,4 @@ class Reader(AbstractReader, ABC):
}
def __frame__(self, c=0, z=0, t=0):
return tifffile.imread(self.path / self.filedict[(c, z, t)])
return tifffile.imread(self.path / self.filedict[c, z, t])
+76 -21
View File
@@ -1,16 +1,18 @@
import re
import warnings
from abc import ABC
from contextlib import ExitStack
from functools import cached_property
from itertools import product
from pathlib import Path
from typing import Any
import numpy as np
import tifffile
import yaml
from ome_types import from_xml, model
from ome_types import OME, from_xml, model
from .. import AbstractReader, try_default
from .. import AbstractReader, Shape, try_default
class Reader(AbstractReader, ABC):
@@ -18,7 +20,7 @@ class Reader(AbstractReader, ABC):
do_not_pickle = "reader"
@staticmethod
def _can_open(path):
def _can_open(path) -> bool:
if isinstance(path, Path) and path.suffix in (".tif", ".tiff"):
with tifffile.TiffFile(path) as tif:
return tif.is_imagej and tif.pages[-1]._nextifd() == 0 # noqa
@@ -26,13 +28,13 @@ class Reader(AbstractReader, ABC):
return False
@cached_property
def metadata(self):
def metadata(self) -> dict[str, Any]:
return {
key: try_default(yaml.safe_load, value, value) if isinstance(value, str) else value
for key, value in self.reader.imagej_metadata.items()
}
def get_ome(self):
def get_ome(self) -> OME:
if self.reader.is_ome:
pos_number_pat = re.compile(r"\d+")
@@ -88,13 +90,17 @@ class Reader(AbstractReader, ABC):
page = self.reader.pages[0]
size_y = page.imagelength
size_x = page.imagewidth
if self.p_ndim == 3:
size_c = page.samplesperpixel
size_t = self.metadata.get("frames", 1) # // C
else:
size_c = self.metadata.get("channels", 1)
size_t = self.metadata.get("frames", 1)
size_z = self.metadata.get("slices", 1)
max_c = max_z = max_t = 0
if self.file_dict:
for ci, zi, ti in self.file_dict.keys():
max_c = max(max_c, ci)
max_z = max(max_z, zi)
max_t = max(max_t, ti)
size_c: int = self.mod_shape["c"] * (max_c + 1) # type: ignore
size_z: int = self.mod_shape["z"] * (max_z + 1) # type: ignore
size_t: int = self.mod_shape["t"] * (max_t + 1) # type: ignore
if 282 in page.tags and 296 in page.tags and page.tags[296].value == 1:
f = page.tags[282].value
pxsize = f[1] / f[0]
@@ -149,7 +155,7 @@ class Reader(AbstractReader, ABC):
return ome
def open(self):
def open(self) -> None:
if self.series != 0:
raise FileNotFoundError(
f"Series {self.series} not found in {self.path}. Tifread only supports one series."
@@ -161,19 +167,68 @@ class Reader(AbstractReader, ABC):
self.p_transpose = [i for i in [page.axes.find(j) for j in "SYX"] if i >= 0] # noqa
else:
self.p_transpose = [i for i in [page.axes.find(j) for j in "YX"] if i >= 0] # noqa
self.file_dict = {} # noqa
pat = re.compile(r"^.*?((?:_[czt]\d+)*)$", re.IGNORECASE)
if (m := pat.match(self.path.stem)) is not None:
m = m.group(1)
cpat = re.compile(r"_c(\d+)", re.IGNORECASE)
zpat = re.compile(r"_z(\d+)", re.IGNORECASE)
tpat = re.compile(r"_t(\d+)", re.IGNORECASE)
for file in self.path.parent.glob(
re.sub(m, re.sub(r"\d+", "*", m), self.path.stem, re.IGNORECASE) + self.path.suffix
):
m = pat.match(file.stem)
if m:
m = m.group(0)
c = cpat.findall(m)
z = zpat.findall(m)
t = tpat.findall(m)
c = int(c[0]) if len(c) else 0
z = int(z[0]) if len(z) else 0
t = int(t[0]) if len(t) else 0
self.file_dict[c, z, t] = file.name
def close(self):
page = self.reader.pages[0]
if self.p_ndim == 3:
mod_c = page.samplesperpixel
mod_t = self.metadata.get("frames", 1) # // C
else:
mod_c = self.metadata.get("channels", 1)
mod_t = self.metadata.get("frames", 1)
mod_z = self.metadata.get("slices")
if mod_z is None:
if self.metadata.get("spacing") is None:
mod_t = len(self.reader.pages)
else:
mod_z = len(self.reader.pages)
self.mod_shape = Shape((mod_c, mod_z, mod_t), "czt") # noqa
def close(self) -> None:
self.reader.close()
def __frame__(self, c: int, z: int, t: int):
def __frame__(self, c: int, z: int, t: int) -> np.ndarray:
with ExitStack() as stack:
if self.file_dict:
filename = self.file_dict[
c - c % self.mod_shape["c"], z - z % self.mod_shape["z"], t - t % self.mod_shape["t"]
]
if self.reader.filename == filename:
reader = self.reader
else:
reader = stack.enter_context(tifffile.TiffFile(self.path.parent / filename))
else:
reader = self.reader
dimension_order = self.ome.images[0].pixels.dimension_order.value
if self.p_ndim == 3:
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "zt"])
ct = {"z": z, "t": t}
n = sum([ct[ax] * np.prod(self.base_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(self.reader.asarray(int(n)), self.p_transpose)[int(c)]
ct = {"z": z % self.mod_shape["z"], "t": t % self.mod_shape["t"]}
n = sum([ct[ax] * np.prod(self.mod_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(reader.asarray(int(n)), self.p_transpose)[int(c % self.mod_shape["c"])]
else:
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "czt"])
czt = {"c": c, "z": z, "t": t}
n = sum([czt[ax] * np.prod(self.base_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(self.reader.asarray(int(n)), self.p_transpose)
czt = {"c": c % self.mod_shape["c"], "z": z % self.mod_shape["z"], "t": t % self.mod_shape["t"]}
n = sum([czt[ax] * np.prod(self.mod_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(reader.asarray(int(n)), self.p_transpose)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "ndbioimage"
version = "2026.7.0"
version = "2026.7.1"
description = "Bio image reading, metadata and some affine registration."
authors = [
{ name = "W. Pomp", email = "w.pomp@nki.nl" }