ndbioimage 0.2.0

Read bio image formats using the bio-formats java package.
use crate::error::Error;
use itertools::Itertools;
use ome_metadata::Ome;
use ome_metadata::ome::{
    BinningType, Convert, Image, Instrument, Objective, Pixels, UnitsLength, UnitsTime,
};

impl Metadata for Ome {
    fn get_instrument(&self) -> Option<&Instrument> {
        let instrument_id = self.get_image()?.instrument_ref.as_ref()?.id.clone();
        self.instrument.iter().find(|i| i.id == instrument_id)
    }

    fn get_image(&self) -> Option<&Image> {
        if let Some(image) = &self.image.first() {
            Some(image)
        } else {
            None
        }
    }
}

/// helper trait to extract useful information from ome metadata
pub trait Metadata {
    /// the instrument used to acquire the image
    fn get_instrument(&self) -> Option<&Instrument>;
    /// the first image in the ome structure
    fn get_image(&self) -> Option<&Image>;

    /// the pixels of the image
    fn get_pixels(&self) -> Option<&Pixels> {
        if let Some(image) = self.get_image() {
            Some(&image.pixels)
        } else {
            None
        }
    }

    /// the objective used to acquire the image
    fn get_objective(&self) -> Option<&Objective> {
        let objective_id = self.get_image()?.objective_settings.as_ref()?.id.clone();
        self.get_instrument()?
            .objective
            .iter()
            .find(|o| o.id == objective_id)
    }

    /// the tube lens used to acquire the image
    fn get_tube_lens(&self) -> Option<&Objective> {
        self.get_instrument()?
            .objective
            .iter()
            .find(|o| o.id.starts_with("Objective:Tubelens"))
    }

    /// shape of the data along cztyx axes
    fn shape(&self) -> Result<(usize, usize, usize, usize, usize), Error> {
        if let Some(pixels) = self.get_pixels() {
            Ok((
                pixels.size_c as usize,
                pixels.size_z as usize,
                pixels.size_t as usize,
                pixels.size_y as usize,
                pixels.size_x as usize,
            ))
        } else {
            Err(Error::NoImageOrPixels)
        }
    }

