ndbioimage 0.2.0

Read bio image formats using the bio-formats java package.
#![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) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
//!     println!("{:?}", arr);
//! } else {
//!     println!("could not convert Frame to Array<i8>");
//! }
//! 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::<u16>()?;
//! println!("{:?}", array.shape());
//! # Ok(())
//! # }
//! ```

/// axis handling: axis enum, slicing and shape
pub mod axes;
/// process-wide LRU caches for frames and materialized arrays
mod cache;
#[cfg(feature = "python")]
mod py;
/// min/max/sum/mean operations along an axis
pub mod stats;
/// the main data structure: an on-disk image that can be sliced without loading it fully
pub mod view;

/// named colors and color conversion
pub mod colors;
/// the error type used throughout the crate
pub mod error;
/// ome metadata helpers
pub mod metadata;
#[cfg(feature = "movie")]
/// saving views as movies
pub mod movie;
/// readers for the different supported image formats
pub mod readers;
#[cfg(feature = "tiffwrite")]
/// saving views as tiff files
pub mod tiffwrite;
mod utils;

/// main entry point for the application
pub mod main {
    #[cfg(feature = "tiffwrite")]
    use crate::axes::{Axis, Operation};
    use crate::error::Error;
    #[cfg(feature = "movie")]
    use crate::movie::MovieOptions;
    use crate::readers::{Dimensions, DynReader, Reader};
    use crate::view::View;
    use clap::{Parser, Subcommand};
    #[cfg(feature = "movie")]
    use ndarray::SliceInfoElem;
    use std::path::PathBuf;

