- implement custom error types

- less restrictive dependency versions
- some extra features and bugfixes for movie writing
- make python tests work again
This commit is contained in:
Wim Pomp
2026-01-04 13:59:57 +01:00
parent 3dc8e6af04
commit 3c14168878
19 changed files with 655 additions and 333 deletions

View File

@@ -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 <w.pomp@nki.nl>"]
@@ -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
@@ -62,3 +62,6 @@ python = ["dep:pyo3", "dep:numpy", "dep:serde_json"]
tiff = ["dep:tiffwrite", "dep:indicatif", "dep:rayon"]
# Enables writing as mp4 using ffmpeg
movie = ["dep:ffmpeg-sidecar"]
[package.metadata.docs.rs]
features = ["gpl-formats", "tiff", "movie"]

View File

@@ -102,6 +102,3 @@ let array = view.as_array::<u16>()?
```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

View File

@@ -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<dyn Error>> {
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<PathBuf, J4RsError> {
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()

View File

@@ -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"""

View File

@@ -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

View File

@@ -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<usize>;
fn pos(&self, axes: &[Axis], slice: &[SliceInfoElem]) -> Result<usize, Error>;
/// 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<usize>;
fn pos_op(
&self,
axes: &[Axis],
slice: &[SliceInfoElem],
op_axes: &[Axis],
) -> Result<usize, Error>;
}
/// 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<Self, Self::Err> {
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<usize> {
fn pos(&self, axes: &[Axis], _slice: &[SliceInfoElem]) -> Result<usize, Error> {
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<usize> {
fn pos_op(
&self,
axes: &[Axis],
_slice: &[SliceInfoElem],
_op_axes: &[Axis],
) -> Result<usize, Error> {
self.pos(axes, _slice)
}
}
@@ -92,7 +102,7 @@ impl Ax for usize {
*self
}
fn pos(&self, _axes: &[Axis], slice: &[SliceInfoElem]) -> Result<usize> {
fn pos(&self, _axes: &[Axis], slice: &[SliceInfoElem]) -> Result<usize, Error> {
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<usize> {
fn pos_op(
&self,
axes: &[Axis],
slice: &[SliceInfoElem],
op_axes: &[Axis],
) -> Result<usize, Error> {
let idx: Vec<_> = axes
.iter()
.zip(slice.iter())
@@ -132,7 +147,7 @@ impl Operation {
&self,
array: Array<T, D>,
axis: usize,
) -> Result<<Array<T, D> as MinMax>::Output>
) -> Result<<Array<T, D> as MinMax>::Output, Error>
where
D: Dimension,
Array<T, D>: MinMax,
@@ -154,8 +169,11 @@ impl PartialEq for Axis {
pub(crate) fn slice_info<D: Dimension>(
info: &[SliceInfoElem],
) -> Result<SliceInfo<&[SliceInfoElem], Ix2, D>> {
Ok(info.try_into()?)
) -> Result<SliceInfo<&[SliceInfoElem], Ix2, D>, 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<SliceInfoElem> for SliceInfoElemDef {
fn serialize_as<S>(
source: &SliceInfoElem,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
fn serialize_as<S>(source: &SliceInfoElem, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
@@ -183,7 +198,7 @@ impl SerializeAs<SliceInfoElem> for SliceInfoElemDef {
}
impl<'de> DeserializeAs<'de, SliceInfoElem> for SliceInfoElemDef {
fn deserialize_as<D>(deserializer: D) -> std::result::Result<SliceInfoElem, D::Error>
fn deserialize_as<D>(deserializer: D) -> Result<SliceInfoElem, D::Error>
where
D: Deserializer<'de>,
{

View File

@@ -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> {
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<Jvm> {
})
}
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<Self> {
pub(crate) fn new(image_reader: &ImageReader) -> Result<Self, Error> {
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<Vec<u8>> {
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>, Error> {
Ok(transmute_vec(self.open_bi8(index)?))
}
@@ -180,16 +179,16 @@ impl Drop for ImageReader {
}
impl ImageReader {
pub(crate) fn new() -> Result<Self> {
pub(crate) fn new() -> Result<Self, Error> {
let reader = jvm().create_instance("loci.formats.ImageReader", InvocationArg::empty())?;
Ok(ImageReader(reader))
}
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>> {
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>, Error> {
Ok(transmute_vec(self.open_bi8(index)?))
}
pub(crate) fn ome_xml(&self) -> Result<String> {
pub(crate) fn ome_xml(&self) -> Result<String, Error> {
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<Self> {
pub(crate) fn new() -> Result<Self, Error> {
let meta_data_tools =
jvm().create_instance("loci.formats.MetadataTools", InvocationArg::empty())?;
Ok(MetadataTools(meta_data_tools))

View File

@@ -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<Self> {
fn from_str(s: &str) -> Result<Self, Self::Err> {
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

65
src/error.rs Normal file
View File

@@ -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),
}

View File

@@ -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<Reader> {
fn open(file: &str) -> Result<Reader, Error> {
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<String> {
fn get_pixel_type(file: &str) -> Result<String, Error> {
let reader = open(file)?;
Ok(format!(
"file: {}, pixel type: {:?}",
@@ -43,13 +46,13 @@ mod tests {
))
}
fn get_frame(file: &str) -> Result<Frame> {
fn get_frame(file: &str) -> Result<Frame, Error> {
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();

View File

@@ -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<PathBuf>,
#[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<f64>,
#[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<String>,
#[arg(short, long, value_name = "OVERWRITE")]
overwrite: bool,
#[arg(short, long, value_name = "REGISTER")]
register: bool,
#[arg(short, long, value_name = "CHANNEL")]
channel: Option<isize>,
#[arg(short, long, value_name = "ZSLICE")]
zslice: Option<String>,
#[arg(short, long, value_name = "TIME")]
time: Option<String>,
#[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<SliceInfoElem, Error> {
let mut t = s
.trim()
.replace("..", ":")
.split(":")
.map(|i| i.parse().ok())
.collect::<Vec<Option<isize>>>();
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 } => {

View File

@@ -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<Option<f64>> {
fn pixel_size(&self) -> Result<Option<f64>, 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<Option<f64>> {
fn delta_z(&self) -> Result<Option<f64>, 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<Option<f64>> {
fn time_interval(&self) -> Result<Option<f64>, 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<Option<f64>> {
fn exposure_time(&self, channel: usize) -> Result<Option<f64>, 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<Option<f64>> {
fn laser_wavelengths(&self, channel: usize) -> Result<Option<f64>, 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<Option<f64>> {
fn laser_powers(&self, channel: usize) -> Result<Option<f64>, 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<String> {
fn summary(&self) -> Result<String, Error> {
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::<Result<Vec<_>>>()?
.collect::<Result<Vec<_>, Error>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
@@ -333,7 +333,7 @@ pub trait Metadata {
}
let laser_wavelengths = (0..size_c)
.map(|c| self.laser_wavelengths(c))
.collect::<Result<Vec<_>>>()?
.collect::<Result<Vec<_>, Error>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
@@ -345,7 +345,7 @@ pub trait Metadata {
}
let laser_powers = (0..size_c)
.map(|c| self.laser_powers(c))
.collect::<Result<Vec<_>>>()?
.collect::<Result<Vec<_>, Error>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();

View File

@@ -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<Vec<Vec<u8>>>,
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<String>,
overwrite: bool,
) -> Result<Self> {
register: bool,
no_scaling: bool,
) -> Result<Self, Error> {
let colors = if colors.is_empty() {
None
} else {
let colors = colors
.iter()
.map(|c| c.parse::<Color>())
.collect::<Result<Vec<_>>>()?;
.collect::<Result<Vec<_>, 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::<Color>())
.collect::<Result<Vec<_>>>()?;
.collect::<Result<Vec<_>, 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<IxDyn>) -> Result<(f64, f64)> {
fn get_ab(tyx: View<IxDyn>) -> Result<(f64, f64), Error> {
let s = tyx
.as_array::<f64>()?
.iter()
@@ -107,14 +116,14 @@ fn get_ab(tyx: View<IxDyn>) -> 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<f64>, color: &[u8], a: f64, b: f64) -> Array3<f64> {
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<f64>, color: &[u8], a: f64, b: f64) -> Array3<f64> {
.map(|&c| (c * &frame).to_owned())
.collect::<Vec<Array2<f64>>>();
let view = frame.iter().map(|c| c.view()).collect::<Vec<_>>();
stack(ndarray::Axis(0), &view).unwrap()
stack(ndarray::Axis(2), &view).unwrap()
}
impl<D> View<D>
where
D: Dimension,
{
pub fn save_as_movie<P>(&self, path: P, options: &MovieOptions) -> Result<()>
pub fn save_as_movie<P>(&self, path: P, options: &MovieOptions) -> Result<(), Error>
where
P: AsRef<Path>,
{
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::<Result<Vec<_>>>()?;
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::<Result<Vec<_>, Error>>()?
};
thread::spawn(move || {
for t in 0..size_t {
let mut frame = Array3::<f64>::zeros((3, size_y, size_y));
let mut frame = Array3::<f64>::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")

193
src/py.rs
View File

@@ -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<crate::error::Error> for PyErr {
fn from(err: crate::error::Error) -> PyErr {
PyErr::new::<PyValueError, _>(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::<PyValueError, _>(
"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<Self> {
fn new<'py>(
py: Python<'py>,
path: Bound<'py, PyAny>,
series: usize,
dtype: &str,
axes: &str,
) -> PyResult<Self> {
if path.is_instance_of::<Self>() {
Ok(path.downcast_into::<Self>()?.extract::<Self>()?)
Ok(path.cast_into::<Self>()?.extract::<Self>()?)
} else {
let mut path = PathBuf::from(path.downcast_into::<PyString>()?.extract::<String>()?);
let builtins = PyModule::import(py, "builtins")?;
let mut path = PathBuf::from(
builtins
.getattr("str")?
.call1((path,))?
.cast_into::<PyString>()?
.extract::<String>()?,
);
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::<Result<Vec<Axis>>>()?;
.collect::<Result<Vec<Axis>, 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<Bound<'py, PyAny>> {
let slice: Vec<_> = if n.is_instance_of::<PyTuple>() {
n.downcast_into::<PyTuple>()?.into_iter().collect()
n.cast_into::<PyTuple>()?.into_iter().collect()
} else if n.is_instance_of::<PyList>() {
n.downcast_into::<PyList>()?.into_iter().collect()
n.cast_into::<PyList>()?.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::<PyInt>() {
new_slice.push(SliceInfoElem::Index(
s.downcast::<PyInt>()?.extract::<isize>()?,
));
new_slice.push(SliceInfoElem::Index(s.cast::<PyInt>()?.extract::<isize>()?));
} else if s.is_instance_of::<PySlice>() {
let u = s.downcast::<PySlice>()?.indices(*t as isize)?;
let u = s.cast::<PySlice>()?.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::<PyEllipsis>() {
if ellipsis.is_some() {
return Err(anyhow!("cannot have more than one ellipsis").into());
return Err(PyErr::new::<PyValueError, _>(
"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::<PyValueError, _>(format!(
"cannot convert {:?} to slice",
s
)));
}
}
if new_slice.len() > shape.len() {
return Err(anyhow!(
return Err(PyErr::new::<PyValueError, _>(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<bool> {
fn __contains__(&self, _item: Bound<PyAny>) -> PyResult<bool> {
Err(PyNotImplementedError::new_err("contains not implemented"))
}
fn __enter__<'p>(slf: PyRef<'p, Self>, _py: Python<'p>) -> PyResult<PyRef<'p, Self>> {
fn __enter__<'py>(slf: PyRef<'py, Self>) -> PyResult<PyRef<'py, Self>> {
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<Bound<PyAny>>,
exc_val: Option<Bound<PyAny>>,
exc_tb: Option<Bound<PyAny>>,
) -> 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::<PyValueError, _>(
"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<usize> {
fn get_ax(&self, axis: Bound<PyAny>) -> PyResult<usize> {
if axis.is_instance_of::<PyString>() {
let axis = axis
.downcast_into::<PyString>()?
.cast_into::<PyString>()?
.extract::<String>()?
.parse::<Axis>()?;
Ok(self
.view
self.view
.axes()
.iter()
.position(|a| *a == axis)
.ok_or_else(|| anyhow!("cannot find axis {:?}", axis))?)
.ok_or_else(|| {
PyErr::new::<PyValueError, _>(format!("cannot find axis {:?}", axis))
})
} else if axis.is_instance_of::<PyInt>() {
Ok(axis.downcast_into::<PyInt>()?.extract::<usize>()?)
Ok(axis.cast_into::<PyInt>()?.extract::<usize>()?)
} else {
Err(anyhow!("cannot convert to axis").into())
Err(PyErr::new::<PyValueError, _>(
"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<Self> {
fn swap_axes(&self, ax0: Bound<PyAny>, ax1: Bound<PyAny>) -> PyResult<Self> {
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<Vec<Bound<'_, PyAny>>>) -> PyResult<Self> {
fn transpose(&self, axes: Option<Vec<Bound<PyAny>>>) -> PyResult<Self> {
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<Bound<'py, PyAny>>,
dtype: Option<Bound<'py, PyAny>>,
out: Option<Bound<'py, PyAny>>,
keepdims: bool,
initial: Option<usize>,
r#where: bool,
) -> PyResult<Bound<'py, PyAny>> {
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<Bound<'py, PyAny>>,
dtype: Option<Bound<'py, PyAny>>,
out: Option<Bound<'py, PyAny>>,
keepdims: bool,
initial: Option<usize>,
r#where: bool,
) -> PyResult<Bound<'py, PyAny>> {
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<Bound<'py, PyAny>>,
dtype: Option<Bound<'py, PyAny>>,
out: Option<Bound<'py, PyAny>>,
keepdims: bool,
r#where: bool,
) -> PyResult<Bound<'py, PyAny>> {
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<Bound<'py, PyAny>>,
dtype: Option<Bound<'py, PyAny>>,
out: Option<Bound<'py, PyAny>>,
keepdims: bool,
initial: Option<usize>,
r#where: bool,
) -> PyResult<Bound<'py, PyAny>> {
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<PathBuf> {
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<PyModule>) -> PyResult<()> {
m.add_class::<PyView>()?;
m.add_class::<ViewConstructor>()?;
#[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(())
}

View File

@@ -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<P>(path: P) -> Result<(PathBuf, Option<usize>)>
pub fn split_path_and_series<P>(path: P) -> Result<(PathBuf, Option<usize>), Error>
where
P: Into<PathBuf>,
{
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::<usize>() {
@@ -72,7 +72,7 @@ impl TryFrom<i32> 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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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::<T>()));
err = Err(Error::Cast(x.to_string(), type_name::<T>().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<P>(path: P, series: usize) -> Result<Self>
pub fn new<P>(path: P, series: usize) -> Result<Self, Error>
where
P: AsRef<Path>,
{
@@ -333,7 +333,7 @@ impl Reader {
}
/// Get ome metadata as ome structure
pub fn get_ome(&self) -> Result<Ome> {
pub fn get_ome(&self) -> Result<Ome, Error> {
let mut ome = self.ome_xml()?.parse::<Ome>()?;
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<String> {
pub fn get_ome_xml(&self) -> Result<String, Error> {
self.ome_xml()
}
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>> {
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, 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<Frame> {
#[allow(clippy::if_same_then_else)]
pub fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
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<u8>) -> Result<Frame> {
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> {
macro_rules! get_frame {
($t:tt, <$n:expr) => {
Ok(Frame::from(Array2::from_shape_vec(

View File

@@ -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<Self::Output>;
fn min(self, axis: usize) -> Result<Self::Output>;
fn sum(self, axis: usize) -> Result<Self::Output>;
fn mean(self, axis: usize) -> Result<Self::Output>;
fn max(self, axis: usize) -> Result<Self::Output, Error>;
fn min(self, axis: usize) -> Result<Self::Output, Error>;
fn sum(self, axis: usize) -> Result<Self::Output, Error>;
fn mean(self, axis: usize) -> Result<Self::Output, Error>;
}
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<Self::Output> {
fn max(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn min(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn sum(self, axis: usize) -> Result<Self::Output, Error> {
Ok(self.sum_axis(Axis(axis)))
}
fn mean(self, axis: usize) -> Result<Self::Output> {
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn max(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn min(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn sum(self, axis: usize) -> Result<Self::Output, Error> {
Ok(self.sum_axis(Axis(axis)))
}
fn mean(self, axis: usize) -> Result<Self::Output> {
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn max(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn min(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn sum(self, axis: usize) -> Result<Self::Output, Error> {
Ok(self.sum_axis(Axis(axis)))
}
fn mean(self, axis: usize) -> Result<Self::Output> {
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn max(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn min(self, axis: usize) -> Result<Self::Output, Error> {
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<Self::Output> {
fn sum(self, axis: usize) -> Result<Self::Output, Error> {
Ok(self.sum_axis(Axis(axis)))
}
fn mean(self, axis: usize) -> Result<Self::Output> {
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
self.mean_axis(Axis(axis)).ok_or(Error::NoMean)
}
}
)*

View File

@@ -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<Compression>,
colors: Vec<String>,
overwrite: bool,
) -> Result<Self> {
) -> Result<Self, Error> {
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::<Color>())
.collect::<Result<Vec<_>>>()?;
.collect::<Result<Vec<_>, 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<T, P>(&self, path: P, options: &TiffOptions) -> Result<()>
pub fn save_as_tiff_with_type<T, P>(&self, path: P, options: &TiffOptions) -> Result<(), Error>
where
P: AsRef<Path>,
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::<Result<()>>()?;
.collect::<Result<(), Error>>()?;
bar.finish();
} else {
iproduct!(0..size_c, 0..size_z, 0..size_t)
@@ -159,16 +159,16 @@ where
tiff.save(&self.get_frame::<T, _>(c, z, t)?, c, z, t)?;
Ok(())
} else {
Err(anyhow::anyhow!("tiff is locked"))
Err(Error::TiffLock)
}
})
.collect::<Result<()>>()?;
.collect::<Result<(), Error>>()?;
};
Ok(())
}
/// save as tiff with whatever pixel type the view has
pub fn save_as_tiff<P>(&self, path: P, options: &TiffOptions) -> Result<()>
pub fn save_as_tiff<P>(&self, path: P, options: &TiffOptions) -> Result<(), Error>
where
P: AsRef<Path>,
{

View File

@@ -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<isize> {
fn idx_bnd(idx: isize, bnd: isize) -> Result<isize, Error> {
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<isize> {
fn slc_bnd(idx: isize, bnd: isize) -> Result<isize, Error> {
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<D: Dimension> View<D> {
}
#[allow(dead_code)]
pub(crate) fn new_with_axes(reader: Arc<Reader>, axes: Vec<Axis>) -> Result<Self> {
pub(crate) fn new_with_axes(reader: Arc<Reader>, axes: Vec<Axis>) -> Result<Self, Error> {
let mut slice = Vec::new();
for axis in axes.iter() {
match axis {
@@ -134,11 +134,7 @@ impl<D: Dimension> View<D> {
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<D: Dimension> View<D> {
}
/// change the dimension into a concrete dimension
pub fn into_dimensionality<D2: Dimension>(self) -> Result<View<D2>> {
pub fn into_dimensionality<D2: Dimension>(self) -> Result<View<D2>, Error> {
if let Some(d) = D2::NDIM {
if d == self.ndim() {
Ok(View {
@@ -191,7 +187,7 @@ impl<D: Dimension> View<D> {
dimensionality: PhantomData,
})
} else {
Err(anyhow!("Dimensionality mismatch: {} != {}", d, self.ndim()))
Err(Error::DimensionalityMismatch(d, self.ndim()))
}
} else {
Ok(View {
@@ -235,7 +231,7 @@ impl<D: Dimension> View<D> {
}
/// remove axes of size 1
pub fn squeeze(&self) -> Result<View<IxDyn>> {
pub fn squeeze(&self) -> Result<View<IxDyn>, Error> {
let view = self.clone().into_dyn();
let slice: Vec<_> = self
.shape()
@@ -369,7 +365,7 @@ impl<D: Dimension> View<D> {
}
/// swap two axes
pub fn swap_axes<A: Ax>(&self, axis0: A, axis1: A) -> Result<Self> {
pub fn swap_axes<A: Ax>(&self, axis0: A, axis1: A) -> Result<Self, Error> {
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<D: Dimension> View<D> {
}
/// subset of gives axes will be reordered in given order
pub fn permute_axes<A: Ax>(&self, axes: &[A]) -> Result<Self> {
pub fn permute_axes<A: Ax>(&self, axes: &[A]) -> Result<Self, Error> {
let idx: Vec<usize> = axes
.iter()
.map(|a| a.pos_op(&self.axes, &self.slice, &self.op_axes()).unwrap())
@@ -397,7 +393,7 @@ impl<D: Dimension> View<D> {
}
/// reverse the order of the axes
pub fn transpose(&self) -> Result<Self> {
pub fn transpose(&self) -> Result<Self, Error> {
Ok(View::new(
self.reader.clone(),
self.slice.iter().rev().cloned().collect(),
@@ -406,7 +402,7 @@ impl<D: Dimension> View<D> {
.with_operations(self.operations.clone()))
}
fn operate<A: Ax>(&self, axis: A, operation: Operation) -> Result<View<D::Smaller>> {
fn operate<A: Ax>(&self, axis: A, operation: Operation) -> Result<View<D::Smaller>, 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<D: Dimension> View<D> {
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<D: Dimension> View<D> {
}
/// maximum along axis
pub fn max_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
pub fn max_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
self.operate(axis, Operation::Max)
}
/// minimum along axis
pub fn min_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
pub fn min_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
self.operate(axis, Operation::Min)
}
/// sum along axis
pub fn sum_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
pub fn sum_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
self.operate(axis, Operation::Sum)
}
/// mean along axis
pub fn mean_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
pub fn mean_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
self.operate(axis, Operation::Mean)
}
/// created a new sliced view
pub fn slice<I>(&self, info: I) -> Result<View<I::OutDim>>
pub fn slice<I>(&self, info: I) -> Result<View<I::OutDim>, Error>
where
I: SliceArg<D>,
{
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<D: Dimension> View<D> {
};
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<D: Dimension> View<D> {
};
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<D: Dimension> View<D> {
}
(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<D: Dimension> View<D> {
}
(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<D: Dimension> View<D> {
/// 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<View<Ix5>> {
pub fn reset_axes(&self) -> Result<View<Ix5>, Error> {
let mut axes = Vec::new();
let mut slice = Vec::new();
@@ -615,7 +603,7 @@ impl<D: Dimension> View<D> {
/// 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<I>(&self, info: I) -> Result<View<I::OutDim>>
pub fn slice_cztyx<I>(&self, info: I) -> Result<View<I::OutDim>, Error>
where
I: SliceArg<Ix5>,
{
@@ -623,7 +611,7 @@ impl<D: Dimension> View<D> {
}
/// the pixel intensity at a given index
pub fn item_at<T>(&self, index: &[isize]) -> Result<T>
pub fn item_at<T>(&self, index: &[isize]) -> Result<T, Error>
where
T: Number,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -637,7 +625,7 @@ impl<D: Dimension> View<D> {
}
/// collect the view into an ndarray
pub fn as_array<T>(&self) -> Result<Array<T, D>>
pub fn as_array<T>(&self) -> Result<Array<T, D>, Error>
where
T: Number,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -648,7 +636,7 @@ impl<D: Dimension> View<D> {
}
/// collect the view into a dynamic-dimension ndarray
pub fn as_array_dyn<T>(&self) -> Result<ArrayD<T>>
pub fn as_array_dyn<T>(&self) -> Result<ArrayD<T>, Error>
where
T: Number,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -889,7 +877,7 @@ impl<D: Dimension> View<D> {
}
/// turn the view into a 1d array
pub fn flatten<T>(&self) -> Result<Array1<T>>
pub fn flatten<T>(&self) -> Result<Array1<T>, Error>
where
T: Number,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -900,7 +888,7 @@ impl<D: Dimension> View<D> {
}
/// turn the data into a byte vector
pub fn to_bytes<T>(&self) -> Result<Vec<u8>>
pub fn to_bytes<T>(&self) -> Result<Vec<u8>, Error>
where
T: Number + ToBytesVec,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -915,7 +903,7 @@ impl<D: Dimension> View<D> {
}
/// retrieve a single frame at czt, sliced accordingly
pub fn get_frame<T, N>(&self, c: N, z: N, t: N) -> Result<Array2<T>>
pub fn get_frame<T, N>(&self, c: N, z: N, t: N) -> Result<Array2<T>, Error>
where
T: Number,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -925,17 +913,17 @@ impl<D: Dimension> View<D> {
{
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<T>(&self, operation: Operation) -> Result<T>
fn get_stat<T>(&self, operation: Operation) -> Result<T, Error>
where
T: Number + Sum,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -958,14 +946,14 @@ impl<D: Dimension> View<D> {
Operation::Mean => {
arr.flatten().into_iter().sum::<T>()
/ T::from_usize(arr.len()).ok_or_else(|| {
anyhow!("cannot convert {} into {}", arr.len(), type_name::<T>())
Error::Cast(arr.len().to_string(), type_name::<T>().to_string())
})?
}
})
}
/// maximum intensity
pub fn max<T>(&self) -> Result<T>
pub fn max<T>(&self) -> Result<T, Error>
where
T: Number + Sum,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -976,7 +964,7 @@ impl<D: Dimension> View<D> {
}
/// minimum intensity
pub fn min<T>(&self) -> Result<T>
pub fn min<T>(&self) -> Result<T, Error>
where
T: Number + Sum,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -987,7 +975,7 @@ impl<D: Dimension> View<D> {
}
/// sum intensity
pub fn sum<T>(&self) -> Result<T>
pub fn sum<T>(&self) -> Result<T, Error>
where
T: Number + Sum,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -998,7 +986,7 @@ impl<D: Dimension> View<D> {
}
/// mean intensity
pub fn mean<T>(&self) -> Result<T>
pub fn mean<T>(&self) -> Result<T, Error>
where
T: Number + Sum,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -1009,7 +997,7 @@ impl<D: Dimension> View<D> {
}
/// gives a helpful summary of the recorded experiment
pub fn summary(&self) -> Result<String> {
pub fn summary(&self) -> Result<String, Error> {
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<T>(&self) -> Result<T>
fn item<T>(&self) -> Result<T, Error>
where
T: Number,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
@@ -1081,7 +1069,7 @@ pub trait Item {
}
impl View<Ix5> {
pub fn from_path<P>(path: P, series: usize) -> Result<Self>
pub fn from_path<P>(path: P, series: usize) -> Result<Self, Error>
where
P: AsRef<Path>,
{
@@ -1100,18 +1088,14 @@ impl View<Ix5> {
}
impl Item for View<Ix0> {
fn item<T>(&self) -> Result<T>
fn item<T>(&self) -> Result<T, Error>
where
T: Number,
ArrayD<T>: MinMax<Output = ArrayD<T>>,
Array1<T>: MinMax<Output = Array0<T>>,
Array2<T>: MinMax<Output = Array1<T>>,
{
Ok(self
.as_array()?
.first()
.ok_or_else(|| anyhow!("Empty view"))?
.clone())
Ok(self.as_array()?.first().ok_or(Error::EmptyView)?.clone())
}
}

View File

@@ -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)