1use super::{
2 frame_len, polarity::polarity_counts_on, EventFrame, EventFrameData, Representation,
3 RepresentationError, RepresentationKind,
4};
5use crate::EventStream;
6
7#[derive(Clone, Copy, Debug)]
22pub struct CountMask {
23 pct: f64,
24 white_frame: bool,
25}
26
27impl CountMask {
28 pub fn new(pct: f64, white_frame: bool) -> Self {
29 Self { pct, white_frame }
30 }
31
32 fn alpha(&self, counts: &[u64]) -> f32 {
35 let mut nonzero: Vec<f64> = counts
39 .iter()
40 .copied()
41 .filter(|&count| count > 0)
42 .map(|count| count as f64)
43 .collect();
44 if nonzero.is_empty() {
45 return 1.0;
46 }
47
48 let alpha = percentile_linear(&mut nonzero, self.pct);
49 if alpha > 0.0 {
50 alpha as f32
51 } else {
52 counts.iter().copied().max().unwrap_or(0).max(1) as f32
55 }
56 }
57}
58
59impl Default for CountMask {
60 fn default() -> Self {
61 Self::new(99.0, false)
62 }
63}
64
65impl Representation for CountMask {
66 type Output = EventFrame;
67
68 fn generate(&self, stream: &EventStream) -> Result<EventFrame, RepresentationError> {
69 self.generate_on(stream, crate::accel::Device::Cpu)
70 }
71
72 fn generate_on(
76 &self,
77 stream: &EventStream,
78 device: crate::accel::Device,
79 ) -> Result<EventFrame, RepresentationError> {
80 if !self.pct.is_finite() || !(0.0..=100.0).contains(&self.pct) {
81 return Err(RepresentationError::InvalidParameter("pct"));
82 }
83 let (width, height, length) = frame_len(stream, 3)?;
84 let plane_len = width * height;
85 let (_, _, counts) = polarity_counts_on(stream, device)?;
86
87 let alpha = self.alpha(&counts);
88 let level = |count: u64| {
92 let value = (count as f32).min(alpha) / alpha;
93 if self.white_frame {
94 1.0 - value
95 } else {
96 value
97 }
98 };
99 let byte = |value: f32| (value * 255.0).clamp(0.0, 255.0) as u8;
102
103 let mut values = vec![0_u8; length];
104 for index in 0..plane_len {
105 let positive = counts[index];
106 let negative = counts[plane_len + index];
107 let active = f32::from(u8::from(positive + negative > 0));
108 values[index] = byte(level(positive));
109 values[plane_len + index] = byte(if self.white_frame { 1.0 - active } else { active });
110 values[2 * plane_len + index] = byte(level(negative));
111 }
112
113 Ok(EventFrame {
114 data: EventFrameData::U8(values),
115 channels: 3,
116 width,
117 height,
118 kind: RepresentationKind::CountMask,
119 channel_names: vec![
120 "positive".to_owned(),
121 "activity".to_owned(),
122 "negative".to_owned(),
123 ],
124 })
125 }
126}
127
128pub(super) fn percentile_linear(values: &mut [f64], pct: f64) -> f64 {
138 let last = values.len() - 1;
139 let virtual_index = (pct / 100.0) * last as f64;
140
141 if virtual_index >= last as f64 {
142 let (_, maximum, _) = values.select_nth_unstable_by(last, f64::total_cmp);
144 return *maximum;
145 }
146
147 let gamma = virtual_index - virtual_index.floor();
148 let previous = virtual_index.floor() as usize;
149 let (_, lower, rest) = values.select_nth_unstable_by(previous, f64::total_cmp);
150 let (lower, upper) = (*lower, rest.iter().copied().fold(f64::INFINITY, f64::min));
152 let delta = upper - lower;
153
154 if gamma >= 0.5 {
155 upper - delta * (1.0 - gamma)
156 } else {
157 lower + delta * gamma
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use ndarray::{array, Array2};
164
165 use super::{CountMask, Representation};
166 use crate::{
167 representation::{EventFrameData, RepresentationError},
168 EventStream,
169 };
170
171 fn golden_stream() -> EventStream {
175 EventStream::from_array2(
176 array![
177 [5, 4, 0, 1],
178 [1, 0, 1, 0],
179 [6, 2, 2, 0],
180 [2, 0, 3, 0],
181 [1, 4, 4, 1],
182 [6, 2, 5, 0],
183 [5, 2, 6, 1],
184 [5, 2, 7, 0],
185 [7, 2, 8, 1],
186 [3, 1, 9, 0],
187 [6, 3, 10, 1],
188 [2, 4, 11, 0],
189 [4, 2, 12, 1],
190 [4, 1, 13, 0],
191 [1, 0, 14, 1],
192 [1, 0, 15, 0],
193 [1, 0, 16, 1],
194 [5, 0, 17, 1],
195 [4, 0, 18, 0],
196 [7, 3, 19, 0],
197 [5, 4, 20, 0],
198 [1, 5, 21, 1],
199 [7, 3, 22, 1],
200 [7, 3, 23, 0],
201 [5, 1, 24, 0],
202 [5, 5, 25, 1],
203 [1, 3, 26, 0],
204 [0, 4, 27, 1],
205 [2, 4, 28, 1],
206 [3, 5, 29, 0],
207 [0, 4, 30, 1],
208 [7, 5, 31, 1],
209 [3, 3, 32, 0],
210 [5, 3, 33, 0],
211 [1, 1, 34, 0],
212 [2, 5, 35, 0],
213 [0, 3, 36, 0],
214 [5, 2, 37, 1],
215 [6, 1, 38, 1],
216 [1, 1, 39, 1],
217 ],
218 8,
219 6,
220 0.001,
221 )
222 }
223
224 #[rustfmt::skip]
225 const GOLDEN: [u8; 144] = [
226 0, 255, 0, 0, 0, 127, 0, 0,
228 0, 127, 0, 0, 0, 0, 127, 0,
229 0, 0, 0, 0, 127, 255, 0, 127,
230 0, 0, 0, 0, 0, 0, 127, 127,
231 255, 127, 127, 0, 0, 127, 0, 0,
232 0, 127, 0, 0, 0, 127, 0, 127,
233 0, 255, 255, 0, 255, 255, 0, 0,
235 0, 255, 0, 255, 255, 255, 255, 0,
236 0, 0, 0, 0, 255, 255, 255, 255,
237 255, 255, 0, 255, 0, 255, 255, 255,
238 255, 255, 255, 0, 0, 255, 0, 0,
239 0, 255, 255, 255, 0, 255, 0, 255,
240 0, 255, 127, 0, 127, 0, 0, 0,
242 0, 127, 0, 127, 127, 127, 0, 0,
243 0, 0, 0, 0, 0, 127, 255, 0,
244 127, 127, 0, 127, 0, 127, 0, 255,
245 0, 0, 127, 0, 0, 127, 0, 0,
246 0, 0, 127, 127, 0, 0, 0, 0,
247 ];
248
249 #[test]
250 fn matches_the_reference_renderer() {
251 let frame = CountMask::default().generate(&golden_stream()).unwrap();
252
253 assert_eq!(frame.shape(), (3, 6, 8));
254 assert_eq!(frame.data(), &EventFrameData::U8(GOLDEN.to_vec()));
255 }
256
257 #[test]
259 fn ignores_timestamps() {
260 let mut rows = golden_stream().to_array2();
261 rows.column_mut(2).map_inplace(|t| *t = *t * 1_000_000 + 7);
262 let shifted = EventStream::from_array2(rows, 8, 6, 0.001);
263
264 let frame = CountMask::default().generate(&shifted).unwrap();
265
266 assert_eq!(frame.data(), &EventFrameData::U8(GOLDEN.to_vec()));
267 }
268
269 #[test]
275 fn normalizes_both_planes_by_one_pooled_percentile() {
276 let mut rows = Vec::new();
278 let pixels = [(0, 1, 1), (1, 1, 1), (2, 1, 1), (3, 4, 0), (4, 8, 0), (5, 12, 0)];
279 for (x, count, polarity) in pixels {
280 for _ in 0..count {
281 let timestamp = rows.len() as u64;
282 rows.push([x, 0, timestamp, polarity]);
283 }
284 }
285 let stream = EventStream::from_array2(
286 Array2::from_shape_fn((rows.len(), 4), |(row, column)| rows[row][column]),
287 6,
288 1,
289 0.001,
290 );
291
292 let frame = CountMask::default().generate(&stream).unwrap();
293
294 assert_eq!(
295 frame.data(),
296 &EventFrameData::U8(vec![
297 21, 21, 21, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 86, 172, 255, ])
301 );
302
303 let inverted = CountMask::new(99.0, true).generate(&stream).unwrap();
304
305 assert_eq!(
306 inverted.data(),
307 &EventFrameData::U8(vec![
308 233, 233, 233, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 168, 82, 0,
311 ])
312 );
313 }
314
315 #[test]
316 fn rejects_out_of_bounds_events() {
317 let stream = EventStream::from_array2(array![[2, 0, 10, 1]], 2, 2, 0.001);
318
319 let error = CountMask::default().generate(&stream).unwrap_err();
320
321 assert_eq!(
322 error.to_string(),
323 "event coordinate (2, 0) exceeds sensor size 2x2"
324 );
325 }
326
327 #[test]
328 fn rejects_percentiles_outside_zero_to_one_hundred() {
329 let stream = golden_stream();
330
331 for pct in [-1.0, 100.5, f64::NAN] {
332 assert_eq!(
333 CountMask::new(pct, false).generate(&stream).unwrap_err(),
334 RepresentationError::InvalidParameter("pct")
335 );
336 }
337 assert_eq!(
338 CountMask::new(150.0, false)
339 .generate(&stream)
340 .unwrap_err()
341 .to_string(),
342 "pct must be between 0 and 100"
343 );
344 }
345}