- 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): 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 re
import warnings import warnings
from abc import ABC from abc import ABC
from contextlib import ExitStack
from functools import cached_property from functools import cached_property
from itertools import product from itertools import product
from pathlib import Path from pathlib import Path
from typing import Any
import numpy as np import numpy as np
import tifffile import tifffile
import yaml 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): class Reader(AbstractReader, ABC):
@@ -18,7 +20,7 @@ class Reader(AbstractReader, ABC):
do_not_pickle = "reader" do_not_pickle = "reader"
@staticmethod @staticmethod
def _can_open(path): def _can_open(path) -> bool:
if isinstance(path, Path) and path.suffix in (".tif", ".tiff"): if isinstance(path, Path) and path.suffix in (".tif", ".tiff"):
with tifffile.TiffFile(path) as tif: with tifffile.TiffFile(path) as tif:
return tif.is_imagej and tif.pages[-1]._nextifd() == 0 # noqa return tif.is_imagej and tif.pages[-1]._nextifd() == 0 # noqa
@@ -26,13 +28,13 @@ class Reader(AbstractReader, ABC):
return False return False
@cached_property @cached_property
def metadata(self): def metadata(self) -> dict[str, Any]:
return { return {
key: try_default(yaml.safe_load, value, value) if isinstance(value, str) else value key: try_default(yaml.safe_load, value, value) if isinstance(value, str) else value
for key, value in self.reader.imagej_metadata.items() for key, value in self.reader.imagej_metadata.items()
} }
def get_ome(self): def get_ome(self) -> OME:
if self.reader.is_ome: if self.reader.is_ome:
pos_number_pat = re.compile(r"\d+") pos_number_pat = re.compile(r"\d+")
@@ -88,13 +90,17 @@ class Reader(AbstractReader, ABC):
page = self.reader.pages[0] page = self.reader.pages[0]
size_y = page.imagelength size_y = page.imagelength
size_x = page.imagewidth size_x = page.imagewidth
if self.p_ndim == 3: max_c = max_z = max_t = 0
size_c = page.samplesperpixel if self.file_dict:
size_t = self.metadata.get("frames", 1) # // C for ci, zi, ti in self.file_dict.keys():
else: max_c = max(max_c, ci)
size_c = self.metadata.get("channels", 1) max_z = max(max_z, zi)
size_t = self.metadata.get("frames", 1) max_t = max(max_t, ti)
size_z = self.metadata.get("slices", 1)
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: if 282 in page.tags and 296 in page.tags and page.tags[296].value == 1:
f = page.tags[282].value f = page.tags[282].value
pxsize = f[1] / f[0] pxsize = f[1] / f[0]
@@ -149,7 +155,7 @@ class Reader(AbstractReader, ABC):
return ome return ome
def open(self): def open(self) -> None:
if self.series != 0: if self.series != 0:
raise FileNotFoundError( raise FileNotFoundError(
f"Series {self.series} not found in {self.path}. Tifread only supports one series." 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 self.p_transpose = [i for i in [page.axes.find(j) for j in "SYX"] if i >= 0] # noqa
else: else:
self.p_transpose = [i for i in [page.axes.find(j) for j in "YX"] if i >= 0] # noqa 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() 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 dimension_order = self.ome.images[0].pixels.dimension_order.value
if self.p_ndim == 3: if self.p_ndim == 3:
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "zt"]) axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "zt"])
ct = {"z": z, "t": t} ct = {"z": z % self.mod_shape["z"], "t": t % self.mod_shape["t"]}
n = sum([ct[ax] * np.prod(self.base_shape[axes[:i]]) for i, ax in enumerate(axes)]) n = sum([ct[ax] * np.prod(self.mod_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(self.reader.asarray(int(n)), self.p_transpose)[int(c)] return np.transpose(reader.asarray(int(n)), self.p_transpose)[int(c % self.mod_shape["c"])]
else: else:
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "czt"]) axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "czt"])
czt = {"c": c, "z": z, "t": t} 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.base_shape[axes[:i]]) for i, ax in enumerate(axes)]) n = sum([czt[ax] * np.prod(self.mod_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(self.reader.asarray(int(n)), self.p_transpose) return np.transpose(reader.asarray(int(n)), self.p_transpose)
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "ndbioimage" name = "ndbioimage"
version = "2026.7.0" version = "2026.7.1"
description = "Bio image reading, metadata and some affine registration." description = "Bio image reading, metadata and some affine registration."
authors = [ authors = [
{ name = "W. Pomp", email = "w.pomp@nki.nl" } { name = "W. Pomp", email = "w.pomp@nki.nl" }