From 3c14168878e05f2555d827bc0fba36c019a0f78f Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Sun, 4 Jan 2026 13:59:57 +0100 Subject: [PATCH] - implement custom error types - less restrictive dependency versions - some extra features and bugfixes for movie writing - make python tests work again --- Cargo.toml | 57 +++++------ README.md | 3 - build.rs | 67 +++++++++++-- py/ndbioimage/__init__.py | 4 + pyproject.toml | 1 + src/axes.rs | 57 ++++++----- src/bioformats.rs | 29 +++--- src/colors.rs | 6 +- src/error.rs | 65 +++++++++++++ src/lib.rs | 49 +++++----- src/main.rs | 83 ++++++++++++++-- src/metadata.rs | 28 +++--- src/movie.rs | 105 +++++++++++++-------- src/py.rs | 193 ++++++++++++++++++++++++++++---------- src/reader.rs | 51 +++++----- src/stats.rs | 50 +++++----- src/tiff.rs | 24 ++--- src/view.rs | 114 ++++++++++------------ tests/test_slicing.py | 2 +- 19 files changed, 655 insertions(+), 333 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 7332247..39f10b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ndbioimage" -version = "2025.8.0" +version = "2026.1.0" edition = "2024" rust-version = "1.85.1" authors = ["Wim Pomp "] @@ -8,6 +8,7 @@ license = "MIT" description = "Read bio image formats using the bio-formats java package." homepage = "https://github.com/wimpomp/ndbioimage/tree/rs" repository = "https://github.com/wimpomp/ndbioimage/tree/rs" +documentation = "https://docs.rs/ndbioimage" readme = "README.md" keywords = ["bioformats", "imread", "ndarray", "metadata"] categories = ["multimedia::images", "science"] @@ -19,39 +20,38 @@ name = "ndbioimage" crate-type = ["cdylib", "rlib"] [dependencies] -anyhow = { version = "1.0.99", features = ["backtrace"] } -clap = { version = "4.5.45", features = ["derive"] } -ffmpeg-sidecar = { version = "2.1.0", optional = true } -itertools = "0.14.0" -indexmap = { version = "2.0.0", features = ["serde"] } -indicatif = { version = "0.18.0", features = ["rayon"], optional = true } -j4rs = "0.22.0" -ndarray = { version = "0.16.1", features = ["serde"] } -num = "0.4.3" -numpy = { version = "0.25.0", optional = true } -ordered-float = "5.0.0" -rayon = { version = "1.11.0", optional = true } -serde = { version = "1.0.219", features = ["rc"] } -serde_json = { version = "1.0.143", optional = true } -serde_with = "3.12.0" -tiffwrite = { version = "2025.5.0", optional = true} -thread_local = "1.1.9" -ome-metadata = "0.2.2" -lazy_static = "1.5.0" +clap = { version = "4", features = ["derive"] } +ffmpeg-sidecar = { version = "2", optional = true } +itertools = "0.14" +indexmap = { version = "2", features = ["serde"] } +indicatif = { version = "0.18", features = ["rayon"], optional = true } +j4rs = "0.24" +ndarray = { version = "0.17", features = ["serde"] } +num = "0.4" +numpy = { version = "0.27", optional = true } +ordered-float = "5" +rayon = { version = "1", optional = true } +serde = { version = "1", features = ["rc"] } +serde_json = { version = "1", optional = true } +serde_with = "3" +tiffwrite = { version = "2025.12.0", optional = true} +thread_local = "1" +ome-metadata = "0.3" +lazy_static = "1" +thiserror = "2" [dependencies.pyo3] -version = "0.25.1" +version = "0.27" features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow"] optional = true [dev-dependencies] -rayon = "1.10.0" +rayon = "1" [build-dependencies] -anyhow = "1.0.99" -j4rs = "0.22.0" -ffmpeg-sidecar = "2.1.0" -retry = "2.1.0" +j4rs = "0.24" +ffmpeg-sidecar = "2" +retry = "2" [features] # Enables formats for which code in bioformats with a GPL license is needed @@ -61,4 +61,7 @@ python = ["dep:pyo3", "dep:numpy", "dep:serde_json"] # Enables writing as tiff tiff = ["dep:tiffwrite", "dep:indicatif", "dep:rayon"] # Enables writing as mp4 using ffmpeg -movie = ["dep:ffmpeg-sidecar"] \ No newline at end of file +movie = ["dep:ffmpeg-sidecar"] + +[package.metadata.docs.rs] +features = ["gpl-formats", "tiff", "movie"] \ No newline at end of file diff --git a/README.md b/README.md index f8080f0..0d6d5e4 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,3 @@ let array = view.as_array::()? ```ndbioimage image```: show metadata about image ```ndbioimage image -w {name}.tif -r```: copy image into image.tif (replacing {name} with image), while registering channels ```ndbioimage image -w image.mp4 -C cyan lime red``` copy image into image.mp4 (z will be max projected), make channel colors cyan lime and red - -# TODO -- more image formats diff --git a/build.rs b/build.rs index 364ed0f..1a8617f 100644 --- a/build.rs +++ b/build.rs @@ -1,8 +1,16 @@ #[cfg(not(feature = "python"))] use j4rs::{JvmBuilder, MavenArtifact, MavenArtifactRepo, MavenSettings, errors::J4RsError}; - #[cfg(not(feature = "python"))] use retry::{delay, delay::Exponential, retry}; +use std::error::Error; +#[cfg(not(feature = "python"))] +use std::fmt::Display; +#[cfg(not(feature = "python"))] +use std::fmt::Formatter; +#[cfg(not(feature = "python"))] +use std::path::PathBuf; +#[cfg(not(feature = "python"))] +use std::{env, fs}; #[cfg(feature = "python")] use j4rs::Jvm; @@ -10,7 +18,23 @@ use j4rs::Jvm; #[cfg(feature = "movie")] use ffmpeg_sidecar::download::auto_download; -fn main() -> anyhow::Result<()> { +#[cfg(not(feature = "python"))] +#[derive(Clone, Debug)] +enum BuildError { + BioFormatsNotDownloaded, +} + +#[cfg(not(feature = "python"))] +impl Display for BuildError { + fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> { + write!(fmt, "Bioformats package not downloaded") + } +} + +#[cfg(not(feature = "python"))] +impl Error for BuildError {} + +fn main() -> Result<(), Box> { println!("cargo::rerun-if-changed=build.rs"); if std::env::var("DOCS_RS").is_err() { @@ -18,10 +42,16 @@ fn main() -> anyhow::Result<()> { auto_download()?; #[cfg(not(feature = "python"))] - retry( - Exponential::from_millis(1000).map(delay::jitter).take(4), - deploy_java_artifacts, - )?; + { + retry( + Exponential::from_millis(1000).map(delay::jitter).take(4), + deploy_java_artifacts, + )?; + let path = default_jassets_path()?; + if !path.join("bioformats_package-8.3.0.jar").exists() { + Err(BuildError::BioFormatsNotDownloaded)?; + } + } #[cfg(feature = "python")] { @@ -58,6 +88,31 @@ fn main() -> anyhow::Result<()> { Ok(()) } +#[cfg(not(feature = "python"))] +fn default_jassets_path() -> Result { + let is_build_script = env::var("OUT_DIR").is_ok(); + + let mut start_path = if is_build_script { + PathBuf::from(env::var("OUT_DIR")?) + } else { + env::current_exe()? + }; + start_path = fs::canonicalize(start_path)?; + + while start_path.pop() { + for entry in std::fs::read_dir(&start_path)? { + let path = entry?.path(); + if path.file_name().map(|x| x == "jassets").unwrap_or(false) { + return Ok(path); + } + } + } + + Err(J4RsError::GeneralError( + "Can not find jassets directory".to_owned(), + )) +} + #[cfg(not(feature = "python"))] fn deploy_java_artifacts() -> Result<(), J4RsError> { let jvm = JvmBuilder::new() diff --git a/py/ndbioimage/__init__.py b/py/ndbioimage/__init__.py index 37d9fc4..a110188 100755 --- a/py/ndbioimage/__init__.py +++ b/py/ndbioimage/__init__.py @@ -45,6 +45,10 @@ if not list((Path(__file__).parent / "jassets").glob("bioformats*.jar")): rs.download_bioformats(True) +class ReaderNotFoundError(Exception): + pass + + class TransformTiff(IJTiffParallel): """transform frames in a parallel process to speed up saving""" diff --git a/pyproject.toml b/pyproject.toml index 694914a..7c4ee75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ python-source = "py" features = ["pyo3/extension-module", "python", "gpl-formats"] module-name = "ndbioimage.ndbioimage_rs" exclude = ["py/ndbioimage/jassets/*", "py/ndbioimage/deps/*"] +strip = true [tool.isort] line_length = 119 diff --git a/src/axes.rs b/src/axes.rs index ab48b97..18db6fe 100644 --- a/src/axes.rs +++ b/src/axes.rs @@ -1,5 +1,5 @@ +use crate::error::Error; use crate::stats::MinMax; -use anyhow::{Error, Result, anyhow}; use ndarray::{Array, Dimension, Ix2, SliceInfo, SliceInfoElem}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::{DeserializeAs, SerializeAs}; @@ -13,10 +13,15 @@ pub trait Ax { fn n(&self) -> usize; /// the indices of axes in self.axes, which always has all of CZTYX - fn pos(&self, axes: &[Axis], slice: &[SliceInfoElem]) -> Result; + fn pos(&self, axes: &[Axis], slice: &[SliceInfoElem]) -> Result; /// the indices of axes in self.axes, which always has all of CZTYX, but skip axes with an operation - fn pos_op(&self, axes: &[Axis], slice: &[SliceInfoElem], op_axes: &[Axis]) -> Result; + fn pos_op( + &self, + axes: &[Axis], + slice: &[SliceInfoElem], + op_axes: &[Axis], + ) -> Result; } /// Enum for CZTYX axes or a new axis @@ -39,7 +44,7 @@ impl Hash for Axis { impl FromStr for Axis { type Err = Error; - fn from_str(s: &str) -> std::result::Result { + fn from_str(s: &str) -> Result { match s.to_uppercase().as_str() { "C" => Ok(Axis::C), "Z" => Ok(Axis::Z), @@ -47,7 +52,7 @@ impl FromStr for Axis { "Y" => Ok(Axis::Y), "X" => Ok(Axis::X), "NEW" => Ok(Axis::New), - _ => Err(anyhow!("invalid axis: {}", s)), + _ => Err(Error::InvalidAxis(s.to_string())), } } } @@ -71,18 +76,23 @@ impl Ax for Axis { *self as usize } - fn pos(&self, axes: &[Axis], _slice: &[SliceInfoElem]) -> Result { + fn pos(&self, axes: &[Axis], _slice: &[SliceInfoElem]) -> Result { if let Some(pos) = axes.iter().position(|a| a == self) { Ok(pos) } else { - Err(Error::msg(format!( - "Axis {:?} not found in axes {:?}", - self, axes - ))) + Err(Error::AxisNotFound( + format!("{:?}", self), + format!("{:?}", axes), + )) } } - fn pos_op(&self, axes: &[Axis], _slice: &[SliceInfoElem], _op_axes: &[Axis]) -> Result { + fn pos_op( + &self, + axes: &[Axis], + _slice: &[SliceInfoElem], + _op_axes: &[Axis], + ) -> Result { self.pos(axes, _slice) } } @@ -92,7 +102,7 @@ impl Ax for usize { *self } - fn pos(&self, _axes: &[Axis], slice: &[SliceInfoElem]) -> Result { + fn pos(&self, _axes: &[Axis], slice: &[SliceInfoElem]) -> Result { let idx: Vec<_> = slice .iter() .enumerate() @@ -101,7 +111,12 @@ impl Ax for usize { Ok(idx[*self]) } - fn pos_op(&self, axes: &[Axis], slice: &[SliceInfoElem], op_axes: &[Axis]) -> Result { + fn pos_op( + &self, + axes: &[Axis], + slice: &[SliceInfoElem], + op_axes: &[Axis], + ) -> Result { let idx: Vec<_> = axes .iter() .zip(slice.iter()) @@ -132,7 +147,7 @@ impl Operation { &self, array: Array, axis: usize, - ) -> Result< as MinMax>::Output> + ) -> Result< as MinMax>::Output, Error> where D: Dimension, Array: MinMax, @@ -154,8 +169,11 @@ impl PartialEq for Axis { pub(crate) fn slice_info( info: &[SliceInfoElem], -) -> Result> { - Ok(info.try_into()?) +) -> Result, Error> { + match info.try_into() { + Ok(slice) => Ok(slice), + Err(err) => Err(Error::TryInto(err.to_string())), + } } #[derive(Serialize, Deserialize)] @@ -171,10 +189,7 @@ pub(crate) enum SliceInfoElemDef { } impl SerializeAs for SliceInfoElemDef { - fn serialize_as( - source: &SliceInfoElem, - serializer: S, - ) -> std::result::Result + fn serialize_as(source: &SliceInfoElem, serializer: S) -> Result where S: Serializer, { @@ -183,7 +198,7 @@ impl SerializeAs for SliceInfoElemDef { } impl<'de> DeserializeAs<'de, SliceInfoElem> for SliceInfoElemDef { - fn deserialize_as(deserializer: D) -> std::result::Result + fn deserialize_as(deserializer: D) -> Result where D: Deserializer<'de>, { diff --git a/src/bioformats.rs b/src/bioformats.rs index 44442c6..ffc00e0 100644 --- a/src/bioformats.rs +++ b/src/bioformats.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use crate::error::Error; use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder}; use std::cell::OnceCell; use std::rc::Rc; @@ -12,7 +12,7 @@ fn jvm() -> Rc { JVM.with(|cell| { cell.get_or_init(move || { #[cfg(feature = "python")] - let path = crate::py::ndbioimage_file().unwrap(); + let path = crate::py::ndbioimage_file(); #[cfg(not(feature = "python"))] let path = std::env::current_exe() @@ -45,13 +45,12 @@ fn jvm() -> Rc { }) } -pub fn download_bioformats(gpl_formats: bool) -> Result<()> { +pub fn download_bioformats(gpl_formats: bool) -> Result<(), Error> { #[cfg(feature = "python")] - let path = crate::py::ndbioimage_file()?; + let path = crate::py::ndbioimage_file(); #[cfg(not(feature = "python"))] - let path = std::env::current_exe() - .unwrap() + let path = std::env::current_exe()? .parent() .unwrap() .to_path_buf(); @@ -82,8 +81,8 @@ pub fn download_bioformats(gpl_formats: bool) -> Result<()> { } macro_rules! method_return { - ($R:ty$(|c)?) => { Result<$R> }; - () => { Result<()> }; + ($R:ty$(|c)?) => { Result<$R, Error> }; + () => { Result<(), Error> }; } macro_rules! method_arg { @@ -139,7 +138,7 @@ pub struct DebugTools; impl DebugTools { /// set debug root level: ERROR, DEBUG, TRACE, INFO, OFF - pub fn set_root_level(level: &str) -> Result<()> { + pub fn set_root_level(level: &str) -> Result<(), Error> { jvm().invoke_static( "loci.common.DebugTools", "setRootLevel", @@ -153,7 +152,7 @@ impl DebugTools { pub(crate) struct ChannelSeparator(Instance); impl ChannelSeparator { - pub(crate) fn new(image_reader: &ImageReader) -> Result { + pub(crate) fn new(image_reader: &ImageReader) -> Result { let jvm = jvm(); let channel_separator = jvm.create_instance( "loci.formats.ChannelSeparator", @@ -162,7 +161,7 @@ impl ChannelSeparator { Ok(ChannelSeparator(channel_separator)) } - pub(crate) fn open_bytes(&self, index: i32) -> Result> { + pub(crate) fn open_bytes(&self, index: i32) -> Result, Error> { Ok(transmute_vec(self.open_bi8(index)?)) } @@ -180,16 +179,16 @@ impl Drop for ImageReader { } impl ImageReader { - pub(crate) fn new() -> Result { + pub(crate) fn new() -> Result { let reader = jvm().create_instance("loci.formats.ImageReader", InvocationArg::empty())?; Ok(ImageReader(reader)) } - pub(crate) fn open_bytes(&self, index: i32) -> Result> { + pub(crate) fn open_bytes(&self, index: i32) -> Result, Error> { Ok(transmute_vec(self.open_bi8(index)?)) } - pub(crate) fn ome_xml(&self) -> Result { + pub(crate) fn ome_xml(&self) -> Result { let mds = self.get_metadata_store()?; Ok(jvm() .chain(&mds)? @@ -224,7 +223,7 @@ impl ImageReader { pub(crate) struct MetadataTools(Instance); impl MetadataTools { - pub(crate) fn new() -> Result { + pub(crate) fn new() -> Result { let meta_data_tools = jvm().create_instance("loci.formats.MetadataTools", InvocationArg::empty())?; Ok(MetadataTools(meta_data_tools)) diff --git a/src/colors.rs b/src/colors.rs index 0cfbe2e..d133982 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,4 +1,4 @@ -use anyhow::{Error, Result}; +use crate::error::Error; use lazy_static::lazy_static; use std::collections::HashMap; use std::fmt::Display; @@ -177,12 +177,12 @@ pub struct Color { impl FromStr for Color { type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { let s = if !s.starts_with("#") { if let Some(s) = COLORS.get(s) { s } else { - return Err(Error::msg(format!("invalid color: {}", s))); + return Err(Error::InvalidColor(s.to_string())); } } else { s diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..dcd5945 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,65 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + IO(#[from] std::io::Error), + #[error(transparent)] + Shape(#[from] ndarray::ShapeError), + #[error(transparent)] + J4rs(#[from] j4rs::errors::J4RsError), + #[error(transparent)] + Infallible(#[from] std::convert::Infallible), + #[error(transparent)] + ParseIntError(#[from] std::num::ParseIntError), + #[error(transparent)] + Ome(#[from] ome_metadata::error::Error), + #[cfg(feature = "tiff")] + #[error(transparent)] + TemplateError(#[from] indicatif::style::TemplateError), + #[cfg(feature = "tiff")] + #[error(transparent)] + TiffWrite(#[from] tiffwrite::error::Error), + #[error("invalid axis: {0}")] + InvalidAxis(String), + #[error("axis {0} not found in axes {1}")] + AxisNotFound(String, String), + #[error("conversion error: {0}")] + TryInto(String), + #[error("file already exists {0}")] + FileAlreadyExists(String), + #[error("could not download ffmpeg: {0}")] + Ffmpeg(String), + #[error("index {0} out of bounds {1}")] + OutOfBounds(isize, isize), + #[error("axis {0} has length {1}, but was not included")] + OutOfBoundsAxis(String, usize), + #[error("dimensionality mismatch: {0} != {0}")] + DimensionalityMismatch(usize, usize), + #[error("axis {0}: {1} is already operated on!")] + AxisAlreadyOperated(usize, String), + #[error("not enough free dimensions")] + NotEnoughFreeDimensions, + #[error("cannot cast {0} to {1}")] + Cast(String, String), + #[error("empty view")] + EmptyView, + #[error("invalid color: {0}")] + InvalidColor(String), + #[error("no image or pixels found")] + NoImageOrPixels, + #[error("invalid attenuation value: {0}")] + InvalidAttenuation(String), + #[error("not a valid file name")] + InvalidFileName, + #[error("unknown pixel type {0}")] + UnknownPixelType(String), + #[error("no mean")] + NoMean, + #[error("tiff is locked")] + TiffLock, + #[error("not implemented: {0}")] + NotImplemented(String), + #[error("cannot parse: {0}")] + Parse(String), +} diff --git a/src/lib.rs b/src/lib.rs index 2ce9f52..66d2d3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + mod bioformats; pub mod axes; @@ -9,6 +11,7 @@ pub mod stats; pub mod view; pub mod colors; +pub mod error; #[cfg(feature = "movie")] pub mod movie; #[cfg(feature = "tiff")] @@ -19,15 +22,15 @@ pub use bioformats::download_bioformats; #[cfg(test)] mod tests { use crate::axes::Axis; + use crate::error::Error; use crate::reader::{Frame, Reader}; use crate::stats::MinMax; use crate::view::Item; - use anyhow::Result; use ndarray::{Array, Array4, Array5, NewAxis}; use ndarray::{Array2, s}; use rayon::prelude::*; - fn open(file: &str) -> Result { + fn open(file: &str) -> Result { let path = std::env::current_dir()? .join("tests") .join("files") @@ -35,7 +38,7 @@ mod tests { Reader::new(&path, 0) } - fn get_pixel_type(file: &str) -> Result { + fn get_pixel_type(file: &str) -> Result { let reader = open(file)?; Ok(format!( "file: {}, pixel type: {:?}", @@ -43,13 +46,13 @@ mod tests { )) } - fn get_frame(file: &str) -> Result { + fn get_frame(file: &str) -> Result { let reader = open(file)?; reader.get_frame(0, 0, 0) } #[test] - fn read_ser() -> Result<()> { + fn read_ser() -> Result<(), Error> { let file = "Experiment-2029.czi"; let reader = open(file)?; println!("size: {}, {}", reader.size_y, reader.size_y); @@ -63,7 +66,7 @@ mod tests { } #[test] - fn read_par() -> Result<()> { + fn read_par() -> Result<(), Error> { let files = vec!["Experiment-2029.czi", "test.tif"]; let pixel_type = files .into_par_iter() @@ -74,7 +77,7 @@ mod tests { } #[test] - fn read_frame_par() -> Result<()> { + fn read_frame_par() -> Result<(), Error> { let files = vec!["Experiment-2029.czi", "test.tif"]; let frames = files .into_par_iter() @@ -85,7 +88,7 @@ mod tests { } #[test] - fn read_sequence() -> Result<()> { + fn read_sequence() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; println!("reader: {:?}", reader); @@ -97,7 +100,7 @@ mod tests { } #[test] - fn read_sequence1() -> Result<()> { + fn read_sequence1() -> Result<(), Error> { let file = "4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif"; let reader = open(file)?; println!("reader: {:?}", reader); @@ -105,7 +108,7 @@ mod tests { } #[test] - fn ome_xml() -> Result<()> { + fn ome_xml() -> Result<(), Error> { let file = "Experiment-2029.czi"; let reader = open(file)?; let xml = reader.get_ome_xml()?; @@ -114,7 +117,7 @@ mod tests { } #[test] - fn view() -> Result<()> { + fn view() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; let view = reader.view(); @@ -127,7 +130,7 @@ mod tests { } #[test] - fn view_shape() -> Result<()> { + fn view_shape() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; let view = reader.view(); @@ -138,7 +141,7 @@ mod tests { } #[test] - fn view_new_axis() -> Result<()> { + fn view_new_axis() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; let view = reader.view(); @@ -153,7 +156,7 @@ mod tests { } #[test] - fn view_permute_axes() -> Result<()> { + fn view_permute_axes() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; let view = reader.view(); @@ -180,7 +183,7 @@ mod tests { ($($name:ident: $b:expr $(,)?)*) => { $( #[test] - fn $name() -> Result<()> { + fn $name() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; let view = reader.view(); @@ -208,7 +211,7 @@ mod tests { ($($name:ident: $b:expr $(,)?)*) => { $( #[test] - fn $name() -> Result<()> { + fn $name() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; let view = reader.view(); @@ -254,7 +257,7 @@ mod tests { } #[test] - fn dyn_view() -> Result<()> { + fn dyn_view() -> Result<(), Error> { let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; let reader = open(file)?; let a = reader.view().into_dyn(); @@ -266,7 +269,7 @@ mod tests { } #[test] - fn item() -> Result<()> { + fn item() -> Result<(), Error> { let file = "1xp53-01-AP1.czi"; let reader = open(file)?; let view = reader.view(); @@ -278,7 +281,7 @@ mod tests { } #[test] - fn slice_cztyx() -> Result<()> { + fn slice_cztyx() -> Result<(), Error> { let file = "1xp53-01-AP1.czi"; let reader = open(file)?; let view = reader.view().max_proj(Axis::Z)?.into_dyn(); @@ -295,7 +298,7 @@ mod tests { } #[test] - fn reset_axes() -> Result<()> { + fn reset_axes() -> Result<(), Error> { let file = "1xp53-01-AP1.czi"; let reader = open(file)?; let view = reader.view().max_proj(Axis::Z)?; @@ -307,7 +310,7 @@ mod tests { } #[test] - fn reset_axes2() -> Result<()> { + fn reset_axes2() -> Result<(), Error> { let file = "Experiment-2029.czi"; let reader = open(file)?; let view = reader.view().squeeze()?; @@ -317,7 +320,7 @@ mod tests { } #[test] - fn reset_axes3() -> Result<()> { + fn reset_axes3() -> Result<(), Error> { let file = "Experiment-2029.czi"; let reader = open(file)?; let view4 = reader.view().squeeze()?; @@ -345,7 +348,7 @@ mod tests { } #[test] - fn max() -> Result<()> { + fn max() -> Result<(), Error> { let file = "Experiment-2029.czi"; let reader = open(file)?; let view = reader.view(); diff --git a/src/main.rs b/src/main.rs index 50d0bdb..76823a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ -use anyhow::Result; use clap::{Parser, Subcommand}; #[cfg(feature = "movie")] +use ndarray::SliceInfoElem; +use ndbioimage::error::Error; +#[cfg(feature = "movie")] use ndbioimage::movie::MovieOptions; use ndbioimage::reader::split_path_and_series; #[cfg(feature = "tiff")] @@ -37,16 +39,26 @@ enum Commands { Movie { #[arg(value_name = "FILE", num_args(1..))] file: Vec, - #[arg(short, long, value_name = "Velocity", default_value = "3.6")] + #[arg(short, long, value_name = "VELOCITY", default_value = "3.6")] velocity: f64, - #[arg(short, long, value_name = "BRIGHTNESS")] + #[arg(short, long, value_name = "BRIGHTNESS", num_args(1..))] brightness: Vec, #[arg(short, long, value_name = "SCALE", default_value = "1.0")] scale: f64, - #[arg(short, long, value_name = "COLOR", num_args(1..))] + #[arg(short = 'C', long, value_name = "COLOR", num_args(1..))] colors: Vec, #[arg(short, long, value_name = "OVERWRITE")] overwrite: bool, + #[arg(short, long, value_name = "REGISTER")] + register: bool, + #[arg(short, long, value_name = "CHANNEL")] + channel: Option, + #[arg(short, long, value_name = "ZSLICE")] + zslice: Option, + #[arg(short, long, value_name = "TIME")] + time: Option, + #[arg(short, long, value_name = "NO-SCALE-BRIGHTNESS")] + no_scaling: bool, }, /// Download the BioFormats jar into the correct folder DownloadBioFormats { @@ -55,7 +67,43 @@ enum Commands { }, } -pub(crate) fn main() -> Result<()> { +#[cfg(feature = "movie")] +fn parse_slice(s: &str) -> Result { + let mut t = s + .trim() + .replace("..", ":") + .split(":") + .map(|i| i.parse().ok()) + .collect::>>(); + if t.len() > 3 { + return Err(Error::Parse(s.to_string())); + } + while t.len() < 3 { + t.push(None); + } + match t[..] { + [Some(start), None, None] => Ok(SliceInfoElem::Index(start)), + [Some(start), end, None] => Ok(SliceInfoElem::Slice { + start, + end, + step: 1, + }), + [Some(start), end, Some(step)] => Ok(SliceInfoElem::Slice { start, end, step }), + [None, end, None] => Ok(SliceInfoElem::Slice { + start: 0, + end, + step: 1, + }), + [None, end, Some(step)] => Ok(SliceInfoElem::Slice { + start: 0, + end, + step, + }), + _ => Err(Error::Parse(s.to_string())), + } +} + +pub(crate) fn main() -> Result<(), Error> { let cli = Cli::parse(); match &cli.command { Commands::Info { file } => { @@ -87,6 +135,11 @@ pub(crate) fn main() -> Result<()> { scale, colors, overwrite, + register, + channel, + zslice, + time, + no_scaling, } => { let options = MovieOptions::new( *speed, @@ -94,11 +147,29 @@ pub(crate) fn main() -> Result<()> { *scale, colors.to_vec(), *overwrite, + *register, + *no_scaling, )?; for f in file { let (path, series) = split_path_and_series(f)?; let view = View::from_path(path, series.unwrap_or(0))?; - view.save_as_movie(f.with_extension("mp4"), &options)?; + let mut s = [SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }; 5]; + if let Some(channel) = channel { + s[0] = SliceInfoElem::Index(*channel); + }; + if let Some(zslice) = zslice { + s[1] = parse_slice(zslice)?; + } + if let Some(time) = time { + s[2] = parse_slice(time)?; + } + view.into_dyn() + .slice(s.as_slice())? + .save_as_movie(f.with_extension("mp4"), &options)?; } } Commands::DownloadBioFormats { gpl_formats } => { diff --git a/src/metadata.rs b/src/metadata.rs index 494e13d..6b0fea1 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, anyhow}; +use crate::error::Error; use itertools::Itertools; use ome_metadata::Ome; use ome_metadata::ome::{ @@ -67,7 +67,7 @@ pub trait Metadata { } /// shape of the data along cztyx axes - fn shape(&self) -> Result<(usize, usize, usize, usize, usize)> { + fn shape(&self) -> Result<(usize, usize, usize, usize, usize), Error> { if let Some(pixels) = self.get_pixels() { Ok(( pixels.size_c as usize, @@ -77,12 +77,12 @@ pub trait Metadata { pixels.size_x as usize, )) } else { - Err(anyhow!("No image or pixels found")) + Err(Error::NoImageOrPixels) } } /// pixel size in nm - fn pixel_size(&self) -> Result> { + fn pixel_size(&self) -> Result, Error> { if let Some(pixels) = self.get_pixels() { match (pixels.physical_size_x, pixels.physical_size_y) { (Some(x), Some(y)) => Ok(Some( @@ -114,7 +114,7 @@ pub trait Metadata { } /// distance between planes in z-stack in nm - fn delta_z(&self) -> Result> { + fn delta_z(&self) -> Result, Error> { if let Some(pixels) = self.get_pixels() { if let Some(z) = pixels.physical_size_z { return Ok(Some( @@ -128,7 +128,7 @@ pub trait Metadata { } /// time interval in seconds for time-lapse images - fn time_interval(&self) -> Result> { + fn time_interval(&self) -> Result, Error> { if let Some(pixels) = self.get_pixels() { if let Some(plane) = &pixels.plane { if let Some(t) = plane.iter().map(|p| p.the_t).max() { @@ -157,7 +157,7 @@ pub trait Metadata { } /// exposure time for channel, z=0 and t=0 - fn exposure_time(&self, channel: usize) -> Result> { + fn exposure_time(&self, channel: usize) -> Result, Error> { let c = channel as i32; if let Some(pixels) = self.get_pixels() { if let Some(plane) = &pixels.plane { @@ -192,7 +192,7 @@ pub trait Metadata { } } - fn laser_wavelengths(&self, channel: usize) -> Result> { + fn laser_wavelengths(&self, channel: usize) -> Result, Error> { if let Some(pixels) = self.get_pixels() { if let Some(channel) = pixels.channel.get(channel) { if let Some(w) = channel.excitation_wavelength { @@ -207,7 +207,7 @@ pub trait Metadata { Ok(None) } - fn laser_powers(&self, channel: usize) -> Result> { + fn laser_powers(&self, channel: usize) -> Result, Error> { if let Some(pixels) = self.get_pixels() { if let Some(channel) = pixels.channel.get(channel) { if let Some(ls) = &channel.light_source_settings { @@ -215,7 +215,7 @@ pub trait Metadata { return if (0. ..=1.).contains(&a) { Ok(Some(1f64 - (a as f64))) } else { - Err(anyhow!("Invalid attenuation value")) + Err(Error::InvalidAttenuation(a.to_string())) }; } } @@ -273,7 +273,7 @@ pub trait Metadata { } } - fn summary(&self) -> Result { + fn summary(&self) -> Result { let size_c = if let Some(pixels) = self.get_pixels() { pixels.channel.len() } else { @@ -291,7 +291,7 @@ pub trait Metadata { } let exposure_time = (0..size_c) .map(|c| self.exposure_time(c)) - .collect::>>()? + .collect::, Error>>()? .into_iter() .flatten() .collect::>(); @@ -333,7 +333,7 @@ pub trait Metadata { } let laser_wavelengths = (0..size_c) .map(|c| self.laser_wavelengths(c)) - .collect::>>()? + .collect::, Error>>()? .into_iter() .flatten() .collect::>(); @@ -345,7 +345,7 @@ pub trait Metadata { } let laser_powers = (0..size_c) .map(|c| self.laser_powers(c)) - .collect::>>()? + .collect::, Error>>()? .into_iter() .flatten() .collect::>(); diff --git a/src/movie.rs b/src/movie.rs index b85fbc0..753009c 100644 --- a/src/movie.rs +++ b/src/movie.rs @@ -1,7 +1,8 @@ use crate::axes::Axis; use crate::colors::Color; +use crate::error::Error; +use crate::reader::PixelType; use crate::view::View; -use anyhow::{Result, anyhow}; use ffmpeg_sidecar::command::FfmpegCommand; use ffmpeg_sidecar::download::auto_download; use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel}; @@ -18,6 +19,8 @@ pub struct MovieOptions { scale: f64, colors: Option>>, overwrite: bool, + register: bool, + no_scaling: bool, } impl Default for MovieOptions { @@ -28,6 +31,8 @@ impl Default for MovieOptions { scale: 1.0, colors: None, overwrite: false, + register: false, + no_scaling: false, } } } @@ -39,14 +44,16 @@ impl MovieOptions { scale: f64, colors: Vec, overwrite: bool, - ) -> Result { + register: bool, + no_scaling: bool, + ) -> Result { let colors = if colors.is_empty() { None } else { let colors = colors .iter() .map(|c| c.parse::()) - .collect::>>()?; + .collect::, Error>>()?; Some(colors.into_iter().map(|c| c.to_rgb()).collect()) }; Ok(Self { @@ -55,6 +62,8 @@ impl MovieOptions { scale, colors, overwrite, + register, + no_scaling, }) } @@ -70,11 +79,11 @@ impl MovieOptions { self.scale = scale; } - pub fn set_colors(&mut self, colors: &[String]) -> Result<()> { + pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> { let colors = colors .iter() .map(|c| c.parse::()) - .collect::>>()?; + .collect::, Error>>()?; self.colors = Some(colors.into_iter().map(|c| c.to_rgb()).collect()); Ok(()) } @@ -84,7 +93,7 @@ impl MovieOptions { } } -fn get_ab(tyx: View) -> Result<(f64, f64)> { +fn get_ab(tyx: View) -> Result<(f64, f64), Error> { let s = tyx .as_array::()? .iter() @@ -107,14 +116,14 @@ fn get_ab(tyx: View) -> Result<(f64, f64)> { b = s[n - 1]; } if a == b { - a = 1.0; + a = 0.0; b = 1.0; } Ok((a, b)) } fn cframe(frame: Array2, color: &[u8], a: f64, b: f64) -> Array3 { - let frame = (frame - a) / (b - a); + let frame = ((frame - a) / (b - a)).clamp(0.0, 1.0); let color = color .iter() .map(|&c| (c as f64) / 255.0) @@ -124,23 +133,26 @@ fn cframe(frame: Array2, color: &[u8], a: f64, b: f64) -> Array3 { .map(|&c| (c * &frame).to_owned()) .collect::>>(); let view = frame.iter().map(|c| c.view()).collect::>(); - stack(ndarray::Axis(0), &view).unwrap() + stack(ndarray::Axis(2), &view).unwrap() } impl View where D: Dimension, { - pub fn save_as_movie

