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