    /// pixel size in nm
    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(
                    (pixels
                        .physical_size_x_unit
                        .convert(&UnitsLength::nm, x as f64)?
                        + pixels
                            .physical_size_y_unit
                            .convert(&UnitsLength::nm, y as f64)?)
                        / 2f64,
                )),
                (Some(x), None) => Ok(Some(
                    pixels
                        .physical_size_x_unit
                        .convert(&UnitsLength::nm, x as f64)?
                        .powi(2),
                )),
                (None, Some(y)) => Ok(Some(
                    pixels
                        .physical_size_y_unit
                        .convert(&UnitsLength::nm, y as f64)?
                        .powi(2),
                )),
                _ => Ok(None),
            }
        } else {
            Ok(None)
        }
    }

    /// distance between planes in z-stack in nm
    fn delta_z(&self) -> Result<Option<f64>, Error> {
        Ok(
            if let Some(pixels) = self.get_pixels()
                && let Some(z) = pixels.physical_size_z
            {
                Some(
                    pixels
                        .physical_size_z_unit
                        .convert(&UnitsLength::nm, z as f64)?,
                )
            } else {
                None
            },
        )
    }

    /// time interval in seconds for time-lapse images
    fn time_interval(&self) -> Result<Option<f64>, Error> {
        if let Some(pixels) = self.get_pixels()
            && let Some(t) = pixels.plane.iter().filter_map(|p| p.the_t).max()
            && (t > 0)
        {
            let plane_a = pixels.plane.iter().find(|p| {
                (p.the_c.is_none() || (p.the_c == Some(0)))
                    && (p.the_z.is_none() || (p.the_z == Some(0)))
                    && (p.the_t == Some(0))
            });
            let plane_b = pixels.plane.iter().find(|p| {
                (p.the_c.is_none() || (p.the_c == Some(0)))
                    && (p.the_z.is_none() || (p.the_z == Some(0)))
                    && (p.the_t == Some(t))
            });
            if let (Some(a), Some(b)) = (plane_a, plane_b)
                && let (Some(a_t), Some(b_t)) = (a.delta_t, b.delta_t)
            {
                return Ok(Some(
                    (b.delta_t_unit.convert(&UnitsTime::s, b_t as f64)?
                        - a.delta_t_unit.convert(&UnitsTime::s, a_t as f64)?)
                    .abs()
                        / (t as f64),
                ));
            }
        }
        Ok(None)
    }

    /// exposure time for channel, z=0 and t=0
    fn exposure_time(&self, channel: usize) -> Result<Option<f64>, Error> {
        let c = channel as i32;
        Ok(
            if let Some(pixels) = self.get_pixels()
                && let Some(p) = pixels.plane.iter().find(|p| {
                    (p.the_c == Some(c))
                        && (p.the_z.is_none() || (p.the_z == Some(0)))
                        && (p.the_t.is_none() || (p.the_t == Some(0)))
                })
                && let Some(t) = p.exposure_time
            {
                Some(p.exposure_time_unit.convert(&UnitsTime::s, t as f64)?)
            } else {
                None
            },
        )
    }

    /// the binning of the detector for a channel (1, 2, 4 or 8)
    fn binning(&self, channel: usize) -> Option<usize> {
        match self
            .get_pixels()?
            .channel
            .get(channel)?
            .detector_settings
            .as_ref()?
            .binning
            .as_ref()?
        {
            BinningType::_1X1 => Some(1),
            BinningType::_2X2 => Some(2),
            BinningType::_4X4 => Some(4),
            BinningType::_8X8 => Some(8),
            BinningType::Other => None,
        }
    }

    /// the excitation wavelength of the laser for a channel in nm
    fn laser_wavelengths(&self, channel: usize) -> Result<Option<f64>, Error> {
        Ok(
            if let Some(pixels) = self.get_pixels()
                && let Some(channel) = pixels.channel.get(channel)
                && let Some(w) = channel.excitation_wavelength
            {
                Some(
                    channel
                        .excitation_wavelength_unit
                        .convert(&UnitsLength::nm, w as f64)?,
                )
            } else {
                None
            },
        )
    }

    /// the laser power (fraction of the maximum) for a channel
    fn laser_powers(&self, channel: usize) -> Result<Option<f64>, Error> {
        if let Some(pixels) = self.get_pixels()
            && let Some(channel) = pixels.channel.get(channel)
            && let Some(ls) = &channel.light_source_settings
            && let Some(a) = ls.attenuation
        {
            if (0. ..=1.).contains(&a) {
                Ok(Some(1f64 - (a as f64)))
            } else {
                Err(Error::InvalidAttenuation(a.to_string()))
            }
        } else {
            Ok(None)
        }
    }

    /// the name of the objective
    fn objective_name(&self) -> Option<String> {
        Some(self.get_objective()?.model.as_ref()?.clone())
    }

    /// the total magnification: objective magnification times tube lens magnification
    fn magnification(&self) -> Option<f64> {
        Some(
            (self.get_objective()?.nominal_magnification? as f64)
                * (self.get_tube_lens()?.nominal_magnification? as f64),
        )
    }

    /// the name of the tube lens
    fn tube_lens_name(&self) -> Option<String> {
        self.get_tube_lens()?.model.clone()
    }

    /// the name of the filter set for a channel
    fn filter_set_name(&self, channel: usize) -> Option<String> {
        let filter_set_id = self
            .get_pixels()?
            .channel
            .get(channel)?
            .filter_set_ref
            .as_ref()?
            .id
            .clone();
        self.get_instrument()
            .as_ref()?
            .filter_set
            .iter()
            .find(|f| f.id == filter_set_id)?
            .model
            .clone()
    }

    /// the gain of the detector for a channel
    fn gain(&self, channel: usize) -> Option<f64> {
        self.get_pixels()
            .and_then(|p| p.channel.get(channel))
            .and_then(|c| c.detector_settings.as_ref())
            .map(|ds| ds.id.as_str())
            .and_then(|detector_id| {
                self.get_instrument()
                    .and_then(|i| i.detector.iter().find(|d| d.id == detector_id))
                    .and_then(|d| d.amplification_gain)
                    .map(|g| g as f64)
            })
    }

    /// whether the image is a z-stack (more than one z slice)
    fn is_zstack(&self) -> Result<bool, Error> {
        self.get_pixels()
            .map(|p| Ok(p.size_z > 1))
            .unwrap_or_else(|| Err(Error::NoImageOrPixels))
    }

    /// whether the image is a time lapse (more than one time point)
    fn is_time_lapse(&self) -> Result<bool, Error> {
        self.get_pixels()
            .map(|p| Ok(p.size_t > 1))
            .unwrap_or_else(|| Err(Error::NoImageOrPixels))
    }

    /// a multi-line summary of the most relevant metadata, one field per line
    fn summary(&self) -> Result<String, Error> {
        let size_c = if let Some(pixels) = self.get_pixels() {
            pixels.channel.len()
        } else {
            0
        };
        let mut s = "".to_string();
        if let Ok(Some(pixel_size)) = self.pixel_size() {
            s.push_str(&format!("pixel size:    {pixel_size:.2} nm\n"));
        }
        if let Ok(Some(delta_z)) = self.delta_z()
            && self.is_zstack()?
        {
            s.push_str(&format!("z-interval:    {delta_z:.2} nm\n"))
        }
        if let Ok(Some(time_interval)) = self.time_interval()
            && self.is_time_lapse()?
        {
            s.push_str(&format!("time interval: {time_interval:.2} s\n"))
        }
        let exposure_time = (0..size_c)
            .map(|c| self.exposure_time(c))
            .collect::<Result<Vec<_>, Error>>()?
            .into_iter()
            .flatten()
            .collect::<Vec<_>>();
        if !exposure_time.is_empty() {
            s.push_str(&format!(
                "exposure time: {} s\n",
                exposure_time
                    .iter()
                    .map(|e| format!("{:.2}", e))
                    .join(" | ")
            ));
        }
        if let Some(magnification) = self.magnification() {
            s.push_str(&format!("magnification: {magnification:.1}x\n"))
        }
        if let Some(objective_name) = self.objective_name() {
            s.push_str(&format!("objective:     {objective_name}\n"))
        }
        if let Some(tube_lens_name) = self.tube_lens_name() {
            s.push_str(&format!("tube lens:     {tube_lens_name}\n"))
        }
        let filter_set_name = (0..size_c)
            .map(|c| self.filter_set_name(c))
            .collect::<Vec<_>>()
            .into_iter()
            .flatten()
            .collect::<Vec<_>>();
        if !filter_set_name.is_empty() {
            s.push_str(&format!(
                "filter set:    {}\n",
                filter_set_name.into_iter().join(" | ")
            ));
        }
        let gain = (0..size_c)
            .map(|c| self.gain(c))
            .collect::<Vec<_>>()
            .into_iter()
            .flatten()
            .collect::<Vec<_>>();
        if !gain.is_empty() {
            s.push_str(&format!(
                "gain:          {}\n",
                gain.into_iter().join(" | ")
            ));
        }
        let laser_wavelengths = (0..size_c)
            .map(|c| self.laser_wavelengths(c))
            .collect::<Result<Vec<_>, Error>>()?
            .into_iter()
            .flatten()
            .collect::<Vec<_>>();
        if !laser_wavelengths.is_empty() {
            s.push_str(&format!(
                "laser colors:  {} nm\n",
                laser_wavelengths.into_iter().join(" | ")
            ));
        }
        let laser_powers = (0..size_c)
            .map(|c| self.laser_powers(c))
            .collect::<Result<Vec<_>, Error>>()?
            .into_iter()
            .flatten()
            .collect::<Vec<_>>();
        if !laser_powers.is_empty() {
            s.push_str(&format!(
                "laser powers:  {} %\n",
                laser_powers
                    .into_iter()
                    .map(|p| format!("{:.3}", 100.0 * p))
                    .join(" | ")
            ));
        }
        let binning = (0..size_c)
            .map(|c| self.binning(c))
            .collect::<Vec<_>>()
            .into_iter()
            .flatten()
            .collect::<Vec<_>>();
        if !binning.is_empty() {
            s.push_str(&format!(
                "binning:       {}\n",
                binning.into_iter().join(" | ")
            ));
        }
        Ok(s.to_string())
    }
}