Skip to main content

eventcv_core/representation/
time_surface.rs

1use super::{
2    age_ms, event_index, frame_len, reference_time, validate_positive, EventFrame, EventFrameData,
3    Representation, RepresentationError, RepresentationKind,
4};
5use crate::EventStream;
6
7#[derive(Clone, Copy, Debug)]
8pub struct TimeSurface {
9    tau_ms: f64,
10}
11
12impl TimeSurface {
13    pub fn new(tau_ms: f64) -> Self {
14        Self { tau_ms }
15    }
16}
17
18impl Default for TimeSurface {
19    fn default() -> Self {
20        Self::new(30.0)
21    }
22}
23
24impl Representation for TimeSurface {
25    type Output = EventFrame;
26
27    fn generate(&self, stream: &EventStream) -> Result<EventFrame, RepresentationError> {
28        validate_positive(self.tau_ms, "tau_ms")?;
29        let (width, height, length) = frame_len(stream, 2)?;
30        let plane_len = width * height;
31        // Two zeroed planes rather than a `Vec<Option<u64>>`: `vec![0; n]` is one `alloc_zeroed`,
32        // so a cell no event touches is never written or even faulted in, where the `Option` was
33        // 16 bytes a cell and had to be filled end to end before the first event was read — about
34        // a millisecond at 640x480, however few events the window held. `seen` rather than a
35        // sentinel timestamp because 0 is a timestamp a stream can hold.
36        //
37        // `touched` then drives the exponential over just those cells. A window cannot touch more
38        // cells than it has events, so `stream.len()` says before the loop whether that is worth
39        // doing: a dense window would reach most cells anyway, and scattering through a
40        // sensor-sized list costs more than one sequential pass. See `AveragedTimeSurface` for the
41        // measurement behind the threshold.
42        let track_touched = stream.len().saturating_mul(4) <= length;
43        let mut latest = vec![0_u64; length];
44        let mut seen = vec![false; length];
45        let mut touched = if track_touched {
46            Vec::with_capacity(stream.len())
47        } else {
48            Vec::new()
49        };
50
51        for event in stream.iter() {
52            let index =
53                event_index(event, width, height)? + if event.polarity { 0 } else { plane_len };
54            if !seen[index] {
55                seen[index] = true;
56                if track_touched {
57                    touched.push(index);
58                }
59                latest[index] = event.timestamp;
60            } else if event.timestamp > latest[index] {
61                latest[index] = event.timestamp;
62            }
63        }
64
65        let mut values = vec![0_f32; length];
66        if let Some(reference) = reference_time(stream) {
67            if track_touched {
68                for index in touched {
69                    values[index] =
70                        (-age_ms(stream, reference, latest[index]) / self.tau_ms).exp() as f32;
71                }
72            } else {
73                for (index, value) in values.iter_mut().enumerate() {
74                    if seen[index] {
75                        *value =
76                            (-age_ms(stream, reference, latest[index]) / self.tau_ms).exp() as f32;
77                    }
78                }
79            }
80        }
81
82        Ok(EventFrame {
83            data: EventFrameData::F32(values),
84            channels: 2,
85            width,
86            height,
87            kind: RepresentationKind::TimeSurface,
88            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
89        })
90    }
91
92    /// The kernel keeps the *smallest age* per pixel and polarity with an integer `atomicMin`,
93    /// which is the same quantity the CPU's "latest timestamp" is, and maps it through the same
94    /// `exp` on readback. Order cannot matter to a minimum, so this is exact up to the one `exp`.
95    fn generate_on(
96        &self,
97        stream: &EventStream,
98        device: crate::accel::Device,
99    ) -> Result<EventFrame, RepresentationError> {
100        if device == crate::accel::Device::Cpu {
101            return self.generate(stream);
102        }
103        validate_positive(self.tau_ms, "tau_ms")?;
104        let (width, height, length) = frame_len(stream, 2)?;
105        let ages = super::on_gpu(
106            stream,
107            &crate::accel::GpuDispatch {
108                entry: "time_surface",
109                cells: length,
110                initial: i32::MAX,
111                bins: 2,
112                span_ms: self.tau_ms as f32,
113                fixed_one: 1.0,
114                window_ms: None,
115                needs_ages: true,
116            },
117        )?;
118        let scale = stream.timestamp_scale_ms();
119        let values = ages
120            .iter()
121            .map(|age| match *age {
122                i32::MAX => 0.0, // no event ever landed on this pixel
123                age => (-(f64::from(age) * scale) / self.tau_ms).exp() as f32,
124            })
125            .collect();
126        Ok(EventFrame {
127            data: EventFrameData::F32(values),
128            channels: 2,
129            width,
130            height,
131            kind: RepresentationKind::TimeSurface,
132            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
133        })
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use ndarray::array;
140
141    use super::{Representation, TimeSurface};
142    use crate::{representation::EventFrameData, EventStream};
143
144    #[test]
145    fn uses_latest_event_per_pixel_and_polarity() {
146        let stream = EventStream::from_array2(
147            array![[0, 0, 30_000, 1], [1, 0, 20_000, 0], [0, 0, 10_000, 1]],
148            2,
149            1,
150            0.001,
151        );
152
153        let frame = TimeSurface::new(10.0).generate(&stream).unwrap();
154        let EventFrameData::F32(values) = frame.data() else {
155            panic!("time surfaces must use float32 data");
156        };
157
158        assert_eq!(values[0], 1.0);
159        assert_eq!(values[1], 0.0);
160        assert!((values[3] - (-1.0_f32).exp()).abs() < 1e-6);
161    }
162
163    /// `0` is a timestamp a stream can hold, and it is also what the zeroed scratch plane holds
164    /// for a cell no event reached. The two must not be confused.
165    #[test]
166    fn a_timestamp_of_zero_is_an_event_not_an_empty_pixel() {
167        let stream = EventStream::from_array2(array![[0, 0, 0, 1]], 2, 1, 0.001);
168
169        let frame = TimeSurface::new(10.0).generate(&stream).unwrap();
170        let EventFrameData::F32(values) = frame.data() else {
171            panic!("time surfaces must use float32 data");
172        };
173
174        assert_eq!(values[0], 1.0); // the event, at age 0
175        assert_eq!(values[1], 0.0); // no event ever landed here
176        assert!(values[2..].iter().all(|&value| value == 0.0)); // nor on the negative plane
177    }
178}