diff --git a/image-registration b/image-registration index e5136f0..2b01f65 160000 --- a/image-registration +++ b/image-registration @@ -1 +1 @@ -Subproject commit e5136f00bbcfce27355637248808c249d749c7cd +Subproject commit 2b01f65cc941dd046ba4a93d21e56d566fd6f76b diff --git a/ndbioimage/src/error.rs b/ndbioimage/src/error.rs index 8dbd05f..3d2791b 100644 --- a/ndbioimage/src/error.rs +++ b/ndbioimage/src/error.rs @@ -117,6 +117,9 @@ pub enum Error { /// the view is empty #[error("empty view")] EmptyView, + /// the view cannot be parsed as a scalar + #[error("not a scalar")] + NotScalar, /// the color string could not be parsed #[error("invalid color: {0}")] InvalidColor(String), diff --git a/ndbioimage/src/py/imread.rs b/ndbioimage/src/py/imread.rs index 61cc7d6..4ad1e3b 100644 --- a/ndbioimage/src/py/imread.rs +++ b/ndbioimage/src/py/imread.rs @@ -4,9 +4,10 @@ use crate::metadata::Metadata; use crate::movie::MovieOptions; use crate::readers::{DynReader, PixelType, Reader}; -use crate::view::{Item, View}; +use crate::view::View; use itertools::Itertools; use ndarray::{ArrayD, Ix0, Ix1, IxDyn, SliceInfo, SliceInfoElem}; +use num::Zero; use numpy::{ AllowTypeChange, IntoPyArray, PyArray, PyArrayDescr, PyArrayLike0, PyArrayLike1, PyArrayMethods, dtype, @@ -26,6 +27,8 @@ use pyo3_stub_gen::derive::*; use pyo3_stub_gen::inventory::submit; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; +use std::fs::OpenOptions; +use std::io::Write; use std::path::PathBuf; /// class to read image files, while taking good care of important metadata, @@ -70,7 +73,7 @@ unsafe impl Send for PyView {} unsafe impl Sync for PyView {} impl PyView { - fn item(py: Python, view: View, dtype: PixelType) -> PyResult> { + fn scalar(py: Python, view: View, dtype: PixelType) -> PyResult> { macro_rules! item { ($t:tt) => { Ok(view @@ -486,7 +489,7 @@ impl PyView { .collect::>(); let view = view.permute_axes(&axes)?; if view.ndim() == 0 { - Self::item(py, view, self.dtype) + Self::scalar(py, view, self.dtype) } else { PyView { view, @@ -544,7 +547,7 @@ impl PyView { } let view = self.view.slice(new_slice.as_slice())?; if view.ndim() == 0 { - Self::item(py, view, self.dtype) + Self::scalar(py, view, self.dtype) } else { PyView { view, @@ -1157,7 +1160,7 @@ impl PyView { } let view = self.view.slice(new_slice.as_slice())?; Some(if view.ndim() == 0 { - Self::item(py, view, self.dtype) + Self::scalar(py, view, self.dtype) } else { PyView { view, @@ -2006,6 +2009,643 @@ impl PyView { /// backwards compatibility #[expect(unused_variables)] fn set_cache_size(&self, size: usize) {} + + /// set a value at an index - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __setitem__(&self, key: Bound, value: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// delete an index - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __delitem__(&self, key: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place addition - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __iadd__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place subtraction - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __isub__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place multiplication - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __imul__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place true division - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __itruediv__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place floor division - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __ifloordiv__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place modulo - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __imod__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place power - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __ipow__(&self, other: Bound, r#mod: Option>) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place bitwise AND - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __iand__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place bitwise OR - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __ior__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place bitwise XOR - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __ixor__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place left shift - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __ilshift__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place right shift - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __irshift__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// in-place matrix multiplication - not supported (read-only) + #[gen_stub(skip)] + #[expect(unused_variables)] + fn __imatmul__(&self, other: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// set a scalar at an index - not supported (read-only) + #[gen_stub(skip)] + #[pyo3(signature = (index, value))] + #[expect(unused_variables)] + fn itemset(&self, index: Bound, value: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// fill the view with a value - not supported (read-only) + #[gen_stub(skip)] + #[pyo3(signature = (value))] + #[expect(unused_variables)] + fn fill(&self, value: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// put values at indices - not supported (read-only) + #[gen_stub(skip)] + #[pyo3(signature = (indices, values, mode="raise"))] + #[expect(unused_variables)] + fn put(&self, indices: Bound, values: Bound, mode: &str) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// put values at masked indices - not supported (read-only) + #[gen_stub(skip)] + #[pyo3(signature = (mask, values))] + #[expect(unused_variables)] + fn putmask(&self, mask: Bound, values: Bound) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// resize the view in place - not supported (read-only) + #[gen_stub(skip)] + #[pyo3(signature = (new_shape, order="C", refcheck=true))] + #[expect(unused_variables)] + fn resize(&self, new_shape: Bound, order: &str, refcheck: bool) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// byteswap pixels in place - not supported (read-only) + #[gen_stub(skip)] + #[pyo3(signature = (inplace=false))] + #[expect(unused_variables)] + fn byteswap(&self, inplace: bool) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Imread is a read-only, lazy view and cannot be mutated in place", + )) + } + + /// index of the maximum - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, out=None, keepdims=None))] + #[expect(unused_variables)] + fn argmax( + &self, + axis: Option>, + out: Option>, + keepdims: Option>, + ) -> PyResult<()> { + todo!() + } + + /// index of the minimum - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, out=None, keepdims=None))] + #[expect(unused_variables)] + fn argmin( + &self, + axis: Option>, + out: Option>, + keepdims: Option>, + ) -> PyResult<()> { + todo!() + } + + /// standard deviation - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, ddof=None, keepdims=None))] + #[expect(unused_variables)] + fn std( + &self, + axis: Option>, + dtype: Option>, + out: Option>, + ddof: Option>, + keepdims: Option>, + ) -> PyResult<()> { + todo!() + } + + /// variance - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, ddof=None, keepdims=None))] + #[expect(unused_variables)] + fn var( + &self, + axis: Option>, + dtype: Option>, + out: Option>, + ddof: Option>, + keepdims: Option>, + ) -> PyResult<()> { + todo!() + } + + /// product - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=None, initial=None))] + #[expect(unused_variables)] + fn prod( + &self, + axis: Option>, + dtype: Option>, + out: Option>, + keepdims: Option>, + initial: Option>, + ) -> PyResult<()> { + todo!() + } + + /// cumulative sum - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None))] + #[expect(unused_variables)] + fn cumsum( + &self, + axis: Option>, + dtype: Option>, + out: Option>, + ) -> PyResult<()> { + todo!() + } + + /// cumulative product - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None))] + #[expect(unused_variables)] + fn cumprod( + &self, + axis: Option>, + dtype: Option>, + out: Option>, + ) -> PyResult<()> { + todo!() + } + + /// get a single Python scalar - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (index))] + #[expect(unused_variables)] + fn item(&self, index: Bound) -> PyResult<()> { + todo!() + } + + /// indices of non-zero elements - not implemented yet + #[gen_stub(skip)] + fn nonzero(&self) -> PyResult<()> { + todo!() + } + + /// gather along an axis - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (indices, axis=None, mode=None, out=None))] + #[expect(unused_variables)] + fn take( + &self, + indices: Bound, + axis: Option>, + mode: Option>, + out: Option>, + ) -> PyResult<()> { + todo!() + } + + /// gather along an axis after broadcasting - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (arr, indices, axis))] + #[expect(unused_variables)] + fn take_along_axis( + &self, + arr: Bound, + indices: Bound, + axis: Bound, + ) -> PyResult<()> { + todo!() + } + + /// find insertion points - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (a, side=None, sorter=None))] + #[expect(unused_variables)] + fn searchsorted( + &self, + a: Bound, + side: Option>, + sorter: Option>, + ) -> PyResult<()> { + todo!() + } + + /// return the n-th diagonal - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (offset=None, axis1=None, axis2=None))] + #[expect(unused_variables)] + fn diagonal( + &self, + offset: Option>, + axis1: Option>, + axis2: Option>, + ) -> PyResult<()> { + todo!() + } + + /// repeat elements - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (repeats, axis=None, out=None))] + #[expect(unused_variables)] + fn repeat( + &self, + repeats: Bound, + axis: Option>, + out: Option>, + ) -> PyResult<()> { + todo!() + } + + /// truncate to a range - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (a_min=None, a_max=None, out=None))] + #[expect(unused_variables)] + fn clip( + &self, + a_min: Option>, + a_max: Option>, + out: Option>, + ) -> PyResult<()> { + todo!() + } + + /// round to decimals - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (decimals=None, out=None))] + #[expect(unused_variables)] + fn round(&self, decimals: Option>, out: Option>) -> PyResult<()> { + todo!() + } + + /// select elements where a condition is true - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (condition, axis=None, out=None))] + #[expect(unused_variables)] + fn compress( + &self, + condition: Bound, + axis: Option>, + out: Option>, + ) -> PyResult<()> { + todo!() + } + + /// dot product - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (b))] + #[expect(unused_variables)] + fn dot(&self, b: Bound) -> PyResult<()> { + todo!() + } + + /// trace along two axes - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (offset=None, axis1=None, axis2=None, dtype=None, out=None))] + #[expect(unused_variables)] + fn trace( + &self, + offset: Option>, + axis1: Option>, + axis2: Option>, + dtype: Option>, + out: Option>, + ) -> PyResult<()> { + todo!() + } + + /// real part + #[getter] + fn real(&self) -> PyView { + self.clone() + } + + /// imaginary part + #[getter] + fn imag<'py>(&self, py: Python<'py>) -> PyResult> { + macro_rules! imag { + ($t:tt) => { + Ok( + ArrayD::<$t>::zeros(self.view.shape().into_iter().collect::>()) + .into_pyarray(py) + .into_any(), + ) + }; + } + + match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => imag!(i8), + PixelType::U8 => imag!(u8), + PixelType::I16 => imag!(i16), + PixelType::U16 => imag!(u16), + PixelType::I32 => imag!(i32), + PixelType::U32 => imag!(u32), + PixelType::F32 => imag!(f32), + PixelType::F64 => imag!(f64), + PixelType::I64 => imag!(i64), + PixelType::U64 => imag!(u64), + 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")), + } + } + + /// complex conjugate + fn conj(&self) -> Self { + self.clone() + } + + /// complex conjugate + fn conjugate(&self) -> Self { + self.clone() + } + + /// ctypes view - not implemented yet + #[gen_stub(skip)] + #[getter] + fn ctypes(&self) -> PyResult<()> { + todo!() + } + + /// total bytes - not implemented yet + #[gen_stub(skip)] + #[getter] + fn nbytes(&self) -> PyResult<()> { + todo!() + } + + /// bytes per element - not implemented yet + #[gen_stub(skip)] + #[getter] + fn itemsize(&self) -> PyResult<()> { + todo!() + } + + /// strides - not implemented yet + #[gen_stub(skip)] + #[getter] + fn strides(&self) -> PyResult<()> { + todo!() + } + + /// raw data buffer - not implemented yet + #[gen_stub(skip)] + #[getter] + fn data(&self) -> PyResult<()> { + todo!() + } + + /// convert to a nested Python list - not implemented yet + #[gen_stub(skip)] + fn tolist(&self) -> PyResult<()> { + todo!() + } + + /// pickle to a file + fn dump(&self, file: PathBuf) -> PyResult<()> { + OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(file)? + .write_all(&to_stdvec(self).map_err(Error::from)?)?; + Ok(()) + } + + /// pickle to a string + fn dumps(&self) -> PyResult> { + Ok(to_stdvec(self).map_err(Error::from)?) + } + + /// a view of the same data + #[pyo3(signature = (dtype=None, r#type=None))] + fn view(&self, py: Python, dtype: Option>, r#type: Option>) -> PyResult { + if r#type.is_some() { + Err(PyNotImplementedError::new_err( + "type not None is not supported", + )) + } else if let Some(dtype) = dtype { + self.as_type(py, dtype) + } else { + Ok(self.clone()) + } + } + + /// a structured view of the data - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (dtype=None))] + #[expect(unused_variables)] + fn tview(&self, dtype: Option>) -> PyResult<()> { + todo!() + } + + /// a view sharing memory with a buffer - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (mem=None))] + #[expect(unused_variables)] + fn share_memory_with(&self, mem: Option>) -> PyResult<()> { + todo!() + } + + /// split horizontally - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (n))] + #[expect(unused_variables)] + fn hsplit(&self, n: Bound) -> PyResult<()> { + todo!() + } + + /// split vertically - not implemented yet + #[gen_stub(skip)] + #[pyo3(signature = (n=None))] + #[expect(unused_variables)] + fn vsplit(&self, n: Option>) -> PyResult<()> { + todo!() + } + + /// boolean value + fn __bool__(&self) -> PyResult { + macro_rules! _bool { + ($t:tt) => { + Ok(self.view.item::<$t>()? != $t::zero()) + }; + } + + match self.dtype { + PixelType::Bool => todo!(), + PixelType::I8 => _bool!(i8), + PixelType::U8 => _bool!(u8), + PixelType::I16 => _bool!(i16), + PixelType::U16 => _bool!(u16), + PixelType::I32 => _bool!(i32), + PixelType::U32 => _bool!(u32), + PixelType::I64 => _bool!(i64), + PixelType::U64 => _bool!(u64), + PixelType::I128 => _bool!(i128), + PixelType::U128 => _bool!(u128), + PixelType::F32 => _bool!(f32), + PixelType::F64 => _bool!(f64), + PixelType::F128 => Err(PyTypeError::new_err("f128 type is not supported")), + } + } + + /// convert to int - 0-dimensional Imread should always already be materialized + fn __int__(&self) -> PyResult<()> { + Err(PyTypeError::new_err( + "only 0-dimensional arrays can be converted to Python scalars", + )) + } + + /// convert to float - 0-dimensional Imread should always already be materialized + fn __float__(&self) -> PyResult<()> { + Err(PyTypeError::new_err( + "only 0-dimensional arrays can be converted to Python scalars", + )) + } + + /// convert to complex - not implemented yet + #[gen_stub(skip)] + fn __complex__(&self) -> PyResult<()> { + todo!() + } + + /// convert to integer (index) - not implemented yet + #[gen_stub(skip)] + fn __index__(&self) -> PyResult<()> { + todo!() + } + + /// object size in bytes - not implemented yet + #[gen_stub(skip)] + fn __sizeof__(&self) -> PyResult<()> { + todo!() + } } submit! { diff --git a/ndbioimage/src/view.rs b/ndbioimage/src/view.rs index f6e895a..7e95bfd 100644 --- a/ndbioimage/src/view.rs +++ b/ndbioimage/src/view.rs @@ -787,7 +787,25 @@ impl View { let slice: Vec<_> = index.iter().map(|s| SliceInfoElem::Index(*s)).collect(); let view = self.clone().into_dyn().slice(slice.as_slice())?; let arr = view.as_array()?; - Ok(*arr.first().unwrap()) + Ok(*arr.first().expect("there should be exactly one value")) + } + + /// retrieve the single item in the slice + pub fn item(&self) -> Result + where + T: Number, + ArrayD: MinMax>, + Array1: MinMax>, + Array2: MinMax>, + { + match self.shape().cztyx().iter().product() { + 0 => Err(Error::EmptyView), + 1 => Ok(*self + .as_array()? + .first() + .expect("there should be exactly one value")), + _ => Err(Error::NotScalar), + } } /// collect the view into an ndarray @@ -1535,17 +1553,6 @@ where } } -/// trait to define a function to retrieve the only item in a 0d array -pub trait Item { - /// the single item in the 0d array, cast to `T` - fn item(&self) -> Result - where - T: Number, - ArrayD: MinMax>, - Array1: MinMax>, - Array2: MinMax>; -} - impl View { /// create a view on the image at `path`, parsing series and position from the file name pub fn from_path

(path: P) -> Result @@ -1557,18 +1564,6 @@ impl View { } } -impl Item for View { - fn item(&self) -> Result - where - T: Number, - ArrayD: MinMax>, - Array1: MinMax>, - Array2: MinMax>, - { - Ok(*self.as_array()?.first().ok_or(Error::EmptyView)?) - } -} - impl Display for View { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Ok(summary) = self.summary() { @@ -1612,7 +1607,7 @@ mod tests { use crate::error::Error; use crate::readers::{DynReader, Frame, Reader}; use crate::stats::MinMax; - use crate::view::{Item, View}; + use crate::view::View; use indexmap::IndexMap; use ndarray::{Array, Array4, Array5, Ix4, Ix5, NewAxis}; use ndarray::{Array2, ArrayD, IxDyn, SliceInfoElem, s};