From 3c7af5da4d9526e8ab2bbad69d0075bc31ab4c30 Mon Sep 17 00:00:00 2001 From: "w.pomp" Date: Tue, 27 Oct 2026 18:23:37 +0100 Subject: [PATCH] - return Error, raise Exception when trying to use f128 data - clean parsing of python types into rust types - finish PyView.reshape --- image-registration | 2 +- image-tiff | 2 +- .../py/ndbioimage/ndbioimage_rs/__init__.pyi | 33 +-- .../ndbioimage/ndbioimage_rs/transforms.pyi | 40 +++- ndbioimage/py/ndbioimage/transforms.pyi | 2 + ndbioimage/src/error.rs | 2 + ndbioimage/src/lib.rs | 4 +- ndbioimage/src/py/imread.rs | 217 +++++++++--------- ndbioimage/src/py/transforms.rs | 140 +++++++++-- ndbioimage/src/readers.rs | 10 +- ndbioimage/src/readers/bioformats_java.rs | 6 +- ndbioimage/src/readers/bioformats_rust.rs | 6 +- ndbioimage/src/readers/czi.rs | 2 +- ndbioimage/src/tiffwrite.rs | 2 +- ndbioimage/src/transforms.rs | 111 +++++---- ndbioimage/src/view.rs | 23 ++ 16 files changed, 394 insertions(+), 208 deletions(-) diff --git a/image-registration b/image-registration index 9257dd1..0049267 160000 --- a/image-registration +++ b/image-registration @@ -1 +1 @@ -Subproject commit 9257dd1fcf4a553fd51e0f3f56d26960ab72f383 +Subproject commit 004926799714dde2bad65d3f9a4971b3cc532105 diff --git a/image-tiff b/image-tiff index a6e8ed7..563b2b1 160000 --- a/image-tiff +++ b/image-tiff @@ -1 +1 @@ -Subproject commit a6e8ed772de58cedf3ceb9a5720fe52997433f62 +Subproject commit 563b2b11d7ca287eea88d8b0d87276a5acca4f00 diff --git a/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi b/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi index dc4cbf5..970ce96 100644 --- a/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi +++ b/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi @@ -237,18 +237,6 @@ class Imread: None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes """ @property - def default_interpolation_value(self) -> transforms.DefaultValue: - r""" - the value of the interpolated image outside the data - """ - @default_interpolation_value.setter - def default_interpolation_value( - self, value: float | DefaultValue | collections.abc.Sequence[float] - ) -> None: - r""" - the value of the interpolated image outside the data - """ - @property def transforms(self) -> transforms.Transforms: r""" get the transformation @@ -322,7 +310,8 @@ class Imread: copy: typing.Optional[builtins.bool] = None, ) -> Imread | numpy.NDArray: r""" - reshape the view with a new axis order + reshape the view with a new axis order, arguments order and copy cannot be used and will + raise an exception if order != "C" or copy is False """ def squeeze( self, axes: str | collections.abc.Sequence[int | str] = None @@ -795,8 +784,24 @@ class Imread: the bspline order used for interpolation when using transforms None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes """ + def get_default_interpolation_value( + self, channel: builtins.int + ) -> transforms.DefaultValue: + r""" + the value of the interpolated image outside the data + """ + def set_default_interpolation_value( + self, + channel: builtins.int, + value: float | transforms.DefaultValue | collections.abc.Sequence[float], + ) -> None: + r""" + the value of the interpolated image outside the data + """ def with_default_interpolation_value( - self, value: float | DefaultValue | collections.abc.Sequence[float] + self, + channel: builtins.int, + value: float | transforms.DefaultValue | collections.abc.Sequence[float], ) -> Imread: r""" the value of the interpolated image outside the data diff --git a/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi b/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi index dc94282..a131639 100644 --- a/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi +++ b/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi @@ -2,6 +2,7 @@ # ruff: noqa: E501, F401, F403, F405 import builtins +import collections.abc import os import pathlib import typing @@ -28,8 +29,11 @@ class DefaultValue: def __new__( cls, values: typing.Sequence[builtins.float], - weights: typing.Sequence[builtins.float], + weights: typing.Optional[typing.Sequence[builtins.float]] = None, ) -> DefaultValue: ... + def __getnewargs__( + self, + ) -> tuple[builtins.list[builtins.float], builtins.list[builtins.float]]: ... class Optimizer: def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -249,7 +253,18 @@ class Transforms: channel: typing.Mapping[builtins.str, Transform] = {}, drift: typing.Sequence[Transform] = [], interpolation_order: typing.Optional[builtins.int] = None, + default_value: typing.Optional[ + typing.Mapping[builtins.str, DefaultValue] + ] = None, ) -> Transforms: ... + def __getnewargs__( + self, + ) -> tuple[ + builtins.dict[builtins.str, Transform], + builtins.list[Transform], + typing.Optional[builtins.int], + builtins.dict[builtins.str, DefaultValue], + ]: ... def with_interpolation_order( self, order: typing.Optional[builtins.int] ) -> Transforms: @@ -257,9 +272,28 @@ class Transforms: the bspline order used for interpolation when using transforms None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes """ - def __getnewargs__( + def get_default_interpolation_value( + self, channel_name: builtins.str + ) -> DefaultValue: + r""" + the value of the interpolated image outside the data + """ + def set_default_interpolation_value( self, - ) -> tuple[builtins.dict[builtins.str, Transform], builtins.list[Transform]]: ... + channel_name: builtins.str, + value: float | DefaultValue | collections.abc.Sequence[float], + ) -> None: + r""" + the value of the interpolated image outside the data + """ + def with_default_interpolation_value( + self, + channel_name: builtins.str, + value: float | DefaultValue | collections.abc.Sequence[float], + ) -> Transforms: + r""" + the value of the interpolated image outside the data + """ @staticmethod def load(path: builtins.str | os.PathLike | pathlib.Path) -> Transforms: ... def save(self, path: builtins.str | os.PathLike | pathlib.Path) -> None: ... diff --git a/ndbioimage/py/ndbioimage/transforms.pyi b/ndbioimage/py/ndbioimage/transforms.pyi index 62d95ed..0274aee 100644 --- a/ndbioimage/py/ndbioimage/transforms.pyi +++ b/ndbioimage/py/ndbioimage/transforms.pyi @@ -1,4 +1,5 @@ from .ndbioimage_rs.transforms import ( + DefaultValue, Optimizer, RegistrationStep, SamplingArg, @@ -8,6 +9,7 @@ from .ndbioimage_rs.transforms import ( ) __all__ = [ + "DefaultValue", "Optimizer", "RegistrationStep", "SamplingArg", diff --git a/ndbioimage/src/error.rs b/ndbioimage/src/error.rs index a40a44b..8dbd05f 100644 --- a/ndbioimage/src/error.rs +++ b/ndbioimage/src/error.rs @@ -184,6 +184,8 @@ pub enum Error { /// no valid blocks in czi file #[error("czi file has no valid blocks")] CziNoValidBlocks, + #[error("f128 is not supported")] + F128NotSupported, } impl Error { diff --git a/ndbioimage/src/lib.rs b/ndbioimage/src/lib.rs index b4a182e..928f2aa 100644 --- a/ndbioimage/src/lib.rs +++ b/ndbioimage/src/lib.rs @@ -6,8 +6,8 @@ //! structure. Additionally, it can automatically calculate an affine transform that corrects for //! chromatic aberrations etc. and apply it on the fly to the image. //! -//! Currently, it supports imagej tif files, czi files, micromanager tif sequences and anything -//! [bioformats](https://www.openmicroscopy.org/bio-formats/) can handle. +//! Currently, it supports imagej and ome tif files, czi files, micromanager tif sequences and +//! anything [bioformats](https://www.openmicroscopy.org/bio-formats/) can handle. //! //! ```rust,no_run //! use ndarray::Array2; diff --git a/ndbioimage/src/py/imread.rs b/ndbioimage/src/py/imread.rs index 79e7525..61cc7d6 100644 --- a/ndbioimage/src/py/imread.rs +++ b/ndbioimage/src/py/imread.rs @@ -17,7 +17,7 @@ use pyo3::exceptions::{ PyIndexError, PyKeyError, PyNotImplementedError, PyTypeError, PyValueError, }; use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyEllipsis, PyInt, PyNone, PySlice, PyString, PyTuple}; +use pyo3::types::{PyEllipsis, PyNone, PySlice, PyTuple}; pub(crate) use pyo3::{ Bound, IntoPyObject, IntoPyObjectExt, PyAny, PyRef, PyResult, Python, pyclass, pyfunction, pymethods, @@ -73,14 +73,15 @@ impl PyView { fn item(py: Python, view: View, dtype: PixelType) -> PyResult> { macro_rules! item { ($t:tt) => { - view.into_dimensionality::()? + Ok(view + .into_dimensionality::()? .item::<$t>()? .into_pyobject(py)? - .into_any() + .into_any()) }; } - Ok(match dtype { + match dtype { PixelType::Bool => todo!(), PixelType::I8 => item!(i8), PixelType::U8 => item!(u8), @@ -94,8 +95,8 @@ impl PyView { PixelType::U64 => item!(u64), PixelType::I128 => item!(i128), PixelType::U128 => item!(u128), - PixelType::F128 => item!(f64), - }) + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } } @@ -123,10 +124,10 @@ impl PyView { 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)?; + if let Ok(view) = path.extract::() { + Ok(view) + } else if let Ok(bytes) = path.extract::>() { + let mut pyview: Self = from_bytes(&bytes).map_err(Error::from)?; pyview.ome = pyview.view.metadata()?; Ok(pyview) } else { @@ -135,7 +136,6 @@ impl PyView { builtins .getattr("str")? .call1((path,))? - .cast_into::()? .extract::()?, ); let axes = axes @@ -255,17 +255,17 @@ impl PyView { self.view.reader_name().to_string() } - /// reshape the view with a new axis order + /// reshape the view with a new axis order, arguments order and copy cannot be used and will + /// raise an exception if order != "C" or copy is False #[gen_stub(override_return_type(type_repr="Imread | numpy.NDArray", imports=("numpy", "numpy.typing")))] #[pyo3(signature = (*shape, order="C", copy=None))] fn reshape<'py>( &self, py: Python<'py>, - shape: Vec, + shape: Bound<'py, PyAny>, order: &str, copy: Option, ) -> PyResult> { - // TODO: check shape argument if order != "C" { return Err(PyNotImplementedError::new_err( "order except C is not implemented", @@ -279,6 +279,28 @@ impl PyView { )); } let n = self.view.shape().iter().product::(); + let shape = if let Ok(shape) = shape.try_iter() { + shape + .map(|i| { + let i = i?; + if let Ok(ii) = i.try_iter() { + ii.map(|j| { + let j = j?; + j.extract::() + }) + .collect::>>() + } else { + Ok(vec![i.extract::()?]) + } + }) + .collect::>>()? + .into_iter() + .flatten() + .collect::>() + } else { + vec![shape.extract::()?] + }; + let m = shape .iter() .fold(1, |i, j| if *j < 0 { i } else { i * (*j as usize) }); @@ -316,14 +338,9 @@ impl PyView { .try_iter()? .map(|a| { let axis = a?; - if axis.is_instance_of::() { - Ok(axis - .cast_into::()? - .extract::()? - .parse::() - .map_err(Error::from)?) - } else if axis.is_instance_of::() { - let mut index = axis.cast_into::()?.extract::()?; + if let Ok(axis) = axis.extract::() { + Ok(axis.parse::().map_err(Error::from)?) + } else if let Ok(mut index) = axis.extract::() { if index < 0 { index += view_axes.len() as isize; } @@ -347,14 +364,15 @@ impl PyView { if view.ndim() == 0 { macro_rules! squeeze { ($t:tt) => { - view.into_dimensionality::()? + Ok(view + .into_dimensionality::()? .item::<$t>()? .into_pyobject(py)? - .into_any() + .into_any()) }; } - Ok(match self.dtype { + match self.dtype { PixelType::Bool => todo!(), PixelType::I8 => squeeze!(i8), PixelType::U8 => squeeze!(u8), @@ -368,8 +386,8 @@ impl PyView { PixelType::U128 => squeeze!(u128), PixelType::F32 => squeeze!(f32), PixelType::F64 => squeeze!(f64), - PixelType::F128 => squeeze!(f64), - }) + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } else { PyView { view, @@ -1191,14 +1209,15 @@ impl PyView { ) -> PyResult> { macro_rules! get_frame { ($t:tt) => { - self.view + Ok(self + .view .get_frame::<$t, _>(c, z, t)? .into_pyarray(py) - .into_any() + .into_any()) }; } - Ok(match self.dtype { + match self.dtype { PixelType::Bool => todo!(), PixelType::I8 => get_frame!(i8), PixelType::U8 => get_frame!(u8), @@ -1210,10 +1229,10 @@ impl PyView { PixelType::F64 => get_frame!(f64), PixelType::I64 => get_frame!(i64), PixelType::U64 => get_frame!(u64), - PixelType::I128 => get_frame!(i64), - PixelType::U128 => get_frame!(u64), - PixelType::F128 => get_frame!(f64), - }) + PixelType::I128 => Err(PyTypeError::new_err("i128 type is not supported")), + PixelType::U128 => Err(PyTypeError::new_err("u128 type is not supported")), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } /// flatten the view into a 1D numpy array @@ -1221,11 +1240,11 @@ impl PyView { fn flatten<'py>(&self, py: Python<'py>) -> PyResult> { macro_rules! flatten { ($t:tt) => { - self.view.flatten::<$t>()?.into_pyarray(py).into_any() + Ok(self.view.flatten::<$t>()?.into_pyarray(py).into_any()) }; } - Ok(match self.dtype { + match self.dtype { PixelType::Bool => todo!(), PixelType::I8 => flatten!(i8), PixelType::U8 => flatten!(u8), @@ -1237,21 +1256,21 @@ impl PyView { PixelType::F64 => flatten!(f64), PixelType::I64 => flatten!(i64), PixelType::U64 => flatten!(u64), - PixelType::I128 => flatten!(i64), - PixelType::U128 => flatten!(u64), - PixelType::F128 => flatten!(f64), - }) + PixelType::I128 => Err(PyTypeError::new_err("i128 type is not supported")), + PixelType::U128 => Err(PyTypeError::new_err("u128 type is not supported")), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } /// convert the view to bytes fn to_bytes(&self) -> PyResult> { macro_rules! to_bytes { ($t:tt) => { - self.view.to_bytes::<$t>()? + Ok(self.view.to_bytes::<$t>()?) }; } - Ok(match self.dtype { + match self.dtype { PixelType::Bool => todo!(), PixelType::I8 => to_bytes!(i8), PixelType::U8 => to_bytes!(u8), @@ -1263,10 +1282,10 @@ impl PyView { PixelType::F64 => to_bytes!(f64), PixelType::I64 => to_bytes!(i64), PixelType::U64 => to_bytes!(u64), - PixelType::I128 => to_bytes!(i64), - PixelType::U128 => to_bytes!(u64), - PixelType::F128 => to_bytes!(f64), - }) + PixelType::I128 => Err(PyTypeError::new_err("i128 type is not supported")), + PixelType::U128 => Err(PyTypeError::new_err("u128 type is not supported")), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } /// convert the view to bytes (alias for to_bytes) @@ -1335,19 +1354,15 @@ impl PyView { &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)?; + if let Ok(axis) = axis.extract::() { + let axis = axis.parse::().map_err(Error::from)?; self.view .axes() .iter() .position(|a| *a == axis) .ok_or_else(|| PyValueError::new_err(format!("cannot find axis {:?}", axis))) - } else if axis.is_instance_of::() { - Ok(axis.cast_into::()?.extract::()?) + } else if let Ok(axis) = axis.extract::() { + Ok(axis) } else { Err(PyValueError::new_err("cannot convert to axis".to_string())) } @@ -1416,13 +1431,14 @@ impl PyView { fn as_array<'py>(&self, py: Python<'py>) -> PyResult> { macro_rules! as_array { ($t:tt) => { - py.detach(|| self.view.as_array_dyn::<$t>())? + Ok(py + .detach(|| self.view.as_array_dyn::<$t>())? .into_pyarray(py) - .into_any() + .into_any()) }; } - Ok(match self.dtype { + match self.dtype { PixelType::Bool => todo!(), PixelType::I8 => as_array!(i8), PixelType::U8 => as_array!(u8), @@ -1434,10 +1450,10 @@ impl PyView { PixelType::F64 => as_array!(f64), PixelType::I64 => as_array!(i64), PixelType::U64 => as_array!(u64), - PixelType::I128 => as_array!(i64), - PixelType::U128 => as_array!(u64), - PixelType::F128 => as_array!(f64), - }) + PixelType::I128 => Err(PyTypeError::new_err("i128 type is not supported")), + PixelType::U128 => Err(PyTypeError::new_err("u128 type is not supported")), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } /// the numpy dtype of the view @@ -1462,15 +1478,9 @@ impl PyView { PixelType::F64 => get_dtype!(f64), PixelType::I64 => get_dtype!(i64), PixelType::U64 => get_dtype!(u64), - 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 => Err(PyTypeError::new_err( - "type is f128, but this is not supported by rust", - )), + PixelType::I128 => Err(PyTypeError::new_err("i128 type is not supported")), + PixelType::U128 => Err(PyTypeError::new_err("u128 type is not supported")), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), } } @@ -1524,11 +1534,11 @@ impl PyView { } else { macro_rules! max { ($t:tt) => { - self.view.max::<$t>()?.into_pyobject(py)?.into_any() + Ok(self.view.max::<$t>()?.into_pyobject(py)?.into_any()) }; } - Ok(match self.dtype { + match self.dtype { PixelType::Bool => todo!(), PixelType::I8 => max!(i8), PixelType::U8 => max!(u8), @@ -1540,10 +1550,10 @@ impl PyView { PixelType::F64 => max!(f64), PixelType::I64 => max!(i64), PixelType::U64 => max!(u64), - PixelType::I128 => max!(i64), - PixelType::U128 => max!(u64), - PixelType::F128 => max!(f64), - }) + PixelType::I128 => Err(PyTypeError::new_err("i128 type is not supported")), + PixelType::U128 => Err(PyTypeError::new_err("u128 type is not supported")), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } } @@ -1585,11 +1595,11 @@ impl PyView { } else { macro_rules! min { ($t:tt) => { - self.view.min::<$t>()?.into_pyobject(py)?.into_any() + Ok(self.view.min::<$t>()?.into_pyobject(py)?.into_any()) }; } - Ok(match self.dtype { + match self.dtype { PixelType::Bool => todo!(), PixelType::I8 => min!(i8), PixelType::U8 => min!(u8), @@ -1601,10 +1611,10 @@ impl PyView { PixelType::F64 => min!(f64), PixelType::I64 => min!(i64), PixelType::U64 => min!(u64), - PixelType::I128 => min!(i64), - PixelType::U128 => min!(u64), - PixelType::F128 => min!(f64), - }) + PixelType::I128 => Err(PyTypeError::new_err("i128 type is not supported")), + PixelType::U128 => Err(PyTypeError::new_err("u128 type is not supported")), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } } } @@ -1701,20 +1711,20 @@ impl PyView { } else { macro_rules! sum { ($t:tt) => { - self.view.sum::<$t>()?.into_pyobject(py)?.into_any() + Ok(self.view.sum::<$t>()?.into_pyobject(py)?.into_any()) }; } - Ok(match self.dtype { + match self.dtype { PixelType::F32 => sum!(f32), PixelType::F64 => sum!(f64), PixelType::I64 => sum!(i64), PixelType::U64 => sum!(u64), PixelType::I128 => sum!(i128), PixelType::U128 => sum!(u128), - PixelType::F128 => sum!(f64), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), _ => sum!(i64), - }) + } } } @@ -1876,16 +1886,14 @@ impl PyView { 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() + let path = if let Ok(view) = path.extract::() { + view.view.path().to_owned() } else { let builtins = PyModule::import(py, "builtins")?; PathBuf::from( builtins .getattr("str")? .call1((path,))? - .cast_into::()? .extract::()?, ) }; @@ -1903,16 +1911,14 @@ impl PyView { 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() + let path = if let Ok(view) = path.extract::() { + view.view.path().to_owned() } else { let builtins = PyModule::import(py, "builtins")?; PathBuf::from( builtins .getattr("str")? .call1((path,))? - .cast_into::()? .extract::()?, ) }; @@ -1963,9 +1969,7 @@ impl PyView { PixelType::U128 => save_as_tiff!(u128), PixelType::F32 => save_as_tiff!(f32), PixelType::F64 => save_as_tiff!(f64), - PixelType::F128 => Err(Error::NotImplemented( - "saving as f128 is not implemented".to_string(), - )), + PixelType::F128 => Err(Error::F128NotSupported), })?; Ok(()) } @@ -2077,8 +2081,7 @@ fn parse_slice( ) -> PyResult<(Vec, usize)> { if s.is_none() { Ok((vec![SliceInfoElem::NewAxis], 0)) - } else if s.is_instance_of::() { - let mut index = s.cast::()?.extract::()?; + } else if let Ok(mut index) = s.extract::() { if index < 0 { index += t as isize; } @@ -2089,8 +2092,8 @@ fn parse_slice( ))); } Ok((vec![SliceInfoElem::Index(index)], 1)) - } else if s.is_instance_of::() { - let u = s.cast::()?.indices(t as isize)?; + } else if let Ok(slice) = s.cast::() { + let u = slice.indices(t as isize)?; Ok(( vec![SliceInfoElem::Slice { start: u.start, @@ -2314,10 +2317,8 @@ impl PyShape { let (idx, is_idx): (Vec, bool) = 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)?; + } else if let Ok(slice) = idx.cast::() { + let indices = slice.indices(self.inner.order.len() as isize)?; ( if indices.step > 0 { (indices.start..indices.stop) @@ -2332,8 +2333,7 @@ impl PyShape { }, true, ) - } else if idx.is_instance_of::() { - let s = idx.cast::()?.extract::()?; + } else if let Ok(s) = idx.extract::() { ( s.to_uppercase() .chars() @@ -2368,9 +2368,8 @@ impl PyShape { .collect::>()?, true, ) - } else if idx.is_instance_of::() { + } else if let Ok(mut i) = idx.extract::() { let len = self.inner.order.len() as isize; - let mut i = idx.cast::()?.extract::()?; if i < 0 { i += len }; diff --git a/ndbioimage/src/py/transforms.rs b/ndbioimage/src/py/transforms.rs index dca527d..e6ae086 100644 --- a/ndbioimage/src/py/transforms.rs +++ b/ndbioimage/src/py/transforms.rs @@ -14,7 +14,6 @@ use numpy::{ use pyo3::IntoPyObjectExt; use pyo3::exceptions::{PyNotImplementedError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::PyFloat; use pyo3_stub_gen::derive::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -37,9 +36,10 @@ pub(crate) struct PyDefaultValue { #[pymethods] impl PyDefaultValue { #[new] - fn new(values: Vec, weights: Vec) -> Self { + #[pyo3(signature = (values, weights = None))] + fn new(values: Vec, weights: Option>) -> Self { Self { - inner: DefaultValue::new(values, weights), + inner: DefaultValue::new(values, weights.unwrap_or_default()), } } @@ -52,6 +52,10 @@ impl PyDefaultValue { fn weights(&self) -> Vec { self.inner.weights().to_vec() } + + fn __getnewargs__(&self) -> (Vec, Vec) { + (self.inner.values().to_vec(), self.inner.weights().to_vec()) + } } #[gen_stub_pyclass] @@ -125,9 +129,9 @@ impl PyTransform { py: Python, #[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound, ) -> PyResult { - if other.is_instance_of::() { + if let Ok(other) = other.extract::() { Ok(PyTransform { - inner: &self.inner * &other.extract::()?.inner, + inner: &self.inner * other.inner, }) } else { let builtins = PyModule::import(py, "builtins")?; @@ -143,9 +147,9 @@ impl PyTransform { py: Python, #[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound, ) -> PyResult { - if other.is_instance_of::() { + if let Ok(other) = other.extract::() { Ok(PyTransform { - inner: &other.extract::()?.inner * &self.inner, + inner: &other.inner * &self.inner, }) } else { let builtins = PyModule::import(py, "builtins")?; @@ -803,29 +807,33 @@ impl PyView { fn set_default_interpolation_value( &mut self, channel: usize, - #[gen_stub(override_type(type_repr="float | DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))] + #[gen_stub(override_type(type_repr="float | transforms.DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))] value: Bound, ) -> PyResult<()> { let channel_name = self.view.channel_name(channel)?.to_string(); - if value.is_instance_of::() { + if let Ok(value) = value.extract::() { self.view .transforms .default_value - .insert(channel_name, value.extract::()?.inner); - } else if value.is_instance_of::() { - self.view.transforms.default_value.insert( - channel_name, - value.cast::()?.extract::()?.into(), - ); - } else { + .insert(channel_name, value.inner); + } else if let Ok(value) = value.extract::() { + self.view + .transforms + .default_value + .insert(channel_name, value.into()); + } else if let Ok(value) = value.try_iter() { self.view.transforms.default_value.insert( channel_name, value - .try_iter()? - .map(|i| i?.cast::()?.extract::()) + .map(|i| i?.extract::()) .collect::>>()? .into(), ); + } else { + return Err(PyValueError::new_err(format!( + "cannot parse value: {:?} into DefaultValue", + value + ))); } Ok(()) } @@ -834,7 +842,7 @@ impl PyView { fn with_default_interpolation_value( &self, channel: usize, - #[gen_stub(override_type(type_repr="float | DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))] + #[gen_stub(override_type(type_repr="float | transforms.DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))] value: Bound, ) -> PyResult { let mut new = self.clone(); @@ -999,17 +1007,22 @@ impl PyView { #[pymethods] impl PyTransforms { #[new] - #[pyo3(signature = (channel = HashMap::new(), drift = Vec::new(), interpolation_order = None))] + #[pyo3(signature = (channel = HashMap::new(), drift = Vec::new(), interpolation_order = None, default_value = None))] fn new( channel: HashMap, drift: Vec, interpolation_order: Option, + default_value: Option>, ) -> 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 { - default_value: Default::default(), + default_value: default_value + .into_iter() + .flatten() + .map(|(k, v)| (k, v.inner)) + .collect::>(), interpolation_order, channel, drift, @@ -1017,6 +1030,35 @@ impl PyTransforms { }) } + #[allow(clippy::type_complexity)] + fn __getnewargs__( + &self, + ) -> ( + HashMap, + Vec, + Option, + HashMap, + ) { + ( + self.inner + .channel + .iter() + .map(|(k, v)| (k.to_string(), PyTransform { inner: v.clone() })) + .collect(), + self.inner + .drift + .iter() + .map(|i| PyTransform { inner: i.clone() }) + .collect(), + self.inner.interpolation_order, + self.inner + .default_value + .iter() + .map(|(k, v)| (k.to_string(), PyDefaultValue { inner: v.clone() })) + .collect(), + ) + } + /// the bspline order used for interpolation when using transforms, /// None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes #[getter] @@ -1039,6 +1081,58 @@ impl PyTransforms { new } + /// the value of the interpolated image outside the data + fn get_default_interpolation_value(&self, channel_name: &str) -> PyResult { + Ok(PyDefaultValue { + inner: self + .inner + .default_value + .get(channel_name) + .cloned() + .unwrap_or_default(), + }) + } + + /// the value of the interpolated image outside the data + fn set_default_interpolation_value( + &mut self, + channel_name: String, + #[gen_stub(override_type(type_repr="float | DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))] + value: Bound, + ) -> PyResult<()> { + if let Ok(value) = value.extract::() { + self.inner.default_value.insert(channel_name, value.inner); + } else if let Ok(value) = value.extract::() { + self.inner.default_value.insert(channel_name, value.into()); + } else if let Ok(value) = value.try_iter() { + self.inner.default_value.insert( + channel_name, + value + .map(|i| i?.extract::()) + .collect::>>()? + .into(), + ); + } else { + return Err(PyValueError::new_err(format!( + "cannot parse value: {:?} into DefaultValue", + value + ))); + } + Ok(()) + } + + /// the value of the interpolated image outside the data + fn with_default_interpolation_value( + &self, + channel_name: String, + #[gen_stub(override_type(type_repr="float | DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))] + value: Bound, + ) -> PyResult { + let mut new = self.clone(); + new.set_default_interpolation_value(channel_name, value)?; + Ok(new) + } + #[getter] fn get_channel_transforms(&self) -> HashMap { self.inner @@ -1069,10 +1163,6 @@ impl PyTransforms { Ok(()) } - fn __getnewargs__(&self) -> (HashMap, Vec) { - (self.get_channel_transforms(), self.get_drift_transforms()) - } - #[staticmethod] pub(crate) fn load(path: PathBuf) -> PyResult { Ok(PyTransforms { diff --git a/ndbioimage/src/readers.rs b/ndbioimage/src/readers.rs index 36bd46f..0813ef3 100644 --- a/ndbioimage/src/readers.rs +++ b/ndbioimage/src/readers.rs @@ -128,7 +128,7 @@ pub enum PixelType { I128, /// unsigned 128-bit integer U128, - /// 128-bit float (emulated) + /// 128-bit float (not supported) F128, } @@ -170,12 +170,10 @@ pub enum ArrayT { I64(Array), /// unsigned 64-bit integer array U64(Array), - /// signed 128-bit integer array + /// signed 128-bit integer array (not supported by numpy) I128(Array), - /// unsigned 128-bit integer array + /// unsigned 128-bit integer array (not supported by numpy) U128(Array), - /// 128-bit float array (emulated as f64) - F128(Array), } /// type alias for a single frame (2D image) @@ -365,7 +363,7 @@ macro_rules! try_into_array_body { T::zero() }) }), - ArrayT::F64(v) | ArrayT::F128(v) => v.$map(|x| { + ArrayT::F64(v) => v.$map(|x| { T::from_f64(x).unwrap_or_else(|| { err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() diff --git a/ndbioimage/src/readers/bioformats_java.rs b/ndbioimage/src/readers/bioformats_java.rs index 719fddf..3c5a899 100644 --- a/ndbioimage/src/readers/bioformats_java.rs +++ b/ndbioimage/src/readers/bioformats_java.rs @@ -521,7 +521,7 @@ impl BioFormatsJavaReader { PixelType::U64 => 8, PixelType::I128 => 16, PixelType::U128 => 16, - PixelType::F128 => 8, + PixelType::F128 => 16, }; Ok(bytes .chunks(chunk_size) @@ -587,7 +587,7 @@ impl BioFormatsJavaReader { (PixelType::U64, true) => get_frame!(u64, <8), (PixelType::I128, true) => get_frame!(i128, <16), (PixelType::U128, true) => get_frame!(u128, <16), - (PixelType::F128, true) => get_frame!(f64, <8), + (PixelType::F128, true) => Err(Error::F128NotSupported), (PixelType::Bool, false) => get_frame!(bool, >1), (PixelType::I8, false) => get_frame!(i8, >1), (PixelType::U8, false) => get_frame!(u8, >1), @@ -601,7 +601,7 @@ impl BioFormatsJavaReader { (PixelType::U64, false) => get_frame!(u64, >8), (PixelType::I128, false) => get_frame!(i128, >16), (PixelType::U128, false) => get_frame!(u128, >16), - (PixelType::F128, false) => get_frame!(f64, >8), + (PixelType::F128, false) => Err(Error::F128NotSupported), } } } diff --git a/ndbioimage/src/readers/bioformats_rust.rs b/ndbioimage/src/readers/bioformats_rust.rs index 9deca4f..f8c1bd5 100644 --- a/ndbioimage/src/readers/bioformats_rust.rs +++ b/ndbioimage/src/readers/bioformats_rust.rs @@ -140,7 +140,7 @@ impl BioFormatsRustReader { PixelType::U64 => 8, PixelType::I128 => 16, PixelType::U128 => 16, - PixelType::F128 => 8, + PixelType::F128 => 16, }; Ok(bytes .chunks(chunk_size) @@ -206,7 +206,7 @@ impl BioFormatsRustReader { (PixelType::U64, true) => get_frame!(u64, <8), (PixelType::I128, true) => get_frame!(i128, <16), (PixelType::U128, true) => get_frame!(u128, <16), - (PixelType::F128, true) => get_frame!(f64, <8), + (PixelType::F128, true) => Err(Error::F128NotSupported), (PixelType::Bool, false) => get_frame!(bool, >1), (PixelType::I8, false) => get_frame!(i8, >1), (PixelType::U8, false) => get_frame!(u8, >1), @@ -220,7 +220,7 @@ impl BioFormatsRustReader { (PixelType::U64, false) => get_frame!(u64, >8), (PixelType::I128, false) => get_frame!(i128, >16), (PixelType::U128, false) => get_frame!(u128, >16), - (PixelType::F128, false) => get_frame!(f64, >8), + (PixelType::F128, false) => Err(Error::F128NotSupported), } } } diff --git a/ndbioimage/src/readers/czi.rs b/ndbioimage/src/readers/czi.rs index c71aeb6..ef73529 100644 --- a/ndbioimage/src/readers/czi.rs +++ b/ndbioimage/src/readers/czi.rs @@ -1132,7 +1132,7 @@ impl Reader for CziReader { PixelType::U64 => get_frame!(u64, 8), PixelType::I128 => get_frame!(i128, 16), PixelType::U128 => get_frame!(u128, 16), - PixelType::F128 => get_frame!(f64, 8), + PixelType::F128 => Err(Error::F128NotSupported), } } diff --git a/ndbioimage/src/tiffwrite.rs b/ndbioimage/src/tiffwrite.rs index 395b162..baaca02 100644 --- a/ndbioimage/src/tiffwrite.rs +++ b/ndbioimage/src/tiffwrite.rs @@ -178,7 +178,7 @@ where PixelType::U64 => self.save_as_tiff_with_type::(path, options)?, PixelType::I128 => self.save_as_tiff_with_type::(path, options)?, PixelType::U128 => self.save_as_tiff_with_type::(path, options)?, - PixelType::F128 => self.save_as_tiff_with_type::(path, options)?, + PixelType::F128 => Err(Error::F128NotSupported)?, } Ok(()) diff --git a/ndbioimage/src/transforms.rs b/ndbioimage/src/transforms.rs index da50364..39a13b9 100644 --- a/ndbioimage/src/transforms.rs +++ b/ndbioimage/src/transforms.rs @@ -4,7 +4,7 @@ use crate::readers::{DynReader, Frame, PixelType, Reader}; use crate::view::View; use image_registration::{bspline::DefaultValue, filter, transform::Transform}; use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, array, concatenate, s}; -use rayon::iter::{ParallelIterator, IntoParallelIterator}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; @@ -64,8 +64,10 @@ pub struct Transforms { pub(crate) interpolation_order: Option, #[serde(default)] pub(crate) default_value: DefaultValues, + /// a transform for each channel by channel name #[serde(default)] pub channel: HashMap>, + /// a transform for each time point #[serde(default)] pub drift: Vec>, } @@ -104,6 +106,7 @@ impl Serialize for Transforms { #[derive(Serialize)] struct Binary<'a> { interpolation_order: Option, + default_value: &'a DefaultValues, channel: &'a HashMap>, drift: &'a Vec>, } @@ -117,6 +120,7 @@ impl Serialize for Transforms { } else { Binary { interpolation_order: self.interpolation_order, + default_value: &self.default_value, channel: &self.channel, drift: &self.drift, } @@ -125,15 +129,21 @@ impl Serialize for Transforms { } } +/// the kind of corrections the transforms are meant to correct for #[derive(Clone, Debug, PartialEq, Copy)] pub enum TransformKind { + /// no tranforms None, + /// correct warp between channels, ex: camera/filter misalignment, chromatic aberrations Channel, + /// drift in the miscrocope between time points Drift, + /// both warp between channels and drift Both, } impl Transforms { + /// the kind of corrections the transforms are meant to correct for pub fn kind(&self) -> TransformKind { match (self.channel.is_empty(), self.drift.is_empty()) { (true, true) => TransformKind::None, @@ -143,6 +153,7 @@ impl Transforms { } } + /// load the transforms from a yml file pub fn load>(path: P) -> Result { let mut file = std::fs::File::open(&path)?; Ok( @@ -157,6 +168,7 @@ impl Transforms { ) } + /// save the transforms to a yml file pub fn save>(&self, path: P) -> Result<(), Error> { let file = std::fs::OpenOptions::new() .write(true) @@ -166,6 +178,7 @@ impl Transforms { Ok(yaml_serde::to_writer(file, self)?) } + /// helper function to eiter load transforms from file or calculate and save them pub fn calculate_transforms_from( transform_file: Option

, bead_files: Option<&[Q]>, @@ -246,18 +259,21 @@ impl Transforms { Ok(()) } + /// calculate drift transforms in yx using image file pub fn calculate_drift_transforms_2d>( file: P, ) -> Result>, Error> { View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_2d() } + /// calculate drift transforms in zyx using image file pub fn calculate_drift_transforms_3d>( file: P, ) -> Result>, Error> { View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_3d() } + /// calculate channel transforms in yx using image file pub fn calculate_channel_transforms_2d>( bead_files: &[P], main_channel: usize, @@ -270,44 +286,54 @@ impl Transforms { let view = View::<_, DynReader>::from_path(file)?; transforms.push(view.calculate_channel_transforms_2d(main_channel)?) } - let channels = transforms + let transforms = transforms.into_iter().flat_map(|map| map.into_iter()).fold( + HashMap::new(), + |mut acc, (channel, transform)| { + acc.entry(channel) + .or_insert_with(Vec::new) + .push(transform * &default_transform); + acc + }, + ); + Ok(transforms .iter() - .flat_map(|t| t.keys()) - .collect::>(); - let mut average_transforms = HashMap::new(); - for channel in channels { - let (s, n) = transforms - .iter() - .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() - .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()), - ); - } - Ok(average_transforms) + .map(|(channel, ts)| { + let n = ts.len(); + let parameters = ts + .iter() + .fold(vec![0.0; 6], |mut acc, t| { + for (a, p) in acc.iter_mut().zip(&t.parameters) { + *a += p; + } + acc + }) + .iter() + .map(|i| i / n as f64) + .collect::>(); + let dparameters = if n == 1 { + ts[0].dparameters.clone() + } else { + ts.iter() + .fold(vec![0.0; 6], |mut acc, t| { + for ((a, p), dp) in acc.iter_mut().zip(¶meters).zip(&t.dparameters) + { + *a += (dp - p).powi(2); + } + acc + }) + .iter() + .map(|i| (i / n as f64).sqrt()) + .collect::>() + }; + ( + channel.to_string(), + Transform::new(parameters).with_dparameters(dparameters), + ) + }) + .collect()) } + /// calculate channel transforms in zyx using image file pub fn calculate_channel_transforms_3d>( bead_files: &[P], main_channel: usize, @@ -357,6 +383,7 @@ impl Transforms { Ok(average_transforms) } + /// get the combination of channel and drift transforms for a certain channel and time point pub fn get_transform( &self, channel: &str, @@ -385,22 +412,26 @@ impl View { self.transforms = transforms; } + /// apply transforms to a new view pub fn with_transforms(&self, transforms: Transforms) -> Self { let mut new = self.clone(); new.transforms = transforms; new } + /// load the transforms from a yml file and apply to a new view pub fn with_transforms_from_yaml(mut self, path: &Path) -> Result { self.set_transforms(Transforms::load(path)?); Ok(self) } + /// load the transforms from a yml file and apply to the view pub fn load_transforms_from_yaml(&mut self, path: &Path) -> Result<(), Error> { self.set_transforms(Transforms::load(path)?); Ok(()) } + /// calculate channel transforms in yx using this image view pub fn calculate_channel_transforms_2d( &self, main_channel: usize, @@ -431,6 +462,7 @@ impl View { Ok(transforms) } + /// calculate channel transforms in zyx using this image view pub fn calculate_channel_transforms_3d( &self, main_channel: usize, @@ -459,6 +491,7 @@ impl View { Ok(transforms) } + /// calculate drift transforms in yx using this image view pub fn calculate_drift_transforms_2d(&self) -> Result>, Error> { let mut view = self.clone().into_dyn(); let axes = view.axes(); @@ -541,10 +574,12 @@ impl View { .collect()) } + /// calculate drift transforms in zyx using this image view pub fn calculate_drift_transforms_3d(&self) -> Result>, Error> { todo!() } + /// retrieve a frame and warp it according to the transform pub(crate) fn get_transformed_frame( &self, c: usize, @@ -598,7 +633,6 @@ impl View { Frame::U128(frame) => interpolate!(frame, U128, u128), Frame::F32(frame) => interpolate!(frame, F32, f32), Frame::F64(frame) => interpolate!(frame, F64, f64), - Frame::F128(frame) => interpolate!(frame, F128, f64), } } 3 => { @@ -701,7 +735,6 @@ impl View { Frame::U64(_) => interpolate!(U64, u64), Frame::I128(_) => interpolate!(I128, i128), Frame::U128(_) => interpolate!(U128, u128), - Frame::F128(_) => interpolate!(F128, f64), } } None => { @@ -720,7 +753,7 @@ impl View { PixelType::U64 => Frame::U64(Array2::zeros((shape.y, shape.x))), PixelType::I128 => Frame::I128(Array2::zeros((shape.y, shape.x))), PixelType::U128 => Frame::U128(Array2::zeros((shape.y, shape.x))), - PixelType::F128 => Frame::F128(Array2::zeros((shape.y, shape.x))), + PixelType::F128 => Err(Error::F128NotSupported)?, })) } } diff --git a/ndbioimage/src/view.rs b/ndbioimage/src/view.rs index 3fa5cdd..d3e768a 100644 --- a/ndbioimage/src/view.rs +++ b/ndbioimage/src/view.rs @@ -2049,4 +2049,27 @@ mod tests { Ok(()) } + + #[cfg(feature = "transforms")] + #[test] + fn serde_transforms_data() -> Result<(), Error> { + use crate::transforms::Transforms; + use image_registration::{bspline::DefaultValue, transform::Transform}; + use std::collections::HashMap; + let t = Transforms { + interpolation_order: Some(3), + default_value: HashMap::from([( + "Cy3".to_string(), + DefaultValue::new(vec![1.0, 2.0, 3.0], vec![0.2, 0.5, 0.3]), + )]), + channel: HashMap::from([( + "Cy3".to_string(), + Transform::new(vec![1.0, 0.0, 0.0, 1.0, 0.1, 0.2]), + )]), + drift: vec![Transform::new(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.5])], + }; + let t2: Transforms = from_bytes(&to_stdvec(&t)?)?; + assert_eq!(t, t2); + Ok(()) + } }