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 open_text_slice, read_text, write_text_stream, ColumnOrder, TextOptions, TextReader, TimeUnit,
24};
25
26use crate::representation::EventFrame;
27use crate::viz::{render_frame, Colormap};
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct RawEvent {
32 pub x: u16,
33 pub y: u16,
34 pub t: i64,
35 pub p: bool,
36}
37
38pub trait EventSource {
41 fn sensor_size(&self) -> (usize, usize);
42 fn timestamp_scale_ms(&self) -> f64;
43 fn next_event(&mut self) -> Result<Option<RawEvent>, IoError>;
45}
46
47pub fn read_all(source: impl EventSource) -> Result<EventStream, IoError> {
49 read_capped(source, None)
50}
51
52pub fn read_capped(
54 mut source: impl EventSource,
55 max: Option<usize>,
56) -> Result<EventStream, IoError> {
57 let (width, height) = source.sensor_size();
58 let mut builder = EventStreamBuilder::new(width, height, source.timestamp_scale_ms());
59 while let Some(event) = source.next_event()? {
60 builder.push(event.x, event.y, event.t, event.p);
61 if max.is_some_and(|max| builder.len() >= max) {
62 break;
63 }
64 }
65 Ok(builder.build())
66}
67
68#[derive(Clone, Debug, Default)]
71pub struct LoadOptions {
72 pub sensor_size: Option<(usize, usize)>,
75 pub time_unit: Option<TimeUnit>,
79 pub order: ColumnOrder,
81 pub topic: Option<String>,
83 pub max_events: Option<usize>,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88enum Format {
89 Npz,
90 Text,
91 Hdf5,
92 Rosbag,
93 Aedat,
94 Aedat4,
95 PropheseeDat,
96 PropheseeRaw,
97 Png,
98}
99
100fn detect_format(path: &Path) -> Result<Format, IoError> {
101 let extension = path
102 .extension()
103 .and_then(|extension| extension.to_str())
104 .map(str::to_ascii_lowercase);
105 match extension.as_deref() {
106 Some("npz") => Ok(Format::Npz),
107 Some("txt") | Some("csv") => Ok(Format::Text),
108 Some("h5") | Some("hdf5") => Ok(Format::Hdf5),
109 Some("bag") => Ok(Format::Rosbag),
110 Some("aedat") => Ok(Format::Aedat),
111 Some("aedat4") => Ok(Format::Aedat4),
112 Some("dat") => Ok(Format::PropheseeDat),
113 Some("raw") => Ok(Format::PropheseeRaw),
114 Some("png") => Ok(Format::Png),
115 Some(other) => Err(IoError::Unsupported(format!(
116 "unrecognised file extension: .{other}"
117 ))),
118 None => Err(IoError::Unsupported(
119 "file has no extension to detect its format".to_owned(),
120 )),
121 }
122}
123
124pub fn load(path: impl AsRef<Path>, options: LoadOptions) -> Result<EventStream, IoError> {
128 let path = path.as_ref();
129 match detect_format(path)? {
130 Format::Npz => npz::read_npz(path, options.sensor_size),
131 Format::Text => text::load_text(path, &options),
132 Format::Rosbag => bag::read_bag(path, &options),
133 Format::Hdf5 => {
134 #[cfg(feature = "hdf5")]
135 {
136 h5::read_hdf5(path, &options)
137 }
138 #[cfg(not(feature = "hdf5"))]
139 {
140 Err(IoError::Unsupported(
141 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
142 ))
143 }
144 }
145 Format::Aedat => aedat::read_aedat(path, &options),
146 Format::Aedat4 => Err(IoError::Unsupported(
147 "AEDAT4 (.aedat4, iniVation DV FlatBuffer/LZ4) reading is not implemented yet"
148 .to_owned(),
149 )),
150 Format::PropheseeDat => prophesee::read_dat(path, &options),
151 Format::PropheseeRaw => Err(IoError::Unsupported(
152 "Prophesee .raw (EVT2/EVT3) reading is not implemented yet".to_owned(),
153 )),
154 Format::Png => Err(IoError::Unsupported(
155 "PNG is a frame export format, not an event stream; use save_frame".to_owned(),
156 )),
157 }
158}
159
160pub trait SliceSource: Send {
164 fn sensor_size(&self) -> (usize, usize);
165 fn timestamp_scale_ms(&self) -> f64;
166 fn n_events(&self) -> usize;
168 fn time_span(&self) -> (i64, i64);
170 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError>;
172 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError>;
174}
175
176pub struct MemorySliceSource {
180 stream: EventStream,
181}
182
183impl MemorySliceSource {
184 pub fn new(stream: EventStream) -> Self {
185 Self { stream }
186 }
187
188 fn rebuild(&self, indices: impl Iterator<Item = usize>) -> EventStream {
189 let (width, height) = self.stream.sensor_size();
190 let mut builder = EventStreamBuilder::new(width, height, self.stream.timestamp_scale_ms());
191 let (xs, ys, ts, ps) = (
192 self.stream.xs(),
193 self.stream.ys(),
194 self.stream.ts(),
195 self.stream.ps(),
196 );
197 for index in indices {
198 builder.push(xs[index], ys[index], ts[index], ps[index]);
199 }
200 builder.build()
201 }
202}
203
204impl SliceSource for MemorySliceSource {
205 fn sensor_size(&self) -> (usize, usize) {
206 self.stream.sensor_size()
207 }
208
209 fn timestamp_scale_ms(&self) -> f64 {
210 self.stream.timestamp_scale_ms()
211 }
212
213 fn n_events(&self) -> usize {
214 self.stream.len()
215 }
216
217 fn time_span(&self) -> (i64, i64) {
218 let ts = self.stream.ts();
219 match (ts.iter().min(), ts.iter().max()) {
220 (Some(&min), Some(&max)) => (min, max),
221 _ => (0, 0),
222 }
223 }
224
225 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError> {
226 let i0 = i0.min(self.stream.len());
227 let i1 = i1.clamp(i0, self.stream.len());
228 Ok(self.rebuild(i0..i1))
229 }
230
231 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError> {
232 let ts = self.stream.ts();
233 Ok(self.rebuild((0..ts.len()).filter(|&index| ts[index] >= t0 && ts[index] < t1)))
234 }
235}
236
237pub type Reader = Box<dyn SliceSource>;
239
240pub fn open(path: impl AsRef<Path>, options: LoadOptions) -> Result<Reader, IoError> {
245 let path = path.as_ref();
246 match detect_format(path)? {
247 Format::Hdf5 => {
248 #[cfg(feature = "hdf5")]
249 {
250 Ok(Box::new(h5::open_hdf5_slice(path, &options)?))
251 }
252 #[cfg(not(feature = "hdf5"))]
253 {
254 Err(IoError::Unsupported(
255 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
256 ))
257 }
258 }
259 Format::Text => Ok(Box::new(text::open_text_slice(path, &options)?)),
260 Format::Rosbag => Ok(Box::new(bag::open_bag_slice(path, &options)?)),
261 _ => Ok(Box::new(MemorySliceSource::new(load(path, options)?))),
262 }
263}
264
265#[derive(Clone, Debug, Default)]
268pub struct SaveOptions {
269 pub topic: Option<String>,
272 pub colormap: Colormap,
274 pub normalize: Option<bool>,
276}
277
278pub fn save_stream(
282 path: impl AsRef<Path>,
283 stream: &EventStream,
284 options: &SaveOptions,
285) -> Result<(), IoError> {
286 let path = path.as_ref();
287 match detect_format(path)? {
288 Format::Npz => npz::write_npz_stream(path, stream),
289 Format::Text => text::write_text_stream(path, stream),
290 Format::Rosbag => bag::write_bag(path, stream, options.topic.as_deref()),
291 Format::Hdf5 => {
292 #[cfg(feature = "hdf5")]
293 {
294 h5::write_hdf5_stream(path, stream)
295 }
296 #[cfg(not(feature = "hdf5"))]
297 {
298 Err(IoError::Unsupported(
299 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
300 ))
301 }
302 }
303 other => Err(IoError::Unsupported(format!(
304 "saving an event stream as {other:?} is not supported"
305 ))),
306 }
307}
308
309pub fn save_frame(
312 path: impl AsRef<Path>,
313 frame: &EventFrame,
314 options: &SaveOptions,
315) -> Result<(), IoError> {
316 let path = path.as_ref();
317 match detect_format(path)? {
318 Format::Npz => npz::write_npz_frame(path, frame),
319 Format::Hdf5 => {
320 #[cfg(feature = "hdf5")]
321 {
322 h5::write_hdf5_frame(path, frame)
323 }
324 #[cfg(not(feature = "hdf5"))]
325 {
326 Err(IoError::Unsupported(
327 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
328 ))
329 }
330 }
331 Format::Png => write_png_frame(path, frame, options),
332 other => Err(IoError::Unsupported(format!(
333 "saving an event frame as {other:?} is not supported"
334 ))),
335 }
336}
337
338fn write_png_frame(path: &Path, frame: &EventFrame, options: &SaveOptions) -> Result<(), IoError> {
341 let image = render_frame(frame, options.colormap, options.normalize.unwrap_or(true));
342 let file = std::fs::File::create(path)?;
343 let writer = std::io::BufWriter::new(file);
344 let mut encoder = png::Encoder::new(writer, image.width as u32, image.height as u32);
345 encoder.set_color(png::ColorType::Rgb);
346 encoder.set_depth(png::BitDepth::Eight);
347 encoder
348 .write_header()
349 .and_then(|mut writer| writer.write_image_data(&image.pixels))
350 .map_err(|error| match error {
351 png::EncodingError::IoError(error) => IoError::Io(error),
352 other => IoError::Format(other.to_string()),
353 })
354}
355
356pub fn load_frame(path: impl AsRef<Path>) -> Result<EventFrame, IoError> {
359 let path = path.as_ref();
360 match detect_format(path)? {
361 Format::Npz => npz::read_npz_frame(path),
362 Format::Hdf5 => {
363 #[cfg(feature = "hdf5")]
364 {
365 h5::read_hdf5_frame(path)
366 }
367 #[cfg(not(feature = "hdf5"))]
368 {
369 Err(IoError::Unsupported(
370 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
371 ))
372 }
373 }
374 other => Err(IoError::Unsupported(format!(
375 "loading an event frame from {other:?} is not supported"
376 ))),
377 }
378}
379
380#[derive(Debug)]
381pub enum IoError {
382 Io(io::Error),
383 Parse { line: usize, message: String },
384 Format(String),
385 InvalidSensorSize,
386 Unsupported(String),
387}
388
389impl fmt::Display for IoError {
390 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
391 match self {
392 Self::Io(error) => error.fmt(formatter),
393 Self::Parse { line, message } => write!(formatter, "line {line}: {message}"),
394 Self::Format(message) => formatter.write_str(message),
395 Self::InvalidSensorSize => {
396 formatter.write_str("sensor width and height must be positive")
397 }
398 Self::Unsupported(message) => formatter.write_str(message),
399 }
400 }
401}
402
403impl Error for IoError {
404 fn source(&self) -> Option<&(dyn Error + 'static)> {
405 match self {
406 Self::Io(error) => Some(error),
407 _ => None,
408 }
409 }
410}
411
412impl From<io::Error> for IoError {
413 fn from(error: io::Error) -> Self {
414 Self::Io(error)
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::{load, open, IoError, LoadOptions, MemorySliceSource, SliceSource};
421 use crate::EventStreamBuilder;
422
423 #[test]
424 fn unknown_extension_is_unsupported() {
425 let error = load("recording.mp4", LoadOptions::default()).unwrap_err();
426 assert!(matches!(error, IoError::Unsupported(_)));
427 }
428
429 #[test]
430 fn hdf5_extension_dispatches_per_feature() {
431 let error = load("recording.h5", LoadOptions::default()).unwrap_err();
434 #[cfg(feature = "hdf5")]
435 assert!(matches!(error, IoError::Io(_)));
436 #[cfg(not(feature = "hdf5"))]
437 match error {
438 IoError::Unsupported(message) => assert!(message.contains("HDF5")),
439 other => panic!("expected unsupported error, got {other:?}"),
440 }
441 }
442
443 #[test]
444 fn text_missing_file_is_reported() {
445 let error = load("events.txt", LoadOptions::default()).unwrap_err();
447 assert!(matches!(error, IoError::Io(_)));
448 }
449
450 #[test]
451 fn aedat_dispatches_to_the_reader() {
452 let error = load("recording.aedat", LoadOptions::default()).unwrap_err();
454 assert!(matches!(error, IoError::Io(_)));
455 }
456
457 #[test]
458 fn prophesee_dat_dispatches_to_the_reader() {
459 let error = load("recording.dat", LoadOptions::default()).unwrap_err();
460 assert!(matches!(error, IoError::Io(_)));
461 }
462
463 #[test]
464 fn aedat4_and_prophesee_raw_are_unsupported() {
465 for path in ["recording.aedat4", "recording.raw"] {
467 match load(path, LoadOptions::default()) {
468 Err(IoError::Unsupported(_)) => {}
469 other => panic!("expected unsupported for {path}, got {other:?}"),
470 }
471 }
472 }
473
474 fn sample_source() -> MemorySliceSource {
475 let mut builder = EventStreamBuilder::new(4, 4, 0.001);
476 builder.push(0, 0, 0, true);
477 builder.push(1, 1, 10, false);
478 builder.push(2, 2, 20, true);
479 builder.push(0, 1, 30, false);
480 MemorySliceSource::new(builder.build())
481 }
482
483 #[test]
484 fn memory_source_reports_span_and_count() {
485 let source = sample_source();
486 assert_eq!(source.n_events(), 4);
487 assert_eq!(source.time_span(), (0, 30));
488 }
489
490 #[test]
491 fn memory_source_slices_by_time_and_index() {
492 let source = sample_source();
493
494 assert_eq!(source.slice_time(10, 30).unwrap().ts(), &[10, 20]);
496 assert_eq!(source.slice_index(1, 3).unwrap().ts(), &[10, 20]);
497 assert_eq!(source.slice_index(2, 100).unwrap().len(), 2); assert!(source.slice_time(100, 200).unwrap().is_empty());
499 }
500
501 #[test]
502 fn open_rejects_unknown_extension() {
503 match open("recording.mp4", LoadOptions::default()) {
505 Err(IoError::Unsupported(_)) => {}
506 Err(other) => panic!("expected unsupported error, got {other:?}"),
507 Ok(_) => panic!("expected an error for an unknown extension"),
508 }
509 }
510}