Skip to main content

scirs2_vision/event_camera/
conversion.rs

1//! Event-to-frame conversion methods.
2//!
3//! Converts asynchronous DVS events into dense image representations
4//! suitable for downstream vision algorithms. Supported representations:
5//!
6//! - **Histogram**: simple event count per pixel
7//! - **Polarity histogram**: separate ON/OFF channels
8//! - **Time surface**: most recent timestamp per pixel (normalized)
9//! - **Exponential decay**: temporal weighting with `exp(-dt/tau)`
10//! - **Voxel grid**: time discretized into B bins
11
12use scirs2_core::ndarray::{Array2, Array3};
13
14use crate::error::{Result, VisionError};
15
16use super::types::{Event, EventFrame, EventProcessingConfig, EventSlice, Polarity};
17
18/// Method used for event-to-frame conversion.
19#[non_exhaustive]
20#[derive(Clone, Debug)]
21pub enum FrameMethod {
22    /// Simple histogram: count events per pixel (polarity-agnostic).
23    Histogram,
24    /// Polarity histogram: accumulate polarity signs (+1/-1) per pixel.
25    PolarityHistogram,
26    /// Time surface: store the most recent timestamp per pixel, normalized to `[0, 1]`.
27    TimeSurface,
28    /// Exponential decay: each event contributes `exp(-(t_end - t_event) / tau)`.
29    ExponentialDecay,
30    /// Voxel grid: discretize time into `n_bins` bins, producing a 3D tensor `[B, H, W]`.
31    VoxelGrid {
32        /// Number of temporal bins.
33        n_bins: usize,
34    },
35}
36
37/// Converts an event slice to a single-channel frame using the given method.
38///
39/// For [`FrameMethod::VoxelGrid`] this returns the sum over bins (use
40/// [`events_to_voxel_grid`] for the full 3D tensor).
41pub fn events_to_frame(
42    events: &EventSlice,
43    method: &FrameMethod,
44    config: &EventProcessingConfig,
45) -> Result<EventFrame> {
46    let h = config.height as usize;
47    let w = config.width as usize;
48    let (t_start, t_end) = events.time_range();
49
50    let data = match method {
51        FrameMethod::Histogram => {
52            let mut frame = Array2::<f64>::zeros((h, w));
53            for e in events.events() {
54                frame[[e.y as usize, e.x as usize]] += 1.0;
55            }
56            frame
57        }
58        FrameMethod::PolarityHistogram => {
59            let mut frame = Array2::<f64>::zeros((h, w));
60            for e in events.events() {
61                frame[[e.y as usize, e.x as usize]] += e.polarity.sign();
62            }
63            frame
64        }
65        FrameMethod::TimeSurface => {
66            return events_to_time_surface(events, config);
67        }
68        FrameMethod::ExponentialDecay => {
69            let mut frame = Array2::<f64>::zeros((h, w));
70            let tau = config.decay_rate;
71            if tau <= 0.0 {
72                return Err(VisionError::InvalidParameter(
73                    "Decay rate (tau) must be positive".to_string(),
74                ));
75            }
76            for e in events.events() {
77                let dt = t_end - e.timestamp;
78                let weight = (-dt / tau).exp();
79                frame[[e.y as usize, e.x as usize]] += e.polarity.sign() * weight;
80            }
81            frame
82        }
83        FrameMethod::VoxelGrid { n_bins } => {
84            let voxel = events_to_voxel_grid(events, *n_bins, config)?;
85            // Sum over time bins to produce a single 2D frame
86            let mut frame = Array2::<f64>::zeros((h, w));
87            for b in 0..*n_bins {
88                for y in 0..h {
89                    for x in 0..w {
90                        frame[[y, x]] += voxel[[b, y, x]];
91                    }
92                }
93            }
94            frame
95        }
96    };
97
98    Ok(EventFrame {
99        data,
100        t_start,
101        t_end,
102    })
103}
104
105/// Converts events into separate ON and OFF event frames.
106///
107/// Returns `(on_frame, off_frame)` where each frame counts events of the
108/// respective polarity.
109pub fn events_to_polarity_frames(
110    events: &EventSlice,
111    config: &EventProcessingConfig,
112) -> Result<(EventFrame, EventFrame)> {
113    let h = config.height as usize;
114    let w = config.width as usize;
115    let (t_start, t_end) = events.time_range();
116
117    let mut on_frame = Array2::<f64>::zeros((h, w));
118    let mut off_frame = Array2::<f64>::zeros((h, w));
119
120    for e in events.events() {
121        match e.polarity {
122            Polarity::On => {
123                on_frame[[e.y as usize, e.x as usize]] += 1.0;
124            }
125            Polarity::Off => {
126                off_frame[[e.y as usize, e.x as usize]] += 1.0;
127            }
128        }
129    }
130
131    Ok((
132        EventFrame {
133            data: on_frame,
134            t_start,
135            t_end,
136        },
137        EventFrame {
138            data: off_frame,
139            t_start,
140            t_end,
141        },
142    ))
143}
144
145/// Converts events to a 3D voxel grid `[n_bins, height, width]`.
146///
147/// Time is linearly discretized into `n_bins` temporal bins. Each event's
148/// polarity sign is added to its corresponding bin.
149pub fn events_to_voxel_grid(
150    events: &EventSlice,
151    n_bins: usize,
152    config: &EventProcessingConfig,
153) -> Result<Array3<f64>> {
154    if n_bins == 0 {
155        return Err(VisionError::InvalidParameter(
156            "n_bins must be at least 1".to_string(),
157        ));
158    }
159
160    let h = config.height as usize;
161    let w = config.width as usize;
162    let (t_start, t_end) = events.time_range();
163    let duration = t_end - t_start;
164
165    let mut voxel = Array3::<f64>::zeros((n_bins, h, w));
166
167    for e in events.events() {
168        let t_norm = if duration > 0.0 {
169            (e.timestamp - t_start) / duration
170        } else {
171            0.5 // single timestamp: put in middle bin
172        };
173        // Clamp to [0, 1) then scale to bin index
174        let t_clamped = t_norm.clamp(0.0, 1.0 - f64::EPSILON);
175        let bin = (t_clamped * n_bins as f64) as usize;
176        let bin = bin.min(n_bins - 1);
177
178        voxel[[bin, e.y as usize, e.x as usize]] += e.polarity.sign();
179    }
180
181    Ok(voxel)
182}
183
184/// Converts events to a time surface.
185///
186/// Each pixel stores the normalized timestamp of the most recent event at
187/// that location. Normalization maps `[t_start, t_end]` to `[0, 1]`.
188/// Pixels with no events remain at `0.0`.
189pub fn events_to_time_surface(
190    events: &EventSlice,
191    config: &EventProcessingConfig,
192) -> Result<EventFrame> {
193    let h = config.height as usize;
194    let w = config.width as usize;
195    let (t_start, t_end) = events.time_range();
196    let duration = t_end - t_start;
197
198    let mut frame = Array2::<f64>::zeros((h, w));
199
200    for e in events.events() {
201        let t_norm = if duration > 0.0 {
202            (e.timestamp - t_start) / duration
203        } else {
204            1.0
205        };
206        // Since events are sorted, later events overwrite earlier ones
207        frame[[e.y as usize, e.x as usize]] = t_norm;
208    }
209
210    Ok(EventFrame {
211        data: frame,
212        t_start,
213        t_end,
214    })
215}
216
217/// A streaming frame accumulator that processes events incrementally.
218///
219/// Maintains an internal frame and a timestamp map for temporal decay.
220pub struct StreamingFrameAccumulator {
221    frame: Array2<f64>,
222    timestamps: Array2<f64>,
223    config: EventProcessingConfig,
224}
225
226impl StreamingFrameAccumulator {
227    /// Creates a new accumulator with the given configuration.
228    pub fn new(config: EventProcessingConfig) -> Self {
229        let h = config.height as usize;
230        let w = config.width as usize;
231        Self {
232            frame: Array2::<f64>::zeros((h, w)),
233            timestamps: Array2::<f64>::zeros((h, w)),
234            config,
235        }
236    }
237
238    /// Adds a single event to the accumulator.
239    ///
240    /// The event's polarity sign is added to the corresponding pixel.
241    /// The pixel's timestamp is updated.
242    pub fn add_event(&mut self, event: &Event) {
243        let y = event.y as usize;
244        let x = event.x as usize;
245        if y < self.config.height as usize && x < self.config.width as usize {
246            self.frame[[y, x]] += event.polarity.sign();
247            self.timestamps[[y, x]] = event.timestamp;
248        }
249    }
250
251    /// Returns a reference to the current accumulated frame.
252    pub fn get_frame(&self) -> &Array2<f64> {
253        &self.frame
254    }
255
256    /// Applies exponential temporal decay to all pixels.
257    ///
258    /// For each pixel, the value is multiplied by `exp(-(current_time - last_timestamp) / tau)`.
259    pub fn decay(&mut self, current_time: f64) {
260        let tau = self.config.decay_rate;
261        if tau <= 0.0 {
262            return;
263        }
264        let h = self.config.height as usize;
265        let w = self.config.width as usize;
266        for y in 0..h {
267            for x in 0..w {
268                let dt = current_time - self.timestamps[[y, x]];
269                if dt > 0.0 {
270                    self.frame[[y, x]] *= (-dt / tau).exp();
271                    self.timestamps[[y, x]] = current_time;
272                }
273            }
274        }
275    }
276
277    /// Resets the accumulator to zero.
278    pub fn reset(&mut self) {
279        self.frame.fill(0.0);
280        self.timestamps.fill(0.0);
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::event_camera::types::{Event, EventSlice, Polarity};
288
289    fn make_config(w: u16, h: u16) -> EventProcessingConfig {
290        EventProcessingConfig {
291            width: w,
292            height: h,
293            time_window: 0.033,
294            decay_rate: 0.01,
295            polarity_threshold: 0.5,
296        }
297    }
298
299    #[test]
300    fn test_histogram_single_event() {
301        let events = vec![Event::new(5, 10, 0.001, Polarity::On)];
302        let slice = EventSlice::new(events, 20, 20).expect("failed");
303        let config = make_config(20, 20);
304        let frame = events_to_frame(&slice, &FrameMethod::Histogram, &config).expect("failed");
305        assert!((frame.data[[10, 5]] - 1.0).abs() < f64::EPSILON);
306        // Other pixels should be zero
307        assert!((frame.data[[0, 0]]).abs() < f64::EPSILON);
308    }
309
310    #[test]
311    fn test_polarity_histogram_separated() {
312        let events = vec![
313            Event::new(0, 0, 0.001, Polarity::On),
314            Event::new(1, 1, 0.002, Polarity::Off),
315            Event::new(0, 0, 0.003, Polarity::On),
316        ];
317        let slice = EventSlice::new(events, 10, 10).expect("failed");
318        let config = make_config(10, 10);
319        let (on_frame, off_frame) = events_to_polarity_frames(&slice, &config).expect("failed");
320        assert!((on_frame.data[[0, 0]] - 2.0).abs() < f64::EPSILON);
321        assert!((off_frame.data[[1, 1]] - 1.0).abs() < f64::EPSILON);
322        assert!((on_frame.data[[1, 1]]).abs() < f64::EPSILON);
323        assert!((off_frame.data[[0, 0]]).abs() < f64::EPSILON);
324    }
325
326    #[test]
327    fn test_time_surface_most_recent() {
328        let events = vec![
329            Event::new(5, 5, 0.0, Polarity::On),
330            Event::new(5, 5, 0.5, Polarity::Off),
331            Event::new(5, 5, 1.0, Polarity::On),
332        ];
333        let slice = EventSlice::new(events, 10, 10).expect("failed");
334        let config = make_config(10, 10);
335        let frame = events_to_time_surface(&slice, &config).expect("failed");
336        // Most recent timestamp normalized: (1.0 - 0.0) / (1.0 - 0.0) = 1.0
337        assert!((frame.data[[5, 5]] - 1.0).abs() < 1e-9);
338    }
339
340    #[test]
341    fn test_exponential_decay_older_lower_weight() {
342        // Two events at the same pixel: one old, one recent
343        let events = vec![
344            Event::new(0, 0, 0.0, Polarity::On),
345            Event::new(1, 0, 1.0, Polarity::On), // recent
346        ];
347        let slice = EventSlice::new(events, 10, 10).expect("failed");
348        let config = EventProcessingConfig {
349            width: 10,
350            height: 10,
351            time_window: 0.033,
352            decay_rate: 0.5, // tau = 0.5s
353            polarity_threshold: 0.5,
354        };
355        let frame =
356            events_to_frame(&slice, &FrameMethod::ExponentialDecay, &config).expect("failed");
357        // Pixel (0,0): event at t=0.0, t_end=1.0, dt=1.0, weight=exp(-1.0/0.5)=exp(-2)
358        let expected_old = (-2.0_f64).exp();
359        assert!((frame.data[[0, 0]] - expected_old).abs() < 1e-9);
360        // Pixel (0,1): event at t=1.0, dt=0.0, weight=exp(0)=1.0
361        assert!((frame.data[[0, 1]] - 1.0).abs() < 1e-9);
362    }
363
364    #[test]
365    fn test_voxel_grid_bin_assignment() {
366        // 3 bins, events at t=0.0, 0.5, 1.0 over [0, 1]
367        let events = vec![
368            Event::new(0, 0, 0.0, Polarity::On), // bin 0
369            Event::new(0, 0, 0.5, Polarity::On), // bin 1
370            Event::new(0, 0, 1.0, Polarity::On), // bin 2 (clamped)
371        ];
372        let slice = EventSlice::new(events, 10, 10).expect("failed");
373        let config = make_config(10, 10);
374        let voxel = events_to_voxel_grid(&slice, 3, &config).expect("failed");
375        assert_eq!(voxel.shape(), &[3, 10, 10]);
376        // Check that events land in different bins
377        let total: f64 = (0..3).map(|b| voxel[[b, 0, 0]]).sum();
378        assert!((total - 3.0).abs() < f64::EPSILON); // all ON events counted
379    }
380
381    #[test]
382    fn test_streaming_accumulator_matches_batch() {
383        let events = vec![
384            Event::new(0, 0, 0.001, Polarity::On),
385            Event::new(1, 1, 0.002, Polarity::Off),
386            Event::new(0, 0, 0.003, Polarity::On),
387        ];
388        let config = make_config(10, 10);
389
390        // Batch
391        let slice = EventSlice::new(events.clone(), 10, 10).expect("failed");
392        let batch_frame =
393            events_to_frame(&slice, &FrameMethod::PolarityHistogram, &config).expect("failed");
394
395        // Streaming
396        let mut acc = StreamingFrameAccumulator::new(make_config(10, 10));
397        for e in &events {
398            acc.add_event(e);
399        }
400
401        // Compare
402        assert!((acc.get_frame()[[0, 0]] - batch_frame.data[[0, 0]]).abs() < f64::EPSILON);
403        assert!((acc.get_frame()[[1, 1]] - batch_frame.data[[1, 1]]).abs() < f64::EPSILON);
404    }
405}