- implement shape in Rust

- implement more readers
- fix downloading of bioformats jar
- (mostly) compatible with python version
This commit is contained in:
w.pomp
2026-07-13 13:40:34 +02:00
parent 705ca16379
commit ff7cd562af
36 changed files with 7946 additions and 2843 deletions
+483
View File
@@ -0,0 +1,483 @@
use crate::error::Error;
use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape};
use ndarray::Array2;
use ome_metadata::{Ome, ome};
use serde::{Deserialize, Serialize};
use std::cell::{RefCell, RefMut};
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use thread_local::ThreadLocal;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag;
#[derive(Debug, Serialize, Deserialize)]
pub struct TiffReader {
#[serde(skip)]
reader: ThreadLocal<RefCell<Decoder<File>>>,
path: PathBuf,
series: usize,
position: usize,
shape: Shape,
pixel_type: PixelType,
n_samples: usize,
p_ndim: u8,
planar: bool,
interval_t: f64,
pixel_size: Option<f64>,
}
impl From<TiffReader> for DynReader {
fn from(value: TiffReader) -> Self {
DynReader::Tiff(value)
}
}
impl Hash for TiffReader {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
self.series.hash(state);
self.position.hash(state);
}
}
impl Clone for TiffReader {
fn clone(&self) -> Self {
TiffReader {
reader: ThreadLocal::default(),
path: self.path.clone(),
series: self.series,
position: self.position,
shape: self.shape.clone(),
pixel_type: self.pixel_type,
n_samples: self.n_samples,
p_ndim: self.p_ndim,
planar: self.planar,
interval_t: self.interval_t,
pixel_size: self.pixel_size,
}
}
}
impl PartialEq for TiffReader {
fn eq(&self, other: &Self) -> bool {
self.path == other.path && self.series == other.series && self.position == other.position
}
}
impl Eq for TiffReader {}
impl TiffReader {
fn get_reader(&self) -> Result<RefMut<'_, Decoder<File>>, Error> {
self.reader
.get_or_try(|| {
let file = File::open(&self.path)?;
Ok(RefCell::new(Decoder::new(file)?))
})
.map(|i| i.borrow_mut())
}
}
fn pixel_type_from_bits_sample(bits: u16, fmt: u16) -> Result<PixelType, Error> {
match (fmt, bits) {
(1, 8) => Ok(PixelType::U8),
(1, 16) => Ok(PixelType::U16),
(1, 32) => Ok(PixelType::U32),
(2, 8) => Ok(PixelType::I8),
(2, 16) => Ok(PixelType::I16),
(2, 32) => Ok(PixelType::I32),
(3, 32) => Ok(PixelType::F32),
(3, 64) => Ok(PixelType::F64),
_ => Err(Error::Parse(format!(
"unsupported pixel type: SampleFormat={}, BitsPerSample={}",
fmt, bits
))),
}
}
fn parse_imagej_metadata(s: &str) -> HashMap<String, String> {
let mut map = HashMap::new();
for line in s.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(pos) = line.find('=') {
let key = line[..pos].trim().to_string();
let val = line[pos + 1..].trim().to_string();
map.insert(key, val);
}
}
map
}
impl Reader for TiffReader {
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
where
P: AsRef<Path>,
{
let mut new = Self {
reader: ThreadLocal::default(),
path: path.as_ref().to_path_buf(),
series,
position,
shape: Shape::default(),
pixel_type: PixelType::U16,
n_samples: 0,
p_ndim: 0,
planar: true,
interval_t: 0.0,
pixel_size: None,
};
let mut reader = new.get_reader()?;
let width = reader.get_tag(Tag::ImageWidth)?.into_u32()? as usize;
let height = reader.get_tag(Tag::ImageLength)?.into_u32()? as usize;
let n_samples = reader
.get_tag(Tag::SamplesPerPixel)
.ok()
.and_then(|t| t.into_u16().ok())
.unwrap_or(1) as usize;
let bits = tag_first_u16(&mut reader, Tag::BitsPerSample).unwrap_or(8);
let sfmt = tag_first_u16(&mut reader, Tag::SampleFormat).unwrap_or(1);
let pixtype = pixel_type_from_bits_sample(bits, sfmt)?;
let planar = reader
.get_tag(Tag::PlanarConfiguration)
.ok()
.and_then(|t| t.into_u16().ok())
.unwrap_or(1)
== 2;
let imagej_bytes = reader
.get_tag(Tag::Unknown(50839))
.or_else(|_| reader.get_tag(Tag::ImageDescription))?;
let imagej_bytes = match imagej_bytes {
tiff::decoder::ifd::Value::Ascii(s) => s.into_bytes(),
other => other.into_u8_vec()?,
};
let metadata_str = String::from_utf8_lossy(&imagej_bytes);
let metadata_map = parse_imagej_metadata(&metadata_str);
let p_ndim = if n_samples > 1 { 3u8 } else { 2u8 };
let size_c = if p_ndim == 3 {
n_samples
} else {
metadata_map
.get("channels")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(1)
};
let size_z = metadata_map
.get("slices")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(1);
let size_t = metadata_map
.get("frames")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(1);
let interval_t = metadata_map
.get("interval")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let ru = reader
.get_tag(Tag::ResolutionUnit)
.ok()
.and_then(|i| i.into_u16().ok())
.unwrap_or(2);
let pixel_size = reader
.get_tag(Tag::XResolution)
.ok()
.and_then(|i| i.into_f64().ok())
.and_then(|i| {
if i <= 0.0 {
None
} else if ru == 3 {
Some(1e4 / i)
} else if ru == 2 {
Some(25400.0 / i)
} else {
Some(1.0 / i)
}
});
let mult = if p_ndim == 3 { n_samples } else { 1 };
let expected = size_c * size_z * size_t / mult;
let mut n_ifds = 1;
while reader.more_images() {
reader.next_image()?;
n_ifds += 1;
}
let size_z = if n_ifds != expected && n_ifds > 0 {
n_ifds / (size_c * size_t / mult).max(1)
} else {
size_z
};
drop(reader);
new.shape = Shape {
c: size_c,
z: size_z,
t: size_t,
y: height,
x: width,
..Default::default()
};
new.pixel_type = pixtype;
new.n_samples = n_samples;
new.p_ndim = p_ndim;
new.interval_t = interval_t;
new.pixel_size = pixel_size;
new.planar = planar;
// test reading first frame, if error another reader (bioformats) can take over
new.get_frame(0, 0, 0)?;
Ok(new)
}
fn metadata(&self) -> Result<Ome, Error> {
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::Uint16,
};
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: self.pixel_size.map(|v| v as f32),
physical_size_x_unit: ome::UnitsLength::um,
physical_size_y: self.pixel_size.map(|v| v as f32),
physical_size_y_unit: ome::UnitsLength::um,
physical_size_z: None,
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 in 0..self.shape.c {
pixels.channel.push(ome::Channel {
id: format!("Channel:{}", c),
name: Some(format!("Channel {}", c)),
..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),
delta_t: if self.interval_t > 0.0 {
Some(t as f32 * self.interval_t as f32)
} else {
None
},
..Default::default()
});
}
}
}
let mut ome = Ome::default();
ome.image.push(ome::Image {
id: "Image:0".to_string(),
name: self
.path
.file_name()
.map(|n| n.to_string_lossy().to_string()),
pixels,
instrument_ref: None,
objective_settings: None,
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(),
});
Ok(ome)
}
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
let (page_idx, offset, stride) = if self.p_ndim == 3 {
(z * self.shape.t + t, c, self.n_samples)
} else {
(c + z * self.shape.c + t * self.shape.c * self.shape.z, 0, 1)
};
let mut reader = self.get_reader()?;
reader.seek_to_image(page_idx)?;
let data: DecodingResult = if self.planar {
let mut result = DecodingResult::U16(vec![]);
reader.read_image_to_buffer(&mut result)?;
result
} else {
reader.read_image()?
};
macro_rules! to_frame {
($var:ident, $ty:ty) => {{
let v: Vec<$ty> = match data {
DecodingResult::$var(v) => v,
_ => {
return Err(Error::Parse(format!(
"type mismatch: expected {}",
stringify!($var)
)));
}
};
let yx = self.shape.y * self.shape.x;
if self.planar {
let chan: Vec<$ty> = v.into_iter().skip(c * yx).take(yx).collect();
Ok(ArrayT::$var(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
chan,
)?))
} else if stride > 1 {
let chan: Vec<$ty> = v
.into_iter()
.skip(offset)
.step_by(stride)
.take(yx)
.collect();
Ok(ArrayT::$var(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
chan,
)?))
} else {
Ok(ArrayT::$var(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
v,
)?))
}
}};
}
match self.pixel_type {
PixelType::U8 => to_frame!(U8, u8),
PixelType::U16 => to_frame!(U16, u16),
PixelType::U32 => to_frame!(U32, u32),
PixelType::U64 => to_frame!(U64, u64),
PixelType::I8 => to_frame!(I8, i8),
PixelType::I16 => to_frame!(I16, i16),
PixelType::I32 => to_frame!(I32, i32),
PixelType::I64 => to_frame!(I64, i64),
PixelType::F32 => to_frame!(F32, f32),
PixelType::F64 => to_frame!(F64, f64),
_ => Err(Error::NotImplemented(format!(
"unsupported TIFF pixel type: {:?}",
self.pixel_type
))),
}
}
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>,
{
Ok(HashSet::from([0]))
}
}
fn tag_first_u16(reader: &mut RefMut<Decoder<File>>, tag: Tag) -> Option<u16> {
let val = reader.get_tag(tag).ok()?;
use tiff::decoder::ifd::Value;
match val {
Value::Short(v) => Some(v),
Value::List(list) => list.first().and_then(|v| match v {
Value::Short(v) => Some(*v),
_ => None,
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn open(file: &str) -> Result<TiffReader, Error> {
let path = std::env::current_dir()?
.join("tests")
.join("files")
.join(file);
TiffReader::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: "tiff/20251014_20-Pos_000_000_mask.tif",
metadata_b: "tiff/20251014_20-Pos_000_000_max.tif",
metadata_c: "tiff/20251014_20-Pos_000_000_loc_results_Cy3.tif",
metadata_e: "tiff/1xp53-01-AP1.tiff",
metadata_f: "tiff/test.tif",
// metadata_g: "tiff/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif",
}
}