1use std::{error::Error, fmt, io, path::Path};
2
3use crate::{EventStream, EventStreamBuilder};
4
5mod aedat;
6mod bag;
7#[cfg(feature = "hdf5")]
8mod h5;
9mod npz;
10mod prophesee;
11mod text;
12
13pub use aedat::read_aedat;
14pub use bag::{open_bag_slice, read_bag, write_bag, BagSliceSource};
15#[cfg(feature = "hdf5")]
16pub use h5::{
17 open_hdf5_slice, read_hdf5, read_hdf5_frame, write_hdf5_frame, write_hdf5_stream,
18 Hdf5FrameSink, Hdf5SliceSource,
19};
20pub use npz::{read_npz, read_npz_frame, write_npz_frame, write_npz_stream};
21pub use prophesee::read_dat;
22pub use text::{
23 load_rows, open_text_slice, read_text, write_text_stream, ColumnOrder, RawRow, TextOptions,
24 TextReader, TimeUnit,
25};
26
27use crate::representation::EventFrame;
28use crate::viz::{render_frame, Colormap};
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct RawEvent {
33 pub x: u16,
34 pub y: u16,
35 pub t: i64,
36 pub p: bool,
37}
38
39pub trait EventSource {
42 fn sensor_size(&self) -> (usize, usize);
43 fn timestamp_scale_ms(&self) -> f64;
44 fn next_event(&mut self) -> Result<Option<RawEvent>, IoError>;
46}
47
48pub fn read_all(source: impl EventSource) -> Result<EventStream, IoError> {
50 read_capped(source, None)
51}
52
53pub fn read_capped(
55 mut source: impl EventSource,
56 max: Option<usize>,
57) -> Result<EventStream, IoError> {
58 let (width, height) = source.sensor_size();
59 let mut builder = EventStreamBuilder::new(width, height, source.timestamp_scale_ms());
60 while let Some(event) = source.next_event()? {
61 builder.push(event.x, event.y, event.t, event.p);
62 if max.is_some_and(|max| builder.len() >= max) {
63 break;
64 }
65 }
66 Ok(builder.build())
67}
68
69#[derive(Clone, Debug, Default)]
72pub struct LoadOptions {
73 pub sensor_size: Option<(usize, usize)>,
76 pub time_unit: Option<TimeUnit>,
80 pub order: ColumnOrder,
82 pub topic: Option<String>,
84 pub max_events: Option<usize>,
86 pub offset: Option<i64>,
89 pub keys: Option<EventKeys>,
94}
95
96#[derive(Clone, Debug)]
100pub struct EventKeys {
101 pub x: String,
102 pub y: String,
103 pub t: String,
104 pub p: String,
105}
106
107pub(crate) const X: usize = 0;
109pub(crate) const Y: usize = 1;
110pub(crate) const T: usize = 2;
111pub(crate) const P: usize = 3;
112
113pub(crate) const ROLE_KEYS: [&[&str]; 4] = [
117 &[
118 "x",
119 "xs",
120 "x_coordinate",
121 "x_coordinates",
122 "u",
123 "col",
124 "cols",
125 "column",
126 "columns",
127 ],
128 &[
129 "y",
130 "ys",
131 "y_coordinate",
132 "y_coordinates",
133 "v",
134 "row",
135 "rows",
136 ],
137 &[
138 "t",
139 "ts",
140 "time",
141 "times",
142 "timestamp",
143 "timestamps",
144 "time_stamp",
145 ],
146 &[
147 "p",
148 "ps",
149 "pol",
150 "pols",
151 "polarity",
152 "polarities",
153 "polarity_bit",
154 "polarity_bits",
155 "sign",
156 ],
157];
158
159pub(crate) fn role_of(name: &str) -> Option<usize> {
163 let lower = name.to_ascii_lowercase();
164 ROLE_KEYS
165 .iter()
166 .position(|keys| keys.contains(&lower.as_str()))
167}
168
169#[cfg_attr(not(feature = "hdf5"), allow(dead_code))]
172pub(crate) fn role_rank(role: usize, name: &str) -> Option<usize> {
173 let lower = name.to_ascii_lowercase();
174 ROLE_KEYS[role].iter().position(|key| *key == lower)
175}
176
177#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178enum Format {
179 Npz,
180 Text,
181 Hdf5,
182 Rosbag,
183 Aedat,
184 Aedat4,
185 PropheseeDat,
186 PropheseeRaw,
187 Png,
188}
189
190fn detect_format(path: &Path) -> Result<Format, IoError> {
191 let extension = path
192 .extension()
193 .and_then(|extension| extension.to_str())
194 .map(str::to_ascii_lowercase);
195 match extension.as_deref() {
196 Some("npz") => Ok(Format::Npz),
197 Some("txt") | Some("csv") => Ok(Format::Text),
198 Some("h5") | Some("hdf5") => Ok(Format::Hdf5),
199 Some("bag") => Ok(Format::Rosbag),
200 Some("aedat") => Ok(Format::Aedat),
201 Some("aedat4") => Ok(Format::Aedat4),
202 Some("dat") => Ok(Format::PropheseeDat),
203 Some("raw") => Ok(Format::PropheseeRaw),
204 Some("png") => Ok(Format::Png),
205 Some(other) => Err(IoError::Unsupported(format!(
206 "unrecognised file extension: .{other}"
207 ))),
208 None => Err(IoError::Unsupported(
209 "file has no extension to detect its format".to_owned(),
210 )),
211 }
212}
213
214pub fn load(path: impl AsRef<Path>, options: LoadOptions) -> Result<EventStream, IoError> {
218 let path = path.as_ref();
219 let Some(cutoff) = options.offset.filter(|&offset| offset > 0) else {
220 return load_format(path, &options);
221 };
222 let mut read_options = options.clone();
225 read_options.max_events = None;
226 load_format(path, &read_options).map(|stream| skip_before(stream, cutoff, options.max_events))
227}
228
229fn skip_before(stream: EventStream, cutoff: i64, max: Option<usize>) -> EventStream {
231 let (ts, (xs, ys, ps)) = (stream.ts(), (stream.xs(), stream.ys(), stream.ps()));
232 if ts.is_empty() {
233 return stream;
234 }
235 let (width, height) = stream.sensor_size();
236 let mut builder = EventStreamBuilder::new(width, height, stream.timestamp_scale_ms());
237 for index in (0..ts.len()).filter(|&index| ts[index] >= cutoff) {
238 builder.push(xs[index], ys[index], ts[index], ps[index]);
239 if max.is_some_and(|max| builder.len() >= max) {
240 break;
241 }
242 }
243 builder.build()
244}
245
246fn load_format(path: &Path, options: &LoadOptions) -> Result<EventStream, IoError> {
247 match detect_format(path)? {
248 Format::Npz => npz::read_npz(path, options.sensor_size),
249 Format::Text => text::load_text(path, options),
250 Format::Rosbag => bag::read_bag(path, options),
251 Format::Hdf5 => {
252 #[cfg(feature = "hdf5")]
253 {
254 h5::read_hdf5(path, options)
255 }
256 #[cfg(not(feature = "hdf5"))]
257 {
258 Err(IoError::Unsupported(
259 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
260 ))
261 }
262 }
263 Format::Aedat => aedat::read_aedat(path, options),
264 Format::Aedat4 => Err(IoError::Unsupported(
265 "AEDAT4 (.aedat4, iniVation DV FlatBuffer/LZ4) reading is not implemented yet"
266 .to_owned(),
267 )),
268 Format::PropheseeDat => prophesee::read_dat(path, options),
269 Format::PropheseeRaw => Err(IoError::Unsupported(
270 "Prophesee .raw (EVT2/EVT3) reading is not implemented yet".to_owned(),
271 )),
272 Format::Png => Err(IoError::Unsupported(
273 "PNG is a frame export format, not an event stream; use save_frame".to_owned(),
274 )),
275 }
276}
277
278pub trait SliceSource: Send {
282 fn sensor_size(&self) -> (usize, usize);
283 fn timestamp_scale_ms(&self) -> f64;
284 fn n_events(&self) -> usize;
286 fn time_span(&self) -> (i64, i64);
288 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError>;
290 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError>;
292
293 fn pixel_counts(&self) -> Result<Vec<u64>, IoError> {
298 const CHUNK: usize = 8_000_000;
299 let (width, height) = self.sensor_size();
300 let mut counts = vec![0u64; width * height];
301 let total = self.n_events();
302 let mut start = 0;
303 while start < total {
304 let end = (start + CHUNK).min(total);
305 self.slice_index(start, end)?.add_pixel_counts(&mut counts);
306 start = end;
307 }
308 Ok(counts)
309 }
310}
311
312pub struct MemorySliceSource {
316 stream: EventStream,
317}
318
319impl MemorySliceSource {
320 pub fn new(stream: EventStream) -> Self {
321 Self { stream }
322 }
323
324 fn rebuild(&self, indices: impl Iterator<Item = usize>) -> EventStream {
325 let (width, height) = self.stream.sensor_size();
326 let mut builder = EventStreamBuilder::new(width, height, self.stream.timestamp_scale_ms());
327 let (xs, ys, ts, ps) = (
328 self.stream.xs(),
329 self.stream.ys(),
330 self.stream.ts(),
331 self.stream.ps(),
332 );
333 for index in indices {
334 builder.push(xs[index], ys[index], ts[index], ps[index]);
335 }
336 builder.build()
337 }
338}
339
340impl SliceSource for MemorySliceSource {
341 fn sensor_size(&self) -> (usize, usize) {
342 self.stream.sensor_size()
343 }
344
345 fn timestamp_scale_ms(&self) -> f64 {
346 self.stream.timestamp_scale_ms()
347 }
348
349 fn n_events(&self) -> usize {
350 self.stream.len()
351 }
352
353 fn time_span(&self) -> (i64, i64) {
354 let ts = self.stream.ts();
355 match (ts.iter().min(), ts.iter().max()) {
356 (Some(&min), Some(&max)) => (min, max),
357 _ => (0, 0),
358 }
359 }
360
361 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError> {
362 let i0 = i0.min(self.stream.len());
363 let i1 = i1.clamp(i0, self.stream.len());
364 Ok(self.rebuild(i0..i1))
365 }
366
367 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError> {
368 let ts = self.stream.ts();
369 Ok(self.rebuild((0..ts.len()).filter(|&index| ts[index] >= t0 && ts[index] < t1)))
370 }
371
372 fn pixel_counts(&self) -> Result<Vec<u64>, IoError> {
373 let (width, height) = self.stream.sensor_size();
375 let mut counts = vec![0u64; width * height];
376 self.stream.add_pixel_counts(&mut counts);
377 Ok(counts)
378 }
379}
380
381pub type Reader = Box<dyn SliceSource>;
383
384pub fn open(path: impl AsRef<Path>, options: LoadOptions) -> Result<Reader, IoError> {
389 let path = path.as_ref();
390 match detect_format(path)? {
391 Format::Hdf5 => {
392 #[cfg(feature = "hdf5")]
393 {
394 Ok(Box::new(h5::open_hdf5_slice(path, &options)?))
395 }
396 #[cfg(not(feature = "hdf5"))]
397 {
398 Err(IoError::Unsupported(
399 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
400 ))
401 }
402 }
403 Format::Text => Ok(Box::new(text::open_text_slice(path, &options)?)),
404 Format::Rosbag => Ok(Box::new(bag::open_bag_slice(path, &options)?)),
405 _ => Ok(Box::new(MemorySliceSource::new(load(path, options)?))),
406 }
407}
408
409#[derive(Clone, Debug, Default)]
412pub struct SaveOptions {
413 pub topic: Option<String>,
416 pub colormap: Colormap,
418 pub normalize: Option<bool>,
420}
421
422pub fn save_stream(
426 path: impl AsRef<Path>,
427 stream: &EventStream,
428 options: &SaveOptions,
429) -> Result<(), IoError> {
430 let path = path.as_ref();
431 match detect_format(path)? {
432 Format::Npz => npz::write_npz_stream(path, stream),
433 Format::Text => text::write_text_stream(path, stream),
434 Format::Rosbag => bag::write_bag(path, stream, options.topic.as_deref()),
435 Format::Hdf5 => {
436 #[cfg(feature = "hdf5")]
437 {
438 h5::write_hdf5_stream(path, stream)
439 }
440 #[cfg(not(feature = "hdf5"))]
441 {
442 Err(IoError::Unsupported(
443 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
444 ))
445 }
446 }
447 other => Err(IoError::Unsupported(format!(
448 "saving an event stream as {other:?} is not supported"
449 ))),
450 }
451}
452
453pub fn save_frame(
456 path: impl AsRef<Path>,
457 frame: &EventFrame,
458 options: &SaveOptions,
459) -> Result<(), IoError> {
460 let path = path.as_ref();
461 match detect_format(path)? {
462 Format::Npz => npz::write_npz_frame(path, frame),
463 Format::Hdf5 => {
464 #[cfg(feature = "hdf5")]
465 {
466 h5::write_hdf5_frame(path, frame)
467 }
468 #[cfg(not(feature = "hdf5"))]
469 {
470 Err(IoError::Unsupported(
471 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
472 ))
473 }
474 }
475 Format::Png => write_png_frame(path, frame, options),
476 other => Err(IoError::Unsupported(format!(
477 "saving an event frame as {other:?} is not supported"
478 ))),
479 }
480}
481
482fn write_png_frame(path: &Path, frame: &EventFrame, options: &SaveOptions) -> Result<(), IoError> {
485 let image = render_frame(frame, options.colormap, options.normalize.unwrap_or(true));
486 let file = std::fs::File::create(path)?;
487 let writer = std::io::BufWriter::new(file);
488 let mut encoder = png::Encoder::new(writer, image.width as u32, image.height as u32);
489 encoder.set_color(png::ColorType::Rgb);
490 encoder.set_depth(png::BitDepth::Eight);
491 encoder
492 .write_header()
493 .and_then(|mut writer| writer.write_image_data(&image.pixels))
494 .map_err(|error| match error {
495 png::EncodingError::IoError(error) => IoError::Io(error),
496 other => IoError::Format(other.to_string()),
497 })
498}
499
500pub fn load_frame(path: impl AsRef<Path>) -> Result<EventFrame, IoError> {
503 let path = path.as_ref();
504 match detect_format(path)? {
505 Format::Npz => npz::read_npz_frame(path),
506 Format::Hdf5 => {
507 #[cfg(feature = "hdf5")]
508 {
509 h5::read_hdf5_frame(path)
510 }
511 #[cfg(not(feature = "hdf5"))]
512 {
513 Err(IoError::Unsupported(
514 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
515 ))
516 }
517 }
518 other => Err(IoError::Unsupported(format!(
519 "loading an event frame from {other:?} is not supported"
520 ))),
521 }
522}
523
524#[derive(Debug)]
525pub enum IoError {
526 Io(io::Error),
527 Parse { line: usize, message: String },
528 Format(String),
529 InvalidSensorSize,
530 Unsupported(String),
531}
532
533impl fmt::Display for IoError {
534 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
535 match self {
536 Self::Io(error) => error.fmt(formatter),
537 Self::Parse { line, message } => write!(formatter, "line {line}: {message}"),
538 Self::Format(message) => formatter.write_str(message),
539 Self::InvalidSensorSize => {
540 formatter.write_str("sensor width and height must be positive")
541 }
542 Self::Unsupported(message) => formatter.write_str(message),
543 }
544 }
545}
546
547impl Error for IoError {
548 fn source(&self) -> Option<&(dyn Error + 'static)> {
549 match self {
550 Self::Io(error) => Some(error),
551 _ => None,
552 }
553 }
554}
555
556impl From<io::Error> for IoError {
557 fn from(error: io::Error) -> Self {
558 Self::Io(error)
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::{load, open, IoError, LoadOptions, MemorySliceSource, SliceSource};
565 use crate::EventStreamBuilder;
566
567 #[test]
568 fn unknown_extension_is_unsupported() {
569 let error = load("recording.mp4", LoadOptions::default()).unwrap_err();
570 assert!(matches!(error, IoError::Unsupported(_)));
571 }
572
573 #[test]
574 fn hdf5_extension_dispatches_per_feature() {
575 let error = load("recording.h5", LoadOptions::default()).unwrap_err();
578 #[cfg(feature = "hdf5")]
579 assert!(matches!(error, IoError::Io(_)));
580 #[cfg(not(feature = "hdf5"))]
581 match error {
582 IoError::Unsupported(message) => assert!(message.contains("HDF5")),
583 other => panic!("expected unsupported error, got {other:?}"),
584 }
585 }
586
587 #[test]
588 fn text_missing_file_is_reported() {
589 let error = load("events.txt", LoadOptions::default()).unwrap_err();
591 assert!(matches!(error, IoError::Io(_)));
592 }
593
594 #[test]
595 fn aedat_dispatches_to_the_reader() {
596 let error = load("recording.aedat", LoadOptions::default()).unwrap_err();
598 assert!(matches!(error, IoError::Io(_)));
599 }
600
601 #[test]
602 fn prophesee_dat_dispatches_to_the_reader() {
603 let error = load("recording.dat", LoadOptions::default()).unwrap_err();
604 assert!(matches!(error, IoError::Io(_)));
605 }
606
607 #[test]
608 fn aedat4_and_prophesee_raw_are_unsupported() {
609 for path in ["recording.aedat4", "recording.raw"] {
611 match load(path, LoadOptions::default()) {
612 Err(IoError::Unsupported(_)) => {}
613 other => panic!("expected unsupported for {path}, got {other:?}"),
614 }
615 }
616 }
617
618 fn sample_source() -> MemorySliceSource {
619 let mut builder = EventStreamBuilder::new(4, 4, 0.001);
620 builder.push(0, 0, 0, true);
621 builder.push(1, 1, 10, false);
622 builder.push(2, 2, 20, true);
623 builder.push(0, 1, 30, false);
624 MemorySliceSource::new(builder.build())
625 }
626
627 #[test]
628 fn memory_source_reports_span_and_count() {
629 let source = sample_source();
630 assert_eq!(source.n_events(), 4);
631 assert_eq!(source.time_span(), (0, 30));
632 }
633
634 #[test]
635 fn memory_source_slices_by_time_and_index() {
636 let source = sample_source();
637
638 assert_eq!(source.slice_time(10, 30).unwrap().ts(), &[10, 20]);
640 assert_eq!(source.slice_index(1, 3).unwrap().ts(), &[10, 20]);
641 assert_eq!(source.slice_index(2, 100).unwrap().len(), 2); assert!(source.slice_time(100, 200).unwrap().is_empty());
643 }
644
645 #[test]
646 fn open_rejects_unknown_extension() {
647 match open("recording.mp4", LoadOptions::default()) {
649 Err(IoError::Unsupported(_)) => {}
650 Err(other) => panic!("expected unsupported error, got {other:?}"),
651 Ok(_) => panic!("expected an error for an unknown extension"),
652 }
653 }
654}