    #[derive(Parser)]
    #[command(arg_required_else_help = true, version, about, long_about = None, propagate_version = true
    )]
    struct Cli {
        #[command(subcommand)]
        command: Commands,
    }

    #[derive(Subcommand)]
    enum Commands {
        /// Print some metadata
        Info {
            #[arg(value_name = "FILE", num_args(1..))]
            file: Vec<PathBuf>,
        },
        /// save ome metadata as xml
        ExtractOME {
            #[arg(value_name = "FILE", num_args(1..))]
            file: Vec<PathBuf>,
        },
        /// Save the image as tiff file
        #[cfg(feature = "tiffwrite")]
        Tiff {
            #[arg(value_name = "FILE", num_args(1..))]
            file: Vec<PathBuf>,
            #[arg(short = 'C', long, value_name = "COLOR", num_args(1..))]
            colors: Vec<String>,
            #[arg(short = 'f', long, value_name = "OVERWRITE")]
            overwrite: bool,
            #[arg(short = 'q', long, value_name = "OPERATIONS", num_args(1..))]
            operations: Vec<String>,
            #[arg(short = 'o', long, value_name = "OUTPUT")]
            output: Option<PathBuf>,
        },
        /// Save the image as mp4 file
        #[cfg(feature = "movie")]
        Movie {
            #[arg(value_name = "FILE", num_args(1..))]
            file: Vec<PathBuf>,
            #[arg(short, long, value_name = "VELOCITY", default_value = "3.6")]
            velocity: f64,
            #[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 = 'C', long, value_name = "COLOR", num_args(1..))]
            colors: Vec<String>,
            #[arg(short = 'f', 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,
            #[arg(short = 'o', long, value_name = "OUTPUT")]
            output: Option<PathBuf>,
        },
    }

    #[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())),
        }
    }

    /// the command line interface, run `ndbioimage --help` for an overview of the commands
    pub fn main(args: Option<Vec<String>>) -> Result<(), Error> {
        let cli = if let Some(args) = args {
            Cli::parse_from(args)
        } else {
            Cli::parse()
        };
        match &cli.command {
            Commands::Info { file } => {
                for f in file {
                    let view = View::<_, DynReader>::from_path(f)?.squeeze()?;
                    println!("{}", view.summary()?);
                }
            }
            Commands::ExtractOME { file } => {
                for f in file {
                    let (path, dimensions) = Dimensions::parse_path(f)?;
                    let reader = DynReader::new(
                        &path,
                        dimensions.s.unwrap_or(0),
                        dimensions.p.unwrap_or(0),
                    )?;
                    let xml = reader.metadata()?.to_xml()?;
                    std::fs::write(path.with_extension("xml"), xml)?;
                }
            }
            #[cfg(feature = "tiffwrite")]
            Commands::Tiff {
                file,
                colors,
                overwrite,
                operations,
                output,
            } => {
                let options = crate::tiffwrite::TiffOptions::new(
                    Some(crate::utils::progress::get_bar(
                        Some(0),
                        Some("writing tiff file".to_string()),
                    )),
                    None,
                    colors.clone(),
                    *overwrite,
                )?;
                for f in file {
                    let mut view: View<_, DynReader> =
                        View::<_, DynReader>::from_path(f)?.into_dyn();
                    for operation in operations {
                        let a = operation.split(":").collect::<Vec<_>>();
                        if let Some(ax) = a.first()
                            && let Some(op) = a.get(1)
                        {
                            view = view.operate(ax.parse::<Axis>()?, op.parse::<Operation>()?)?;
                        }
                    }
                    let out = output
                        .as_ref()
                        .filter(|_| file.len() == 1)
                        .cloned()
                        .unwrap_or_else(|| f.with_extension("tiff"));
                    view.save_as_tiff(&out, &options)?;
                }
            }
            #[cfg(feature = "movie")]
            Commands::Movie {
                file,
                velocity: speed,
                brightness,
                scale,
                colors,
                overwrite,
                register,
                channel,
                zslice,
                time,
                no_scaling,
                output,
            } => {
                let options = MovieOptions::new(
                    *speed,
                    brightness.to_vec(),
                    *scale,
                    colors.to_vec(),
                    *overwrite,
                    *register,
                    *no_scaling,
                )?;
                for f in file {
                    let view: View<_, DynReader> = View::from_path(f)?;
                    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)?;
                    }
                    let out = output
                        .as_ref()
                        .filter(|_| file.len() == 1)
                        .cloned()
                        .unwrap_or_else(|| f.with_extension("mp4"));
                    view.into_dyn()
                        .slice(s.as_slice())?
                        .save_as_movie(&out, &options)?;
                }
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::error::Error;
    use crate::readers::{DynReader, Frame, Reader};
    use ndarray::Array2;
    use rayon::prelude::*;

    fn open(file: &str) -> Result<DynReader, Error> {
        let path = std::env::current_dir()?
            .join("tests")
            .join("files")
            .join(file);
        DynReader::new(&path, 0, 0)
    }

    fn get_pixel_type(file: &str) -> Result<String, Error> {
        let reader = open(file)?;
        Ok(format!(
            "file: {}, pixel type: {:?}",
            file,
            reader.pixel_type()
        ))
    }

    fn get_frame(file: &str) -> Result<Frame, Error> {
        let reader = open(file)?;
        reader.get_frame(0, 0, 0)
    }

    #[test]
    fn read_ser() -> Result<(), Error> {
        let file = "czi/Experiment-2029.czi";
        let reader = open(file)?;
        println!("shape: {}", reader.shape());
        let frame = reader.get_frame(0, 0, 0)?;
        if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
            println!("{:?}", arr);
        } else {
            println!("could not convert Frame to Array<i8>");
        }
        Ok(())
    }

    #[test]
    fn read_par() -> Result<(), Error> {
        let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"];
        let pixel_type = files
            .into_par_iter()
            .map(|file| get_pixel_type(file).unwrap())
            .collect::<Vec<_>>();
        println!("{:?}", pixel_type);
        Ok(())
    }

    #[test]
    fn read_frame_par() -> Result<(), Error> {
        let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"];
        let frames = files
            .into_par_iter()
            .map(|file| get_frame(file).unwrap())
            .collect::<Vec<_>>();
        println!("{:?}", frames);
        Ok(())
    }
}