diff --git a/Cargo.toml b/Cargo.toml index db6e18b..341823e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ name = "ndbioimage" crate-type = ["cdylib", "rlib"] [dependencies] -bioformats = { version = "0.1", optional = true } +bioformats = { version = "0.1", optional = true, default-features = false, features = ["zarr", "tissuefaxs"] } clap = { version = "4", features = ["derive"] } console = { version = "0.16", optional = true } downloader = { version = "0.2", optional = true, default-features = false, features = ["rustls-tls"] } @@ -83,5 +83,8 @@ features = ["bioformats_java", "bioformats_rust", "czi", "movie", "tiffseq", "ti [profile.test] inherits = "release" +[profile.release] +debug = true + [profile.dev.package.backtrace] opt-level = 3 \ No newline at end of file diff --git a/py/ndbioimage/__init__.py b/py/ndbioimage/__init__.py index 1aab780..a52af34 100755 --- a/py/ndbioimage/__init__.py +++ b/py/ndbioimage/__init__.py @@ -13,8 +13,14 @@ from numpy.typing import ArrayLike os.environ["RUST_BACKTRACE"] = "full" os.environ["COLORBT_SHOW_HIDDEN"] = "1" -from . import ndbioimage_rs as rs -from .ndbioimage_rs import Imread, Transform, Transforms +from .ndbioimage_rs import Imread, generate_stub + +try: + from .ndbioimage_rs import transforms + + sys.modules["ndbioimage.transforms"] = transforms +except (ImportError, ModuleNotFoundError): + pass try: from .ndbioimage_rs import batch_to_tiff @@ -23,8 +29,12 @@ except ImportError: try: import ome_metadata # noqa -except ModuleNotFoundError: - sys.modules["ome_metadata"] = rs.ome_metadata # noqa +except (ImportError, ModuleNotFoundError): + from .ndbioimage_rs import ome_metadata # noqa + + sys.modules["ome_metadata"] = ome_metadata + import ome_metadata # noqa + try: __version__ = version(Path(__file__).parent.name) @@ -58,13 +68,26 @@ def ndbioimage_generate_stub(): path = Path(sys.argv[1]).resolve() else: path = Path.cwd().resolve() - if (path / "py" / "ndbioimage" / "__init__.py").exists(): - rs.generate_stub(str(path)) # noqa - else: - raise ModuleNotFoundError(str(path / "py" / "ndbioimage" / "__init__.py")) - (path / "py" / "ndbioimage" / "__init__.pyi").rename( - path / "py" / "ndbioimage" / "ndbioimage_rs.pyi" - ) + nd = path / "py" / "ndbioimage" + if (nd / "__init__.pyi").exists(): + (nd / "__init__.pyi").rename(nd / "ndbioimage.pyi") + try: + if (nd / "__init__.py").exists(): + generate_stub(str(path)) # noqa + else: + raise ModuleNotFoundError(str(nd / "__init__.py")) + rs = nd / "ndbioimage_rs" + rs.mkdir(parents=True, exist_ok=True) + (nd / "__init__.pyi").rename(rs / "__init__.pyi") + for stub in nd.glob("**/__init__.pyi"): + if stub.parent != rs: + stub.rename((rs / stub.parent.name).with_suffix(".pyi")) + stub.parent.rmdir() + except Exception as e: + raise e + finally: + if (nd / "ndbioimage.pyi").exists(): + (nd / "ndbioimage.pyi").rename(nd / "__init__.pyi") R = TypeVar("R") diff --git a/py/ndbioimage/__init__.pyi b/py/ndbioimage/__init__.pyi new file mode 100644 index 0000000..43c5441 --- /dev/null +++ b/py/ndbioimage/__init__.pyi @@ -0,0 +1,3 @@ +from .ndbioimage_rs import Imread, Shape + +__all__ = ["Imread", "Shape"] diff --git a/py/ndbioimage/ndbioimage_rs.pyi b/py/ndbioimage/ndbioimage_rs/__init__.pyi similarity index 79% rename from py/ndbioimage/ndbioimage_rs.pyi rename to py/ndbioimage/ndbioimage_rs/__init__.pyi index e5531cf..9b2e158 100644 --- a/py/ndbioimage/ndbioimage_rs.pyi +++ b/py/ndbioimage/ndbioimage_rs/__init__.pyi @@ -9,15 +9,50 @@ import typing import numpy import numpy.typing +from . import transforms + __all__ = [ + "Flags", "Imread", "Shape", - "Transform", - "Transforms", "batch_to_tiff", + "generate_stub", "main", + "transforms", ] +class Flags: + r""" + https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html + """ + @property + def c_contiguous(self) -> builtins.bool: ... + @property + def f_contiguous(self) -> builtins.bool: ... + @property + def owndata(self) -> builtins.bool: ... + @property + def writable(self) -> builtins.bool: ... + @property + def aligned(self) -> builtins.bool: ... + @property + def writebackifcopy(self) -> builtins.bool: ... + @property + def fnc(self) -> builtins.bool: ... + @property + def forc(self) -> builtins.bool: ... + @property + def behaved(self) -> builtins.bool: ... + @property + def carray(self) -> builtins.bool: ... + @property + def farray(self) -> builtins.bool: ... + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __hash__(self) -> builtins.int: ... + def __new__(cls) -> Flags: ... + def __getnewargs__(self) -> None: ... + def __getitem__(self, item: builtins.str) -> builtins.bool: ... + class Imread: r""" class to read image files, while taking good care of important metadata, @@ -48,16 +83,28 @@ class Imread: TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var """ + + available_readers: builtins.list[builtins.str] + @property + def flags(self) -> Flags: + r""" + https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html + """ @property def reader_name(self) -> builtins.str: r""" the name of the reader used to open the file """ @property - def transform(self) -> Transforms: + def transforms(self) -> transforms.Transforms: r""" get the transformation """ + @transforms.setter + def transforms(self, value: transforms.Transforms) -> None: + r""" + set the transformation + """ @property def path(self) -> pathlib.Path: r""" @@ -183,6 +230,15 @@ class Imread: r""" new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64) """ + def set_flags( + self, + write: typing.Optional[builtins.bool] = None, + align: typing.Optional[builtins.bool] = None, + uic: typing.Optional[builtins.bool] = None, + ) -> None: + r""" + https://numpy.org/doc/stable/reference/generated/numpy.ndarray.setflags.html#numpy.ndarray.setflags + """ @staticmethod def get_positions( path: str | pathlib.Path | Imread | bytes, @@ -210,22 +266,31 @@ class Imread: ) -> Imread: r""" return a new view with transformations applied (channel alignment, drift correction) + backward compatibility """ - def set_transform(self, transform: Transforms) -> None: + def with_transforms( + self, + channels: builtins.bool = True, + 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""" - set the transformation + return a new view with transformations applied (channel alignment, drift correction) """ - def load_transform_from_yaml( + def load_transforms_from_yaml( self, path: builtins.str | os.PathLike | pathlib.Path ) -> None: ... def calculate_channel_transforms_2d( self, main_channel: builtins.int - ) -> builtins.list[Transform]: ... + ) -> builtins.dict[builtins.str, transforms.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]: ... + ) -> builtins.dict[builtins.str, transforms.Transform]: ... + def calculate_drift_transforms_2d(self) -> builtins.list[transforms.Transform]: ... + def calculate_drift_transforms_3d(self) -> builtins.list[transforms.Transform]: ... def squeeze(self) -> numpy.ndarray | int | float: ... def close(self) -> None: r""" @@ -678,7 +743,7 @@ class Shape: """ def __getitem__( self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int] - ) -> typing.Optional[int | list[int]]: + ) -> typing.Optional[int | tuple[int]]: r""" get dimension size by index or axis name """ @@ -691,127 +756,6 @@ 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], @@ -827,6 +771,11 @@ def batch_to_tiff( batch convert multiple image files to TIFF format """ +def generate_stub(dest_path: builtins.str) -> None: + r""" + generates ndbioimage/__init__.pyi + """ + def main() -> None: r""" main entry point for the command-line interface diff --git a/py/ndbioimage/ndbioimage_rs/transforms.pyi b/py/ndbioimage/ndbioimage_rs/transforms.pyi new file mode 100644 index 0000000..f1d85e1 --- /dev/null +++ b/py/ndbioimage/ndbioimage_rs/transforms.pyi @@ -0,0 +1,244 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401, F403, F405 + +import builtins +import os +import pathlib +import typing + +import numpy +import numpy.typing + +__all__ = [ + "Optimizer", + "RegistrationStep", + "SamplingArg", + "Sigma", + "Transform", + "Transforms", +] + +class Optimizer: + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __new__(cls, optimizer: builtins.str) -> Optimizer: ... + def __getnewargs__(self) -> tuple[builtins.str]: ... + @staticmethod + def lbfgs() -> Optimizer: ... + @staticmethod + def asgd() -> Optimizer: ... + +class RegistrationStep: + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __new__( + cls, + sigma: Sigma, + sample: SamplingArg, + n_bins: builtins.int, + tolerance: builtins.float, + edge: builtins.float, + max_iterations: builtins.int, + learning_rate: builtins.float, + optimizer: Optimizer, + ) -> RegistrationStep: ... + def __getnewargs__( + self, + ) -> tuple[ + Sigma, + SamplingArg, + builtins.int, + builtins.float, + builtins.float, + builtins.int, + builtins.float, + Optimizer, + ]: ... + @staticmethod + def default_steps( + ndim: builtins.int, n: builtins.int + ) -> builtins.list[RegistrationStep]: ... + +class SamplingArg: + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __new__( + cls, + fixed: typing.Optional[builtins.int] = None, + fixed_at: typing.Optional[ + typing.Sequence[typing.Sequence[builtins.float]] + ] = None, + random: typing.Optional[builtins.int] = None, + ) -> SamplingArg: ... + def __getnewargs__( + self, + ) -> tuple[ + typing.Optional[builtins.int], + typing.Optional[builtins.list[builtins.list[builtins.float]]], + typing.Optional[builtins.int], + ]: ... + @staticmethod + def fixed(size: builtins.int) -> SamplingArg: ... + @staticmethod + def fixed_at( + f: typing.Sequence[typing.Sequence[builtins.float]], + ) -> SamplingArg: ... + @staticmethod + def random(size: builtins.int) -> SamplingArg: ... + +class Sigma: + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __new__( + cls, + absolute: typing.Optional[typing.Sequence[builtins.float]] = None, + relative: typing.Optional[typing.Sequence[builtins.float]] = None, + ) -> Sigma: ... + def __getnewargs__( + self, + ) -> tuple[builtins.str, typing.Optional[builtins.list[builtins.float]]]: ... + @staticmethod + def none() -> Sigma: ... + @staticmethod + def absolute(sigma: typing.Sequence[builtins.float]) -> Sigma: ... + @staticmethod + def relative(sigma: typing.Sequence[builtins.float]) -> Sigma: ... + def smooth( + self, array: numpy.typing.ArrayLike + ) -> numpy.typing.NDArray[numpy.float64]: ... + def sigma( + self, shape: typing.Sequence[builtins.int] + ) -> typing.Optional[builtins.list[builtins.float]]: ... + +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.Optional[typing.Sequence[builtins.float]] = None, + shape: typing.Optional[typing.Sequence[builtins.int]] = None, + 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: + @property + def channel_transforms(self) -> builtins.dict[builtins.str, Transform]: ... + @channel_transforms.setter + def channel_transforms( + self, value: typing.Mapping[builtins.str, Transform] + ) -> None: ... + @property + def drift_transforms(self) -> builtins.list[Transform]: ... + @drift_transforms.setter + def drift_transforms(self, value: typing.Sequence[Transform]) -> None: ... + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __new__( + cls, + channel: typing.Mapping[builtins.str, Transform] = {}, + drift: typing.Sequence[Transform] = [], + ) -> Transforms: ... + def __getnewargs__( + self, + ) -> tuple[builtins.dict[builtins.str, Transform], builtins.list[Transform]]: ... + @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.dict[builtins.str, 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.dict[builtins.str, Transform]: ... diff --git a/py/ndbioimage/transforms.pyi b/py/ndbioimage/transforms.pyi new file mode 100644 index 0000000..62d95ed --- /dev/null +++ b/py/ndbioimage/transforms.pyi @@ -0,0 +1,17 @@ +from .ndbioimage_rs.transforms import ( + Optimizer, + RegistrationStep, + SamplingArg, + Sigma, + Transform, + Transforms, +) + +__all__ = [ + "Optimizer", + "RegistrationStep", + "SamplingArg", + "Sigma", + "Transform", + "Transforms", +] diff --git a/pyproject.toml b/pyproject.toml index 56532e6..ca28e32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ndbioimage" -version = "2027.0.4" +version = "2027.0.5" requires-python = ">=3.10" classifiers = [ "License :: OSI Approved :: MIT License", diff --git a/src/py.rs b/src/py.rs index fc7b4e6..4ba9737 100644 --- a/src/py.rs +++ b/src/py.rs @@ -41,6 +41,7 @@ pub(crate) fn ndbioimage_file() -> PathBuf { } /// generates ndbioimage/__init__.pyi +#[gen_stub_pyfunction] #[pyfunction] pub fn generate_stub(dest_path: String) -> PyResult<()> { Ok(StubInfo::from_project_root( @@ -84,14 +85,31 @@ mod ndbioimage_rs { #[pymodule_export] use super::imread::PyShape; + #[pymodule_export] + use super::imread::PyFlags; + #[pymodule_export] use ome_metadata::py::ome_metadata; #[cfg(feature = "transforms")] - #[pymodule_export] - use super::transforms::PyTransform; + #[pymodule] + mod transforms { + #[pymodule_export] + use super::super::transforms::PyTransform; - #[cfg(feature = "transforms")] - #[pymodule_export] - use super::transforms::PyTransforms; + #[pymodule_export] + use super::super::transforms::PyTransforms; + + #[pymodule_export] + use super::super::transforms::PyRegistrationStep; + + #[pymodule_export] + use super::super::transforms::PySigma; + + #[pymodule_export] + use super::super::transforms::PySamplingArg; + + #[pymodule_export] + use super::super::transforms::PyOptimizer; + } } diff --git a/src/py/imread.rs b/src/py/imread.rs index 5deb639..9b4134d 100644 --- a/src/py/imread.rs +++ b/src/py/imread.rs @@ -15,7 +15,9 @@ use numpy::{ }; use ome_metadata::Ome; use postcard::{from_bytes, to_stdvec}; -use pyo3::exceptions::{PyIndexError, PyNotImplementedError, PyTypeError, PyValueError}; +use pyo3::exceptions::{ + PyIndexError, PyKeyError, PyNotImplementedError, PyTypeError, PyValueError, +}; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyEllipsis, PyInt, PyList, PyNone, PySlice, PyString, PyTuple}; pub(crate) use pyo3::{ @@ -25,7 +27,7 @@ pub(crate) use pyo3::{ use pyo3_stub_gen::derive::*; use pyo3_stub_gen::inventory::submit; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; /// class to read image files, while taking good care of important metadata, @@ -204,6 +206,42 @@ impl PyView { } } + /// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html + #[getter] + fn flags(&self) -> PyFlags { + PyFlags::new() + } + + /// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.setflags.html#numpy.ndarray.setflags + #[pyo3(signature = (write = None, align = None, uic = None))] + fn set_flags( + &self, + write: Option, + align: Option, + uic: Option, + ) -> PyResult<()> { + if let Some(write) = write + && write + { + Err(PyValueError::new_err("write cannot be set True")) + } else if let Some(align) = align + && align + { + Err(PyValueError::new_err("align cannot be set True")) + } else if let Some(uic) = uic + && uic + { + Err(PyValueError::new_err("uic cannot be set True")) + } else { + Ok(()) + } + } + + #[classattr] + fn available_readers() -> Vec { + DynReader::available_readers() + } + /// get all available positions (series) in the file #[staticmethod] fn get_positions<'py>( @@ -232,11 +270,34 @@ impl PyView { todo!() } + /// return a new view with transformations applied (channel alignment, drift correction) + /// backward compatibility + #[cfg(feature = "transforms")] + #[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 { + self.with_transforms( + channels, + drift, + file, + bead_files, + main_channel, + default_transform, + ) + } + /// 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>( + fn with_transforms<'py>( &self, channels: bool, drift: bool, @@ -251,21 +312,22 @@ impl PyView { /// get the transformation #[cfg(feature = "transforms")] #[getter] - fn get_transform(&self) -> PyResult { + fn get_transforms(&self) -> PyResult { Ok(PyTransforms { - inner: self.view.transforms.clone(), + inner: self.view.get_transforms().clone(), }) } /// set the transformation #[cfg(feature = "transforms")] - fn set_transform(&mut self, transform: PyTransforms) { - self.view.transforms = transform.inner; + #[setter] + fn set_transforms(&mut self, transform: PyTransforms) { + self.view.set_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())?; + fn load_transforms_from_yaml(&mut self, path: PathBuf) -> PyResult<()> { + self.view.load_transforms_from_yaml(path.as_path())?; Ok(()) } @@ -274,14 +336,19 @@ impl PyView { &self, py: Python, main_channel: usize, - ) -> PyResult> { + ) -> PyResult> { py.detach(|| { Ok(self .view .calculate_channel_transforms_2d(main_channel)? .into_iter() - .map(|t| PyTransform { - inner: t.into_dyn(), + .map(|(n, t)| { + ( + n, + PyTransform { + inner: t.into_dyn(), + }, + ) }) .collect()) }) @@ -292,25 +359,30 @@ impl PyView { &self, py: Python, main_channel: usize, - ) -> PyResult> { + ) -> PyResult> { py.detach(|| { Ok(self .view .calculate_channel_transforms_3d(main_channel)? .into_iter() - .map(|t| PyTransform { - inner: t.into_dyn(), + .map(|(n, t)| { + ( + n, + PyTransform { + inner: t.into_dyn(), + }, + ) }) .collect()) }) } #[cfg(feature = "transforms")] - fn calculate_drift_transform_2d(&self, py: Python) -> PyResult> { + fn calculate_drift_transforms_2d(&self, py: Python) -> PyResult> { py.detach(|| { Ok(self .view - .calculate_drift_transform_2d()? + .calculate_drift_transforms_2d()? .into_iter() .map(|t| PyTransform { inner: t.into_dyn(), @@ -320,11 +392,11 @@ impl PyView { } #[cfg(feature = "transforms")] - fn calculate_drift_transform_3d(&self, py: Python) -> PyResult> { + fn calculate_drift_transforms_3d(&self, py: Python) -> PyResult> { py.detach(|| { Ok(self .view - .calculate_drift_transform_3d()? + .calculate_drift_transforms_3d()? .into_iter() .map(|t| PyTransform { inner: t.into_dyn(), @@ -2094,7 +2166,7 @@ impl PyShape { } /// get dimension size by index or axis name - #[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing") + #[gen_stub(override_return_type(type_repr="typing.Optional[int | tuple[int]]", imports=("typing") ))] fn __getitem__<'py>( &self, @@ -2185,7 +2257,7 @@ impl PyShape { } else if shape.len() == 1 { Ok(shape[0].into_bound_py_any(py)?) } else { - Ok(shape.into_bound_py_any(py)?) + Ok(PyTuple::new(py, shape)?.into_bound_py_any(py)?) } } @@ -2215,3 +2287,90 @@ impl PyShape { .collect::() } } + +/// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + frozen, + eq, + hash, + name = "Flags", + module = "ndbioimage" +)] +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub(crate) struct PyFlags { + #[pyo3(get)] + c_contiguous: bool, + #[pyo3(get)] + f_contiguous: bool, + #[pyo3(get)] + owndata: bool, + #[pyo3(get)] + writable: bool, + #[pyo3(get)] + aligned: bool, + #[pyo3(get)] + writebackifcopy: bool, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyFlags { + #[new] + fn new() -> Self { + Self { + c_contiguous: false, + f_contiguous: false, + owndata: false, + writable: false, + aligned: false, + writebackifcopy: false, + } + } + + fn __getnewargs__(&self) {} + + fn __getitem__(&self, item: String) -> PyResult { + match item.to_lowercase().as_str() { + "c_contiguous" | "c" => Ok(self.c_contiguous), + "f_contiguous" | "f" => Ok(self.f_contiguous), + "owndata" | "o" => Ok(self.owndata), + "writable" | "w" => Ok(self.writable), + "aligned" | "a" => Ok(self.aligned), + "writebackifcopy" | "x" => Ok(self.writebackifcopy), + "fnc" => Ok(self.fnc()), + "forc" => Ok(self.forc()), + "behaved" | "b" => Ok(self.behaved()), + "carray" | "ca" => Ok(self.carray()), + "farray" | "fa" => Ok(self.farray()), + _ => Err(PyKeyError::new_err(format!("no such key: {}", item))), + } + } + + #[getter] + fn fnc(&self) -> bool { + self.c_contiguous && !self.f_contiguous + } + + #[getter] + fn forc(&self) -> bool { + self.c_contiguous || self.f_contiguous + } + + #[getter] + fn behaved(&self) -> bool { + self.aligned && self.writable + } + + #[getter] + fn carray(&self) -> bool { + self.behaved() && self.c_contiguous + } + + #[getter] + fn farray(&self) -> bool { + self.behaved() && self.f_contiguous && !self.c_contiguous + } +} diff --git a/src/py/transforms.rs b/src/py/transforms.rs index 20714ef..c6abab8 100644 --- a/src/py/transforms.rs +++ b/src/py/transforms.rs @@ -1,16 +1,18 @@ use crate::error::Error; use crate::transforms::Transforms; +use image_registration::metric::{SamplingArg, Sigma}; +use image_registration::register::{Optimizer, RegistrationStep}; 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::collections::HashMap; use std::path::PathBuf; #[gen_stub_pyclass] @@ -19,7 +21,7 @@ use std::path::PathBuf; from_py_object, eq, name = "Transform", - module = "ndbioimage" + module = "ndbioimage.transforms" )] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub(crate) struct PyTransform { @@ -30,8 +32,14 @@ pub(crate) struct PyTransform { #[pymethods] impl PyTransform { #[new] - #[pyo3(signature = (parameters, shape, center = None))] - fn new(parameters: Vec, shape: Vec, center: Option>) -> Self { + #[pyo3(signature = (parameters = None, shape = None, center = None))] + fn new( + parameters: Option>, + shape: Option>, + center: Option>, + ) -> Self { + let parameters = parameters.unwrap_or_else(|| vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); + let shape = shape.unwrap_or_else(|| vec![512, 512]); if let Some(center) = center { Self { inner: Transform::new_with_center(parameters, center, shape), @@ -598,7 +606,7 @@ impl PyTransform { from_py_object, eq, name = "Transforms", - module = "ndbioimage" + module = "ndbioimage.transforms" )] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub(crate) struct PyTransforms { @@ -608,12 +616,51 @@ pub(crate) struct PyTransforms { #[gen_stub_pymethods] #[pymethods] impl PyTransforms { - pub(crate) fn __getstate__(&self) -> PyResult> { - Ok(to_stdvec(self).map_err(Error::from)?) + #[new] + #[pyo3(signature = (channel = HashMap::new(), drift = Vec::new()))] + fn new( + channel: HashMap, + drift: Vec, + ) -> PyResult { + let channel = channel.into_iter().map(|(n, t)| (n, t.inner)).collect(); + let drift = drift.into_iter().map(|t| t.inner).collect(); + Ok(PyTransforms { + inner: Transforms { channel, drift }, + }) } - pub(crate) fn __setstate__(&mut self, state: Vec) -> PyResult<()> { - Ok(from_bytes(&state).map_err(Error::from)?) + #[getter] + fn get_channel_transforms(&self) -> HashMap { + self.inner + .channel + .iter() + .map(|(n, t)| (n.clone(), PyTransform { inner: t.clone() })) + .collect() + } + + #[setter] + fn set_channel_transforms(&mut self, transforms: HashMap) -> PyResult<()> { + self.inner.channel = transforms.into_iter().map(|(n, t)| (n, t.inner)).collect(); + Ok(()) + } + + #[getter] + fn get_drift_transforms(&self) -> Vec { + self.inner + .drift + .iter() + .map(|t| PyTransform { inner: t.clone() }) + .collect() + } + + #[setter] + fn set_drift_transforms(&mut self, transforms: Vec) -> PyResult<()> { + self.inner.drift = transforms.into_iter().map(|t| t.inner).collect(); + Ok(()) + } + + fn __getnewargs__(&self) -> (HashMap, Vec) { + (self.get_channel_transforms(), self.get_drift_transforms()) } #[staticmethod] @@ -632,7 +679,7 @@ impl PyTransforms { bead_files: Vec, main_channel: usize, default_transform: Option, - ) -> PyResult> { + ) -> PyResult> { Ok(Transforms::calculate_channel_transforms_2d( &bead_files .iter() @@ -644,8 +691,13 @@ impl PyTransforms { .transpose()?, )? .into_iter() - .map(|t| PyTransform { - inner: t.into_dyn(), + .map(|(n, t)| { + ( + n, + PyTransform { + inner: t.into_dyn(), + }, + ) }) .collect()) } @@ -655,7 +707,7 @@ impl PyTransforms { bead_files: Vec, main_channel: usize, default_transform: Option, - ) -> PyResult> { + ) -> PyResult> { Ok(Transforms::calculate_channel_transforms_3d( &bead_files .iter() @@ -667,9 +719,298 @@ impl PyTransforms { .transpose()?, )? .into_iter() - .map(|t| PyTransform { - inner: t.into_dyn(), + .map(|(n, t)| { + ( + n, + PyTransform { + inner: t.into_dyn(), + }, + ) }) .collect()) } } + +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + eq, + name = "RegistrationStep", + module = "ndbioimage.transforms" +)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct PyRegistrationStep { + pub(crate) inner: RegistrationStep, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyRegistrationStep { + #[new] + #[expect(clippy::too_many_arguments)] + fn new( + sigma: PySigma, + sample: PySamplingArg, + n_bins: usize, + tolerance: f64, + edge: f64, + max_iterations: usize, + learning_rate: f64, + optimizer: PyOptimizer, + ) -> PyResult { + Ok(PyRegistrationStep { + inner: RegistrationStep::new( + sigma.inner, + sample.inner, + n_bins, + tolerance, + edge, + max_iterations, + learning_rate, + optimizer.inner, + ), + }) + } + + fn __getnewargs__( + &self, + ) -> ( + PySigma, + PySamplingArg, + usize, + f64, + f64, + usize, + f64, + PyOptimizer, + ) { + ( + PySigma { + inner: self.inner.sigma.clone(), + }, + PySamplingArg { + inner: self.inner.samples.clone(), + }, + self.inner.n_bins, + self.inner.tolerance, + self.inner.edge, + self.inner.max_iterations, + self.inner.learning_rate, + PyOptimizer { + inner: self.inner.optimizer.clone(), + }, + ) + } + + #[staticmethod] + fn default_steps(ndim: usize, n: usize) -> Vec { + RegistrationStep::default_steps(ndim, n) + .into_iter() + .map(|s| PyRegistrationStep { inner: s }) + .collect() + } +} + +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + eq, + name = "Sigma", + module = "ndbioimage.transforms" +)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct PySigma { + pub(crate) inner: Sigma, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PySigma { + #[new] + #[pyo3(signature = (absolute = None, relative = None))] + fn new(absolute: Option>, relative: Option>) -> Self { + if let Some(absolute) = absolute { + Self { + inner: Sigma::Absolute(absolute), + } + } else if let Some(relative) = relative { + Self { + inner: Sigma::Relative(relative), + } + } else { + Self { inner: Sigma::None } + } + } + + fn __getnewargs__(&self) -> (String, Option>) { + match &self.inner { + Sigma::None => ("none".to_string(), None), + Sigma::Absolute(sigma) => ("absolute".to_string(), Some(sigma.clone())), + Sigma::Relative(sigma) => ("relative".to_string(), Some(sigma.clone())), + } + } + + #[staticmethod] + fn none() -> PySigma { + PySigma { inner: Sigma::None } + } + + #[staticmethod] + fn absolute(sigma: Vec) -> PySigma { + PySigma { + inner: Sigma::Absolute(sigma), + } + } + + #[staticmethod] + fn relative(sigma: Vec) -> PySigma { + PySigma { + inner: Sigma::Relative(sigma), + } + } + + fn smooth<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] + array: PyArrayLikeDyn, + ) -> PyResult>> { + let array = array.as_array(); + Ok(py + .detach(|| self.inner.smooth(array)) + .map_err(Error::from)? + .into_pyarray(py)) + } + + fn sigma(&self, shape: Vec) -> Option> { + self.inner.sigma(shape.as_slice()) + } +} + +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + eq, + name = "SamplingArg", + module = "ndbioimage.transforms" +)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct PySamplingArg { + pub(crate) inner: SamplingArg, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PySamplingArg { + #[new] + #[pyo3(signature = (fixed = None, fixed_at = None, random = None))] + fn new( + fixed: Option, + fixed_at: Option>>, + random: Option, + ) -> PyResult { + if let Some(fixed) = fixed { + Ok(Self { + inner: SamplingArg::Fixed(fixed), + }) + } else if let Some(fixed_at) = fixed_at { + Ok(Self { + inner: SamplingArg::FixedAt(fixed_at), + }) + } else if let Some(random) = random { + Ok(Self { + inner: SamplingArg::Random(random), + }) + } else { + Err(PyValueError::new_err( + "missing argument 'fixed', 'fixed_at' or 'random'", + )) + } + } + + fn __getnewargs__(&self) -> (Option, Option>>, Option) { + match &self.inner { + SamplingArg::Fixed(fixed) => (Some(*fixed), None, None), + SamplingArg::FixedAt(fixed_at) => (None, Some(fixed_at.clone()), None), + SamplingArg::Random(usize) => (None, None, Some(*usize)), + } + } + + #[staticmethod] + fn fixed(size: usize) -> PySamplingArg { + PySamplingArg { + inner: SamplingArg::Fixed(size), + } + } + + #[staticmethod] + fn fixed_at(f: Vec>) -> PySamplingArg { + PySamplingArg { + inner: SamplingArg::FixedAt(f), + } + } + + #[staticmethod] + fn random(size: usize) -> PySamplingArg { + PySamplingArg { + inner: SamplingArg::Random(size), + } + } +} + +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + eq, + name = "Optimizer", + module = "ndbioimage.transforms" +)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct PyOptimizer { + pub(crate) inner: Optimizer, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyOptimizer { + #[new] + fn new(optimizer: String) -> PyResult { + match optimizer.to_lowercase().as_str() { + "lbfgs" => Ok(Self { + inner: Optimizer::LBFGS, + }), + "asgd" => Ok(Self { + inner: Optimizer::ASGD, + }), + _ => Err(PyValueError::new_err(format!( + "invalid optimizer: {}", + optimizer + ))), + } + } + + fn __getnewargs__(&self) -> (String,) { + (match &self.inner { + Optimizer::LBFGS => "lbfgs".to_string(), + Optimizer::ASGD => "asgd".to_string(), + },) + } + + #[staticmethod] + fn lbfgs() -> PyOptimizer { + PyOptimizer { + inner: Optimizer::LBFGS, + } + } + + #[staticmethod] + fn asgd() -> PyOptimizer { + PyOptimizer { + inner: Optimizer::ASGD, + } + } +} diff --git a/src/readers.rs b/src/readers.rs index 9d5fe37..4827f3d 100644 --- a/src/readers.rs +++ b/src/readers.rs @@ -229,6 +229,7 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into { P: AsRef; } +/// Bioformats numbering impl TryFrom for PixelType { type Error = Error; @@ -257,6 +258,7 @@ impl FromStr for PixelType { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { + "bool" | "bit" => Ok(PixelType::Bool), "int8" | "i8" => Ok(PixelType::I8), "uint8" | "u8" => Ok(PixelType::U8), "int16" | "i16" => Ok(PixelType::I16), @@ -683,6 +685,22 @@ impl Reader for DynReader { } impl DynReader { + /// list of enabled readers + pub fn available_readers() -> Vec { + vec![ + #[cfg(feature = "tiff")] + "tiff".to_string(), + #[cfg(feature = "tiffseq")] + "tiffseq".to_string(), + #[cfg(feature = "czi")] + "czi".to_string(), + #[cfg(feature = "bioformats_rust")] + "bioformats_rust".to_string(), + #[cfg(feature = "bioformats_java")] + "bioformats_java".to_string(), + ] + } + /// create a dynreader by selecting the reader type explicitly pub fn from_path_select_reader(path: P, reader: R) -> Result where @@ -694,7 +712,7 @@ impl DynReader { let reader = reader.as_ref(); let reader = match reader.to_lowercase().as_str() { #[cfg(feature = "tiff")] - "tif" => Ok(DynReader::Tiff(tiff::TiffReader::new( + "tiff" => Ok(DynReader::Tiff(tiff::TiffReader::new( path, dimensions.s.unwrap_or(0), dimensions.p.unwrap_or(0), @@ -746,7 +764,7 @@ impl DynReader { let reader = reader.as_ref(); match reader.to_lowercase().as_str() { #[cfg(feature = "tiff")] - "tif" => Ok(tiff::TiffReader::get_available_positions(path, series)?), + "tiff" => Ok(tiff::TiffReader::get_available_positions(path, series)?), #[cfg(feature = "tiffseq")] "tiffseq" => Ok(tiffseq::TiffSeqReader::get_available_positions( path, series, @@ -781,7 +799,7 @@ impl DynReader { let reader = reader.as_ref(); match reader.to_lowercase().as_str() { #[cfg(feature = "tiff")] - "tif" => Ok(tiff::TiffReader::get_available_series(path)?), + "tiff" => Ok(tiff::TiffReader::get_available_series(path)?), #[cfg(feature = "tiffseq")] "tiffseq" => Ok(tiffseq::TiffSeqReader::get_available_series(path)?), #[cfg(feature = "czi")] diff --git a/src/readers/tiff.rs b/src/readers/tiff.rs index 8251ef1..260df0d 100644 --- a/src/readers/tiff.rs +++ b/src/readers/tiff.rs @@ -9,6 +9,7 @@ use std::fs::File; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use thread_local::ThreadLocal; +use tiff::decoder::ifd::Value; use tiff::decoder::{Decoder, DecodingResult}; use tiff::tags::Tag; @@ -25,8 +26,6 @@ pub struct TiffReader { n_samples: usize, p_ndim: u8, planar: bool, - interval_t: f64, - pixel_size: Option, } impl From for DynReader { @@ -55,8 +54,6 @@ impl Clone for TiffReader { n_samples: self.n_samples, p_ndim: self.p_ndim, planar: self.planar, - interval_t: self.interval_t, - pixel_size: self.pixel_size, } } } @@ -128,8 +125,6 @@ impl Reader for TiffReader { n_samples: 0, p_ndim: 0, planar: true, - interval_t: 0.0, - pixel_size: None, }; let mut reader = new.get_reader()?; @@ -157,7 +152,7 @@ impl Reader for TiffReader { .get_tag(Tag::Unknown(50839)) .or_else(|_| reader.get_tag(Tag::ImageDescription))?; let imagej_bytes = match imagej_bytes { - tiff::decoder::ifd::Value::Ascii(s) => s.into_bytes(), + Value::Ascii(s) => s.into_bytes(), other => other.into_u8_vec()?, }; let metadata_str = String::from_utf8_lossy(&imagej_bytes); @@ -179,30 +174,6 @@ impl Reader for TiffReader { .get("frames") .and_then(|v| v.parse::().ok()) .unwrap_or(1); - let interval_t = metadata_map - .get("interval") - .and_then(|v| v.parse::().ok()) - .unwrap_or(0.0); - let ru = reader - .get_tag(Tag::ResolutionUnit) - .ok() - .and_then(|i| i.into_u16().ok()) - .unwrap_or(2); - let pixel_size = reader - .get_tag(Tag::XResolution) - .ok() - .and_then(|i| i.into_f64().ok()) - .and_then(|i| { - if i <= 0.0 { - None - } else if ru == 3 { - Some(1e4 / i) - } else if ru == 2 { - Some(25400.0 / i) - } else { - Some(1.0 / i) - } - }); let mult = if p_ndim == 3 { n_samples } else { 1 }; let expected = size_c * size_z * size_t / mult; @@ -231,8 +202,6 @@ impl Reader for TiffReader { new.pixel_type = pixtype; new.n_samples = n_samples; new.p_ndim = p_ndim; - new.interval_t = interval_t; - new.pixel_size = pixel_size; new.planar = planar; // test reading first frame, if error another reader (bioformats) can take over @@ -241,6 +210,48 @@ impl Reader for TiffReader { } fn metadata(&self) -> Result { + let mut reader = self.get_reader()?; + let imagej_bytes = reader + .get_tag(Tag::Unknown(50839)) + .or_else(|_| reader.get_tag(Tag::ImageDescription))?; + let imagej_bytes = match imagej_bytes { + Value::Ascii(s) => s.into_bytes(), + other => other.into_u8_vec()?, + }; + let metadata_str = String::from_utf8_lossy(&imagej_bytes); + let metadata_map = parse_imagej_metadata(&metadata_str); + + let interval_t = metadata_map + .get("interval") + .and_then(|v| v.parse::().ok()); + let ru = reader + .get_tag(Tag::ResolutionUnit) + .ok() + .and_then(|i| i.into_u16().ok()) + .unwrap_or(2); + let pixel_size = reader + .get_tag(Tag::XResolution) + .ok() + .and_then(|i| i.into_u64_vec().ok()) + .and_then(|i| { + i.first() + .and_then(|&n| i.get(1).map(|&d| (n as f64) / (d as f64))) + }) + .and_then(|i| { + if i <= 0.0 { + None + } else if ru == 3 { + Some(1e4 / i) + } else if ru == 2 { + Some(25400.0 / i) + } else { + Some(1.0 / i) + } + }); + let interval_z = metadata_map + .get("spacing") + .and_then(|v| v.parse::().ok()); + let ome_pixel_type = match self.pixel_type { PixelType::I8 => ome::PixelType::Int8, PixelType::U8 => ome::PixelType::Uint8, @@ -262,13 +273,13 @@ impl Reader for TiffReader { size_t: self.shape.t as i32, dimension_order: ome::PixelsDimensionOrderType::Xyczt, r#type: ome_pixel_type, - physical_size_x: self.pixel_size.map(|v| v as f32), + physical_size_x: pixel_size.map(|v| v as f32), physical_size_x_unit: ome::UnitsLength::um, - physical_size_y: self.pixel_size.map(|v| v as f32), + physical_size_y: pixel_size.map(|v| v as f32), physical_size_y_unit: ome::UnitsLength::um, - physical_size_z: None, + physical_size_z: interval_z.map(|v| v as f32), physical_size_z_unit: ome::UnitsLength::um, - time_increment: None, + time_increment: interval_t.map(|v| v as f32), time_increment_unit: ome::UnitsTime::s, significant_bits: None, interleaved: None, @@ -295,11 +306,13 @@ impl Reader for TiffReader { the_c: Some(c as i32), the_z: Some(z as i32), the_t: Some(t as i32), - delta_t: if self.interval_t > 0.0 { - Some(t as f32 * self.interval_t as f32) - } else { - None - }, + delta_t: interval_t.and_then(|v| { + if v > 0.0 { + Some((t as f64 * v) as f32) + } else { + None + } + }), ..Default::default() }); } @@ -341,7 +354,7 @@ impl Reader for TiffReader { reader.seek_to_image(page_idx)?; let data: DecodingResult = if self.planar { - let mut result = DecodingResult::U16(vec![]); + let mut result = DecodingResult::U16(Vec::new()); reader.read_image_to_buffer(&mut result)?; result } else { diff --git a/src/transforms.rs b/src/transforms.rs index b064ebb..498b8c4 100644 --- a/src/transforms.rs +++ b/src/transforms.rs @@ -3,30 +3,73 @@ use crate::error::Error; use crate::readers::{DynReader, Reader}; use crate::view::View; pub use image_registration::transform::Transform; -use ndarray::{Dimension, Ix2, Ix3, s}; +use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, s}; use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::io::Seek; use std::path::Path; -#[expect(clippy::upper_case_acronyms)] -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum TransformD { - YX(Transform), - ZYX(Transform), +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +struct TransformOld { + #[serde(alias = "CenterOfRotationPoint")] + center_of_rotation_point: Vec, + #[serde(alias = "Size")] + size: Vec, + #[serde(alias = "TransformParameters")] + transform_parameters: Vec, + #[serde(alias = "dTransformParameters")] + dtransform_parameters: Vec, +} + +impl TryFrom> for Transforms { + type Error = Error; + + fn try_from(t: HashMap) -> Result { + Ok(Self { + channel: t + .into_iter() + .map(|(n, i)| { + Ok(( + n, + Transform::new_with_center( + i.transform_parameters, + i.center_of_rotation_point, + i.size.into_iter().map(|j| j as usize).collect(), + ) + .with_dparameters(i.dtransform_parameters) + .inverse()?, + )) + }) + .collect::>()?, + drift: Vec::new(), + }) + } } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct Transforms { - pub channel: Vec, - pub drift: Vec, + #[serde(skip_serializing_if = "HashMap::is_empty", default = "HashMap::new")] + pub channel: HashMap>, + #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")] + 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 load>(path: P) -> Result { + let mut file = std::fs::File::open(&path)?; + Ok( + if let Ok(t) = yaml_serde::from_reader::<_, Self>(&file) + && !(t.channel.is_empty() && t.drift.is_empty()) + { + t + } else { + file.seek(std::io::SeekFrom::Start(0))?; + yaml_serde::from_reader::<_, HashMap>(&file)?.try_into()? + }, + ) } - pub fn save(&self, path: &Path) -> Result<(), Error> { + pub fn save>(&self, path: P) -> Result<(), Error> { let file = std::fs::OpenOptions::new() .write(true) .create(true) @@ -39,30 +82,43 @@ impl Transforms { bead_files: &[&Path], main_channel: usize, default_transform: Option>, - ) -> Result>, Error> { + ) -> 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 + let channels = transforms + .iter() + .flat_map(|t| t.keys()) + .collect::>(); + let mut average_transforms = HashMap::new(); + for channel in channels { + let (s, n) = transforms .iter() - .map(|t| (&t[channel] * &default_transform).matrix()) - .reduce(|a, b| a + b) - .unwrap() - / n_channels as f64; - let dmatrix = transforms + .filter_map(|t| { + if t.contains_key(channel) { + Some((&t[channel] * &default_transform).matrix()) + } else { + None + } + }) + .fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1)); + let matrix = s / (n as f64); + let (s, n) = 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( + .filter_map(|t| { + if t.contains_key(channel) { + Some(((&t[channel] * &default_transform).matrix() - &matrix).powi(2)) + } else { + None + } + }) + .fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1)); + let dmatrix = (s / n as f64).sqrt(); + average_transforms.insert( + channel.clone(), Transform::default() .with_matrix(matrix.view()) .with_dmatrix(dmatrix.view()), @@ -75,30 +131,43 @@ impl Transforms { bead_files: &[&Path], main_channel: usize, default_transform: Option>, - ) -> Result>, Error> { + ) -> 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 + let channels = transforms + .iter() + .flat_map(|t| t.keys()) + .collect::>(); + let mut average_transforms = HashMap::new(); + for channel in channels { + let (s, n) = transforms .iter() - .map(|t| (&t[channel] * &default_transform).matrix()) - .reduce(|a, b| a + b) - .unwrap() - / n_channels as f64; - let dmatrix = transforms + .filter_map(|t| { + if t.contains_key(channel) { + Some((&t[channel] * &default_transform).matrix()) + } else { + None + } + }) + .fold((Array2::zeros((4, 4)), 0), |(s, n), t| (s + t, n + 1)); + let matrix = s / (n as f64); + let (s, n) = 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( + .filter_map(|t| { + if t.contains_key(channel) { + Some(((&t[channel] * &default_transform).matrix() - &matrix).powi(2)) + } else { + None + } + }) + .fold((Array2::zeros((4, 4)), 0), |(s, n), t| (s + t, n + 1)); + let dmatrix = (s / n as f64).sqrt(); + average_transforms.insert( + channel.clone(), Transform::default() .with_matrix(matrix.view()) .with_dmatrix(dmatrix.view()), @@ -109,34 +178,61 @@ impl Transforms { } impl View { - pub fn with_transform_from_yaml(mut self, path: &Path) -> Result { - self.transforms = Transforms::load(path)?; + #[cfg(feature = "transforms")] + pub(crate) fn get_transforms(&self) -> &Transforms { + &self.transforms + } + + #[cfg(feature = "transforms")] + pub(crate) fn set_transforms(&mut self, transforms: Transforms) { + self.transforms = transforms; + } + + pub fn with_transforms_from_yaml(mut self, path: &Path) -> Result { + self.set_transforms(Transforms::load(path)?); Ok(self) } - pub fn load_transform_from_yaml(&mut self, path: &Path) -> Result<(), Error> { - self.transforms = Transforms::load(path)?; + pub fn load_transforms_from_yaml(&mut self, path: &Path) -> Result<(), Error> { + self.set_transforms(Transforms::load(path)?); Ok(()) } pub fn calculate_channel_transforms_2d( &self, main_channel: usize, - ) -> Result>, Error> { + ) -> 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 { + let mut transforms = HashMap::new(); + let shape = vec![self.shape().y, self.shape().x]; + let center = shape + .iter() + .map(|&i| (i as f64 - 1.0) / 2.0) + .collect::>(); + for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() { + let name = metadata + .name + .clone() + .unwrap_or_else(|| format!("channel{}", channel)); if channel == main_channel { - transforms.push(Transform::default()); + transforms.insert( + name, + Transform::default() + .with_shape(shape.clone()) + .with_center(center.clone()), + ); } 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())?); + transforms.insert( + name, + Transform::register_affine(main_max.view(), max.view())?, + ); } } Ok(transforms) @@ -145,29 +241,46 @@ impl View { pub fn calculate_channel_transforms_3d( &self, main_channel: usize, - ) -> Result>, Error> { + ) -> 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 { + let mut transforms = HashMap::new(); + let shape = vec![self.shape().z, self.shape().y, self.shape().x]; + let center = shape + .iter() + .map(|&i| (i as f64 - 1.0) / 2.0) + .collect::>(); + for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() { + let name = metadata + .name + .clone() + .unwrap_or_else(|| format!("channel{}", channel)); if channel == main_channel { - transforms.push(Transform::default()); + transforms.insert( + name, + Transform::default() + .with_shape(shape.clone()) + .with_center(center.clone()), + ); } else { let max = self .slice_cztyx(s![channel, .., 0, .., ..])? .as_array::()?; - transforms.push(Transform::register_affine(main_max.view(), max.view())?); + transforms.insert( + name, + Transform::register_affine(main_max.view(), max.view())?, + ); } } Ok(transforms) } - pub fn calculate_drift_transform_2d(&self) -> Result>, Error> { + pub fn calculate_drift_transforms_2d(&self) -> Result>, Error> { todo!() } - pub fn calculate_drift_transform_3d(&self) -> Result>, Error> { + pub fn calculate_drift_transforms_3d(&self) -> Result>, Error> { todo!() } } @@ -182,4 +295,22 @@ mod tests { Ok(()) } + + #[test] + fn load_transforms() -> Result<(), Box> { + let t = Transforms::load("tests/files/transforms.yml")?; + println!("t: {:?}", t); + assert_eq!(t.channel["TV1"].ndim(), 2); + assert_eq!(t.channel["TV2"].ndim(), 2); + Ok(()) + } + + #[test] + fn load_old_transforms() -> Result<(), Box> { + let t = Transforms::load("tests/files/transform_red.yml")?; + println!("t: {:?}", t); + assert_eq!(t.channel["TV1"].ndim(), 2); + assert_eq!(t.channel["TV2"].ndim(), 2); + Ok(()) + } }