Files
ndbioimage/src/readers/czi.rs
T
2026-07-22 13:40:27 +02:00

1251 lines
54 KiB
Rust

use crate::colors::Color;
use crate::error::Error;
use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape};
use indexmap::IndexMap;
use itertools::Itertools;
use libczirw_sys::{AttachmentData, Dimension, InputStream, ReaderOpenInfo};
use ndarray::{Array2, s};
use ome_metadata::{Ome, ome};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use thread_local::ThreadLocal;
#[derive(Debug, Deserialize, Serialize)]
pub struct CziReader {
#[serde(skip)]
reader: ThreadLocal<libczirw_sys::CziReader>,
block_map: HashMap<(usize, usize, usize), Vec<i32>>,
path: PathBuf,
series: usize,
position: usize,
shape: Shape,
pixel_type: PixelType,
}
impl From<CziReader> for DynReader {
fn from(value: CziReader) -> Self {
DynReader::Czi(value)
}
}
impl Hash for CziReader {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
self.series.hash(state);
self.position.hash(state);
}
}
impl PartialEq for CziReader {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
&& self.series == other.series
&& self.position == other.position
&& self.shape == other.shape
&& self.pixel_type == other.pixel_type
}
}
impl Eq for CziReader {}
impl Clone for CziReader {
fn clone(&self) -> Self {
Self {
reader: ThreadLocal::default(),
block_map: self.block_map.clone(),
path: self.path.clone(),
series: self.series,
position: self.position,
shape: self.shape.clone(),
pixel_type: self.pixel_type,
}
}
}
impl CziReader {
fn get_reader(&self) -> Result<&libczirw_sys::CziReader, Error> {
self.reader.get_or_try(|| {
let reader = libczirw_sys::CziReader::create()?;
let stream = InputStream::create_from_file_utf8(self.path.to_string_lossy().as_ref())?;
let open_info = ReaderOpenInfo::new(&stream);
reader.open(open_info)?;
Ok(reader)
})
}
// pub fn set_reader(&self) -> Result<(), Error> {
// self.get_reader().map(|_| ())
// }
fn metadata_xml(&self) -> Result<String, Error> {
let reader = self.get_reader()?;
let metadata_segment = reader.get_metadata_segment()?;
let xml = metadata_segment.get_metadata_as_xml()?;
Ok(String::try_from(&xml)?)
}
fn attachments(&self) -> Result<HashMap<String, AttachmentData>, Error> {
let reader = self.get_reader()?;
let n = reader.get_attachment_count()?;
let mut attachments = HashMap::new();
for i in 0..n {
let attachment = reader.read_attachment(i)?;
let info = attachment.get_info()?;
let name = info.get_name()?;
let data = attachment.get_data()?;
attachments.insert(name, data);
}
Ok(attachments)
}
}
#[derive(Debug, Clone)]
enum Version {
/// unspecified
None,
/// 1.0
One,
/// 1.1, 1.2, etc
Other,
}
impl From<Option<String>> for Version {
fn from(s: Option<String>) -> Self {
if let Some(v) = s {
if v == "1.0" { Self::One } else { Self::Other }
} else {
Self::None
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CziError {
#[error("czi file has no valid blocks")]
NoValidBlocks,
}
impl Reader for CziReader {
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
where
P: AsRef<Path>,
{
let mut new = Self {
reader: ThreadLocal::default(),
block_map: HashMap::new(),
path: path.as_ref().to_path_buf(),
series,
position,
shape: Shape::default(),
pixel_type: PixelType::U8,
};
let reader = new.get_reader()?;
let statistics_simple = reader.get_statistics_simple()?;
let c = statistics_simple.get_sub_block_count();
let mut block_map: HashMap<(usize, usize, usize), Vec<i32>> = HashMap::new();
let mut pixel_type = None;
let m = statistics_simple.get_max_m_index() != i32::MIN;
for i in 0..c {
if let Ok(info) = reader.try_get_sub_block_info_for_index(i) {
if m && info.get_m_index() != position as i32 {
continue;
}
let coordinate = info.get_coordinate();
let d = Dimension::vec_from_bitflags(coordinate.get_dimensions_valid());
let v = coordinate.get_value();
if d.contains(&Dimension::S) && v[Dimension::S as usize - 1] as usize != series {
continue;
}
let c = if d.contains(&Dimension::C) {
v[Dimension::C as usize - 1] as usize
} else {
0
};
let z = if d.contains(&Dimension::Z) {
v[Dimension::Z as usize - 1] as usize
} else {
0
};
let t = if d.contains(&Dimension::T) {
v[Dimension::T as usize - 1] as usize
} else {
0
};
if let Some(blocks) = block_map.get_mut(&(c, z, t)) {
blocks.push(i);
} else {
block_map.insert((c, z, t), vec![i]);
}
if pixel_type.is_none() {
pixel_type = Some(info.get_pixel_type()?.try_into()?)
}
}
}
let mut min_x = i32::MAX;
let mut max_x = i32::MIN;
let mut min_y = i32::MAX;
let mut max_y = i32::MIN;
for &i in Ok(block_map.get(&(0, 0, 0)))
.transpose()
.unwrap_or_else(|| block_map.values().next().ok_or(CziError::NoValidBlocks))?
{
if let Ok(info) = reader.try_get_sub_block_info_for_index(i) {
let rect = info.get_logical_rect();
min_x = min_x.min(rect.get_x());
max_x = max_x.max(rect.get_x() + rect.get_w());
min_y = min_y.min(rect.get_y());
max_y = max_y.max(rect.get_y() + rect.get_h());
}
}
let dim_bounds = statistics_simple.get_dim_bounds();
let dimensions = Dimension::vec_from_bitflags(dim_bounds.get_dimensions_valid());
for (i, d) in dim_bounds.get_size().into_iter().zip(dimensions) {
match d {
Dimension::C => new.shape.c = i as usize,
Dimension::Z => new.shape.z = i as usize,
Dimension::T => new.shape.t = i as usize,
_ => {}
}
}
new.shape.x = (max_y - min_y) as usize;
new.shape.y = (max_x - min_x) as usize;
new.block_map = block_map;
new.pixel_type = pixel_type.ok_or(CziError::NoValidBlocks)?;
Ok(new)
}
fn metadata(&self) -> Result<Ome, Error> {
let mut ome = Ome::default();
let xml = xmltree::Element::parse(self.metadata_xml()?.as_bytes())?;
if let Some(metadata) = xml.get_child("Metadata") {
let experiment = metadata.get_child("Experiment");
let version: Version = if let Some(version) = metadata.get_child("Version") {
version.get_text().map(|s| s.to_string())
} else if let Some(experiment) = experiment
&& let Some(version) = experiment.attributes.get("Version")
{
Some(version.to_string())
} else {
None
}
.into();
let information = metadata.get_child("Information");
let display_setting = metadata.get_child("DisplaySetting");
let acquisition_block = experiment
.and_then(|e| e.get_child("ExperimentBlocks"))
.and_then(|e| e.get_child("AcquisitionBlock"));
let instrument = information.and_then(|i| i.get_child("Instrument"));
let image = information.and_then(|i| i.get_child("Image"));
let multi_track_setup = acquisition_block.and_then(|e| e.get_child("MultiTrackSetup"));
// set experimenters
if let Version::One = version {
ome.experimenter.push(ome::Experimenter {
id: "Experimenter:0".to_string(),
user_name: information
.and_then(|i| i.get_child("User"))
.and_then(|u| u.get_child("DisplayName"))
.and_then(|d| d.get_text())
.map(|t| t.to_string()),
..Default::default()
});
} else if let Version::Other = version {
ome.experimenter.push(ome::Experimenter {
id: "Experimenter:0".to_string(),
user_name: information
.and_then(|i| i.get_child("Document"))
.and_then(|d| d.get_child("UserName"))
.and_then(|u| u.get_text())
.map(|t| t.to_string()),
..Default::default()
});
}
// set instruments
if let Version::One = version {
ome.instrument.push(ome::Instrument {
id: instrument
.and_then(|i| i.attributes.get("Id"))
.map(|i| i.to_string())
.unwrap_or("Instrument:0".to_string()),
..Default::default()
})
} else if let Version::Other = version
&& let Some(microscopes) = instrument.and_then(|i| i.get_child("Microscopes"))
{
for i in 0..microscopes.children.len() {
ome.instrument.push(ome::Instrument {
id: format!("Instrument:{}", i),
..Default::default()
})
}
}
// set detectors
if let Some(detectors) = instrument.and_then(|i| i.get_child("Detectors")) {
if let Version::One = version {
for detector in &detectors.children {
if let Some(detector) = detector.as_element() {
let n = ome.instrument[0].detector.len();
ome.instrument[0].detector.push(ome::Detector {
id: detector
.attributes
.get("Id")
.map(|i| i.to_string())
.unwrap_or(format!("Detector:{}", n)),
model: detector
.get_child("Manufacturer")
.and_then(|m| m.get_child("Model"))
.and_then(|m| m.get_text())
.map(|t| t.to_string()),
amplification_gain: detector
.get_child("AmplificationGain")
.and_then(|a| a.get_text())
.and_then(|t| t.parse().ok()),
gain: detector
.get_child("Gain")
.and_then(|g| g.get_text())
.and_then(|t| t.parse().ok()),
zoom: detector
.get_child("Zoom")
.and_then(|z| z.get_text())
.and_then(|t| t.parse().ok()),
r#type: Some(
detector
.get_child("Type")
.and_then(|t| t.get_text())
.and_then(|t| t.parse().ok())
.unwrap_or(ome::DetectorType::Other),
),
..Default::default()
})
}
}
} else if let Version::Other = version {
for detector in &detectors.children {
if let Some(detector) = detector.as_element() {
let n = ome.instrument[0].detector.len();
ome.instrument[0].detector.push(ome::Detector {
id: detector
.attributes
.get("Id")
.map(|i| i.replace(" ", ""))
.unwrap_or(format!("Detector:{}", n)),
model: detector
.get_child("Manufacturer")
.and_then(|m| m.get_child("Model"))
.and_then(|m| m.get_text())
.map(|t| t.to_string()),
r#type: Some(
detector
.get_child("Type")
.and_then(|t| t.get_text())
.and_then(|t| t.parse().ok())
.unwrap_or(ome::DetectorType::Other),
),
..Default::default()
})
}
}
}
}
// set objectives
if let Some(objectives) = instrument.and_then(|i| i.get_child("Objectives")) {
for objective in &objectives.children {
if let Some(objective) = objective.as_element() {
let n = ome.instrument[0].objective.len();
ome.instrument[0].objective.push(ome::Objective {
id: objective
.attributes
.get("Id")
.map(|t| t.to_string())
.unwrap_or(format!("Objective:{}", n)),
model: objective
.get_child("Manufacturer")
.and_then(|m| m.get_child("Model"))
.and_then(|m| m.get_text())
.map(|t| t.to_string()),
lens_na: objective
.get_child("LensNA")
.and_then(|l| l.get_text())
.and_then(|l| l.parse().ok()),
nominal_magnification: objective
.get_child("NominalMagnification")
.and_then(|n| n.get_text())
.and_then(|l| l.parse().ok()),
..Default::default()
})
}
}
}
// set tube lenses
let pat = regex::Regex::new(r"\d+[,.]\d*")?;
if let Version::One = version {
if let Some(multi_track_setup) = multi_track_setup {
for (idx, track_setup) in multi_track_setup.children.iter().enumerate() {
if let Some(tube_lens) = track_setup
.as_element()
.and_then(|i| i.get_child("TubeLens"))
{
let text = tube_lens.get_text().map(|t| t.to_string());
ome.instrument[0].objective.push(ome::Objective {
id: format!("Objective:Tubelens:{idx}"),
nominal_magnification: Some(
text.as_ref()
.map(|t| t.replace(",", "."))
.and_then(|t| {
pat.captures(t.as_str())
.and_then(|c| c.get(0))
.and_then(|c| c.as_str().parse().ok())
})
.unwrap_or(1.0),
),
model: text,
..Default::default()
});
}
}
}
} else if let Version::Other = version
&& let Some(tube_lenses) = instrument.and_then(|i| i.get_child("TubeLenses"))
{
for (idx, tube_lens) in tube_lenses.children.iter().enumerate() {
if let Some(tube_lens) = tube_lens.as_element() {
let text = tube_lens.attributes.get("Name").map(|t| t.to_string());
ome.instrument[0].objective.push(ome::Objective {
id: format!(
"Objective:{}",
tube_lenses
.attributes
.get("Id")
.unwrap_or(&format!("Tubelens:{}", idx))
),
nominal_magnification: Some(
text.as_ref()
.map(|t| t.replace(",", "."))
.and_then(|t| {
pat.captures(t.as_str())
.and_then(|c| c.get(0))
.and_then(|c| c.as_str().parse().ok())
})
.unwrap_or(1.0),
),
model: text,
..Default::default()
})
}
}
}
// set light sources
if let Some(light_sources) = instrument.and_then(|i| i.get_child("LightSources")) {
if let Version::One = version {
for (idx, light_source) in light_sources.children.iter().enumerate() {
if let Some(light_source) = light_source.as_element()
&& let Some(laser) = light_source
.get_child("LightSourceType")
.and_then(|l| l.get_child("Laser"))
{
ome.instrument[0]
.light_source_group
.push(ome::LightSourceGroup::Laser(ome::Laser {
id: light_source
.attributes
.get("Id")
.map(|i| i.to_string())
.unwrap_or(format!("Laser:{}", idx)),
model: light_source
.get_child("Manufacturer")
.and_then(|m| m.get_child("Model"))
.and_then(|m| m.get_text())
.map(|t| t.to_string()),
power: light_source
.get_child("Power")
.and_then(|p| p.get_text())
.and_then(|t| t.parse().ok()),
wavelength: laser
.get_child("Wavelength")
.and_then(|w| w.get_text())
.and_then(|t| t.parse().ok()),
..Default::default()
}))
}
}
} else if let Version::Other = version {
let pat = regex::Regex::new(r"^.*?(\d*)$")?;
for (idx, light_source) in light_sources.children.iter().enumerate() {
if let Some(light_source) = light_source.as_element()
&& light_source
.get_child("LightSourceType")
.and_then(|l| l.get_child("Laser"))
.is_some()
{
let id = light_source.attributes.get("Id");
ome.instrument[0]
.light_source_group
.push(ome::LightSourceGroup::Laser(ome::Laser {
id: format!(
"LightSource:{}",
id.unwrap_or(&format!("{}", idx))
),
power: light_source
.get_child("Power")
.and_then(|p| p.get_text())
.and_then(|t| t.parse().ok()),
wavelength: id.and_then(|i| {
pat.captures(i)
.and_then(|c| c.get(1))
.and_then(|c| c.as_str().parse().ok())
}),
..Default::default()
}))
}
}
}
}
// set filters
if let Version::One = version
&& let Some(multi_track_setup) = multi_track_setup
{
for (idx, track_setup) in multi_track_setup.children.iter().enumerate() {
if let Some(track_setup) = track_setup.as_element()
&& let Some(beam_splitters) = track_setup.get_child("BeamSplitters")
{
for beam_splitter in &beam_splitters.children {
if let Some(beam_splitter) = beam_splitter.as_element() {
ome.instrument[0].filter_set.push(ome::FilterSet {
id: format!("FilterSet:{}", idx),
model: beam_splitter
.get_child("Filter")
.and_then(|f| f.get_text())
.map(|t| t.to_string()),
..Default::default()
})
}
}
}
}
}
// get positions
let (pos_x, pos_y, pos_z) = if let Version::One = version {
image
.and_then(|i| i.get_child("S"))
.and_then(|s| s.get_child("Scenes"))
.and_then(|s| s.children.first())
.and_then(|c| c.as_element())
.and_then(|e| e.get_child("Positions"))
.and_then(|p| p.children.first())
.and_then(|p| p.as_element())
.map(|p| {
(
p.attributes.get("X").and_then(|x| x.parse::<f32>().ok()),
p.attributes.get("Y").and_then(|y| y.parse::<f32>().ok()),
p.attributes.get("Z").and_then(|z| z.parse::<f32>().ok()),
)
})
.unwrap_or((None, None, None))
} else if let Version::Other = version {
image
.and_then(|i| i.get_child("Dimensions"))
.and_then(|d| d.get_child("S"))
.and_then(|s| s.get_child("Scenes"))
.and_then(|s| s.get_child("CenterPosition"))
.and_then(|c| c.get_text())
.map(|s| {
let c = s
.split(',')
.take(2)
.map(|i| i.parse::<f32>().ok())
.collect::<Vec<_>>();
(
c.first().cloned().flatten(),
c.get(1).cloned().flatten(),
None,
)
})
.unwrap_or((None, None, None))
} else {
(None, None, None)
};
// set pixels
if let Some(image) = information.and_then(|i| i.get_child("Image")) {
let mut pxsize_x: Option<f32> = None;
let mut pxsize_y: Option<f32> = None;
let mut pxsize_z: Option<f32> = None;
if let Some(items) = metadata
.get_child("Scaling")
.and_then(|m| m.get_child("Items"))
{
for distance in &items.children {
if let Some(distance) = distance.as_element() {
match distance.attributes.get("Id").map(|i| i.as_str()) {
Some("X") => {
pxsize_x = distance
.get_child("Value")
.and_then(|v| v.get_text())
.and_then(|v| v.parse().ok())
}
Some("Y") => {
pxsize_y = distance
.get_child("Value")
.and_then(|v| v.get_text())
.and_then(|v| v.parse().ok())
}
Some("Z") => {
pxsize_z = distance
.get_child("Value")
.and_then(|v| v.get_text())
.and_then(|v| v.parse().ok())
}
_ => {}
}
}
}
}
let objective_settings = image.get_child("ObjectiveSettings");
ome.image.push(ome::Image {
id: "Image:0".to_string(),
name: information
.and_then(|i| i.get_child("Document"))
.and_then(|d| d.get_child("Name"))
.and_then(|n| n.get_text())
.map(|t| format!("{} #1", t)),
pixels: ome::Pixels {
id: "Pixels:0".to_string(),
size_x: self.shape.x as i32,
size_y: self.shape.y as i32,
size_z: self.shape.z as i32,
size_c: self.shape.c as i32,
size_t: self.shape.t as i32,
dimension_order: ome::PixelsDimensionOrderType::Xyczt,
r#type: image
.get_child("PixelType")
.and_then(|p| p.get_text())
.and_then(|t| t.to_lowercase().replace("gray", "uint").parse().ok())
.unwrap_or(ome::PixelType::Uint16),
significant_bits: image
.get_child("ComponentBitCount")
.and_then(|c| c.get_text())
.and_then(|t| t.parse().ok()),
big_endian: None,
interleaved: None,
metadata_only: None,
physical_size_x: pxsize_x.map(|p| p * 1e9),
physical_size_x_unit: ome::UnitsLength::nm,
physical_size_y: pxsize_y.map(|p| p * 1e9),
physical_size_y_unit: ome::UnitsLength::nm,
physical_size_z: pxsize_z.map(|p| p * 1e9),
physical_size_z_unit: ome::UnitsLength::nm,
time_increment: None,
time_increment_unit: ome::UnitsTime::s,
channel: Vec::new(),
bin_data: Vec::new(),
tiff_data: Vec::new(),
plane: Vec::new(),
},
experimenter_ref: Some(ome::AnnotationRef {
id: "Experimenter:0".to_string(),
}),
instrument_ref: Some(ome::AnnotationRef {
id: "Instrument:0".to_string(),
}),
objective_settings: objective_settings.map(|o| ome::ObjectiveSettings {
id: o
.get_child("ObjectiveRef")
.and_then(|o| o.attributes.get("Id"))
.map(|t| t.to_string())
.unwrap_or("Objective:0".to_string()),
medium: o
.get_child("Medium")
.and_then(|m| m.get_text())
.and_then(|t| t.parse().ok()),
refractive_index: o
.get_child("RefractiveIndex")
.and_then(|r| r.get_text())
.and_then(|t| t.parse().ok()),
..Default::default()
}),
stage_label: Some(ome::StageLabel {
name: "Scene position #0".to_string(),
x: pos_x,
x_unit: ome::UnitsLength::um,
y: pos_y,
y_unit: ome::UnitsLength::um,
z: pos_z,
z_unit: ome::UnitsLength::um,
}),
acquisition_date: None,
description: None,
experiment_ref: None,
experimenter_group_ref: None,
imaging_environment: None,
roi_ref: Vec::new(),
microbeam_manipulation_ref: Vec::new(),
annotation_ref: Vec::new(),
});
}
// channels
let channels_im = image
.and_then(|i| i.get_child("Dimensions"))
.and_then(|d| d.get_child("Channels"))
.map(|c| {
c.children
.iter()
.filter_map(|c| {
c.as_element()
.and_then(|e| e.attributes.get("Id").map(|id| (id, e)))
})
.collect::<IndexMap<_, _>>()
})
.unwrap_or_else(IndexMap::new);
let channels_ds = display_setting
.and_then(|d| d.get_child("Channels"))
.map(|c| {
c.children
.iter()
.filter_map(|c| {
c.as_element()
.and_then(|e| e.attributes.get("Id").map(|id| (id, e)))
})
.collect::<HashMap<_, _>>()
})
.unwrap_or_else(HashMap::new);
let channels_ts = experiment
.and_then(|e| e.get_child("ExperimentBlocks"))
.and_then(|e| e.get_child("AcquisitionBlock"))
.and_then(|a| a.get_child("MultiTrackSetup"))
.map(|m| {
m.children
.iter()
.filter_map(|t| {
t.as_element().and_then(|ts| {
ts.get_child("Detectors").map(move |d| {
d.children.iter().filter_map(move |d| {
d.as_element()
.and_then(|d| d.attributes.get("Id").map(|id| (id, ts)))
})
})
})
})
.flatten()
.collect::<HashMap<_, _>>()
})
.unwrap_or_else(HashMap::new);
// set channels
for (idx, (&key, &channel)) in channels_im.iter().enumerate() {
let detector_settings = channel.get_child("DetectorSettings");
let laser_scan_info = channel.get_child("LaserScanInfo");
let detector = detector_settings.and_then(|ds| ds.get_child("Detector"));
let filter_set = channels_ts
.get(key)
.and_then(|c| c.get_child("BeamSplitters"))
.and_then(|b| b.children.first())
.and_then(|b| b.as_element())
.and_then(|b| b.get_child("Filter"))
.and_then(|f| f.get_text())
.map(|t| t.to_string())
.and_then(|fs| {
ome.instrument[0]
.filter_set
.iter()
.find(|&f| Some(&fs) == f.model.as_ref())
});
let light_source_settings = if let Version::One = version {
// no space in ome for multiple lightsources simultaneously
channel
.get_child("LightSourcesSettings")
.and_then(|ls| {
if ls.children.is_empty() {
None
} else if ls.children.len() > idx {
Some(&ls.children[idx])
} else {
Some(&ls.children[0])
}
})
.and_then(|lss| lss.as_element())
.and_then(|lss| lss.get_child("LightSource").map(|ls| (lss, ls)))
.and_then(|(lss, ls)| ls.attributes.get("Id").map(|id| (lss, id)))
.map(|(lss, id)| ome::LightSourceSettings {
id: id.to_string(),
attenuation: lss
.get_child("Attenuation")
.and_then(|a| a.get_text())
.and_then(|t| t.parse().ok()),
wavelength: lss
.get_child("Wavelength")
.and_then(|w| w.get_text())
.and_then(|t| t.parse().ok())
.filter(|&w| w > 0.0),
wavelength_unit: ome::UnitsLength::nm,
})
} else if let Version::Other = version {
channel.get_child("LightSourcesSettings").and_then(|ls| {
ls.children.first().and_then(|l| l.as_element()).map(|f| {
ome::LightSourceSettings {
id: format!(
"LightSource:{}",
ls.children
.iter()
.filter_map(|l| l
.as_element()
.and_then(|i| i.attributes.get("Id")))
.join("_")
),
attenuation: f
.get_child("Attenuation")
.and_then(|a| a.get_text())
.and_then(|t| t.parse().ok()),
wavelength: f
.get_child("Wavelength")
.and_then(|w| w.get_text())
.and_then(|t| t.parse().ok()),
wavelength_unit: ome::UnitsLength::nm,
}
})
})
} else {
None
};
ome.image[0].pixels.channel.push(ome::Channel {
id: format!("Channel:{}", idx),
name: channel.attributes.get("Name").map(|n| n.to_string()),
acquisition_mode: channel
.get_child("AcquisitionMode")
.and_then(|a| a.get_text())
.and_then(|t| {
t.replace("SingleMoleculeLocalisation", "SingleMoleculeImaging")
.parse()
.ok()
}),
color: channel
.attributes
.get("Id")
.and_then(|id| channels_ds.get(id))
.and_then(|c| c.get_child("Color"))
.and_then(|c| c.get_text())
.and_then(|c| c.parse::<Color>().ok())
.map(|i| {
let rgb = i.to_rgb();
65536 * (rgb[0] as i32) + 256 * (rgb[1] as i32) + (rgb[2] as i32)
})
.unwrap_or(0),
detector_settings: detector.and_then(|d| d.attributes.get("Id")).map(|id| {
ome::DetectorSettings {
id: id.to_string().replace(" ", ""),
binning: Some(
detector_settings
.and_then(|d| d.get_child("Binning"))
.and_then(|b| {
if b.children.is_empty() {
None
} else if b.children.len() < self.shape.c {
b.children.first()
} else {
b.children.get(idx)
}
})
.and_then(|b| b.as_text())
.and_then(|t| t.parse().ok())
.unwrap_or(ome::BinningType::Other),
),
..Default::default()
}
}),
emission_wavelength: channel
.get_child("EmissionWaveLength")
.and_then(|e| e.get_text())
.and_then(|t| t.parse().ok())
.filter(|&w| w > 0.0),
emission_wavelength_unit: ome::UnitsLength::nm,
excitation_wavelength: light_source_settings
.as_ref()
.and_then(|ls| ls.wavelength)
.or_else(|| {
channel
.get_child("ExcitationWavelength")
.and_then(|e| e.get_text())
.and_then(|t| t.parse().ok())
}),
excitation_wavelength_unit: light_source_settings
.as_ref()
.map(|ls| ls.wavelength_unit)
.unwrap_or(ome::UnitsLength::nm),
filter_set_ref: filter_set.map(|fs| ome::AnnotationRef { id: fs.id.clone() }),
illumination_type: channel
.get_child("IlluminationType")
.and_then(|i| i.get_text())
.and_then(|t| t.parse().ok()),
light_source_settings,
samples_per_pixel: laser_scan_info
.and_then(|ls| ls.get_child("Averaging"))
.and_then(|a| a.get_text())
.and_then(|t| t.parse().ok()),
contrast_method: channel
.get_child("ContrastMethod")
.and_then(|cm| cm.get_text())
.and_then(|t| t.parse().ok()),
..Default::default()
});
}
// set planes
let time_stamps = self
.attachments()?
.get("TimeStamps")
.and_then(|a| a.try_into_float().ok())
.map(|mut time_stamps| {
time_stamps.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut dt = time_stamps
.array_windows()
.filter_map(|[a, b]| if b > a { Some(b - a) } else { None })
.collect::<Vec<_>>();
if !dt.is_empty() {
let mean = dt.iter().sum::<f64>() / dt.len() as f64;
let var =
dt.iter().map(|i| (i - mean).powi(2)).sum::<f64>() / dt.len() as f64;
if dt.len() > 2 && var.sqrt() / mean > 0.02 {
dt.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = dt.len();
let median = if n.is_multiple_of(2) {
(dt[n / 2 - 1] + dt[n / 2]) / 2.0
} else {
dt[n / 2]
};
println!(
"warning: delta_t is inconsistent, using median value {}",
median
);
(0..time_stamps.len())
.map(|i| (i as f64) * median)
.collect::<Vec<_>>()
} else {
time_stamps
}
} else {
time_stamps
}
});
let exposure_times = channels_im
.iter()
.filter_map(|(_, &c)| {
c.get_child("LaserScanInfo")
.and_then(|ls| ls.get_child("FrameTime"))
.and_then(|ft| ft.get_text())
.and_then(|t| t.parse::<f32>().ok())
})
.collect::<Vec<_>>();
for c in 0..self.shape.c {
let exposure_time = if exposure_times.is_empty() {
None
} else if c < exposure_times.len() {
Some(exposure_times[0])
} else {
Some(exposure_times[c])
};
for z in 0..self.shape.z {
for t in 0..self.shape.t {
ome.image[0].pixels.plane.push(ome::Plane {
the_c: Some(c as i32),
the_z: Some(z as i32),
the_t: Some(t as i32),
delta_t: time_stamps
.as_ref()
.and_then(|ts| ts.get(t).map(|&t| t as f32)),
position_x: pos_x,
position_x_unit: ome::UnitsLength::nm,
position_y: pos_y,
position_y_unit: ome::UnitsLength::nm,
position_z: pos_z,
position_z_unit: ome::UnitsLength::nm,
exposure_time,
exposure_time_unit: ome::UnitsTime::s,
..Default::default()
})
}
}
}
// annotations
if let Some(layers) = metadata.get_child("Layers") {
for layer in &layers.children {
if let Some(geometry) = layer
.as_element()
.and_then(|layer| layer.get_child("Elements"))
.and_then(|elements| elements.get_child("Rectangle"))
.and_then(|rectangle| rectangle.get_child("Geometry"))
{
ome.roi.push(ome::Roi {
id: format!("ROI:{}", ome.roi.len()),
union: Some(ome::RoiUnion {
shape_group: vec![ome::ShapeGroup::Rectangle(ome::Rectangle {
id: "Shape:0:0".to_string(),
height: geometry
.get_child("Height")
.and_then(|height| height.get_text())
.and_then(|t| t.parse().ok())
.unwrap_or(f32::NAN),
width: geometry
.get_child("Width")
.and_then(|width| width.get_text())
.and_then(|t| t.parse().ok())
.unwrap_or(f32::NAN),
x: geometry
.get_child("Left")
.and_then(|left| left.get_text())
.and_then(|t| t.parse().ok())
.unwrap_or(f32::NAN),
y: geometry
.get_child("Right")
.and_then(|right| right.get_text())
.and_then(|t| t.parse().ok())
.unwrap_or(f32::NAN),
..Default::default()
})],
}),
..Default::default()
})
}
}
}
}
Ok(ome)
}
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
let reader = self.get_reader()?;
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
if let Some(indices) = self.block_map.get(&(c, z, t)) {
for &i in indices {
if let Ok(info) = reader.try_get_sub_block_info_for_index(i) {
let rect = info.get_logical_rect();
min_x = min_x.min(rect.get_x());
min_y = min_y.min(rect.get_y());
}
}
}
macro_rules! get_frame {
($t:tt, $n:expr) => {{
let mut array = Array2::zeros((self.shape.y, self.shape.x));
if let Some(indices) = self.block_map.get(&(c, z, t)) {
for &i in indices {
let sub_block = reader.read_sub_block(i)?;
let bitmap = sub_block.create_bitmap()?.lock()?;
let bytes = bitmap.lock_info.get_data_roi();
let info = sub_block.get_info()?;
let rect = info.get_logical_rect();
let x = (rect.get_x() - min_x) as usize;
let y = (rect.get_y() - min_y) as usize;
let w = rect.get_w() as usize;
let h = rect.get_h() as usize;
array
.slice_mut(s![x..x + w, y..y + h])
.assign(&Array2::from_shape_vec(
(w, h),
bytes
.chunks($n)
.map(|x| $t::from_le_bytes(x.try_into().unwrap()))
.collect(),
)?);
}
}
Ok(ArrayT::from(array))
}};
}
match self.pixel_type {
PixelType::I8 => get_frame!(i8, 1),
PixelType::U8 => get_frame!(u8, 1),
PixelType::I16 => get_frame!(i16, 2),
PixelType::U16 => get_frame!(u16, 2),
PixelType::I32 => get_frame!(i32, 4),
PixelType::U32 => get_frame!(u32, 4),
PixelType::F32 => get_frame!(f32, 4),
PixelType::F64 => get_frame!(f64, 8),
PixelType::I64 => get_frame!(i64, 8),
PixelType::U64 => get_frame!(u64, 8),
PixelType::I128 => get_frame!(i128, 16),
PixelType::U128 => get_frame!(u128, 16),
PixelType::F128 => get_frame!(f64, 8),
}
}
fn path(&self) -> &Path {
&self.path
}
fn series(&self) -> usize {
self.series
}
fn position(&self) -> usize {
self.position
}
fn shape(&self) -> &Shape {
&self.shape
}
fn pixel_type(&self) -> &PixelType {
&self.pixel_type
}
fn get_available_positions<P>(path: P, series: usize) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>,
{
let new = Self {
reader: ThreadLocal::default(),
block_map: HashMap::new(),
path: path.as_ref().to_path_buf(),
series,
position: 0,
shape: Shape::default(),
pixel_type: PixelType::U8,
};
let reader = new.get_reader()?;
let statistics_simple = reader.get_statistics_simple()?;
let c = statistics_simple.get_sub_block_count();
let mut positions = HashSet::new();
for i in 0..c {
if let Ok(info) = reader.try_get_sub_block_info_for_index(i) {
let coordinate = info.get_coordinate();
let d = Dimension::vec_from_bitflags(coordinate.get_dimensions_valid());
let v = coordinate.get_value();
if d.contains(&Dimension::S) && v[Dimension::S as usize - 1] as usize != series {
continue;
}
positions.insert(info.get_m_index() as usize);
}
}
if positions.is_empty() {
positions.insert(0);
}
Ok(positions)
}
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>,
{
let new = Self {
reader: ThreadLocal::default(),
block_map: HashMap::new(),
path: path.as_ref().to_path_buf(),
series: 0,
position: 0,
shape: Shape::default(),
pixel_type: PixelType::U8,
};
let reader = new.get_reader()?;
let statistics_simple = reader.get_statistics_simple()?;
let c = statistics_simple.get_sub_block_count();
let mut series = HashSet::new();
for i in 0..c {
if let Ok(info) = reader.try_get_sub_block_info_for_index(i) {
let coordinate = info.get_coordinate();
let d = Dimension::vec_from_bitflags(coordinate.get_dimensions_valid());
let v = coordinate.get_value();
if d.contains(&Dimension::S) {
series.insert(v[Dimension::S as usize - 1] as usize);
}
}
}
if series.is_empty() {
series.insert(0);
}
Ok(series)
}
}
impl TryFrom<libczirw_sys::PixelType> for PixelType {
type Error = Error;
fn try_from(value: libczirw_sys::PixelType) -> Result<Self, Self::Error> {
match value {
libczirw_sys::PixelType::Gray8 => Ok(PixelType::U8),
libczirw_sys::PixelType::Gray16 => Ok(PixelType::U16),
libczirw_sys::PixelType::Gray32 => Ok(PixelType::U32),
libczirw_sys::PixelType::Gray64Float => Ok(PixelType::F64),
_ => Err(Error::Conversion(format!("{:?}", value))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn open(file: &str) -> Result<CziReader, Error> {
let path = std::env::current_dir()?
.join("tests")
.join("files")
.join(file);
CziReader::new(&path, 0, 0)
}
// fn write_xml(file: &str, xml: &str) -> Result<(), Error> {
// let path = std::env::current_dir()?
// .join("tests")
// .join("files")
// .join("czi_xml")
// .join(file)
// .with_extension("xml");
// std::fs::write(path, xml)?;
// Ok(())
// }
macro_rules! test_metadata {
($($name:ident: $file:expr $(,)?)*) => {
$(
#[test]
fn $name() -> Result<(), Error> {
let czi = open($file)?;
// let ome = czi.metadata()?;
// write_xml($file, &czi.metadata_xml()?)?;
// let ome = czi.metadata()?;
// println!("{:?}", ome);
println!("{}", czi.view().squeeze()?.summary()?);
// println!("block map: {:#?}", czi.block_map);
Ok(())
}
)*
};
}
test_metadata! {
metadata_a: "czi/1xp53-01-AP1.czi",
metadata_b: "czi/beads_2023_05_04__19_00_22.czi",
metadata_c: "czi/Experiment-2029.czi",
metadata_d: "czi/MK022_cE9_1-01-Airyscan Processing-01-Scene-2-P1.czi",
metadata_e: "czi/YTL1849A131_2023_05_04__13_36_36.czi",
metadata_f: "czi/EU_UV_t=1-01.czi",
}
}