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 accel;
31pub mod analytics;
32pub mod augment;
33pub mod bias;
34pub mod camera;
35pub mod cluster;
36pub mod cmax;
37#[cfg(feature = "camera")]
38pub mod device;
39pub mod feast;
40pub mod features;
41pub mod filter;
42pub mod flow;
43pub mod image;
44pub mod interp;
45pub mod io;
46pub mod mask;
47/// ONNX inference. Requires the `onnx` feature (on in the published wheels).
48#[cfg(feature = "onnx")]
49pub mod model;
50pub mod net;
51#[cfg(feature = "ros2")]
52pub mod ros2;
53
54pub mod representation;
55pub mod simulate;
56pub mod track;
57pub mod transform;
58pub mod video;
59pub mod viz;
60
61const COLUMN_COUNT: usize = 4;
62
63/// A stream of events stored column-wise (struct-of-arrays). Columns compress and
64/// transform far better than interleaved rows, and timestamps use `i64` (µs) so
65/// real multi-second recordings fit. See `TASKS.md` §3.
66///
67/// The columns are shared, not owned: a transform that rewrites one column hands the other three
68/// on untouched, and `clone` is four refcount bumps rather than a copy of the whole recording.
69/// `Arc<Vec<T>>` rather than `Arc<[T]>` because the latter cannot reuse a `Vec`'s allocation (the
70/// refcount header sits in the same block), so every `EventStreamBuilder::build` — every reader
71/// slice, every subsetting transform — would copy all four columns; and because `Arc::make_mut`
72/// gives copy-on-write for free on a `Vec` and is not available on an unsized `[T]`.
73#[derive(Clone, Debug)]
74pub struct EventStream {
75 xs: Vec<u16>,
76 ys: Vec<u16>,
77 ts: Vec<i64>,
78 ps: Vec<bool>,
79 width: usize,
80 height: usize,
81 timestamp_scale_ms: f64,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct Event {
86 pub x: usize,
87 pub y: usize,
88 pub timestamp: u64,
89 pub polarity: bool,
90}
91
92impl EventStream {
93 pub fn len(&self) -> usize {
94 self.xs.len()
95 }
96
97 pub fn is_empty(&self) -> bool {
98 self.xs.is_empty()
99 }
100
101 pub fn sensor_size(&self) -> (usize, usize) {
102 (self.width, self.height)
103 }
104
105 pub fn timestamp_scale_ms(&self) -> f64 {
106 self.timestamp_scale_ms
107 }
108
109 pub fn xs(&self) -> &[u16] {
110 &self.xs
111 }
112
113 pub fn ys(&self) -> &[u16] {
114 &self.ys
115 }
116
117 pub fn ts(&self) -> &[i64] {
118 &self.ts
119 }
120
121 pub fn ps(&self) -> &[bool] {
122 &self.ps
123 }
124
125 pub fn iter(&self) -> impl Iterator<Item = Event> + '_ {
126 // Bind the columns once rather than re-projecting them per event: every representation
127 // consumes the stream through here.
128 let (xs, ys, ts, ps) = (self.xs(), self.ys(), self.ts(), self.ps());
129 (0..self.len()).map(move |index| Event {
130 x: xs[index] as usize,
131 y: ys[index] as usize,
132 timestamp: ts[index] as u64,
133 polarity: ps[index],
134 })
135 }
136
137 /// Materialises an owned `(N, 4)` array of `[x, y, t, p]` rows for numpy interop.
138 pub fn to_array2(&self) -> Array2<u64> {
139 let mut values = Vec::with_capacity(self.len() * COLUMN_COUNT);
140 for index in 0..self.len() {
141 values.push(u64::from(self.xs[index]));
142 values.push(u64::from(self.ys[index]));
143 values.push(self.ts[index] as u64);
144 values.push(u64::from(self.ps[index]));
145 }
146 Array2::from_shape_vec((self.len(), COLUMN_COUNT), values)
147 .expect("columns share a length by construction")
148 }
149
150 /// Test-only constructor from an `(N, 4)` `[x, y, t, p]` array. Preserves every
151 /// row verbatim (no bounds filtering) so fixtures can exercise error paths.
152 #[cfg(test)]
153 pub(crate) fn from_array2(
154 events: Array2<u64>,
155 width: usize,
156 height: usize,
157 timestamp_scale_ms: f64,
158 ) -> Self {
159 Self {
160 xs: events.column(0).iter().map(|&value| value as u16).collect(),
161 ys: events.column(1).iter().map(|&value| value as u16).collect(),
162 ts: events.column(2).iter().map(|&value| value as i64).collect(),
163 ps: events.column(3).iter().map(|&value| value != 0).collect(),
164 width,
165 height,
166 timestamp_scale_ms,
167 }
168 }
169}
170
171/// Builds an [`EventStream`] one event at a time, dropping events outside the
172/// sensor. The single construction path shared by readers and (future) transforms.
173#[derive(Clone, Debug)]
174pub struct EventStreamBuilder {
175 xs: Vec<u16>,
176 ys: Vec<u16>,
177 ts: Vec<i64>,
178 ps: Vec<bool>,
179 width: usize,
180 height: usize,
181 timestamp_scale_ms: f64,
182}
183
184impl EventStreamBuilder {
185 pub fn new(width: usize, height: usize, timestamp_scale_ms: f64) -> Self {
186 Self::with_capacity(width, height, timestamp_scale_ms, 0)
187 }
188
189 pub fn with_capacity(
190 width: usize,
191 height: usize,
192 timestamp_scale_ms: f64,
193 capacity: usize,
194 ) -> Self {
195 Self {
196 xs: Vec::with_capacity(capacity),
197 ys: Vec::with_capacity(capacity),
198 ts: Vec::with_capacity(capacity),
199 ps: Vec::with_capacity(capacity),
200 width,
201 height,
202 timestamp_scale_ms,
203 }
204 }
205
206 /// Appends an event, returning `false` if it lies outside the sensor and was
207 /// dropped. Callers that treat out-of-bounds events as errors inspect the result.
208 pub fn push(&mut self, x: u16, y: u16, timestamp: i64, polarity: bool) -> bool {
209 if usize::from(x) >= self.width || usize::from(y) >= self.height {
210 return false;
211 }
212 self.xs.push(x);
213 self.ys.push(y);
214 self.ts.push(timestamp);
215 self.ps.push(polarity);
216 true
217 }
218
219 /// Appends an event already known to lie on this builder's sensor, skipping the test
220 /// [`push`](Self::push) makes. For the callers that have just made that test themselves —
221 /// `EventStream::remap`, which has to range-check in `i64` before it can cast to `u16` — and
222 /// would otherwise pay for it twice on every surviving event.
223 ///
224 /// The four pushes are spelled out again rather than shared with [`push`](Self::push): having
225 /// `push` delegate here cost its callers about 25% on a per-event loop (measured on
226 /// `decimate`), `#[inline]` included, and `push` is on the hot path of every reader. Inlined
227 /// here so that `remap`'s call site pays nothing for the split.
228 #[inline]
229 pub(crate) fn push_in_bounds(&mut self, x: u16, y: u16, timestamp: i64, polarity: bool) {
230 self.xs.push(x);
231 self.ys.push(y);
232 self.ts.push(timestamp);
233 self.ps.push(polarity);
234 }
235
236 /// Appends every event of `stream`, dropping any that fall outside this builder's sensor.
237 ///
238 /// The bulk counterpart of [`push`](Self::push), for the callers that join streams rather than
239 /// generate them — concatenation, and the streaming writers that hand on a window at a time.
240 /// When every event fits (the case for anything produced by a reader or the simulator, which
241 /// cannot emit a coordinate its own sensor does not have) this is four `extend_from_slice`
242 /// calls instead of a bounds check and four pushes per event; the scan that establishes that is
243 /// two comparisons per event and vectorises.
244 pub fn extend_from_stream(&mut self, stream: &EventStream) {
245 let fits = stream
246 .xs()
247 .iter()
248 .zip(stream.ys())
249 .all(|(&x, &y)| usize::from(x) < self.width && usize::from(y) < self.height);
250 if fits {
251 self.extend_from_columns(stream.xs(), stream.ys(), stream.ts(), stream.ps());
252 return;
253 }
254 for index in 0..stream.len() {
255 self.push(
256 stream.xs()[index],
257 stream.ys()[index],
258 stream.ts()[index],
259 stream.ps()[index],
260 );
261 }
262 }
263
264 /// Appends events whose coordinates are already known to lie on this builder's sensor —
265 /// four `extend_from_slice` calls and no per-event check. The four slices must share a length.
266 /// Callers that cannot make that guarantee want [`push`](Self::push) or
267 /// [`extend_from_stream`](Self::extend_from_stream) instead.
268 pub(crate) fn extend_from_columns(&mut self, xs: &[u16], ys: &[u16], ts: &[i64], ps: &[bool]) {
269 self.xs.extend_from_slice(xs);
270 self.ys.extend_from_slice(ys);
271 self.ts.extend_from_slice(ts);
272 self.ps.extend_from_slice(ps);
273 }
274
275 /// Takes ownership of columns a reader has already filled, rather than copying them in.
276 ///
277 /// The counterpart to [`extend_from_columns`](Self::extend_from_columns) for a reader that
278 /// decodes straight into its final buffers: AEDAT 4 knows every packet's output range before
279 /// it decompresses anything, so its workers fill disjoint slices of these vectors and there is
280 /// nothing left to append. The four must share a length and every coordinate must already lie
281 /// on the sensor — the caller has made both guarantees, and re-checking here would mean
282 /// walking every event again to learn nothing.
283 pub(crate) fn from_columns(
284 width: usize,
285 height: usize,
286 timestamp_scale_ms: f64,
287 xs: Vec<u16>,
288 ys: Vec<u16>,
289 ts: Vec<i64>,
290 ps: Vec<bool>,
291 ) -> Self {
292 debug_assert!(
293 xs.len() == ys.len() && xs.len() == ts.len() && xs.len() == ps.len(),
294 "columns must share a length"
295 );
296 Self {
297 xs,
298 ys,
299 ts,
300 ps,
301 width,
302 height,
303 timestamp_scale_ms,
304 }
305 }
306
307 /// Reserves room for `additional` more events across every column.
308 pub fn reserve(&mut self, additional: usize) {
309 self.xs.reserve(additional);
310 self.ys.reserve(additional);
311 self.ts.reserve(additional);
312 self.ps.reserve(additional);
313 }
314
315 pub fn len(&self) -> usize {
316 self.xs.len()
317 }
318
319 pub fn is_empty(&self) -> bool {
320 self.xs.is_empty()
321 }
322
323 pub fn build(self) -> EventStream {
324 EventStream {
325 xs: self.xs,
326 ys: self.ys,
327 ts: self.ts,
328 ps: self.ps,
329 width: self.width,
330 height: self.height,
331 timestamp_scale_ms: self.timestamp_scale_ms,
332 }
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use std::path::PathBuf;
339
340 use crate::io::{load, LoadOptions};
341
342 use super::{EventStream, EventStreamBuilder};
343
344 #[test]
345 fn loads_n_imagenet_events() {
346 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data/test/example.npz");
347 let stream = load(path, LoadOptions::default()).unwrap();
348 let events = stream.to_array2();
349
350 assert!(!stream.is_empty());
351 assert_eq!(stream.sensor_size(), (640, 480));
352 assert_eq!(events.dim(), (stream.len(), 4));
353 assert!(events.column(0).iter().all(|&x| x < 640));
354 assert!(events.column(1).iter().all(|&y| y < 480));
355 assert!(events.column(3).iter().all(|&polarity| polarity <= 1));
356 }
357
358 #[test]
359 fn builder_drops_out_of_bounds_events_and_keeps_columns_aligned() {
360 let mut builder = EventStreamBuilder::new(4, 3, 0.001);
361
362 assert!(builder.push(1, 2, 10, true));
363 assert!(!builder.push(4, 0, 20, false)); // x == width -> dropped
364 assert!(!builder.push(0, 3, 30, true)); // y == height -> dropped
365 assert!(builder.push(3, 0, 40, false));
366
367 let stream = builder.build();
368 assert_eq!(stream.len(), 2);
369 assert_eq!(stream.xs(), &[1, 3]);
370 assert_eq!(stream.ys(), &[2, 0]);
371 assert_eq!(stream.ts(), &[10, 40]);
372 assert_eq!(stream.ps(), &[true, false]);
373 assert_eq!(stream.sensor_size(), (4, 3));
374 }
375
376 #[test]
377 fn to_array2_round_trips_columns_in_xytp_order() {
378 let stream =
379 EventStream::from_array2(ndarray::array![[1, 2, 100, 1], [3, 0, 250, 0]], 4, 3, 0.001);
380 let events = stream.to_array2();
381
382 assert_eq!(events.dim(), (2, 4));
383 assert_eq!(events.row(0).to_vec(), vec![1, 2, 100, 1]);
384 assert_eq!(events.row(1).to_vec(), vec![3, 0, 250, 0]);
385 }
386}