- testing 2d transforms

This commit is contained in:
Wim Pomp committed 2026-10-10 12:44:37 +02:00
1 parent 4815690621
commit 453237cae9
20 files changed
+754 -260

No files matched your search

+2 -2
View File
@@ -3,9 +3,9 @@ resolver = "3"
members = ["ndbioimage", "image-tiff", "image-registration"] members = ["ndbioimage", "image-tiff", "image-registration"]
[workspace.package] [workspace.package]
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
rust-version = "1.94.0" rust-version = "1.94.1"
authors = ["Wim Pomp <w.pomp@nki.nl>"] authors = ["Wim Pomp <w.pomp@nki.nl>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Read bio image formats using the bio-formats java package." description = "Read bio image formats using the bio-formats java package."
+1 -1
View File
@@ -37,7 +37,7 @@ ome-metadata = "0.6"
ordered-float = { version = "5", optional = true } ordered-float = { version = "5", optional = true }
phf = { version = "0.14", features = ["macros"] } phf = { version = "0.14", features = ["macros"] }
postcard = { version = "1", features = ["use-std"], optional = true } postcard = { version = "1", features = ["use-std"], optional = true }
pyo3 = { version = "0.29", features = ["abi3-py310", "anyhow", "generate-import-lib"], optional = true } pyo3 = { version = "0.29", features = ["abi3-py310", "anyhow", "generate-import-lib", "multiple-pymethods"], optional = true }
pyo3-stub-gen = { version = "0.23", optional = true } pyo3-stub-gen = { version = "0.23", optional = true }
rayon = { version = "1", optional = true } rayon = { version = "1", optional = true }
regex = "1" regex = "1"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project] [project]
name = "ndbioimage" name = "ndbioimage"
version = "2027.0.5" version = "2027.0.6"
requires-python = ">=3.10" requires-python = ">=3.10"
classifiers = [ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
+3 -3
View File
@@ -198,9 +198,9 @@ impl<'de> DeserializeAs<'de, SliceInfoElem> for SliceInfoElemDef {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct Slice { pub(crate) struct Slice {
start: isize, pub(crate) start: isize,
end: isize, pub(crate) end: isize,
step: isize, pub(crate) step: isize,
} }
impl Slice { impl Slice {
+80 -6
View File
@@ -1,14 +1,22 @@
use crate::axes::{Axis, Operation}; use crate::axes::{Axis, Operation};
use crate::readers::Frame;
#[cfg(feature = "transforms")]
use image_registration::transform::Transform;
use indexmap::{Equivalent, IndexMap}; use indexmap::{Equivalent, IndexMap};
#[cfg(feature = "transforms")]
use ndarray::IxDyn;
use ndarray::{ArrayD, SliceInfoElem}; use ndarray::{ArrayD, SliceInfoElem};
use std::any::Any; use std::any::Any;
#[cfg(feature = "transforms")]
use std::borrow::Cow;
use std::cmp::PartialEq;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock}; use std::sync::{Arc, Mutex, OnceLock};
use std::time::SystemTime; use std::time::SystemTime;
#[cfg(feature = "transforms")]
use crate::readers::Frame; use crate::transforms::Transforms;
/// maximum number of frames held in the cache /// maximum number of frames held in the cache
const DEFAULT_FRAME_CACHE_SIZE: usize = 128; const DEFAULT_FRAME_CACHE_SIZE: usize = 128;
@@ -132,7 +140,15 @@ impl ReaderKeyRef<'_> {
} }
} }
pub(crate) type FrameKey = (ReaderKey, usize, usize, usize); #[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct FrameKey {
pub(crate) reader_key: ReaderKey,
pub(crate) c: usize,
pub(crate) z: usize,
pub(crate) t: usize,
#[cfg(feature = "transforms")]
pub(crate) transform: Option<(Transform<IxDyn>, usize)>,
}
/// borrowed view of [`FrameKey`] for cache lookups without allocation. /// borrowed view of [`FrameKey`] for cache lookups without allocation.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@@ -141,6 +157,8 @@ pub(crate) struct FrameKeyRef<'a> {
pub(crate) c: usize, pub(crate) c: usize,
pub(crate) z: usize, pub(crate) z: usize,
pub(crate) t: usize, pub(crate) t: usize,
#[cfg(feature = "transforms")]
pub(crate) transform: Option<(Cow<'a, Transform<IxDyn>>, usize)>,
} }
impl Hash for FrameKeyRef<'_> { impl Hash for FrameKeyRef<'_> {
@@ -149,19 +167,50 @@ impl Hash for FrameKeyRef<'_> {
self.c.hash(state); self.c.hash(state);
self.z.hash(state); self.z.hash(state);
self.t.hash(state); self.t.hash(state);
#[cfg(feature = "transforms")]
self.transform.hash(state);
} }
} }
impl Equivalent<FrameKey> for FrameKeyRef<'_> { impl Equivalent<FrameKey> for FrameKeyRef<'_> {
fn equivalent(&self, key: &FrameKey) -> bool { fn equivalent(&self, key: &FrameKey) -> bool {
let (rk, c, z, t) = key; let FrameKey {
self.reader.equivalent(rk) && self.c == *c && self.z == *z && self.t == *t reader_key,
c,
z,
t,
#[cfg(feature = "transforms")]
transform,
} = key;
#[cfg(feature = "transforms")]
{
self.reader.equivalent(reader_key)
&& 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))
}
#[cfg(not(feature = "transforms"))]
{
self.reader.equivalent(reader_key) && self.c == *c && self.z == *z && self.t == *t
}
} }
} }
impl FrameKeyRef<'_> { impl FrameKeyRef<'_> {
pub(crate) fn to_owned(&self) -> FrameKey { pub(crate) fn to_owned(&self) -> FrameKey {
(self.reader.to_owned(), self.c, self.z, self.t) FrameKey {
reader_key: self.reader.to_owned(),
c: self.c,
z: self.z,
t: self.t,
#[cfg(feature = "transforms")]
transform: self
.transform
.as_ref()
.map(|(t, i)| (t.clone().into_owned(), *i)),
}
} }
} }
@@ -173,6 +222,8 @@ pub(crate) struct ArrayKey {
pub(crate) slice: Vec<SliceInfoElem>, pub(crate) slice: Vec<SliceInfoElem>,
pub(crate) axes: Vec<Axis>, pub(crate) axes: Vec<Axis>,
pub(crate) operations: Vec<(Axis, Operation)>, pub(crate) operations: Vec<(Axis, Operation)>,
#[cfg(feature = "transforms")]
pub(crate) transforms: Transforms,
} }
/// borrowed view of [`ArrayKey`] for cache lookups without allocation. /// borrowed view of [`ArrayKey`] for cache lookups without allocation.
@@ -183,6 +234,8 @@ pub(crate) struct ArrayKeyRef<'a> {
pub(crate) slice: &'a [SliceInfoElem], pub(crate) slice: &'a [SliceInfoElem],
pub(crate) axes: &'a [Axis], pub(crate) axes: &'a [Axis],
pub(crate) operations: &'a IndexMap<Axis, Operation>, pub(crate) operations: &'a IndexMap<Axis, Operation>,
#[cfg(feature = "transforms")]
pub(crate) transforms: &'a Transforms,
} }
impl Hash for ArrayKeyRef<'_> { impl Hash for ArrayKeyRef<'_> {
@@ -196,11 +249,15 @@ impl Hash for ArrayKeyRef<'_> {
ax.hash(state); ax.hash(state);
op.hash(state); op.hash(state);
} }
#[cfg(feature = "transforms")]
self.transforms.hash(state);
} }
} }
impl Equivalent<ArrayKey> for ArrayKeyRef<'_> { impl Equivalent<ArrayKey> for ArrayKeyRef<'_> {
fn equivalent(&self, key: &ArrayKey) -> bool { fn equivalent(&self, key: &ArrayKey) -> bool {
#[cfg(feature = "transforms")]
{
self.reader.equivalent(&key.reader) self.reader.equivalent(&key.reader)
&& self.dtype == key.dtype && self.dtype == key.dtype
&& self.slice == key.slice && self.slice == key.slice
@@ -211,6 +268,21 @@ impl Equivalent<ArrayKey> for ArrayKeyRef<'_> {
.iter() .iter()
.zip(&key.operations) .zip(&key.operations)
.all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op) .all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op)
&& self.transforms == key.transforms
}
#[cfg(not(feature = "transforms"))]
{
self.reader.equivalent(&key.reader)
&& self.dtype == key.dtype
&& self.slice == key.slice
&& self.axes == key.axes
&& self.operations.len() == key.operations.len()
&& self
.operations
.iter()
.zip(&key.operations)
.all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op)
}
} }
} }
@@ -226,6 +298,8 @@ impl ArrayKeyRef<'_> {
.iter() .iter()
.map(|(ax, op)| (*ax, op.clone())) .map(|(ax, op)| (*ax, op.clone()))
.collect(), .collect(),
#[cfg(feature = "transforms")]
transforms: self.transforms.to_owned(),
} }
} }
} }
+4
View File
@@ -179,6 +179,10 @@ pub enum Error {
it cannot use. read the file before forking, or start the child with spawn/exec instead." it cannot use. read the file before forking, or start the child with spawn/exec instead."
)] )]
ForkedJvm(u32, u32), ForkedJvm(u32, u32),
#[error("{0}D transformation is not implemented")]
TransformationNotImplemented(usize),
#[error("no channel {0} present")]
NoChannel(usize),
} }
impl Error { impl Error {
+4 -4
View File
@@ -1,4 +1,4 @@
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
//! The ndbioimage crate exposes (bio) images as a struct that can be sliced like an ndarray Array //! The ndbioimage crate exposes (bio) images as a struct that can be sliced like an ndarray Array
//! (Rust), but without loading the whole image into memory, reading from the file only when needed. //! (Rust), but without loading the whole image into memory, reading from the file only when needed.
//! Some metadata is read //! Some metadata is read
@@ -16,7 +16,7 @@
//! # fn main() -> Result<(), ndbioimage::error::Error> { //! # fn main() -> Result<(), ndbioimage::error::Error> {
//! let path = "/path/to/file"; //! let path = "/path/to/file";
//! let reader = DynReader::new(&path, 0, 0)?; //! let reader = DynReader::new(&path, 0, 0)?;
//! println!("shape: {}", reader.shape()); //! println!("shape: {}", reader.raw_shape());
//! let frame = reader.get_frame(0, 0, 0)?; //! let frame = reader.get_frame(0, 0, 0)?;
//! if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) { //! if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
//! println!("{:?}", arr); //! println!("{:?}", arr);
@@ -70,7 +70,7 @@ pub mod readers;
pub mod tiffwrite; pub mod tiffwrite;
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
/// image registration and warping images using affine transforms /// image registration and warping images using affine transforms
mod transforms; pub mod transforms;
mod utils; mod utils;
/// main entry point for the application /// main entry point for the application
@@ -338,7 +338,7 @@ mod tests {
fn read_ser() -> Result<(), Error> { fn read_ser() -> Result<(), Error> {
let file = "czi/Experiment-2029.czi"; let file = "czi/Experiment-2029.czi";
let reader = open(file)?; let reader = open(file)?;
println!("shape: {}", reader.shape()); println!("shape: {}", reader.raw_shape());
let frame = reader.get_frame(0, 0, 0)?; let frame = reader.get_frame(0, 0, 0)?;
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) { if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
println!("{:?}", arr); println!("{:?}", arr);
+1
View File
@@ -67,6 +67,7 @@ fn main() -> PyResult<()> {
#[pymodule] #[pymodule]
#[pyo3(name = "ndbioimage_rs")] #[pyo3(name = "ndbioimage_rs")]
mod ndbioimage_rs { mod ndbioimage_rs {
#[cfg(feature = "transforms")]
use pyo3::prelude::*; use pyo3::prelude::*;
#[pymodule_export] #[pymodule_export]
+3 -139
View File
@@ -3,8 +3,6 @@ use crate::error::Error;
use crate::metadata::Metadata; use crate::metadata::Metadata;
use crate::movie::MovieOptions; use crate::movie::MovieOptions;
#[cfg(feature = "transforms")]
use crate::py::transforms::{PyTransform, PyTransforms};
use crate::readers::{DynReader, PixelType, Reader}; use crate::readers::{DynReader, PixelType, Reader};
use crate::view::{Item, View}; use crate::view::{Item, View};
use itertools::Itertools; use itertools::Itertools;
@@ -27,7 +25,7 @@ pub(crate) use pyo3::{
use pyo3_stub_gen::derive::*; use pyo3_stub_gen::derive::*;
use pyo3_stub_gen::inventory::submit; use pyo3_stub_gen::inventory::submit;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
/// class to read image files, while taking good care of important metadata, /// class to read image files, while taking good care of important metadata,
@@ -61,7 +59,7 @@ use std::path::PathBuf;
#[pyclass(subclass, from_py_object, name = "Imread", module = "ndbioimage")] #[pyclass(subclass, from_py_object, name = "Imread", module = "ndbioimage")]
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct PyView { pub(crate) struct PyView {
view: View<IxDyn, DynReader>, pub(crate) view: View<IxDyn, DynReader>,
dtype: PixelType, dtype: PixelType,
#[serde(skip)] #[serde(skip)]
ome: Ome, ome: Ome,
@@ -296,141 +294,6 @@ impl PyView {
todo!() todo!()
} }
/// return a new view with transformations applied (channel alignment, drift correction)
/// backward compatibility
#[cfg(feature = "transforms")]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transform<'py>(
&self,
channels: bool,
drift: bool,
file: Option<Bound<'py, PyAny>>,
bead_files: Option<Bound<'py, PyAny>>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> PyResult<PyView> {
self.with_transforms(
channels,
drift,
file,
bead_files,
main_channel,
default_transform,
)
}
/// return a new view with transformations applied (channel alignment, drift correction)
#[cfg(feature = "transforms")]
#[expect(unused_variables)]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transforms<'py>(
&self,
channels: bool,
drift: bool,
file: Option<Bound<'py, PyAny>>,
bead_files: Option<Bound<'py, PyAny>>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> PyResult<PyView> {
todo!()
}
/// get the transformation
#[cfg(feature = "transforms")]
#[getter]
fn get_transforms(&self) -> PyResult<PyTransforms> {
Ok(PyTransforms {
inner: self.view.get_transforms().clone(),
})
}
/// set the transformation
#[cfg(feature = "transforms")]
#[setter]
fn set_transforms(&mut self, transform: PyTransforms) {
self.view.set_transforms(transform.inner);
}
#[cfg(feature = "transforms")]
fn load_transforms_from_yaml(&mut self, path: PathBuf) -> PyResult<()> {
self.view.load_transforms_from_yaml(path.as_path())?;
Ok(())
}
#[cfg(feature = "transforms")]
fn calculate_channel_transforms_2d(
&self,
py: Python,
main_channel: usize,
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_2d(main_channel)?
.into_iter()
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
}
#[cfg(feature = "transforms")]
fn calculate_channel_transforms_3d(
&self,
py: Python,
main_channel: usize,
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_3d(main_channel)?
.into_iter()
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
}
#[cfg(feature = "transforms")]
fn calculate_drift_transforms_2d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transforms_2d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
})
}
#[cfg(feature = "transforms")]
fn calculate_drift_transforms_3d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transforms_3d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
})
}
#[gen_stub(override_return_type(type_repr="numpy.ndarray | int | float", imports=("numpy")))] #[gen_stub(override_return_type(type_repr="numpy.ndarray | int | float", imports=("numpy")))]
fn squeeze<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { fn squeeze<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let view = self.view.squeeze()?; let view = self.view.squeeze()?;
@@ -1481,6 +1344,7 @@ impl PyView {
))] ))]
axes: Option<Vec<Bound<PyAny>>>, axes: Option<Vec<Bound<PyAny>>>,
) -> PyResult<Self> { ) -> PyResult<Self> {
// TODO: transpose("xyz"), transpose(*axes)
let view = if let Some(axes) = axes { let view = if let Some(axes) = axes {
let ax = axes let ax = axes
.into_iter() .into_iter()
+133
View File
@@ -1,4 +1,5 @@
use crate::error::Error; use crate::error::Error;
use crate::py::imread::PyView;
use crate::transforms::Transforms; use crate::transforms::Transforms;
use image_registration::metric::{SamplingArg, Sigma}; use image_registration::metric::{SamplingArg, Sigma};
use image_registration::register::{Optimizer, RegistrationStep}; use image_registration::register::{Optimizer, RegistrationStep};
@@ -709,6 +710,138 @@ pub(crate) struct PyTransforms {
pub(crate) inner: Transforms, pub(crate) inner: Transforms,
} }
#[gen_stub_pymethods]
#[pymethods]
impl PyView {
/// return a new view with transformations applied (channel alignment, drift correction)
/// backward compatibility
#[cfg(feature = "transforms")]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transform<'py>(
&self,
channels: bool,
drift: bool,
file: Option<Bound<'py, PyAny>>,
bead_files: Option<Bound<'py, PyAny>>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> PyResult<PyView> {
self.with_transforms(
channels,
drift,
file,
bead_files,
main_channel,
default_transform,
)
}
/// return a new view with transformations applied (channel alignment, drift correction)
#[cfg(feature = "transforms")]
#[expect(unused_variables)]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transforms<'py>(
&self,
channels: bool,
drift: bool,
file: Option<Bound<'py, PyAny>>,
bead_files: Option<Bound<'py, PyAny>>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> PyResult<PyView> {
todo!()
}
/// get the transformation
#[getter]
fn get_transforms(&self) -> PyResult<PyTransforms> {
Ok(PyTransforms {
inner: self.view.get_transforms().clone(),
})
}
/// set the transformation
#[setter]
fn set_transforms(&mut self, transform: PyTransforms) {
self.view.set_transforms(transform.inner);
}
fn load_transforms_from_yaml(&mut self, path: PathBuf) -> PyResult<()> {
self.view.load_transforms_from_yaml(path.as_path())?;
Ok(())
}
fn calculate_channel_transforms_2d(
&self,
py: Python,
main_channel: usize,
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_2d(main_channel)?
.into_iter()
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
}
fn calculate_channel_transforms_3d(
&self,
py: Python,
main_channel: usize,
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_3d(main_channel)?
.into_iter()
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
}
fn calculate_drift_transforms_2d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transforms_2d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
})
}
fn calculate_drift_transforms_3d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transforms_3d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
})
}
}
#[gen_stub_pymethods] #[gen_stub_pymethods]
#[pymethods] #[pymethods]
impl PyTransforms { impl PyTransforms {
+14 -11
View File
@@ -2,7 +2,7 @@ use crate::axes::{Axis, Shape};
use crate::error::Error; use crate::error::Error;
use crate::view::View; use crate::view::View;
use ndarray::{Array, Dimension, Ix2, Ix5, s}; use ndarray::{Array, Dimension, Ix2, Ix5, s};
use num::{FromPrimitive, Zero}; use num::{FromPrimitive, One, Zero};
use ome_metadata::Ome; use ome_metadata::Ome;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -148,6 +148,8 @@ impl PixelType {
/// array data with a specific pixel type /// array data with a specific pixel type
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ArrayT<D: Dimension> { pub enum ArrayT<D: Dimension> {
/// boolean array
Bool(Array<bool, D>),
/// signed 8-bit integer array /// signed 8-bit integer array
I8(Array<i8, D>), I8(Array<i8, D>),
/// unsigned 8-bit integer array /// unsigned 8-bit integer array
@@ -197,7 +199,7 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
/// get a sliceable view on the image file /// get a sliceable view on the image file
fn view(&self) -> View<Ix5, Self> { fn view(&self) -> View<Ix5, Self> {
let shape = self.shape(); let shape = self.raw_shape();
let slice = s![0..shape.c, 0..shape.z, 0..shape.t, 0..shape.y, 0..shape.x,]; let slice = s![0..shape.c, 0..shape.z, 0..shape.t, 0..shape.y, 0..shape.x,];
View::new( View::new(
self.clone(), self.clone(),
@@ -216,7 +218,7 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
/// the position index /// the position index
fn position(&self) -> usize; fn position(&self) -> usize;
/// the shape of the image data /// the shape of the image data
fn shape(&self) -> &Shape; fn raw_shape(&self) -> &Shape;
/// the pixel type of the image data /// the pixel type of the image data
fn pixel_type(&self) -> &PixelType; fn pixel_type(&self) -> &PixelType;
/// get all available positions for a given series /// get all available positions for a given series
@@ -314,6 +316,7 @@ macro_rules! try_into_array_body {
($self:expr, $map:ident) => {{ ($self:expr, $map:ident) => {{
let mut err = Ok(()); let mut err = Ok(());
let arr = match $self { let arr = match $self {
ArrayT::Bool(v) => v.$map(|x| if x { T::one() } else { T::zero() }),
ArrayT::I8(v) => v.$map(|x| { ArrayT::I8(v) => v.$map(|x| {
T::from_i8(x).unwrap_or_else(|| { T::from_i8(x).unwrap_or_else(|| {
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string())); err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
@@ -397,7 +400,7 @@ macro_rules! try_into_array_body {
impl<D, T> TryInto<Array<T, D>> for ArrayT<D> impl<D, T> TryInto<Array<T, D>> for ArrayT<D>
where where
D: Dimension, D: Dimension,
T: FromPrimitive + Zero + 'static, T: FromPrimitive + Zero + One + 'static,
{ {
type Error = Error; type Error = Error;
@@ -409,7 +412,7 @@ where
impl<D, T> TryInto<Array<T, D>> for &ArrayT<D> impl<D, T> TryInto<Array<T, D>> for &ArrayT<D>
where where
D: Dimension, D: Dimension,
T: Clone + FromPrimitive + Zero + 'static, T: Clone + FromPrimitive + Zero + One + 'static,
{ {
type Error = Error; type Error = Error;
@@ -577,18 +580,18 @@ impl Reader for DynReader {
} }
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
match self { match self {
#[cfg(feature = "tiff")] #[cfg(feature = "tiff")]
DynReader::Tiff(r) => r.shape(), DynReader::Tiff(r) => r.raw_shape(),
#[cfg(feature = "tiffseq")] #[cfg(feature = "tiffseq")]
DynReader::TiffSeq(r) => r.shape(), DynReader::TiffSeq(r) => r.raw_shape(),
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
DynReader::Czi(r) => r.shape(), DynReader::Czi(r) => r.raw_shape(),
#[cfg(feature = "bioformats_rust")] #[cfg(feature = "bioformats_rust")]
DynReader::BioFormatsRust(r) => r.shape(), DynReader::BioFormatsRust(r) => r.raw_shape(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.shape(), DynReader::BioFormatsJava(r) => r.raw_shape(),
#[expect(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
+2 -2
View File
@@ -38,7 +38,7 @@ static JVM_PID: AtomicU32 = AtomicU32::new(0);
/// else -- verified by `JNI_GetCreatedJavaVMs`, which still reports 1 in the child, so a second /// else -- verified by `JNI_GetCreatedJavaVMs`, which still reports 1 in the child, so a second
/// `JNI_CreateJavaVM` (which is what j4rs' `Jvm::attach_thread` calls) takes the already-exists /// `JNI_CreateJavaVM` (which is what j4rs' `Jvm::attach_thread` calls) takes the already-exists
/// path. An `exec` replaces the address space, and that does reset the count to 0. /// path. An `exec` replaces the address space, and that does reset the count to 0.
fn check_not_forked() -> Result<(), Error> { pub fn check_not_forked() -> Result<(), Error> {
let built_in = JVM_PID.load(Ordering::Relaxed); let built_in = JVM_PID.load(Ordering::Relaxed);
if built_in != 0 && built_in != std::process::id() { if built_in != 0 && built_in != std::process::id() {
return Err(Error::ForkedJvm(built_in, std::process::id())); return Err(Error::ForkedJvm(built_in, std::process::id()));
@@ -698,7 +698,7 @@ impl Reader for BioFormatsJavaReader {
0 0
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
+1 -1
View File
@@ -309,7 +309,7 @@ impl Reader for BioFormatsRustReader {
0 0
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
+1 -1
View File
@@ -1149,7 +1149,7 @@ impl Reader for CziReader {
self.position self.position
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
+1 -1
View File
@@ -459,7 +459,7 @@ impl Reader for TiffReader {
fn position(&self) -> usize { fn position(&self) -> usize {
self.position self.position
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
fn pixel_type(&self) -> &PixelType { fn pixel_type(&self) -> &PixelType {
+1 -1
View File
@@ -572,7 +572,7 @@ impl Reader for TiffSeqReader {
self.position self.position
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
+234 -6
View File
@@ -1,13 +1,16 @@
use crate::axes::Axis; use crate::axes::Axis;
use crate::error::Error; use crate::error::Error;
use crate::readers::{DynReader, Reader}; use crate::readers::{DynReader, Frame, PixelType, Reader};
use crate::view::View; use crate::view::View;
pub use image_registration::transform::Transform; pub use image_registration::transform::Transform;
use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, s}; use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, array, s};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::io::Seek; use std::io::Seek;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
struct TransformOld { struct TransformOld {
@@ -54,6 +57,24 @@ pub struct Transforms {
pub drift: Vec<Transform<IxDyn>>, pub drift: Vec<Transform<IxDyn>>,
} }
impl Hash for Transforms {
fn hash<H: Hasher>(&self, state: &mut H) {
let mut keys: Vec<_> = self.channel.keys().collect();
keys.sort();
for key in keys {
key.hash(state);
self.channel[key].hash(state);
}
self.drift.hash(state);
}
}
impl PartialEq<Transforms> for &Transforms {
fn eq(&self, other: &Transforms) -> bool {
**self == *other
}
}
impl Serialize for Transforms { impl Serialize for Transforms {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@@ -89,7 +110,23 @@ impl Serialize for Transforms {
} }
} }
pub enum TransformKind {
None,
Channel,
Drift,
Both,
}
impl Transforms { impl Transforms {
pub fn kind(&self) -> TransformKind {
match (self.channel.is_empty(), self.drift.is_empty()) {
(true, true) => TransformKind::None,
(false, true) => TransformKind::Channel,
(true, false) => TransformKind::Drift,
(false, false) => TransformKind::Both,
}
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> { pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let mut file = std::fs::File::open(&path)?; let mut file = std::fs::File::open(&path)?;
Ok( Ok(
@@ -210,16 +247,32 @@ impl Transforms {
} }
Ok(average_transforms) Ok(average_transforms)
} }
pub fn get_transform(
&self,
channel: &str,
t: usize,
) -> Result<Option<Cow<'_, Transform<IxDyn>>>, Error> {
match (self.channel.get(channel), self.drift.get(t)) {
(Some(channel), Some(drift)) => match (channel.ndim(), drift.ndim()) {
(2, 2) | (3, 3) => Ok(Some(Cow::Owned(drift * channel))),
(2, 3) => Ok(Some(Cow::Owned(drift.add_dimension(0, 1) * channel))),
(3, 2) => Ok(Some(Cow::Owned(drift * channel.add_dimension(0, 1)))),
(2 | 3, n) | (n, _) => Err(Error::TransformationNotImplemented(n)),
},
(Some(channel), None) => Ok(Some(Cow::Borrowed(channel))),
(None, Some(drift)) => Ok(Some(Cow::Borrowed(drift))),
(None, None) => Ok(None),
}
}
} }
impl<D: Dimension, R: Reader> View<D, R> { impl<D: Dimension, R: Reader> View<D, R> {
#[cfg(feature = "transforms")] pub fn get_transforms(&self) -> &Transforms {
pub(crate) fn get_transforms(&self) -> &Transforms {
&self.transforms &self.transforms
} }
#[cfg(feature = "transforms")] pub fn set_transforms(&mut self, transforms: Transforms) {
pub(crate) fn set_transforms(&mut self, transforms: Transforms) {
self.transforms = transforms; self.transforms = transforms;
} }
@@ -318,6 +371,181 @@ impl<D: Dimension, R: Reader> View<D, R> {
pub fn calculate_drift_transforms_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> { pub fn calculate_drift_transforms_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> {
todo!() todo!()
} }
pub(crate) fn get_transformed_frame(
&self,
c: usize,
z: usize,
t: usize,
transform: &Transform<IxDyn>,
order: usize,
) -> Result<Arc<Frame>, Error> {
match transform.ndim() {
2 => {
let tr = transform.clone().into_dimensionality::<Ix2>()?;
macro_rules! interpolate {
($array:expr ,$var:ident, bool) => {
Ok(Arc::from(Frame::$var(
tr.interpolate_par_order(
&$array.mapv(|i| if i { 1.0 } else { 0.0 }),
order,
)?
.mapv(|i| i > 0.5),
)))
};
($array:expr ,$var:ident, f64) => {
Ok(Arc::from(Frame::$var(
tr.interpolate_par_order($array, order)?,
)))
};
($array:expr ,$var:ident, $t:ty) => {
Ok(Arc::from(Frame::$var(
tr.interpolate_par_order($array, order)?.mapv(|i| i as $t),
)))
};
}
match self.get_cached_frame(c, z, t, None)?.as_ref() {
Frame::Bool(frame) => interpolate!(frame, Bool, bool),
Frame::I8(frame) => interpolate!(frame, I8, i8),
Frame::U8(frame) => interpolate!(frame, U8, u8),
Frame::I16(frame) => interpolate!(frame, I16, i16),
Frame::U16(frame) => interpolate!(frame, U16, u16),
Frame::I32(frame) => interpolate!(frame, I32, i32),
Frame::U32(frame) => interpolate!(frame, U32, u32),
Frame::U64(frame) => interpolate!(frame, U64, u64),
Frame::I64(frame) => interpolate!(frame, I64, i64),
Frame::I128(frame) => interpolate!(frame, I128, i128),
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 => {
let tr = transform.clone().into_dimensionality::<Ix3>()?;
let shape = self.shape();
let zf = z as f64;
let yf = shape.y as f64 - 1.0;
let xf = shape.x as f64 - 1.0;
let corners = array![[zf, 0.0, 0.0], [zf, 0.0, xf], [zf, yf, 0.0], [zf, yf, xf]];
let extent = tr.transform_points(&corners)?;
let z_extent = extent.slice(s![.., 2]);
let z_min = (z_extent
.iter()
.min_by(|a, b| a.total_cmp(b))
.unwrap()
.floor() as isize
- 2)
.max(0) as usize;
let z_max = (z_extent
.iter()
.max_by(|a, b| a.total_cmp(b))
.unwrap()
.ceil() as isize
+ 2)
.min(shape.z as isize - 1)
.max(0) as usize;
let frames = (z_min..=z_max)
.map(|zi| self.get_cached_frame(c, zi, t, None))
.collect::<Result<Vec<_>, Error>>()?;
// TODO; check these translations
let u0 = tr.as_translation(&[(z_min as f64), 0.0, 0.0]);
let u1 = tr.as_translation(&[-(z_min as f64), 0.0, 0.0]);
let tr = u0 * tr * u1;
match frames.first().map(|f| f.as_ref()) {
Some(first) => {
macro_rules! interpolate {
($var:ident, bool) => {{
let views = frames
.iter()
.map(|f| match f.as_ref() {
Frame::$var(a) => Ok(a.view()),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, Error>>()?;
let stack = ndarray::stack(ndarray::Axis(0), &views)?;
let transformed = tr.interpolate_par_order(
&stack.mapv(|i| if i { 1.0 } else { 0.0 }),
order,
)?;
Ok(Arc::from(Frame::$var(
transformed.slice(s![0, .., ..]).mapv(|i| i > 0.5),
)))
}};
($var:ident, f64) => {{
let views = frames
.iter()
.map(|f| match f.as_ref() {
Frame::$var(a) => Ok(a.view()),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, Error>>()?;
let stack = ndarray::stack(ndarray::Axis(0), &views)?;
let transformed = tr.interpolate_par_order(&stack, order)?;
Ok(Arc::from(Frame::$var(
transformed.slice(s![0, .., ..]).to_owned(),
)))
}};
($var:ident, $t:ty) => {{
let views = frames
.iter()
.map(|f| match f.as_ref() {
Frame::$var(a) => Ok(a.view()),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, Error>>()?;
let stack = ndarray::stack(ndarray::Axis(0), &views)?;
let transformed = tr.interpolate_par_order(&stack, order)?;
Ok(Arc::from(Frame::$var(
transformed.slice(s![0, .., ..]).mapv(|i| i as $t),
)))
}};
}
match first {
Frame::Bool(_) => interpolate!(Bool, bool),
Frame::I8(_) => interpolate!(I8, i8),
Frame::U8(_) => interpolate!(U8, u8),
Frame::I16(_) => interpolate!(I16, i16),
Frame::U16(_) => interpolate!(U16, u16),
Frame::I32(_) => interpolate!(I32, i32),
Frame::U32(_) => interpolate!(U32, u32),
Frame::F32(_) => interpolate!(F32, f32),
Frame::F64(_) => interpolate!(F64, f64),
Frame::I64(_) => interpolate!(I64, i64),
Frame::U64(_) => interpolate!(U64, u64),
Frame::I128(_) => interpolate!(I128, i128),
Frame::U128(_) => interpolate!(U128, u128),
Frame::F128(_) => interpolate!(F128, f64),
}
}
None => {
let shape = self.shape();
Ok(Arc::new(match self.pixel_type() {
PixelType::Bool => Frame::Bool(Array2::default((shape.y, shape.x))),
PixelType::I8 => Frame::I8(Array2::zeros((shape.y, shape.x))),
PixelType::U8 => Frame::U8(Array2::zeros((shape.y, shape.x))),
PixelType::I16 => Frame::I16(Array2::zeros((shape.y, shape.x))),
PixelType::U16 => Frame::U16(Array2::zeros((shape.y, shape.x))),
PixelType::I32 => Frame::I32(Array2::zeros((shape.y, shape.x))),
PixelType::U32 => Frame::U32(Array2::zeros((shape.y, shape.x))),
PixelType::F32 => Frame::F32(Array2::zeros((shape.y, shape.x))),
PixelType::F64 => Frame::F64(Array2::zeros((shape.y, shape.x))),
PixelType::I64 => Frame::I64(Array2::zeros((shape.y, shape.x))),
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))),
}))
}
}
}
d => Err(Error::TransformationNotImplemented(d)),
}
}
} }
#[cfg(test)] #[cfg(test)]
+266 -79
View File
@@ -5,18 +5,23 @@ use crate::metadata::Metadata;
use crate::readers::{Dimensions, DynReader, Frame, Reader}; use crate::readers::{Dimensions, DynReader, Frame, Reader};
use crate::stats::MinMax; use crate::stats::MinMax;
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
use crate::transforms::Transforms; use crate::transforms::{TransformKind, Transforms};
#[cfg(feature = "transforms")]
use image_registration::transform::Transform;
use indexmap::IndexMap; use indexmap::IndexMap;
use itertools::{Itertools, iproduct}; use itertools::{Itertools, iproduct};
use ndarray::{ use ndarray::{
Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix5, IxDyn, Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix5, IxDyn,
SliceArg, SliceInfoElem, s, SliceArg, SliceInfoElem, s,
}; };
use num::traits::ToBytes; use num::traits::{AsPrimitive, ToBytes};
use num::{Bounded, FromPrimitive, ToPrimitive, Zero}; use num::{Bounded, FromPrimitive, One, ToPrimitive, Zero};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
use std::any::type_name; use std::any::type_name;
#[cfg(feature = "transforms")]
use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@@ -57,26 +62,42 @@ pub trait Number:
+ Sync + Sync
+ AddAssign + AddAssign
+ Bounded + Bounded
+ Clone + Copy
+ Div<Self, Output = Self> + Div<Self, Output = Self>
+ FromPrimitive + FromPrimitive
+ PartialOrd + PartialOrd
+ Zero + Zero
+ One
+ AsPrimitive<f64>
{ {
const ISFLOAT: bool;
} }
impl<T> Number for T where
T: 'static macro_rules! impl_int {
+ Send ($($t:tt),+ $(,)?) => {
+ Sync $(
+ AddAssign impl Number for $t {
+ Bounded const ISFLOAT: bool = false;
+ Clone
+ Div<Self, Output = Self>
+ FromPrimitive
+ PartialOrd
+ Zero
{
} }
)*
};
}
impl_int!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
);
macro_rules! impl_float {
($($t:tt),+ $(,)?) => {
$(
impl Number for $t {
const ISFLOAT: bool = true;
}
)*
};
}
impl_float!(f32, f64);
/// sliceable view on an image file /// sliceable view on an image file
#[serde_as] #[serde_as]
@@ -89,6 +110,8 @@ pub struct View<D: Dimension, R: Reader = DynReader> {
/// always has all of cztyx with possibly some new axes added /// always has all of cztyx with possibly some new axes added
axes: Vec<Axis>, axes: Vec<Axis>,
operations: IndexMap<Axis, Operation>, operations: IndexMap<Axis, Operation>,
#[serde(skip)]
channel_names: OnceCell<HashMap<usize, String>>,
dimensionality: PhantomData<D>, dimensionality: PhantomData<D>,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
pub(crate) transforms: Transforms, pub(crate) transforms: Transforms,
@@ -117,6 +140,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
slice, slice,
axes, axes,
operations: IndexMap::new(), operations: IndexMap::new(),
channel_names: OnceCell::new(),
dimensionality: PhantomData, dimensionality: PhantomData,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: Transforms::default(), transforms: Transforms::default(),
@@ -126,7 +150,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
#[expect(dead_code)] #[expect(dead_code)]
pub(crate) fn new_with_axes(reader: R, axes: Vec<Axis>) -> Result<Self, Error> { pub(crate) fn new_with_axes(reader: R, axes: Vec<Axis>) -> Result<Self, Error> {
let mut slice = Vec::new(); let mut slice = Vec::new();
let shape = reader.shape(); let shape = reader.raw_shape();
for axis in axes.iter() { for axis in axes.iter() {
match axis { match axis {
Axis::C => slice.push(SliceInfoElem::Slice { Axis::C => slice.push(SliceInfoElem::Slice {
@@ -183,6 +207,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
axes, axes,
operations: IndexMap::new(), operations: IndexMap::new(),
dimensionality: PhantomData, dimensionality: PhantomData,
channel_names: OnceCell::new(),
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: Transforms::default(), transforms: Transforms::default(),
}) })
@@ -198,6 +223,26 @@ impl<D: Dimension, R: Reader> View<D, R> {
self.reader.series() self.reader.series()
} }
/// the name of a particular channel
pub fn channel_name(&self, channel: usize) -> Result<&str, Error> {
if self.channel_names.get().is_none() {
let names = self.metadata()?.image[0]
.pixels
.channel
.iter()
.enumerate()
.map(|(i, c)| (i, c.name.clone().unwrap_or_else(|| format!("{}", i))))
.collect::<HashMap<_, _>>();
let _ = self.channel_names.set(names);
}
self.channel_names
.get()
.and_then(|names| names.get(&channel))
.map(String::as_str)
.ok_or(Error::NoChannel(channel))
}
/// set the maximum number of frames the process-wide cache may hold, /// set the maximum number of frames the process-wide cache may hold,
/// evicting the least recently used frames /// evicting the least recently used frames
pub fn with_cache_capacity(self, capacity: usize) -> Self { pub fn with_cache_capacity(self, capacity: usize) -> Self {
@@ -234,6 +279,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
slice: self.slice, slice: self.slice,
axes: self.axes, axes: self.axes,
operations: self.operations, operations: self.operations,
channel_names: self.channel_names,
dimensionality: PhantomData, dimensionality: PhantomData,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: Transforms::default(), transforms: Transforms::default(),
@@ -249,6 +295,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
slice: self.slice, slice: self.slice,
axes: self.axes, axes: self.axes,
operations: self.operations, operations: self.operations,
channel_names: self.channel_names,
dimensionality: PhantomData, dimensionality: PhantomData,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: self.transforms, transforms: self.transforms,
@@ -262,6 +309,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
slice: self.slice, slice: self.slice,
axes: self.axes, axes: self.axes,
operations: self.operations, operations: self.operations,
channel_names: self.channel_names,
dimensionality: PhantomData, dimensionality: PhantomData,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: self.transforms, transforms: self.transforms,
@@ -671,7 +719,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
let slice: Vec<_> = index.iter().map(|s| SliceInfoElem::Index(*s)).collect(); let slice: Vec<_> = index.iter().map(|s| SliceInfoElem::Index(*s)).collect();
let view = self.clone().into_dyn().slice(slice.as_slice())?; let view = self.clone().into_dyn().slice(slice.as_slice())?;
let arr = view.as_array()?; let arr = view.as_array()?;
Ok(arr.first().unwrap().clone()) Ok(*arr.first().unwrap())
} }
/// collect the view into an ndarray /// collect the view into an ndarray
@@ -714,7 +762,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
IndexMap::new() IndexMap::new()
}; };
let mut shape_out = Vec::new(); let mut shape_out = Vec::new();
let mut slice = Vec::new(); let mut slice_out = Vec::new();
let mut ax_out = Vec::new(); let mut ax_out = Vec::new();
for (s, a) in self.slice.iter().zip(&self.axes) { for (s, a) in self.slice.iter().zip(&self.axes) {
match s { match s {
@@ -722,7 +770,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
let end = end.expect("slice has no end"); let end = end.expect("slice has no end");
if !op_xy.contains_key(a) && !op_czt.contains_key(a) { if !op_xy.contains_key(a) && !op_czt.contains_key(a) {
shape_out.push(((end - start).max(0) / step) as usize); shape_out.push(((end - start).max(0) / step) as usize);
slice.push(SliceInfoElem::Slice { slice_out.push(SliceInfoElem::Slice {
start: 0, start: 0,
end: None, end: None,
step: 1, step: 1,
@@ -733,19 +781,18 @@ impl<D: Dimension, R: Reader> View<D, R> {
SliceInfoElem::Index(_) => {} SliceInfoElem::Index(_) => {}
SliceInfoElem::NewAxis => { SliceInfoElem::NewAxis => {
shape_out.push(1); shape_out.push(1);
slice.push(SliceInfoElem::Index(0)); slice_out.push(SliceInfoElem::Index(0));
ax_out.push(*a); ax_out.push(*a);
} }
} }
} }
let mut slice_reader = vec![Slice::empty(); 5]; let mut slice_reader = vec![Slice::empty(); 5];
let mut xy_dim = 0usize; let mut xy_dim = 0usize;
let shape_reader = self.reader.shape(); let shape_reader = self.reader.raw_shape();
for (s, &axis) in self.slice.iter().zip(&self.axes) { for (s, &axis) in self.slice.iter().zip(&self.axes) {
match axis { match (axis, s) {
Axis::New => {} (Axis::New, _) => {}
_ => match s { (_, SliceInfoElem::Slice { start, end, step }) => {
SliceInfoElem::Slice { start, end, step } => {
if let Axis::X | Axis::Y = axis if let Axis::X | Axis::Y = axis
&& !op_xy.contains_key(&axis) && !op_xy.contains_key(&axis)
{ {
@@ -757,35 +804,31 @@ impl<D: Dimension, R: Reader> View<D, R> {
*step, *step,
); );
} }
SliceInfoElem::Index(j) => { (_, SliceInfoElem::Index(j)) => {
slice_reader[axis as usize] = Slice::new( slice_reader[axis as usize] = Slice::new(
idx_bnd(*j, shape_reader[axis] as isize)?, idx_bnd(*j, shape_reader[axis] as isize)?,
slc_bnd(*j + 1, shape_reader[axis] as isize)?, slc_bnd(*j + 1, shape_reader[axis] as isize)?,
1, 1,
); );
} }
SliceInfoElem::NewAxis => panic!("axis cannot be a new axis"), (_, SliceInfoElem::NewAxis) => panic!("axis cannot be a new axis"),
},
} }
} }
let xy = [ let xy = [
self.slice[Axis::Y.pos(&self.axes, &self.slice)?], self.slice[Axis::Y.pos(&self.axes, &self.slice)?],
self.slice[Axis::X.pos(&self.axes, &self.slice)?], self.slice[Axis::X.pos(&self.axes, &self.slice)?],
]; ];
let mut array = if let Some((_, op)) = op_czt.first() { let mut array = match op_czt.first() {
match op { Some((_, Operation::Max)) => {
Operation::Max => {
ArrayD::<T>::from_elem(shape_out.into_dimension(), T::min_value()) ArrayD::<T>::from_elem(shape_out.into_dimension(), T::min_value())
} }
Operation::Min => { Some((_, Operation::Min)) => {
ArrayD::<T>::from_elem(shape_out.into_dimension(), T::max_value()) ArrayD::<T>::from_elem(shape_out.into_dimension(), T::max_value())
} }
_ => ArrayD::<T>::zeros(shape_out.into_dimension()), _ => ArrayD::<T>::zeros(shape_out.into_dimension()),
}
} else {
ArrayD::<T>::zeros(shape_out.into_dimension())
}; };
let shape = self.reader.shape();
let shape = self.reader.raw_shape();
let mut axes_out_idx = [None; 5]; let mut axes_out_idx = [None; 5];
for (i, ax) in ax_out.iter().enumerate() { for (i, ax) in ax_out.iter().enumerate() {
if *ax < Axis::New { if *ax < Axis::New {
@@ -793,22 +836,56 @@ impl<D: Dimension, R: Reader> View<D, R> {
} }
} }
#[cfg(feature = "transforms")]
let kind = self.transforms.kind();
#[cfg(feature = "transforms")]
let order = if T::ISFLOAT { 3 } else { 0 };
#[cfg(feature = "transforms")]
let mut transform_done = HashSet::new();
for (c, z, t) in iproduct!(&slice_reader[0], &slice_reader[1], &slice_reader[2]) { for (c, z, t) in iproduct!(&slice_reader[0], &slice_reader[1], &slice_reader[2]) {
if let Some(i) = axes_out_idx[0] { if let Some(i) = axes_out_idx[0] {
slice[i] = SliceInfoElem::Index(c) slice_out[i] = SliceInfoElem::Index(c)
}; };
if let Some(i) = axes_out_idx[1] { if let Some(i) = axes_out_idx[1] {
slice[i] = SliceInfoElem::Index(z) slice_out[i] = SliceInfoElem::Index(z)
}; };
if let Some(i) = axes_out_idx[2] { if let Some(i) = axes_out_idx[2] {
slice[i] = SliceInfoElem::Index(t) slice_out[i] = SliceInfoElem::Index(t)
}; };
#[cfg(feature = "transforms")]
let transform = self
.transforms
.get_transform(self.channel_name(c as usize)?, t as usize)?;
#[cfg(feature = "transforms")]
let transform_now = if let (TransformKind::Channel, Some((&Axis::C, _)), _)
| (TransformKind::Drift, Some((&Axis::T, _)), _)
| (TransformKind::Both, Some((&Axis::C | &Axis::T, _)), _)
| (
TransformKind::Channel | TransformKind::Drift | TransformKind::Both,
_,
false,
) = (&kind, op_czt.first(), xy_dim == 2)
{
transform_done.insert((c, z, t));
transform.map(|transform| (transform, order))
} else {
None
};
#[cfg(feature = "transforms")]
let frame = self.get_cached_frame(
(c % shape.c as isize) as usize,
(z % shape.z as isize) as usize,
(t % shape.t as isize) as usize,
transform_now,
)?;
#[cfg(not(feature = "transforms"))]
let frame = self.get_cached_frame( let frame = self.get_cached_frame(
(c % shape.c as isize) as usize, (c % shape.c as isize) as usize,
(z % shape.z as isize) as usize, (z % shape.z as isize) as usize,
(t % shape.t as isize) as usize, (t % shape.t as isize) as usize,
)?; )?;
// 0, 1 or 2d frame depending on x or y operations
let arr_frame: Array2<T> = frame.as_ref().try_into()?; let arr_frame: Array2<T> = frame.as_ref().try_into()?;
let arr_frame = match xy_dim { let arr_frame = match xy_dim {
0 => { 0 => {
@@ -855,35 +932,28 @@ impl<D: Dimension, R: Reader> View<D, R> {
unreachable!("xy cannot be 3d or more"); unreachable!("xy cannot be 3d or more");
} }
}; };
if let Some((_, op)) = op_czt.first() { // assign arr_frame to output array, apply first of c, z or t operation
match op { match op_czt.first() {
Operation::Max => { Some((_, Operation::Max)) => {
array array
.slice_mut(slice.as_slice()) .slice_mut(slice_out.as_slice())
.zip_mut_with(&arr_frame, |x, y| { .zip_mut_with(&arr_frame, |x, y| {
*x = if *x >= *y { x.clone() } else { y.clone() } *x = if *x >= *y { *x } else { *y }
}); });
} }
Operation::Min => { Some((_, Operation::Min)) => {
array array
.slice_mut(slice.as_slice()) .slice_mut(slice_out.as_slice())
.zip_mut_with(&arr_frame, |x, y| { .zip_mut_with(&arr_frame, |x, y| {
*x = if *x < *y { x.clone() } else { y.clone() } *x = if *x < *y { *x } else { *y }
}); });
} }
Operation::Sum => { Some((_, Operation::Mean | Operation::Sum)) => {
array array
.slice_mut(slice.as_slice()) .slice_mut(slice_out.as_slice())
.zip_mut_with(&arr_frame, |x, y| *x += y.clone()); .zip_mut_with(&arr_frame, |x, y| *x += *y);
} }
Operation::Mean => { _ => array.slice_mut(slice_out.as_slice()).assign(&arr_frame),
array
.slice_mut(slice.as_slice())
.zip_mut_with(&arr_frame, |x, y| *x += y.clone());
}
}
} else {
array.slice_mut(slice.as_slice()).assign(&arr_frame)
} }
} }
let mut out = Some(array); let mut out = Some(array);
@@ -894,23 +964,61 @@ impl<D: Dimension, R: Reader> View<D, R> {
.collect(); .collect();
for (ax, op) in self.operations.iter().skip(op_xy.len() + op_czt.len()) { for (ax, op) in self.operations.iter().skip(op_xy.len() + op_czt.len()) {
if let Some(idx) = ax_out.remove(ax) { if let Some(idx) = ax_out.remove(ax) {
#[cfg(feature = "transforms")]
let mut arr = out.take().unwrap();
#[cfg(not(feature = "transforms"))]
let arr = out.take().unwrap();
#[cfg(feature = "transforms")]
if let (Axis::C, TransformKind::Channel | TransformKind::Both)
| (Axis::T, TransformKind::Drift | TransformKind::Both) = (ax, &kind)
{
let mut slice_transform = vec![
SliceInfoElem::Slice {
start: 0,
end: None,
step: 1
};
ax_out.len()
];
for (c, z, t) in iproduct!(&slice_reader[0], &slice_reader[1], &slice_reader[2])
{
if let Some(transform) = self
.transforms
.get_transform(self.channel_name(c as usize)?, t as usize)?
&& !transform_done.contains(&(c, z, t))
{
for (axis, i) in [&Axis::C, &Axis::Z, &Axis::T].iter().zip([c, z, t]) {
if let Some(axis_pos) = ax_out.get(axis) {
slice_transform[*axis_pos] = SliceInfoElem::Index(i);
}
}
let untransformed = arr.slice(slice_transform.as_slice());
let transformed = transform
.interpolate_par_order(untransformed, order)?
.mapv(|i| T::from_f64(i).unwrap());
arr.slice_mut(slice_transform.as_slice())
.assign(&transformed);
}
transform_done.insert((c, z, t));
}
}
let a = op.operate(arr, idx)?;
let _ = out.insert(a);
for i in ax_out.values_mut() { for i in ax_out.values_mut() {
if *i > idx { if *i > idx {
*i -= 1; *i -= 1;
} }
} }
let arr = out.take().unwrap();
let a = op.operate(arr, idx)?;
let _ = out.insert(a);
} }
} }
let n = if let Some((&ax, op)) = op_czt.first() let n = if let Some((ax, Operation::Mean)) = op_czt.first() {
&& *op == Operation::Mean
{
self.axes self.axes
.iter() .iter()
.zip(self.slice.iter()) .zip(self.slice.iter())
.find(|(a, _)| **a == ax) .find(|(a, _)| *a == ax)
.and_then(|(_, s)| match s { .and_then(|(_, s)| match s {
SliceInfoElem::Slice { start, end, step } => { SliceInfoElem::Slice { start, end, step } => {
end.map(|e| (((e - start).max(0) / step) as usize).max(1)) end.map(|e| (((e - start).max(0) / step) as usize).max(1))
@@ -921,12 +1029,29 @@ impl<D: Dimension, R: Reader> View<D, R> {
} else { } else {
1 1
}; };
#[cfg(not(feature = "transforms"))]
let array = if n == 1 { let array = if n == 1 {
out.take().unwrap() out.take().unwrap()
} else { } else {
let m = T::from_usize(n).unwrap_or_else(|| T::zero()); let m = T::from_usize(n).unwrap_or_else(|| T::zero());
out.take().unwrap().mapv(|x| x / m.clone()) out.take().unwrap().mapv(|x| x / m)
}; };
#[cfg(feature = "transforms")]
let mut array = if n == 1 {
out.take().unwrap()
} else {
let m = T::from_usize(n).unwrap_or_else(|| T::zero());
out.take().unwrap().mapv(|x| x / m)
};
#[cfg(feature = "transforms")]
if transform_done.is_empty() {
let c = slice_reader[0].start;
let t = slice_reader[2].start;
if let Some(transform) = self.transforms.get_transform(self.channel_name(c as usize)?, t as usize)? {
array = transform.interpolate_par_order(&array, order)?.mapv(|i| T::from_f64(i).unwrap());
}
}
ArrayCache::global().insert(key.to_owned(), array.clone()); ArrayCache::global().insert(key.to_owned(), array.clone());
Ok(array) Ok(array)
} }
@@ -957,7 +1082,13 @@ impl<D: Dimension, R: Reader> View<D, R> {
.collect()) .collect())
} }
fn frame_key(&self, c: usize, z: usize, t: usize) -> FrameKeyRef<'_> { fn frame_key<'a>(
&'a self,
c: usize,
z: usize,
t: usize,
#[cfg(feature = "transforms")] transform: Option<(Cow<'a, Transform<IxDyn>>, usize)>,
) -> FrameKeyRef<'a> {
FrameKeyRef { FrameKeyRef {
reader: ReaderKeyRef { reader: ReaderKeyRef {
name: self.reader.reader_name(), name: self.reader.reader_name(),
@@ -969,6 +1100,8 @@ impl<D: Dimension, R: Reader> View<D, R> {
c, c,
z, z,
t, t,
#[cfg(feature = "transforms")]
transform,
} }
} }
@@ -985,14 +1118,37 @@ impl<D: Dimension, R: Reader> View<D, R> {
slice: &self.slice, slice: &self.slice,
axes: &self.axes, axes: &self.axes,
operations: &self.operations, operations: &self.operations,
#[cfg(feature = "transforms")]
transforms: &self.transforms,
} }
} }
fn get_cached_frame(&self, c: usize, z: usize, t: usize) -> Result<Arc<Frame>, Error> { pub(crate) fn get_cached_frame(
let key = self.frame_key(c, z, t); &self,
c: usize,
z: usize,
t: usize,
#[cfg(feature = "transforms")] transform: Option<(Cow<Transform<IxDyn>>, usize)>,
) -> Result<Arc<Frame>, Error> {
let key = self.frame_key(
c,
z,
t,
#[cfg(feature = "transforms")]
transform
.as_ref()
.map(|(transform, order)| (Cow::Borrowed(&**transform), *order)),
);
if let Some(frame) = FrameCache::global().get(&key) { if let Some(frame) = FrameCache::global().get(&key) {
return Ok(frame); 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)?
} else {
Arc::new(self.reader.get_frame(c, z, t)?)
};
#[cfg(not(feature = "transforms"))]
let frame = Arc::new(self.reader.get_frame(c, z, t)?); let frame = Arc::new(self.reader.get_frame(c, z, t)?);
FrameCache::global().insert(key.to_owned(), frame.clone()); FrameCache::global().insert(key.to_owned(), frame.clone());
Ok(frame) Ok(frame)
@@ -1187,7 +1343,7 @@ impl<R: Reader> Item for View<Ix0, R> {
Array1<T>: MinMax<Output = Array0<T>>, Array1<T>: MinMax<Output = Array0<T>>,
Array2<T>: MinMax<Output = Array1<T>>, Array2<T>: MinMax<Output = Array1<T>>,
{ {
Ok(self.as_array()?.first().ok_or(Error::EmptyView)?.clone()) Ok(*self.as_array()?.first().ok_or(Error::EmptyView)?)
} }
} }
@@ -1229,7 +1385,7 @@ to_bytes_vec_impl!(
mod tests { mod tests {
use crate::axes::{Axis, Operation}; use crate::axes::{Axis, Operation};
use crate::cache::{ use crate::cache::{
ArrayCache, ArrayKey, ArrayKeyRef, FileStamp, FrameCache, ReaderKey, ReaderKeyRef, ArrayCache, ArrayKey, ArrayKeyRef, FileStamp, FrameCache, FrameKey, ReaderKey, ReaderKeyRef,
}; };
use crate::error::Error; use crate::error::Error;
use crate::readers::{DynReader, Frame, Reader}; use crate::readers::{DynReader, Frame, Reader};
@@ -1241,6 +1397,8 @@ mod tests {
use postcard::{from_bytes, to_stdvec}; use postcard::{from_bytes, to_stdvec};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "transforms")]
use crate::transforms::Transforms;
fn open(file: &str) -> Result<DynReader, Error> { fn open(file: &str) -> Result<DynReader, Error> {
let path = std::env::current_dir()? let path = std::env::current_dir()?
@@ -1512,9 +1670,8 @@ mod tests {
#[test] #[test]
fn frame_cache() -> Result<(), Error> { fn frame_cache() -> Result<(), Error> {
let cache = FrameCache::new(2); let cache = FrameCache::new(2);
let key = |c: usize, z: usize, t: usize| { let key = |c: usize, z: usize, t: usize| FrameKey {
( reader_key: ReaderKey {
ReaderKey {
name: "test".to_string(), name: "test".to_string(),
path: PathBuf::from("test.tif"), path: PathBuf::from("test.tif"),
series: 0, series: 0,
@@ -1524,7 +1681,8 @@ mod tests {
c, c,
z, z,
t, t,
) #[cfg(feature = "transforms")]
transform: None,
}; };
cache.insert( cache.insert(
key(0, 0, 0), key(0, 0, 0),
@@ -1561,6 +1719,8 @@ mod tests {
slice: vec![SliceInfoElem::Index(index)], slice: vec![SliceInfoElem::Index(index)],
axes: axes.to_vec(), axes: axes.to_vec(),
operations: vec![], operations: vec![],
#[cfg(feature = "transforms")]
transforms: Transforms::default(),
}; };
let k0 = key("u16", 0, &[Axis::T]); let k0 = key("u16", 0, &[Axis::T]);
let k1 = key("u16", 1, &[Axis::T]); let k1 = key("u16", 1, &[Axis::T]);
@@ -1587,6 +1747,8 @@ mod tests {
slice: &[SliceInfoElem::Index(0)], slice: &[SliceInfoElem::Index(0)],
axes: &[Axis::T], axes: &[Axis::T],
operations: &ops, operations: &ops,
#[cfg(feature = "transforms")]
transforms: &Transforms::default(),
}; };
assert!(cache.get::<f64, _>(&borrowed).is_none()); assert!(cache.get::<f64, _>(&borrowed).is_none());
assert!(cache.get::<u16, _>(&borrowed).is_some()); assert!(cache.get::<u16, _>(&borrowed).is_some());
@@ -1602,12 +1764,37 @@ mod tests {
let (c, z, t) = (shape[0] - 1, shape[1] - 1, shape[2] - 1); let (c, z, t) = (shape[0] - 1, shape[1] - 1, shape[2] - 1);
let cache = FrameCache::global(); let cache = FrameCache::global();
let a: Array5<usize> = view.clone().try_into()?; let a: Array5<usize> = view.clone().try_into()?;
assert!(cache.get(&view.frame_key(c, z, t)).is_some()); assert!(
cache
.get(&view.frame_key(
c,
z,
t,
#[cfg(feature = "transforms")]
None
))
.is_some()
);
let b: Array5<usize> = view.clone().try_into()?; let b: Array5<usize> = view.clone().try_into()?;
assert_eq!(a, b); assert_eq!(a, b);
// a fresh reader on the same file shares the same cache key // a fresh reader on the same file shares the same cache key
let view2 = open(file)?.view(); let view2 = open(file)?.view();
assert_eq!(view.frame_key(c, z, t), view2.frame_key(c, z, t)); assert_eq!(
view.frame_key(
c,
z,
t,
#[cfg(feature = "transforms")]
None
),
view2.frame_key(
c,
z,
t,
#[cfg(feature = "transforms")]
None
)
);
let c2: Array5<usize> = view2.clone().try_into()?; let c2: Array5<usize> = view2.clone().try_into()?;
assert_eq!(a, c2); assert_eq!(a, c2);
Ok(()) Ok(())