(&self, path: P, options: &MovieOptions) -> Result<()> + pub fn save_as_movie

(&self, path: P, options: &MovieOptions) -> Result<(), Error> where P: AsRef, { + if options.register { + return Err(Error::NotImplemented("register".to_string())); + } let path = path.as_ref().to_path_buf(); if path.exists() { if options.overwrite { std::fs::remove_file(&path)?; } else { - return Err(anyhow!("File {} already exists", path.display())); + return Err(Error::FileAlreadyExists(path.display().to_string())); } } let view = self.max_proj(Axis::Z)?.reset_axes()?; @@ -150,10 +162,10 @@ where let shape = view.shape(); let size_c = shape[0]; let size_t = shape[2]; - let size_x = shape[3]; - let size_y = shape[4]; - let shape_x = 2 * (((size_x as f64 * scale) / 2.).round() as usize); + let size_y = shape[3]; + let size_x = shape[4]; let shape_y = 2 * (((size_y as f64 * scale) / 2.).round() as usize); + let shape_x = 2 * (((size_x as f64 * scale) / 2.).round() as usize); while brightness.len() < size_c { brightness.push(1.0); @@ -167,15 +179,18 @@ where colors.push(vec![255, 255, 255]); } - auto_download()?; + if let Err(e) = auto_download() { + return Err(Error::Ffmpeg(e.to_string())); + } + let mut movie = FfmpegCommand::new() .args([ "-f", "rawvideo", "-pix_fmt", - "gray", + "rgb24", "-s", - &format!("{}x{}", size_x, size_y), + &format!("{size_x}x{size_y}"), ]) .input("-") .args([ @@ -194,16 +209,33 @@ where .spawn()?; let mut stdin = movie.take_stdin().unwrap(); - let ab = (0..size_c) - .map(|c| match view.slice(s![c, .., .., .., ..]) { - Ok(slice) => get_ab(slice.into_dyn()), - Err(e) => Err(e), - }) - .collect::>>()?; + let ab = if options.no_scaling { + vec![ + match view.pixel_type { + PixelType::I8 => (i8::MIN as f64, i8::MAX as f64), + PixelType::U8 => (u8::MIN as f64, u8::MAX as f64), + PixelType::I16 => (i16::MIN as f64, i16::MAX as f64), + PixelType::U16 => (u16::MIN as f64, u16::MAX as f64), + PixelType::I32 => (i32::MIN as f64, i32::MAX as f64), + PixelType::U32 => (u32::MIN as f64, u32::MAX as f64), + PixelType::I64 => (i64::MIN as f64, i64::MAX as f64), + PixelType::U64 => (u64::MIN as f64, u64::MAX as f64), + _ => (0.0, 1.0), + }; + view.size_c + ] + } else { + (0..size_c) + .map(|c| match view.slice(s![c, .., .., .., ..]) { + Ok(slice) => get_ab(slice.into_dyn()), + Err(e) => Err(e), + }) + .collect::, Error>>()? + }; thread::spawn(move || { for t in 0..size_t { - let mut frame = Array3::::zeros((3, size_y, size_y)); + let mut frame = Array3::::zeros((size_y, size_x, 3)); for c in 0..size_c { frame = frame + cframe( @@ -213,25 +245,20 @@ where ab[c].1 / brightness[c], ); } - let frame = frame.mapv(|i| { - if i < 0.0 { - 0 - } else if i > 1.0 { - 1 - } else { - (255.0 * i).round() as u8 - } - }); + let frame = (frame.clamp(0.0, 1.0) * 255.0).round().mapv(|i| i as u8); let bytes: Vec<_> = frame.flatten().into_iter().collect(); stdin.write_all(&bytes).unwrap(); } }); - movie.iter()?.for_each(|e| match e { - FfmpegEvent::Log(LogLevel::Error, e) => println!("Error: {}", e), - FfmpegEvent::Progress(p) => println!("Progress: {} / 00:00:15", p.time), - _ => {} - }); + movie + .iter() + .map_err(|e| Error::Ffmpeg(e.to_string()))? + .for_each(|e| match e { + FfmpegEvent::Log(LogLevel::Error, e) => println!("Error: {}", e), + FfmpegEvent::Progress(p) => println!("Progress: {} / 00:00:15", p.time), + _ => {} + }); Ok(()) } } @@ -242,7 +269,7 @@ mod tests { use crate::reader::Reader; #[test] - fn movie() -> Result<()> { + fn movie() -> Result<(), Error> { let file = "1xp53-01-AP1.czi"; let path = std::env::current_dir()? .join("tests") diff --git a/src/py.rs b/src/py.rs index 31c8a75..d469e75 100644 --- a/src/py.rs +++ b/src/py.rs @@ -1,15 +1,15 @@ use crate::axes::Axis; use crate::bioformats::download_bioformats; +use crate::error::Error; use crate::metadata::Metadata; use crate::reader::{PixelType, Reader}; use crate::view::{Item, View}; -use anyhow::{Result, anyhow}; use itertools::Itertools; use ndarray::{Ix0, IxDyn, SliceInfoElem}; use numpy::IntoPyArray; use ome_metadata::Ome; use pyo3::IntoPyObjectExt; -use pyo3::exceptions::PyNotImplementedError; +use pyo3::exceptions::{PyNotImplementedError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyEllipsis, PyInt, PyList, PySlice, PySliceMethods, PyString, PyTuple}; use serde::{Deserialize, Serialize}; @@ -17,6 +17,12 @@ use serde_json::{from_str, to_string}; use std::path::PathBuf; use std::sync::Arc; +impl From for PyErr { + fn from(err: crate::error::Error) -> PyErr { + PyErr::new::(err.to_string()) + } +} + #[pyclass(module = "ndbioimage.ndbioimage_rs")] struct ViewConstructor; @@ -36,7 +42,9 @@ impl ViewConstructor { if let Ok(new) = from_str(&state) { Ok(new) } else { - Err(anyhow!("cannot parse state").into()) + Err(PyErr::new::( + "cannot parse state".to_string(), + )) } } } @@ -55,11 +63,24 @@ impl PyView { /// new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64) #[new] #[pyo3(signature = (path, series = 0, dtype = "uint16", axes = "cztyx"))] - fn new(path: Bound<'_, PyAny>, series: usize, dtype: &str, axes: &str) -> PyResult { + fn new<'py>( + py: Python<'py>, + path: Bound<'py, PyAny>, + series: usize, + dtype: &str, + axes: &str, + ) -> PyResult { if path.is_instance_of::() { - Ok(path.downcast_into::()?.extract::()?) + Ok(path.cast_into::()?.extract::()?) } else { - let mut path = PathBuf::from(path.downcast_into::()?.extract::()?); + let builtins = PyModule::import(py, "builtins")?; + let mut path = PathBuf::from( + builtins + .getattr("str")? + .call1((path,))? + .cast_into::()? + .extract::()?, + ); if path.is_dir() { for file in path.read_dir()?.flatten() { let p = file.path(); @@ -72,7 +93,7 @@ impl PyView { let axes = axes .chars() .map(|a| a.to_string().parse()) - .collect::>>()?; + .collect::, Error>>()?; let reader = Reader::new(&path, series)?; let view = View::new_with_axes(Arc::new(reader), axes)?; let dtype = dtype.parse()?; @@ -187,9 +208,9 @@ impl PyView { n: Bound<'py, PyAny>, ) -> PyResult> { let slice: Vec<_> = if n.is_instance_of::() { - n.downcast_into::()?.into_iter().collect() + n.cast_into::()?.into_iter().collect() } else if n.is_instance_of::() { - n.downcast_into::()?.into_iter().collect() + n.cast_into::()?.into_iter().collect() } else { vec![n] }; @@ -198,11 +219,9 @@ impl PyView { let shape = self.view.shape(); for (i, (s, t)) in slice.iter().zip(shape.iter()).enumerate() { if s.is_instance_of::() { - new_slice.push(SliceInfoElem::Index( - s.downcast::()?.extract::()?, - )); + new_slice.push(SliceInfoElem::Index(s.cast::()?.extract::()?)); } else if s.is_instance_of::() { - let u = s.downcast::()?.indices(*t as isize)?; + let u = s.cast::()?.indices(*t as isize)?; new_slice.push(SliceInfoElem::Slice { start: u.start, end: Some(u.stop), @@ -210,20 +229,24 @@ impl PyView { }); } else if s.is_instance_of::() { if ellipsis.is_some() { - return Err(anyhow!("cannot have more than one ellipsis").into()); + return Err(PyErr::new::( + "cannot have more than one ellipsis".to_string(), + )); } let _ = ellipsis.insert(i); } else { - return Err(anyhow!("cannot convert {:?} to slice", s).into()); + return Err(PyErr::new::(format!( + "cannot convert {:?} to slice", + s + ))); } } if new_slice.len() > shape.len() { - return Err(anyhow!( + return Err(PyErr::new::(format!( "got more indices ({}) than dimensions ({})", new_slice.len(), shape.len() - ) - .into()); + ))); } while new_slice.len() < shape.len() { if let Some(i) = ellipsis { @@ -331,15 +354,22 @@ impl PyView { } } - fn __contains__(&self, _item: Bound<'_, PyAny>) -> PyResult { + fn __contains__(&self, _item: Bound) -> PyResult { Err(PyNotImplementedError::new_err("contains not implemented")) } - fn __enter__<'p>(slf: PyRef<'p, Self>, _py: Python<'p>) -> PyResult> { + fn __enter__<'py>(slf: PyRef<'py, Self>) -> PyResult> { Ok(slf) } - fn __exit__(&self) -> PyResult<()> { + #[allow(unused_variables)] + #[pyo3(signature = (exc_type=None, exc_val=None, exc_tb=None))] + fn __exit__( + &self, + exc_type: Option>, + exc_val: Option>, + exc_tb: Option>, + ) -> PyResult<()> { self.close() } @@ -347,7 +377,9 @@ impl PyView { if let Ok(s) = to_string(self) { Ok((ViewConstructor, (s,))) } else { - Err(anyhow!("cannot get state").into()) + Err(PyErr::new::( + "cannot get state".to_string(), + )) } } @@ -549,28 +581,31 @@ impl PyView { /// find the position of an axis #[pyo3(text_signature = "axis: str | int")] - fn get_ax(&self, axis: Bound<'_, PyAny>) -> PyResult { + fn get_ax(&self, axis: Bound) -> PyResult { if axis.is_instance_of::() { let axis = axis - .downcast_into::()? + .cast_into::()? .extract::()? .parse::()?; - Ok(self - .view + self.view .axes() .iter() .position(|a| *a == axis) - .ok_or_else(|| anyhow!("cannot find axis {:?}", axis))?) + .ok_or_else(|| { + PyErr::new::(format!("cannot find axis {:?}", axis)) + }) } else if axis.is_instance_of::() { - Ok(axis.downcast_into::()?.extract::()?) + Ok(axis.cast_into::()?.extract::()?) } else { - Err(anyhow!("cannot convert to axis").into()) + Err(PyErr::new::( + "cannot convert to axis".to_string(), + )) } } /// swap two axes #[pyo3(text_signature = "ax0: str | int, ax1: str | int")] - fn swap_axes(&self, ax0: Bound<'_, PyAny>, ax1: Bound<'_, PyAny>) -> PyResult { + fn swap_axes(&self, ax0: Bound, ax1: Bound) -> PyResult { let ax0 = self.get_ax(ax0)?; let ax1 = self.get_ax(ax1)?; let view = self.view.swap_axes(ax0, ax1)?; @@ -583,7 +618,7 @@ impl PyView { /// permute the order of the axes #[pyo3(signature = (axes = None), text_signature = "axes: list[str | int] = None")] - fn transpose(&self, axes: Option>>) -> PyResult { + fn transpose(&self, axes: Option>>) -> PyResult { let view = if let Some(axes) = axes { let ax = axes .into_iter() @@ -651,12 +686,30 @@ impl PyView { } /// get the maximum overall or along a given axis - #[pyo3(signature = (axis = None), text_signature = "axis: str | int")] + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=0, r#where=true), text_signature = "axis: str | int")] fn max<'py>( &self, py: Python<'py>, axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + initial: Option, + r#where: bool, ) -> PyResult> { + if let Some(i) = initial + && i != 0 + { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } if let Some(axis) = axis { PyView { dtype: self.dtype.clone(), @@ -684,12 +737,30 @@ impl PyView { } /// get the minimum overall or along a given axis - #[pyo3(signature = (axis = None), text_signature = "axis: str | int")] + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=0, r#where=true), text_signature = "axis: str | int")] fn min<'py>( &self, py: Python<'py>, axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + initial: Option, + r#where: bool, ) -> PyResult> { + if let Some(i) = initial + && i != 0 + { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } if let Some(axis) = axis { PyView { dtype: self.dtype.clone(), @@ -716,13 +787,21 @@ impl PyView { } } - /// get the mean overall or along a given axis - #[pyo3(signature = (axis = None), text_signature = "axis: str | int")] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int")] fn mean<'py>( &self, py: Python<'py>, axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + r#where: bool, ) -> PyResult> { + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } if let Some(axis) = axis { let dtype = if let PixelType::F32 = self.dtype { PixelType::F32 @@ -744,12 +823,30 @@ impl PyView { } /// get the sum overall or along a given axis - #[pyo3(signature = (axis = None), text_signature = "axis: str | int")] + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=0, r#where=true), text_signature = "axis: str | int")] fn sum<'py>( &self, py: Python<'py>, axis: Option>, + dtype: Option>, + out: Option>, + keepdims: bool, + initial: Option, + r#where: bool, ) -> PyResult> { + if let Some(i) = initial + && i != 0 + { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } + if dtype.is_some() || out.is_some() || keepdims || !r#where { + Err(Error::NotImplemented( + "arguments beyond axis are not implemented".to_string(), + ))?; + } let dtype = match self.dtype { PixelType::I8 => PixelType::I16, PixelType::U8 => PixelType::U16, @@ -864,29 +961,29 @@ impl PyView { } } -pub(crate) fn ndbioimage_file() -> anyhow::Result { - let file = Python::with_gil(|py| { +pub(crate) fn ndbioimage_file() -> PathBuf { + let file = Python::attach(|py| { py.import("ndbioimage") .unwrap() .filename() .unwrap() .to_string() }); - Ok(PathBuf::from(file)) + PathBuf::from(file) +} + +#[pyfunction] +#[pyo3(name = "download_bioformats")] +fn py_download_bioformats(gpl_formats: bool) -> PyResult<()> { + download_bioformats(gpl_formats)?; + Ok(()) } #[pymodule] #[pyo3(name = "ndbioimage_rs")] -fn ndbioimage_rs(m: &Bound<'_, PyModule>) -> PyResult<()> { +fn ndbioimage_rs(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - - #[pyfn(m)] - #[pyo3(name = "download_bioformats")] - fn py_download_bioformats(gpl_formats: bool) -> PyResult<()> { - download_bioformats(gpl_formats)?; - Ok(()) - } - + m.add_function(wrap_pyfunction!(py_download_bioformats, m)?)?; Ok(()) } diff --git a/src/reader.rs b/src/reader.rs index 89d40e4..1bc8539 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -1,8 +1,8 @@ use crate::axes::Axis; use crate::bioformats; use crate::bioformats::{DebugTools, ImageReader, MetadataTools}; +use crate::error::Error; use crate::view::View; -use anyhow::{Error, Result, anyhow}; use ndarray::{Array2, Ix5, s}; use num::{FromPrimitive, Zero}; use ome_metadata::Ome; @@ -15,16 +15,16 @@ use std::str::FromStr; use std::sync::Arc; use thread_local::ThreadLocal; -pub fn split_path_and_series

(path: P) -> Result<(PathBuf, Option)> +pub fn split_path_and_series

(path: P) -> Result<(PathBuf, Option), Error> where P: Into, { let path = path.into(); let file_name = path .file_name() - .ok_or_else(|| anyhow!("No file name"))? + .ok_or(Error::InvalidFileName)? .to_str() - .ok_or_else(|| anyhow!("No file name"))?; + .ok_or(Error::InvalidFileName)?; if file_name.to_lowercase().starts_with("pos") { if let Some(series) = file_name.get(3..) { if let Ok(series) = series.parse::() { @@ -72,7 +72,7 @@ impl TryFrom for PixelType { 10 => Ok(PixelType::I128), 11 => Ok(PixelType::U128), 12 => Ok(PixelType::F128), - _ => Err(anyhow::anyhow!("Unknown pixel type {}", value)), + _ => Err(Error::UnknownPixelType(value.to_string())), } } } @@ -95,7 +95,7 @@ impl FromStr for PixelType { "int128" | "i128" => Ok(PixelType::I128), "uint128" | "u128" => Ok(PixelType::U128), "extended" | "f128" => Ok(PixelType::F128), - _ => Err(anyhow::anyhow!("Unknown pixel type {}", s)), + _ => Err(Error::UnknownPixelType(s.to_string())), } } } @@ -163,73 +163,73 @@ where let arr = match self { Frame::I8(v) => v.mapv_into_any(|x| { T::from_i8(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::U8(v) => v.mapv_into_any(|x| { T::from_u8(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::I16(v) => v.mapv_into_any(|x| { T::from_i16(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::U16(v) => v.mapv_into_any(|x| { T::from_u16(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::I32(v) => v.mapv_into_any(|x| { T::from_i32(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::U32(v) => v.mapv_into_any(|x| { T::from_u32(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::F32(v) => v.mapv_into_any(|x| { T::from_f32(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::F64(v) | Frame::F128(v) => v.mapv_into_any(|x| { T::from_f64(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::I64(v) => v.mapv_into_any(|x| { T::from_i64(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::U64(v) => v.mapv_into_any(|x| { T::from_u64(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::I128(v) => v.mapv_into_any(|x| { T::from_i128(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), Frame::U128(v) => v.mapv_into_any(|x| { T::from_u128(x).unwrap_or_else(|| { - err = Err(anyhow!("cannot convert {} into {}", x, type_name::())); + err = Err(Error::Cast(x.to_string(), type_name::().to_string())); T::zero() }) }), @@ -305,7 +305,7 @@ impl Debug for Reader { impl Reader { /// Create a new reader for the image file at a path, and open series #. - pub fn new

(path: P, series: usize) -> Result + pub fn new

(path: P, series: usize) -> Result where P: AsRef, { @@ -333,7 +333,7 @@ impl Reader { } /// Get ome metadata as ome structure - pub fn get_ome(&self) -> Result { + pub fn get_ome(&self) -> Result { let mut ome = self.ome_xml()?.parse::()?; if let Some(image) = ome.image.as_ref() { if image.len() > 1 { @@ -344,11 +344,11 @@ impl Reader { } /// Get ome metadata as xml string - pub fn get_ome_xml(&self) -> Result { + pub fn get_ome_xml(&self) -> Result { self.ome_xml() } - fn deinterleave(&self, bytes: Vec, channel: usize) -> Result> { + fn deinterleave(&self, bytes: Vec, channel: usize) -> Result, Error> { let chunk_size = match self.pixel_type { PixelType::I8 => 1, PixelType::U8 => 1, @@ -373,7 +373,8 @@ impl Reader { } /// Retrieve fame at channel c, slize z and time t. - pub fn get_frame(&self, c: usize, z: usize, t: usize) -> Result { + #[allow(clippy::if_same_then_else)] + pub fn get_frame(&self, c: usize, z: usize, t: usize) -> Result { let bytes = if self.is_rgb()? && self.is_interleaved()? { let index = self.get_index(z as i32, 0, t as i32)?; self.deinterleave(self.open_bytes(index)?, c)? @@ -382,7 +383,7 @@ impl Reader { let index = channel_separator.get_index(z as i32, c as i32, t as i32)?; channel_separator.open_bytes(index)? } else if self.is_indexed()? { - let index = self.get_index(z as i32, 0, t as i32)?; + let index = self.get_index(z as i32, c as i32, t as i32)?; self.open_bytes(index)? // TODO: apply LUT // let _bytes_lut = match self.pixel_type { @@ -401,7 +402,7 @@ impl Reader { self.bytes_to_frame(bytes) } - fn bytes_to_frame(&self, bytes: Vec) -> Result { + fn bytes_to_frame(&self, bytes: Vec) -> Result { macro_rules! get_frame { ($t:tt, <$n:expr) => { Ok(Frame::from(Array2::from_shape_vec( diff --git a/src/stats.rs b/src/stats.rs index 1ea6ca8..3dfab28 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -1,14 +1,14 @@ -use anyhow::{Result, anyhow}; +use crate::error::Error; use ndarray::{Array, ArrayD, ArrayView, Axis, Dimension, RemoveAxis}; /// a trait to define the min, max, sum and mean operations along an axis pub trait MinMax { type Output; - fn max(self, axis: usize) -> Result; - fn min(self, axis: usize) -> Result; - fn sum(self, axis: usize) -> Result; - fn mean(self, axis: usize) -> Result; + fn max(self, axis: usize) -> Result; + fn min(self, axis: usize) -> Result; + fn sum(self, axis: usize) -> Result; + fn mean(self, axis: usize) -> Result; } macro_rules! impl_frame_stats_float_view { @@ -20,7 +20,7 @@ macro_rules! impl_frame_stats_float_view { { type Output = Array<$t, D::Smaller>; - fn max(self, axis: usize) -> Result { + fn max(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -34,7 +34,7 @@ macro_rules! impl_frame_stats_float_view { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn min(self, axis: usize) -> Result { + fn min(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -48,12 +48,12 @@ macro_rules! impl_frame_stats_float_view { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn sum(self, axis: usize) -> Result { + fn sum(self, axis: usize) -> Result { Ok(self.sum_axis(Axis(axis))) } - fn mean(self, axis: usize) -> Result { - self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean")) + fn mean(self, axis: usize) -> Result { + self.mean_axis(Axis(axis)).ok_or(Error::NoMean) } } )* @@ -69,7 +69,7 @@ macro_rules! impl_frame_stats_int_view { { type Output = Array<$t, D::Smaller>; - fn max(self, axis: usize) -> Result { + fn max(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -80,7 +80,7 @@ macro_rules! impl_frame_stats_int_view { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn min(self, axis: usize) -> Result { + fn min(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -91,12 +91,12 @@ macro_rules! impl_frame_stats_int_view { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn sum(self, axis: usize) -> Result { + fn sum(self, axis: usize) -> Result { Ok(self.sum_axis(Axis(axis))) } - fn mean(self, axis: usize) -> Result { - self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean")) + fn mean(self, axis: usize) -> Result { + self.mean_axis(Axis(axis)).ok_or(Error::NoMean) } } )* @@ -112,7 +112,7 @@ macro_rules! impl_frame_stats_float { { type Output = Array<$t, D::Smaller>; - fn max(self, axis: usize) -> Result { + fn max(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -126,7 +126,7 @@ macro_rules! impl_frame_stats_float { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn min(self, axis: usize) -> Result { + fn min(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -140,12 +140,12 @@ macro_rules! impl_frame_stats_float { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn sum(self, axis: usize) -> Result { + fn sum(self, axis: usize) -> Result { Ok(self.sum_axis(Axis(axis))) } - fn mean(self, axis: usize) -> Result { - self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean")) + fn mean(self, axis: usize) -> Result { + self.mean_axis(Axis(axis)).ok_or(Error::NoMean) } } )* @@ -161,7 +161,7 @@ macro_rules! impl_frame_stats_int { { type Output = Array<$t, D::Smaller>; - fn max(self, axis: usize) -> Result { + fn max(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -172,7 +172,7 @@ macro_rules! impl_frame_stats_int { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn min(self, axis: usize) -> Result { + fn min(self, axis: usize) -> Result { let a: Vec<_> = self .lanes(Axis(axis)) .into_iter() @@ -183,12 +183,12 @@ macro_rules! impl_frame_stats_int { Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } - fn sum(self, axis: usize) -> Result { + fn sum(self, axis: usize) -> Result { Ok(self.sum_axis(Axis(axis))) } - fn mean(self, axis: usize) -> Result { - self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean")) + fn mean(self, axis: usize) -> Result { + self.mean_axis(Axis(axis)).ok_or(Error::NoMean) } } )* diff --git a/src/tiff.rs b/src/tiff.rs index 2eed311..fa50e65 100644 --- a/src/tiff.rs +++ b/src/tiff.rs @@ -1,9 +1,9 @@ use crate::colors::Color; +use crate::error::Error; use crate::metadata::Metadata; use crate::reader::PixelType; use crate::stats::MinMax; use crate::view::{Number, View}; -use anyhow::{Result, anyhow}; use indicatif::{ProgressBar, ProgressStyle}; use itertools::iproduct; use ndarray::{Array0, Array1, Array2, ArrayD, Dimension}; @@ -37,7 +37,7 @@ impl TiffOptions { compression: Option, colors: Vec, overwrite: bool, - ) -> Result { + ) -> Result { let mut options = Self { bar: None, compression: compression.unwrap_or(Compression::Zstd(10)), @@ -54,7 +54,7 @@ impl TiffOptions { } /// show a progress bar while saving tiff - pub fn enable_bar(&mut self) -> Result<()> { + pub fn enable_bar(&mut self) -> Result<(), Error> { self.bar = Some(ProgressStyle::with_template( "{spinner:.green} [{elapsed_precise}, {percent}%] [{wide_bar:.green/lime}] {pos:>7}/{len:7} ({eta_precise}, {per_sec:<5})", )?.progress_chars("▰▱▱")); @@ -81,11 +81,11 @@ impl TiffOptions { self.compression = Compression::Deflate } - pub fn set_colors(&mut self, colors: &[String]) -> Result<()> { + pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> { let colors = colors .iter() .map(|c| c.parse::()) - .collect::>>()?; + .collect::, Error>>()?; self.colors = Some(colors.into_iter().map(|c| c.to_rgb()).collect()); Ok(()) } @@ -100,7 +100,7 @@ where D: Dimension, { /// save as tiff with a certain type - pub fn save_as_tiff_with_type(&self, path: P, options: &TiffOptions) -> Result<()> + pub fn save_as_tiff_with_type(&self, path: P, options: &TiffOptions) -> Result<(), Error> where P: AsRef, T: Bytes + Number + Send + Sync, @@ -113,7 +113,7 @@ where if options.overwrite { std::fs::remove_file(&path)?; } else { - return Err(anyhow!("File {} already exists", path.display())); + return Err(Error::FileAlreadyExists(path.display().to_string())); } } let size_c = self.size_c(); @@ -145,10 +145,10 @@ where bar.inc(1); Ok(()) } else { - Err(anyhow::anyhow!("tiff is locked")) + Err(Error::TiffLock) } }) - .collect::>()?; + .collect::>()?; bar.finish(); } else { iproduct!(0..size_c, 0..size_z, 0..size_t) @@ -159,16 +159,16 @@ where tiff.save(&self.get_frame::(c, z, t)?, c, z, t)?; Ok(()) } else { - Err(anyhow::anyhow!("tiff is locked")) + Err(Error::TiffLock) } }) - .collect::>()?; + .collect::>()?; }; Ok(()) } /// save as tiff with whatever pixel type the view has - pub fn save_as_tiff

(&self, path: P, options: &TiffOptions) -> Result<()> + pub fn save_as_tiff

(&self, path: P, options: &TiffOptions) -> Result<(), Error> where P: AsRef, { diff --git a/src/view.rs b/src/view.rs index 111ea52..7ecb8e8 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,8 +1,8 @@ use crate::axes::{Ax, Axis, Operation, Slice, SliceInfoElemDef, slice_info}; +use crate::error::Error; use crate::metadata::Metadata; use crate::reader::Reader; use crate::stats::MinMax; -use anyhow::{Error, Result, anyhow}; use indexmap::IndexMap; use itertools::{Itertools, iproduct}; use ndarray::{ @@ -22,27 +22,27 @@ use std::ops::{AddAssign, Deref, Div}; use std::path::{Path, PathBuf}; use std::sync::Arc; -fn idx_bnd(idx: isize, bnd: isize) -> Result { +fn idx_bnd(idx: isize, bnd: isize) -> Result { if idx < -bnd { - Err(anyhow!("Index {} out of bounds {}", idx, bnd)) + Err(Error::OutOfBounds(idx, bnd)) } else if idx < 0 { Ok(bnd - idx) } else if idx < bnd { Ok(idx) } else { - Err(anyhow!("Index {} out of bounds {}", idx, bnd)) + Err(Error::OutOfBounds(idx, bnd)) } } -fn slc_bnd(idx: isize, bnd: isize) -> Result { +fn slc_bnd(idx: isize, bnd: isize) -> Result { if idx < -bnd { - Err(anyhow!("Index {} out of bounds {}", idx, bnd)) + Err(Error::OutOfBounds(idx, bnd)) } else if idx < 0 { Ok(bnd - idx) } else if idx <= bnd { Ok(idx) } else { - Err(anyhow!("Index {} out of bounds {}", idx, bnd)) + Err(Error::OutOfBounds(idx, bnd)) } } @@ -88,7 +88,7 @@ impl View { } #[allow(dead_code)] - pub(crate) fn new_with_axes(reader: Arc, axes: Vec) -> Result { + pub(crate) fn new_with_axes(reader: Arc, axes: Vec) -> Result { let mut slice = Vec::new(); for axis in axes.iter() { match axis { @@ -134,11 +134,7 @@ impl View { Axis::New => 1, }; if size > 1 { - return Err(anyhow!( - "Axis {:?} has length {}, but was not included", - axis, - size - )); + return Err(Error::OutOfBoundsAxis(format!("{:?}", axis), size)); } slice.push(SliceInfoElem::Index(0)); axes.push(axis); @@ -180,7 +176,7 @@ impl View { } /// change the dimension into a concrete dimension - pub fn into_dimensionality(self) -> Result> { + pub fn into_dimensionality(self) -> Result, Error> { if let Some(d) = D2::NDIM { if d == self.ndim() { Ok(View { @@ -191,7 +187,7 @@ impl View { dimensionality: PhantomData, }) } else { - Err(anyhow!("Dimensionality mismatch: {} != {}", d, self.ndim())) + Err(Error::DimensionalityMismatch(d, self.ndim())) } } else { Ok(View { @@ -235,7 +231,7 @@ impl View { } /// remove axes of size 1 - pub fn squeeze(&self) -> Result> { + pub fn squeeze(&self) -> Result, Error> { let view = self.clone().into_dyn(); let slice: Vec<_> = self .shape() @@ -369,7 +365,7 @@ impl View { } /// swap two axes - pub fn swap_axes(&self, axis0: A, axis1: A) -> Result { + pub fn swap_axes(&self, axis0: A, axis1: A) -> Result { let idx0 = axis0.pos_op(&self.axes, &self.slice, &self.op_axes())?; let idx1 = axis1.pos_op(&self.axes, &self.slice, &self.op_axes())?; let mut slice = self.slice.to_vec(); @@ -380,7 +376,7 @@ impl View { } /// subset of gives axes will be reordered in given order - pub fn permute_axes(&self, axes: &[A]) -> Result { + pub fn permute_axes(&self, axes: &[A]) -> Result { let idx: Vec = axes .iter() .map(|a| a.pos_op(&self.axes, &self.slice, &self.op_axes()).unwrap()) @@ -397,7 +393,7 @@ impl View { } /// reverse the order of the axes - pub fn transpose(&self) -> Result { + pub fn transpose(&self) -> Result { Ok(View::new( self.reader.clone(), self.slice.iter().rev().cloned().collect(), @@ -406,7 +402,7 @@ impl View { .with_operations(self.operations.clone())) } - fn operate(&self, axis: A, operation: Operation) -> Result> { + fn operate(&self, axis: A, operation: Operation) -> Result, Error> { let pos = axis.pos_op(&self.axes, &self.slice, &self.op_axes())?; let ax = self.axes[pos]; let (axes, slice, operations) = if Axis::New == ax { @@ -423,7 +419,7 @@ impl View { self.operations.clone(), ) } else { - return Err(anyhow!("axis {}: {} is already operated on!", pos, ax)); + return Err(Error::AxisAlreadyOperated(pos, ax.to_string())); } } else { let mut operations = self.operations.clone(); @@ -434,32 +430,32 @@ impl View { } /// maximum along axis - pub fn max_proj(&self, axis: A) -> Result> { + pub fn max_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Max) } /// minimum along axis - pub fn min_proj(&self, axis: A) -> Result> { + pub fn min_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Min) } /// sum along axis - pub fn sum_proj(&self, axis: A) -> Result> { + pub fn sum_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Sum) } /// mean along axis - pub fn mean_proj(&self, axis: A) -> Result> { + pub fn mean_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Mean) } /// created a new sliced view - pub fn slice(&self, info: I) -> Result> + pub fn slice(&self, info: I) -> Result, Error> where I: SliceArg, { if self.slice.out_ndim() < info.in_ndim() { - return Err(Error::msg("not enough free dimensions")); + return Err(Error::NotEnoughFreeDimensions); } let info = info.as_ref(); let mut n_idx = 0; @@ -495,11 +491,7 @@ impl View { }; let new_step = (step * info_step).abs(); if new_start > end { - return Err(anyhow!( - "Index {} out of bounds {}", - info_start, - (end - start) / step - )); + return Err(Error::OutOfBounds(*info_start, (end - start) / step)); } new_slice.push(SliceInfoElem::Slice { start: new_start, @@ -521,11 +513,7 @@ impl View { }; let end = end.expect("slice has no end"); if i >= end { - return Err(anyhow!( - "Index {} out of bounds {}", - i, - (end - start) / step - )); + return Err(Error::OutOfBounds(i, (end - start) / step)); } new_slice.push(SliceInfoElem::Index(i)); new_axes.push(*a.expect("axis should exist when slice exists")); @@ -534,7 +522,7 @@ impl View { } (Some(SliceInfoElem::Slice { start, .. }), Some(SliceInfoElem::NewAxis)) => { if *start != 0 { - return Err(anyhow!("Index {} out of bounds 1", start)); + return Err(Error::OutOfBounds(*start, 1)); } new_slice.push(SliceInfoElem::NewAxis); new_axes.push(Axis::New); @@ -543,7 +531,7 @@ impl View { } (Some(SliceInfoElem::Index(k)), Some(SliceInfoElem::NewAxis)) => { if *k != 0 { - return Err(anyhow!("Index {} out of bounds 1", k)); + return Err(Error::OutOfBounds(*k, 1)); } n_idx += 1; r_idx += 1; @@ -587,7 +575,7 @@ impl View { /// resets axes to cztyx order, with all 5 axes present, /// inserts new axes in place of axes under operation (max_proj etc.) - pub fn reset_axes(&self) -> Result> { + pub fn reset_axes(&self) -> Result, Error> { let mut axes = Vec::new(); let mut slice = Vec::new(); @@ -615,7 +603,7 @@ impl View { /// slice, but slice elements are in cztyx order, all cztyx must be given, /// but axes not present in view will be ignored, view axes are reordered in cztyx order - pub fn slice_cztyx(&self, info: I) -> Result> + pub fn slice_cztyx(&self, info: I) -> Result, Error> where I: SliceArg, { @@ -623,7 +611,7 @@ impl View { } /// the pixel intensity at a given index - pub fn item_at(&self, index: &[isize]) -> Result + pub fn item_at(&self, index: &[isize]) -> Result where T: Number, ArrayD: MinMax>, @@ -637,7 +625,7 @@ impl View { } /// collect the view into an ndarray - pub fn as_array(&self) -> Result> + pub fn as_array(&self) -> Result, Error> where T: Number, ArrayD: MinMax>, @@ -648,7 +636,7 @@ impl View { } /// collect the view into a dynamic-dimension ndarray - pub fn as_array_dyn(&self) -> Result> + pub fn as_array_dyn(&self) -> Result, Error> where T: Number, ArrayD: MinMax>, @@ -889,7 +877,7 @@ impl View { } /// turn the view into a 1d array - pub fn flatten(&self) -> Result> + pub fn flatten(&self) -> Result, Error> where T: Number, ArrayD: MinMax>, @@ -900,7 +888,7 @@ impl View { } /// turn the data into a byte vector - pub fn to_bytes(&self) -> Result> + pub fn to_bytes(&self) -> Result, Error> where T: Number + ToBytesVec, ArrayD: MinMax>, @@ -915,7 +903,7 @@ impl View { } /// retrieve a single frame at czt, sliced accordingly - pub fn get_frame(&self, c: N, z: N, t: N) -> Result> + pub fn get_frame(&self, c: N, z: N, t: N) -> Result, Error> where T: Number, ArrayD: MinMax>, @@ -925,17 +913,17 @@ impl View { { let c = c .to_isize() - .ok_or_else(|| anyhow!("cannot convert {} into isize", c))?; + .ok_or_else(|| Error::Cast(c.to_string(), "isize".to_string()))?; let z = z .to_isize() - .ok_or_else(|| anyhow!("cannot convert {} into isize", z))?; + .ok_or_else(|| Error::Cast(z.to_string(), "isize".to_string()))?; let t = t .to_isize() - .ok_or_else(|| anyhow!("cannot convert {} into isize", t))?; + .ok_or_else(|| Error::Cast(t.to_string(), "isize".to_string()))?; self.slice_cztyx(s![c, z, t, .., ..])?.as_array() } - fn get_stat(&self, operation: Operation) -> Result + fn get_stat(&self, operation: Operation) -> Result where T: Number + Sum, ArrayD: MinMax>, @@ -958,14 +946,14 @@ impl View { Operation::Mean => { arr.flatten().into_iter().sum::() / T::from_usize(arr.len()).ok_or_else(|| { - anyhow!("cannot convert {} into {}", arr.len(), type_name::()) + Error::Cast(arr.len().to_string(), type_name::().to_string()) })? } }) } /// maximum intensity - pub fn max(&self) -> Result + pub fn max(&self) -> Result where T: Number + Sum, ArrayD: MinMax>, @@ -976,7 +964,7 @@ impl View { } /// minimum intensity - pub fn min(&self) -> Result + pub fn min(&self) -> Result where T: Number + Sum, ArrayD: MinMax>, @@ -987,7 +975,7 @@ impl View { } /// sum intensity - pub fn sum(&self) -> Result + pub fn sum(&self) -> Result where T: Number + Sum, ArrayD: MinMax>, @@ -998,7 +986,7 @@ impl View { } /// mean intensity - pub fn mean(&self) -> Result + pub fn mean(&self) -> Result where T: Number + Sum, ArrayD: MinMax>, @@ -1009,7 +997,7 @@ impl View { } /// gives a helpful summary of the recorded experiment - pub fn summary(&self) -> Result { + pub fn summary(&self) -> Result { let mut s = "".to_string(); s.push_str(&format!("path/filename: {}\n", self.path.display())); s.push_str(&format!("series/pos: {}\n", self.series)); @@ -1072,7 +1060,7 @@ where /// trait to define a function to retrieve the only item in a 0d array pub trait Item { - fn item(&self) -> Result + fn item(&self) -> Result where T: Number, ArrayD: MinMax>, @@ -1081,7 +1069,7 @@ pub trait Item { } impl View { - pub fn from_path

(path: P, series: usize) -> Result + pub fn from_path

(path: P, series: usize) -> Result where P: AsRef, { @@ -1100,18 +1088,14 @@ impl View { } impl Item for View { - fn item(&self) -> Result + fn item(&self) -> Result where T: Number, ArrayD: MinMax>, Array1: MinMax>, Array2: MinMax>, { - Ok(self - .as_array()? - .first() - .ok_or_else(|| anyhow!("Empty view"))? - .clone()) + Ok(self.as_array()?.first().ok_or(Error::EmptyView)?.clone()) } } diff --git a/tests/test_slicing.py b/tests/test_slicing.py index 6aa04b9..3fa9a57 100644 --- a/tests/test_slicing.py +++ b/tests/test_slicing.py @@ -37,5 +37,5 @@ def test_slicing(s, image, array): assert s_im == s_a else: assert isinstance(s_im, Imread) - assert s_im.shape == s_a.shape + assert tuple(s_im.shape) == s_a.shape assert np.all(s_im == s_a)