Skip to main content

fmf/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4extern crate byteorder;
5extern crate chrono;
6extern crate machine_vision_formats as formats;
7extern crate strand_dynamic_frame;
8
9extern crate strand_datetime_conversion;
10
11use std::f64;
12use std::io::{Seek, SeekFrom, Write};
13
14use byteorder::{LittleEndian, WriteBytesExt};
15use formats::{ImageStride, PixFmt, PixelFormat};
16
17pub type FMFResult<M> = std::result::Result<M, FMFError>;
18
19pub(crate) mod pixel_formats;
20
21#[derive(thiserror::Error, Debug)]
22pub enum FMFError {
23    #[error("unexpected size")]
24    UnexpectedSize,
25    #[error("unexpected pixel_format {0} (expected {1})")]
26    UnexpectedEncoding(PixFmt, PixFmt),
27    #[error("unimplemented pixel_format {0}")]
28    UnimplementedPixelFormat(PixFmt),
29
30    #[error("Unimplemented FMF file version {0}. Only FMF v3 files supported.")]
31    UnimplementedVersion(u32),
32    #[error("premature file end")]
33    PrematureFileEnd,
34    #[error("unknown format {0}")]
35    UnknownFormat(String),
36    #[error("inconsistent state")]
37    InconsistentState,
38    #[error("already closed")]
39    AlreadyClosed,
40
41    #[error("reading past the end of the file")]
42    ReadingPastEnd,
43
44    #[error("{source}")]
45    Io {
46        #[from]
47        source: std::io::Error,
48    },
49
50    #[error("From {path}: {source}")]
51    IoPath {
52        path: String,
53        #[source]
54        source: std::io::Error,
55    },
56
57    #[error("{0}")]
58    Cell(std::cell::BorrowMutError),
59}
60
61impl From<std::cell::BorrowMutError> for FMFError {
62    fn from(orig: std::cell::BorrowMutError) -> FMFError {
63        FMFError::Cell(orig)
64    }
65}
66
67pub mod reader;
68pub use crate::reader::FMFReader;
69
70/// Writes FMF (fly movie format) movie files.
71///
72/// The FMF format is very simple and writes a fixed sized chunk of bytes to
73/// disk on every frame. This allows random access to individual frames. The
74/// bytes are not compressed but rather store the raw image bytes.
75pub struct FMFWriter<F: Write + Seek> {
76    state: WriterState<F>,
77}
78
79enum WriterState<F: Write + Seek> {
80    FileOpened(F),
81    Writing(FMFWriterInner<F>),
82    InconsistentState,
83}
84
85struct FMFWriterInner<F: Write + Seek> {
86    f: Option<F>,
87    w: u32,
88    h: u32,
89    e: PixFmt,
90    row_bytes: usize,
91    n_frames_pos_bytes: u64,
92    n_frames: u64,
93}
94
95// Things to improve in FMF v 4:
96//  * Specify a [file signature](https://en.wikipedia.org/wiki/List_of_file_signatures).
97//  * Specify that timestamp is in UTC. (Provide timezone in header?)
98//  * Provide magic number at start of file.
99//  * Allow ability to save arbitrary (timestamp,event) data or
100//    (timestamp,value) data
101//  * Save camera device timestamps and host computer timestamps and experiment
102//    timestamps (or minimally specify which)
103//  * Allow metadata such as camera name.
104//
105// Note also the file replacing-fmf.md which discusses eliminating FMF
106// completely.
107
108impl<F: Write + Seek> FMFWriter<F> {
109    /// Open a new writer.
110    pub fn new(f: F) -> FMFResult<Self> {
111        Ok(Self {
112            state: WriterState::FileOpened(f),
113        })
114    }
115
116    /// Write a frame.
117    pub fn write<TZ, FMT>(
118        &mut self,
119        frame: &dyn ImageStride<FMT>,
120        dtl: chrono::DateTime<TZ>,
121    ) -> FMFResult<()>
122    where
123        TZ: chrono::TimeZone,
124        FMT: PixelFormat,
125    {
126        let timestamp = strand_datetime_conversion::datetime_to_f64(&dtl);
127
128        // We need this dance with InconsistentState to prevent moving out of
129        // borrowed struct. TODO: remove it.
130        let state = std::mem::replace(&mut self.state, WriterState::InconsistentState);
131        let new_state = match state {
132            WriterState::FileOpened(f) => {
133                let inner = FMFWriterInner::new(
134                    f,
135                    frame.width(),
136                    frame.height(),
137                    machine_vision_formats::pixel_format::pixfmt::<FMT>().unwrap(),
138                )?;
139                WriterState::Writing(inner)
140            }
141            WriterState::Writing(inner) => WriterState::Writing(inner),
142            WriterState::InconsistentState => return Err(FMFError::InconsistentState),
143        };
144        self.state = new_state;
145
146        match self.state {
147            WriterState::Writing(ref mut inner) => {
148                if (frame.width() != inner.w)
149                    || (frame.height() != inner.h)
150                    || (inner.row_bytes > frame.stride())
151                {
152                    return Err(FMFError::UnexpectedSize);
153                }
154                if machine_vision_formats::pixel_format::pixfmt::<FMT>().unwrap() != inner.e {
155                    return Err(FMFError::UnexpectedEncoding(
156                        machine_vision_formats::pixel_format::pixfmt::<FMT>().unwrap(),
157                        inner.e,
158                    ));
159                }
160                inner.write_inner(frame, timestamp)?;
161            }
162            _ => {
163                return Err(FMFError::InconsistentState);
164            }
165        }
166        Ok(())
167    }
168
169    /// Close the writer.
170    ///
171    /// Ideally, this is called prior to dropping to prevent the possibility of
172    /// silently ignoring errors.
173    pub fn close(self) -> FMFResult<F> {
174        match self.state {
175            WriterState::FileOpened(f) => Ok(f),
176            WriterState::Writing(mut inner) => inner.close(),
177            WriterState::InconsistentState => Err(FMFError::InconsistentState),
178        }
179    }
180}
181
182impl<F: Write + Seek> FMFWriterInner<F> {
183    fn new(mut f: F, w: u32, h: u32, pixel_format: PixFmt) -> FMFResult<Self> {
184        let format = pixel_formats::get_format(pixel_format)?;
185
186        let bytes_per_pixel = pixel_format.bits_per_pixel() / 8;
187
188        let row_bytes = w as usize * bytes_per_pixel as usize;
189        let chunksize = row_bytes * h as usize + 8;
190
191        let mut pos = 0;
192        f.write_u32::<LittleEndian>(3)?;
193        pos += 4; // FMF version = 3
194        f.write_u32::<LittleEndian>(format.len() as u32)?;
195        pos += 4;
196        f.write_all(&format)?;
197        pos += format.len();
198        f.write_u32::<LittleEndian>(bytes_per_pixel as u32 * 8)?;
199        pos += 4;
200        f.write_u32::<LittleEndian>(h)?;
201        pos += 4;
202        f.write_u32::<LittleEndian>(w)?;
203        pos += 4;
204        f.write_u64::<LittleEndian>(chunksize as u64)?;
205        pos += 8;
206        f.write_u64::<LittleEndian>(0)?; // n_frames = 0
207
208        let f = Some(f);
209
210        Ok(Self {
211            f,
212            w,
213            h,
214            e: pixel_format,
215            row_bytes,
216            n_frames_pos_bytes: pos as u64,
217            n_frames: 0,
218        })
219    }
220
221    fn write_inner<FMT>(&mut self, frame: &dyn ImageStride<FMT>, timestamp: f64) -> FMFResult<()>
222    where
223        FMT: PixelFormat,
224    {
225        let self_f = match self.f {
226            Some(ref mut f) => f,
227            None => {
228                return Err(FMFError::AlreadyClosed);
229            }
230        };
231        self_f.write_f64::<LittleEndian>(timestamp)?;
232
233        // `frame` might be a region of interest into an outer frame. In this
234        // case, `width*bytes_per_pixel` may be less than stride, and we do not
235        // want to write all these bytes.
236
237        let image_data = frame.image_data();
238        let e = machine_vision_formats::pixel_format::pixfmt::<FMT>().unwrap();
239        let bpp = e.bits_per_pixel();
240        let n_bytes_per_row = self.w as usize * (bpp / 8) as usize;
241        let mut ptr = 0;
242        for _ in 0..self.h {
243            let end = ptr + n_bytes_per_row;
244            let row_buf = &image_data[ptr..end];
245            self_f.write_all(row_buf)?;
246            ptr += frame.stride();
247        }
248
249        self.n_frames += 1;
250
251        Ok(())
252    }
253
254    /// Close the writer.
255    ///
256    /// Ideally, this is called prior to dropping to prevent the possibility of
257    /// silently ignoring errors.
258    fn close(&mut self) -> FMFResult<F> {
259        let opt_f = self.f.take();
260
261        let mut self_f = match opt_f {
262            Some(f) => f,
263            None => {
264                return Err(FMFError::AlreadyClosed);
265            }
266        };
267
268        // Write n_frames to the file header.
269        self_f.seek(SeekFrom::Start(self.n_frames_pos_bytes))?;
270        self_f.write_u64::<LittleEndian>(self.n_frames)?;
271        self_f.flush()?;
272        Ok(self_f)
273    }
274}
275
276/// This will silently ignore any error.
277impl<F: Write + Seek> Drop for FMFWriterInner<F> {
278    fn drop(&mut self) {
279        // We silently drop error.
280        match self.close() {
281            Ok(_f) => {}
282            Err(_e) => {} // See https://github.com/rust-lang/rfcs/issues/814
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::FMFWriter;
290
291    use machine_vision_formats::{owned::OImage, pixel_format::Mono8};
292
293    fn zeros(w: u32, h: u32) -> OImage<Mono8> {
294        let image_data = vec![0; (w * h) as usize];
295
296        OImage::new(w, h, w.try_into().unwrap(), image_data).unwrap()
297    }
298
299    #[test]
300    fn test_writing() {
301        let w = 320;
302        let h = 240;
303        let f = std::io::Cursor::new(Vec::new());
304        // create fmf
305        let mut writer = FMFWriter::new(f).unwrap();
306
307        // write some frames
308        let f1 = zeros(w, h);
309        let dt = chrono::DateTime::from_timestamp(61, 0).unwrap();
310
311        writer.write(&f1, dt).unwrap();
312
313        let f2 = zeros(w, h);
314        writer.write(&f2, dt).unwrap();
315
316        let f3 = zeros(w, h);
317        writer.write(&f3, dt).unwrap();
318
319        let f = writer.close().unwrap();
320
321        // check what was writen
322        let buf = f.into_inner();
323
324        let expected = [3, 0, 0, 0, 5, 0, 0, 0, 77, 79]; // TODO improve test
325        assert_eq!(&buf[0..10], expected);
326    }
327}