- return Error, raise Exception when trying to use f128 data

- clean parsing of python types into rust types
- finish PyView.reshape
This commit is contained in:
w.pomp committed 2026-10-27 18:23:37 +01:00
1 parent 8a4c965477
commit 3c7af5da4d
16 files changed
+393 -207

No files matched your search

@@ -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
@@ -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: ...
+2
View File
@@ -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",
+2
View File
@@ -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 {
+2 -2
View File
@@ -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;
+108 -109
View File
@@ -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<IxDyn>, dtype: PixelType) -> PyResult<Bound<PyAny>> {
macro_rules! item {
($t:tt) => {
view.into_dimensionality::<Ix0>()?
Ok(view
.into_dimensionality::<Ix0>()?
.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<Self> {
if path.is_instance_of::<Self>() {
Ok(path.cast_into::<Self>()?.extract::<Self>()?)
} else if path.is_instance_of::<PyBytes>() {
let mut pyview: Self = from_bytes(&path.extract::<Vec<u8>>()?).map_err(Error::from)?;
if let Ok(view) = path.extract::<Self>() {
Ok(view)
} else if let Ok(bytes) = path.extract::<Vec<u8>>() {
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::<PyString>()?
.extract::<String>()?,
);
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<isize>,
shape: Bound<'py, PyAny>,
order: &str,
copy: Option<bool>,
) -> PyResult<Bound<'py, PyAny>> {
// 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::<usize>();
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::<isize>()
})
.collect::<PyResult<Vec<_>>>()
} else {
Ok(vec![i.extract::<isize>()?])
}
})
.collect::<PyResult<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>()
} else {
vec![shape.extract::<isize>()?]
};
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::<PyString>() {
Ok(axis
.cast_into::<PyString>()?
.extract::<String>()?
.parse::<Axis>()
.map_err(Error::from)?)
} else if axis.is_instance_of::<PyInt>() {
let mut index = axis.cast_into::<PyInt>()?.extract::<isize>()?;
if let Ok(axis) = axis.extract::<String>() {
Ok(axis.parse::<Axis>().map_err(Error::from)?)
} else if let Ok(mut index) = axis.extract::<isize>() {
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::<Ix0>()?
Ok(view
.into_dimensionality::<Ix0>()?
.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<Bound<'py, PyAny>> {
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<Bound<'py, PyAny>> {
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<Vec<u8>> {
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<PyAny>,
) -> PyResult<usize> {
if axis.is_instance_of::<PyString>() {
let axis = axis
.cast_into::<PyString>()?
.extract::<String>()?
.parse::<Axis>()
.map_err(Error::from)?;
if let Ok(axis) = axis.extract::<String>() {
let axis = axis.parse::<Axis>().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::<PyInt>() {
Ok(axis.cast_into::<PyInt>()?.extract::<usize>()?)
} else if let Ok(axis) = axis.extract::<usize>() {
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<Bound<'py, PyAny>> {
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<HashSet<usize>> {
let path = if path.is_instance_of::<Self>() {
let py_view: Self = path.cast_into::<Self>()?.extract::<Self>()?;
py_view.view.path().to_owned()
let path = if let Ok(view) = path.extract::<Self>() {
view.view.path().to_owned()
} else {
let builtins = PyModule::import(py, "builtins")?;
PathBuf::from(
builtins
.getattr("str")?
.call1((path,))?
.cast_into::<PyString>()?
.extract::<String>()?,
)
};
@@ -1903,16 +1911,14 @@ impl PyView {
series: usize,
reader: Option<&str>,
) -> PyResult<HashSet<usize>> {
let path = if path.is_instance_of::<Self>() {
let py_view: Self = path.cast_into::<Self>()?.extract::<Self>()?;
py_view.view.path().to_owned()
let path = if let Ok(view) = path.extract::<Self>() {
view.view.path().to_owned()
} else {
let builtins = PyModule::import(py, "builtins")?;
PathBuf::from(
builtins
.getattr("str")?
.call1((path,))?
.cast_into::<PyString>()?
.extract::<String>()?,
)
};
@@ -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<SliceInfoElem>, usize)> {
if s.is_none() {
Ok((vec![SliceInfoElem::NewAxis], 0))
} else if s.is_instance_of::<PyInt>() {
let mut index = s.cast::<PyInt>()?.extract::<isize>()?;
} else if let Ok(mut index) = s.extract::<isize>() {
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::<PySlice>() {
let u = s.cast::<PySlice>()?.indices(t as isize)?;
} else if let Ok(slice) = s.cast::<PySlice>() {
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<usize>, bool) =
if idx.is_instance_of::<PyNone>() || idx.is_instance_of::<PyEllipsis>() {
((0..self.inner.order.len()).collect(), true)
} else if idx.is_instance_of::<PySlice>() {
let indices = idx
.cast::<PySlice>()?
.indices(self.inner.order.len() as isize)?;
} else if let Ok(slice) = idx.cast::<PySlice>() {
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::<PyString>() {
let s = idx.cast::<PyString>()?.extract::<String>()?;
} else if let Ok(s) = idx.extract::<String>() {
(
s.to_uppercase()
.chars()
@@ -2368,9 +2368,8 @@ impl PyShape {
.collect::<PyResult<_>>()?,
true,
)
} else if idx.is_instance_of::<PyInt>() {
} else if let Ok(mut i) = idx.extract::<isize>() {
let len = self.inner.order.len() as isize;
let mut i = idx.cast::<PyInt>()?.extract::<isize>()?;
if i < 0 {
i += len
};
+115 -25
View File
@@ -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<f64>, weights: Vec<f64>) -> Self {
#[pyo3(signature = (values, weights = None))]
fn new(values: Vec<f64>, weights: Option<Vec<f64>>) -> 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<f64> {
self.inner.weights().to_vec()
}
fn __getnewargs__(&self) -> (Vec<f64>, Vec<f64>) {
(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<PyAny>,
) -> PyResult<PyTransform> {
if other.is_instance_of::<PyTransform>() {
if let Ok(other) = other.extract::<PyTransform>() {
Ok(PyTransform {
inner: &self.inner * &other.extract::<PyTransform>()?.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<PyAny>,
) -> PyResult<PyTransform> {
if other.is_instance_of::<PyTransform>() {
if let Ok(other) = other.extract::<PyTransform>() {
Ok(PyTransform {
inner: &other.extract::<PyTransform>()?.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<PyAny>,
) -> PyResult<()> {
let channel_name = self.view.channel_name(channel)?.to_string();
if value.is_instance_of::<PyTransform>() {
if let Ok(value) = value.extract::<PyDefaultValue>() {
self.view
.transforms
.default_value
.insert(channel_name, value.extract::<PyDefaultValue>()?.inner);
} else if value.is_instance_of::<PyFloat>() {
self.view.transforms.default_value.insert(
channel_name,
value.cast::<PyFloat>()?.extract::<f64>()?.into(),
);
} else {
.insert(channel_name, value.inner);
} else if let Ok(value) = value.extract::<f64>() {
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::<PyFloat>()?.extract::<f64>())
.map(|i| i?.extract::<f64>())
.collect::<PyResult<Vec<_>>>()?
.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<PyAny>,
) -> PyResult<Self> {
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<String, PyTransform>,
drift: Vec<PyTransform>,
interpolation_order: Option<usize>,
default_value: Option<HashMap<String, PyDefaultValue>>,
) -> PyResult<PyTransforms> {
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::<HashMap<_, _>>(),
interpolation_order,
channel,
drift,
@@ -1017,6 +1030,35 @@ impl PyTransforms {
})
}
#[allow(clippy::type_complexity)]
fn __getnewargs__(
&self,
) -> (
HashMap<String, PyTransform>,
Vec<PyTransform>,
Option<usize>,
HashMap<String, PyDefaultValue>,
) {
(
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<PyDefaultValue> {
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<PyAny>,
) -> PyResult<()> {
if let Ok(value) = value.extract::<PyDefaultValue>() {
self.inner.default_value.insert(channel_name, value.inner);
} else if let Ok(value) = value.extract::<f64>() {
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::<f64>())
.collect::<PyResult<Vec<_>>>()?
.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<PyAny>,
) -> PyResult<Self> {
let mut new = self.clone();
new.set_default_interpolation_value(channel_name, value)?;
Ok(new)
}
#[getter]
fn get_channel_transforms(&self) -> HashMap<String, PyTransform> {
self.inner
@@ -1069,10 +1163,6 @@ impl PyTransforms {
Ok(())
}
fn __getnewargs__(&self) -> (HashMap<String, PyTransform>, Vec<PyTransform>) {
(self.get_channel_transforms(), self.get_drift_transforms())
}
#[staticmethod]
pub(crate) fn load(path: PathBuf) -> PyResult<PyTransforms> {
Ok(PyTransforms {
+4 -6
View File
@@ -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<D: Dimension> {
I64(Array<i64, D>),
/// unsigned 64-bit integer array
U64(Array<u64, D>),
/// signed 128-bit integer array
/// signed 128-bit integer array (not supported by numpy)
I128(Array<i128, D>),
/// unsigned 128-bit integer array
/// unsigned 128-bit integer array (not supported by numpy)
U128(Array<u128, D>),
/// 128-bit float array (emulated as f64)
F128(Array<f64, D>),
}
/// 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::<T>().to_string()));
T::zero()
+3 -3
View File
@@ -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),
}
}
}
+3 -3
View File
@@ -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),
}
}
}
+1 -1
View File
@@ -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),
}
}
+1 -1
View File
@@ -178,7 +178,7 @@ where
PixelType::U64 => self.save_as_tiff_with_type::<u64, P>(path, options)?,
PixelType::I128 => self.save_as_tiff_with_type::<i64, P>(path, options)?,
PixelType::U128 => self.save_as_tiff_with_type::<u64, P>(path, options)?,
PixelType::F128 => self.save_as_tiff_with_type::<f64, P>(path, options)?,
PixelType::F128 => Err(Error::F128NotSupported)?,
}
Ok(())
+71 -38
View File
@@ -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<usize>,
#[serde(default)]
pub(crate) default_value: DefaultValues,
/// a transform for each channel by channel name
#[serde(default)]
pub channel: HashMap<String, Transform<IxDyn>>,
/// a transform for each time point
#[serde(default)]
pub drift: Vec<Transform<IxDyn>>,
}
@@ -104,6 +106,7 @@ impl Serialize for Transforms {
#[derive(Serialize)]
struct Binary<'a> {
interpolation_order: Option<usize>,
default_value: &'a DefaultValues,
channel: &'a HashMap<String, Transform<IxDyn>>,
drift: &'a Vec<Transform<IxDyn>>,
}
@@ -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<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
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<P: AsRef<Path>>(&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<P, Q, R>(
transform_file: Option<P>,
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<P: AsRef<Path>>(
file: P,
) -> Result<Vec<Transform<Ix2>>, Error> {
View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_2d()
}
/// calculate drift transforms in zyx using image file
pub fn calculate_drift_transforms_3d<P: AsRef<Path>>(
file: P,
) -> Result<Vec<Transform<Ix3>>, Error> {
View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_3d()
}
/// calculate channel transforms in yx using image file
pub fn calculate_channel_transforms_2d<P: AsRef<Path>>(
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
.iter()
.flat_map(|t| t.keys())
.collect::<HashSet<_>>();
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()),
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()
.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;
}
Ok(average_transforms)
acc
})
.iter()
.map(|i| i / n as f64)
.collect::<Vec<_>>();
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(&parameters).zip(&t.dparameters)
{
*a += (dp - p).powi(2);
}
acc
})
.iter()
.map(|i| (i / n as f64).sqrt())
.collect::<Vec<_>>()
};
(
channel.to_string(),
Transform::new(parameters).with_dparameters(dparameters),
)
})
.collect())
}
/// calculate channel transforms in zyx using image file
pub fn calculate_channel_transforms_3d<P: AsRef<Path>>(
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<D: Dimension, R: Reader> View<D, R> {
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, Error> {
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<D: Dimension, R: Reader> View<D, R> {
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<D: Dimension, R: Reader> View<D, R> {
Ok(transforms)
}
/// calculate drift transforms in yx using this image view
pub fn calculate_drift_transforms_2d(&self) -> Result<Vec<Transform<Ix2>>, Error> {
let mut view = self.clone().into_dyn();
let axes = view.axes();
@@ -541,10 +574,12 @@ impl<D: Dimension, R: Reader> View<D, R> {
.collect())
}
/// calculate drift transforms in zyx using this image view
pub fn calculate_drift_transforms_3d(&self) -> Result<Vec<Transform<Ix3>>, 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<D: Dimension, R: Reader> View<D, R> {
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<D: Dimension, R: Reader> View<D, R> {
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<D: Dimension, R: Reader> View<D, R> {
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)?,
}))
}
}
+23
View File
@@ -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(())
}
}