use crate::error::Error;
use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape};
use ndarray::Array2;
use ome_metadata::{Ome, ome};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TiffSeqReader {
path: PathBuf,
series: usize,
position: usize,
shape: Shape,
pixel_type: PixelType,
filedict: HashMap<(usize, usize, usize), PathBuf>,
cnamelist: Vec<String>,
#[serde(skip)]
metadata_map: HashMap<String, serde_yaml::Value>,
}
impl From<TiffSeqReader> for DynReader {
fn from(value: TiffSeqReader) -> Self {
DynReader::TiffSeq(value)
}
}
impl Hash for TiffSeqReader {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
self.series.hash(state);
self.position.hash(state);
}
}
impl TiffSeqReader {
fn find_pos_dir<P: AsRef<Path>>(path: P, series: usize) -> Result<PathBuf, Error> {
let pat = Regex::new(&format!("(?i)^(?:\\d+-)?Pos0*{}", series))?;
let pos_dir = path.as_ref().to_path_buf();
if pos_dir
.file_name()
.map(|n| pat.is_match(&n.to_string_lossy()))
== Some(true)
{
return Ok(pos_dir);
}
for file in pos_dir.read_dir()?.flatten() {
let p = file.path();
if p.file_name().map(|n| pat.is_match(&n.to_string_lossy())) == Some(true) {
return Ok(p);
}
}
Ok(pos_dir)
}
fn list_tiff_files<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>, Error> {
let pat = Regex::new(r"(?i)^img_\d{3,}.*\d{3,}\.tif$")?;
let mut files = Vec::new();
for entry in std::fs::read_dir(path.as_ref())? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if pat.is_match(&name) {
files.push(entry.path());
}
}
files.sort();
Ok(files)
}
fn read_metadata_from_file(dir: &Path) -> Result<HashMap<String, serde_yaml::Value>, Error> {
let md_path = dir.join("metadata.txt");
let text = std::fs::read_to_string(&md_path)?;
let parsed: serde_yaml::Value = serde_yaml::from_str(&text)?;
let mut map = HashMap::new();
map.insert("Info".to_string(), parsed);
Ok(map)
}
fn read_tiff_dimensions(path: &Path) -> Result<(usize, usize), Error> {
let file = std::fs::File::open(path)?;
let mut decoder = Decoder::new(file)?;
let width = decoder.get_tag(Tag::ImageWidth)?.into_u32()? as usize;
let height = decoder.get_tag(Tag::ImageLength)?.into_u32()? as usize;
Ok((width, height))
}
}
impl PartialEq for TiffSeqReader {
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 TiffSeqReader {}
impl Reader for TiffSeqReader {
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let pos_path = Self::find_pos_dir(path, series)?;
let filelist = Self::list_tiff_files(&pos_path)?;
if filelist.is_empty() {
return Err(Error::InvalidReader(
"TiffSeqReader".to_string(),
pos_path.display().to_string(),
"no tiff files found".to_string(),
));
}
let first = &filelist[0];
let (width, height) = Self::read_tiff_dimensions(first)?;
let metadata = Self::read_metadata_from_file(&pos_path)?;
let info = metadata
.get("Info")
.and_then(|v| v.as_mapping())
.ok_or_else(|| Error::Parse("missing Info key in tag 50839".to_string()))?;
let lookup = |key: &str| {
info.get(serde_yaml::Value::String(key.to_string()))
.or_else(|| {
info.get(serde_yaml::Value::String("Summary".to_string()))
.and_then(|s| s.as_mapping())
.and_then(|s| s.get(serde_yaml::Value::String(key.to_string())))
})
};
let pixel_type_str = lookup("PixelType")
.and_then(|v| v.as_str())
.unwrap_or("gray16");
let pixel_type =
PixelType::from_str(&pixel_type_str.to_lowercase().replace("gray", "uint"))
.unwrap_or(PixelType::U16);
let cnamelist: Vec<String> = lookup("Summary")
.and_then(|v| v.get("ChNames"))
.and_then(|v| v.as_sequence())
.map(|seq| {
seq.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let pattern_c = Regex::new(r"(?i)img_\d{3,}_(.*)_\d{3,}$")?;
let pattern_z = Regex::new(r"(\d{3,})$")?;
let pattern_t = Regex::new(r"(?i)img_(\d{3,})")?;
let cnamelist: Vec<String> = if cnamelist.is_empty() {
let mut names: Vec<String> = filelist
.iter()
.filter_map(|f| {
let stem = f.file_stem()?;
let stem = stem.to_string_lossy();
pattern_c
.captures(&stem)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
})
.collect();
names.sort();
names.dedup();
names
} else {
cnamelist
.into_iter()
.filter(|c| filelist.iter().any(|f| f.to_string_lossy().contains(c)))
.collect()
};
let mut filedict = HashMap::new();
for f in &filelist {
let stem = f
.file_stem()
.ok_or_else(|| Error::Parse("no stem".to_string()))?
.to_string_lossy()
.to_string();
let chan = pattern_c
.captures(&stem)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
.ok_or_else(|| Error::Parse(format!("could not parse channel from {}", stem)))?;
let z: usize = pattern_z
.captures(&stem)
.and_then(|c| c.get(1))
.map(|m| m.as_str().parse().unwrap_or(0))
.ok_or_else(|| Error::Parse(format!("could not parse z from {}", stem)))?;
let t: usize = pattern_t
.captures(&stem)
.and_then(|c| c.get(1))
.map(|m| m.as_str().parse().unwrap_or(0))
.ok_or_else(|| Error::Parse(format!("could not parse t from {}", stem)))?;
let c_idx = cnamelist
.iter()
.position(|cn| cn == &chan)
.ok_or_else(|| Error::Parse(format!("channel '{}' not in cnamelist", chan)))?;
filedict.insert((c_idx, z, t), f.clone());
}
let size_c = filedict.keys().map(|(c, _, _)| c).max().unwrap_or(&0) + 1;
let size_z = filedict.keys().map(|(_, z, _)| z).max().unwrap_or(&0) + 1;
let size_t = filedict.keys().map(|(_, _, t)| t).max().unwrap_or(&0) + 1;
Ok(TiffSeqReader {
path: pos_path,
series,
position,
shape: Shape {
c: size_c,
z: size_z,
t: size_t,
y: height,
x: width,
..Default::default()
},
pixel_type,
filedict,
cnamelist,
metadata_map: metadata,
})
}
fn metadata(&self) -> Result<Ome, Error> {
let mut ome = Ome::default();
let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping());
let slookup =
|key: &str| info.and_then(|m| m.get(serde_yaml::Value::String(key.to_string())));
let summary = slookup("Summary").and_then(|v| v.as_mapping());
let summary_lookup =
|key: &str| summary.and_then(|m| m.get(serde_yaml::Value::String(key.to_string())));
let first_frame = info.and_then(|m| {
m.iter()
.find(|(k, _)| k.as_str().is_some_and(|s| s.starts_with("FrameKey-")))
.and_then(|(_, v)| v.as_mapping())
});
let frame_lookup =
|key: &str| first_frame.and_then(|m| m.get(serde_yaml::Value::String(key.to_string())));
let ome_pixel_type = match self.pixel_type {
PixelType::I8 => ome::PixelType::Int8,
PixelType::U8 => ome::PixelType::Uint8,
PixelType::I16 => ome::PixelType::Int16,
PixelType::U16 => ome::PixelType::Uint16,
PixelType::I32 => ome::PixelType::Int32,
PixelType::U32 => ome::PixelType::Uint32,
PixelType::F32 => ome::PixelType::Float,
PixelType::F64 => ome::PixelType::Double,
_ => ome::PixelType::Bit,
};
let zstep = summary_lookup("z-step_um").and_then(|v| v.as_f64());
let exposure = frame_lookup("Exposure-ms")
.and_then(|v| v.as_f64())
.map(|v| v as f32 / 1000.0);
let objective_str = frame_lookup("ZeissObjectiveTurret-Label")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tubelens_str = frame_lookup("ZeissOptovar-Label")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let filter_set_str = frame_lookup("ZeissReflectorTurret-Label")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let binning_str = frame_lookup("Hamamatsu_sCMOS-Binning")
.or_else(|| frame_lookup("Binning"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let user_name = summary_lookup("UserName").and_then(|v| v.as_str());
let pxsize = frame_lookup("PixelSizeUm")
.and_then(|v| v.as_f64())
.or_else(|| summary_lookup("PixelSize_um").and_then(|v| v.as_f64()));
let pxsize = pxsize.map(|v| {
if v == 0.0 {
let camera_str = frame_lookup("Core-Camera")
.and_then(|v| v.as_str())
.unwrap_or("");
let cam_px_um = if camera_str.to_lowercase().contains("hamamatsu") {
Some(6.5_f64)
} else {
None
};
let bin_factor = binning_str
.as_ref()
.and_then(|s| s.split('x').next().and_then(|n| n.parse::<f64>().ok()))
.unwrap_or(1.0);
let obj_mag = objective_str.as_ref().and_then(|s| {
Regex::new(r"(\d+(?:\.\d+)?)x")
.ok()
.and_then(|re| re.captures(s))
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().parse::<f64>().ok())
});
let tube_mag = tubelens_str.as_ref().and_then(|s| {
Regex::new(r"(\d+(?:[,.]\d+)?)x$")
.ok()
.and_then(|re| re.captures(s))
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().replace(",", ".").parse::<f64>().ok())
});
match (cam_px_um, obj_mag, tube_mag) {
(Some(cam), Some(obj), Some(tube)) => cam * bin_factor / (obj * tube),
(Some(cam), Some(obj), None) => cam * bin_factor / obj,
_ => v,
}
} else {
v
}
});
let objective = objective_str.as_ref().map(|s| {
let mag = Regex::new(r"(\d+(?:\.\d+)?)x")
.ok()
.and_then(|re| re.captures(s))
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().parse::<f32>().ok());
let na = Regex::new(r"/(\d+\.\d+)")
.ok()
.and_then(|re| re.captures(s))
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().parse::<f32>().ok());
let immersion = if s.to_lowercase().contains("oil") {
Some(ome::ObjectiveImmersionType::Oil)
} else {
None
};
ome::Objective {
id: "Objective:0".to_string(),
manufacturer: Some("Zeiss".to_string()),
model: Some(s.clone()),
lens_na: na,
nominal_magnification: mag,
immersion,
..Default::default()
}
});
let tubelens = tubelens_str.as_ref().map(|s| {
let mag = Regex::new(r"(\d+(?:[,.]\d+)?)x$")
.ok()
.and_then(|re| re.captures(s))
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().replace(",", ".").parse::<f32>().ok());
ome::Objective {
id: "Objective:Tubelens:0".to_string(),
manufacturer: Some("Zeiss".to_string()),
model: Some(s.clone()),
nominal_magnification: mag,
..Default::default()
}
});
let filter_set = filter_set_str.as_ref().map(|s| ome::FilterSet {
id: "FilterSet:0".to_string(),
model: Some(s.clone()),
..Default::default()
});
let mut instrument = ome::Instrument {
id: "Instrument:0".to_string(),
..Default::default()
};
if let Some(o) = objective {
instrument.objective.push(o);
}
if let Some(t) = tubelens {
instrument.objective.push(t);
}
instrument.detector.push(ome::Detector {
id: "Detector:0".to_string(),
manufacturer: Some("Hamamatsu".to_string()),
amplification_gain: Some(100.0),
..Default::default()
});
if let Some(f) = filter_set {
instrument.filter_set.push(f);
}
ome.instrument.push(instrument);
let mut 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: ome_pixel_type,
physical_size_x: pxsize.map(|v| v as f32),
physical_size_x_unit: ome::UnitsLength::um,
physical_size_y: pxsize.map(|v| v as f32),
physical_size_y_unit: ome::UnitsLength::um,
physical_size_z: zstep.map(|v| v as f32),
physical_size_z_unit: ome::UnitsLength::um,
time_increment: None,
time_increment_unit: ome::UnitsTime::s,
significant_bits: None,
interleaved: None,
big_endian: None,
channel: Vec::new(),
bin_data: Vec::new(),
tiff_data: Vec::new(),
metadata_only: None,
plane: Vec::new(),
};
for (c_idx, cname) in self.cnamelist.iter().enumerate() {
pixels.channel.push(ome::Channel {
id: format!("Channel:{}", c_idx),
name: Some(cname.clone()),
detector_settings: Some(ome::DetectorSettings {
id: "Detector:0".to_string(),
binning: binning_str.as_ref().map(|b| b.parse()).transpose()?,
gain: Some(100.0),
..Default::default()
}),
filter_set_ref: filter_set_str.as_ref().map(|_| ome::AnnotationRef {
id: "FilterSet:0".to_string(),
}),
..Default::default()
});
}
for c in 0..self.shape.c {
for z in 0..self.shape.z {
for t in 0..self.shape.t {
pixels.plane.push(ome::Plane {
the_c: Some(c as i32),
the_z: Some(z as i32),
the_t: Some(t as i32),
exposure_time: exposure,
..Default::default()
});
}
}
}
let mut image = ome::Image {
id: "Image:0".to_string(),
name: self
.path
.file_name()
.map(|n| n.to_string_lossy().to_string()),
pixels,
instrument_ref: Some(ome::AnnotationRef {
id: "Instrument:0".to_string(),
}),
objective_settings: objective_str.as_ref().map(|_| ome::ObjectiveSettings {
id: "Objective:0".to_string(),
..Default::default()
}),
acquisition_date: None,
description: None,
experimenter_ref: None,
experiment_ref: None,
experimenter_group_ref: None,
imaging_environment: None,
stage_label: None,
roi_ref: Vec::new(),
microbeam_manipulation_ref: Vec::new(),
annotation_ref: Vec::new(),
};
if let Some(uname) = user_name {
ome.experimenter.push(ome::Experimenter {
id: "Experimenter:0".to_string(),
user_name: Some(uname.to_string()),
..Default::default()
});
image.experimenter_ref = Some(ome::AnnotationRef {
id: "Experimenter:0".to_string(),
});
}
ome.image.push(image);
Ok(ome)
}
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
let file = self
.filedict
.get(&(c, z, t))
.ok_or_else(|| Error::OutOfBounds(c.max(z).max(t) as isize, 0))?;
let rdr = std::fs::File::open(file)?;
let mut decoder = Decoder::new(rdr)?;
match decoder.read_image()? {
DecodingResult::U8(d) => Ok(ArrayT::U8(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
DecodingResult::U16(d) => Ok(ArrayT::U16(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
DecodingResult::U32(d) => Ok(ArrayT::U32(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
DecodingResult::I8(d) => Ok(ArrayT::I8(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
DecodingResult::I16(d) => Ok(ArrayT::I16(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
DecodingResult::I32(d) => Ok(ArrayT::I32(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
DecodingResult::F32(d) => Ok(ArrayT::F32(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
DecodingResult::F64(d) => Ok(ArrayT::F64(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
d,
)?)),
_ => Err(Error::NotImplemented(
"unsupported TIFF pixel type".to_string(),
)),
}
}
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>,
{
Ok(HashSet::from([0]))
}
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>,
{
let pat = Regex::new(r"(?i)^(?:\d+-)?Pos(\d+)$")?;
let mut series = HashSet::new();
for entry in std::fs::read_dir(path.as_ref())? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let name = entry.file_name().to_string_lossy().to_string();
if let Some(caps) = pat.captures(&name)
&& let Ok(s) = caps[1].parse::<usize>()
{
series.insert(s);
}
}
}
Ok(series)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn open(file: &str) -> Result<TiffSeqReader, Error> {
let path = std::env::current_dir()?
.join("tests")
.join("files")
.join(file);
TiffSeqReader::new(&path, 0, 0)
}
macro_rules! test_metadata {
($($name:ident: $file:expr $(,)?)*) => {
$(
#[test]
fn $name() -> Result<(), Error> {
let ts = open($file)?;
println!("{}", ts.view().squeeze()?.summary()?);
Ok(())
}
)*
};
}
test_metadata! {
metadata_a: "tiffseq/4-Pos_001_002",
metadata_b: "tiffseq/20-Pos_005_005",
metadata_c: "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1",
}
}