From 8a4c965477742f6b4ce8360330d971a3416aca8f Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Sun, 25 Oct 2026 10:23:57 +0100 Subject: [PATCH] - default value argument for interpolation --- image-registration | 2 +- .../py/ndbioimage/ndbioimage_rs/__init__.pyi | 26 ++- .../ndbioimage/ndbioimage_rs/transforms.pyi | 28 ++- ndbioimage/src/cache.rs | 50 ++++- ndbioimage/src/py.rs | 3 + ndbioimage/src/py/imread.rs | 7 +- ndbioimage/src/py/transforms.rs | 190 ++++++++++++++---- ndbioimage/src/transforms.rs | 57 ++++-- ndbioimage/src/view.rs | 75 +++++-- 9 files changed, 343 insertions(+), 95 deletions(-) diff --git a/image-registration b/image-registration index b12982d..9257dd1 160000 --- a/image-registration +++ b/image-registration @@ -1 +1 @@ -Subproject commit b12982da48e422087030a90b2850992b2312bcaa +Subproject commit 9257dd1fcf4a553fd51e0f3f56d26960ab72f383 diff --git a/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi b/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi index f19db1f..dc4cbf5 100644 --- a/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi +++ b/ndbioimage/py/ndbioimage/ndbioimage_rs/__init__.pyi @@ -2,6 +2,8 @@ # ruff: noqa: E501, F401, F403, F405 import builtins +import collections +import collections.abc import os import pathlib import typing @@ -235,6 +237,18 @@ 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 @@ -306,12 +320,12 @@ class Imread: *shape: typing.Any, order: builtins.str = "C", copy: typing.Optional[builtins.bool] = None, - ) -> typing.Any: + ) -> Imread | numpy.NDArray: r""" reshape the view with a new axis order """ def squeeze( - self, axes: str | typing.Sequence[int | str] = None + self, axes: str | collections.abc.Sequence[int | str] = None ) -> Imread | float | int: ... def close(self) -> None: r""" @@ -635,7 +649,7 @@ class Imread: swap two axes """ def transpose( - self, axes: typing.Optional[typing.Sequence[int | str] | str] = None + self, axes: typing.Optional[collections.abc.Sequence[int | str] | str] = None ) -> Imread: r""" permute the order of the axes @@ -781,6 +795,12 @@ class Imread: the bspline order used for interpolation when using transforms None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes """ + def with_default_interpolation_value( + self, value: float | DefaultValue | collections.abc.Sequence[float] + ) -> Imread: + r""" + the value of the interpolated image outside the data + """ def with_transform( self, channels: builtins.bool = True, diff --git a/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi b/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi index b80e77f..dc94282 100644 --- a/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi +++ b/ndbioimage/py/ndbioimage/ndbioimage_rs/transforms.pyi @@ -10,6 +10,7 @@ import numpy import numpy.typing __all__ = [ + "DefaultValue", "Optimizer", "RegistrationStep", "SamplingArg", @@ -18,6 +19,18 @@ __all__ = [ "Transforms", ] +class DefaultValue: + @property + def values(self) -> builtins.list[builtins.float]: ... + @property + def weights(self) -> builtins.list[builtins.float]: ... + def __eq__(self, other: builtins.object, /) -> builtins.bool: ... + def __new__( + cls, + values: typing.Sequence[builtins.float], + weights: typing.Sequence[builtins.float], + ) -> DefaultValue: ... + class Optimizer: def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__(cls, optimizer: builtins.str) -> Optimizer: ... @@ -160,13 +173,22 @@ class Transform: self, theta: builtins.float, center: typing.Sequence[builtins.float] ) -> Transform: ... def interpolate( - self, order: builtins.int, image: numpy.typing.ArrayLike + self, + order: builtins.int, + image: numpy.typing.ArrayLike, + default_value: typing.Optional[DefaultValue] = None, ) -> numpy.typing.NDArray[numpy.float64]: ... def interpolate_par( - self, order: builtins.int, image: numpy.typing.ArrayLike + self, + order: builtins.int, + image: numpy.typing.ArrayLike, + default_value: typing.Optional[DefaultValue] = None, ) -> numpy.typing.NDArray[numpy.float64]: ... def frame( - self, image: numpy.typing.ArrayLike, default: typing.Optional[typing.Any] = None + self, + image: numpy.typing.ArrayLike, + default: typing.Optional[typing.Any] = None, + default_value: typing.Optional[DefaultValue] = None, ) -> numpy.ndarray: r""" backwards compatibility, default parameter is not implemented diff --git a/ndbioimage/src/cache.rs b/ndbioimage/src/cache.rs index 49620aa..ef20638 100644 --- a/ndbioimage/src/cache.rs +++ b/ndbioimage/src/cache.rs @@ -1,7 +1,7 @@ use crate::axes::{Axis, Operation}; use crate::readers::Frame; #[cfg(feature = "transforms")] -use crate::transforms::Transforms; +use crate::transforms::{DefaultValues, Transforms}; #[cfg(feature = "transforms")] use image_registration::transform::Transform; use indexmap::{Equivalent, IndexMap}; @@ -140,14 +140,32 @@ impl ReaderKeyRef<'_> { } } -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq)] pub(crate) struct FrameKey { - pub(crate) reader_key: ReaderKey, + pub(crate) reader: ReaderKey, pub(crate) c: usize, pub(crate) z: usize, pub(crate) t: usize, #[cfg(feature = "transforms")] - pub(crate) transform: Option<(Transform, usize)>, + pub(crate) transform: Option<(Transform, usize, DefaultValues)>, +} + +impl Hash for FrameKey { + fn hash(&self, state: &mut H) { + self.reader.hash(state); + self.c.hash(state); + self.z.hash(state); + self.t.hash(state); + #[cfg(feature = "transforms")] + if let Some(transform) = self.transform.as_ref() { + transform.0.hash(state); + transform.1.hash(state); + transform.2.iter().for_each(|(k, v)| { + k.hash(state); + v.hash(state); + }) + } + } } /// borrowed view of [`FrameKey`] for cache lookups without allocation. @@ -158,7 +176,7 @@ pub(crate) struct FrameKeyRef<'a> { pub(crate) z: usize, pub(crate) t: usize, #[cfg(feature = "transforms")] - pub(crate) transform: Option<(Cow<'a, Transform>, usize)>, + pub(crate) transform: Option<(Cow<'a, Transform>, usize, Cow<'a, DefaultValues>)>, } impl Hash for FrameKeyRef<'_> { @@ -168,14 +186,21 @@ impl Hash for FrameKeyRef<'_> { self.z.hash(state); self.t.hash(state); #[cfg(feature = "transforms")] - self.transform.hash(state); + if let Some(transform) = self.transform.as_ref() { + transform.0.hash(state); + transform.1.hash(state); + transform.2.iter().for_each(|(k, v)| { + k.hash(state); + v.hash(state); + }) + } } } impl Equivalent for FrameKeyRef<'_> { fn equivalent(&self, key: &FrameKey) -> bool { let FrameKey { - reader_key, + reader: reader_key, c, z, t, @@ -188,8 +213,11 @@ impl Equivalent for FrameKeyRef<'_> { && self.c == *c && self.z == *z && self.t == *t - && self.transform.as_ref().map(|(t, i)| (t.as_ref(), i)) - == transform.as_ref().map(|(t, i)| (t, i)) + && self + .transform + .as_ref() + .map(|(t, i, d)| (t.as_ref(), i, d.as_ref())) + == transform.as_ref().map(|(t, i, d)| (t, i, d)) } #[cfg(not(feature = "transforms"))] { @@ -201,7 +229,7 @@ impl Equivalent for FrameKeyRef<'_> { impl FrameKeyRef<'_> { pub(crate) fn to_owned(&self) -> FrameKey { FrameKey { - reader_key: self.reader.to_owned(), + reader: self.reader.to_owned(), c: self.c, z: self.z, t: self.t, @@ -209,7 +237,7 @@ impl FrameKeyRef<'_> { transform: self .transform .as_ref() - .map(|(t, i)| (t.clone().into_owned(), *i)), + .map(|(t, i, d)| (t.clone().into_owned(), *i, d.clone().into_owned())), } } } diff --git a/ndbioimage/src/py.rs b/ndbioimage/src/py.rs index 481d1e5..5d812ca 100644 --- a/ndbioimage/src/py.rs +++ b/ndbioimage/src/py.rs @@ -101,6 +101,9 @@ mod ndbioimage_rs { #[pymodule_export] use super::super::transforms::PyTransforms; + #[pymodule_export] + use super::super::transforms::PyDefaultValue; + #[pymodule_export] use super::super::transforms::PyRegistrationStep; diff --git a/ndbioimage/src/py/imread.rs b/ndbioimage/src/py/imread.rs index b390817..79e7525 100644 --- a/ndbioimage/src/py/imread.rs +++ b/ndbioimage/src/py/imread.rs @@ -256,6 +256,7 @@ impl PyView { } /// reshape the view with a new axis order + #[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, @@ -297,13 +298,13 @@ impl PyView { } } - #[gen_stub(override_return_type(type_repr="Imread | float | int", imports=("numpy", "numpy.typing")))] + #[gen_stub(override_return_type(type_repr = "Imread | float | int"))] #[pyo3(signature = (axes = None))] fn squeeze<'py>( &self, py: Python<'py>, #[gen_stub( - override_type(type_repr="str | typing.Sequence[int | str]", imports=("typing")) + override_type(type_repr="str | collections.abc.Sequence[int | str]", imports=("collections")) )] axes: Option>, ) -> PyResult> { @@ -1382,7 +1383,7 @@ impl PyView { #[pyo3(signature = (axes = None))] fn transpose( &self, - #[gen_stub(override_type(type_repr="typing.Optional[typing.Sequence[int | str] | str]", imports=("typing") + #[gen_stub(override_type(type_repr="typing.Optional[collections.abc.Sequence[int | str] | str]", imports=("typing", "collections.abc") ))] axes: Option>, ) -> PyResult { diff --git a/ndbioimage/src/py/transforms.rs b/ndbioimage/src/py/transforms.rs index 203542a..dca527d 100644 --- a/ndbioimage/src/py/transforms.rs +++ b/ndbioimage/src/py/transforms.rs @@ -1,6 +1,7 @@ use crate::error::Error; use crate::py::imread::PyView; use crate::transforms::Transforms; +use image_registration::bspline::DefaultValue; use image_registration::metric::{SamplingArg, Sigma}; use image_registration::register::{Optimizer, RegistrationStep}; use image_registration::transform::Transform; @@ -13,11 +14,46 @@ 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; use std::path::PathBuf; +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + eq, + name = "DefaultValue", + module = "ndbioimage.transforms" +)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct PyDefaultValue { + inner: DefaultValue, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyDefaultValue { + #[new] + fn new(values: Vec, weights: Vec) -> Self { + Self { + inner: DefaultValue::new(values, weights), + } + } + + #[getter] + fn values(&self) -> Vec { + self.inner.values().to_vec() + } + + #[getter] + fn weights(&self) -> Vec { + self.inner.weights().to_vec() + } +} + #[gen_stub_pyclass] #[pyclass( subclass, @@ -230,68 +266,86 @@ impl PyTransform { } } + #[pyo3(signature = (order, image, default_value = None))] fn interpolate<'py>( &self, py: Python<'py>, order: usize, #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] image: PyArrayLikeDyn, + default_value: Option, ) -> PyResult>> { let image = image.as_array(); + let default_value = default_value.map(|i| i.inner).unwrap_or_default(); Ok(py .detach(|| match order { - 0 => Ok(self - .inner - .interpolate::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 1 => Ok(self - .inner - .interpolate::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 2 => Ok(self - .inner - .interpolate::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 3 => Ok(self - .inner - .interpolate::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 4 => Ok(self - .inner - .interpolate::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 5 => Ok(self - .inner - .interpolate::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 0 => Ok(self.inner.interpolate::<0, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 1 => Ok(self.inner.interpolate::<1, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 2 => Ok(self.inner.interpolate::<2, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 3 => Ok(self.inner.interpolate::<3, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 4 => Ok(self.inner.interpolate::<4, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 5 => Ok(self.inner.interpolate::<5, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), _ => Err(PyValueError::new_err("order must be 0 <= order < 6")), })? .map_err(Error::from)? .into_pyarray(py)) } + #[pyo3(signature = (order, image, default_value = None))] fn interpolate_par<'py>( &self, py: Python<'py>, order: usize, #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] image: PyArrayLikeDyn, + default_value: Option, ) -> PyResult>> { let image = image.as_array(); + let default_value = default_value.map(|i| i.inner).unwrap_or_default(); Ok(py .detach(|| match order { - 0 => Ok(self - .inner - .interpolate_par::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 1 => Ok(self - .inner - .interpolate_par::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 2 => Ok(self - .inner - .interpolate_par::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 3 => Ok(self - .inner - .interpolate_par::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 4 => Ok(self - .inner - .interpolate_par::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)), - 5 => Ok(self - .inner - .interpolate_par::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)), + 0 => Ok(self.inner.interpolate_par::<0, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 1 => Ok(self.inner.interpolate_par::<1, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 2 => Ok(self.inner.interpolate_par::<2, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 3 => Ok(self.inner.interpolate_par::<3, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 4 => Ok(self.inner.interpolate_par::<4, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), + 5 => Ok(self.inner.interpolate_par::<5, _, _>( + image.into_dimensionality().map_err(Error::from)?, + &default_value, + )), _ => Err(PyValueError::new_err("order must be 0 <= order < 6")), })? .map_err(Error::from)? @@ -300,13 +354,14 @@ impl PyTransform { /// backwards compatibility, default parameter is not implemented #[gen_stub(override_return_type(type_repr="numpy.ndarray", imports=("numpy")))] - #[pyo3(signature = (image, default = None))] + #[pyo3(signature = (image, default = None, default_value = None))] fn frame<'py>( &self, py: Python<'py>, #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] image: Bound<'py, PyUntypedArray>, default: Option>, + default_value: Option, ) -> PyResult> { if default.is_some() { return Err(PyNotImplementedError::new_err( @@ -320,6 +375,7 @@ impl PyTransform { self.inner.ndim() ))); } + let default_value = default_value.map(|i| i.inner).unwrap_or_default(); macro_rules! interp { ($order:expr, $t:tt) => { @@ -330,6 +386,7 @@ impl PyTransform { .into_dimensionality() .map_err(Error::from)? .view(), + &default_value, ) .map_err(Error::from)? .mapv(|i| i as $t) @@ -347,6 +404,7 @@ impl PyTransform { .into_dimensionality() .map_err(Error::from)? .view(), + &default_value, ) .map_err(Error::from)? .mapv(|i| i > 0.5) @@ -727,6 +785,63 @@ impl PyView { new } + /// the value of the interpolated image outside the data + fn get_default_interpolation_value(&self, channel: usize) -> PyResult { + let channel_name = self.view.channel_name(channel)?.to_string(); + Ok(PyDefaultValue { + inner: self + .view + .transforms + .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: usize, + #[gen_stub(override_type(type_repr="float | 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::() { + 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 { + self.view.transforms.default_value.insert( + channel_name, + value + .try_iter()? + .map(|i| i?.cast::()?.extract::()) + .collect::>>()? + .into(), + ); + } + Ok(()) + } + + /// the value of the interpolated image outside the data + fn with_default_interpolation_value( + &self, + channel: usize, + #[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, value)?; + Ok(new) + } + /// return a new view with transformations applied (channel alignment, drift correction) /// backward compatibility #[cfg(feature = "transforms")] @@ -894,6 +1009,7 @@ impl PyTransforms { let drift = drift.into_iter().map(|t| t.inner).collect(); Ok(PyTransforms { inner: Transforms { + default_value: Default::default(), interpolation_order, channel, drift, diff --git a/ndbioimage/src/transforms.rs b/ndbioimage/src/transforms.rs index 95c1788..da50364 100644 --- a/ndbioimage/src/transforms.rs +++ b/ndbioimage/src/transforms.rs @@ -1,19 +1,18 @@ -use rayon::iter::ParallelIterator; use crate::axes::Axis; use crate::error::Error; use crate::readers::{DynReader, Frame, PixelType, Reader}; use crate::view::View; -use image_registration::filter; -pub use image_registration::transform::Transform; -use ndarray::{array, s, Array2, Dimension, Ix2, Ix3, IxDyn, concatenate}; +use image_registration::{bspline::DefaultValue, filter, transform::Transform}; +use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, array, concatenate, s}; +use rayon::iter::{ParallelIterator, IntoParallelIterator}; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; use std::hash::{Hash, Hasher}; use std::io::Seek; use std::path::Path; use std::sync::Arc; -use rayon::prelude::IntoParallelIterator; use tiffwrite::IJTiffFile; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -33,6 +32,7 @@ impl TryFrom> for Transforms { fn try_from(t: HashMap) -> Result { Ok(Self { + default_value: Default::default(), interpolation_order: None, channel: t .into_iter() @@ -56,11 +56,15 @@ impl TryFrom> for Transforms { } } +pub type DefaultValues = HashMap; + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] pub struct Transforms { #[serde(default)] pub(crate) interpolation_order: Option, #[serde(default)] + pub(crate) default_value: DefaultValues, + #[serde(default)] pub channel: HashMap>, #[serde(default)] pub drift: Vec>, @@ -174,7 +178,9 @@ impl Transforms { Q: AsRef, R: AsRef, { - if let Some(transform_file) = transform_file.as_ref() && let Ok(transforms) = Self::load(transform_file) { + if let Some(transform_file) = transform_file.as_ref() + && let Ok(transforms) = Self::load(transform_file.as_ref().with_extension("yml")) + { Ok(transforms) } else { let default_transform = default_transform.map(Transform::new); @@ -196,13 +202,15 @@ impl Transforms { .map(|t| t.into_dyn()) .collect(); }; - if let Some(transform_file) = transform_file { + if let Some(transform_file) = transform_file { let transform_file = transform_file.as_ref(); - transforms.save(transform_file)?; + transforms.save(transform_file.with_extension("yml"))?; if let Some(bead_files) = bead_files { - transforms.save_channel_transform_tiff(transform_file.with_extension("tif"), bead_files)?; + transforms.save_channel_transform_tiff( + transform_file.with_extension("tif"), + bead_files, + )?; } - } Ok(transforms) } @@ -238,11 +246,15 @@ impl Transforms { Ok(()) } - pub fn calculate_drift_transforms_2d>(file: P) -> Result>, Error> { + pub fn calculate_drift_transforms_2d>( + file: P, + ) -> Result>, Error> { View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_2d() } - pub fn calculate_drift_transforms_3d>(file: P) -> Result>, Error> { + pub fn calculate_drift_transforms_3d>( + file: P, + ) -> Result>, Error> { View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_3d() } @@ -540,6 +552,7 @@ impl View { t: usize, transform: &Transform, order: usize, + default_value: &DefaultValue, ) -> Result, Error> { match transform.ndim() { 2 => { @@ -551,18 +564,22 @@ impl View { tr.interpolate_par_order( &$array.mapv(|i| if i { 1.0 } else { 0.0 }), order, + default_value, )? .mapv(|i| i > 0.5), ))) }; ($array:expr ,$var:ident, f64) => { - Ok(Arc::from(Frame::$var( - tr.interpolate_par_order($array, order)?, - ))) + Ok(Arc::from(Frame::$var(tr.interpolate_par_order( + $array, + order, + default_value, + )?))) }; ($array:expr ,$var:ident, $t:ty) => { Ok(Arc::from(Frame::$var( - tr.interpolate_par_order($array, order)?.mapv(|i| i as $t), + tr.interpolate_par_order($array, order, default_value)? + .mapv(|i| i as $t), ))) }; } @@ -633,6 +650,7 @@ impl View { let transformed = tr.interpolate_par_order( &stack.mapv(|i| if i { 1.0 } else { 0.0 }), order, + default_value, )?; Ok(Arc::from(Frame::$var( transformed.slice(s![0, .., ..]).mapv(|i| i > 0.5), @@ -647,7 +665,8 @@ impl View { }) .collect::, Error>>()?; let stack = ndarray::stack(ndarray::Axis(0), &views)?; - let transformed = tr.interpolate_par_order(&stack, order)?; + let transformed = + tr.interpolate_par_order(&stack, order, default_value)?; Ok(Arc::from(Frame::$var( transformed.slice(s![0, .., ..]).to_owned(), ))) @@ -661,7 +680,8 @@ impl View { }) .collect::, Error>>()?; let stack = ndarray::stack(ndarray::Axis(0), &views)?; - let transformed = tr.interpolate_par_order(&stack, order)?; + let transformed = + tr.interpolate_par_order(&stack, order, default_value)?; Ok(Arc::from(Frame::$var( transformed.slice(s![0, .., ..]).mapv(|i| i as $t), ))) @@ -775,6 +795,7 @@ mod tests { ]), ); let view_t = view.with_transforms(Transforms { + default_value: Default::default(), interpolation_order: None, channel, drift: Vec::new(), diff --git a/ndbioimage/src/view.rs b/ndbioimage/src/view.rs index 643ee48..3fa5cdd 100644 --- a/ndbioimage/src/view.rs +++ b/ndbioimage/src/view.rs @@ -5,14 +5,14 @@ use crate::metadata::Metadata; use crate::readers::{Dimensions, DynReader, Frame, Reader}; use crate::stats::MinMax; #[cfg(feature = "transforms")] -use crate::transforms::{TransformKind, Transforms}; +use crate::transforms::{DefaultValues, TransformKind, Transforms}; #[cfg(feature = "transforms")] -use image_registration::transform::Transform; +use image_registration::{bspline::DefaultValue, transform::Transform}; use indexmap::IndexMap; use itertools::{Itertools, iproduct}; use ndarray::{ - Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, - Ix2, Ix5, IxDyn, SliceArg, SliceInfoElem, s, + Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix5, IxDyn, + SliceArg, SliceInfoElem, s, }; use num::traits::{AsPrimitive, ToBytes}; use num::{Bounded, FromPrimitive, One, ToPrimitive, Zero}; @@ -990,7 +990,13 @@ impl View { ) = (&kind, op_czt.first(), xy_dim == 2) { transform_done.insert((c, z, t)); - transform.map(|transform| (transform, order)) + transform.map(|transform| { + ( + transform, + order, + Cow::Borrowed(&self.transforms.default_value), + ) + }) } else { None }; @@ -1097,7 +1103,6 @@ impl View { &slice_reader, &ax_out, defer_slice_xy, - order, )?; } @@ -1143,7 +1148,6 @@ impl View { &slice_reader, &ax_out, defer_slice_xy, - order, )?; } @@ -1173,7 +1177,6 @@ impl View { slice: &[Slice], ax: &HashMap, defer_slice_xy: bool, - order: usize, ) -> Result<(), Error> where T: Number, @@ -1188,7 +1191,6 @@ impl View { }; ax.len() ]; - let shape = array.shape(); let slice_out = [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X] .into_iter() @@ -1200,9 +1202,17 @@ impl View { ) }) .collect::>(); - let transpose = ax[&Axis::X] < ax[&Axis::Y]; for (ci, c) in slice_out[0].into_iter().zip(&slice[0]) { + let default_value = if let Some(default_value) = self + .transforms + .default_value + .get(self.channel_name(c as usize)?) + { + Cow::Borrowed(default_value) + } else { + Cow::Owned(DefaultValue::default()) + }; let channel_name = self.channel_name(c as usize)?; for (ti, t) in slice_out[2].into_iter().zip(&slice[2]) { if let Some(transform) = self.transforms.get_transform(channel_name, t as usize)? { @@ -1224,7 +1234,16 @@ impl View { } let untransformed = array.slice(slice_transform.as_slice()).into_dyn(); let transformed = transform - .interpolate_par_order(untransformed, order)? + .interpolate_par_order( + untransformed, + self.transforms + .interpolation_order + .unwrap_or(if T::ISFLOAT { 3 } else { 0 }), + self.transforms + .default_value + .get(channel_name) + .unwrap_or(&default_value), + )? .mapv(|i| T::from_f64(i).unwrap()); array .slice_mut(slice_transform.as_slice()) @@ -1269,7 +1288,11 @@ impl View { c: usize, z: usize, t: usize, - #[cfg(feature = "transforms")] transform: Option<(Cow<'a, Transform>, usize)>, + #[cfg(feature = "transforms")] transform: Option<( + Cow<'a, Transform>, + usize, + Cow<'a, DefaultValues>, + )>, ) -> FrameKeyRef<'a> { FrameKeyRef { reader: ReaderKeyRef { @@ -1310,23 +1333,37 @@ impl View { c: usize, z: usize, t: usize, - #[cfg(feature = "transforms")] transform: Option<(Cow>, usize)>, + #[cfg(feature = "transforms")] transform: Option<( + Cow>, + usize, + Cow, + )>, ) -> Result, Error> { let key = self.frame_key( c, z, t, #[cfg(feature = "transforms")] - transform - .as_ref() - .map(|(transform, order)| (Cow::Borrowed(&**transform), *order)), + transform.as_ref().map(|(transform, order, default_value)| { + ( + Cow::Borrowed(&**transform), + *order, + Cow::Borrowed(&**default_value), + ) + }), ); if let Some(frame) = FrameCache::global().get(&key) { return Ok(frame); } #[cfg(feature = "transforms")] - let frame = if let Some((transform, order)) = transform.as_ref() { - self.get_transformed_frame(c, z, t, transform, *order)? + let frame = if let Some((transform, order, default_value)) = transform.as_ref() { + let channel_name = self.channel_name(c)?; + let default_value = if let Some(default_value) = default_value.get(channel_name) { + Cow::Borrowed(default_value) + } else { + Cow::Owned(DefaultValue::default()) + }; + self.get_transformed_frame(c, z, t, transform, *order, &default_value)? } else { Arc::new(self.reader.get_frame(c, z, t)?) }; @@ -1864,7 +1901,7 @@ mod tests { fn frame_cache() -> Result<(), Error> { let cache = FrameCache::new(2); let key = |c: usize, z: usize, t: usize| FrameKey { - reader_key: ReaderKey { + reader: ReaderKey { name: "test".to_string(), path: PathBuf::from("test.tif"), series: 0,