1use super::{
2 frame_len, polarity::polarity_counts, 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 if !self.pct.is_finite() || !(0.0..=100.0).contains(&self.pct) {
70 return Err(RepresentationError::InvalidParameter("pct"));
71 }
72 let (width, height, length) = frame_len(stream, 3)?;
73 let plane_len = width * height;
74 let (_, _, counts) = polarity_counts(stream)?;
75
76 let alpha = self.alpha(&counts);
77 let level = |count: u64| {
81 let value = (count as f32).min(alpha) / alpha;
82 if self.white_frame {
83 1.0 - value
84 } else {
85 value
86 }
87 };
88 let byte = |value: f32| (value * 255.0).clamp(0.0, 255.0) as u8;
91
92 let mut values = vec![0_u8; length];
93 for index in 0..plane_len {
94 let positive = counts[index];
95 let negative = counts[plane_len + index];
96 let active = f32::from(u8::from(positive + negative > 0));
97 values[index] = byte(level(positive));
98 values[plane_len + index] = byte(if self.white_frame { 1.0 - active } else { active });
99 values[2 * plane_len + index] = byte(level(negative));
100 }
101
102 Ok(EventFrame {
103 data: EventFrameData::U8(values),
104 channels: 3,
105 width,
106 height,
107 kind: RepresentationKind::CountMask,
108 channel_names: vec![
109 "positive".to_owned(),
110 "activity".to_owned(),
111 "negative".to_owned(),
112 ],
113 })
114 }
115}
116
117fn percentile_linear(values: &mut [f64], pct: f64) -> f64 {
127 let last = values.len() - 1;
128 let virtual_index = (pct / 100.0) * last as f64;
129
130 if virtual_index >= last as f64 {
131 let (_, maximum, _) = values.select_nth_unstable_by(last, f64::total_cmp);
133 return *maximum;
134 }
135
136 let gamma = virtual_index - virtual_index.floor();
137 let previous = virtual_index.floor() as usize;
138 let (_, lower, rest) = values.select_nth_unstable_by(previous, f64::total_cmp);
139 let (lower, upper) = (*lower, rest.iter().copied().fold(f64::INFINITY, f64::min));
141 let delta = upper - lower;
142
143 if gamma >= 0.5 {
144 upper - delta * (1.0 - gamma)
145 } else {
146 lower + delta * gamma
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use ndarray::{array, Array2};
153
154 use super::{CountMask, Representation};
155 use crate::{
156 representation::{EventFrameData, RepresentationError},
157 EventStream,
158 };
159
160 fn golden_stream() -> EventStream {
164 EventStream::from_array2(
165 array![
166 [5, 4, 0, 1],
167 [1, 0, 1, 0],
168 [6, 2, 2, 0],
169 [2, 0, 3, 0],
170 [1, 4, 4, 1],
171 [6, 2, 5, 0],
172 [5, 2, 6, 1],
173 [5, 2, 7, 0],
174 [7, 2, 8, 1],
175 [3, 1, 9, 0],
176 [6, 3, 10, 1],
177 [2, 4, 11, 0],
178 [4, 2, 12, 1],
179 [4, 1, 13, 0],
180 [1, 0, 14, 1],
181 [1, 0, 15, 0],
182 [1, 0, 16, 1],
183 [5, 0, 17, 1],
184 [4, 0, 18, 0],
185 [7, 3, 19, 0],
186 [5, 4, 20, 0],
187 [1, 5, 21, 1],
188 [7, 3, 22, 1],
189 [7, 3, 23, 0],
190 [5, 1, 24, 0],
191 [5, 5, 25, 1],
192 [1, 3, 26, 0],
193 [0, 4, 27, 1],
194 [2, 4, 28, 1],
195 [3, 5, 29, 0],
196 [0, 4, 30, 1],
197 [7, 5, 31, 1],
198 [3, 3, 32, 0],
199 [5, 3, 33, 0],
200 [1, 1, 34, 0],
201 [2, 5, 35, 0],
202 [0, 3, 36, 0],
203 [5, 2, 37, 1],
204 [6, 1, 38, 1],
205 [1, 1, 39, 1],
206 ],
207 8,
208 6,
209 0.001,
210 )
211 }
212
213 #[rustfmt::skip]
214 const GOLDEN: [u8; 144] = [
215 0, 255, 0, 0, 0, 127, 0, 0,
217 0, 127, 0, 0, 0, 0, 127, 0,
218 0, 0, 0, 0, 127, 255, 0, 127,
219 0, 0, 0, 0, 0, 0, 127, 127,
220 255, 127, 127, 0, 0, 127, 0, 0,
221 0, 127, 0, 0, 0, 127, 0, 127,
222 0, 255, 255, 0, 255, 255, 0, 0,
224 0, 255, 0, 255, 255, 255, 255, 0,
225 0, 0, 0, 0, 255, 255, 255, 255,
226 255, 255, 0, 255, 0, 255, 255, 255,
227 255, 255, 255, 0, 0, 255, 0, 0,
228 0, 255, 255, 255, 0, 255, 0, 255,
229 0, 255, 127, 0, 127, 0, 0, 0,
231 0, 127, 0, 127, 127, 127, 0, 0,
232 0, 0, 0, 0, 0, 127, 255, 0,
233 127, 127, 0, 127, 0, 127, 0, 255,
234 0, 0, 127, 0, 0, 127, 0, 0,
235 0, 0, 127, 127, 0, 0, 0, 0,
236 ];
237
238 #[test]
239 fn matches_the_reference_renderer() {
240 let frame = CountMask::default().generate(&golden_stream()).unwrap();
241
242 assert_eq!(frame.shape(), (3, 6, 8));
243 assert_eq!(frame.data(), &EventFrameData::U8(GOLDEN.to_vec()));
244 }
245
246 #[test]
248 fn ignores_timestamps() {
249 let mut rows = golden_stream().to_array2();
250 rows.column_mut(2).map_inplace(|t| *t = *t * 1_000_000 + 7);
251 let shifted = EventStream::from_array2(rows, 8, 6, 0.001);
252
253 let frame = CountMask::default().generate(&shifted).unwrap();
254
255 assert_eq!(frame.data(), &EventFrameData::U8(GOLDEN.to_vec()));
256 }
257
258 #[test]
264 fn normalizes_both_planes_by_one_pooled_percentile() {
265 let mut rows = Vec::new();
267 let pixels = [(0, 1, 1), (1, 1, 1), (2, 1, 1), (3, 4, 0), (4, 8, 0), (5, 12, 0)];
268 for (x, count, polarity) in pixels {
269 for _ in 0..count {
270 let timestamp = rows.len() as u64;
271 rows.push([x, 0, timestamp, polarity]);
272 }
273 }
274 let stream = EventStream::from_array2(
275 Array2::from_shape_fn((rows.len(), 4), |(row, column)| rows[row][column]),
276 6,
277 1,
278 0.001,
279 );
280
281 let frame = CountMask::default().generate(&stream).unwrap();
282
283 assert_eq!(
284 frame.data(),
285 &EventFrameData::U8(vec![
286 21, 21, 21, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 86, 172, 255, ])
290 );
291
292 let inverted = CountMask::new(99.0, true).generate(&stream).unwrap();
293
294 assert_eq!(
295 inverted.data(),
296 &EventFrameData::U8(vec![
297 233, 233, 233, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 168, 82, 0,
300 ])
301 );
302 }
303
304 #[test]
305 fn rejects_out_of_bounds_events() {
306 let stream = EventStream::from_array2(array![[2, 0, 10, 1]], 2, 2, 0.001);
307
308 let error = CountMask::default().generate(&stream).unwrap_err();
309
310 assert_eq!(
311 error.to_string(),
312 "event coordinate (2, 0) exceeds sensor size 2x2"
313 );
314 }
315
316 #[test]
317 fn rejects_percentiles_outside_zero_to_one_hundred() {
318 let stream = golden_stream();
319
320 for pct in [-1.0, 100.5, f64::NAN] {
321 assert_eq!(
322 CountMask::new(pct, false).generate(&stream).unwrap_err(),
323 RepresentationError::InvalidParameter("pct")
324 );
325 }
326 assert_eq!(
327 CountMask::new(150.0, false)
328 .generate(&stream)
329 .unwrap_err()
330 .to_string(),
331 "pct must be between 0 and 100"
332 );
333 }
334}