Skip to main content

frame_source/
pv_tiff_stack.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::{io::Cursor, path::Path};
5
6use super::*;
7
8const IJIJINFO_KEY: u16 = 50839;
9const MAGICSTR: &str = "IJIJinfo";
10
11pub fn from_path_pattern(pattern: &str) -> Result<PvTiffStack> {
12    PvTiffStack::new(pattern)
13}
14
15#[derive(Clone, PartialEq)]
16pub struct TiffImage {
17    // Maybe TODO: make this reference to the data source? (don't force a clone
18    // of it).
19    pub buf: Vec<u8>,
20    pub width: u32,
21    pub height: u32,
22    pub metadata: TiffMetadata,
23}
24
25#[derive(Clone, PartialEq)]
26pub struct TiffMetadata {
27    /// all metadata
28    ///
29    /// The metadata was parsed from the `.tiff` file EXIF data.
30    json: serde_json::Value,
31    /// raw `PVCAM-FMD-FrameNr` value
32    framenumber: usize,
33    /// converted from `PVCAM-FMD-TimestampBofPs`
34    ///
35    /// This truly is a timestamp in the sense that it is an absolute time from
36    /// when the camera turned on. However, the type used to best represent this
37    /// is `std::time::Duration` because there is no reference to external
38    /// clocks.
39    pub timestamp: std::time::Duration,
40    /// raw `PVCAM-FMD-BitDepth` value
41    pub bit_depth: u8,
42}
43
44/// A stack of TIFF images
45///
46/// Note that this is not a generic TIFF stack reader but rather one saved by
47/// Micromanager using a Photometrics camera.
48pub struct PvTiffStack {
49    paths: Vec<std::path::PathBuf>,
50    /// Absolute time of frame0.
51    frame0_time: chrono::DateTime<chrono::FixedOffset>,
52    /// Offset of the metadata timestamp
53    ///
54    /// This the value of the metadata timestamp for the frame 0. All other
55    /// timestamps are offset by this value so the timestamp of frame 0 is 0.
56    frame0_timestamp_offset: std::time::Duration,
57    width: u32,
58    height: u32,
59    tiff_image0: TiffImage,
60}
61
62impl FrameDataSource for PvTiffStack {
63    fn width(&self) -> u32 {
64        self.width
65    }
66    fn height(&self) -> u32 {
67        self.height
68    }
69    fn decode_order_iter<'a>(&'a mut self) -> Box<dyn Iterator<Item = Result<FrameData>> + 'a> {
70        Box::new(ImageStackIter::new(self))
71    }
72    fn skip_n_frames(&mut self, n_frames: usize) -> Result<()> {
73        let paths = self.paths.split_off(n_frames);
74        let new_self = Self::new_from_paths(paths)?;
75        *self = new_self;
76        Ok(())
77    }
78    fn frame0_time(&self) -> Option<chrono::DateTime<chrono::FixedOffset>> {
79        Some(self.frame0_time)
80    }
81    fn average_framerate(&self) -> Option<f64> {
82        None
83    }
84    fn estimate_luminance_range(&mut self) -> Result<(u16, u16)> {
85        // take 5 images or all of them, whatever is less.
86        let n_images = self.paths.len().min(5);
87        let step_size = self.paths.len() / n_images;
88        let (mut low, mut high) = self.tiff_image0.luminance_range()?;
89        for this_frame in self.decode_order_iter().step_by(step_size) {
90            let image = match this_frame?.image {
91                ImageData::Tiff(image) => image,
92                _ => {
93                    return Err(crate::Error::ExpectedTiffImage);
94                }
95            };
96            let (this_low, this_high) = image.luminance_range()?;
97            if this_low < low {
98                low = this_low;
99            }
100            if this_high > high {
101                high = this_high;
102            }
103        }
104        Ok((low, high))
105    }
106    fn timestamp_source(&self) -> &str {
107        "PVCAM-FMD-TimestampBofPs"
108    }
109    fn has_timestamps(&self) -> bool {
110        true
111    }
112}
113
114impl PvTiffStack {
115    fn new(pattern: &str) -> Result<Self> {
116        let mut paths = vec![];
117        for path in glob::glob_with(
118            pattern,
119            glob::MatchOptions {
120                case_sensitive: false,
121                require_literal_separator: true,
122                require_literal_leading_dot: true,
123            },
124        )? {
125            paths.push(path?);
126        }
127        if paths.is_empty() {
128            return Err(crate::Error::NoFilesFound);
129        }
130        Self::new_from_paths(paths)
131    }
132    fn new_from_paths(mut paths: Vec<PathBuf>) -> Result<Self> {
133        paths.sort();
134
135        let file0_buf = read_file(&paths[0])?;
136        let frame0_metadata = extract_tiff_metadata(&file0_buf)?;
137
138        // For the initial time, use the `ReceivedTime` metadata. Corrected by
139        // subtracting exposure duration. I guess there remains some error due
140        // to transfer time and CPU scheduling.
141        let mut frame0_time = frame0_metadata.json["ReceivedTime"]
142            .as_str()
143            .ok_or_else(json_parse_err)?
144            .parse()?;
145
146        let pvcam_fmd_exposure_str =
147            frame0_metadata.json["UserData"]["PVCAM-FMD-ExposureTimePs"]["scalar"]
148                .as_str()
149                .ok_or_else(json_parse_err)?;
150        let exposure_duration =
151            chrono::Duration::from_std(parse_picosecs(pvcam_fmd_exposure_str)?)?;
152        frame0_time -= exposure_duration;
153
154        let frame0_timestamp_offset = frame0_metadata.timestamp;
155        let tiff_image0 = read_tiff_image(&file0_buf, frame0_metadata)?;
156        let width = tiff_image0.width;
157        let height = tiff_image0.height;
158
159        Ok(Self {
160            paths,
161            frame0_time,
162            frame0_timestamp_offset,
163            width,
164            height,
165            tiff_image0,
166        })
167    }
168    pub fn len(&self) -> usize {
169        self.paths.len()
170    }
171    pub fn is_empty(&self) -> bool {
172        self.paths.is_empty()
173    }
174}
175
176struct ImageStackIter<'a> {
177    idx: usize,
178    frame0_timestamp_offset: std::time::Duration,
179    inner: std::slice::Iter<'a, std::path::PathBuf>,
180}
181
182impl<'a> ImageStackIter<'a> {
183    fn new(parent: &'a PvTiffStack) -> Self {
184        Self {
185            idx: 0,
186            frame0_timestamp_offset: parent.frame0_timestamp_offset,
187            inner: parent.paths.iter(),
188        }
189    }
190}
191
192impl Iterator for ImageStackIter<'_> {
193    type Item = Result<FrameData>;
194    fn next(&mut self) -> Option<Self::Item> {
195        let result = self
196            .inner
197            .next()
198            .map(|path| path_to_tiff(path, self.frame0_timestamp_offset, self.idx));
199        self.idx += 1;
200        result
201    }
202    fn size_hint(&self) -> (usize, Option<usize>) {
203        self.inner.size_hint()
204    }
205}
206
207impl TiffImage {
208    fn luminance_range(&self) -> Result<(u16, u16)> {
209        // Ideally we should use `tiff-decoder` crate and `read_tiff_image` here
210        // but that converts to 8 bit already, so it would be useless.
211        let mut decoder = tiff::decoder::Decoder::new(Cursor::new(&self.buf))?;
212
213        let buf = decoder.read_image()?;
214        let color = decoder.colortype()?;
215
216        let (width, _height) = decoder.dimensions()?;
217        let width: usize = width.try_into()?;
218
219        // TODO do we need to worry about the byte order? The decoder can return
220        // byte order information, but for now I am assuming it decodes into the
221        // native machine byte order.
222
223        let mut min = 255;
224        let mut max = 0;
225        match (color, buf) {
226            (tiff::ColorType::Gray(16), tiff::decoder::DecodingResult::U16(vals)) => {
227                for row in vals.chunks_exact(width) {
228                    for val_u16 in row {
229                        if *val_u16 < min {
230                            min = *val_u16;
231                        }
232                        if *val_u16 > max {
233                            max = *val_u16;
234                        }
235                    }
236                }
237            }
238            _ => {
239                return Err(crate::Error::UnsupportedForEsimatingLuminangeRange);
240            }
241        }
242        Ok((min, max))
243    }
244}
245
246fn read_tiff_image(buf: &[u8], metadata: TiffMetadata) -> Result<TiffImage> {
247    let mut decoder = tiff::decoder::Decoder::new(Cursor::new(buf))?;
248    let (width, height) = decoder.dimensions()?;
249    Ok(TiffImage {
250        buf: buf.to_vec(),
251        width,
252        height,
253        metadata,
254    })
255}
256
257fn extract_tiff_metadata(buf: &[u8]) -> Result<TiffMetadata> {
258    let mut rdr = Cursor::new(buf);
259    let exifreader = exif::Reader::new();
260    let exif = exifreader.read_from_container(&mut rdr)?;
261
262    let imagej_metadata = exif.get_field(
263        exif::Tag(exif::Context::Tiff, IJIJINFO_KEY),
264        exif::In::PRIMARY,
265    );
266    let bytes = if let Some(imagej_metadata) = imagej_metadata {
267        if let exif::Value::Byte(bytes) = &imagej_metadata.value {
268            bytes
269        } else {
270            return Err(crate::Error::ImageJDataExpectedToBeBytes);
271        }
272    } else {
273        return Err(crate::Error::FailedToReadMetadata);
274    };
275    let mystr = String::from_utf8(bytes.clone())?.replace(['\x00', '\x01'], "");
276    if !mystr.starts_with(MAGICSTR) {
277        return Err(crate::Error::ExifMetadataFailsMagic);
278    }
279    let mystr = &mystr[MAGICSTR.len()..];
280    let json: serde_json::Value = serde_json::from_str(mystr)?;
281    // dbg!(&json);
282
283    let framenumber = json["UserData"]["PVCAM-FMD-FrameNr"]["scalar"]
284        .as_str()
285        .ok_or_else(json_parse_err)?
286        .parse()?;
287    let pvcam_fmd_timestamp_str = json["UserData"]["PVCAM-FMD-TimestampBofPs"]["scalar"]
288        .as_str()
289        .ok_or_else(json_parse_err)?;
290    let timestamp = parse_picosecs(pvcam_fmd_timestamp_str)?;
291
292    let bit_depth = json["UserData"]["PVCAM-FMD-BitDepth"]["scalar"]
293        .as_str()
294        .ok_or_else(json_parse_err)?
295        .parse()?;
296    Ok(TiffMetadata {
297        json,
298        framenumber,
299        timestamp,
300        bit_depth,
301    })
302}
303
304fn path_to_tiff(
305    path: &std::path::PathBuf,
306    frame0_timestamp_offset: std::time::Duration,
307    assign_idx: usize,
308) -> Result<FrameData> {
309    let buf = read_file(path)?;
310    let buf_len = buf.len();
311    let mut metadata = extract_tiff_metadata(&buf)?;
312    metadata.timestamp -= frame0_timestamp_offset;
313    let timestamp = Timestamp::Duration(metadata.timestamp);
314    Ok(FrameData {
315        image: ImageData::Tiff(read_tiff_image(&buf, metadata)?),
316        timestamp,
317        buf_len,
318        idx: assign_idx,
319        poc: None,
320    })
321}
322
323fn json_parse_err() -> crate::Error {
324    crate::Error::JsonParseError
325}
326
327fn read_file<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
328    Ok(std::fs::read(path.as_ref())?)
329}
330
331fn parse_picosecs(picosecs_str: &str) -> Result<std::time::Duration> {
332    let elapsed_picosecs: u128 = picosecs_str.parse()?;
333    let elapsed_msecs: f64 = elapsed_picosecs as f64 / 1_000_000_000f64;
334    Ok(std::time::Duration::from_secs_f64(elapsed_msecs / 1000.0))
335}