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 Hdf5EventSink, 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 supports_event_append(path: impl AsRef<Path>) -> bool {
218 matches!(detect_format(path.as_ref()), Ok(Format::Hdf5))
219}
220
221pub fn load(path: impl AsRef<Path>, options: LoadOptions) -> Result<EventStream, IoError> {
225 let path = path.as_ref();
226 let Some(cutoff) = options.offset.filter(|&offset| offset > 0) else {
227 return load_format(path, &options);
228 };
229 let mut read_options = options.clone();
232 read_options.max_events = None;
233 load_format(path, &read_options).map(|stream| skip_before(stream, cutoff, options.max_events))
234}
235
236fn skip_before(stream: EventStream, cutoff: i64, max: Option<usize>) -> EventStream {
238 let (ts, (xs, ys, ps)) = (stream.ts(), (stream.xs(), stream.ys(), stream.ps()));
239 if ts.is_empty() {
240 return stream;
241 }
242 let (width, height) = stream.sensor_size();
243 let mut builder = EventStreamBuilder::new(width, height, stream.timestamp_scale_ms());
244 for index in (0..ts.len()).filter(|&index| ts[index] >= cutoff) {
245 builder.push(xs[index], ys[index], ts[index], ps[index]);
246 if max.is_some_and(|max| builder.len() >= max) {
247 break;
248 }
249 }
250 builder.build()
251}
252
253fn load_format(path: &Path, options: &LoadOptions) -> Result<EventStream, IoError> {
254 match detect_format(path)? {
255 Format::Npz => npz::read_npz(path, options.sensor_size),
256 Format::Text => text::load_text(path, options),
257 Format::Rosbag => bag::read_bag(path, options),
258 Format::Hdf5 => {
259 #[cfg(feature = "hdf5")]
260 {
261 h5::read_hdf5(path, options)
262 }
263 #[cfg(not(feature = "hdf5"))]
264 {
265 Err(IoError::Unsupported(
266 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
267 ))
268 }
269 }
270 Format::Aedat => aedat::read_aedat(path, options),
271 Format::Aedat4 => Err(IoError::Unsupported(
272 "AEDAT4 (.aedat4, iniVation DV FlatBuffer/LZ4) reading is not implemented yet"
273 .to_owned(),
274 )),
275 Format::PropheseeDat => prophesee::read_dat(path, options),
276 Format::PropheseeRaw => Err(IoError::Unsupported(
277 "Prophesee .raw (EVT2/EVT3) reading is not implemented yet".to_owned(),
278 )),
279 Format::Png => Err(IoError::Unsupported(
280 "PNG is a frame export format, not an event stream; use save_frame".to_owned(),
281 )),
282 }
283}
284
285pub trait SliceSource: Send {
289 fn sensor_size(&self) -> (usize, usize);
290 fn timestamp_scale_ms(&self) -> f64;
291 fn n_events(&self) -> usize;
293 fn time_span(&self) -> (i64, i64);
295 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError>;
297 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError>;
299
300 fn pixel_counts(&self) -> Result<Vec<u64>, IoError> {
305 const CHUNK: usize = 8_000_000;
306 let (width, height) = self.sensor_size();
307 let mut counts = vec![0u64; width * height];
308 let total = self.n_events();
309 let mut start = 0;
310 while start < total {
311 let end = (start + CHUNK).min(total);
312 self.slice_index(start, end)?.add_pixel_counts(&mut counts);
313 start = end;
314 }
315 Ok(counts)
316 }
317}
318
319pub struct MemorySliceSource {
323 stream: EventStream,
324}
325
326impl MemorySliceSource {
327 pub fn new(stream: EventStream) -> Self {
328 Self { stream }
329 }
330
331 fn rebuild(&self, indices: impl Iterator<Item = usize>) -> EventStream {
332 let (width, height) = self.stream.sensor_size();
333 let mut builder = EventStreamBuilder::new(width, height, self.stream.timestamp_scale_ms());
334 let (xs, ys, ts, ps) = (
335 self.stream.xs(),
336 self.stream.ys(),
337 self.stream.ts(),
338 self.stream.ps(),
339 );
340 for index in indices {
341 builder.push(xs[index], ys[index], ts[index], ps[index]);
342 }
343 builder.build()
344 }
345}
346
347impl SliceSource for MemorySliceSource {
348 fn sensor_size(&self) -> (usize, usize) {
349 self.stream.sensor_size()
350 }
351
352 fn timestamp_scale_ms(&self) -> f64 {
353 self.stream.timestamp_scale_ms()
354 }
355
356 fn n_events(&self) -> usize {
357 self.stream.len()
358 }
359
360 fn time_span(&self) -> (i64, i64) {
361 let ts = self.stream.ts();
362 match (ts.iter().min(), ts.iter().max()) {
363 (Some(&min), Some(&max)) => (min, max),
364 _ => (0, 0),
365 }
366 }
367
368 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError> {
369 let i0 = i0.min(self.stream.len());
370 let i1 = i1.clamp(i0, self.stream.len());
371 Ok(self.rebuild(i0..i1))
372 }
373
374 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError> {
375 let ts = self.stream.ts();
376 Ok(self.rebuild((0..ts.len()).filter(|&index| ts[index] >= t0 && ts[index] < t1)))
377 }
378
379 fn pixel_counts(&self) -> Result<Vec<u64>, IoError> {
380 let (width, height) = self.stream.sensor_size();
382 let mut counts = vec![0u64; width * height];
383 self.stream.add_pixel_counts(&mut counts);
384 Ok(counts)
385 }
386}
387
388pub type Reader = Box<dyn SliceSource>;
390
391pub fn open(path: impl AsRef<Path>, options: LoadOptions) -> Result<Reader, IoError> {
396 let path = path.as_ref();
397 match detect_format(path)? {
398 Format::Hdf5 => {
399 #[cfg(feature = "hdf5")]
400 {
401 Ok(Box::new(h5::open_hdf5_slice(path, &options)?))
402 }
403 #[cfg(not(feature = "hdf5"))]
404 {
405 Err(IoError::Unsupported(
406 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
407 ))
408 }
409 }
410 Format::Text => Ok(Box::new(text::open_text_slice(path, &options)?)),
411 Format::Rosbag => Ok(Box::new(bag::open_bag_slice(path, &options)?)),
412 _ => Ok(Box::new(MemorySliceSource::new(load(path, options)?))),
413 }
414}
415
416#[derive(Clone, Debug, Default)]
419pub struct SaveOptions {
420 pub topic: Option<String>,
423 pub colormap: Colormap,
425 pub normalize: Option<bool>,
427}
428
429pub fn save_stream(
433 path: impl AsRef<Path>,
434 stream: &EventStream,
435 options: &SaveOptions,
436) -> Result<(), IoError> {
437 let path = path.as_ref();
438 match detect_format(path)? {
439 Format::Npz => npz::write_npz_stream(path, stream),
440 Format::Text => text::write_text_stream(path, stream),
441 Format::Rosbag => bag::write_bag(path, stream, options.topic.as_deref()),
442 Format::Hdf5 => {
443 #[cfg(feature = "hdf5")]
444 {
445 h5::write_hdf5_stream(path, stream)
446 }
447 #[cfg(not(feature = "hdf5"))]
448 {
449 Err(IoError::Unsupported(
450 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
451 ))
452 }
453 }
454 other => Err(IoError::Unsupported(format!(
455 "saving an event stream as {other:?} is not supported"
456 ))),
457 }
458}
459
460pub fn save_frame(
463 path: impl AsRef<Path>,
464 frame: &EventFrame,
465 options: &SaveOptions,
466) -> Result<(), IoError> {
467 let path = path.as_ref();
468 match detect_format(path)? {
469 Format::Npz => npz::write_npz_frame(path, frame),
470 Format::Hdf5 => {
471 #[cfg(feature = "hdf5")]
472 {
473 h5::write_hdf5_frame(path, frame)
474 }
475 #[cfg(not(feature = "hdf5"))]
476 {
477 Err(IoError::Unsupported(
478 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
479 ))
480 }
481 }
482 Format::Png => write_png_frame(path, frame, options),
483 other => Err(IoError::Unsupported(format!(
484 "saving an event frame as {other:?} is not supported"
485 ))),
486 }
487}
488
489fn write_png_frame(path: &Path, frame: &EventFrame, options: &SaveOptions) -> Result<(), IoError> {
492 let image = render_frame(frame, options.colormap, options.normalize.unwrap_or(true));
493 let file = std::fs::File::create(path)?;
494 let writer = std::io::BufWriter::new(file);
495 let mut encoder = png::Encoder::new(writer, image.width as u32, image.height as u32);
496 encoder.set_color(png::ColorType::Rgb);
497 encoder.set_depth(png::BitDepth::Eight);
498 encoder
499 .write_header()
500 .and_then(|mut writer| writer.write_image_data(&image.pixels))
501 .map_err(|error| match error {
502 png::EncodingError::IoError(error) => IoError::Io(error),
503 other => IoError::Format(other.to_string()),
504 })
505}
506
507pub fn load_frame(path: impl AsRef<Path>) -> Result<EventFrame, IoError> {
510 let path = path.as_ref();
511 match detect_format(path)? {
512 Format::Npz => npz::read_npz_frame(path),
513 Format::Hdf5 => {
514 #[cfg(feature = "hdf5")]
515 {
516 h5::read_hdf5_frame(path)
517 }
518 #[cfg(not(feature = "hdf5"))]
519 {
520 Err(IoError::Unsupported(
521 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
522 ))
523 }
524 }
525 other => Err(IoError::Unsupported(format!(
526 "loading an event frame from {other:?} is not supported"
527 ))),
528 }
529}
530
531#[derive(Debug)]
532pub enum IoError {
533 Io(io::Error),
534 Parse { line: usize, message: String },
535 Format(String),
536 InvalidSensorSize,
537 Unsupported(String),
538}
539
540impl fmt::Display for IoError {
541 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
542 match self {
543 Self::Io(error) => error.fmt(formatter),
544 Self::Parse { line, message } => write!(formatter, "line {line}: {message}"),
545 Self::Format(message) => formatter.write_str(message),
546 Self::InvalidSensorSize => {
547 formatter.write_str("sensor width and height must be positive")
548 }
549 Self::Unsupported(message) => formatter.write_str(message),
550 }
551 }
552}
553
554impl Error for IoError {
555 fn source(&self) -> Option<&(dyn Error + 'static)> {
556 match self {
557 Self::Io(error) => Some(error),
558 _ => None,
559 }
560 }
561}
562
563impl From<io::Error> for IoError {
564 fn from(error: io::Error) -> Self {
565 Self::Io(error)
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::{load, open, IoError, LoadOptions, MemorySliceSource, SliceSource};
572 use crate::EventStreamBuilder;
573
574 #[test]
575 fn unknown_extension_is_unsupported() {
576 let error = load("recording.mp4", LoadOptions::default()).unwrap_err();
577 assert!(matches!(error, IoError::Unsupported(_)));
578 }
579
580 #[test]
581 fn hdf5_extension_dispatches_per_feature() {
582 let error = load("recording.h5", LoadOptions::default()).unwrap_err();
585 #[cfg(feature = "hdf5")]
586 assert!(matches!(error, IoError::Io(_)));
587 #[cfg(not(feature = "hdf5"))]
588 match error {
589 IoError::Unsupported(message) => assert!(message.contains("HDF5")),
590 other => panic!("expected unsupported error, got {other:?}"),
591 }
592 }
593
594 #[test]
595 fn text_missing_file_is_reported() {
596 let error = load("events.txt", LoadOptions::default()).unwrap_err();
598 assert!(matches!(error, IoError::Io(_)));
599 }
600
601 #[test]
602 fn aedat_dispatches_to_the_reader() {
603 let error = load("recording.aedat", LoadOptions::default()).unwrap_err();
605 assert!(matches!(error, IoError::Io(_)));
606 }
607
608 #[test]
609 fn prophesee_dat_dispatches_to_the_reader() {
610 let error = load("recording.dat", LoadOptions::default()).unwrap_err();
611 assert!(matches!(error, IoError::Io(_)));
612 }
613
614 #[test]
615 fn aedat4_and_prophesee_raw_are_unsupported() {
616 for path in ["recording.aedat4", "recording.raw"] {
618 match load(path, LoadOptions::default()) {
619 Err(IoError::Unsupported(_)) => {}
620 other => panic!("expected unsupported for {path}, got {other:?}"),
621 }
622 }
623 }
624
625 fn sample_source() -> MemorySliceSource {
626 let mut builder = EventStreamBuilder::new(4, 4, 0.001);
627 builder.push(0, 0, 0, true);
628 builder.push(1, 1, 10, false);
629 builder.push(2, 2, 20, true);
630 builder.push(0, 1, 30, false);
631 MemorySliceSource::new(builder.build())
632 }
633
634 #[test]
635 fn memory_source_reports_span_and_count() {
636 let source = sample_source();
637 assert_eq!(source.n_events(), 4);
638 assert_eq!(source.time_span(), (0, 30));
639 }
640
641 #[test]
642 fn memory_source_slices_by_time_and_index() {
643 let source = sample_source();
644
645 assert_eq!(source.slice_time(10, 30).unwrap().ts(), &[10, 20]);
647 assert_eq!(source.slice_index(1, 3).unwrap().ts(), &[10, 20]);
648 assert_eq!(source.slice_index(2, 100).unwrap().len(), 2); assert!(source.slice_time(100, 200).unwrap().is_empty());
650 }
651
652 #[test]
653 fn open_rejects_unknown_extension() {
654 match open("recording.mp4", LoadOptions::default()) {
656 Err(IoError::Unsupported(_)) => {}
657 Err(other) => panic!("expected unsupported error, got {other:?}"),
658 Ok(_) => panic!("expected an error for an unknown extension"),
659 }
660 }
661}