From 6041cda475c6dfe750d6252880d28009e517cdfe Mon Sep 17 00:00:00 2001 From: "w.pomp" Date: Tue, 15 Sep 2026 16:13:51 +0200 Subject: [PATCH] - more transforms --- Cargo.toml | 11 +- py/ndbioimage/__init__.py | 9 +- py/ndbioimage/ndbioimage_rs.pyi | 144 +- py/ndbioimage/transform.txt | 7 - py/ndbioimage/transforms.py | 572 -------- pyproject.toml | 4 +- src/error.rs | 38 +- src/py.rs | 2156 +----------------------------- src/py/imread.rs | 2217 +++++++++++++++++++++++++++++++ src/py/transforms.rs | 675 ++++++++++ src/readers.rs | 23 +- src/readers/bioformats_java.rs | 25 +- src/readers/bioformats_rust.rs | 59 +- src/readers/czi.rs | 39 + src/readers/tiffseq.rs | 18 +- src/tiffwrite.rs | 1 + src/transforms.rs | 169 ++- src/view.rs | 10 +- 18 files changed, 3383 insertions(+), 2794 deletions(-) delete mode 100644 py/ndbioimage/transform.txt delete mode 100644 py/ndbioimage/transforms.py create mode 100644 src/py/imread.rs create mode 100644 src/py/transforms.rs diff --git a/Cargo.toml b/Cargo.toml index fd8d7d1..db6e18b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] bioformats = { version = "0.1", optional = true } clap = { version = "4", features = ["derive"] } -color-eyre = { version = "0.6", optional = true } console = { version = "0.16", optional = true } downloader = { version = "0.2", optional = true, default-features = false, features = ["rustls-tls"] } ffmpeg-sidecar = { version = "2", optional = true } @@ -38,12 +37,11 @@ ome-metadata = "0.5" ordered-float = { version = "5", optional = true } phf = { version = "0.14", features = ["macros"] } postcard = { version = "1", features = ["use-std"], optional = true } -pyo3 = { version = "0.29", features = ["abi3-py310", "eyre", "anyhow", "generate-import-lib"], optional = true } +pyo3 = { version = "0.29", features = ["abi3-py310", "anyhow", "generate-import-lib"], optional = true } pyo3-stub-gen = { version = "0.23", optional = true } rayon = { version = "1", optional = true } regex = "1" serde = { version = "1", features = ["rc", "derive"] } -serde_yaml = { version = "0.9", optional = true } serde_with = "3" strum = { version = "0.28", features = ["derive"] } thiserror = "2" @@ -52,6 +50,7 @@ tiffwrite = { version = "2026.6.0", optional = true } tokio = { version = "1", features = ["rt", "rt-multi-thread"], optional = true } thread_local = { version = "1", optional = true } xmltree = { version = "0.12", optional = true } +yaml_serde = { version = "0.10", optional = true } [dev-dependencies] rayon = "1" @@ -67,15 +66,15 @@ toml = "1" default = ["bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "movie", "tiffwrite"] all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tiffseq", "tiffwrite", "tiff", "transforms"] gpl-formats = [] -python = ["dep:pyo3", "dep:numpy", "dep:color-eyre", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"] +python = ["dep:pyo3", "dep:numpy", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"] czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"] bioformats_rust = ["dep:bioformats", "dep:thread_local"] bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"] tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"] -tiffseq = ["dep:tiff", "dep:serde_yaml"] +tiffseq = ["dep:tiff", "dep:yaml_serde"] tiff = ["dep:tiff", "dep:thread_local"] movie = ["dep:ffmpeg-sidecar", "dep:tokio", "dep:ordered-float", "dep:indicatif", "dep:console"] -transforms = ["dep:image-registration"] +transforms = ["dep:image-registration", "dep:yaml_serde"] [package.metadata.docs.rs] no-default-features = true diff --git a/py/ndbioimage/__init__.py b/py/ndbioimage/__init__.py index 7ba167b..1aab780 100755 --- a/py/ndbioimage/__init__.py +++ b/py/ndbioimage/__init__.py @@ -13,9 +13,8 @@ from numpy.typing import ArrayLike os.environ["RUST_BACKTRACE"] = "full" os.environ["COLORBT_SHOW_HIDDEN"] = "1" -from . import ndbioimage_rs as rs # noqa -from .ndbioimage_rs import Imread -from .transforms import Transform, Transforms # noqa: F401 +from . import ndbioimage_rs as rs +from .ndbioimage_rs import Imread, Transform, Transforms try: from .ndbioimage_rs import batch_to_tiff @@ -63,11 +62,9 @@ def ndbioimage_generate_stub(): rs.generate_stub(str(path)) # noqa else: raise ModuleNotFoundError(str(path / "py" / "ndbioimage" / "__init__.py")) - (path / "py" / "ndbioimage" / "__init__.pyi").unlink(missing_ok=True) - (path / "py" / "ndbioimage" / "ndbioimage_rs" / "__init__.pyi").rename( + (path / "py" / "ndbioimage" / "__init__.pyi").rename( path / "py" / "ndbioimage" / "ndbioimage_rs.pyi" ) - (path / "py" / "ndbioimage" / "ndbioimage_rs").rmdir() R = TypeVar("R") diff --git a/py/ndbioimage/ndbioimage_rs.pyi b/py/ndbioimage/ndbioimage_rs.pyi index 3b74d60..e5531cf 100644 --- a/py/ndbioimage/ndbioimage_rs.pyi +++ b/py/ndbioimage/ndbioimage_rs.pyi @@ -12,6 +12,8 @@ import numpy.typing __all__ = [ "Imread", "Shape", + "Transform", + "Transforms", "batch_to_tiff", "main", ] @@ -52,9 +54,9 @@ class Imread: the name of the reader used to open the file """ @property - def transform(self) -> None: + def transform(self) -> Transforms: r""" - get the transformation matrix (not yet implemented) + get the transformation """ @property def path(self) -> pathlib.Path: @@ -203,10 +205,27 @@ class Imread: drift: builtins.bool = False, file: typing.Optional[typing.Any] = None, bead_files: typing.Optional[typing.Any] = None, + main_channel: typing.Optional[builtins.int] = None, + default_transform: typing.Optional[typing.Sequence[builtins.float]] = None, ) -> Imread: r""" return a new view with transformations applied (channel alignment, drift correction) """ + def set_transform(self, transform: Transforms) -> None: + r""" + set the transformation + """ + def load_transform_from_yaml( + self, path: builtins.str | os.PathLike | pathlib.Path + ) -> None: ... + def calculate_channel_transforms_2d( + self, main_channel: builtins.int + ) -> builtins.list[Transform]: ... + def calculate_channel_transforms_3d( + self, main_channel: builtins.int + ) -> builtins.list[Transform]: ... + def calculate_drift_transform_2d(self) -> builtins.list[Transform]: ... + def calculate_drift_transform_3d(self) -> builtins.list[Transform]: ... def squeeze(self) -> numpy.ndarray | int | float: ... def close(self) -> None: r""" @@ -672,6 +691,127 @@ class Shape: convert shape to a list of dimension sizes in order """ +class Transform: + @property + def parameters(self) -> builtins.list[builtins.float]: ... + @parameters.setter + def parameters(self, value: typing.Sequence[builtins.float]) -> None: ... + @property + def dparameters(self) -> builtins.list[builtins.float]: ... + @dparameters.setter + def dparameters(self, value: typing.Sequence[builtins.float]) -> None: ... + @property + def center(self) -> builtins.list[builtins.float]: ... + @center.setter + def center(self, value: typing.Sequence[builtins.float]) -> None: ... + @property + def shape(self) -> builtins.list[builtins.int]: ... + @shape.setter + def shape(self, value: typing.Sequence[builtins.int]) -> None: ... + @property + def ndim(self) -> builtins.int: ... + @property + def matrix(self) -> numpy.typing.NDArray[numpy.float64]: ... + @matrix.setter + def matrix(self, value: numpy.typing.ArrayLike) -> None: ... + @property + def dmatrix(self) -> numpy.typing.NDArray[numpy.float64]: ... + @dmatrix.setter + def dmatrix(self, value: numpy.typing.ArrayLike) -> None: ... + @property + def inverse(self) -> Transform: ... + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __new__( + cls, + parameters: typing.Sequence[builtins.float], + shape: typing.Sequence[builtins.int], + center: typing.Optional[typing.Sequence[builtins.float]] = None, + ) -> Transform: ... + def __getnewargs__( + self, + ) -> tuple[ + builtins.list[builtins.float], + builtins.list[builtins.int], + typing.Optional[builtins.list[builtins.float]], + ]: ... + def __getstate__(self) -> builtins.list[builtins.float]: ... + def __setstate__(self, state: typing.Sequence[builtins.float]) -> None: ... + def __add__(self, other: Transform) -> Transform: ... + def __radd__(self, other: Transform) -> Transform: ... + def __sub__(self, other: Transform) -> Transform: ... + def __rsub__(self, other: Transform) -> Transform: ... + def __mul__(self, other: Transform | float) -> Transform: ... + def __rmul__(self, other: Transform | float) -> Transform: ... + def __truediv__(self, other: builtins.float) -> Transform: ... + def adapt( + self, + center: typing.Sequence[builtins.float], + shape: typing.Sequence[builtins.int], + ) -> None: ... + @staticmethod + def from_scaling(scaling: typing.Sequence[builtins.float]) -> Transform: ... + @staticmethod + def from_translation(translation: typing.Sequence[builtins.float]) -> Transform: ... + @staticmethod + def from_rotation( + theta: builtins.float, center: typing.Sequence[builtins.float] + ) -> Transform: ... + def with_scaling(self, scaling: typing.Sequence[builtins.float]) -> Transform: ... + def with_translation( + self, translation: typing.Sequence[builtins.float] + ) -> Transform: ... + def with_rotation( + self, theta: builtins.float, center: typing.Sequence[builtins.float] + ) -> Transform: ... + def interpolate( + self, order: builtins.int, image: numpy.typing.ArrayLike + ) -> numpy.typing.NDArray[numpy.float64]: ... + def interpolate_par( + self, order: builtins.int, image: numpy.typing.ArrayLike + ) -> numpy.typing.NDArray[numpy.float64]: ... + def is_unity(self) -> builtins.bool: ... + def transform_point( + self, point: numpy.typing.ArrayLike + ) -> numpy.typing.NDArray[numpy.float64]: ... + def transform_points( + self, points: numpy.typing.ArrayLike + ) -> numpy.typing.NDArray[numpy.float64]: ... + @staticmethod + def register( + fixed: numpy.typing.ArrayLike, + moving: numpy.typing.ArrayLike, + fixed_mu: typing.Sequence[typing.Optional[builtins.float]], + initial_guess: typing.Optional[typing.Sequence[builtins.float]] = None, + ) -> Transform: ... + @staticmethod + def register_affine( + fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike + ) -> Transform: ... + @staticmethod + def register_translation( + fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike + ) -> Transform: ... + +class Transforms: + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __getstate__(self) -> builtins.list[builtins.int]: ... + def __setstate__(self, state: typing.Sequence[builtins.int]) -> None: ... + @staticmethod + def load(path: builtins.str | os.PathLike | pathlib.Path) -> Transforms: ... + def save(self, path: builtins.str | os.PathLike | pathlib.Path) -> None: ... + @staticmethod + def calculate_channel_transforms_2d( + bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path], + main_channel: builtins.int, + default_transform: typing.Optional[Transform], + ) -> builtins.list[Transform]: ... + @staticmethod + def calculate_channel_transforms_3d( + bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path], + main_channel: builtins.int, + default_transform: typing.Optional[Transform], + ) -> builtins.list[Transform]: ... + def batch_to_tiff( files_in: typing.Sequence[builtins.str | os.PathLike | pathlib.Path], files_out: typing.Sequence[builtins.str | os.PathLike | pathlib.Path], diff --git a/py/ndbioimage/transform.txt b/py/ndbioimage/transform.txt deleted file mode 100644 index a177cc0..0000000 --- a/py/ndbioimage/transform.txt +++ /dev/null @@ -1,7 +0,0 @@ -#Insight Transform File V1.0 -#Transform 0 -Transform: CompositeTransform_double_2_2 -#Transform 1 -Transform: AffineTransform_double_2_2 -Parameters: 1 0 0 1 0 0 -FixedParameters: 255.5 255.5 diff --git a/py/ndbioimage/transforms.py b/py/ndbioimage/transforms.py deleted file mode 100644 index 38a0cff..0000000 --- a/py/ndbioimage/transforms.py +++ /dev/null @@ -1,572 +0,0 @@ -import warnings -from copy import deepcopy -from pathlib import Path - -import numpy as np -import yaml -from parfor import Chunks, pmap -from skimage import filters -from tiffwrite import IJTiffFile -from tqdm.auto import tqdm - -try: - # best if SimpleElastix is installed: https://simpleelastix.readthedocs.io/GettingStarted.html - import SimpleITK as sitk # noqa -except ImportError: - sitk = None - -try: - from pandas import DataFrame, Series, concat -except ImportError: - DataFrame, Series, concat = None, None, None - - -if hasattr(yaml, "full_load"): - yamlload = yaml.full_load -else: - yamlload = yaml.load - - -class Transforms(dict): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.default = Transform() - - @classmethod - def from_file(cls, file, C=True, T=True): - with open(Path(file).with_suffix(".yml")) as f: - return cls.from_dict(yamlload(f), C, T) - - @classmethod - def from_dict(cls, d, C=True, T=True): - new = cls() - for key, value in d.items(): - if isinstance(key, str) and C: - new[key.replace(r"\:", ":").replace("\\\\", "\\")] = ( - Transform.from_dict(value) - ) - elif T: - new[key] = Transform.from_dict(value) - return new - - @classmethod - def from_shifts(cls, shifts): - new = cls() - for key, shift in shifts.items(): - new[key] = Transform.from_shift(shift) - return new - - def __mul__(self, other): - new = Transforms() - if isinstance(other, Transforms): - for key0, value0 in self.items(): - for key1, value1 in other.items(): - new[key0 + key1] = value0 * value1 - return new - elif other is None: - return self - else: - for key in self.keys(): - new[key] = self[key] * other - return new - - def asdict(self): - return { - key.replace("\\", "\\\\").replace(":", r"\:") - if isinstance(key, str) - else key: value.asdict() - for key, value in self.items() - } - - def __getitem__(self, item): - return ( - np.prod([self[i] for i in item[::-1]]) - if isinstance(item, tuple) - else super().__getitem__(item) - ) - - def __missing__(self, key): - return self.default - - def __getstate__(self): - return self.__dict__ - - def __setstate__(self, state): - self.__dict__.update(state) - - def __hash__(self): - return hash(frozenset((*self.__dict__.items(), *self.items()))) - - def save(self, file): - with open(Path(file).with_suffix(".yml"), "w") as f: - yaml.safe_dump(self.asdict(), f, default_flow_style=None) - - def copy(self): - return deepcopy(self) - - def adapt(self, origin, shape, channel_names): - def key_map(a, b): - def fun(b, key_a): - for key_b in b: - if key_b in key_a or key_a in key_b: - return key_a, key_b - - return {n[0]: n[1] for key_a in a if (n := fun(b, key_a))} - - for value in self.values(): - value.adapt(origin, shape) - self.default.adapt(origin, shape) - transform_channels = {key for key in self.keys() if isinstance(key, str)} - if set(channel_names) - transform_channels: - mapping = key_map(channel_names, transform_channels) - warnings.warn( - f"The image file and the transform do not have the same channels," - f" creating a mapping: {mapping}" - ) - for key_im, key_t in mapping.items(): - self[key_im] = self[key_t] - - @property - def inverse(self): - # TODO: check for C@T - inverse = self.copy() - for key, value in self.items(): - inverse[key] = value.inverse - return inverse - - def coords_pandas(self, array, channel_names, columns=None): - if isinstance(array, DataFrame): - return concat( - [ - self.coords_pandas(row, channel_names, columns) - for _, row in array.iterrows() - ], - axis=1, - ).T - elif isinstance(array, Series): - key = [] - if "C" in array: - key.append(channel_names[int(array["C"])]) - if "T" in array: - key.append(int(array["T"])) - return self[tuple(key)].coords(array, columns) - else: - raise TypeError("Not a pandas DataFrame or Series.") - - def with_beads(self, cyllens, bead_files): - assert len(bead_files) > 0, ( - "At least one file is needed to calculate the registration." - ) - transforms = [ - self.calculate_channel_transforms(file, cyllens) for file in bead_files - ] - for key in {key for transform in transforms for key in transform.keys()}: - new_transforms = [ - transform[key] for transform in transforms if key in transform - ] - if len(new_transforms) == 1: - self[key] = new_transforms[0] - else: - self[key] = Transform() - self[key].parameters = np.mean( - [t.parameters for t in new_transforms], 0 - ) - self[key].dparameters = ( - np.std([t.parameters for t in new_transforms], 0) - / np.sqrt(len(new_transforms)) - ).tolist() - return self - - @staticmethod - def get_bead_files(path): - from . import Imread - - files = [] - for file in path.iterdir(): - if file.name.lower().startswith("beads"): - try: - with Imread(file): - files.append(file) - except Exception: - pass - files = sorted(files) - if not files: - raise Exception("No bead file found!") - checked_files = [] - for file in files: - try: - if file.is_dir(): - file /= "Pos0" - with Imread(file): # check for errors opening the file - checked_files.append(file) - except (Exception,): - continue - if not checked_files: - raise Exception("No bead file found!") - return checked_files - - @staticmethod - def calculate_channel_transforms(bead_file, cyllens): - """When no channel is not transformed by a cylindrical lens, assume that the image is scaled by a factor 1.162 - in the horizontal direction""" - from . import Imread - - with Imread(bead_file, axes="zcyx") as im: # noqa - max_ims = im.max("z") - goodch = [c for c, max_im in enumerate(max_ims) if not im.is_noise(max_im)] - if not goodch: - goodch = list(range(len(max_ims))) - untransformed = [ - c - for c in range(im.shape["c"]) - if cyllens[im.detector[c]].lower() == "none" - ] - - good_and_untrans = sorted(set(goodch) & set(untransformed)) - if good_and_untrans: - masterch = good_and_untrans[0] - else: - masterch = goodch[0] - transform = Transform() - if not good_and_untrans: - matrix = transform.matrix - matrix[0, 0] = 0.86 - transform.matrix = matrix - transforms = Transforms() - for c in tqdm(goodch, desc="Calculating channel transforms"): # noqa - if c == masterch: - transforms[im.channel_names[c]] = transform - else: - transforms[im.channel_names[c]] = ( - Transform.register(max_ims[masterch], max_ims[c]) * transform - ) - return transforms - - @staticmethod - def save_channel_transform_tiff(bead_files, tiffile): - from . import Imread - - n_channels = 0 - for file in bead_files: - with Imread(file) as im: - n_channels = max(n_channels, im.shape["c"]) - with IJTiffFile(tiffile) as tif: - for t, file in enumerate(bead_files): - with Imread(file) as im: - with Imread(file).with_transform() as jm: - for c in range(im.shape["c"]): - tif.save( - np.hstack( - (im(c=c, t=0).max("z"), jm(c=c, t=0).max("z")) - ), - c, - 0, - t, - ) - - def with_drift(self, im): - """Calculate shifts relative to the first frame - divide the sequence into groups, - compare each frame to the frame in the middle of the group and compare these middle frames to each other - """ - im = im.transpose("tzycx") - t_groups = [ - list(chunk) - for chunk in Chunks( - range(im.shape["t"]), size=round(np.sqrt(im.shape["t"])) - ) - ] - t_keys = [int(np.round(np.mean(t_group))) for t_group in t_groups] - t_pairs = [ - (int(np.round(np.mean(t_group))), frame) - for t_group in t_groups - for frame in t_group - ] - t_pairs.extend(zip(t_keys, t_keys[1:])) - fmaxz_keys = { - t_key: filters.gaussian(im[t_key].max("z"), 5) for t_key in t_keys - } - - def fun(t_key_t, im, fmaxz_keys): - t_key, t = t_key_t - if t_key == t: - return 0, 0 - else: - fmaxz = filters.gaussian(im[t].max("z"), 5) - return Transform.register( - fmaxz_keys[t_key], fmaxz, "translation" - ).parameters[4:] - - shifts = np.array( - pmap(fun, t_pairs, (im, fmaxz_keys), desc="Calculating image shifts.") - ) - shift_keys_cum = np.zeros(2) - for shift_keys, t_group in zip( - np.vstack((-shifts[0], shifts[im.shape["t"] :])), t_groups - ): - shift_keys_cum += shift_keys - shifts[t_group] += shift_keys_cum - - for i, shift in enumerate(shifts[: im.shape["t"]]): - self[i] = Transform.from_shift(shift) - return self - - -class Transform: - def __init__(self): - if sitk is None: - self.transform = None - else: - self.transform = sitk.ReadTransform( - str(Path(__file__).parent / "transform.txt") - ) - self.dparameters = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - self.shape = [512.0, 512.0] - self.origin = [255.5, 255.5] - self._last, self._inverse = None, None - - def __reduce__(self): - return self.from_dict, (self.asdict(),) - - def __repr__(self): - return self.asdict().__repr__() - - def __str__(self): - return self.asdict().__str__() - - @classmethod - def register(cls, fix, mov, kind=None): - """kind: 'affine', 'translation', 'rigid'""" - if sitk is None: - raise ImportError( - "SimpleElastix is not installed: " - "https://simpleelastix.readthedocs.io/GettingStarted.html" - ) - new = cls() - kind = kind or "affine" - new.shape = fix.shape - fix, mov = new.cast_image(fix), new.cast_image(mov) - # TODO: implement RigidTransform - tfilter = sitk.ElastixImageFilter() - tfilter.LogToConsoleOff() - tfilter.SetFixedImage(fix) - tfilter.SetMovingImage(mov) - tfilter.SetParameterMap(sitk.GetDefaultParameterMap(kind)) - tfilter.Execute() - transform = tfilter.GetTransformParameterMap()[0] - if kind == "affine": - new.parameters = [float(t) for t in transform["TransformParameters"]] - new.shape = [float(t) for t in transform["Size"]] - new.origin = [float(t) for t in transform["CenterOfRotationPoint"]] - elif kind == "translation": - new.parameters = [1.0, 0.0, 0.0, 1.0] + [ - float(t) for t in transform["TransformParameters"] - ] - new.shape = [float(t) for t in transform["Size"]] - new.origin = [(t - 1) / 2 for t in new.shape] - else: - raise NotImplementedError(f"{kind} tranforms not implemented (yet)") - new.dparameters = 6 * [np.nan] - return new - - @classmethod - def from_shift(cls, shift): - return cls.from_array(np.array(((1, 0, shift[0]), (0, 1, shift[1]), (0, 0, 1)))) - - @classmethod - def from_array(cls, array): - new = cls() - new.matrix = array - return new - - @classmethod - def from_file(cls, file): - with open(Path(file).with_suffix(".yml")) as f: - return cls.from_dict(yamlload(f)) - - @classmethod - def from_dict(cls, d): - new = cls() - new.origin = ( - None - if d["CenterOfRotationPoint"] is None - else [float(i) for i in d["CenterOfRotationPoint"]] - ) - new.parameters = ( - (1.0, 0.0, 0.0, 1.0, 0.0, 0.0) - if d["TransformParameters"] is None - else [float(i) for i in d["TransformParameters"]] - ) - new.dparameters = ( - [ - (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) if i is None else float(i) - for i in d["dTransformParameters"] - ] - if "dTransformParameters" in d - else 6 * [np.nan] and d["dTransformParameters"] is not None - ) - new.shape = ( - None - if d["Size"] is None - else [None if i is None else float(i) for i in d["Size"]] - ) - return new - - def __mul__(self, other): # TODO: take care of dmatrix - result = self.copy() - if isinstance(other, Transform): - result.matrix = self.matrix @ other.matrix - result.dmatrix = self.dmatrix @ other.matrix + self.matrix @ other.dmatrix - else: - result.matrix = self.matrix @ other - result.dmatrix = self.dmatrix @ other - return result - - def is_unity(self): - return self.parameters == [1, 0, 0, 1, 0, 0] - - def copy(self): - return deepcopy(self) - - @staticmethod - def cast_image(im): - if not isinstance(im, sitk.Image): - im = sitk.GetImageFromArray(np.asarray(im)) - return im - - @staticmethod - def cast_array(im): - if isinstance(im, sitk.Image): - im = sitk.GetArrayFromImage(im) - return im - - @property - def matrix(self): - return np.array( - ( - (*self.parameters[:2], self.parameters[4]), - (*self.parameters[2:4], self.parameters[5]), - (0, 0, 1), - ) - ) - - @matrix.setter - def matrix(self, value): - value = np.asarray(value) - self.parameters = [*value[0, :2], *value[1, :2], *value[:2, 2]] - - @property - def dmatrix(self): - return np.array( - ( - (*self.dparameters[:2], self.dparameters[4]), - (*self.dparameters[2:4], self.dparameters[5]), - (0, 0, 0), - ) - ) - - @dmatrix.setter - def dmatrix(self, value): - value = np.asarray(value) - self.dparameters = [*value[0, :2], *value[1, :2], *value[:2, 2]] - - @property - def parameters(self): - if self.transform is not None: - return list(self.transform.GetParameters()) - else: - return [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] - - @parameters.setter - def parameters(self, value): - if self.transform is not None: - value = np.asarray(value) - self.transform.SetParameters(value.tolist()) - - @property - def origin(self): - if self.transform is not None: - return self.transform.GetFixedParameters() - - @origin.setter - def origin(self, value): - if self.transform is not None: - value = np.asarray(value) - self.transform.SetFixedParameters(value.tolist()) - - @property - def inverse(self): - if self.is_unity(): - return self - if self._last is None or self._last != self.asdict(): - self._last = self.asdict() - self._inverse = Transform.from_dict(self.asdict()) - self._inverse.transform = self._inverse.transform.GetInverse() - self._inverse._last = self._inverse.asdict() - self._inverse._inverse = self - return self._inverse - - def adapt(self, origin, shape): - self.origin -= np.array(origin) + (self.shape - np.array(shape)[:2]) / 2 - self.shape = shape[:2] - - def asdict(self): - return { - "CenterOfRotationPoint": self.origin, - "Size": self.shape, - "TransformParameters": self.parameters, - "dTransformParameters": np.nan_to_num(self.dparameters, nan=1e99).tolist(), - } - - def frame(self, im, default=0): - if self.is_unity(): - return im - else: - if sitk is None: - raise ImportError( - "SimpleElastix is not installed: " - "https://simpleelastix.readthedocs.io/GettingStarted.html" - ) - dtype = im.dtype - im = im.astype("float") - intp = ( - sitk.sitkBSpline - if np.issubdtype(dtype, np.floating) - else sitk.sitkNearestNeighbor - ) - return self.cast_array( - sitk.Resample(self.cast_image(im), self.transform, intp, default) - ).astype(dtype) - - def coords(self, array, columns=None): - """Transform coordinates in 2 column numpy array, - or in pandas DataFrame or Series objects in columns ['x', 'y'] - """ - if self.is_unity(): - return array.copy() - elif DataFrame is not None and isinstance(array, (DataFrame, Series)): - columns = columns or ["x", "y"] - array = array.copy() - if isinstance(array, DataFrame): - array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy())) - elif isinstance(array, Series): - array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))[ - 0 - ] - return array - else: # somehow we need to use the inverse here to get the same effect as when using self.frame - return np.array( - [ - self.inverse.transform.TransformPoint(i.tolist()) - for i in np.asarray(array) - ] - ) - - def save(self, file): - """save the parameters of the transform calculated - with affine_registration to a yaml file - """ - if not file[-3:] == "yml": - file += ".yml" - with open(file, "w") as f: - yaml.safe_dump(self.asdict(), f, default_flow_style=None) diff --git a/pyproject.toml b/pyproject.toml index 016bd70..56532e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ndbioimage" -version = "2027.0.3" +version = "2027.0.4" requires-python = ">=3.10" classifiers = [ "License :: OSI Approved :: MIT License", @@ -35,7 +35,7 @@ ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub" [tool.maturin] python-source = "py" -features = ["python", "bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie"] +features = ["python", "bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"] no-default-features = true module-name = "ndbioimage.ndbioimage_rs" include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"] diff --git a/src/error.rs b/src/error.rs index a885b20..a75a237 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,8 +10,8 @@ pub enum Error { /// an ndarray shape error #[error(transparent)] Shape(#[from] ndarray::ShapeError), - #[cfg(feature = "bioformats_java")] /// an error from the j4rs java bridge + #[cfg(feature = "bioformats_java")] #[error(transparent)] J4rs(#[from] j4rs::errors::J4RsError), /// an infallible conversion @@ -23,60 +23,64 @@ pub enum Error { /// an ome metadata error #[error(transparent)] Ome(#[from] ome_metadata::error::Error), - #[cfg(feature = "bioformats_java")] /// an error while downloading (e.g. the bioformats jar) + #[cfg(feature = "bioformats_java")] #[error(transparent)] Downloader(#[from] downloader::Error), /// an error parsing an enum string with strum #[error(transparent)] Strum(#[from] strum::ParseError), - #[cfg(feature = "tiffwrite")] /// an indicatif progress bar template error + #[cfg(feature = "tiffwrite")] #[error(transparent)] TemplateError(#[from] indicatif::style::TemplateError), - #[cfg(feature = "tiffwrite")] /// an error from the tiffwrite crate + #[cfg(feature = "tiffwrite")] #[error(transparent)] TiffWrite(#[from] tiffwrite::error::Error), - #[cfg(feature = "tiffseq")] /// a yaml (de)serialization error + #[cfg(feature = "tiffseq")] #[error(transparent)] - SerdeYaml(#[from] serde_yaml::Error), - #[cfg(any(feature = "tiffseq", feature = "tiff"))] + SerdeYaml(#[from] yaml_serde::Error), /// an error from the tiff crate + #[cfg(any(feature = "tiffseq", feature = "tiff"))] #[error(transparent)] Tiff(#[from] tiff::TiffError), - #[cfg(feature = "python")] /// a postcard (de)serialization error + #[cfg(feature = "python")] #[error(transparent)] PostCard(#[from] postcard::Error), - #[cfg(feature = "czi")] /// an error from the libczi binding + #[cfg(feature = "czi")] #[error(transparent)] LibCzi(#[from] libczirw_sys::error::Error), /// a regex error #[error(transparent)] RegexError(#[from] regex::Error), - #[cfg(feature = "czi")] /// an xmltree error + #[cfg(feature = "czi")] #[error(transparent)] XmlTree(#[from] xmltree::Error), - #[cfg(feature = "czi")] /// an xmltree parse error + #[cfg(feature = "czi")] #[error(transparent)] XmlTreeParse(#[from] xmltree::ParseError), - #[cfg(feature = "czi")] /// a czi-specific error + #[cfg(feature = "czi")] #[error(transparent)] Czi(#[from] crate::readers::czi::CziError), - #[cfg(feature = "movie")] /// an error joining a tokio task + #[cfg(feature = "movie")] #[error(transparent)] TokioJoin(#[from] tokio::task::JoinError), - #[cfg(feature = "bioformats_rust")] /// an error from the bioformats rust crate + #[cfg(feature = "bioformats_rust")] #[error(transparent)] BioFormats(#[from] bioformats::error::BioFormatsError), + /// an image registration / transforms error + #[cfg(feature = "transforms")] + #[error(transparent)] + ImageRegistration(#[from] image_registration::error::Error), /// the axis string could not be parsed #[error("invalid axis: {0}")] @@ -162,6 +166,12 @@ pub enum Error { /// cannot remove axes that have a size != 1 #[error("cannot remove axes {0}, size {1} != 1")] SizeMismatch(String, usize), + /// shape mismatch + #[error("shape mismatch: {0:?}, {1:?}")] + ShapeMismatch(Vec, Vec), + /// file mismatch + #[error("{0} do not match in {1} and {2}")] + FileMismatch(String, String, String), } impl Error { diff --git a/src/py.rs b/src/py.rs index d9f06f9..fc7b4e6 100644 --- a/src/py.rs +++ b/src/py.rs @@ -1,36 +1,16 @@ -use crate::axes::{Axis, Shape}; use crate::error::Error; -use crate::metadata::Metadata; -#[cfg(feature = "movie")] -use crate::movie::MovieOptions; -use crate::readers::{DynReader, PixelType, Reader}; -use crate::view::{Item, View}; -use itertools::Itertools; -use ndarray::{Ix0, Ix1, IxDyn, SliceInfoElem}; -use numpy::{ - AllowTypeChange, IntoPyArray, PyArray, PyArrayDescr, PyArrayLike0, PyArrayLike1, - PyArrayMethods, dtype, -}; -use ome_metadata::Ome; -use postcard::{from_bytes, to_stdvec}; -use pyo3::IntoPyObjectExt; -use pyo3::exceptions::{ - PyIndexError, PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError, -}; +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -use pyo3::types::{ - PyBytes, PyEllipsis, PyInt, PyList, PyNone, PySlice, PySliceMethods, PyString, PyTuple, -}; use pyo3_stub_gen::derive::*; -use pyo3_stub_gen::inventory::submit; use pyo3_stub_gen::{StubGenConfig, StubInfo}; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; use std::error::Error as StdError; -use std::fmt::Debug; use std::path::PathBuf; -fn format_error_chain(err: &crate::error::Error) -> String { +mod imread; +#[cfg(feature = "transforms")] +mod transforms; + +fn format_error_chain(err: &Error) -> String { let mut msg = format!("[{}] {err}", err.variant_name()); let mut source = err.source(); while let Some(s) = source { @@ -40,2115 +20,15 @@ fn format_error_chain(err: &crate::error::Error) -> String { msg } -impl From for PyErr { +impl From for PyErr { #[track_caller] - fn from(err: crate::error::Error) -> PyErr { + fn from(err: Error) -> PyErr { let location = std::panic::Location::caller(); let msg = format!("{}: {}", location, format_error_chain(&err)); PyRuntimeError::new_err(msg) } } -/// class to read image files, while taking good care of important metadata, -/// currently optimized for .czi files, but can open anything that bioformats can handle -/// path: path to the image file -/// optional: -/// axes: order of axes, default: cztyx, but omitting any axes with lenght 1 -/// dtype: datatype to be used when returning frames -/// -/// Examples: -/// >> im = Imread('/path/to/file.image', axes='czt) -/// >> im -/// << shows summary -/// >> im.shape -/// << (15, 26, 1000, 1000) -/// >> im.axes -/// << 'ztyx' -/// >> plt.imshow(im[1, 0]) -/// << plots frame at position z=1, t=0 (python type indexing) -/// >> plt.imshow(im[:, 0].max('z')) -/// << plots max-z projection at t=0 -/// >> im.pxsize -/// << 0.09708737864077668 image-plane pixel size in um -/// >> im.laserwavelengths -/// << [642, 488] -/// >> im.laserpowers -/// << [0.02, 0.0005] in % -/// -/// TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var -#[gen_stub_pyclass] -#[pyclass( - subclass, - from_py_object, - name = "Imread", - module = "ndbioimage.ndbioimage_rs" -)] -#[derive(Clone, Debug, Serialize, Deserialize)] -struct PyView { - view: View, - dtype: PixelType, - #[serde(skip)] - ome: Ome, - index: usize, -} - -unsafe impl Send for PyView {} -unsafe impl Sync for PyView {} - -impl PyView { - fn item(py: Python, view: View, dtype: PixelType) -> PyResult> { - Ok(match dtype { - PixelType::I8 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U8 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I16 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U16 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - }) - } -} - -#[gen_stub_pymethods] -#[pymethods] -impl PyView { - /// new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64) - #[new] - #[pyo3(signature = (path, dtype = None, axes = "cztyx", reader = None))] - fn new<'py>( - py: Python<'py>, - #[gen_stub( - override_type(type_repr="str | pathlib.Path | Imread | bytes", imports=("pathlib")) - )] - path: Bound<'py, PyAny>, - #[gen_stub(override_type(type_repr = "typing.Optional[numpy.typing.DTypeLike]", imports=("typing", "numpy", "numpy.typing") - ))] - dtype: Option>, - axes: &str, - reader: Option<&str>, - ) -> PyResult { - if path.is_instance_of::() { - Ok(path.cast_into::()?.extract::()?) - } else if path.is_instance_of::() { - let mut pyview: Self = from_bytes(&path.extract::>()?).map_err(Error::from)?; - pyview.ome = pyview.view.metadata()?; - Ok(pyview) - } else { - let builtins = PyModule::import(py, "builtins")?; - let path = PathBuf::from( - builtins - .getattr("str")? - .call1((path,))? - .cast_into::()? - .extract::()?, - ); - let axes = axes - .chars() - .map(|a| a.to_string().parse().map_err(Error::from)) - .collect::, Error>>()?; - let view = if let Some(reader) = reader { - DynReader::from_path_select_reader(&path, reader)?.view() - } else { - View::<_, DynReader>::from_path(&path)? - } - .permute_axes_dyn(&axes)?; - let dtype = if let Some(dtype) = dtype { - let np = PyModule::import(py, "numpy")?; - let dt = np.getattr("dtype")?.call1((&dtype,))?; - let name = dt.getattr("name")?; - let dtype_str = name.extract::()?; - dtype_str.parse()? - } else { - *view.pixel_type() - }; - let ome = view.metadata()?; - Ok(Self { - view, - dtype, - ome, - index: 0, - }) - } - } - - /// get all available positions (series) in the file - #[staticmethod] - fn get_positions<'py>( - py: Python, - #[gen_stub( - override_type(type_repr="str | pathlib.Path | Imread | bytes", imports=("pathlib")) - )] - path: Bound<'py, PyAny>, - ) -> PyResult> { - Self::get_available_series(py, path, None) - } - - /// only remains for backwards compatibility - #[staticmethod] - fn kill_vm() {} - - /// the name of the reader used to open the file - #[getter] - fn reader_name(&self) -> String { - self.view.reader_name().to_string() - } - - /// reshape the view with a new axis order - #[allow(unused_variables)] - fn reshape<'py>(&self, order: &str, copy: bool) -> PyResult> { - todo!() - } - - /// return a new view with transformations applied (channel alignment, drift correction) - #[allow(unused_variables)] - #[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None))] - fn with_transform<'py>( - &self, - channels: bool, - drift: bool, - file: Option>, - bead_files: Option>, - ) -> PyResult { - todo!() - } - - /// get the transformation matrix (not yet implemented) - #[getter] - fn get_transform(&self) -> PyResult<()> { - todo!() - } - - #[gen_stub(override_return_type(type_repr="numpy.ndarray | int | float", imports=("numpy")))] - fn squeeze<'py>(&self, py: Python<'py>) -> PyResult> { - let view = self.view.squeeze()?; - if view.ndim() == 0 { - Ok(match self.dtype { - PixelType::I8 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U8 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I16 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U16 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - }) - } else { - PyView { - view, - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - } - .into_bound_py_any(py) - } - } - - /// close the file: does nothing as this is handled automatically - fn close(&self) -> PyResult<()> { - Ok(()) - } - - /// change the data type of the view: (u)int(8/16/32) or float(32/64) - fn as_type( - &self, - py: Python<'_>, - #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing") - ))] - dtype: Bound<'_, PyAny>, - ) -> PyResult { - let np = PyModule::import(py, "numpy")?; - let dt = np.getattr("dtype")?.call1((&dtype,))?; - let name = dt.getattr("name")?; - let dtype_str = name.extract::()?; - Ok(PyView { - view: self.view.clone(), - dtype: dtype_str.parse()?, - ome: self.ome.clone(), - index: 0, - }) - } - - /// change the data type of the view: (u)int(8/16/32) or float(32/64) - fn astype( - &self, - py: Python<'_>, - #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing") - ))] - dtype: Bound<'_, PyAny>, - ) -> PyResult { - self.as_type(py, dtype) - } - - /// slice the view and return a new view or a single number - fn __getitem__<'py>( - &self, - py: Python<'py>, - n: Bound<'py, PyAny>, - ) -> PyResult> { - // TODO: newaxis - let slice: Vec<_> = if n.is_instance_of::() { - n.cast_into::()?.into_iter().collect() - } else if n.is_instance_of::() { - n.cast_into::()?.into_iter().collect() - } else { - vec![n] - }; - let mut new_slice = Vec::new(); - let mut ellipsis = None; - let shape = self.view.shape(); - for (i, (s, t)) in slice.iter().zip(shape.iter()).enumerate() { - if s.is_none() { - new_slice.push(SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }); - } else if s.is_instance_of::() { - new_slice.push(SliceInfoElem::Index(s.cast::()?.extract::()?)); - } else if s.is_instance_of::() { - let u = s.cast::()?.indices(*t as isize)?; - new_slice.push(SliceInfoElem::Slice { - start: u.start, - end: Some(u.stop), - step: u.step, - }); - } else if s.is_instance_of::() { - if ellipsis.is_some() { - return Err(PyErr::new::( - "cannot have more than one ellipsis".to_string(), - )); - } - let _ = ellipsis.insert(i); - } else if let Ok(arr_like) = s.extract::>() { - let pyarr: &Bound> = &arr_like; - let mut index = *pyarr.readonly().as_array().into_scalar(); - let index0 = index; - if index < 0 { - index += *t as isize; - } - if (index < 0) || (index >= *t as isize) { - return Err(PyIndexError::new_err(format!( - "index {} is out of bounds for axis {} with size {}", - index0, i, t - ))); - } - new_slice.push(SliceInfoElem::Index(index)); - } else if let Ok(arr_like) = s.extract::>() { - let pyarr: &Bound> = &arr_like; - let read = pyarr.readonly(); - let mut indices = read.as_array().to_vec(); - for index in indices.iter_mut() { - let index0 = *index; - if *index < 0 { - *index += *t as isize; - } - if (*index < 0) || (*index >= *t as isize) { - return Err(PyIndexError::new_err(format!( - "index {} is out of bounds for axis {} with size {}", - index0, i, t - ))); - } - } - if indices.is_empty() { - new_slice.push(SliceInfoElem::Slice { - start: 0, - end: Some(0), - step: 1, - }) - } else { - let d = indices - .windows(2) - .map(|i| i[1] - i[0]) - .collect::>(); - if d.is_empty() { - let index = indices[0]; - new_slice.push(SliceInfoElem::Slice { - start: index, - end: Some(index + 1), - step: 1, - }); - } else if d.len() == 1 { - new_slice.push(SliceInfoElem::Slice { - start: indices[0], - end: indices.last().map(|j| j + 1), - step: d.into_iter().collect::>()[0], - }); - } else { - return Err(PyValueError::new_err("indices array must regularly spaced")); - } - }; - } else { - return Err(PyValueError::new_err(format!( - "cannot convert {:?} to slice", - s - ))); - } - } - if new_slice.len() > shape.len() { - return Err(PyValueError::new_err(format!( - "got more indices ({}) than dimensions ({})", - new_slice.len(), - shape.len() - ))); - } - while new_slice.len() < shape.len() { - if let Some(i) = ellipsis { - new_slice.insert( - i, - SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }, - ) - } else { - new_slice.push(SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }) - } - } - let view = self.view.slice(new_slice.as_slice())?; - if view.ndim() == 0 { - Self::item(py, view, self.dtype) - } else { - PyView { - view, - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - } - .into_bound_py_any(py) - } - } - - /// convert to a numpy array, optionally with a different dtype - #[allow(unused_variables)] - #[pyo3(signature = (dtype = None, copy = None))] - fn __array__<'py>( - &self, - py: Python<'py>, - #[gen_stub(override_type(type_repr = "typing.Optional[numpy.typing.DTypeLike]", imports=("typing", "numpy", "numpy.typing") - ))] - dtype: Option>, - copy: Option, - ) -> PyResult> { - if let Some(dtype) = dtype { - self.as_type(py, dtype)?.as_array(py) - } else { - self.as_array(py) - } - } - - /// check if an item is contained in the view (not implemented) - fn __contains__(&self, _item: Bound) -> PyResult { - Err(PyNotImplementedError::new_err("contains not implemented")) - } - - /// element-wise less than comparison - fn __lt__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("less")?.call1((&a, &b)) - } - - /// element-wise less than or equal comparison - fn __le__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("less_equal")?.call1((&a, &b)) - } - - /// element-wise equality comparison - fn __eq__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("equal")?.call1((&a, &b)) - } - - /// element-wise not equal comparison - fn __ne__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("not_equal")?.call1((&a, &b)) - } - - /// element-wise greater than comparison - fn __gt__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("greater")?.call1((&a, &b)) - } - - /// element-wise greater than or equal comparison - fn __ge__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("greater_equal")?.call1((&a, &b)) - } - - /// element-wise addition - fn __add__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("add")?.call1((&a, &b)) - } - - /// element-wise addition (reflected) - fn __radd__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("add")?.call1((&a, &b)) - } - - /// element-wise subtraction - fn __sub__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("subtract")?.call1((&a, &b)) - } - - /// element-wise subtraction (reflected) - fn __rsub__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("subtract")?.call1((&a, &b)) - } - - /// element-wise multiplication - fn __mul__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("multiply")?.call1((&a, &b)) - } - - /// element-wise multiplication (reflected) - fn __rmul__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("multiply")?.call1((&a, &b)) - } - - /// element-wise true division - fn __truediv__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("true_divide")?.call1((&a, &b)) - } - - /// element-wise true division (reflected) - fn __rtruediv__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("true_divide")?.call1((&a, &b)) - } - - /// element-wise floor division - fn __floordiv__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("floor_divide")?.call1((&a, &b)) - } - - /// element-wise floor division (reflected) - fn __rfloordiv__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("floor_divide")?.call1((&a, &b)) - } - - /// element-wise modulo - fn __mod__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("remainder")?.call1((&a, &b)) - } - - /// element-wise modulo (reflected) - fn __rmod__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("remainder")?.call1((&a, &b)) - } - - #[gen_stub(skip)] - fn __pow__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - r#mod: Option>, - ) -> PyResult> { - if r#mod.is_some() { - return Err(PyNotImplementedError::new_err( - "cannot use pow or ** on Imread with mod != None", - )); - } - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("power")?.call1((&a, &b)) - } - - #[gen_stub(skip)] - fn __rpow__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - r#mod: Option>, - ) -> PyResult> { - if r#mod.is_some() { - return Err(PyNotImplementedError::new_err( - "cannot use pow or ** on Imread with mod != None", - )); - } - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("power")?.call1((&a, &b)) - } - - /// element-wise matrix multiplication - fn __matmul__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("matmul")?.call1((&a, &b)) - } - - /// element-wise matrix multiplication (reflected) - fn __rmatmul__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("matmul")?.call1((&a, &b)) - } - - /// element-wise bitwise AND - fn __and__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("bitwise_and")?.call1((&a, &b)) - } - - /// element-wise bitwise AND (reflected) - fn __rand__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("bitwise_and")?.call1((&a, &b)) - } - - /// element-wise bitwise OR - fn __or__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("bitwise_or")?.call1((&a, &b)) - } - - /// element-wise bitwise OR (reflected) - fn __ror__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("bitwise_or")?.call1((&a, &b)) - } - - /// element-wise bitwise XOR - fn __xor__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("bitwise_xor")?.call1((&a, &b)) - } - - /// element-wise bitwise XOR (reflected) - fn __rxor__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("bitwise_xor")?.call1((&a, &b)) - } - - /// element-wise left shift - fn __lshift__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("left_shift")?.call1((&a, &b)) - } - - /// element-wise left shift (reflected) - fn __rlshift__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("left_shift")?.call1((&a, &b)) - } - - /// element-wise right shift - fn __rshift__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - let b = np.getattr("asarray")?.call1((&other,))?; - np.getattr("right_shift")?.call1((&a, &b)) - } - - /// element-wise right shift (reflected) - fn __rrshift__<'py>( - &self, - py: Python<'py>, - other: Bound<'py, PyAny>, - ) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let b = self.as_array(py)?; - let a = np.getattr("asarray")?.call1((&other,))?; - np.getattr("right_shift")?.call1((&a, &b)) - } - - /// element-wise negation - fn __neg__<'py>(&self, py: Python<'py>) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - np.getattr("negative")?.call1((&a,)) - } - - /// element-wise positive - fn __pos__<'py>(&self, py: Python<'py>) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - np.getattr("positive")?.call1((&a,)) - } - - /// element-wise absolute value - fn __abs__<'py>(&self, py: Python<'py>) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - np.getattr("absolute")?.call1((&a,)) - } - - /// element-wise bitwise inversion - fn __invert__<'py>(&self, py: Python<'py>) -> PyResult> { - let np = PyModule::import(py, "numpy")?; - let a = self.as_array(py)?; - np.getattr("invert")?.call1((&a,)) - } - - /// context manager entry - fn __enter__<'py>(slf: PyRef<'py, Self>) -> PyResult> { - Ok(slf) - } - - /// context manager exit - #[allow(unused_variables)] - #[pyo3(signature = (exc_type=None, exc_val=None, exc_tb=None))] - fn __exit__( - &self, - exc_type: Option>, - exc_val: Option>, - exc_tb: Option>, - ) -> PyResult<()> { - self.close() - } - - /// arguments for pickling - pub(crate) fn __getnewargs__(&self) -> PyResult<(Vec,)> { - Ok((to_stdvec(self).map_err(Error::from)?,)) - } - - /// shallow copy of the view - fn __copy__(&self) -> Self { - Self { - view: self.view.clone(), - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - } - } - - /// deep copy of the view (same as shallow copy for this type) - fn __deepcopy__(&self) -> Self { - Self { - view: self.view.clone(), - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - } - } - - /// create a copy of the view - fn copy(&self) -> Self { - Self { - view: self.view.clone(), - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - } - } - - /// iterate over the first axis - fn __iter__(&self) -> Self { - Self { - view: self.view.clone(), - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - } - } - - /// get the next item in the iteration - fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult>> { - let shape = self.view.shape(); - if shape.is_empty() || (self.index == shape[0]) { - self.index = 0; - Ok(None) - } else { - let mut new_slice = vec![SliceInfoElem::Index(self.index as isize)]; - self.index += 1; - for _ in 1..shape.len() { - new_slice.push(SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }) - } - let view = self.view.slice(new_slice.as_slice())?; - Some(if view.ndim() == 0 { - Self::item(py, view, self.dtype) - } else { - PyView { - view, - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - } - .into_bound_py_any(py) - }) - .transpose() - } - } - - /// number of elements in the first axis - fn __len__(&self) -> PyResult { - Ok(self.view.len()) - } - - /// string representation with a summary of the image - fn __repr__(&self) -> PyResult { - Ok(self.view.summary()?) - } - - /// the file path as a string - fn __str__(&self) -> PyResult { - Ok(self.view.path().display().to_string()) - } - - /// retrieve a single frame at czt, sliced accordingly - fn get_frame<'py>( - &self, - py: Python<'py>, - c: isize, - z: isize, - t: isize, - ) -> PyResult> { - Ok(match self.dtype { - PixelType::I8 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::U8 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::I16 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::U16 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::I32 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::U32 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::F32 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::F64 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::I64 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::U64 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::I128 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::U128 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - PixelType::F128 => self - .view - .get_frame::(c, z, t)? - .into_pyarray(py) - .into_any(), - }) - } - - /// flatten the view into a 1D numpy array - fn flatten<'py>(&self, py: Python<'py>) -> PyResult> { - Ok(match self.dtype { - PixelType::I8 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::U8 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::I16 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::U16 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::I32 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::U32 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::F32 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::F64 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::I64 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::U64 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::I128 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::U128 => self.view.flatten::()?.into_pyarray(py).into_any(), - PixelType::F128 => self.view.flatten::()?.into_pyarray(py).into_any(), - }) - } - - /// convert the view to bytes - fn to_bytes(&self) -> PyResult> { - Ok(match self.dtype { - PixelType::I8 => self.view.to_bytes::()?, - PixelType::U8 => self.view.to_bytes::()?, - PixelType::I16 => self.view.to_bytes::()?, - PixelType::U16 => self.view.to_bytes::()?, - PixelType::I32 => self.view.to_bytes::()?, - PixelType::U32 => self.view.to_bytes::()?, - PixelType::F32 => self.view.to_bytes::()?, - PixelType::F64 => self.view.to_bytes::()?, - PixelType::I64 => self.view.to_bytes::()?, - PixelType::U64 => self.view.to_bytes::()?, - PixelType::I128 => self.view.to_bytes::()?, - PixelType::U128 => self.view.to_bytes::()?, - PixelType::F128 => self.view.to_bytes::()?, - }) - } - - /// convert the view to bytes (alias for to_bytes) - fn tobytes(&self) -> PyResult> { - self.to_bytes() - } - - /// retrieve the ome metadata as an XML string - #[gen_stub(skip)] - #[getter] - fn get_ome(&self) -> Ome { - self.ome.clone() - } - - /// the file path - #[getter] - fn path(&self) -> PyResult { - Ok(self.view.path().to_owned()) - } - - /// the series in the file - #[getter] - fn series(&self) -> PyResult { - Ok(self.view.series()) - } - - /// the axes in the view - #[getter] - fn axes(&self) -> String { - self.view.axes().iter().map(|a| format!("{:?}", a)).join("") - } - - /// the shape of the view - #[getter] - fn shape(&self) -> PyShape { - PyShape { - inner: self.view.shape(), - } - } - - /// the current slice applied to the view - #[getter] - fn slice(&self) -> PyResult> { - Ok(self - .view - .get_slice() - .iter() - .map(|s| format!("{:#?}", s)) - .collect()) - } - - /// the number of pixels in the view - #[getter] - fn size(&self) -> usize { - self.view.size() - } - - /// the number of dimensions in the view - #[getter] - fn ndim(&self) -> usize { - self.view.ndim() - } - - /// find the position of an axis - fn get_ax( - &self, - #[gen_stub(override_type(type_repr = "int | str"))] axis: Bound, - ) -> PyResult { - if axis.is_instance_of::() { - let axis = axis - .cast_into::()? - .extract::()? - .parse::() - .map_err(Error::from)?; - self.view - .axes() - .iter() - .position(|a| *a == axis) - .ok_or_else(|| { - PyErr::new::(format!("cannot find axis {:?}", axis)) - }) - } else if axis.is_instance_of::() { - Ok(axis.cast_into::()?.extract::()?) - } else { - Err(PyErr::new::( - "cannot convert to axis".to_string(), - )) - } - } - - /// swap two axes - fn swap_axes( - &self, - #[gen_stub(override_type(type_repr = "int | str"))] ax0: Bound, - #[gen_stub(override_type(type_repr = "int | str"))] ax1: Bound, - ) -> PyResult { - let ax0 = self.get_ax(ax0)?; - let ax1 = self.get_ax(ax1)?; - let view = self.view.swap_axes(ax0, ax1)?; - Ok(PyView { - view, - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - }) - } - - /// permute the order of the axes - #[pyo3(signature = (axes = None))] - fn transpose( - &self, - #[gen_stub(override_type(type_repr="typing.Optional[typing.Sequence[int | str]]", imports=("typing") - ))] - axes: Option>>, - ) -> PyResult { - let view = if let Some(axes) = axes { - let ax = axes - .into_iter() - .map(|a| self.get_ax(a)) - .collect::, _>>()?; - self.view.permute_axes(&ax)? - } else { - self.view.transpose()? - }; - Ok(PyView { - view, - dtype: self.dtype, - ome: self.ome.clone(), - index: 0, - }) - } - - /// transposed view (alias for transpose(None)) - #[allow(non_snake_case)] - #[getter] - fn T(&self) -> PyResult { - self.transpose(None) - } - - /// collect data into a numpy array - fn as_array<'py>(&self, py: Python<'py>) -> PyResult> { - Ok(match self.dtype { - PixelType::I8 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::U8 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::I16 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::U16 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::I32 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::U32 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::F32 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::F64 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::I64 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::U64 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::I128 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::U128 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - PixelType::F128 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), - }) - } - - /// the numpy dtype of the view - #[gen_stub(override_return_type(type_repr = "numpy.dtype", imports=("numpy")))] - #[getter] - fn get_dtype<'py>(&self, py: Python<'py>) -> PyResult> { - match self.dtype { - PixelType::I8 => Ok(dtype::(py)), - PixelType::U8 => Ok(dtype::(py)), - PixelType::I16 => Ok(dtype::(py)), - PixelType::U16 => Ok(dtype::(py)), - PixelType::I32 => Ok(dtype::(py)), - PixelType::U32 => Ok(dtype::(py)), - PixelType::F32 => Ok(dtype::(py)), - PixelType::F64 => Ok(dtype::(py)), - PixelType::I64 => Ok(dtype::(py)), - PixelType::U64 => Ok(dtype::(py)), - PixelType::I128 => Err(PyTypeError::new_err( - "type is i128, but this cannot be represented by numpy.dtype", - )), - PixelType::U128 => Err(PyTypeError::new_err( - "type is u128, but this cannot be represented by numpy.dtype", - )), - PixelType::F128 => Ok(dtype::(py)), - } - } - - /// set the dtype of the view - #[gen_stub(skip)] - #[setter] - fn set_dtype(&mut self, py: Python, dtype: Bound<'_, PyAny>) -> PyResult<()> { - let np = PyModule::import(py, "numpy")?; - let dt = np.getattr("dtype")?.call1((&dtype,))?; - let name = dt.getattr("name")?; - let dtype_str = name.extract::()?; - self.dtype = dtype_str.parse()?; - Ok(()) - } - - /// get the maximum overall or along a given axis - #[allow(clippy::too_many_arguments)] - #[gen_stub(skip)] - #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=None, r#where=true), text_signature = "axis: str | int" - )] - fn max<'py>( - &self, - py: Python<'py>, - axis: Option>, - dtype: Option>, - out: Option>, - keepdims: bool, - initial: Option, - r#where: bool, - ) -> PyResult> { - if let Some(i) = initial - && i != 0 - { - Err(Error::NotImplemented( - "arguments beyond axis are not implemented".to_string(), - ))?; - } - if dtype.is_some() || out.is_some() || keepdims || !r#where { - Err(Error::NotImplemented( - "arguments beyond axis are not implemented".to_string(), - ))?; - } - if let Some(axis) = axis { - PyView { - dtype: self.dtype, - view: self.view.max_proj(self.get_ax(axis)?)?, - ome: self.ome.clone(), - index: 0, - } - .into_bound_py_any(py) - } else { - Ok(match self.dtype { - PixelType::I8 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::U8 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::I16 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::U16 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::I32 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::U32 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::F32 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::F64 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::I64 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::U64 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::I128 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::U128 => self.view.max::()?.into_pyobject(py)?.into_any(), - PixelType::F128 => self.view.max::()?.into_pyobject(py)?.into_any(), - }) - } - } - - /// get the minimum overall or along a given axis - #[allow(clippy::too_many_arguments)] - #[gen_stub(skip)] - #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int" - )] - fn min<'py>( - &self, - py: Python<'py>, - axis: Option>, - dtype: Option>, - out: Option>, - keepdims: bool, - initial: Option, - r#where: bool, - ) -> PyResult> { - if let Some(i) = initial - && i != 0 - { - Err(Error::NotImplemented( - "arguments beyond axis are not implemented".to_string(), - ))?; - } - if dtype.is_some() || out.is_some() || keepdims || !r#where { - Err(Error::NotImplemented( - "arguments beyond axis are not implemented".to_string(), - ))?; - } - if let Some(axis) = axis { - PyView { - dtype: self.dtype, - view: self.view.min_proj(self.get_ax(axis)?)?, - ome: self.ome.clone(), - index: 0, - } - .into_bound_py_any(py) - } else { - Ok(match self.dtype { - PixelType::I8 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::U8 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::I16 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::U16 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::I32 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::U32 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::F32 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::F64 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::I64 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::U64 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::I128 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::U128 => self.view.min::()?.into_pyobject(py)?.into_any(), - PixelType::F128 => self.view.min::()?.into_pyobject(py)?.into_any(), - }) - } - } - - /// get the mean overall or along a given axis - #[gen_stub(skip)] - #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int" - )] - fn mean<'py>( - &self, - py: Python<'py>, - axis: Option>, - dtype: Option>, - out: Option>, - keepdims: bool, - r#where: bool, - ) -> PyResult> { - if dtype.is_some() || out.is_some() || keepdims || !r#where { - Err(Error::NotImplemented( - "arguments beyond axis are not implemented".to_string(), - ))?; - } - if let Some(axis) = axis { - let dtype = if let PixelType::F32 = self.dtype { - PixelType::F32 - } else { - PixelType::F64 - }; - PyView { - dtype, - view: self.view.mean_proj(self.get_ax(axis)?)?, - ome: self.ome.clone(), - index: 0, - } - .into_bound_py_any(py) - } else { - Ok(match self.dtype { - PixelType::F32 => self.view.mean::()?.into_pyobject(py)?.into_any(), - _ => self.view.mean::()?.into_pyobject(py)?.into_any(), - }) - } - } - - /// get the sum overall or along a given axis - #[allow(clippy::too_many_arguments)] - #[gen_stub(skip)] - #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int" - )] - fn sum<'py>( - &self, - py: Python<'py>, - axis: Option>, - dtype: Option>, - out: Option>, - keepdims: bool, - initial: Option, - r#where: bool, - ) -> PyResult> { - if let Some(i) = initial - && i != 0 - { - Err(Error::NotImplemented( - "arguments beyond axis are not implemented".to_string(), - ))?; - } - if dtype.is_some() || out.is_some() || keepdims || !r#where { - Err(Error::NotImplemented( - "arguments beyond axis are not implemented".to_string(), - ))?; - } - let dtype = match self.dtype { - PixelType::I8 => PixelType::I16, - PixelType::U8 => PixelType::U16, - PixelType::I16 => PixelType::I32, - PixelType::U16 => PixelType::U32, - PixelType::I32 => PixelType::I64, - PixelType::U32 => PixelType::U64, - PixelType::F32 => PixelType::F32, - PixelType::F64 => PixelType::F64, - PixelType::I64 => PixelType::I128, - PixelType::U64 => PixelType::U128, - PixelType::I128 => PixelType::I128, - PixelType::U128 => PixelType::U128, - PixelType::F128 => PixelType::F128, - }; - if let Some(axis) = axis { - PyView { - dtype, - view: self.view.sum_proj(self.get_ax(axis)?)?, - ome: self.ome.clone(), - index: 0, - } - .into_bound_py_any(py) - } else { - Ok(match self.dtype { - PixelType::F32 => self.view.sum::()?.into_pyobject(py)?.into_any(), - PixelType::F64 => self.view.sum::()?.into_pyobject(py)?.into_any(), - PixelType::I64 => self.view.sum::()?.into_pyobject(py)?.into_any(), - PixelType::U64 => self.view.sum::()?.into_pyobject(py)?.into_any(), - PixelType::I128 => self.view.sum::()?.into_pyobject(py)?.into_any(), - PixelType::U128 => self.view.sum::()?.into_pyobject(py)?.into_any(), - PixelType::F128 => self.view.sum::()?.into_pyobject(py)?.into_any(), - _ => self.view.sum::()?.into_pyobject(py)?.into_any(), - }) - } - } - - /// whether the view contains a z-stack (more than one z slice) - #[getter] - fn z_stack(&self) -> PyResult { - if let Some(s) = self.view.size_ax(Axis::Z) { - Ok(s > 1) - } else { - Ok(false) - } - } - - /// backwards compatibility - #[getter] - fn zstack(&self) -> PyResult { - if let Some(s) = self.view.size_ax(Axis::Z) { - Ok(s > 1) - } else { - Ok(false) - } - } - - /// whether the view contains a time series (more than one time point) - #[getter] - fn time_series(&self) -> PyResult { - if let Some(s) = self.view.size_ax(Axis::T) { - Ok(s > 1) - } else { - Ok(false) - } - } - - /// backwards compatibility - #[getter] - fn timeseries(&self) -> PyResult { - if let Some(s) = self.view.size_ax(Axis::T) { - Ok(s > 1) - } else { - Ok(false) - } - } - - /// the pixel size in micrometers - #[getter] - fn pixel_size(&self) -> PyResult> { - Ok(self.ome.pixel_size()?) - } - - /// backwards compatibility - #[getter] - fn pxsize_um(&self) -> PyResult> { - Ok(self.ome.pixel_size()?.map(|p| p / 1000.)) - } - - /// backwards compatibility - #[getter] - fn deltaz_um(&self) -> PyResult> { - Ok(self.ome.delta_z()?.map(|p| p / 1000.)) - } - - /// the z-step size in micrometers - #[getter] - fn delta_z(&self) -> PyResult> { - Ok(self.ome.delta_z()?) - } - - /// the time interval between frames in seconds - #[getter] - fn time_interval(&self) -> PyResult> { - Ok(self.ome.time_interval()?) - } - - /// backwards compatibility - #[getter] - fn timeinterval(&self) -> PyResult> { - Ok(self.ome.time_interval()?) - } - - /// the exposure time for a given channel in seconds - fn exposure_time(&self, channel: usize) -> PyResult> { - Ok(self.ome.exposure_time(channel)?) - } - - /// backwards compatibility - #[getter] - fn exposuretime_s(&self) -> PyResult>> { - Ok((0..self.view.shape().c) - .map(|c| self.ome.exposure_time(c)) - .collect::, Error>>()?) - } - - /// the binning for a given channel - fn binning(&self, channel: usize) -> Option { - self.ome.binning(channel) - } - - /// the laser wavelength for a given channel in nanometers - fn laser_wavelengths(&self, channel: usize) -> PyResult> { - Ok(self.ome.laser_wavelengths(channel)?) - } - - /// the laser power for a given channel as a fraction - fn laser_power(&self, channel: usize) -> PyResult> { - Ok(self.ome.laser_powers(channel)?) - } - - /// the name of the objective - #[getter] - fn objective_name(&self) -> Option { - self.ome.objective_name() - } - - /// the total magnification (objective × tube lens) - #[getter] - fn magnification(&self) -> Option { - self.ome.magnification() - } - - /// the name of the tube lens - #[getter] - fn tube_lens_name(&self) -> Option { - self.ome.tube_lens_name() - } - - /// the name of the filter set for a given channel - fn filter_set_name(&self, channel: usize) -> Option { - self.ome.filter_set_name(channel) - } - - /// the detector gain for a given channel - fn gain(&self, channel: usize) -> Option { - self.ome.gain(channel) - } - - /// gives a helpful summary of the recorded experiment - fn summary(&self) -> PyResult { - Ok(self.view.summary()?) - } - - /// get all series contained in the file - #[staticmethod] - #[pyo3(signature = (path, reader = None))] - fn get_available_series<'py>( - py: Python<'py>, - #[gen_stub(override_type(type_repr="str | pathlib.Path | Imread", imports=("pathlib")))] - path: Bound<'py, PyAny>, - reader: Option<&str>, - ) -> PyResult> { - let path = if path.is_instance_of::() { - let py_view: Self = path.cast_into::()?.extract::()?; - py_view.view.path().to_owned() - } else { - let builtins = PyModule::import(py, "builtins")?; - PathBuf::from( - builtins - .getattr("str")? - .call1((path,))? - .cast_into::()? - .extract::()?, - ) - }; - - Ok(DynReader::get_available_series_select_reader(path, reader)?) - } - - /// get all series contained in the file - #[staticmethod] - #[pyo3(signature = (path, series, reader = None))] - fn get_available_positions<'py>( - py: Python<'py>, - #[gen_stub(override_type(type_repr="str | pathlib.Path | Imread", imports=("pathlib")))] - path: Bound<'py, PyAny>, - series: usize, - reader: Option<&str>, - ) -> PyResult> { - let path = if path.is_instance_of::() { - let py_view: Self = path.cast_into::()?.extract::()?; - py_view.view.path().to_owned() - } else { - let builtins = PyModule::import(py, "builtins")?; - PathBuf::from( - builtins - .getattr("str")? - .call1((path,))? - .cast_into::()? - .extract::()?, - ) - }; - Ok(DynReader::get_available_positions_select_reader( - path, series, reader, - )?) - } - - /// save the view as a TIFF file - #[cfg(feature = "tiffwrite")] - #[pyo3(signature = (file, colors = None, overwrite = false, bar = true))] - fn save_as_tiff( - &self, - py: Python, - file: PathBuf, - colors: Option>, - overwrite: bool, - bar: bool, - ) -> PyResult<()> { - let bar = if bar { - Some(crate::utils::progress::get_bar( - Some(0), - Some("writing tiff file".to_string()), - )) - } else { - None - }; - let options = - crate::tiffwrite::TiffOptions::new(bar, None, colors.unwrap_or_default(), overwrite)?; - py.detach(|| match self.dtype { - PixelType::I8 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::U8 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::I16 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::U16 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::I32 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::U32 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::I64 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::U64 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::I128 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::U128 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::F32 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::F64 => self.view.save_as_tiff_with_type::(file, &options), - PixelType::F128 => Err(Error::NotImplemented( - "saving as f128 is not implemented".to_string(), - )), - })?; - Ok(()) - } - - /// save the view as a movie file (MP4) - #[cfg(feature = "movie")] - #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (file, speed = 1.0, brightness = None, scale = 1.0, colors = None, overwrite = false, register = false, no_scaling = false) - )] - fn save_as_movie( - &self, - file: PathBuf, - speed: f64, - brightness: Option>, - scale: f64, - colors: Option>, - overwrite: bool, - register: bool, - no_scaling: bool, - ) -> PyResult<()> { - let options = MovieOptions::new( - speed, - brightness.unwrap_or_default(), - scale, - colors.unwrap_or_default(), - overwrite, - register, - no_scaling, - )?; - self.view.save_as_movie(file, &options)?; - Ok(()) - } - - /// backwards compatibility - #[allow(unused_variables)] - fn set_cache_size(&self, size: usize) {} -} - -submit! { - gen_methods_from_python! { - r#" - import typing - import numpy.typing - - class PyView: - def __pow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... - def __rpow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... - - def max(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: - """ Return the maximum along a given axis. Arguments beyond axis are not implemented """ - - def min(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: - """ Return the minimum along a given axis. Arguments beyond axis are not implemented """ - - def mean(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False) -> Imread | numpy.typing.NDArray | int | float: - """ Return the mean along a given axis. Arguments beyond axis are not implemented """ - - def sum(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: - """ Return the sum along a given axis. Arguments beyond axis are not implemented """ - "# - } -} - -/// batch convert multiple image files to TIFF format -#[cfg(feature = "tiffwrite")] -#[allow(clippy::too_many_arguments)] -#[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")] -#[pyfunction] -#[pyo3(signature = (files_in, files_out, operations = None, colors = None, overwrite = false, bar = true, message = None) -)] -fn batch_to_tiff( - py: Python, - files_in: Vec, - files_out: Vec, - operations: Option>, - colors: Option>, - overwrite: bool, - bar: bool, - message: Option, -) -> PyResult<()> { - py.detach(|| { - crate::tiffwrite::batch_to_tiff( - &files_in, &files_out, operations, colors, overwrite, bar, message, - ) - })?; - Ok(()) -} - -/// represents the shape of an image with named dimensions (c, z, t, y, x) -#[gen_stub_pyclass] -#[pyclass( - subclass, - from_py_object, - frozen, - name = "Shape", - module = "ndbioimage.ndbioimage_rs" -)] -#[derive(Clone, Debug, Serialize, Deserialize)] -struct PyShape { - inner: Shape, -} - -#[gen_stub_pymethods] -#[pymethods] -impl PyShape { - /// create a new shape with the given dimensions - #[new] - #[pyo3(signature = (order, c = 1, z = 1, t = 1, y = 1, x = 1))] - fn new(order: String, c: usize, z: usize, t: usize, y: usize, x: usize) -> PyResult { - Ok(Self { - inner: Shape { - c, - z, - t, - y, - x, - order: order - .chars() - .map(|c| c.to_uppercase().to_string().parse::()) - .collect::, _>>() - .map_err(Error::from)?, - }, - }) - } - - /// the number of channels - #[getter] - fn get_c(&self) -> usize { - self.inner.c - } - - /// the number of z slices - #[getter] - fn get_z(&self) -> usize { - self.inner.z - } - - /// the number of time points - #[getter] - fn get_t(&self) -> usize { - self.inner.t - } - - /// the number of pixels along y - #[getter] - fn get_y(&self) -> usize { - self.inner.y - } - - /// the number of pixels along x - #[getter] - fn get_x(&self) -> usize { - self.inner.x - } - - /// string representation - fn __str__(&self) -> String { - format!("{}", self.inner) - } - - /// detailed representation for debugging - fn __repr__(&self) -> String { - format!( - "Shape({}, {}, {}, {}, {})", - self.inner.c, self.inner.z, self.inner.t, self.inner.x, self.inner.y - ) - } - - /// arguments for pickling - fn __getnewargs__(&self) -> (String, usize, usize, usize, usize, usize) { - ( - self.inner - .order - .iter() - .map(|axis| format!("{}", axis)) - .collect::>() - .join(""), - self.inner.c, - self.inner.z, - self.inner.t, - self.inner.y, - self.inner.x, - ) - } - - /// get dimension size by index or axis name - #[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing") - ))] - fn __getitem__<'py>( - &self, - py: Python<'py>, - #[gen_stub(override_type( - type_repr = "str | int | None | Ellipsis | slice | list[int] | tuple[int]" - ))] - idx: Bound<'py, PyAny>, - ) -> PyResult> { - let (idx, is_idx) = if idx.is_instance_of::() || idx.is_instance_of::() - { - ((0..self.inner.order.len()).collect(), true) - } else if idx.is_instance_of::() { - let indices = idx - .cast::()? - .indices(self.inner.order.len() as isize)?; - ( - if indices.step > 0 { - (indices.start..indices.stop) - .step_by(indices.step as usize) - .map(|i| i as usize) - .collect::>() - } else { - (indices.stop..indices.start) - .step_by(-indices.step as usize) - .map(|i| i as usize) - .collect::>() - }, - true, - ) - } else if idx.is_instance_of::() { - (idx.cast::()?.extract::>()?, true) - } else if idx.is_instance_of::() { - (idx.cast::()?.extract::>()?, true) - } else if idx.is_instance_of::() { - let s = idx.cast::()?.extract::()?; - ( - s.to_uppercase() - .chars() - .map(|i| match i { - 'C' => Ok(self.inner.c), - 'Z' => Ok(self.inner.z), - 'T' => Ok(self.inner.t), - 'Y' => Ok(self.inner.y), - 'X' => Ok(self.inner.x), - _ => Err(Error::Parse(s.to_string())), - }) - .collect::, _>>()?, - false, - ) - } else if idx.is_instance_of::() { - let i = idx.cast::()?.extract::()?; - let len = self.inner.order.len() as isize; - let i = if i < 0 { i + len } else { i }; - if i < 0 || i >= len { - return Err(PyIndexError::new_err(format!( - "index {} is out of bounds for size {}", - i, len - ))); - } - (vec![i as usize], true) - } else { - return Err(PyErr::new::(format!( - "Unknown type: {:?}", - idx - ))); - }; - let shape = if is_idx { - let mut shape = Vec::new(); - for axis in &self.inner.order { - match axis { - Axis::C => shape.push(self.inner.c), - Axis::Z => shape.push(self.inner.z), - Axis::T => shape.push(self.inner.t), - Axis::Y => shape.push(self.inner.y), - Axis::X => shape.push(self.inner.x), - Axis::New => shape.push(1), - } - } - idx.into_iter() - .map(|i| shape[i % shape.len()]) - .collect::>() - } else { - idx - }; - if shape.is_empty() { - Ok(PyNone::get(py).into_bound_py_any(py)?) - } else if shape.len() == 1 { - Ok(shape[0].into_bound_py_any(py)?) - } else { - Ok(shape.into_bound_py_any(py)?) - } - } - - /// number of dimensions in the shape - fn __len__(&self) -> usize { - self.inner.order.len() - } - - /// convert shape to a list of dimension sizes in order - fn to_list(&self) -> Vec { - vec![ - self.inner.c, - self.inner.z, - self.inner.t, - self.inner.y, - self.inner.x, - ] - } - - /// the axis order as a string (e.g., "CZTYX") - #[getter] - fn axes(&self) -> String { - self.inner - .order - .iter() - .map(|axis| format!("{}", axis)) - .collect::() - } -} - pub(crate) fn ndbioimage_file() -> PathBuf { let file = Python::attach(|py| { py.import("ndbioimage") @@ -2173,7 +53,7 @@ pub fn generate_stub(dest_path: String) -> PyResult<()> { } /// main entry point for the command-line interface -#[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")] +#[gen_stub_pyfunction(module = "ndbioimage")] #[pyfunction] fn main() -> PyResult<()> { Ok(crate::main::main(Some( @@ -2193,23 +73,25 @@ mod ndbioimage_rs { #[cfg(feature = "tiffwrite")] #[pymodule_export] - use super::batch_to_tiff; + use super::imread::batch_to_tiff; #[pymodule_export] use super::generate_stub; #[pymodule_export] - use super::PyView; + use super::imread::PyView; #[pymodule_export] - use super::PyShape; + use super::imread::PyShape; #[pymodule_export] use ome_metadata::py::ome_metadata; - #[pymodule_init] - fn init(_: &Bound<'_, PyModule>) -> PyResult<()> { - let _ = color_eyre::install(); - Ok(()) - } + #[cfg(feature = "transforms")] + #[pymodule_export] + use super::transforms::PyTransform; + + #[cfg(feature = "transforms")] + #[pymodule_export] + use super::transforms::PyTransforms; } diff --git a/src/py/imread.rs b/src/py/imread.rs new file mode 100644 index 0000000..5deb639 --- /dev/null +++ b/src/py/imread.rs @@ -0,0 +1,2217 @@ +use crate::axes::{Axis, Shape}; +use crate::error::Error; +use crate::metadata::Metadata; +use crate::movie::MovieOptions; + +#[cfg(feature = "transforms")] +use crate::py::transforms::{PyTransform, PyTransforms}; +use crate::readers::{DynReader, PixelType, Reader}; +use crate::view::{Item, View}; +use itertools::Itertools; +use ndarray::{Ix0, Ix1, IxDyn, SliceInfoElem}; +use numpy::{ + AllowTypeChange, IntoPyArray, PyArray, PyArrayDescr, PyArrayLike0, PyArrayLike1, + PyArrayMethods, dtype, +}; +use ome_metadata::Ome; +use postcard::{from_bytes, to_stdvec}; +use pyo3::exceptions::{PyIndexError, PyNotImplementedError, PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyBytes, PyEllipsis, PyInt, PyList, PyNone, PySlice, PyString, PyTuple}; +pub(crate) use pyo3::{ + Bound, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyRef, PyResult, Python, pyclass, + pyfunction, pymethods, +}; +use pyo3_stub_gen::derive::*; +use pyo3_stub_gen::inventory::submit; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::path::PathBuf; + +/// class to read image files, while taking good care of important metadata, +/// currently optimized for .czi files, but can open anything that bioformats can handle +/// path: path to the image file +/// optional: +/// axes: order of axes, default: cztyx, but omitting any axes with lenght 1 +/// dtype: datatype to be used when returning frames +/// +/// Examples: +/// >> im = Imread('/path/to/file.image', axes='czt) +/// >> im +/// << shows summary +/// >> im.shape +/// << (15, 26, 1000, 1000) +/// >> im.axes +/// << 'ztyx' +/// >> plt.imshow(im[1, 0]) +/// << plots frame at position z=1, t=0 (python type indexing) +/// >> plt.imshow(im[:, 0].max('z')) +/// << plots max-z projection at t=0 +/// >> im.pxsize +/// << 0.09708737864077668 image-plane pixel size in um +/// >> im.laserwavelengths +/// << [642, 488] +/// >> im.laserpowers +/// << [0.02, 0.0005] in % +/// +/// TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var +#[gen_stub_pyclass] +#[pyclass(subclass, from_py_object, name = "Imread", module = "ndbioimage")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct PyView { + view: View, + dtype: PixelType, + #[serde(skip)] + ome: Ome, + index: usize, +} + +unsafe impl Send for PyView {} +unsafe impl Sync for PyView {} + +impl PyView { + fn item(py: Python, view: View, dtype: PixelType) -> PyResult> { + Ok(match dtype { + PixelType::Bool => todo!(), + PixelType::I8 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U8 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I16 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U16 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + }) + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyView { + /// new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64) + #[new] + #[pyo3(signature = (path, dtype = None, axes = "cztyx", reader = None))] + fn new<'py>( + py: Python<'py>, + #[gen_stub( + override_type(type_repr="str | pathlib.Path | Imread | bytes", imports=("pathlib")) + )] + path: Bound<'py, PyAny>, + #[gen_stub(override_type(type_repr = "typing.Optional[numpy.typing.DTypeLike]", imports=("typing", "numpy", "numpy.typing") + ))] + dtype: Option>, + axes: &str, + reader: Option<&str>, + ) -> PyResult { + if path.is_instance_of::() { + Ok(path.cast_into::()?.extract::()?) + } else if path.is_instance_of::() { + let mut pyview: Self = from_bytes(&path.extract::>()?).map_err(Error::from)?; + pyview.ome = pyview.view.metadata()?; + Ok(pyview) + } else { + let builtins = PyModule::import(py, "builtins")?; + let path = PathBuf::from( + builtins + .getattr("str")? + .call1((path,))? + .cast_into::()? + .extract::()?, + ); + let axes = axes + .chars() + .map(|a| a.to_string().parse().map_err(Error::from)) + .collect::, Error>>()?; + let view = if let Some(reader) = reader { + DynReader::from_path_select_reader(&path, reader)?.view() + } else { + View::<_, DynReader>::from_path(&path)? + } + .permute_axes_dyn(&axes)?; + let dtype = if let Some(dtype) = dtype { + let np = PyModule::import(py, "numpy")?; + let dt = np.getattr("dtype")?.call1((&dtype,))?; + let name = dt.getattr("name")?; + let dtype_str = name.extract::()?; + dtype_str.parse()? + } else { + *view.pixel_type() + }; + let ome = view.metadata()?; + Ok(Self { + view, + dtype, + ome, + index: 0, + }) + } + } + + /// get all available positions (series) in the file + #[staticmethod] + fn get_positions<'py>( + py: Python, + #[gen_stub( + override_type(type_repr="str | pathlib.Path | Imread | bytes", imports=("pathlib")) + )] + path: Bound<'py, PyAny>, + ) -> PyResult> { + Self::get_available_series(py, path, None) + } + + /// only remains for backwards compatibility + #[staticmethod] + fn kill_vm() {} + + /// the name of the reader used to open the file + #[getter] + fn reader_name(&self) -> String { + self.view.reader_name().to_string() + } + + /// reshape the view with a new axis order + #[expect(unused_variables)] + fn reshape<'py>(&self, order: &str, copy: bool) -> PyResult> { + todo!() + } + + /// return a new view with transformations applied (channel alignment, drift correction) + #[cfg(feature = "transforms")] + #[expect(unused_variables)] + #[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))] + fn with_transform<'py>( + &self, + channels: bool, + drift: bool, + file: Option>, + bead_files: Option>, + main_channel: Option, + default_transform: Option>, + ) -> PyResult { + todo!() + } + + /// get the transformation + #[cfg(feature = "transforms")] + #[getter] + fn get_transform(&self) -> PyResult { + Ok(PyTransforms { + inner: self.view.transforms.clone(), + }) + } + + /// set the transformation + #[cfg(feature = "transforms")] + fn set_transform(&mut self, transform: PyTransforms) { + self.view.transforms = transform.inner; + } + + #[cfg(feature = "transforms")] + fn load_transform_from_yaml(&mut self, path: PathBuf) -> PyResult<()> { + self.view.load_transform_from_yaml(path.as_path())?; + Ok(()) + } + + #[cfg(feature = "transforms")] + fn calculate_channel_transforms_2d( + &self, + py: Python, + main_channel: usize, + ) -> PyResult> { + py.detach(|| { + Ok(self + .view + .calculate_channel_transforms_2d(main_channel)? + .into_iter() + .map(|t| PyTransform { + inner: t.into_dyn(), + }) + .collect()) + }) + } + + #[cfg(feature = "transforms")] + fn calculate_channel_transforms_3d( + &self, + py: Python, + main_channel: usize, + ) -> PyResult> { + py.detach(|| { + Ok(self + .view + .calculate_channel_transforms_3d(main_channel)? + .into_iter() + .map(|t| PyTransform { + inner: t.into_dyn(), + }) + .collect()) + }) + } + + #[cfg(feature = "transforms")] + fn calculate_drift_transform_2d(&self, py: Python) -> PyResult> { + py.detach(|| { + Ok(self + .view + .calculate_drift_transform_2d()? + .into_iter() + .map(|t| PyTransform { + inner: t.into_dyn(), + }) + .collect()) + }) + } + + #[cfg(feature = "transforms")] + fn calculate_drift_transform_3d(&self, py: Python) -> PyResult> { + py.detach(|| { + Ok(self + .view + .calculate_drift_transform_3d()? + .into_iter() + .map(|t| PyTransform { + inner: t.into_dyn(), + }) + .collect()) + }) + } + + #[gen_stub(override_return_type(type_repr="numpy.ndarray | int | float", imports=("numpy")))] + fn squeeze<'py>(&self, py: Python<'py>) -> PyResult> { + let view = self.view.squeeze()?; + if view.ndim() == 0 { + Ok(match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U8 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I16 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U16 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + }) + } else { + PyView { + view, + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + } + } + + /// close the file: does nothing as this is handled automatically + fn close(&self) -> PyResult<()> { + Ok(()) + } + + /// change the data type of the view: (u)int(8/16/32) or float(32/64) + fn as_type( + &self, + py: Python<'_>, + #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing") + ))] + dtype: Bound<'_, PyAny>, + ) -> PyResult { + let np = PyModule::import(py, "numpy")?; + let dt = np.getattr("dtype")?.call1((&dtype,))?; + let name = dt.getattr("name")?; + let dtype_str = name.extract::()?; + Ok(PyView { + view: self.view.clone(), + dtype: dtype_str.parse()?, + ome: self.ome.clone(), + index: 0, + }) + } + + /// change the data type of the view: (u)int(8/16/32) or float(32/64) + fn astype( + &self, + py: Python<'_>, + #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing") + ))] + dtype: Bound<'_, PyAny>, + ) -> PyResult { + self.as_type(py, dtype) + } + + /// slice the view and return a new view or a single number + fn __getitem__<'py>( + &self, + py: Python<'py>, + n: Bound<'py, PyAny>, + ) -> PyResult> { + // TODO: newaxis + let slice: Vec<_> = if n.is_instance_of::() { + n.cast_into::()?.into_iter().collect() + } else if n.is_instance_of::() { + n.cast_into::()?.into_iter().collect() + } else { + vec![n] + }; + let mut new_slice = Vec::new(); + let mut ellipsis = None; + let shape = self.view.shape(); + for (i, (s, t)) in slice.iter().zip(shape.iter()).enumerate() { + if s.is_none() { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }); + } else if s.is_instance_of::() { + new_slice.push(SliceInfoElem::Index(s.cast::()?.extract::()?)); + } else if s.is_instance_of::() { + let u = s.cast::()?.indices(*t as isize)?; + new_slice.push(SliceInfoElem::Slice { + start: u.start, + end: Some(u.stop), + step: u.step, + }); + } else if s.is_instance_of::() { + if ellipsis.is_some() { + return Err(PyErr::new::( + "cannot have more than one ellipsis".to_string(), + )); + } + let _ = ellipsis.insert(i); + } else if let Ok(arr_like) = s.extract::>() { + let pyarr: &Bound> = &arr_like; + let mut index = *pyarr.readonly().as_array().into_scalar(); + let index0 = index; + if index < 0 { + index += *t as isize; + } + if (index < 0) || (index >= *t as isize) { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for axis {} with size {}", + index0, i, t + ))); + } + new_slice.push(SliceInfoElem::Index(index)); + } else if let Ok(arr_like) = s.extract::>() { + let pyarr: &Bound> = &arr_like; + let read = pyarr.readonly(); + let mut indices = read.as_array().to_vec(); + for index in indices.iter_mut() { + let index0 = *index; + if *index < 0 { + *index += *t as isize; + } + if (*index < 0) || (*index >= *t as isize) { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for axis {} with size {}", + index0, i, t + ))); + } + } + if indices.is_empty() { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: Some(0), + step: 1, + }) + } else { + let d = indices + .windows(2) + .map(|i| i[1] - i[0]) + .collect::>(); + if d.is_empty() { + let index = indices[0]; + new_slice.push(SliceInfoElem::Slice { + start: index, + end: Some(index + 1), + step: 1, + }); + } else if d.len() == 1 { + new_slice.push(SliceInfoElem::Slice { + start: indices[0], + end: indices.last().map(|j| j + 1), + step: d.into_iter().collect::>()[0], + }); + } else { + return Err(PyValueError::new_err("indices array must regularly spaced")); + } + }; + } else { + return Err(PyValueError::new_err(format!( + "cannot convert {:?} to slice", + s + ))); + } + } + if new_slice.len() > shape.len() { + return Err(PyValueError::new_err(format!( + "got more indices ({}) than dimensions ({})", + new_slice.len(), + shape.len() + ))); + } + while new_slice.len() < shape.len() { + if let Some(i) = ellipsis { + new_slice.insert( + i, + SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }, + ) + } else { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }) + } + } + let view = self.view.slice(new_slice.as_slice())?; + if view.ndim() == 0 { + Self::item(py, view, self.dtype) + } else { + PyView { + view, + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + } + } + + /// convert to a numpy array, optionally with a different dtype + #[expect(unused_variables)] + #[pyo3(signature = (dtype = None, copy = None))] + fn __array__<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type(type_repr = "typing.Optional[numpy.typing.DTypeLike]", imports=("typing", "numpy", "numpy.typing") + ))] + dtype: Option>, + copy: Option, + ) -> PyResult> { + if let Some(dtype) = dtype { + self.as_type(py, dtype)?.as_array(py) + } else { + self.as_array(py) + } + } + + /// check if an item is contained in the view (not implemented) + fn __contains__(&self, _item: Bound) -> PyResult { + Err(PyNotImplementedError::new_err("contains not implemented")) + } + + /// element-wise less than comparison + fn __lt__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("less")?.call1((&a, &b)) + } + + /// element-wise less than or equal comparison + fn __le__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("less_equal")?.call1((&a, &b)) + } + + /// element-wise equality comparison + fn __eq__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("equal")?.call1((&a, &b)) + } + + /// element-wise not equal comparison + fn __ne__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("not_equal")?.call1((&a, &b)) + } + + /// element-wise greater than comparison + fn __gt__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("greater")?.call1((&a, &b)) + } + + /// element-wise greater than or equal comparison + fn __ge__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("greater_equal")?.call1((&a, &b)) + } + + /// element-wise addition + fn __add__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("add")?.call1((&a, &b)) + } + + /// element-wise addition (reflected) + fn __radd__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("add")?.call1((&a, &b)) + } + + /// element-wise subtraction + fn __sub__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("subtract")?.call1((&a, &b)) + } + + /// element-wise subtraction (reflected) + fn __rsub__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("subtract")?.call1((&a, &b)) + } + + /// element-wise multiplication + fn __mul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("multiply")?.call1((&a, &b)) + } + + /// element-wise multiplication (reflected) + fn __rmul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("multiply")?.call1((&a, &b)) + } + + /// element-wise true division + fn __truediv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("true_divide")?.call1((&a, &b)) + } + + /// element-wise true division (reflected) + fn __rtruediv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("true_divide")?.call1((&a, &b)) + } + + /// element-wise floor division + fn __floordiv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("floor_divide")?.call1((&a, &b)) + } + + /// element-wise floor division (reflected) + fn __rfloordiv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("floor_divide")?.call1((&a, &b)) + } + + /// element-wise modulo + fn __mod__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("remainder")?.call1((&a, &b)) + } + + /// element-wise modulo (reflected) + fn __rmod__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("remainder")?.call1((&a, &b)) + } + + #[gen_stub(skip)] + fn __pow__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + r#mod: Option>, + ) -> PyResult> { + if r#mod.is_some() { + return Err(PyNotImplementedError::new_err( + "cannot use pow or ** on Imread with mod != None", + )); + } + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("power")?.call1((&a, &b)) + } + + #[gen_stub(skip)] + fn __rpow__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + r#mod: Option>, + ) -> PyResult> { + if r#mod.is_some() { + return Err(PyNotImplementedError::new_err( + "cannot use pow or ** on Imread with mod != None", + )); + } + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("power")?.call1((&a, &b)) + } + + /// element-wise matrix multiplication + fn __matmul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("matmul")?.call1((&a, &b)) + } + + /// element-wise matrix multiplication (reflected) + fn __rmatmul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("matmul")?.call1((&a, &b)) + } + + /// element-wise bitwise AND + fn __and__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_and")?.call1((&a, &b)) + } + + /// element-wise bitwise AND (reflected) + fn __rand__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_and")?.call1((&a, &b)) + } + + /// element-wise bitwise OR + fn __or__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_or")?.call1((&a, &b)) + } + + /// element-wise bitwise OR (reflected) + fn __ror__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_or")?.call1((&a, &b)) + } + + /// element-wise bitwise XOR + fn __xor__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_xor")?.call1((&a, &b)) + } + + /// element-wise bitwise XOR (reflected) + fn __rxor__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_xor")?.call1((&a, &b)) + } + + /// element-wise left shift + fn __lshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("left_shift")?.call1((&a, &b)) + } + + /// element-wise left shift (reflected) + fn __rlshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("left_shift")?.call1((&a, &b)) + } + + /// element-wise right shift + fn __rshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("right_shift")?.call1((&a, &b)) + } + + /// element-wise right shift (reflected) + fn __rrshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("right_shift")?.call1((&a, &b)) + } + + /// element-wise negation + fn __neg__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("negative")?.call1((&a,)) + } + + /// element-wise positive + fn __pos__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("positive")?.call1((&a,)) + } + + /// element-wise absolute value + fn __abs__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("absolute")?.call1((&a,)) + } + + /// element-wise bitwise inversion + fn __invert__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("invert")?.call1((&a,)) + } + + /// context manager entry + fn __enter__<'py>(slf: PyRef<'py, Self>) -> PyResult> { + Ok(slf) + } + + /// context manager exit + #[expect(unused_variables)] + #[pyo3(signature = (exc_type=None, exc_val=None, exc_tb=None))] + fn __exit__( + &self, + exc_type: Option>, + exc_val: Option>, + exc_tb: Option>, + ) -> PyResult<()> { + self.close() + } + + /// arguments for pickling + pub(crate) fn __getnewargs__(&self) -> PyResult<(Vec,)> { + Ok((to_stdvec(self).map_err(Error::from)?,)) + } + + /// shallow copy of the view + fn __copy__(&self) -> Self { + Self { + view: self.view.clone(), + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + } + + /// deep copy of the view (same as shallow copy for this type) + fn __deepcopy__(&self) -> Self { + Self { + view: self.view.clone(), + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + } + + /// create a copy of the view + fn copy(&self) -> Self { + Self { + view: self.view.clone(), + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + } + + /// iterate over the first axis + fn __iter__(&self) -> Self { + Self { + view: self.view.clone(), + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + } + + /// get the next item in the iteration + fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult>> { + let shape = self.view.shape(); + if shape.is_empty() || (self.index == shape[0]) { + self.index = 0; + Ok(None) + } else { + let mut new_slice = vec![SliceInfoElem::Index(self.index as isize)]; + self.index += 1; + for _ in 1..shape.len() { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }) + } + let view = self.view.slice(new_slice.as_slice())?; + Some(if view.ndim() == 0 { + Self::item(py, view, self.dtype) + } else { + PyView { + view, + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + }) + .transpose() + } + } + + /// number of elements in the first axis + fn __len__(&self) -> PyResult { + Ok(self.view.len()) + } + + /// string representation with a summary of the image + fn __repr__(&self) -> PyResult { + Ok(self.view.summary()?) + } + + /// the file path as a string + fn __str__(&self) -> PyResult { + Ok(self.view.path().display().to_string()) + } + + /// retrieve a single frame at czt, sliced accordingly + fn get_frame<'py>( + &self, + py: Python<'py>, + c: isize, + z: isize, + t: isize, + ) -> PyResult> { + Ok(match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::U8 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::I16 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::U16 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::I32 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::U32 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::F32 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::F64 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::I64 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::U64 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::I128 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::U128 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + PixelType::F128 => self + .view + .get_frame::(c, z, t)? + .into_pyarray(py) + .into_any(), + }) + } + + /// flatten the view into a 1D numpy array + fn flatten<'py>(&self, py: Python<'py>) -> PyResult> { + Ok(match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::U8 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::I16 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::U16 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::I32 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::U32 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::F32 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::F64 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::I64 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::U64 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::I128 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::U128 => self.view.flatten::()?.into_pyarray(py).into_any(), + PixelType::F128 => self.view.flatten::()?.into_pyarray(py).into_any(), + }) + } + + /// convert the view to bytes + fn to_bytes(&self) -> PyResult> { + Ok(match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => self.view.to_bytes::()?, + PixelType::U8 => self.view.to_bytes::()?, + PixelType::I16 => self.view.to_bytes::()?, + PixelType::U16 => self.view.to_bytes::()?, + PixelType::I32 => self.view.to_bytes::()?, + PixelType::U32 => self.view.to_bytes::()?, + PixelType::F32 => self.view.to_bytes::()?, + PixelType::F64 => self.view.to_bytes::()?, + PixelType::I64 => self.view.to_bytes::()?, + PixelType::U64 => self.view.to_bytes::()?, + PixelType::I128 => self.view.to_bytes::()?, + PixelType::U128 => self.view.to_bytes::()?, + PixelType::F128 => self.view.to_bytes::()?, + }) + } + + /// convert the view to bytes (alias for to_bytes) + fn tobytes(&self) -> PyResult> { + self.to_bytes() + } + + /// retrieve the ome metadata as an XML string + #[gen_stub(skip)] + #[getter] + fn get_ome(&self) -> Ome { + self.ome.clone() + } + + /// the file path + #[getter] + fn path(&self) -> PyResult { + Ok(self.view.path().to_owned()) + } + + /// the series in the file + #[getter] + fn series(&self) -> PyResult { + Ok(self.view.series()) + } + + /// the axes in the view + #[getter] + fn axes(&self) -> String { + self.view.axes().iter().map(|a| format!("{:?}", a)).join("") + } + + /// the shape of the view + #[getter] + fn shape(&self) -> PyShape { + PyShape { + inner: self.view.shape(), + } + } + + /// the current slice applied to the view + #[getter] + fn slice(&self) -> PyResult> { + Ok(self + .view + .get_slice() + .iter() + .map(|s| format!("{:#?}", s)) + .collect()) + } + + /// the number of pixels in the view + #[getter] + fn size(&self) -> usize { + self.view.size() + } + + /// the number of dimensions in the view + #[getter] + fn ndim(&self) -> usize { + self.view.ndim() + } + + /// find the position of an axis + fn get_ax( + &self, + #[gen_stub(override_type(type_repr = "int | str"))] axis: Bound, + ) -> PyResult { + if axis.is_instance_of::() { + let axis = axis + .cast_into::()? + .extract::()? + .parse::() + .map_err(Error::from)?; + self.view + .axes() + .iter() + .position(|a| *a == axis) + .ok_or_else(|| { + PyErr::new::(format!("cannot find axis {:?}", axis)) + }) + } else if axis.is_instance_of::() { + Ok(axis.cast_into::()?.extract::()?) + } else { + Err(PyErr::new::( + "cannot convert to axis".to_string(), + )) + } + } + + /// swap two axes + fn swap_axes( + &self, + #[gen_stub(override_type(type_repr = "int | str"))] ax0: Bound, + #[gen_stub(override_type(type_repr = "int | str"))] ax1: Bound, + ) -> PyResult { + let ax0 = self.get_ax(ax0)?; + let ax1 = self.get_ax(ax1)?; + let view = self.view.swap_axes(ax0, ax1)?; + Ok(PyView { + view, + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + }) + } + + /// permute the order of the axes + #[pyo3(signature = (axes = None))] + fn transpose( + &self, + #[gen_stub(override_type(type_repr="typing.Optional[typing.Sequence[int | str]]", imports=("typing") + ))] + axes: Option>>, + ) -> PyResult { + let view = if let Some(axes) = axes { + let ax = axes + .into_iter() + .map(|a| self.get_ax(a)) + .collect::, _>>()?; + self.view.permute_axes(&ax)? + } else { + self.view.transpose()? + }; + Ok(PyView { + view, + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + }) + } + + /// transposed view (alias for transpose(None)) + #[expect(non_snake_case)] + #[getter] + fn T(&self) -> PyResult { + self.transpose(None) + } + + /// collect data into a numpy array + fn as_array<'py>(&self, py: Python<'py>) -> PyResult> { + Ok(match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::U8 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::I16 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::U16 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::I32 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::U32 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::F32 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::F64 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::I64 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::U64 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::I128 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::U128 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + PixelType::F128 => self.view.as_array_dyn::()?.into_pyarray(py).into_any(), + }) + } + + /// the numpy dtype of the view + #[gen_stub(override_return_type(type_repr = "numpy.dtype", imports=("numpy")))] + #[getter] + fn get_dtype<'py>(&self, py: Python<'py>) -> PyResult> { + match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => Ok(dtype::(py)), + PixelType::U8 => Ok(dtype::(py)), + PixelType::I16 => Ok(dtype::(py)), + PixelType::U16 => Ok(dtype::(py)), + PixelType::I32 => Ok(dtype::(py)), + PixelType::U32 => Ok(dtype::(py)), + PixelType::F32 => Ok(dtype::(py)), + PixelType::F64 => Ok(dtype::(py)), + PixelType::I64 => Ok(dtype::(py)), + PixelType::U64 => Ok(dtype::(py)), + PixelType::I128 => Err(PyTypeError::new_err( + "type is i128, but this cannot be represented by numpy.dtype", + )), + PixelType::U128 => Err(PyTypeError::new_err( + "type is u128, but this cannot be represented by numpy.dtype", + )), + PixelType::F128 => Ok(dtype::(py)), + } + } + + /// set the dtype of the view + #[gen_stub(skip)] + #[setter] + fn set_dtype(&mut self, py: Python, dtype: Bound<'_, PyAny>) -> PyResult<()> { + let np = PyModule::import(py, "numpy")?; + let dt = np.getattr("dtype")?.call1((&dtype,))?; + let name = dt.getattr("name")?; + let dtype_str = name.extract::()?; + self.dtype = dtype_str.parse()?; + Ok(()) + } + + /// get the maximum overall or along a given axis + #[expect(clippy::too_many_arguments)] + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=None, r#where=true), text_signature = "axis: str | int" + )] + fn max<'py>( + &self, + py: Python<'py>, + axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + initial: Option, + r#where: bool, + ) -> PyResult> { + if let Some(i) = initial + && i != 0 + { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if let Some(axis) = axis { + PyView { + dtype: self.dtype, + view: self.view.max_proj(self.get_ax(axis)?)?, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + } else { + Ok(match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::U8 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::I16 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::U16 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::I32 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::U32 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::F32 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::F64 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::I64 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::U64 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::I128 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::U128 => self.view.max::()?.into_pyobject(py)?.into_any(), + PixelType::F128 => self.view.max::()?.into_pyobject(py)?.into_any(), + }) + } + } + + /// get the minimum overall or along a given axis + #[expect(clippy::too_many_arguments)] + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int" + )] + fn min<'py>( + &self, + py: Python<'py>, + axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + initial: Option, + r#where: bool, + ) -> PyResult> { + if let Some(i) = initial + && i != 0 + { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if let Some(axis) = axis { + PyView { + dtype: self.dtype, + view: self.view.min_proj(self.get_ax(axis)?)?, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + } else { + Ok(match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::U8 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::I16 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::U16 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::I32 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::U32 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::F32 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::F64 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::I64 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::U64 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::I128 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::U128 => self.view.min::()?.into_pyobject(py)?.into_any(), + PixelType::F128 => self.view.min::()?.into_pyobject(py)?.into_any(), + }) + } + } + + /// get the mean overall or along a given axis + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int" + )] + fn mean<'py>( + &self, + py: Python<'py>, + axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + r#where: bool, + ) -> PyResult> { + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if let Some(axis) = axis { + let dtype = if let PixelType::F32 = self.dtype { + PixelType::F32 + } else { + PixelType::F64 + }; + PyView { + dtype, + view: self.view.mean_proj(self.get_ax(axis)?)?, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + } else { + Ok(match self.dtype { + PixelType::F32 => self.view.mean::()?.into_pyobject(py)?.into_any(), + _ => self.view.mean::()?.into_pyobject(py)?.into_any(), + }) + } + } + + /// get the sum overall or along a given axis + #[expect(clippy::too_many_arguments)] + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int" + )] + fn sum<'py>( + &self, + py: Python<'py>, + axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + initial: Option, + r#where: bool, + ) -> PyResult> { + if let Some(i) = initial + && i != 0 + { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + let dtype = match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => PixelType::I16, + PixelType::U8 => PixelType::U16, + PixelType::I16 => PixelType::I32, + PixelType::U16 => PixelType::U32, + PixelType::I32 => PixelType::I64, + PixelType::U32 => PixelType::U64, + PixelType::F32 => PixelType::F32, + PixelType::F64 => PixelType::F64, + PixelType::I64 => PixelType::I128, + PixelType::U64 => PixelType::U128, + PixelType::I128 => PixelType::I128, + PixelType::U128 => PixelType::U128, + PixelType::F128 => PixelType::F128, + }; + if let Some(axis) = axis { + PyView { + dtype, + view: self.view.sum_proj(self.get_ax(axis)?)?, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + } else { + Ok(match self.dtype { + PixelType::F32 => self.view.sum::()?.into_pyobject(py)?.into_any(), + PixelType::F64 => self.view.sum::()?.into_pyobject(py)?.into_any(), + PixelType::I64 => self.view.sum::()?.into_pyobject(py)?.into_any(), + PixelType::U64 => self.view.sum::()?.into_pyobject(py)?.into_any(), + PixelType::I128 => self.view.sum::()?.into_pyobject(py)?.into_any(), + PixelType::U128 => self.view.sum::()?.into_pyobject(py)?.into_any(), + PixelType::F128 => self.view.sum::()?.into_pyobject(py)?.into_any(), + _ => self.view.sum::()?.into_pyobject(py)?.into_any(), + }) + } + } + + /// whether the view contains a z-stack (more than one z slice) + #[getter] + fn z_stack(&self) -> PyResult { + if let Some(s) = self.view.size_ax(Axis::Z) { + Ok(s > 1) + } else { + Ok(false) + } + } + + /// backwards compatibility + #[getter] + fn zstack(&self) -> PyResult { + if let Some(s) = self.view.size_ax(Axis::Z) { + Ok(s > 1) + } else { + Ok(false) + } + } + + /// whether the view contains a time series (more than one time point) + #[getter] + fn time_series(&self) -> PyResult { + if let Some(s) = self.view.size_ax(Axis::T) { + Ok(s > 1) + } else { + Ok(false) + } + } + + /// backwards compatibility + #[getter] + fn timeseries(&self) -> PyResult { + if let Some(s) = self.view.size_ax(Axis::T) { + Ok(s > 1) + } else { + Ok(false) + } + } + + /// the pixel size in micrometers + #[getter] + fn pixel_size(&self) -> PyResult> { + Ok(self.ome.pixel_size()?) + } + + /// backwards compatibility + #[getter] + fn pxsize_um(&self) -> PyResult> { + Ok(self.ome.pixel_size()?.map(|p| p / 1000.)) + } + + /// backwards compatibility + #[getter] + fn deltaz_um(&self) -> PyResult> { + Ok(self.ome.delta_z()?.map(|p| p / 1000.)) + } + + /// the z-step size in micrometers + #[getter] + fn delta_z(&self) -> PyResult> { + Ok(self.ome.delta_z()?) + } + + /// the time interval between frames in seconds + #[getter] + fn time_interval(&self) -> PyResult> { + Ok(self.ome.time_interval()?) + } + + /// backwards compatibility + #[getter] + fn timeinterval(&self) -> PyResult> { + Ok(self.ome.time_interval()?) + } + + /// the exposure time for a given channel in seconds + fn exposure_time(&self, channel: usize) -> PyResult> { + Ok(self.ome.exposure_time(channel)?) + } + + /// backwards compatibility + #[getter] + fn exposuretime_s(&self) -> PyResult>> { + Ok((0..self.view.shape().c) + .map(|c| self.ome.exposure_time(c)) + .collect::, Error>>()?) + } + + /// the binning for a given channel + fn binning(&self, channel: usize) -> Option { + self.ome.binning(channel) + } + + /// the laser wavelength for a given channel in nanometers + fn laser_wavelengths(&self, channel: usize) -> PyResult> { + Ok(self.ome.laser_wavelengths(channel)?) + } + + /// the laser power for a given channel as a fraction + fn laser_power(&self, channel: usize) -> PyResult> { + Ok(self.ome.laser_powers(channel)?) + } + + /// the name of the objective + #[getter] + fn objective_name(&self) -> Option { + self.ome.objective_name() + } + + /// the total magnification (objective × tube lens) + #[getter] + fn magnification(&self) -> Option { + self.ome.magnification() + } + + /// the name of the tube lens + #[getter] + fn tube_lens_name(&self) -> Option { + self.ome.tube_lens_name() + } + + /// the name of the filter set for a given channel + fn filter_set_name(&self, channel: usize) -> Option { + self.ome.filter_set_name(channel) + } + + /// the detector gain for a given channel + fn gain(&self, channel: usize) -> Option { + self.ome.gain(channel) + } + + /// gives a helpful summary of the recorded experiment + fn summary(&self) -> PyResult { + Ok(self.view.summary()?) + } + + /// get all series contained in the file + #[staticmethod] + #[pyo3(signature = (path, reader = None))] + fn get_available_series<'py>( + py: Python<'py>, + #[gen_stub(override_type(type_repr="str | pathlib.Path | Imread", imports=("pathlib")))] + path: Bound<'py, PyAny>, + reader: Option<&str>, + ) -> PyResult> { + let path = if path.is_instance_of::() { + let py_view: Self = path.cast_into::()?.extract::()?; + py_view.view.path().to_owned() + } else { + let builtins = PyModule::import(py, "builtins")?; + PathBuf::from( + builtins + .getattr("str")? + .call1((path,))? + .cast_into::()? + .extract::()?, + ) + }; + + Ok(DynReader::get_available_series_select_reader(path, reader)?) + } + + /// get all series contained in the file + #[staticmethod] + #[pyo3(signature = (path, series, reader = None))] + fn get_available_positions<'py>( + py: Python<'py>, + #[gen_stub(override_type(type_repr="str | pathlib.Path | Imread", imports=("pathlib")))] + path: Bound<'py, PyAny>, + series: usize, + reader: Option<&str>, + ) -> PyResult> { + let path = if path.is_instance_of::() { + let py_view: Self = path.cast_into::()?.extract::()?; + py_view.view.path().to_owned() + } else { + let builtins = PyModule::import(py, "builtins")?; + PathBuf::from( + builtins + .getattr("str")? + .call1((path,))? + .cast_into::()? + .extract::()?, + ) + }; + Ok(DynReader::get_available_positions_select_reader( + path, series, reader, + )?) + } + + /// save the view as a TIFF file + #[cfg(feature = "tiffwrite")] + #[pyo3(signature = (file, colors = None, overwrite = false, bar = true))] + fn save_as_tiff( + &self, + py: Python, + file: PathBuf, + colors: Option>, + overwrite: bool, + bar: bool, + ) -> PyResult<()> { + let bar = if bar { + Some(crate::utils::progress::get_bar( + Some(0), + Some("writing tiff file".to_string()), + )) + } else { + None + }; + let options = + crate::tiffwrite::TiffOptions::new(bar, None, colors.unwrap_or_default(), overwrite)?; + py.detach(|| match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U8 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I16 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U16 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I32 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U32 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I64 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U64 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I128 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U128 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::F32 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::F64 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::F128 => Err(Error::NotImplemented( + "saving as f128 is not implemented".to_string(), + )), + })?; + Ok(()) + } + + /// save the view as a movie file (MP4) + #[cfg(feature = "movie")] + #[expect(clippy::too_many_arguments)] + #[pyo3(signature = (file, speed = 1.0, brightness = None, scale = 1.0, colors = None, overwrite = false, register = false, no_scaling = false) + )] + fn save_as_movie( + &self, + file: PathBuf, + speed: f64, + brightness: Option>, + scale: f64, + colors: Option>, + overwrite: bool, + register: bool, + no_scaling: bool, + ) -> PyResult<()> { + let options = MovieOptions::new( + speed, + brightness.unwrap_or_default(), + scale, + colors.unwrap_or_default(), + overwrite, + register, + no_scaling, + )?; + self.view.save_as_movie(file, &options)?; + Ok(()) + } + + /// backwards compatibility + #[expect(unused_variables)] + fn set_cache_size(&self, size: usize) {} +} + +submit! { + gen_methods_from_python! { + r#" + import typing + import numpy.typing + + class PyView: + def __pow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... + def __rpow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... + + def max(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: + """ Return the maximum along a given axis. Arguments beyond axis are not implemented """ + + def min(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: + """ Return the minimum along a given axis. Arguments beyond axis are not implemented """ + + def mean(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False) -> Imread | numpy.typing.NDArray | int | float: + """ Return the mean along a given axis. Arguments beyond axis are not implemented """ + + def sum(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: + """ Return the sum along a given axis. Arguments beyond axis are not implemented """ + "# + } +} + +/// batch convert multiple image files to TIFF format +#[cfg(feature = "tiffwrite")] +#[expect(clippy::too_many_arguments)] +#[gen_stub_pyfunction(module = "ndbioimage")] +#[pyfunction] +#[pyo3(signature = (files_in, files_out, operations = None, colors = None, overwrite = false, bar = true, message = None) +)] +pub(crate) fn batch_to_tiff( + py: Python, + files_in: Vec, + files_out: Vec, + operations: Option>, + colors: Option>, + overwrite: bool, + bar: bool, + message: Option, +) -> PyResult<()> { + py.detach(|| { + crate::tiffwrite::batch_to_tiff( + &files_in, &files_out, operations, colors, overwrite, bar, message, + ) + })?; + Ok(()) +} + +/// represents the shape of an image with named dimensions (c, z, t, y, x) +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + frozen, + name = "Shape", + module = "ndbioimage" +)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct PyShape { + inner: Shape, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyShape { + /// create a new shape with the given dimensions + #[new] + #[pyo3(signature = (order, c = 1, z = 1, t = 1, y = 1, x = 1))] + fn new(order: String, c: usize, z: usize, t: usize, y: usize, x: usize) -> PyResult { + Ok(Self { + inner: Shape { + c, + z, + t, + y, + x, + order: order + .chars() + .map(|c| c.to_uppercase().to_string().parse::()) + .collect::, _>>() + .map_err(Error::from)?, + }, + }) + } + + /// the number of channels + #[getter] + fn get_c(&self) -> usize { + self.inner.c + } + + /// the number of z slices + #[getter] + fn get_z(&self) -> usize { + self.inner.z + } + + /// the number of time points + #[getter] + fn get_t(&self) -> usize { + self.inner.t + } + + /// the number of pixels along y + #[getter] + fn get_y(&self) -> usize { + self.inner.y + } + + /// the number of pixels along x + #[getter] + fn get_x(&self) -> usize { + self.inner.x + } + + /// string representation + fn __str__(&self) -> String { + format!("{}", self.inner) + } + + /// detailed representation for debugging + fn __repr__(&self) -> String { + format!( + "Shape({}, {}, {}, {}, {})", + self.inner.c, self.inner.z, self.inner.t, self.inner.x, self.inner.y + ) + } + + /// arguments for pickling + fn __getnewargs__(&self) -> (String, usize, usize, usize, usize, usize) { + ( + self.inner + .order + .iter() + .map(|axis| format!("{}", axis)) + .collect::>() + .join(""), + self.inner.c, + self.inner.z, + self.inner.t, + self.inner.y, + self.inner.x, + ) + } + + /// get dimension size by index or axis name + #[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing") + ))] + fn __getitem__<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type( + type_repr = "str | int | None | Ellipsis | slice | list[int] | tuple[int]" + ))] + idx: Bound<'py, PyAny>, + ) -> PyResult> { + let (idx, is_idx) = if idx.is_instance_of::() || idx.is_instance_of::() + { + ((0..self.inner.order.len()).collect(), true) + } else if idx.is_instance_of::() { + let indices = idx + .cast::()? + .indices(self.inner.order.len() as isize)?; + ( + if indices.step > 0 { + (indices.start..indices.stop) + .step_by(indices.step as usize) + .map(|i| i as usize) + .collect::>() + } else { + (indices.stop..indices.start) + .step_by(-indices.step as usize) + .map(|i| i as usize) + .collect::>() + }, + true, + ) + } else if idx.is_instance_of::() { + (idx.cast::()?.extract::>()?, true) + } else if idx.is_instance_of::() { + (idx.cast::()?.extract::>()?, true) + } else if idx.is_instance_of::() { + let s = idx.cast::()?.extract::()?; + ( + s.to_uppercase() + .chars() + .map(|i| match i { + 'C' => Ok(self.inner.c), + 'Z' => Ok(self.inner.z), + 'T' => Ok(self.inner.t), + 'Y' => Ok(self.inner.y), + 'X' => Ok(self.inner.x), + _ => Err(Error::Parse(s.to_string())), + }) + .collect::, _>>()?, + false, + ) + } else if idx.is_instance_of::() { + let i = idx.cast::()?.extract::()?; + let len = self.inner.order.len() as isize; + let i = if i < 0 { i + len } else { i }; + if i < 0 || i >= len { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for size {}", + i, len + ))); + } + (vec![i as usize], true) + } else { + return Err(PyErr::new::(format!( + "Unknown type: {:?}", + idx + ))); + }; + let shape = if is_idx { + let mut shape = Vec::new(); + for axis in &self.inner.order { + match axis { + Axis::C => shape.push(self.inner.c), + Axis::Z => shape.push(self.inner.z), + Axis::T => shape.push(self.inner.t), + Axis::Y => shape.push(self.inner.y), + Axis::X => shape.push(self.inner.x), + Axis::New => shape.push(1), + } + } + idx.into_iter() + .map(|i| shape[i % shape.len()]) + .collect::>() + } else { + idx + }; + if shape.is_empty() { + Ok(PyNone::get(py).into_bound_py_any(py)?) + } else if shape.len() == 1 { + Ok(shape[0].into_bound_py_any(py)?) + } else { + Ok(shape.into_bound_py_any(py)?) + } + } + + /// number of dimensions in the shape + fn __len__(&self) -> usize { + self.inner.order.len() + } + + /// convert shape to a list of dimension sizes in order + fn to_list(&self) -> Vec { + vec![ + self.inner.c, + self.inner.z, + self.inner.t, + self.inner.y, + self.inner.x, + ] + } + + /// the axis order as a string (e.g., "CZTYX") + #[getter] + fn axes(&self) -> String { + self.inner + .order + .iter() + .map(|axis| format!("{}", axis)) + .collect::() + } +} diff --git a/src/py/transforms.rs b/src/py/transforms.rs new file mode 100644 index 0000000..20714ef --- /dev/null +++ b/src/py/transforms.rs @@ -0,0 +1,675 @@ +use crate::error::Error; +use crate::transforms::Transforms; +use image_registration::transform::Transform; +use ndarray::{Ix0, Ix1, Ix2, Ix3, Ix4, Ix5, IxDyn}; +use numpy::{ + AllowTypeChange, IntoPyArray, PyArray, PyArray1, PyArray2, PyArrayDyn, PyArrayLike1, + PyArrayLike2, PyArrayLikeDyn, +}; +use postcard::{from_bytes, to_stdvec}; +use pyo3::exceptions::{PyNotImplementedError, PyValueError}; +use pyo3::prelude::*; +use pyo3_stub_gen::derive::*; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + eq, + name = "Transform", + module = "ndbioimage" +)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct PyTransform { + pub(crate) inner: Transform, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyTransform { + #[new] + #[pyo3(signature = (parameters, shape, center = None))] + fn new(parameters: Vec, shape: Vec, center: Option>) -> Self { + if let Some(center) = center { + Self { + inner: Transform::new_with_center(parameters, center, shape), + } + } else { + Self { + inner: Transform::new(parameters, shape), + } + } + } + + fn __getnewargs__(&self) -> (Vec, Vec, Option>) { + ( + self.inner.parameters.clone(), + self.inner.shape.clone(), + Some(self.inner.center.clone()), + ) + } + + fn __getstate__(&self) -> Vec { + self.inner.dparameters.clone() + } + + fn __setstate__(&mut self, state: Vec) { + self.inner.dparameters = state; + } + + fn __add__(&self, other: &PyTransform) -> PyTransform { + PyTransform { + inner: &self.inner + &other.inner, + } + } + + fn __radd__(&self, other: &PyTransform) -> PyTransform { + PyTransform { + inner: &other.inner + &self.inner, + } + } + + fn __sub__(&self, other: &PyTransform) -> PyTransform { + PyTransform { + inner: &self.inner - &other.inner, + } + } + + fn __rsub__(&self, other: &PyTransform) -> PyTransform { + PyTransform { + inner: &other.inner - &self.inner, + } + } + + fn __mul__( + &self, + py: Python, + #[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound, + ) -> PyResult { + if other.is_instance_of::() { + Ok(PyTransform { + inner: &self.inner * &other.extract::()?.inner, + }) + } else { + let builtins = PyModule::import(py, "builtins")?; + let other = builtins.getattr("float")?.call1((&other,))?; + Ok(PyTransform { + inner: &self.inner * other.extract::()?, + }) + } + } + + fn __rmul__( + &self, + py: Python, + #[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound, + ) -> PyResult { + if other.is_instance_of::() { + Ok(PyTransform { + inner: &other.extract::()?.inner * &self.inner, + }) + } else { + let builtins = PyModule::import(py, "builtins")?; + let other = builtins.getattr("float")?.call1((&other,))?; + Ok(PyTransform { + inner: other.extract::()? * &self.inner, + }) + } + } + + fn __truediv__(&self, py: Python, other: f64) -> PyResult { + let builtins = PyModule::import(py, "builtins")?; + let other = builtins.getattr("float")?.call1((&other,))?; + Ok(PyTransform { + inner: &self.inner / other.extract::()?, + }) + } + + #[getter] + fn get_parameters(&self) -> Vec { + self.inner.parameters.clone() + } + + #[setter] + fn set_parameters(&mut self, parameters: Vec) { + self.inner.parameters = parameters; + } + + #[getter] + fn get_dparameters(&self) -> Vec { + self.inner.dparameters.clone() + } + + #[setter] + fn set_dparameters(&mut self, dparameters: Vec) { + self.inner.dparameters = dparameters; + } + + #[getter] + fn get_center(&self) -> Vec { + self.inner.center.clone() + } + + #[setter] + fn set_center(&mut self, center: Vec) { + self.inner.center = center; + } + + #[getter] + fn get_shape(&self) -> Vec { + self.inner.shape.clone() + } + + #[setter] + fn set_shape(&mut self, shape: Vec) { + self.inner.shape = shape; + } + + #[getter] + fn get_ndim(&self) -> usize { + self.inner.ndim() + } + + #[getter] + fn get_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + self.inner.matrix().into_pyarray(py) + } + + #[setter] + fn set_matrix( + &mut self, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + matrix: PyArrayLike2, + ) { + let matrix = matrix.as_array(); + self.inner.set_matrix(matrix); + } + + #[getter] + fn get_dmatrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + self.inner.dmatrix().into_pyarray(py) + } + + #[setter] + fn set_dmatrix( + &mut self, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + dmatrix: PyArrayLike2, + ) { + let dmatrix = dmatrix.as_array(); + self.inner.set_dmatrix(dmatrix); + } + + #[getter] + fn inverse(&self) -> PyResult { + Ok(PyTransform { + inner: self.inner.inverse().map_err(Error::from)?, + }) + } + + fn adapt(&mut self, center: Vec, shape: Vec) { + self.inner.adapt(center.as_slice(), shape.as_slice()); + } + + #[staticmethod] + fn from_scaling(scaling: Vec) -> PyTransform { + PyTransform { + inner: Transform::from_scaling(scaling.as_slice()), + } + } + + #[staticmethod] + fn from_translation(translation: Vec) -> PyTransform { + PyTransform { + inner: Transform::from_translation(translation.as_slice()), + } + } + + #[staticmethod] + fn from_rotation(theta: f64, center: Vec) -> PyTransform { + PyTransform { + inner: Transform::from_rotation(theta, center.as_slice()).into_dyn(), + } + } + + fn with_scaling(&self, scaling: Vec) -> PyTransform { + PyTransform { + inner: Transform::from_scaling(scaling.as_slice()) * self.inner.clone(), + } + } + + fn with_translation(&self, translation: Vec) -> PyTransform { + PyTransform { + inner: Transform::from_translation(translation.as_slice()) * self.inner.clone(), + } + } + + fn with_rotation(&self, theta: f64, center: Vec) -> PyTransform { + PyTransform { + inner: Transform::from_rotation(theta, center.as_slice()).into_dyn() + * self.inner.clone(), + } + } + + fn interpolate<'py>( + &self, + py: Python<'py>, + order: usize, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + image: PyArrayLikeDyn, + ) -> PyResult>> { + let image = image.as_array(); + Ok(py + .detach(|| match order { + 0 => Ok(self + .inner + .interpolate::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 1 => Ok(self + .inner + .interpolate::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 2 => Ok(self + .inner + .interpolate::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 3 => Ok(self + .inner + .interpolate::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 4 => Ok(self + .inner + .interpolate::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 5 => Ok(self + .inner + .interpolate::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)), + _ => Err(PyValueError::new_err("order must be 0 <= order < 6")), + })? + .map_err(Error::from)? + .into_pyarray(py)) + } + + fn interpolate_par<'py>( + &self, + py: Python<'py>, + order: usize, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + image: PyArrayLikeDyn, + ) -> PyResult>> { + let image = image.as_array(); + Ok(py + .detach(|| match order { + 0 => Ok(self + .inner + .interpolate_par::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 1 => Ok(self + .inner + .interpolate_par::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 2 => Ok(self + .inner + .interpolate_par::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 3 => Ok(self + .inner + .interpolate_par::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 4 => Ok(self + .inner + .interpolate_par::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 5 => Ok(self + .inner + .interpolate_par::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)), + _ => Err(PyValueError::new_err("order must be 0 <= order < 6")), + })? + .map_err(Error::from)? + .into_pyarray(py)) + } + + fn is_unity(&self) -> bool { + self.inner.is_unity() + } + + fn transform_point<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + point: PyArrayLike1, + ) -> Bound<'py, PyArray1> { + let point = point.as_array(); + if let Some(slice) = point.as_slice() { + self.inner.transform_point(slice) + } else { + let point = point.to_vec(); + self.inner.transform_point(&point) + } + .into_pyarray(py) + } + + fn transform_points<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + points: PyArrayLike2, + ) -> PyResult>> { + let points = points.as_array(); + Ok(py + .detach(|| self.inner.transform_points(points)) + .map_err(Error::from)? + .into_pyarray(py)) + } + + #[staticmethod] + #[pyo3(signature = (fixed, moving, fixed_mu, initial_guess = None))] + fn register<'py>( + py: Python<'py>, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + fixed: PyArrayLikeDyn, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + moving: PyArrayLikeDyn, + fixed_mu: Vec>, + initial_guess: Option>, + ) -> PyResult { + let fixed = fixed.as_array(); + let moving = moving.as_array(); + if fixed.shape() != moving.shape() { + return Err(PyErr::from(Error::ShapeMismatch( + fixed.shape().to_vec(), + moving.shape().to_vec(), + ))); + } + py.detach(|| match fixed.ndim() { + 0 => Ok(PyTransform { + inner: Transform::register( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + fixed_mu, + None, + initial_guess, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 1 => Ok(PyTransform { + inner: Transform::register( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + fixed_mu, + None, + initial_guess, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 2 => Ok(PyTransform { + inner: Transform::register( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + fixed_mu, + None, + initial_guess, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 3 => Ok(PyTransform { + inner: Transform::register( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + fixed_mu, + None, + initial_guess, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 4 => Ok(PyTransform { + inner: Transform::register( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + fixed_mu, + None, + initial_guess, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 5 => Ok(PyTransform { + inner: Transform::register( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + fixed_mu, + None, + initial_guess, + ) + .map_err(Error::from)? + .into_dyn(), + }), + _ => Err(PyNotImplementedError::new_err(format!( + "registration in {} dimensions is not implemented", + fixed.ndim() + ))), + }) + } + + #[staticmethod] + fn register_affine<'py>( + py: Python<'py>, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + fixed: PyArrayLikeDyn, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + moving: PyArrayLikeDyn, + ) -> PyResult { + let fixed = fixed.as_array(); + let moving = moving.as_array(); + if fixed.shape() != moving.shape() { + return Err(PyErr::from(Error::ShapeMismatch( + fixed.shape().to_vec(), + moving.shape().to_vec(), + ))); + } + py.detach(|| match fixed.ndim() { + 0 => Ok(PyTransform { + inner: Transform::register_affine( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 1 => Ok(PyTransform { + inner: Transform::register_affine( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 2 => Ok(PyTransform { + inner: Transform::register_affine( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 3 => Ok(PyTransform { + inner: Transform::register_affine( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 4 => Ok(PyTransform { + inner: Transform::register_affine( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 5 => Ok(PyTransform { + inner: Transform::register_affine( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + _ => Err(PyNotImplementedError::new_err(format!( + "registration in {} dimensions is not implemented", + fixed.ndim() + ))), + }) + } + + #[staticmethod] + fn register_translation<'py>( + py: Python<'py>, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + fixed: PyArrayLikeDyn, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + moving: PyArrayLikeDyn, + ) -> PyResult { + let fixed = fixed.as_array(); + let moving = moving.as_array(); + if fixed.shape() != moving.shape() { + return Err(PyErr::from(Error::ShapeMismatch( + fixed.shape().to_vec(), + moving.shape().to_vec(), + ))); + } + py.detach(|| match fixed.ndim() { + 0 => Ok(PyTransform { + inner: Transform::register_translation( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 1 => Ok(PyTransform { + inner: Transform::register_translation( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 2 => Ok(PyTransform { + inner: Transform::register_translation( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 3 => Ok(PyTransform { + inner: Transform::register_translation( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 4 => Ok(PyTransform { + inner: Transform::register_translation( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + 5 => Ok(PyTransform { + inner: Transform::register_translation( + fixed.into_dimensionality::().map_err(Error::from)?, + moving.into_dimensionality::().map_err(Error::from)?, + ) + .map_err(Error::from)? + .into_dyn(), + }), + _ => Err(PyNotImplementedError::new_err(format!( + "registration in {} dimensions is not implemented", + fixed.ndim() + ))), + }) + } +} + +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + eq, + name = "Transforms", + module = "ndbioimage" +)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct PyTransforms { + pub(crate) inner: Transforms, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyTransforms { + pub(crate) fn __getstate__(&self) -> PyResult> { + Ok(to_stdvec(self).map_err(Error::from)?) + } + + pub(crate) fn __setstate__(&mut self, state: Vec) -> PyResult<()> { + Ok(from_bytes(&state).map_err(Error::from)?) + } + + #[staticmethod] + pub(crate) fn load(path: PathBuf) -> PyResult { + Ok(PyTransforms { + inner: Transforms::load(&path)?, + }) + } + + pub(crate) fn save(&self, path: PathBuf) -> PyResult<()> { + Ok(self.inner.save(&path)?) + } + + #[staticmethod] + fn calculate_channel_transforms_2d( + bead_files: Vec, + main_channel: usize, + default_transform: Option, + ) -> PyResult> { + Ok(Transforms::calculate_channel_transforms_2d( + &bead_files + .iter() + .map(|file| file.as_path()) + .collect::>(), + main_channel, + default_transform + .map(|d| d.inner.into_dimensionality().map_err(Error::from)) + .transpose()?, + )? + .into_iter() + .map(|t| PyTransform { + inner: t.into_dyn(), + }) + .collect()) + } + + #[staticmethod] + fn calculate_channel_transforms_3d( + bead_files: Vec, + main_channel: usize, + default_transform: Option, + ) -> PyResult> { + Ok(Transforms::calculate_channel_transforms_3d( + &bead_files + .iter() + .map(|file| file.as_path()) + .collect::>(), + main_channel, + default_transform + .map(|d| d.inner.into_dimensionality().map_err(Error::from)) + .transpose()?, + )? + .into_iter() + .map(|t| PyTransform { + inner: t.into_dyn(), + }) + .collect()) + } +} diff --git a/src/readers.rs b/src/readers.rs index 3f3e5f5..9d5fe37 100644 --- a/src/readers.rs +++ b/src/readers.rs @@ -100,9 +100,10 @@ impl Dimensions { } /// pixel type enum -#[allow(clippy::upper_case_acronyms)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub enum PixelType { + /// true / false + Bool, /// signed 8-bit integer I8, /// unsigned 8-bit integer @@ -135,7 +136,7 @@ impl PixelType { /// number of bytes per pixel for this type pub fn bytes_per_pixel(&self) -> usize { match self { - PixelType::I8 | PixelType::U8 => 1, + PixelType::Bool | PixelType::I8 | PixelType::U8 => 1, PixelType::I16 | PixelType::U16 => 2, PixelType::I32 | PixelType::U32 | PixelType::F32 => 4, PixelType::I64 | PixelType::U64 | PixelType::F64 => 8, @@ -145,7 +146,6 @@ impl PixelType { } /// array data with a specific pixel type -#[allow(clippy::upper_case_acronyms)] #[derive(Clone, Debug)] pub enum ArrayT { /// signed 8-bit integer array @@ -207,7 +207,6 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into { } /// retrieve frame at channel c, slice z and time t - #[allow(clippy::if_same_then_else)] fn get_frame(&self, c: usize, z: usize, t: usize) -> Result; /// the path to the image file @@ -486,7 +485,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.reader_name(), #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.reader_name(), - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), } } @@ -503,7 +502,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.metadata()?, #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.metadata()?, - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), }) } @@ -520,7 +519,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.get_frame(c, z, t), #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.get_frame(c, z, t), - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), } } @@ -537,7 +536,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.path(), #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.path(), - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), } } @@ -554,7 +553,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.series(), #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.series(), - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), } } @@ -571,7 +570,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.position(), #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.position(), - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), } } @@ -588,7 +587,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.shape(), #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.shape(), - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), } } @@ -605,7 +604,7 @@ impl Reader for DynReader { DynReader::BioFormatsRust(r) => r.pixel_type(), #[cfg(feature = "bioformats_java")] DynReader::BioFormatsJava(r) => r.pixel_type(), - #[allow(unreachable_patterns)] + #[expect(unreachable_patterns)] _ => unreachable!(), } } diff --git a/src/readers/bioformats_java.rs b/src/readers/bioformats_java.rs index e6f999a..306ee38 100644 --- a/src/readers/bioformats_java.rs +++ b/src/readers/bioformats_java.rs @@ -389,6 +389,7 @@ impl BioFormatsJavaReader { fn deinterleave(&self, bytes: Vec, channel: usize) -> Result, Error> { let chunk_size = match self.pixel_type { + PixelType::Bool => 1, PixelType::I8 => 1, PixelType::U8 => 1, PixelType::I16 => 2, @@ -413,6 +414,26 @@ impl BioFormatsJavaReader { fn bytes_to_frame(&self, bytes: Vec) -> Result { macro_rules! get_frame { + (bool, <$n:expr) => { + Ok(ArrayT::from(Array2::from_shape_vec( + (self.shape.y, self.shape.x), + bytes + .iter() + .map(|x| [x & 128, x & 64, x & 32, x & 16, x & 8, x & 4, x & 2, x & 1]) + .flatten() + .collect(), + )?)) + }; + (bool, >$n:expr) => { + Ok(ArrayT::from(Array2::from_shape_vec( + (self.shape.y, self.shape.x), + bytes + .iter() + .map(|x| [x & 1, x & 2, x & 4, x & 8, x & 16, x & 32, x & 64, x & 128]) + .flatten() + .collect(), + )?)) + }; ($t:tt, <$n:expr) => { Ok(ArrayT::from(Array2::from_shape_vec( (self.shape.y, self.shape.x), @@ -434,6 +455,7 @@ impl BioFormatsJavaReader { } match (&self.pixel_type, self.little_endian) { + (PixelType::Bool, true) => get_frame!(bool, <1), (PixelType::I8, true) => get_frame!(i8, <1), (PixelType::U8, true) => get_frame!(u8, <1), (PixelType::I16, true) => get_frame!(i16, <2), @@ -447,6 +469,7 @@ impl BioFormatsJavaReader { (PixelType::I128, true) => get_frame!(i128, <16), (PixelType::U128, true) => get_frame!(u128, <16), (PixelType::F128, true) => get_frame!(f64, <8), + (PixelType::Bool, false) => get_frame!(bool, >1), (PixelType::I8, false) => get_frame!(i8, >1), (PixelType::U8, false) => get_frame!(u8, >1), (PixelType::I16, false) => get_frame!(i16, >2), @@ -501,7 +524,7 @@ impl Reader for BioFormatsJavaReader { Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string()) })?; } - let mut new = BioFormatsJavaReader { + let mut new = Self { reader: ThreadLocal::default(), path, series, diff --git a/src/readers/bioformats_rust.rs b/src/readers/bioformats_rust.rs index b28e844..878bee0 100644 --- a/src/readers/bioformats_rust.rs +++ b/src/readers/bioformats_rust.rs @@ -78,19 +78,20 @@ impl Deref for BioFormatsRustReader { } } -fn map_pixel_type(bf: bioformats::PixelType) -> Result { - use bioformats::PixelType as Bf; - Ok(match bf { - Bf::Int8 => PixelType::I8, - Bf::Uint8 => PixelType::U8, - Bf::Int16 => PixelType::I16, - Bf::Uint16 => PixelType::U16, - Bf::Int32 => PixelType::I32, - Bf::Uint32 => PixelType::U32, - Bf::Float32 => PixelType::F32, - Bf::Float64 => PixelType::F64, - Bf::Bit => PixelType::U8, - }) +impl From for PixelType { + fn from(bf: bioformats::PixelType) -> Self { + match bf { + bioformats::PixelType::Bit => PixelType::Bool, + bioformats::PixelType::Int8 => PixelType::I8, + bioformats::PixelType::Uint8 => PixelType::U8, + bioformats::PixelType::Int16 => PixelType::I16, + bioformats::PixelType::Uint16 => PixelType::U16, + bioformats::PixelType::Int32 => PixelType::I32, + bioformats::PixelType::Uint32 => PixelType::U32, + bioformats::PixelType::Float32 => PixelType::F32, + bioformats::PixelType::Float64 => PixelType::F64, + } + } } impl BioFormatsRustReader { @@ -126,6 +127,7 @@ impl BioFormatsRustReader { fn deinterleave(&self, bytes: Vec, channel: usize) -> Result, Error> { let chunk_size = match self.pixel_type { + PixelType::Bool => 1, PixelType::I8 => 1, PixelType::U8 => 1, PixelType::I16 => 2, @@ -150,6 +152,26 @@ impl BioFormatsRustReader { fn bytes_to_frame(&self, bytes: Vec) -> Result { macro_rules! get_frame { + (bool, <$n:expr) => { + Ok(ArrayT::from(Array2::from_shape_vec( + (self.shape.y, self.shape.x), + bytes + .iter() + .map(|x| [x & 128, x & 64, x & 32, x & 16, x & 8, x & 4, x & 2, x & 1]) + .flatten() + .collect(), + )?)) + }; + (bool, >$n:expr) => { + Ok(ArrayT::from(Array2::from_shape_vec( + (self.shape.y, self.shape.x), + bytes + .iter() + .map(|x| [x & 1, x & 2, x & 4, x & 8, x & 16, x & 32, x & 64, x & 128]) + .flatten() + .collect(), + )?)) + }; ($t:tt, <$n:expr) => { Ok(ArrayT::from(Array2::from_shape_vec( (self.shape.y, self.shape.x), @@ -171,6 +193,7 @@ impl BioFormatsRustReader { } match (&self.pixel_type, self.little_endian) { + (PixelType::Bool, true) => get_frame!(bool, <1), (PixelType::I8, true) => get_frame!(i8, <1), (PixelType::U8, true) => get_frame!(u8, <1), (PixelType::I16, true) => get_frame!(i16, <2), @@ -184,6 +207,7 @@ impl BioFormatsRustReader { (PixelType::I128, true) => get_frame!(i128, <16), (PixelType::U128, true) => get_frame!(u128, <16), (PixelType::F128, true) => get_frame!(f64, <8), + (PixelType::Bool, false) => get_frame!(bool, >1), (PixelType::I8, false) => get_frame!(i8, >1), (PixelType::U8, false) => get_frame!(u8, >1), (PixelType::I16, false) => get_frame!(i16, >2), @@ -246,7 +270,7 @@ impl Reader for BioFormatsRustReader { new.shape.y = metadata.size_y as usize; new.shape.x = metadata.size_x as usize; new.little_endian = metadata.is_little_endian; - new.pixel_type = map_pixel_type(metadata.pixel_type)?; + new.pixel_type = PixelType::from(metadata.pixel_type); Ok(new) } @@ -304,6 +328,13 @@ impl Reader for BioFormatsRustReader { where P: AsRef, { + let mut path = path.as_ref().to_path_buf(); + if path.is_dir() { + let orig = path.clone(); + path = find_tiff(&path)?.ok_or_else(|| { + Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string()) + })?; + } let reader = ImageReader::open(path.as_ref()) .map_err(|e| Error::Parse(format!("bioformats failed to open: {}", e)))?; let n = reader.series_count(); diff --git a/src/readers/czi.rs b/src/readers/czi.rs index fb82099..13c6467 100644 --- a/src/readers/czi.rs +++ b/src/readers/czi.rs @@ -1053,6 +1053,44 @@ impl Reader for CziReader { } macro_rules! get_frame { + (bool, $n:expr) => {{ + let mut array = Array2::zeros((self.shape.y, self.shape.x)); + if let Some(indices) = self.block_map.get(&(c, z, t)) { + for &i in indices { + let sub_block = reader.read_sub_block(i)?; + let bitmap = sub_block.create_bitmap()?.lock()?; + let bytes = bitmap.lock_info.get_data_roi(); + let info = sub_block.get_info()?; + let rect = info.get_logical_rect(); + let x = (rect.get_x() - min_x) as usize; + let y = (rect.get_y() - min_y) as usize; + let w = rect.get_w() as usize; + let h = rect.get_h() as usize; + array + .slice_mut(s![x..x + w, y..y + h]) + .assign(&Array2::from_shape_vec( + (w, h), + bytes + .iter() + .map(|x| { + [ + x & 128, + x & 64, + x & 32, + x & 16, + x & 8, + x & 4, + x & 2, + x & 1, + ] + }) + .flatten() + .collect(), + )?); + } + } + Ok(ArrayT::from(array)) + }}; ($t:tt, $n:expr) => {{ let mut array = Array2::zeros((self.shape.y, self.shape.x)); if let Some(indices) = self.block_map.get(&(c, z, t)) { @@ -1082,6 +1120,7 @@ impl Reader for CziReader { } match self.pixel_type { + PixelType::Bool => get_frame!(bool, 1), PixelType::I8 => get_frame!(i8, 1), PixelType::U8 => get_frame!(u8, 1), PixelType::I16 => get_frame!(i16, 2), diff --git a/src/readers/tiffseq.rs b/src/readers/tiffseq.rs index 1277a60..49b9f82 100644 --- a/src/readers/tiffseq.rs +++ b/src/readers/tiffseq.rs @@ -22,7 +22,7 @@ pub struct TiffSeqReader { filedict: HashMap<(usize, usize, usize), PathBuf>, cnamelist: Vec, #[serde(skip)] - metadata_map: HashMap, + metadata_map: HashMap, } impl From for DynReader { @@ -73,10 +73,10 @@ impl TiffSeqReader { Ok(files) } - fn read_metadata_from_file(dir: &Path) -> Result, Error> { + fn read_metadata_from_file(dir: &Path) -> Result, Error> { let md_path = dir.join("metadata.txt"); let text = std::fs::read_to_string(&md_path)?; - let parsed: serde_yaml::Value = serde_yaml::from_str(&text)?; + let parsed: yaml_serde::Value = yaml_serde::from_str(&text)?; let mut map = HashMap::new(); map.insert("Info".to_string(), parsed); Ok(map) @@ -129,11 +129,11 @@ impl Reader for TiffSeqReader { .ok_or_else(|| Error::Parse("missing Info key in tag 50839".to_string()))?; let lookup = |key: &str| { - info.get(serde_yaml::Value::String(key.to_string())) + info.get(yaml_serde::Value::String(key.to_string())) .or_else(|| { - info.get(serde_yaml::Value::String("Summary".to_string())) + info.get(yaml_serde::Value::String("Summary".to_string())) .and_then(|s| s.as_mapping()) - .and_then(|s| s.get(serde_yaml::Value::String(key.to_string()))) + .and_then(|s| s.get(yaml_serde::Value::String(key.to_string()))) }) }; @@ -240,12 +240,12 @@ impl Reader for TiffSeqReader { let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping()); let slookup = - |key: &str| info.and_then(|m| m.get(serde_yaml::Value::String(key.to_string()))); + |key: &str| info.and_then(|m| m.get(yaml_serde::Value::String(key.to_string()))); let summary = slookup("Summary").and_then(|v| v.as_mapping()); let summary_lookup = - |key: &str| summary.and_then(|m| m.get(serde_yaml::Value::String(key.to_string()))); + |key: &str| summary.and_then(|m| m.get(yaml_serde::Value::String(key.to_string()))); let first_frame = info.and_then(|m| { m.iter() @@ -254,7 +254,7 @@ impl Reader for TiffSeqReader { }); let frame_lookup = - |key: &str| first_frame.and_then(|m| m.get(serde_yaml::Value::String(key.to_string()))); + |key: &str| first_frame.and_then(|m| m.get(yaml_serde::Value::String(key.to_string()))); let ome_pixel_type = match self.pixel_type { PixelType::I8 => ome::PixelType::Int8, diff --git a/src/tiffwrite.rs b/src/tiffwrite.rs index 5105f3c..66a18f0 100644 --- a/src/tiffwrite.rs +++ b/src/tiffwrite.rs @@ -165,6 +165,7 @@ where P: AsRef, { match self.pixel_type() { + PixelType::Bool => self.save_as_tiff_with_type::(path, options)?, PixelType::I8 => self.save_as_tiff_with_type::(path, options)?, PixelType::U8 => self.save_as_tiff_with_type::(path, options)?, PixelType::I16 => self.save_as_tiff_with_type::(path, options)?, diff --git a/src/transforms.rs b/src/transforms.rs index 465ec0f..b064ebb 100644 --- a/src/transforms.rs +++ b/src/transforms.rs @@ -1,20 +1,175 @@ -use crate::readers::Reader; +use crate::axes::Axis; +use crate::error::Error; +use crate::readers::{DynReader, Reader}; use crate::view::View; pub use image_registration::transform::Transform; -use ndarray::{Dimension, Ix2, Ix3}; +use ndarray::{Dimension, Ix2, Ix3, s}; use serde::{Deserialize, Serialize}; +use std::path::Path; +#[expect(clippy::upper_case_acronyms)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum TransformD { YX(Transform), ZYX(Transform), } -impl View {} - #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct Transforms { - channel: Vec, - drift: Vec, + pub channel: Vec, + pub drift: Vec, +} + +impl Transforms { + pub fn load(path: &Path) -> Result { + let file = std::fs::File::open(path)?; + Ok(yaml_serde::from_reader(file)?) + } + + pub fn save(&self, path: &Path) -> Result<(), Error> { + let file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; + Ok(yaml_serde::to_writer(file, self)?) + } + + pub fn calculate_channel_transforms_2d( + bead_files: &[&Path], + main_channel: usize, + default_transform: Option>, + ) -> Result>, Error> { + let mut transforms = Vec::new(); + let default_transform = default_transform.unwrap_or_default(); + for file in bead_files { + let view = View::<_, DynReader>::from_path(file)?; + transforms.push(view.calculate_channel_transforms_2d(main_channel)?) + } + let n_channels = transforms.iter().map(|t| t.len()).max().unwrap(); + let mut average_transforms = Vec::new(); + for channel in 0..n_channels { + let matrix = transforms + .iter() + .map(|t| (&t[channel] * &default_transform).matrix()) + .reduce(|a, b| a + b) + .unwrap() + / n_channels as f64; + let dmatrix = transforms + .iter() + .map(|t| ((&t[channel] * &default_transform).matrix() - &matrix).powi(2)) + .reduce(|a, b| a + b) + .unwrap() + .sqrt() + / (n_channels as f64).sqrt(); + average_transforms.push( + Transform::default() + .with_matrix(matrix.view()) + .with_dmatrix(dmatrix.view()), + ); + } + Ok(average_transforms) + } + + pub fn calculate_channel_transforms_3d( + bead_files: &[&Path], + main_channel: usize, + default_transform: Option>, + ) -> Result>, Error> { + let mut transforms = Vec::new(); + let default_transform = default_transform.unwrap_or_default(); + for file in bead_files { + let view = View::<_, DynReader>::from_path(file)?; + transforms.push(view.calculate_channel_transforms_3d(main_channel)?) + } + let n_channels = transforms.iter().map(|t| t.len()).max().unwrap(); + let mut average_transforms = Vec::new(); + for channel in 0..n_channels { + let matrix = transforms + .iter() + .map(|t| (&t[channel] * &default_transform).matrix()) + .reduce(|a, b| a + b) + .unwrap() + / n_channels as f64; + let dmatrix = transforms + .iter() + .map(|t| ((&t[channel] * &default_transform).matrix() - &matrix).powi(2)) + .reduce(|a, b| a + b) + .unwrap() + .sqrt() + / (n_channels as f64).sqrt(); + average_transforms.push( + Transform::default() + .with_matrix(matrix.view()) + .with_dmatrix(dmatrix.view()), + ); + } + Ok(average_transforms) + } +} + +impl View { + pub fn with_transform_from_yaml(mut self, path: &Path) -> Result { + self.transforms = Transforms::load(path)?; + Ok(self) + } + + pub fn load_transform_from_yaml(&mut self, path: &Path) -> Result<(), Error> { + self.transforms = Transforms::load(path)?; + Ok(()) + } + + pub fn calculate_channel_transforms_2d( + &self, + main_channel: usize, + ) -> Result>, Error> { + let main_max = self + .slice_cztyx(s![main_channel, .., 0, .., ..])? + .max_proj(Axis::Z)? + .as_array::()?; + let mut transforms = Vec::new(); + for channel in 0..self.shape().c { + if channel == main_channel { + transforms.push(Transform::default()); + } else { + let max = self + .slice_cztyx(s![channel, .., 0, .., ..])? + .max_proj(Axis::Z)? + .as_array::()?; + transforms.push(Transform::register_affine(main_max.view(), max.view())?); + } + } + Ok(transforms) + } + + pub fn calculate_channel_transforms_3d( + &self, + main_channel: usize, + ) -> Result>, Error> { + let main_max = self + .slice_cztyx(s![main_channel, .., 0, .., ..])? + .as_array::()?; + let mut transforms = Vec::new(); + for channel in 0..self.shape().c { + if channel == main_channel { + transforms.push(Transform::default()); + } else { + let max = self + .slice_cztyx(s![channel, .., 0, .., ..])? + .as_array::()?; + transforms.push(Transform::register_affine(main_max.view(), max.view())?); + } + } + Ok(transforms) + } + + pub fn calculate_drift_transform_2d(&self) -> Result>, Error> { + todo!() + } + + pub fn calculate_drift_transform_3d(&self) -> Result>, Error> { + todo!() + } } #[cfg(test)] @@ -23,7 +178,7 @@ mod tests { #[test] fn test_transforms() -> Result<(), Box> { - let t = Transforms::default(); + let _t = Transforms::default(); Ok(()) } diff --git a/src/view.rs b/src/view.rs index 1939bae..a9fb055 100644 --- a/src/view.rs +++ b/src/view.rs @@ -91,7 +91,7 @@ pub struct View { operations: IndexMap, dimensionality: PhantomData, #[cfg(feature = "transforms")] - transforms: Transforms, + pub(crate) transforms: Transforms, } impl Hash for View @@ -123,7 +123,7 @@ impl View { } } - #[allow(dead_code)] + #[expect(dead_code)] pub(crate) fn new_with_axes(reader: R, axes: Vec) -> Result { let mut slice = Vec::new(); let shape = reader.shape(); @@ -251,7 +251,7 @@ impl View { operations: self.operations, dimensionality: PhantomData, #[cfg(feature = "transforms")] - transforms: Transforms::default(), + transforms: self.transforms, }) } else { Err(Error::DimensionalityMismatch(d, self.ndim())) @@ -264,7 +264,7 @@ impl View { operations: self.operations, dimensionality: PhantomData, #[cfg(feature = "transforms")] - transforms: Transforms::default(), + transforms: self.transforms, }) } } @@ -274,7 +274,7 @@ impl View { &self.axes } - #[allow(dead_code)] + #[expect(dead_code)] pub(crate) fn get_operations(&self) -> &IndexMap { &self.operations }