- implement shape in Rust
- implement more readers - fix downloading of bioformats jar - (mostly) compatible with python version
This commit is contained in:
@@ -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",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user