1use 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
104pub struct BraidzArchive<R: Read + Seek> {
113 archive: zip_or_dir::ZipDirArchive<R>, 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 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 pub fn into_inner(self) -> zip_or_dir::ZipDirArchive<R> {
147 self.archive
148 }
149
150 pub fn display(&self) -> std::path::Display<'_> {
152 self.archive.display()
153 }
154
155 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#[derive(PartialEq)]
183pub struct Seq2d {
184 pub frame: Vec<i64>,
186 pub xdata: Vec<NotNan<f64>>,
188 pub ydata: Vec<NotNan<f64>>,
190 pub max_pixel: NotNan<f64>,
192 pub timestamp_trigger: Vec<Option<FlydraFloatTimestampLocal<Triggerbox>>>,
200 pub timestamp_host: Vec<FlydraFloatTimestampLocal<HostClock>>,
203}
204
205pub 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 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 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 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 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
482fn 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(); 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
498pub 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 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 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}