diff --git a/ndbioimage/readers/seqread.py b/ndbioimage/readers/seqread.py index da05430..28a6688 100644 --- a/ndbioimage/readers/seqread.py +++ b/ndbioimage/readers/seqread.py @@ -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]) diff --git a/ndbioimage/readers/tifread.py b/ndbioimage/readers/tifread.py index 6933db8..b990695 100644 --- a/ndbioimage/readers/tifread.py +++ b/ndbioimage/readers/tifread.py @@ -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): - 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)] - 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) + 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 % 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 % 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) diff --git a/pyproject.toml b/pyproject.toml index 12cdbb4..c83ca8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" }