diff --git a/Cargo.toml b/Cargo.toml
index c725066..e2962df 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,7 +28,7 @@ itertools = "0.15"
indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.18", features = ["rayon"], optional = true }
j4rs = { version = "0.25", optional = true }
-libczirw-sys = { path = "../libczirw-sys", optional = true }
+libczirw-sys = { version = "0.5", optional = true }
ndarray = { version = "0.17", features = ["serde"] }
num = "0.4"
numpy = { version = "0.29", optional = true }
@@ -76,7 +76,7 @@ tiff = ["dep:tiff", "dep:thread_local"]
movie = ["dep:ffmpeg-sidecar", "dep:tokio", "dep:ordered-float", "dep:indicatif", "dep:console"]
[package.metadata.docs.rs]
-features = ["bioformats_java", "gpl-formats", "czi", "tiff", "movie"]
+features = ["bioformats_java", "czi", "tiff", "tiffseq", "movie", "tiffwrite", "movie"]
[profile.test]
inherits = "release"
diff --git a/README.md b/README.md
index 90a63a7..ea56c1f 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,14 @@
# ndbioimage
+
[](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml)
## Work in progress
+
Rust rewrite of python version. Read bio image formats using the bio-formats java package.
[https://www.openmicroscopy.org/bio-formats/](https://www.openmicroscopy.org/bio-formats/)
-Exposes (bio) images as a numpy ndarray-like object, but without loading the whole
-image into memory, reading from the file only when needed. Some metadata is read
+Exposes (bio) images as a numpy ndarray-like object (Python) or a struct that can be sliced like an ndarray Array
+(Rust), but without loading the whole image into memory, reading from the file only when needed. Some metadata is read
and stored in an [ome](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2005-6-5-r47) structure.
Additionally, it can automatically calculate an affine transform that corrects for chromatic aberrations etc. and apply
it on the fly to the image.
@@ -23,6 +25,7 @@ pip install ndbioimage
```
### Installation with option to write mp4 or mkv:
+
Work in progress! Make sure ffmpeg is installed.
```
@@ -30,6 +33,7 @@ pip install ndbioimage[write]
```
## Usage
+
### Python
- Reading an image file and plotting the frame at channel=2, time=1
@@ -60,7 +64,6 @@ with Imread('image_file.tif', axes='cztyx') as im:
sliced_im is an instance of Imread which will load any image data from file only when needed
-
- Converting (part) of the image to a numpy ndarray
```
@@ -71,36 +74,45 @@ with Imread('image_file.tif', axes='cztyx') as im:
```
### Rust
-```
-use ndarray::Array2;
-use ndbioimage::Reader;
-let path = "/path/to/file";
-let reader = Reader::new(&path, 0)?;
-println!("size: {}, {}", reader.size_y, reader.size_y);
-let frame = reader.get_frame(0, 0, 0).unwrap();
-if let Ok(arr) = >>::try_into(frame) {
- println!("{:?}", arr);
-} else {
- println!("could not convert Frame to Array");
+```rust
+use ndarray::Array2;
+use ndbioimage::{DynReader, Frame, Reader};
+
+fn main() -> Result<(), ndbioimage::error::Error> {
+ let path = "/path/to/file";
+ let reader = DynReader::new(&path, 0, 0)?;
+ println!("shape: {}", reader.shape());
+ let frame = reader.get_frame(0, 0, 0)?;
+ if let Ok(arr) = >>::try_into(frame) {
+ println!("{:?}", arr);
+ } else {
+ println!("could not convert Frame to Array");
+ }
+ let xml = reader.metadata()?.to_xml()?;
+ println!("{}", xml);
+ Ok(())
}
-let xml = reader.get_ome_xml().unwrap();
-println!("{}", xml);
```
-```
-use ndarray::Array2;
-use ndbioimage::Reader;
+```rust
+use ndbioimage::{DynReader, Reader};
-let path = "/path/to/file";
-let reader = Reader::new(&path, 0)?;
-let view = reader.view();
-let view = view.max_proj(3)?;
-let array = view.as_array::()?
+fn main() -> Result<(), ndbioimage::error::Error> {
+ let path = "/path/to/file";
+ let reader = DynReader::new(&path, 0, 0)?;
+ let view = reader.view();
+ let view = view.max_proj(3)?;
+ let array = view.as_array::()?;
+ println!("{:?}", array.shape());
+ Ok(())
+}
```
### Command line
+
```ndbioimage --help```: show help
-```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
+```ndbioimage info image```: show metadata about image
+```ndbioimage tiff image image.tif -r```: copy image into image.tif, while registering channels
+```ndbioimage movie image image.mp4 -C cyan lime red``` copy image into image.mp4 (z will be max projected), make channel
+colors cyan lime and red
diff --git a/pyproject.toml b/pyproject.toml
index f45b2d8..016bd70 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ndbioimage"
-version = "2027.0.2"
+version = "2027.0.3"
requires-python = ">=3.10"
classifiers = [
"License :: OSI Approved :: MIT License",
diff --git a/src/lib.rs b/src/lib.rs
index 139810a..fab7273 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,47 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
+//! The ndbioimage crate exposes (bio) images a struct that can be sliced like an ndarray Array
+//! (Rust), but without loading the whole image into memory, reading from the file only when needed.
+//! Some metadata is read
+//! and stored in an [ome](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2005-6-5-r47)
+//! structure. Additionally, it can automatically calculate an affine transform that corrects for
+//! chromatic aberrations etc. and apply it on the fly to the image.
+//!
+//! Currently, it supports imagej tif files, czi files, micromanager tif sequences and anything
+//! [bioformats](https://www.openmicroscopy.org/bio-formats/) can handle.
+//!
+//! ```rust,no_run
+//! use ndarray::Array2;
+//! use ndbioimage::readers::{DynReader, Frame, Reader};
+//!
+//! # fn main() -> Result<(), ndbioimage::error::Error> {
+//! let path = "/path/to/file";
+//! let reader = DynReader::new(&path, 0, 0)?;
+//! println!("shape: {}", reader.shape());
+//! let frame = reader.get_frame(0, 0, 0)?;
+//! if let Ok(arr) = >>::try_into(frame) {
+//! println!("{:?}", arr);
+//! } else {
+//! println!("could not convert Frame to Array");
+//! }
+//! let xml = reader.metadata()?.to_xml()?;
+//! println!("{}", xml);
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! ```rust,no_run
+//! use ndbioimage::readers::{DynReader, Reader};
+//!
+//! # fn main() -> Result<(), ndbioimage::error::Error> {
+//! let path = "/path/to/file";
+//! let reader = DynReader::new(&path, 0, 0)?;
+//! let view = reader.view();
+//! let view = view.max_proj(3)?;
+//! let array = view.as_array::()?;
+//! println!("{:?}", array.shape());
+//! # Ok(())
+//! # }
+//! ```
pub mod axes;
#[cfg(feature = "python")]
@@ -14,7 +57,7 @@ pub mod movie;
pub mod readers;
#[cfg(feature = "tiffwrite")]
pub mod tiffwrite;
-// mod cache;
+mod utils;
pub mod main {
#[cfg(feature = "tiffwrite")]
@@ -30,7 +73,8 @@ pub mod main {
use std::path::PathBuf;
#[derive(Parser)]
- #[command(arg_required_else_help = true, version, about, long_about = None, propagate_version = true)]
+ #[command(arg_required_else_help = true, version, about, long_about = None, propagate_version = true
+ )]
struct Cli {
#[command(subcommand)]
command: Commands,
@@ -162,7 +206,7 @@ pub mod main {
output,
} => {
let options = crate::tiffwrite::TiffOptions::new(
- Some(crate::tiffwrite::get_bar(
+ Some(crate::utils::progress::get_bar(
Some(0),
Some("writing tiff file".to_string()),
)),
diff --git a/src/movie.rs b/src/movie.rs
index d00b740..4eefb14 100644
--- a/src/movie.rs
+++ b/src/movie.rs
@@ -2,18 +2,16 @@ use crate::axes::Axis;
use crate::colors::Color;
use crate::error::Error;
use crate::readers::{PixelType, Reader};
+use crate::utils::progress::get_bar;
use crate::view::View;
-use console::Term;
use ffmpeg_sidecar::command::FfmpegCommand;
use ffmpeg_sidecar::download::auto_download;
use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel};
-use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use itertools::Itertools;
use ndarray::{Array2, Array3, Dimension, IxDyn, s, stack};
use ordered_float::OrderedFloat;
use std::io::Write;
use std::path::Path;
-use std::time::Duration;
pub struct MovieOptions {
velocity: f64,
@@ -138,20 +136,6 @@ fn cframe(frame: Array2, color: &[u8], a: f64, b: f64) -> Array3 {
stack(ndarray::Axis(2), &view).unwrap()
}
-/// a progress bar with an ok style that when py::detach is used also works in jupyter
-pub fn get_bar(count: Option) -> ProgressBar {
- let style = ProgressStyle::with_template(
- "{spinner:.green} {percent}% [{wide_bar:.green/lime}] {pos:>7}/{len:7} [{elapsed}/{eta}, {per_sec:<5}]",
- ).expect("template should be working").progress_chars("#>-");
- let bar = ProgressBar::with_draw_target(
- count.map(|i| i as u64),
- ProgressDrawTarget::term_like_with_hz(Box::new(Term::buffered_stdout()), 20),
- )
- .with_style(style);
- bar.enable_steady_tick(Duration::from_millis(100));
- bar
-}
-
impl View
where
D: Dimension,
@@ -252,7 +236,10 @@ where
};
let rt = tokio::runtime::Runtime::new()?;
- let bar = get_bar(Some(size_t));
+ let bar = get_bar(
+ Some(size_t),
+ Some("autoscaling movie brightness".to_string()),
+ );
let rt_bar = bar.clone();
let write_task = rt.spawn(async move {
for t in 0..size_t {
@@ -275,7 +262,7 @@ where
});
bar.finish();
- let bar = get_bar(Some(size_t));
+ let bar = get_bar(Some(size_t), Some("saving movie".to_string()));
let rt_bar = bar.clone();
let progress_task = rt.spawn(async move {
for event in movie.iter().map_err(|e| Error::Ffmpeg(e.to_string()))? {
diff --git a/src/py.rs b/src/py.rs
index fc38a33..cd19713 100644
--- a/src/py.rs
+++ b/src/py.rs
@@ -1741,7 +1741,7 @@ impl PyView {
bar: bool,
) -> PyResult<()> {
let bar = if bar {
- Some(crate::tiffwrite::get_bar(
+ Some(crate::utils::progress::get_bar(
Some(0),
Some("writing tiff file".to_string()),
))
@@ -1991,7 +1991,16 @@ impl PyShape {
false,
)
} else if idx.is_instance_of::() {
- (vec![idx.cast::()?.extract::()?], true)
+ let i = idx.cast::()?.extract::()?;
+ let len = self.inner.order.len() as isize;
+ let i = if i < 0 { i + len } else { i };
+ if i < 0 || i >= len {
+ return Err(PyIndexError::new_err(format!(
+ "index {} is out of bounds for size {}",
+ i, len
+ )));
+ }
+ (vec![i as usize], true)
} else {
return Err(PyErr::new::(format!(
"Unknown type: {:?}",
diff --git a/src/readers.rs b/src/readers.rs
index 9cd7591..87abe51 100644
--- a/src/readers.rs
+++ b/src/readers.rs
@@ -136,7 +136,7 @@ pub enum ArrayT {
F128(Array), // f128 is nightly
}
-pub(crate) type Frame = ArrayT;
+pub type Frame = ArrayT;
pub trait Reader: Clone + Sized + Debug + Send + Hash + Into {
fn new
(path: P, series: usize, position: usize) -> Result
diff --git a/src/readers/bioformats_java.rs b/src/readers/bioformats_java.rs
index cce6a7f..149cbf9 100644
--- a/src/readers/bioformats_java.rs
+++ b/src/readers/bioformats_java.rs
@@ -5,10 +5,9 @@ use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
-pub use crate::readers::{ArrayT, PixelType, Reader};
-use crate::readers::{DynReader, Frame, Shape};
+use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape};
use itertools::Itertools;
-use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};
+use j4rs::{Instance, InvocationArg, JavaOpt, Jvm, JvmBuilder};
use std::cell::OnceCell;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
@@ -109,8 +108,16 @@ fn jvm() -> Rc {
let j = JvmBuilder::new()
.skip_setting_native_lib()
.with_base_path(class_path.to_str().unwrap())
- .build()
- .expect("Failed to build JVM");
+ .java_opt(JavaOpt::new("--enable-native-access=ALL-UNNAMED"))
+ .build();
+ let j = match j {
+ Ok(j) => j,
+ Err(_) => JvmBuilder::new()
+ .skip_setting_native_lib()
+ .with_base_path(class_path.to_str().unwrap())
+ .build()
+ .expect("Failed to build JVM"),
+ };
if let Ok(e) = InvocationArg::try_from("ERROR") {
let _ = j.invoke_static(
"loci.common.DebugTools",
@@ -122,7 +129,7 @@ fn jvm() -> Rc {
})
}
})
- .clone()
+ .clone()
})
}
diff --git a/src/readers/tiff.rs b/src/readers/tiff.rs
index 113376e..32703ec 100644
--- a/src/readers/tiff.rs
+++ b/src/readers/tiff.rs
@@ -331,7 +331,7 @@ impl Reader for TiffReader {
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result {
let (page_idx, offset, stride) = if self.p_ndim == 3 {
- (z * self.shape.t + t, c, self.n_samples)
+ (t * self.shape.z + z, c, self.n_samples)
} else {
(c + z * self.shape.c + t * self.shape.c * self.shape.z, 0, 1)
};
diff --git a/src/tiffwrite.rs b/src/tiffwrite.rs
index 620636b..8374632 100644
--- a/src/tiffwrite.rs
+++ b/src/tiffwrite.rs
@@ -4,15 +4,14 @@ use crate::error::Error;
use crate::metadata::Metadata;
use crate::readers::{DynReader, PixelType, Reader};
use crate::stats::MinMax;
+use crate::utils::progress::get_bar;
use crate::view::{Number, View};
-use console::Term;
-use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
+use indicatif::ProgressBar;
use itertools::iproduct;
use ndarray::{Array0, Array1, Array2, ArrayD, Dimension};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex};
-use std::time::Duration;
use tiffwrite::{Bytes, Colors, Compression, IJTiffFile};
#[derive(Debug, Clone)]
@@ -34,23 +33,6 @@ impl Default for TiffOptions {
}
}
-/// a progress bar with an ok style that when py::detach is used also works in jupyter
-pub fn get_bar(count: Option, message: Option) -> ProgressBar {
- let style = ProgressStyle::with_template(
- "{spinner:.green} {percent}% [{wide_bar:.green/lime}] {pos:>7}/{len:7} [{elapsed}/{eta}, {per_sec:<5}]",
- ).expect("template should be working").progress_chars("#>-");
- let bar = ProgressBar::with_draw_target(
- count.map(|i| i as u64),
- ProgressDrawTarget::term_like_with_hz(Box::new(Term::buffered_stdout()), 20),
- )
- .with_style(style);
- if let Some(message) = message {
- bar.set_message(message);
- }
- bar.enable_steady_tick(Duration::from_millis(100));
- bar
-}
-
impl TiffOptions {
pub fn new(
bar: Option,
@@ -258,14 +240,25 @@ mod tests {
use std::fs::create_dir_all;
use std::path::PathBuf;
+ #[cfg(any(
+ feature = "czi",
+ feature = "tiffseq",
+ feature = "tiff",
+ feature = "bioformats_java"
+ ))]
#[test]
fn tiff() -> Result<(), Error> {
- #[cfg(any(feature = "czi", feature = "bioformats_java"))]
- let file = "czi/1xp53-01-AP1.czi";
- #[cfg(feature = "tiff")]
- let file = "tiff/20251014_20-Pos_000_000_loc_results_Cy3.tif";
- #[cfg(feature = "tiffseq")]
- let file = "tiffseq/20-Pos_005_005";
+ let file = if cfg!(any(feature = "czi", feature = "bioformats_java")) {
+ "czi/1xp53-01-AP1.czi"
+ } else if cfg!(feature = "tiff") {
+ "tiff/20251014_20-Pos_000_000_loc_results_Cy3.tif"
+ } else if cfg!(feature = "tiffseq") {
+ "tiffseq/20-Pos_005_005"
+ } else {
+ unreachable!(
+ "need to enable one of these features: czi, bioformats_java, tiff, tiffseq"
+ );
+ };
let path = std::env::current_dir()?
.join("tests")
.join("files")
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..54f4531
--- /dev/null
+++ b/src/utils.rs
@@ -0,0 +1,32 @@
+#[cfg(any(feature = "tiffwrite", feature = "movie"))]
+pub(crate) mod progress {
+ use console::Term;
+ use indicatif::{ProgressBar, ProgressDrawTarget, ProgressState, ProgressStyle};
+ use std::time::Duration;
+
+ /// a progress bar with an ok style that when py::detach is used also works in jupyter
+ pub fn get_bar(count: Option, message: Option) -> ProgressBar {
+ let style = ProgressStyle::with_template(
+ "{spinner:.green} {msg} {percent}% [{wide_bar:.green/lime}] {pos:>7}/{len:7} [{elapsed}/{eta}, {rate}]",
+ )
+ .expect("could not build progress bar style")
+ .with_key("rate", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
+ if state.per_sec() < 1.0 {
+ write!(w, "{:>4.2} s", 1.0 / state.per_sec()).expect("could not write to progress bar");
+ } else {
+ write!(w, "{:>4.2}/s", state.per_sec()).expect("could not write to progress bar");
+ }
+ })
+ .progress_chars("#>-");
+ let bar = ProgressBar::with_draw_target(
+ count.map(|i| i as u64),
+ ProgressDrawTarget::term_like_with_hz(Box::new(Term::buffered_stdout()), 20),
+ )
+ .with_style(style);
+ if let Some(message) = message {
+ bar.set_message(message);
+ }
+ bar.enable_steady_tick(Duration::from_millis(100));
+ bar
+ }
+}
diff --git a/src/view.rs b/src/view.rs
index 7e035b1..6a6551e 100644
--- a/src/view.rs
+++ b/src/view.rs
@@ -1197,14 +1197,23 @@ impl View {
let _ = out.insert(a);
}
}
- let mut n = 1;
- for (ax, size) in self.shape().to_hashmap().into_iter() {
- if ((ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T))
- && let Some(Operation::Mean) = self.operations.get(&ax)
- {
- n *= size;
- }
- }
+ let n = if let Some((&ax, op)) = op_czt.first()
+ && *op == Operation::Mean
+ {
+ self.axes
+ .iter()
+ .zip(self.slice.iter())
+ .find(|(a, _)| **a == ax)
+ .and_then(|(_, s)| match s {
+ SliceInfoElem::Slice { start, end, step } => {
+ end.map(|e| (((e - start).max(0) / step) as usize).max(1))
+ }
+ _ => Some(1),
+ })
+ .unwrap_or(1)
+ } else {
+ 1
+ };
let array = if n == 1 {
out.take().unwrap()
} else {
diff --git a/tests/test_ufuncs.py b/tests/test_ufuncs.py
index 96bcfdc..15424c8 100644
--- a/tests/test_ufuncs.py
+++ b/tests/test_ufuncs.py
@@ -14,10 +14,11 @@ def array():
return np.random.randint(0, 255, (64, 64, 2, 3, 4), "uint16")
-@pytest.fixture()
+@pytest.fixture
def image(array):
with tempfile.TemporaryDirectory() as folder:
file = Path(folder) / "tiff" / "test.tif"
+ file.parent.mkdir(parents=True)
tiffwrite(file, array, "yxczt")
with Imread(file, axes="yxczt") as im:
yield im