use super::{
frame_len, polarity::polarity_counts, EventFrame, EventFrameData, Representation,
RepresentationError, RepresentationKind,
};
use crate::EventStream;
#[derive(Clone, Copy, Debug)]
pub struct CountMask {
pct: f64,
white_frame: bool,
}
impl CountMask {
pub fn new(pct: f64, white_frame: bool) -> Self {
Self { pct, white_frame }
}
fn alpha(&self, counts: &[u64]) -> f32 {
let mut nonzero: Vec<f64> = counts
.iter()
.copied()
.filter(|&count| count > 0)
.map(|count| count as f64)
.collect();
if nonzero.is_empty() {
return 1.0;
}
let alpha = percentile_linear(&mut nonzero, self.pct);
if alpha > 0.0 {
alpha as f32
} else {
counts.iter().copied().max().unwrap_or(0).max(1) as f32
}
}
}
impl Default for CountMask {
fn default() -> Self {
Self::new(99.0, false)
}
}
impl Representation for CountMask {
type Output = EventFrame;
fn generate(&self, stream: &EventStream) -> Result<EventFrame, RepresentationError> {
if !self.pct.is_finite() || !(0.0..=100.0).contains(&self.pct) {
return Err(RepresentationError::InvalidParameter("pct"));
}
let (width, height, length) = frame_len(stream, 3)?;
let plane_len = width * height;
let (_, _, counts) = polarity_counts(stream)?;
let alpha = self.alpha(&counts);
let level = |count: u64| {
let value = (count as f32).min(alpha) / alpha;
if self.white_frame {
1.0 - value
} else {
value
}
};
let byte = |value: f32| (value * 255.0).clamp(0.0, 255.0) as u8;
let mut values = vec![0_u8; length];
for index in 0..plane_len {
let positive = counts[index];
let negative = counts[plane_len + index];
let active = f32::from(u8::from(positive + negative > 0));
values[index] = byte(level(positive));
values[plane_len + index] = byte(if self.white_frame { 1.0 - active } else { active });
values[2 * plane_len + index] = byte(level(negative));
}
Ok(EventFrame {
data: EventFrameData::U8(values),
channels: 3,
width,
height,
kind: RepresentationKind::CountMask,
channel_names: vec![
"positive".to_owned(),
"activity".to_owned(),
"negative".to_owned(),
],
})
}
}
fn percentile_linear(values: &mut [f64], pct: f64) -> f64 {
let last = values.len() - 1;
let virtual_index = (pct / 100.0) * last as f64;
if virtual_index >= last as f64 {
let (_, maximum, _) = values.select_nth_unstable_by(last, f64::total_cmp);
return *maximum;
}
let gamma = virtual_index - virtual_index.floor();
let previous = virtual_index.floor() as usize;
let (_, lower, rest) = values.select_nth_unstable_by(previous, f64::total_cmp);
let (lower, upper) = (*lower, rest.iter().copied().fold(f64::INFINITY, f64::min));
let delta = upper - lower;
if gamma >= 0.5 {
upper - delta * (1.0 - gamma)
} else {
lower + delta * gamma
}
}
#[cfg(test)]
mod tests {
use ndarray::{array, Array2};
use super::{CountMask, Representation};
use crate::{
representation::{EventFrameData, RepresentationError},
EventStream,
};
fn golden_stream() -> EventStream {
EventStream::from_array2(
array![
[5, 4, 0, 1],
[1, 0, 1, 0],
[6, 2, 2, 0],
[2, 0, 3, 0],
[1, 4, 4, 1],
[6, 2, 5, 0],
[5, 2, 6, 1],
[5, 2, 7, 0],
[7, 2, 8, 1],
[3, 1, 9, 0],
[6, 3, 10, 1],
[2, 4, 11, 0],
[4, 2, 12, 1],
[4, 1, 13, 0],
[1, 0, 14, 1],
[1, 0, 15, 0],
[1, 0, 16, 1],
[5, 0, 17, 1],
[4, 0, 18, 0],
[7, 3, 19, 0],
[5, 4, 20, 0],
[1, 5, 21, 1],
[7, 3, 22, 1],
[7, 3, 23, 0],
[5, 1, 24, 0],
[5, 5, 25, 1],
[1, 3, 26, 0],
[0, 4, 27, 1],
[2, 4, 28, 1],
[3, 5, 29, 0],
[0, 4, 30, 1],
[7, 5, 31, 1],
[3, 3, 32, 0],
[5, 3, 33, 0],
[1, 1, 34, 0],
[2, 5, 35, 0],
[0, 3, 36, 0],
[5, 2, 37, 1],
[6, 1, 38, 1],
[1, 1, 39, 1],
],
8,
6,
0.001,
)
}
#[rustfmt::skip]
const GOLDEN: [u8; 144] = [
0, 255, 0, 0, 0, 127, 0, 0,
0, 127, 0, 0, 0, 0, 127, 0,
0, 0, 0, 0, 127, 255, 0, 127,
0, 0, 0, 0, 0, 0, 127, 127,
255, 127, 127, 0, 0, 127, 0, 0,
0, 127, 0, 0, 0, 127, 0, 127,
0, 255, 255, 0, 255, 255, 0, 0,
0, 255, 0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 255, 255, 255, 255,
255, 255, 0, 255, 0, 255, 255, 255,
255, 255, 255, 0, 0, 255, 0, 0,
0, 255, 255, 255, 0, 255, 0, 255,
0, 255, 127, 0, 127, 0, 0, 0,
0, 127, 0, 127, 127, 127, 0, 0,
0, 0, 0, 0, 0, 127, 255, 0,
127, 127, 0, 127, 0, 127, 0, 255,
0, 0, 127, 0, 0, 127, 0, 0,
0, 0, 127, 127, 0, 0, 0, 0,
];
#[test]
fn matches_the_reference_renderer() {
let frame = CountMask::default().generate(&golden_stream()).unwrap();
assert_eq!(frame.shape(), (3, 6, 8));
assert_eq!(frame.data(), &EventFrameData::U8(GOLDEN.to_vec()));
}
#[test]
fn ignores_timestamps() {
let mut rows = golden_stream().to_array2();
rows.column_mut(2).map_inplace(|t| *t = *t * 1_000_000 + 7);
let shifted = EventStream::from_array2(rows, 8, 6, 0.001);
let frame = CountMask::default().generate(&shifted).unwrap();
assert_eq!(frame.data(), &EventFrameData::U8(GOLDEN.to_vec()));
}
#[test]
fn normalizes_both_planes_by_one_pooled_percentile() {
let mut rows = Vec::new();
let pixels = [(0, 1, 1), (1, 1, 1), (2, 1, 1), (3, 4, 0), (4, 8, 0), (5, 12, 0)];
for (x, count, polarity) in pixels {
for _ in 0..count {
let timestamp = rows.len() as u64;
rows.push([x, 0, timestamp, polarity]);
}
}
let stream = EventStream::from_array2(
Array2::from_shape_fn((rows.len(), 4), |(row, column)| rows[row][column]),
6,
1,
0.001,
);
let frame = CountMask::default().generate(&stream).unwrap();
assert_eq!(
frame.data(),
&EventFrameData::U8(vec![
21, 21, 21, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 86, 172, 255, ])
);
let inverted = CountMask::new(99.0, true).generate(&stream).unwrap();
assert_eq!(
inverted.data(),
&EventFrameData::U8(vec![
233, 233, 233, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 168, 82, 0,
])
);
}
#[test]
fn rejects_out_of_bounds_events() {
let stream = EventStream::from_array2(array![[2, 0, 10, 1]], 2, 2, 0.001);
let error = CountMask::default().generate(&stream).unwrap_err();
assert_eq!(
error.to_string(),
"event coordinate (2, 0) exceeds sensor size 2x2"
);
}
#[test]
fn rejects_percentiles_outside_zero_to_one_hundred() {
let stream = golden_stream();
for pct in [-1.0, 100.5, f64::NAN] {
assert_eq!(
CountMask::new(pct, false).generate(&stream).unwrap_err(),
RepresentationError::InvalidParameter("pct")
);
}
assert_eq!(
CountMask::new(150.0, false)
.generate(&stream)
.unwrap_err()
.to_string(),
"pct must be between 0 and 100"
);
}
}