- 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:
57
Cargo.toml
57
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 <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
|
||||
@@ -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"]
|
||||
movie = ["dep:ffmpeg-sidecar"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["gpl-formats", "tiff", "movie"]
|
||||
@@ -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
|
||||
|
||||
67
build.rs
67
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<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()
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
57
src/axes.rs
57
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<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>,
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
65
src/error.rs
Normal 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),
|
||||
}
|
||||
49
src/lib.rs
49
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<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();
|
||||
|
||||
83
src/main.rs
83
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<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 } => {
|
||||
|
||||
@@ -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<_>>();
|
||||
|
||||
105
src/movie.rs
105
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<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
193
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<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(())
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
50
src/stats.rs
50
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<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)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
24
src/tiff.rs
24
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<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>,
|
||||
{
|
||||
|
||||
114
src/view.rs
114
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<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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user