1use std::{error::Error, fmt, io, path::Path};
2
3use crate::{EventStream, EventStreamBuilder};
4
5mod aedat;
6mod aedat4;
7mod bag;
8mod e2vid;
9#[cfg(feature = "hdf5")]
10mod h5;
11mod npz;
12mod prophesee;
13mod prophesee_raw;
14mod text;
15
16pub use aedat::{open_aedat_slice, read_aedat, AedatEventSink};
17pub use aedat4::{
18 open_aedat4_slice, read_aedat4, Aedat4EventSink, Compression as PacketCompression,
19};
20pub use bag::{
21 bag_topics, open_bag_slice, read_bag, read_bag_camera_info, read_bag_frames, read_bag_imu,
22 write_bag, BagEventSink, BagSliceSource, ImuSample,
23};
24pub use e2vid::{write_e2vid, E2vidWriter};
25#[cfg(feature = "hdf5")]
26pub use h5::{
27 open_hdf5_slice, read_hdf5, read_hdf5_frame, write_hdf5_frame, write_hdf5_stream,
28 Hdf5EventSink, Hdf5FrameSink, Hdf5SliceSource,
29};
30pub use npz::{read_npz, read_npz_frame, write_npz_frame, write_npz_stream, NpzEventSink};
31pub use prophesee::{read_dat, DatEventSink};
32pub use prophesee_raw::{decode_words, open_raw_slice, read_raw, EvtVersion, RawEventSink};
33pub use text::{
34 load_rows, open_text_slice, read_text, write_text_stream, ColumnOrder, RawRow, TextEventSink,
35 TextOptions, TextReader, TimeUnit,
36};
37
38use crate::representation::{EventFrame, EventFrameData};
39use crate::viz::{render_frame, Colormap};
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct RawEvent {
44 pub x: u16,
45 pub y: u16,
46 pub t: i64,
47 pub p: bool,
48}
49
50pub trait EventSource {
53 fn sensor_size(&self) -> (usize, usize);
54 fn timestamp_scale_ms(&self) -> f64;
55 fn next_event(&mut self) -> Result<Option<RawEvent>, IoError>;
57}
58
59pub fn read_all(source: impl EventSource) -> Result<EventStream, IoError> {
61 read_capped(source, None)
62}
63
64pub fn read_capped(
66 mut source: impl EventSource,
67 max: Option<usize>,
68) -> Result<EventStream, IoError> {
69 let (width, height) = source.sensor_size();
70 let mut builder = EventStreamBuilder::new(width, height, source.timestamp_scale_ms());
71 while let Some(event) = source.next_event()? {
72 builder.push(event.x, event.y, event.t, event.p);
73 if max.is_some_and(|max| builder.len() >= max) {
74 break;
75 }
76 }
77 Ok(builder.build())
78}
79
80#[derive(Clone, Debug, Default)]
83pub struct LoadOptions {
84 pub sensor_size: Option<(usize, usize)>,
87 pub time_unit: Option<TimeUnit>,
91 pub order: ColumnOrder,
93 pub topic: Option<String>,
95 pub max_events: Option<usize>,
97 pub offset: Option<i64>,
100 pub keys: Option<EventKeys>,
105}
106
107#[derive(Clone, Debug)]
111pub struct EventKeys {
112 pub x: String,
113 pub y: String,
114 pub t: String,
115 pub p: String,
116}
117
118pub(crate) const X: usize = 0;
120pub(crate) const Y: usize = 1;
121pub(crate) const T: usize = 2;
122pub(crate) const P: usize = 3;
123
124pub(crate) const ROLE_KEYS: [&[&str]; 4] = [
128 &[
129 "x",
130 "xs",
131 "x_coordinate",
132 "x_coordinates",
133 "u",
134 "col",
135 "cols",
136 "column",
137 "columns",
138 ],
139 &[
140 "y",
141 "ys",
142 "y_coordinate",
143 "y_coordinates",
144 "v",
145 "row",
146 "rows",
147 ],
148 &[
149 "t",
150 "ts",
151 "time",
152 "times",
153 "timestamp",
154 "timestamps",
155 "time_stamp",
156 ],
157 &[
158 "p",
159 "ps",
160 "pol",
161 "pols",
162 "polarity",
163 "polarities",
164 "polarity_bit",
165 "polarity_bits",
166 "sign",
167 ],
168];
169
170pub(crate) fn role_of(name: &str) -> Option<usize> {
174 let lower = name.to_ascii_lowercase();
175 ROLE_KEYS
176 .iter()
177 .position(|keys| keys.contains(&lower.as_str()))
178}
179
180#[cfg_attr(not(feature = "hdf5"), allow(dead_code))]
183pub(crate) fn role_rank(role: usize, name: &str) -> Option<usize> {
184 let lower = name.to_ascii_lowercase();
185 ROLE_KEYS[role].iter().position(|key| *key == lower)
186}
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189enum Format {
190 Npz,
191 Text,
192 Hdf5,
193 Rosbag,
194 Aedat,
195 Aedat4,
196 PropheseeDat,
197 PropheseeRaw,
198 Png,
199 E2vid,
201}
202
203fn detect_format(path: &Path) -> Result<Format, IoError> {
204 let extension = path
205 .extension()
206 .and_then(|extension| extension.to_str())
207 .map(str::to_ascii_lowercase);
208 match extension.as_deref() {
209 Some("npz") => Ok(Format::Npz),
210 Some("txt") | Some("csv") => Ok(Format::Text),
211 Some("h5") | Some("hdf5") => Ok(Format::Hdf5),
212 Some("bag") => Ok(Format::Rosbag),
213 Some("aedat") => Ok(Format::Aedat),
214 Some("aedat4") => Ok(Format::Aedat4),
215 Some("dat") => Ok(Format::PropheseeDat),
216 Some("raw") => Ok(Format::PropheseeRaw),
217 Some("png") => Ok(Format::Png),
218 Some("zip") => Ok(Format::E2vid),
219 Some(other) => Err(IoError::Unsupported(format!(
220 "unrecognised file extension: .{other}"
221 ))),
222 None => Err(IoError::Unsupported(
223 "file has no extension to detect its format".to_owned(),
224 )),
225 }
226}
227
228pub trait EventSink: Send {
236 fn append(&mut self, stream: &EventStream) -> Result<(), IoError>;
239
240 fn n_events(&self) -> usize;
242
243 fn flush(&mut self) -> Result<(), IoError>;
247
248 fn finish(self: Box<Self>) -> Result<(), IoError>;
250}
251
252pub fn open_sink(
255 path: impl AsRef<Path>,
256 options: &SaveOptions,
257) -> Result<Box<dyn EventSink>, IoError> {
258 let path = path.as_ref();
259 match requested_format(path, options)? {
260 Format::Npz => Ok(Box::new(npz::NpzEventSink::create(path)?)),
261 Format::Text => Ok(Box::new(text::TextEventSink::create(path)?)),
262 Format::E2vid => Ok(Box::new(e2vid::E2vidWriter::create(path)?)),
263 Format::Rosbag => Ok(Box::new(bag::BagEventSink::create(
264 path,
265 options.topic.as_deref(),
266 )?)),
267 Format::Aedat => Ok(Box::new(aedat::AedatEventSink::create(path)?)),
268 Format::Aedat4 => Ok(Box::new(aedat4::Aedat4EventSink::create(
269 path,
270 options.packet_compression,
271 )?)),
272 Format::PropheseeDat => Ok(Box::new(prophesee::DatEventSink::create(path)?)),
273 Format::PropheseeRaw => Ok(Box::new(prophesee_raw::RawEventSink::create(
274 path,
275 options.evt_version(),
276 )?)),
277 Format::Hdf5 => {
278 #[cfg(feature = "hdf5")]
279 {
280 Ok(Box::new(h5::Hdf5EventSink::open(path, options.compression)?))
281 }
282 #[cfg(not(feature = "hdf5"))]
283 {
284 Err(IoError::Unsupported(
285 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
286 ))
287 }
288 }
289 Format::Png => Err(IoError::Unsupported(
290 "PNG is a frame export format, not an event container".to_owned(),
291 )),
292 }
293}
294
295pub fn supports_event_append(path: impl AsRef<Path>) -> bool {
299 !matches!(
300 detect_format(path.as_ref()),
301 Err(_) | Ok(Format::Png)
302 )
303}
304
305pub fn is_e2vid_target(path: impl AsRef<Path>, format: Option<&str>) -> bool {
309 let options = SaveOptions {
310 format: format.map(str::to_owned),
311 ..SaveOptions::default()
312 };
313 matches!(requested_format(path.as_ref(), &options), Ok(Format::E2vid))
314}
315
316pub fn load(path: impl AsRef<Path>, options: LoadOptions) -> Result<EventStream, IoError> {
320 let path = path.as_ref();
321 let Some(cutoff) = options.offset.filter(|&offset| offset > 0) else {
322 return load_format(path, &options);
323 };
324 let mut read_options = options.clone();
327 read_options.max_events = None;
328 load_format(path, &read_options).map(|stream| skip_before(stream, cutoff, options.max_events))
329}
330
331fn skip_before(stream: EventStream, cutoff: i64, max: Option<usize>) -> EventStream {
333 let (ts, (xs, ys, ps)) = (stream.ts(), (stream.xs(), stream.ys(), stream.ps()));
334 if ts.is_empty() {
335 return stream;
336 }
337 let (width, height) = stream.sensor_size();
338 let mut builder = EventStreamBuilder::new(width, height, stream.timestamp_scale_ms());
339 for index in (0..ts.len()).filter(|&index| ts[index] >= cutoff) {
340 builder.push(xs[index], ys[index], ts[index], ps[index]);
341 if max.is_some_and(|max| builder.len() >= max) {
342 break;
343 }
344 }
345 builder.build()
346}
347
348fn load_format(path: &Path, options: &LoadOptions) -> Result<EventStream, IoError> {
349 match detect_format(path)? {
350 Format::Npz => npz::read_npz(path, options.sensor_size),
351 Format::Text => text::load_text(path, options),
352 Format::Rosbag => bag::read_bag(path, options),
353 Format::Hdf5 => {
354 #[cfg(feature = "hdf5")]
355 {
356 h5::read_hdf5(path, options)
357 }
358 #[cfg(not(feature = "hdf5"))]
359 {
360 Err(IoError::Unsupported(
361 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
362 ))
363 }
364 }
365 Format::Aedat => aedat::read_aedat(path, options),
366 Format::Aedat4 => aedat4::read_aedat4(path, options),
367 Format::PropheseeDat => prophesee::read_dat(path, options),
368 Format::PropheseeRaw => prophesee_raw::read_raw(path, options),
369 Format::Png => Err(IoError::Unsupported(
370 "PNG is a frame export format, not an event stream; use save_frame".to_owned(),
371 )),
372 Format::E2vid => Err(IoError::Unsupported(
373 "E2VID's .zip is an export format for that reconstruction pipeline, not one eventcv \
374 reads back; save an npz/h5/bag alongside it to keep the recording"
375 .to_owned(),
376 )),
377 }
378}
379
380pub trait EventConsumer {
390 fn push(&mut self, x: u16, y: u16, t: i64, p: bool);
394}
395
396impl EventConsumer for EventStreamBuilder {
397 #[inline]
398 fn push(&mut self, x: u16, y: u16, t: i64, p: bool) {
399 self.push_in_bounds(x, y, t, p);
400 }
401}
402
403pub trait SliceSource: Send {
404 fn sensor_size(&self) -> (usize, usize);
405 fn timestamp_scale_ms(&self) -> f64;
406 fn n_events(&self) -> usize;
408 fn time_span(&self) -> (i64, i64);
410 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError>;
412 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError>;
414
415 fn slice_time_into(&self, t0: i64, t1: i64, sink: &mut dyn EventConsumer) -> Result<(), IoError> {
421 let stream = self.slice_time(t0, t1)?;
422 let (xs, ys, ts, ps) = (stream.xs(), stream.ys(), stream.ts(), stream.ps());
423 for index in 0..stream.len() {
424 sink.push(xs[index], ys[index], ts[index], ps[index]);
425 }
426 Ok(())
427 }
428
429 fn frames(&self, _t0: i64, _t1: i64) -> Result<Vec<(i64, EventFrame)>, IoError> {
436 Ok(Vec::new())
437 }
438
439 fn imu(&self, _t0: i64, _t1: i64) -> Result<Vec<ImuSample>, IoError> {
441 Ok(Vec::new())
442 }
443
444 fn camera(&self) -> Result<Option<crate::camera::Camera>, IoError> {
446 Ok(None)
447 }
448
449 fn pixel_counts(&self) -> Result<Vec<u64>, IoError> {
454 const CHUNK: usize = 8_000_000;
455 let (width, height) = self.sensor_size();
456 let mut counts = vec![0u64; width * height];
457 let total = self.n_events();
458 let mut start = 0;
459 while start < total {
460 let end = (start + CHUNK).min(total);
461 self.slice_index(start, end)?.add_pixel_counts(&mut counts);
462 start = end;
463 }
464 Ok(counts)
465 }
466}
467
468pub struct MemorySliceSource {
472 stream: EventStream,
473}
474
475impl MemorySliceSource {
476 pub fn new(stream: EventStream) -> Self {
477 Self { stream }
478 }
479
480 fn rebuild(&self, indices: impl Iterator<Item = usize>) -> EventStream {
481 let (width, height) = self.stream.sensor_size();
482 let mut builder = EventStreamBuilder::new(width, height, self.stream.timestamp_scale_ms());
483 let (xs, ys, ts, ps) = (
484 self.stream.xs(),
485 self.stream.ys(),
486 self.stream.ts(),
487 self.stream.ps(),
488 );
489 for index in indices {
490 builder.push(xs[index], ys[index], ts[index], ps[index]);
491 }
492 builder.build()
493 }
494}
495
496impl SliceSource for MemorySliceSource {
497 fn sensor_size(&self) -> (usize, usize) {
498 self.stream.sensor_size()
499 }
500
501 fn timestamp_scale_ms(&self) -> f64 {
502 self.stream.timestamp_scale_ms()
503 }
504
505 fn n_events(&self) -> usize {
506 self.stream.len()
507 }
508
509 fn time_span(&self) -> (i64, i64) {
510 let ts = self.stream.ts();
511 match (ts.iter().min(), ts.iter().max()) {
512 (Some(&min), Some(&max)) => (min, max),
513 _ => (0, 0),
514 }
515 }
516
517 fn slice_index(&self, i0: usize, i1: usize) -> Result<EventStream, IoError> {
518 let i0 = i0.min(self.stream.len());
519 let i1 = i1.clamp(i0, self.stream.len());
520 Ok(self.rebuild(i0..i1))
521 }
522
523 fn slice_time(&self, t0: i64, t1: i64) -> Result<EventStream, IoError> {
524 let ts = self.stream.ts();
525 Ok(self.rebuild((0..ts.len()).filter(|&index| ts[index] >= t0 && ts[index] < t1)))
526 }
527
528 fn pixel_counts(&self) -> Result<Vec<u64>, IoError> {
529 let (width, height) = self.stream.sensor_size();
531 let mut counts = vec![0u64; width * height];
532 self.stream.add_pixel_counts(&mut counts);
533 Ok(counts)
534 }
535}
536
537pub type Reader = Box<dyn SliceSource>;
539
540pub fn open(path: impl AsRef<Path>, options: LoadOptions) -> Result<Reader, IoError> {
546 let path = path.as_ref();
547 match detect_format(path)? {
548 Format::Hdf5 => {
549 #[cfg(feature = "hdf5")]
550 {
551 Ok(Box::new(h5::open_hdf5_slice(path, &options)?))
552 }
553 #[cfg(not(feature = "hdf5"))]
554 {
555 Err(IoError::Unsupported(
556 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
557 ))
558 }
559 }
560 Format::Text => Ok(Box::new(text::open_text_slice(path, &options)?)),
561 Format::Rosbag => Ok(Box::new(bag::open_bag_slice(path, &options)?)),
562 Format::Aedat => Ok(Box::new(aedat::open_aedat_slice(path, &options)?)),
563 Format::Aedat4 => Ok(Box::new(aedat4::open_aedat4_slice(path, &options)?)),
564 Format::PropheseeRaw => Ok(Box::new(prophesee_raw::open_raw_slice(path, &options)?)),
565 _ => Ok(Box::new(MemorySliceSource::new(load(path, options)?))),
566 }
567}
568
569#[derive(Clone, Copy, Debug, PartialEq, Eq)]
576pub enum Compression {
577 None,
579 Gzip(u8),
581}
582
583impl Default for Compression {
588 fn default() -> Self {
589 Self::Gzip(1)
590 }
591}
592
593impl Compression {
594 pub fn level(self) -> Option<u8> {
597 match self {
598 Self::None | Self::Gzip(0) => None,
599 Self::Gzip(level) => Some(level.min(9)),
600 }
601 }
602}
603
604#[derive(Clone, Debug, Default)]
607pub struct SaveOptions {
608 pub topic: Option<String>,
611 pub colormap: Colormap,
613 pub normalize: Option<bool>,
615 pub format: Option<String>,
620 pub compression: Compression,
622 pub packet_compression: PacketCompression,
624}
625
626impl SaveOptions {
627 pub fn evt_version(&self) -> EvtVersion {
633 match self.format.as_deref() {
634 Some("evt3") => EvtVersion::Evt3,
635 _ => EvtVersion::Evt2,
636 }
637 }
638}
639
640fn requested_format(path: &Path, options: &SaveOptions) -> Result<Format, IoError> {
643 match options.format.as_deref() {
644 None => detect_format(path),
645 Some("e2vid") => Ok(Format::E2vid),
646 Some("npz") => Ok(Format::Npz),
647 Some("txt") | Some("csv") | Some("text") => Ok(Format::Text),
648 Some("h5") | Some("hdf5") => Ok(Format::Hdf5),
649 Some("bag") | Some("rosbag") => Ok(Format::Rosbag),
650 Some("aedat") | Some("aedat2") => Ok(Format::Aedat),
651 Some("aedat4") => Ok(Format::Aedat4),
652 Some("dat") => Ok(Format::PropheseeDat),
653 Some("raw") | Some("evt2") | Some("evt3") => Ok(Format::PropheseeRaw),
656 Some("png") => Ok(Format::Png),
657 Some(other) => Err(IoError::Unsupported(format!(
658 "unknown format: {other} (expected npz, txt, h5, bag, aedat, aedat4, dat, raw, evt2, \
659 evt3, png, or e2vid)"
660 ))),
661 }
662}
663
664pub fn save_stream(
670 path: impl AsRef<Path>,
671 stream: &EventStream,
672 options: &SaveOptions,
673) -> Result<(), IoError> {
674 let path = path.as_ref();
675 match requested_format(path, options)? {
676 Format::Npz => npz::write_npz_stream(path, stream),
677 Format::Text => text::write_text_stream(path, stream),
678 Format::E2vid => e2vid::write_e2vid(path, stream),
679 Format::Rosbag => bag::write_bag(path, stream, options.topic.as_deref()),
680 Format::Hdf5 => {
681 #[cfg(feature = "hdf5")]
682 {
683 h5::write_hdf5_stream(path, stream, options.compression)
684 }
685 #[cfg(not(feature = "hdf5"))]
686 {
687 Err(IoError::Unsupported(
688 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
689 ))
690 }
691 }
692 Format::Aedat | Format::Aedat4 | Format::PropheseeDat | Format::PropheseeRaw => {
696 let mut sink = open_sink(path, options)?;
697 sink.append(stream)?;
698 sink.finish()
699 }
700 Format::Png => Err(IoError::Unsupported(
701 "PNG is a frame export format; use save_frame".to_owned(),
702 )),
703 }
704}
705
706pub fn save_frame(
709 path: impl AsRef<Path>,
710 frame: &EventFrame,
711 options: &SaveOptions,
712) -> Result<(), IoError> {
713 let path = path.as_ref();
714 match detect_format(path)? {
715 Format::Npz => npz::write_npz_frame(path, frame),
716 Format::Hdf5 => {
717 #[cfg(feature = "hdf5")]
718 {
719 h5::write_hdf5_frame(path, frame)
720 }
721 #[cfg(not(feature = "hdf5"))]
722 {
723 Err(IoError::Unsupported(
724 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
725 ))
726 }
727 }
728 Format::Png => write_png_frame(path, frame, options),
729 other => Err(IoError::Unsupported(format!(
730 "saving an event frame as {other:?} is not supported"
731 ))),
732 }
733}
734
735fn write_png_frame(path: &Path, frame: &EventFrame, options: &SaveOptions) -> Result<(), IoError> {
738 let image = render_frame(frame, options.colormap, options.normalize.unwrap_or(true));
739 let file = std::fs::File::create(path)?;
740 let writer = std::io::BufWriter::new(file);
741 let mut encoder = png::Encoder::new(writer, image.width as u32, image.height as u32);
742 encoder.set_color(png::ColorType::Rgb);
743 encoder.set_depth(png::BitDepth::Eight);
744 encoder
745 .write_header()
746 .and_then(|mut writer| writer.write_image_data(&image.pixels))
747 .map_err(png_encoding_error)
748}
749
750fn png_encoding_error(error: png::EncodingError) -> IoError {
751 match error {
752 png::EncodingError::IoError(error) => IoError::Io(error),
753 other => IoError::Format(other.to_string()),
754 }
755}
756
757pub fn write_mask(
761 path: impl AsRef<Path>,
762 mask: &[bool],
763 width: usize,
764 height: usize,
765) -> Result<(), IoError> {
766 let path = path.as_ref();
767 if !matches!(detect_format(path)?, Format::Png) {
768 return Err(IoError::Unsupported("masks are saved as .png".to_owned()));
769 }
770 if width == 0 || height == 0 {
771 return Err(IoError::InvalidSensorSize);
772 }
773 if mask.len() != width * height {
774 return Err(IoError::Format(format!(
775 "mask has {} pixels, expected {} for a {width}x{height} grid",
776 mask.len(),
777 width * height
778 )));
779 }
780 let pixels: Vec<u8> = mask.iter().map(|&keep| if keep { 255 } else { 0 }).collect();
781 let writer = std::io::BufWriter::new(std::fs::File::create(path)?);
782 let mut encoder = png::Encoder::new(writer, width as u32, height as u32);
783 encoder.set_color(png::ColorType::Grayscale);
784 encoder.set_depth(png::BitDepth::Eight);
785 encoder
786 .write_header()
787 .and_then(|mut writer| writer.write_image_data(&pixels))
788 .map_err(png_encoding_error)
789}
790
791pub fn read_mask(path: impl AsRef<Path>) -> Result<(Vec<bool>, usize, usize), IoError> {
796 let png = decode_png(path.as_ref(), "masks are loaded from .png")?;
797 let mut mask = Vec::with_capacity(png.width * png.height);
798 png.for_each_pixel(|colour, opacity| {
799 mask.push(colour.iter().any(|&value| value != 0) && opacity.first() != Some(&0));
802 });
803 Ok((mask, png.width, png.height))
804}
805
806pub fn read_png_frame(path: impl AsRef<Path>) -> Result<EventFrame, IoError> {
812 let png = decode_png(path.as_ref(), "frames are loaded from .png")?;
813 let mut samples = Vec::with_capacity(png.width * png.height);
814 png.for_each_pixel(|colour, _| samples.push(luma(colour)));
815 EventFrame::intensity(EventFrameData::U8(samples), png.width, png.height)
816 .map_err(|error| IoError::Format(error.to_string()))
817}
818
819pub(crate) fn luma(colour: &[u8]) -> u8 {
822 match colour {
823 [grey] => *grey,
824 [r, g, b, ..] => {
825 (0.299 * f32::from(*r) + 0.587 * f32::from(*g) + 0.114 * f32::from(*b)).round() as u8
826 }
827 _ => 0,
828 }
829}
830
831struct DecodedPng {
833 buffer: Vec<u8>,
834 width: usize,
835 height: usize,
836 line_size: usize,
837 samples: usize,
838 alpha: bool,
839}
840
841impl DecodedPng {
842 fn for_each_pixel(&self, mut visit: impl FnMut(&[u8], &[u8])) {
845 for row in self.buffer.chunks_exact(self.line_size).take(self.height) {
846 for pixel in row[..self.width * self.samples].chunks_exact(self.samples) {
847 let (colour, opacity) = pixel.split_at(self.samples - usize::from(self.alpha));
848 visit(colour, opacity);
849 }
850 }
851 }
852}
853
854fn decode_png(path: &Path, unsupported: &str) -> Result<DecodedPng, IoError> {
857 if !matches!(detect_format(path)?, Format::Png) {
858 return Err(IoError::Unsupported(unsupported.to_owned()));
859 }
860 let mut decoder = png::Decoder::new(std::io::BufReader::new(std::fs::File::open(path)?));
861 decoder.set_transformations(png::Transformations::normalize_to_color8());
863 let mut reader = decoder.read_info().map_err(png_decoding_error)?;
864 let mut buffer = vec![0; reader.output_buffer_size()];
865 let info = reader.next_frame(&mut buffer).map_err(png_decoding_error)?;
866 Ok(DecodedPng {
867 width: info.width as usize,
868 height: info.height as usize,
869 line_size: info.line_size,
870 samples: info.color_type.samples(),
871 alpha: matches!(
872 info.color_type,
873 png::ColorType::GrayscaleAlpha | png::ColorType::Rgba
874 ),
875 buffer,
876 })
877}
878
879fn png_decoding_error(error: png::DecodingError) -> IoError {
880 match error {
881 png::DecodingError::IoError(error) => IoError::Io(error),
882 other => IoError::Format(other.to_string()),
883 }
884}
885
886pub fn load_frame(path: impl AsRef<Path>) -> Result<EventFrame, IoError> {
889 let path = path.as_ref();
890 match detect_format(path)? {
891 Format::Npz => npz::read_npz_frame(path),
892 Format::Png => read_png_frame(path),
895 Format::Hdf5 => {
896 #[cfg(feature = "hdf5")]
897 {
898 h5::read_hdf5_frame(path)
899 }
900 #[cfg(not(feature = "hdf5"))]
901 {
902 Err(IoError::Unsupported(
903 "HDF5 support is not built in; rebuild with --features hdf5".to_owned(),
904 ))
905 }
906 }
907 other => Err(IoError::Unsupported(format!(
908 "loading an event frame from {other:?} is not supported"
909 ))),
910 }
911}
912
913#[derive(Debug)]
914pub enum IoError {
915 Io(io::Error),
916 Parse { line: usize, message: String },
917 Format(String),
918 InvalidSensorSize,
919 Unsupported(String),
920}
921
922impl fmt::Display for IoError {
923 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
924 match self {
925 Self::Io(error) => error.fmt(formatter),
926 Self::Parse { line, message } => write!(formatter, "line {line}: {message}"),
927 Self::Format(message) => formatter.write_str(message),
928 Self::InvalidSensorSize => {
929 formatter.write_str("sensor width and height must be positive")
930 }
931 Self::Unsupported(message) => formatter.write_str(message),
932 }
933 }
934}
935
936impl Error for IoError {
937 fn source(&self) -> Option<&(dyn Error + 'static)> {
938 match self {
939 Self::Io(error) => Some(error),
940 _ => None,
941 }
942 }
943}
944
945impl From<io::Error> for IoError {
946 fn from(error: io::Error) -> Self {
947 Self::Io(error)
948 }
949}
950
951#[cfg(test)]
952mod tests {
953 use super::{
954 load, open, open_sink, read_mask, save_stream, supports_event_append, write_mask, Format,
955 IoError, LoadOptions, MemorySliceSource, SaveOptions, SliceSource,
956 };
957 use crate::{EventStream, EventStreamBuilder};
958
959 fn round_trip_stream() -> EventStream {
966 let mut builder = EventStreamBuilder::new(346, 260, 0.001);
967 for index in 0..500i64 {
968 let group = index / 20;
969 let (x, y, polarity) = if group % 2 == 0 {
970 ((index % 20) as u16, group as u16, true)
971 } else {
972 (((index * 7) % 300) as u16, ((index * 13) % 200) as u16, index % 3 == 0)
973 };
974 builder.push(x, y, group * 50_000, polarity);
975 }
976 builder.build()
977 }
978
979 fn assert_same(written: &EventStream, read: &EventStream, what: &str) {
980 assert_eq!(written.len(), read.len(), "{what}: event count");
981 assert_eq!(written.xs(), read.xs(), "{what}: x");
982 assert_eq!(written.ys(), read.ys(), "{what}: y");
983 assert_eq!(written.ts(), read.ts(), "{what}: t");
984 assert_eq!(written.ps(), read.ps(), "{what}: p");
985 }
986
987 const ROUND_TRIP_FORMATS: &[(&str, Option<&str>)] = &[
990 ("npz", None),
991 ("txt", None),
992 ("aedat", None),
993 ("aedat4", None),
994 ("dat", None),
995 ("raw", Some("evt2")),
996 ("raw", Some("evt3")),
997 ("bag", None),
998 #[cfg(feature = "hdf5")]
999 ("h5", None),
1000 ];
1001
1002 fn scratch(name: &str, extension: &str) -> std::path::PathBuf {
1003 std::env::temp_dir().join(format!(
1004 "eventcv_sink_{}_{}_{name}.{extension}",
1005 std::process::id(),
1006 std::time::SystemTime::now()
1007 .duration_since(std::time::UNIX_EPOCH)
1008 .map(|since| since.as_nanos())
1009 .unwrap_or(0),
1010 ))
1011 }
1012
1013 #[test]
1014 fn every_format_round_trips_through_save_and_load() {
1015 let stream = round_trip_stream();
1016 for (extension, format) in ROUND_TRIP_FORMATS {
1017 let path = scratch(format.unwrap_or("plain"), extension);
1018 let options = SaveOptions {
1019 format: format.map(str::to_owned),
1020 ..SaveOptions::default()
1021 };
1022 save_stream(&path, &stream, &options).expect(extension);
1023 let load_options = LoadOptions {
1025 sensor_size: Some(stream.sensor_size()),
1026 ..LoadOptions::default()
1027 };
1028 let read = load(&path, load_options).expect(extension);
1029 assert_same(&stream, &read, format.unwrap_or(extension));
1030 std::fs::remove_file(&path).ok();
1031 }
1032 }
1033
1034 #[test]
1035 fn appending_windows_matches_saving_the_whole_stream() {
1036 let stream = round_trip_stream();
1037 for (extension, format) in ROUND_TRIP_FORMATS {
1038 let path = scratch("append", extension);
1039 let options = SaveOptions {
1040 format: format.map(str::to_owned),
1041 ..SaveOptions::default()
1042 };
1043 let mut sink = open_sink(&path, &options).expect(extension);
1044 for window in [0..100, 100..340, 340..stream.len()] {
1046 sink.append(&slice(&stream, window)).expect(extension);
1047 }
1048 assert_eq!(sink.n_events(), stream.len());
1049 sink.finish().expect(extension);
1050 let read = load(
1051 &path,
1052 LoadOptions {
1053 sensor_size: Some(stream.sensor_size()),
1054 ..LoadOptions::default()
1055 },
1056 )
1057 .expect(extension);
1058 assert_same(&stream, &read, format.unwrap_or(extension));
1059 std::fs::remove_file(&path).ok();
1060 }
1061 }
1062
1063 fn slice(stream: &EventStream, range: std::ops::Range<usize>) -> EventStream {
1064 let (width, height) = stream.sensor_size();
1065 let mut builder = EventStreamBuilder::new(width, height, stream.timestamp_scale_ms());
1066 let (xs, ys, ts, ps) = (stream.xs(), stream.ys(), stream.ts(), stream.ps());
1067 for index in range {
1068 builder.push(xs[index], ys[index], ts[index], ps[index]);
1069 }
1070 builder.build()
1071 }
1072
1073 #[test]
1074 fn every_event_format_can_be_appended_but_png_cannot() {
1075 for (extension, _) in ROUND_TRIP_FORMATS {
1076 assert!(
1077 supports_event_append(format!("recording.{extension}")),
1078 ".{extension} should be appendable"
1079 );
1080 }
1081 assert!(supports_event_append("export.zip")); assert!(!supports_event_append("frame.png"));
1083 assert!(!supports_event_append("clip.mp4"));
1084 }
1085
1086 #[test]
1087 fn raw_encoding_follows_the_requested_format() {
1088 let evt3 = SaveOptions {
1089 format: Some("evt3".to_owned()),
1090 ..SaveOptions::default()
1091 };
1092 assert_eq!(evt3.evt_version(), super::EvtVersion::Evt3);
1093 assert_eq!(SaveOptions::default().evt_version(), super::EvtVersion::Evt2);
1096 assert!(matches!(
1097 super::requested_format(std::path::Path::new("out.raw"), &evt3),
1098 Ok(Format::PropheseeRaw)
1099 ));
1100 }
1101
1102 #[test]
1106 fn the_spilling_sinks_leave_no_scratch_files() {
1107 let stream = round_trip_stream();
1108 for extension in ["npz", "bag"] {
1109 let path = scratch("spill", extension);
1110 let siblings = |suffix: &str| -> Vec<std::path::PathBuf> {
1111 let stem = path.file_stem().and_then(|stem| stem.to_str()).unwrap_or("");
1112 std::fs::read_dir(path.parent().expect("a parent"))
1113 .into_iter()
1114 .flatten()
1115 .flatten()
1116 .map(|entry| entry.path())
1117 .filter(|found| {
1118 found
1119 .file_name()
1120 .and_then(|name| name.to_str())
1121 .is_some_and(|name| name.starts_with(stem) && name.ends_with(suffix))
1122 })
1123 .collect()
1124 };
1125
1126 let mut sink = open_sink(&path, &SaveOptions::default()).expect(extension);
1128 sink.append(&stream).expect(extension);
1129 drop(sink);
1130 assert!(
1131 siblings(".part").is_empty(),
1132 "{extension}: an abandoned sink left its scratch file behind"
1133 );
1134
1135 let mut sink = open_sink(&path, &SaveOptions::default()).expect(extension);
1137 sink.append(&stream).expect(extension);
1138 sink.finish().expect(extension);
1139 assert!(path.exists(), "{extension}: no archive was written");
1140 assert!(
1141 siblings(".part").is_empty(),
1142 "{extension}: a finished sink left its scratch file behind"
1143 );
1144 std::fs::remove_file(&path).ok();
1145 }
1146 }
1147
1148 #[test]
1149 fn out_of_order_events_are_refused_by_the_raw_writer() {
1150 let mut builder = EventStreamBuilder::new(64, 64, 0.001);
1151 builder.push(1, 1, 500, true);
1152 builder.push(2, 2, 100, false);
1153 let path = scratch("unsorted", "raw");
1154 let error = save_stream(&path, &builder.build(), &SaveOptions::default()).unwrap_err();
1155 std::fs::remove_file(&path).ok();
1156 match error {
1157 IoError::Unsupported(message) => assert!(message.contains("time order"), "{message}"),
1158 other => panic!("expected an ordering error, got {other:?}"),
1159 }
1160 }
1161
1162 #[test]
1163 fn unknown_extension_is_unsupported() {
1164 let error = load("recording.mp4", LoadOptions::default()).unwrap_err();
1165 assert!(matches!(error, IoError::Unsupported(_)));
1166 }
1167
1168 #[test]
1169 fn hdf5_extension_dispatches_per_feature() {
1170 let error = load("recording.h5", LoadOptions::default()).unwrap_err();
1173 #[cfg(feature = "hdf5")]
1174 assert!(matches!(error, IoError::Io(_)));
1175 #[cfg(not(feature = "hdf5"))]
1176 match error {
1177 IoError::Unsupported(message) => assert!(message.contains("HDF5")),
1178 other => panic!("expected unsupported error, got {other:?}"),
1179 }
1180 }
1181
1182 #[test]
1183 fn text_missing_file_is_reported() {
1184 let error = load("events.txt", LoadOptions::default()).unwrap_err();
1186 assert!(matches!(error, IoError::Io(_)));
1187 }
1188
1189 #[test]
1190 fn aedat_dispatches_to_the_reader() {
1191 let error = load("recording.aedat", LoadOptions::default()).unwrap_err();
1193 assert!(matches!(error, IoError::Io(_)));
1194 }
1195
1196 #[test]
1197 fn prophesee_dat_dispatches_to_the_reader() {
1198 let error = load("recording.dat", LoadOptions::default()).unwrap_err();
1199 assert!(matches!(error, IoError::Io(_)));
1200 }
1201
1202 #[test]
1203 fn aedat4_and_raw_dispatch_to_their_readers() {
1204 for path in ["recording.aedat4", "recording.raw"] {
1206 assert!(
1207 matches!(load(path, LoadOptions::default()), Err(IoError::Io(_))),
1208 "{path} should dispatch into its reader"
1209 );
1210 }
1211 }
1212
1213 fn sample_source() -> MemorySliceSource {
1214 let mut builder = EventStreamBuilder::new(4, 4, 0.001);
1215 builder.push(0, 0, 0, true);
1216 builder.push(1, 1, 10, false);
1217 builder.push(2, 2, 20, true);
1218 builder.push(0, 1, 30, false);
1219 MemorySliceSource::new(builder.build())
1220 }
1221
1222 #[test]
1223 fn memory_source_reports_span_and_count() {
1224 let source = sample_source();
1225 assert_eq!(source.n_events(), 4);
1226 assert_eq!(source.time_span(), (0, 30));
1227 }
1228
1229 #[test]
1230 fn memory_source_slices_by_time_and_index() {
1231 let source = sample_source();
1232
1233 assert_eq!(source.slice_time(10, 30).unwrap().ts(), &[10, 20]);
1235 assert_eq!(source.slice_index(1, 3).unwrap().ts(), &[10, 20]);
1236 assert_eq!(source.slice_index(2, 100).unwrap().len(), 2); assert!(source.slice_time(100, 200).unwrap().is_empty());
1238 }
1239
1240 #[test]
1241 fn open_rejects_unknown_extension() {
1242 match open("recording.mp4", LoadOptions::default()) {
1244 Err(IoError::Unsupported(_)) => {}
1245 Err(other) => panic!("expected unsupported error, got {other:?}"),
1246 Ok(_) => panic!("expected an error for an unknown extension"),
1247 }
1248 }
1249
1250 #[test]
1251 fn mask_png_round_trips_and_validates() {
1252 let path = std::env::temp_dir().join(format!("eventcv_mask_{}.png", std::process::id()));
1253 let mask = crate::mask::ellipse(16, 12, 8.0, 6.0, 5.0, 4.0);
1254
1255 write_mask(&path, &mask, 16, 12).unwrap();
1256 let (loaded, width, height) = read_mask(&path).unwrap();
1257 assert_eq!((width, height), (16, 12));
1258 assert_eq!(loaded, mask);
1259 std::fs::remove_file(&path).ok();
1260
1261 assert!(matches!(
1263 write_mask(&path, &mask, 16, 11),
1264 Err(IoError::Format(_))
1265 ));
1266 assert!(matches!(
1267 write_mask("mask.npz", &mask, 16, 12),
1268 Err(IoError::Unsupported(_))
1269 ));
1270 }
1271}