Skip to main content

braidz_parser/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::{
5    collections::BTreeMap,
6    fs::File,
7    io::{BufReader, Read, Seek},
8};
9
10use hdrhistogram::serialization::interval_log;
11use ordered_float::NotNan;
12
13use braid_types::{
14    CamInfoRow, CamNum, Data2dDistortedRow, FlydraFloatTimestampLocal, HostClock,
15    KalmanEstimatesRow, TextlogRow, TrackingParams, Triggerbox,
16};
17
18use braidz_types::{
19    BraidMetadata, BraidzSummary, CalibrationInfo, CamInfo, Data2dSummary, HistogramSummary,
20    KalmanEstimatesSummary,
21};
22
23use groupby::{AscendingGroupIter, BufferedSortIter, GroupedRows};
24
25use csv_eof::EarlyEofOk;
26
27pub mod incremental_parser;
28
29#[derive(thiserror::Error, Debug)]
30pub enum Error {
31    #[error("Did not find metadata in YAML file or textlog")]
32    MissingMetadata {},
33    #[error("{source}")]
34    Mvg {
35        #[from]
36        source: braid_mvg::MvgError,
37    },
38    #[error("{0}")]
39    FlydraMvg(#[from] flydra_mvg::FlydraMvgError),
40    #[error("{source}")]
41    Io {
42        #[from]
43        source: std::io::Error,
44    },
45    #[error("{source}")]
46    Zip {
47        #[from]
48        source: zip::result::ZipError,
49    },
50    #[error("{source}")]
51    Yaml {
52        #[from]
53        source: serde_yaml::Error,
54    },
55    #[error("{source}")]
56    Json {
57        #[from]
58        source: serde_json::Error,
59    },
60    #[error("{source}")]
61    Csv {
62        #[from]
63        source: csv::Error,
64    },
65    #[error("XML error")]
66    Xml,
67    #[error("{source}")]
68    ZipOrDir {
69        #[from]
70        source: zip_or_dir::Error,
71    },
72    #[error("{source}")]
73    ParseFloat {
74        #[from]
75        source: std::num::ParseFloatError,
76    },
77    #[error("{source}")]
78    ImageError {
79        #[from]
80        source: image::ImageError,
81    },
82    #[error("Compressed and uncompressed data copies exist simultaneously")]
83    DualData,
84    #[error("textlog data could not be parsed")]
85    UnknownTextlogData,
86    #[error("Multiple tracking parameters")]
87    MultipleTrackingParameters,
88    #[error("Missing tracking parameters")]
89    MissingTrackingParameters,
90    #[error("Error opening {filename}: {source}")]
91    FileError {
92        what: &'static str,
93        filename: String,
94        source: Box<dyn std::error::Error + Sync + Send>,
95    },
96}
97
98impl From<serde_xml_rs::Error> for Error {
99    fn from(_source: serde_xml_rs::Error) -> Error {
100        Error::Xml
101    }
102}
103
104/// The entire file contents, loaded to memory.
105///
106/// Currently, the implementation does not load everything to memory, but it
107/// should and will do so in the future. To load only a summary, use the
108/// `BraidzSummary` type. Currently, a summary can only be made by loading the
109/// entire archive first, but more efficient path can be made later.
110// A perhaps even better idea for the future is to combine both the summary and
111// the archive type and have it read and parse in a lazy and caching way.
112pub struct BraidzArchive<R: Read + Seek> {
113    archive: zip_or_dir::ZipDirArchive<R>, //incremental_parser::IncrementalParser<R, incremental_parser::FullyParsed>,
114    pub metadata: BraidMetadata,
115    pub expected_fps: f64,
116    pub calibration_info: Option<CalibrationInfo>,
117    pub kalman_estimates_info: Option<KalmanEstimatesInfo>,
118    pub kalman_estimates_table: Option<Vec<KalmanEstimatesRow>>,
119    pub data_association: Option<Vec<braid_types::DataAssocRow>>,
120    pub reconstruction_latency_hlog: Option<HistogramLog>,
121    pub reprojection_distance_hlog: Option<HistogramLog>,
122    pub cam_info: CamInfo,
123    pub data2d_distorted: Option<D2DInfo>,
124    /// A mapping from camera name to (width, height).
125    pub image_sizes: Option<BTreeMap<String, (usize, usize)>>,
126}
127
128#[derive(Debug)]
129pub struct HistogramLog {
130    histogram: hdrhistogram::Histogram<u64>,
131}
132
133impl From<&HistogramLog> for HistogramSummary {
134    fn from(orig: &HistogramLog) -> Self {
135        HistogramSummary {
136            len: orig.histogram.len(),
137            mean: orig.histogram.mean(),
138            min: orig.histogram.min(),
139            max: orig.histogram.max(),
140        }
141    }
142}
143
144impl<R: Read + Seek> BraidzArchive<R> {
145    /// Consume and return the raw storage archive.
146    pub fn into_inner(self) -> zip_or_dir::ZipDirArchive<R> {
147        self.archive
148    }
149
150    /// Display the path to the archive.
151    pub fn display(&self) -> std::path::Display<'_> {
152        self.archive.display()
153    }
154
155    /// Get the path to the archive.
156    pub fn path(&self) -> &std::path::Path {
157        self.archive.path()
158    }
159}
160
161#[derive(PartialEq)]
162pub struct D2DInfo {
163    pub qz: BTreeMap<CamNum, Seq2d>,
164    pub frame_lim: [u64; 2],
165    pub time_limits: [chrono::DateTime<chrono::Utc>; 2],
166    pub num_rows: u64,
167}
168
169impl std::fmt::Debug for D2DInfo {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
171        f.debug_struct("D2DInfo")
172            .field("frame_lim", &self.frame_lim)
173            .field("time_limits", &self.time_limits)
174            .field("num_rows", &self.num_rows)
175            .finish()
176    }
177}
178
179/// Column store for 2D detections for a single camera.
180///
181/// Note that these are not filled when there is no detection.
182#[derive(PartialEq)]
183pub struct Seq2d {
184    /// The frame number in the synchronized, global frame count.
185    pub frame: Vec<i64>,
186    /// The x coordinate of the detections.
187    pub xdata: Vec<NotNan<f64>>,
188    /// The y coordinate of the detections.
189    pub ydata: Vec<NotNan<f64>>,
190    /// The maximum value of all x and y coordinates.
191    pub max_pixel: NotNan<f64>,
192    /// The time at which the hardware trigger was computed to have fired to
193    /// initiate image acquisition at this frame.
194    ///
195    /// This is computed based on a model of the clock running on the triggerbox
196    /// and keeping this model updated via continual sampling of both the
197    /// triggerbox clock and the system clock of the computer hosting the
198    /// triggerbox.
199    pub timestamp_trigger: Vec<Option<FlydraFloatTimestampLocal<Triggerbox>>>,
200    /// The time at which the image was available to the system clock of the
201    /// host computer of the camera.
202    pub timestamp_host: Vec<FlydraFloatTimestampLocal<HostClock>>,
203}
204
205// TODO: rename KalmanEstimates? or ..Data?
206pub struct KalmanEstimatesInfo {
207    pub xlim: [f64; 2],
208    pub ylim: [f64; 2],
209    pub zlim: [f64; 2],
210    pub trajectories: BTreeMap<u32, TrajectoryData>,
211    pub num_rows: u64,
212    pub tracking_parameters: TrackingParams,
213    /// The sum of all distances in all trajectories.
214    pub total_distance: f64,
215}
216
217impl std::fmt::Debug for KalmanEstimatesInfo {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
219        f.debug_struct("KalmanEstimatesInfo")
220            .field("xlim", &self.xlim)
221            .field("ylim", &self.ylim)
222            .field("zlim", &self.zlim)
223            .field("num_rows", &self.num_rows)
224            .finish()
225    }
226}
227
228pub struct TrajectoryData {
229    pub position: Vec<[f32; 3]>,
230    pub start_frame: u64,
231    pub distance: f64,
232}
233
234impl Seq2d {
235    fn new() -> Self {
236        Self {
237            frame: vec![],
238            xdata: vec![],
239            ydata: vec![],
240            max_pixel: NotNan::new(0.0).unwrap(),
241            timestamp_trigger: vec![],
242            timestamp_host: vec![],
243        }
244    }
245
246    fn push(
247        &mut self,
248        f: i64,
249        x: NotNan<f64>,
250        y: NotNan<f64>,
251        timestamp_trigger: Option<FlydraFloatTimestampLocal<Triggerbox>>,
252        timestamp_host: FlydraFloatTimestampLocal<HostClock>,
253    ) {
254        self.frame.push(f);
255        self.xdata.push(x);
256        self.ydata.push(y);
257        self.timestamp_trigger.push(timestamp_trigger);
258        self.timestamp_host.push(timestamp_host);
259        self.max_pixel = NotNan::new(max(*self.max_pixel, max(*x, *y))).unwrap();
260    }
261}
262
263pub fn summarize_braidz<R: Read + Seek>(
264    braidz_archive: &BraidzArchive<R>,
265    filename: String,
266    filesize: u64,
267) -> BraidzSummary {
268    let data2d_summary = braidz_archive.data2d_distorted.as_ref().map(Into::into);
269    let kalman_estimates_summary = braidz_archive
270        .kalman_estimates_info
271        .as_ref()
272        .map(Into::into);
273
274    let reconstruct_latency_usec_summary = braidz_archive
275        .reconstruction_latency_hlog
276        .as_ref()
277        .map(Into::into);
278
279    let reprojection_distance_100x_pixels_summary = braidz_archive
280        .reprojection_distance_hlog
281        .as_ref()
282        .map(Into::into);
283
284    BraidzSummary {
285        metadata: braidz_archive.metadata.clone(),
286        calibration_info: braidz_archive.calibration_info.clone().map(Into::into),
287        expected_fps: braidz_archive.expected_fps,
288        cam_info: braidz_archive.cam_info.clone(),
289        filename,
290        filesize,
291        kalman_estimates_summary,
292        data2d_summary,
293        reconstruct_latency_usec_summary,
294        reprojection_distance_100x_pixels_summary,
295    }
296}
297
298pub fn braidz_parse_reader<R: Read + Seek>(
299    rdr: R,
300    display_name: String,
301) -> Result<BraidzArchive<R>, Error> {
302    let zs = zip_or_dir::ZipDirArchive::from_zip(rdr, display_name)?;
303    let parsed = braidz_parse(zs)?;
304    Ok(parsed)
305}
306
307pub fn braidz_parse_path<P: AsRef<std::path::Path>>(
308    path: P,
309) -> Result<BraidzArchive<BufReader<File>>, Error> {
310    let zs = zip_or_dir::ZipDirArchive::auto_from_path(&path)?;
311    let parsed = braidz_parse(zs)?;
312    Ok(parsed)
313}
314
315pub fn braidz_parse<R: Read + Seek>(
316    archive: zip_or_dir::ZipDirArchive<R>,
317) -> Result<BraidzArchive<R>, Error> {
318    let ip = incremental_parser::IncrementalParser::from_archive(archive);
319    let ip = ip.parse_everything()?;
320    let state = ip.state;
321    let archive = ip.archive;
322
323    Ok(BraidzArchive {
324        archive,
325        metadata: state.metadata,
326        expected_fps: state.expected_fps,
327        calibration_info: state.calibration_info,
328        cam_info: state.cam_info,
329        kalman_estimates_info: state.kalman_estimates_info,
330        kalman_estimates_table: state.kalman_estimates_table,
331        data2d_distorted: state.data2d_distorted,
332        data_association: state.data_association,
333        reconstruction_latency_hlog: state.reconstruction_latency_hlog,
334        reprojection_distance_hlog: state.reprojection_distance_hlog,
335        image_sizes: state.image_sizes,
336    })
337}
338
339impl<'a, R: Read + Seek> BraidzArchive<R> {
340    /// Iterate over the rows of the `data2d_distorted` table.
341    ///
342    /// This takes a mutable reference because the read location in the archive
343    /// is changed during operation.
344    ///
345    /// Note that, as described in the "Details about how data are processed
346    /// online and saved for later analysis" section in the "3D Tracking in
347    /// Braid" chapter of the [User's
348    /// Guide](https://strawlab.github.io/strand-braid/), these will not, in
349    /// general, be returned in a monotonically increasing order.
350    pub fn iter_data2d_distorted(
351        &'a mut self,
352    ) -> Result<impl Iterator<Item = Result<Data2dDistortedRow, csv::Error>> + 'a, Error> {
353        let data_fname = self
354            .archive
355            .path_starter()
356            .join(braid_types::DATA2D_DISTORTED_CSV_FNAME);
357        let rdr = open_maybe_gzipped(data_fname)?;
358        let rdr2 = csv::Reader::from_reader(rdr);
359        Ok(rdr2.into_deserialize().early_eof_ok())
360    }
361
362    /// Iterate over synchronized frames in `data2d_distorted` table.
363    ///
364    /// This sorts the data by looking ahead up to `bufsize` rows. Furthermore,
365    /// it can exclude nan data if `include_nan_data` is set to `false`. No
366    /// guarantees are made about whether frames may be skipped and indeed they
367    /// often may be.
368    // TODO: create a variant that does not skip frames
369    pub fn iter_grouped_data2d_distorted(
370        &'a mut self,
371        include_nan_data: bool,
372        bufsize: usize,
373    ) -> Result<
374        impl Iterator<Item = Result<GroupedRows<i64, braid_types::Data2dDistortedRow>, Error>> + 'a,
375        Error,
376    > {
377        let single_iter = self
378            .iter_data2d_distorted()?
379            .map(|res| res.map_err(Error::from));
380        let single_iter = single_iter.filter_map(move |res_row| {
381            if !include_nan_data {
382                let keep_row = if let Ok(row) = res_row.as_ref() {
383                    !row.x.is_nan()
384                } else {
385                    true
386                };
387                if keep_row { Some(res_row) } else { None }
388            } else {
389                Some(res_row)
390            }
391        });
392        let sorted_data_iter = BufferedSortIter::new(single_iter, bufsize)?;
393        let data_row_frame_iter = AscendingGroupIter::new(sorted_data_iter);
394        Ok(data_row_frame_iter)
395    }
396}
397
398fn get_hlog<R: Read>(mut rdr: R) -> Result<Option<HistogramLog>, ()> {
399    /*
400    # Python reader
401    from hdrh.histogram import HdrHistogram
402    from hdrh.log import HistogramLogReader
403    h=HdrHistogram(1,100000,2)
404    rdr = HistogramLogReader('reprojection_distance_100x_pixels.hlog', h)
405    h1 = rdr.get_next_interval_histogram()
406    print(h1.get_total_count())
407    */
408
409    let mut buf = vec![];
410    rdr.read_to_end(&mut buf).map_err(|_| ())?;
411
412    let iter = interval_log::IntervalLogIterator::new(&buf);
413
414    use hdrhistogram::{
415        Histogram,
416        serialization::{Deserializer, interval_log::LogEntry},
417    };
418
419    let mut deserializer = Deserializer::new();
420    let mut result: Option<Histogram<u64>> = None;
421
422    for interval in iter {
423        let interval = interval.map_err(|_| ())?;
424        match interval {
425            LogEntry::Interval(ilh) => {
426                use base64::Engine;
427                let serialized_histogram = base64::engine::general_purpose::STANDARD
428                    .decode(ilh.encoded_histogram())
429                    .map_err(|_| ())?;
430                let decoded_hist: Histogram<u64> = deserializer
431                    .deserialize(&mut std::io::Cursor::new(&serialized_histogram))
432                    .map_err(|_| ())?;
433                result = match result {
434                    Some(mut x) => {
435                        x.add(&decoded_hist).map_err(|_| ())?;
436                        Some(x)
437                    }
438                    None => Some(decoded_hist),
439                };
440            }
441            LogEntry::BaseTime(_) | LogEntry::StartTime(_) => {}
442        }
443    }
444
445    Ok(result.map(|histogram| HistogramLog { histogram }))
446}
447
448impl From<&KalmanEstimatesInfo> for KalmanEstimatesSummary {
449    fn from(orig: &KalmanEstimatesInfo) -> Self {
450        Self {
451            num_rows: orig.num_rows,
452            x_limits: orig.xlim,
453            y_limits: orig.ylim,
454            z_limits: orig.zlim,
455            num_trajectories: orig.trajectories.len().try_into().unwrap(),
456            tracking_parameters: orig.tracking_parameters.clone(),
457            total_distance: orig.total_distance,
458        }
459    }
460}
461
462impl From<&D2DInfo> for Data2dSummary {
463    fn from(orig: &D2DInfo) -> Self {
464        let num_cameras_with_data = orig.qz.len().try_into().unwrap();
465        Self {
466            time_limits: orig.time_limits,
467            frame_limits: orig.frame_lim,
468            num_cameras_with_data,
469            num_rows: orig.num_rows,
470        }
471    }
472}
473
474fn min(a: f64, b: f64) -> f64 {
475    if a > b { b } else { a }
476}
477
478fn max(a: f64, b: f64) -> f64 {
479    if a < b { b } else { a }
480}
481
482/// Append a suffix to a path.
483fn append_to_path(path: &std::path::Path, suffix: &str) -> std::path::PathBuf {
484    let mut s1: std::ffi::OsString = path.to_path_buf().into_os_string(); // copy data
485    s1.push(suffix);
486    s1.into()
487}
488
489#[test]
490fn test_append_to_path() {
491    let foo = std::path::Path::new("foo");
492    assert!(append_to_path(foo, ".gz") == std::path::Path::new("foo.gz"));
493
494    let foo_csv = std::path::Path::new("foo.csv");
495    assert!(append_to_path(foo_csv, ".gz") == std::path::Path::new("foo.csv.gz"));
496}
497
498/// Pick the `.csv` file (if it exists) as first choice, else pick `.csv.gz`.
499pub fn open_maybe_gzipped<R: Read + Seek>(
500    mut path_like: zip_or_dir::PathLike<R>,
501) -> Result<MaybeGzippedReader, Error> {
502    let compressed_relname = append_to_path(path_like.path(), ".gz");
503
504    if path_like.exists() {
505        const CHECK_NO_DUAL_DATA: bool = true;
506        if CHECK_NO_DUAL_DATA {
507            // Check the compressed variant does not exist. Due to reasons, we
508            // have replace, but not clone, so we replace the original with the
509            // new and then back again.
510            let uncompressed_relname = path_like.replace(compressed_relname);
511            if path_like.exists() {
512                return Err(Error::DualData);
513            }
514            path_like.replace(uncompressed_relname);
515        }
516        Ok(MaybeGzippedReader::Raw(path_like.open()?))
517    } else {
518        // Use the compressed variant.
519        path_like.replace(compressed_relname);
520        let gz_fd = path_like.open()?;
521        Ok(MaybeGzippedReader::Gzipped(libflate::gzip::Decoder::new(
522            gz_fd,
523        )?))
524    }
525}
526
527#[derive(Debug)]
528pub enum MaybeGzippedReader<'a> {
529    Raw(zip_or_dir::FileReader<'a>),
530    Gzipped(libflate::gzip::Decoder<zip_or_dir::FileReader<'a>>),
531}
532
533impl<'a> Read for MaybeGzippedReader<'a> {
534    fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
535        match self {
536            Self::Raw(f) => f.read(buf),
537            Self::Gzipped(gz) => gz.read(buf),
538        }
539    }
540}