Skip to main content

eventcv_core/
lib.rs

1//! # eventcv-core
2//!
3//! The Rust core of **EventCV** — "OpenCV for event-based vision".
4//!
5//! Everything is built around [`EventStream`], a struct-of-arrays container of events
6//! (`xs`, `ys`, `ts`, `ps` columns plus sensor size and timestamp scale). Streams are
7//! constructed only through [`EventStreamBuilder`], which drops out-of-bounds events.
8//!
9//! The crate is organised into focused modules:
10//!
11//! - [`io`] — readers/writers for `.npz`, `.txt`, `.bag`, `.h5`, `.aedat`, `.dat`, plus the
12//!   [`io::load`] extension dispatcher and lazy [`io::SliceSource`] indexing for large files.
13//! - [`representation`] — event → dense tensor ([`representation::Representation`], e.g. voxel
14//!   grids and time surfaces).
15//! - [`transform`] — chainable event-domain geometry/temporal/polarity ops on streams.
16//! - [`camera`] — intrinsics and `undistort`.
17//! - [`features`], [`flow`], [`cluster`] — corner detection, optical flow, connected components.
18//! - [`filter`], [`image`], [`viz`] — hot-pixel filtering, frame-domain resize, colormapped export.
19//!
20//! The `hdf5` feature (off by default to keep `cargo test` fast) enables the `.h5`/`.hdf5`
21//! reader by building libhdf5 from source.
22
23use ndarray::Array2;
24
25pub mod camera;
26pub mod cluster;
27pub mod features;
28pub mod filter;
29pub mod flow;
30pub mod image;
31pub mod io;
32pub mod representation;
33pub mod transform;
34pub mod viz;
35
36const COLUMN_COUNT: usize = 4;
37
38/// A stream of events stored column-wise (struct-of-arrays). Columns compress and
39/// transform far better than interleaved rows, and timestamps use `i64` (µs) so
40/// real multi-second recordings fit. See `TASKS.md` §3.
41#[derive(Clone, Debug)]
42pub struct EventStream {
43    xs: Vec<u16>,
44    ys: Vec<u16>,
45    ts: Vec<i64>,
46    ps: Vec<bool>,
47    width: usize,
48    height: usize,
49    timestamp_scale_ms: f64,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct Event {
54    pub x: usize,
55    pub y: usize,
56    pub timestamp: u64,
57    pub polarity: bool,
58}
59
60impl EventStream {
61    pub fn len(&self) -> usize {
62        self.xs.len()
63    }
64
65    pub fn is_empty(&self) -> bool {
66        self.xs.is_empty()
67    }
68
69    pub fn sensor_size(&self) -> (usize, usize) {
70        (self.width, self.height)
71    }
72
73    pub fn timestamp_scale_ms(&self) -> f64 {
74        self.timestamp_scale_ms
75    }
76
77    pub fn xs(&self) -> &[u16] {
78        &self.xs
79    }
80
81    pub fn ys(&self) -> &[u16] {
82        &self.ys
83    }
84
85    pub fn ts(&self) -> &[i64] {
86        &self.ts
87    }
88
89    pub fn ps(&self) -> &[bool] {
90        &self.ps
91    }
92
93    pub fn iter(&self) -> impl Iterator<Item = Event> + '_ {
94        (0..self.len()).map(move |index| Event {
95            x: self.xs[index] as usize,
96            y: self.ys[index] as usize,
97            timestamp: self.ts[index] as u64,
98            polarity: self.ps[index],
99        })
100    }
101
102    /// Materialises an owned `(N, 4)` array of `[x, y, t, p]` rows for numpy interop.
103    pub fn to_array2(&self) -> Array2<u64> {
104        let mut values = Vec::with_capacity(self.len() * COLUMN_COUNT);
105        for index in 0..self.len() {
106            values.push(u64::from(self.xs[index]));
107            values.push(u64::from(self.ys[index]));
108            values.push(self.ts[index] as u64);
109            values.push(u64::from(self.ps[index]));
110        }
111        Array2::from_shape_vec((self.len(), COLUMN_COUNT), values)
112            .expect("columns share a length by construction")
113    }
114
115    /// Test-only constructor from an `(N, 4)` `[x, y, t, p]` array. Preserves every
116    /// row verbatim (no bounds filtering) so fixtures can exercise error paths.
117    #[cfg(test)]
118    pub(crate) fn from_array2(
119        events: Array2<u64>,
120        width: usize,
121        height: usize,
122        timestamp_scale_ms: f64,
123    ) -> Self {
124        Self {
125            xs: events.column(0).iter().map(|&value| value as u16).collect(),
126            ys: events.column(1).iter().map(|&value| value as u16).collect(),
127            ts: events.column(2).iter().map(|&value| value as i64).collect(),
128            ps: events.column(3).iter().map(|&value| value != 0).collect(),
129            width,
130            height,
131            timestamp_scale_ms,
132        }
133    }
134}
135
136/// Builds an [`EventStream`] one event at a time, dropping events outside the
137/// sensor. The single construction path shared by readers and (future) transforms.
138#[derive(Clone, Debug)]
139pub struct EventStreamBuilder {
140    xs: Vec<u16>,
141    ys: Vec<u16>,
142    ts: Vec<i64>,
143    ps: Vec<bool>,
144    width: usize,
145    height: usize,
146    timestamp_scale_ms: f64,
147}
148
149impl EventStreamBuilder {
150    pub fn new(width: usize, height: usize, timestamp_scale_ms: f64) -> Self {
151        Self::with_capacity(width, height, timestamp_scale_ms, 0)
152    }
153
154    pub fn with_capacity(
155        width: usize,
156        height: usize,
157        timestamp_scale_ms: f64,
158        capacity: usize,
159    ) -> Self {
160        Self {
161            xs: Vec::with_capacity(capacity),
162            ys: Vec::with_capacity(capacity),
163            ts: Vec::with_capacity(capacity),
164            ps: Vec::with_capacity(capacity),
165            width,
166            height,
167            timestamp_scale_ms,
168        }
169    }
170
171    /// Appends an event, returning `false` if it lies outside the sensor and was
172    /// dropped. Callers that treat out-of-bounds events as errors inspect the result.
173    pub fn push(&mut self, x: u16, y: u16, timestamp: i64, polarity: bool) -> bool {
174        if usize::from(x) >= self.width || usize::from(y) >= self.height {
175            return false;
176        }
177        self.xs.push(x);
178        self.ys.push(y);
179        self.ts.push(timestamp);
180        self.ps.push(polarity);
181        true
182    }
183
184    pub fn len(&self) -> usize {
185        self.xs.len()
186    }
187
188    pub fn is_empty(&self) -> bool {
189        self.xs.is_empty()
190    }
191
192    pub fn build(self) -> EventStream {
193        EventStream {
194            xs: self.xs,
195            ys: self.ys,
196            ts: self.ts,
197            ps: self.ps,
198            width: self.width,
199            height: self.height,
200            timestamp_scale_ms: self.timestamp_scale_ms,
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use std::path::PathBuf;
208
209    use crate::io::{load, LoadOptions};
210
211    use super::{EventStream, EventStreamBuilder};
212
213    #[test]
214    fn loads_n_imagenet_events() {
215        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data/test/example.npz");
216        let stream = load(path, LoadOptions::default()).unwrap();
217        let events = stream.to_array2();
218
219        assert!(!stream.is_empty());
220        assert_eq!(stream.sensor_size(), (640, 480));
221        assert_eq!(events.dim(), (stream.len(), 4));
222        assert!(events.column(0).iter().all(|&x| x < 640));
223        assert!(events.column(1).iter().all(|&y| y < 480));
224        assert!(events.column(3).iter().all(|&polarity| polarity <= 1));
225    }
226
227    #[test]
228    fn builder_drops_out_of_bounds_events_and_keeps_columns_aligned() {
229        let mut builder = EventStreamBuilder::new(4, 3, 0.001);
230
231        assert!(builder.push(1, 2, 10, true));
232        assert!(!builder.push(4, 0, 20, false)); // x == width -> dropped
233        assert!(!builder.push(0, 3, 30, true)); // y == height -> dropped
234        assert!(builder.push(3, 0, 40, false));
235
236        let stream = builder.build();
237        assert_eq!(stream.len(), 2);
238        assert_eq!(stream.xs(), &[1, 3]);
239        assert_eq!(stream.ys(), &[2, 0]);
240        assert_eq!(stream.ts(), &[10, 40]);
241        assert_eq!(stream.ps(), &[true, false]);
242        assert_eq!(stream.sensor_size(), (4, 3));
243    }
244
245    #[test]
246    fn to_array2_round_trips_columns_in_xytp_order() {
247        let stream =
248            EventStream::from_array2(ndarray::array![[1, 2, 100, 1], [3, 0, 250, 0]], 4, 3, 0.001);
249        let events = stream.to_array2();
250
251        assert_eq!(events.dim(), (2, 4));
252        assert_eq!(events.row(0).to_vec(), vec![1, 2, 100, 1]);
253        assert_eq!(events.row(1).to_vec(), vec![3, 0, 250, 0]);
254    }
255}