1use crate::representation::{EventFrame, EventFrameData, RepresentationKind};
9use crate::EventStream;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Rgb8Image {
14 pub width: usize,
15 pub height: usize,
16 pub pixels: Vec<u8>,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
23pub enum Colormap {
24 Grayscale,
25 #[default]
26 Viridis,
27 Turbo,
28 RedBlue,
29}
30
31impl Colormap {
32 pub fn from_name(name: &str) -> Option<Self> {
33 Some(match name {
34 "grayscale" | "gray" | "grey" => Self::Grayscale,
35 "viridis" => Self::Viridis,
36 "turbo" => Self::Turbo,
37 "redblue" | "diverging" => Self::RedBlue,
38 _ => return None,
39 })
40 }
41}
42
43pub fn render_frame(frame: &EventFrame, colormap: Colormap, normalize: bool) -> Rgb8Image {
48 render_frame_scaled(frame, colormap, Scale::from_normalize(normalize))
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
57pub enum Scale {
58 Natural,
60 Auto,
62 Fixed(f64),
66}
67
68impl Scale {
69 fn from_normalize(normalize: bool) -> Self {
70 if normalize {
71 Self::Auto
72 } else {
73 Self::Natural
74 }
75 }
76}
77
78pub fn render_frame_scaled(frame: &EventFrame, colormap: Colormap, scale: Scale) -> Rgb8Image {
80 let normalize = matches!(scale, Scale::Auto);
81 let (_, height, width) = frame.shape();
82 let plane_len = width * height;
83
84 if matches!(
85 frame.kind(),
86 RepresentationKind::Tencode | RepresentationKind::CountMask | RepresentationKind::RedBlue
87 ) {
88 return Rgb8Image {
89 width,
90 height,
91 pixels: render_rgb8_planes(frame, plane_len, normalize),
92 };
93 }
94 if frame.kind() == RepresentationKind::Flow {
96 return Rgb8Image {
97 width,
98 height,
99 pixels: render_flow(frame, plane_len, normalize),
100 };
101 }
102
103 let (field, signed) = scalar_field(frame, plane_len);
104 let scale = match scale {
105 Scale::Fixed(extent) if extent > 0.0 => 1.0 / extent,
106 Scale::Fixed(_) => field_scale(&field, signed, is_float(frame.data()), false),
107 _ => field_scale(&field, signed, is_float(frame.data()), normalize),
108 };
109 let colormap = if signed { Colormap::RedBlue } else { colormap };
110
111 let mut pixels = Vec::with_capacity(plane_len * 3);
112 for &value in &field {
113 let normalized = value * scale;
114 let [r, g, b] = if signed {
115 colormap.sample_signed(normalized.clamp(-1.0, 1.0))
116 } else {
117 colormap.sample(normalized.clamp(0.0, 1.0))
118 };
119 pixels.extend_from_slice(&[r, g, b]);
120 }
121 Rgb8Image {
122 width,
123 height,
124 pixels,
125 }
126}
127
128pub fn frame_extent(frame: &EventFrame) -> f64 {
135 if matches!(
136 frame.kind(),
137 RepresentationKind::Tencode
138 | RepresentationKind::CountMask
139 | RepresentationKind::RedBlue
140 | RepresentationKind::Flow
141 ) {
142 return 0.0;
143 }
144 let (_, height, width) = frame.shape();
145 let (field, signed) = scalar_field(frame, width * height);
146 robust_extent(&field, signed)
147}
148
149fn scalar_field(frame: &EventFrame, plane_len: usize) -> (Vec<f64>, bool) {
151 let (channels, _, _) = frame.shape();
152 let data = frame.data();
153 match frame.kind() {
154 RepresentationKind::Binary
158 | RepresentationKind::Count
159 | RepresentationKind::Intensity
160 | RepresentationKind::Labels => {
161 ((0..plane_len).map(|i| value_at(data, i)).collect(), false)
162 }
163 RepresentationKind::Flow => (vec![0.0; plane_len], false),
165 RepresentationKind::Polarity
167 | RepresentationKind::TimeSurface
168 | RepresentationKind::AveragedTimeSurface => (
169 (0..plane_len)
170 .map(|i| value_at(data, i) - value_at(data, plane_len + i))
171 .collect(),
172 true,
173 ),
174 RepresentationKind::Mcts => {
176 let half = channels / 2;
177 (
178 (0..plane_len)
179 .map(|i| {
180 let neg: f64 = (0..half).map(|c| value_at(data, c * plane_len + i)).sum();
181 let pos: f64 = (half..channels)
182 .map(|c| value_at(data, c * plane_len + i))
183 .sum();
184 pos - neg
185 })
186 .collect(),
187 true,
188 )
189 }
190 RepresentationKind::Voxel => (
192 (0..plane_len)
193 .map(|i| {
194 (0..channels)
195 .map(|c| value_at(data, c * plane_len + i))
196 .sum()
197 })
198 .collect(),
199 true,
200 ),
201 RepresentationKind::Tencode | RepresentationKind::CountMask | RepresentationKind::RedBlue => {
203 (vec![0.0; plane_len], false)
204 }
205 }
206}
207
208fn field_scale(field: &[f64], signed: bool, is_float: bool, normalize: bool) -> f64 {
213 if normalize {
214 let extent = robust_extent(field, signed);
215 if extent > 0.0 {
216 1.0 / extent
217 } else {
218 0.0
219 }
220 } else if is_float {
221 1.0 } else {
223 1.0 / 255.0 }
225}
226
227fn robust_extent(field: &[f64], signed: bool) -> f64 {
231 let mut mags: Vec<f64> = field
232 .iter()
233 .map(|&v| if signed { v.abs() } else { v })
234 .filter(|&v| v > 0.0)
235 .collect();
236 if mags.len() < 100 {
237 return mags.iter().copied().fold(0.0_f64, f64::max);
238 }
239 let index = (((mags.len() as f64) * 0.99).ceil() as usize - 1).min(mags.len() - 1);
240 mags.select_nth_unstable_by(index, f64::total_cmp);
241 mags[index]
242}
243
244const FLOW_GAMMA: f64 = 0.5;
250
251fn render_flow(frame: &EventFrame, plane_len: usize, normalize: bool) -> Vec<u8> {
252 let data = frame.data();
253 let magnitude = |i: usize| value_at(data, i).hypot(value_at(data, plane_len + i));
254 let scale = if normalize {
255 let mags: Vec<f64> = (0..plane_len).map(magnitude).collect();
256 let extent = robust_extent(&mags, false);
257 if extent > 0.0 {
258 1.0 / extent
259 } else {
260 0.0
261 }
262 } else {
263 1.0
264 };
265
266 let wheel = flow_color_wheel();
267 let ncols = wheel.len();
268 let mut pixels = Vec::with_capacity(plane_len * 3);
269 for i in 0..plane_len {
270 let (fx, fy) = (value_at(data, i), value_at(data, plane_len + i));
271 let rad = (magnitude(i) * scale).powf(FLOW_GAMMA);
275 let angle = (-fy).atan2(-fx) / std::f64::consts::PI; let fk = (angle + 1.0) / 2.0 * (ncols as f64 - 1.0);
278 let k0 = fk.floor() as usize;
279 let k1 = (k0 + 1) % ncols;
280 let f = fk - k0 as f64;
281 let mut rgb = [0_u8; 3];
282 for (channel, slot) in rgb.iter_mut().enumerate() {
283 let base = (1.0 - f) * wheel[k0][channel] + f * wheel[k1][channel];
284 let col = if rad <= 1.0 {
286 1.0 - rad * (1.0 - base)
287 } else {
288 base * 0.75
289 };
290 *slot = (255.0 * col).round().clamp(0.0, 255.0) as u8;
291 }
292 pixels.extend_from_slice(&rgb);
293 }
294 pixels
295}
296
297fn flow_color_wheel() -> Vec<[f64; 3]> {
300 const SEGMENTS: [(usize, [f64; 3], [f64; 3]); 6] = [
301 (15, [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]), (6, [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]), (4, [0.0, 1.0, 0.0], [0.0, 1.0, 1.0]), (11, [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]), (13, [0.0, 0.0, 1.0], [1.0, 0.0, 1.0]), (6, [1.0, 0.0, 1.0], [1.0, 0.0, 0.0]), ];
308 let mut wheel = Vec::with_capacity(55);
309 for (count, from, to) in SEGMENTS {
310 for step in 0..count {
311 let t = step as f64 / count as f64;
312 wheel.push([
313 from[0] + t * (to[0] - from[0]),
314 from[1] + t * (to[1] - from[1]),
315 from[2] + t * (to[2] - from[2]),
316 ]);
317 }
318 }
319 wheel
320}
321
322fn render_rgb8_planes(frame: &EventFrame, plane_len: usize, normalize: bool) -> Vec<u8> {
326 let data = frame.data();
327 let scale = if normalize {
328 let max = (0..plane_len * 3)
329 .map(|i| value_at(data, i))
330 .fold(0.0, f64::max);
331 if max > 0.0 {
332 255.0 / max
333 } else {
334 0.0
335 }
336 } else {
337 1.0
338 };
339 let channel = |plane: usize, i: usize| {
340 (value_at(data, plane * plane_len + i) * scale)
341 .round()
342 .clamp(0.0, 255.0) as u8
343 };
344 let mut pixels = Vec::with_capacity(plane_len * 3);
345 for i in 0..plane_len {
346 pixels.extend_from_slice(&[channel(0, i), channel(1, i), channel(2, i)]);
347 }
348 pixels
349}
350
351fn value_at(data: &EventFrameData, index: usize) -> f64 {
352 match data {
353 EventFrameData::U8(values) => f64::from(values[index]),
354 EventFrameData::U16(values) => f64::from(values[index]),
355 EventFrameData::U64(values) => values[index] as f64,
356 EventFrameData::F32(values) => f64::from(values[index]),
357 }
358}
359
360fn is_float(data: &EventFrameData) -> bool {
361 matches!(data, EventFrameData::F32(_))
362}
363
364impl Colormap {
365 fn sample(self, t: f64) -> [u8; 3] {
367 match self {
368 Self::Grayscale => {
369 let v = (t * 255.0).round() as u8;
370 [v, v, v]
371 }
372 Self::Viridis => interpolate(&VIRIDIS, t),
373 Self::Turbo => interpolate(&TURBO, t),
374 Self::RedBlue => self.sample_signed(t),
376 }
377 }
378
379 fn sample_signed(self, s: f64) -> [u8; 3] {
382 match self {
383 Self::RedBlue => {
384 let positive = s.max(0.0);
385 let negative = (-s).max(0.0);
386 [
387 (positive * 255.0).round() as u8,
388 ((positive.min(negative)) * 40.0).round() as u8,
389 (negative * 255.0).round() as u8,
390 ]
391 }
392 other => other.sample(s.abs()),
394 }
395 }
396}
397
398fn interpolate(anchors: &[[u8; 3]], t: f64) -> [u8; 3] {
400 let last = anchors.len() - 1;
401 let position = t.clamp(0.0, 1.0) * last as f64;
402 let lower = position.floor() as usize;
403 if lower >= last {
404 return anchors[last];
405 }
406 let frac = position - lower as f64;
407 let a = anchors[lower];
408 let b = anchors[lower + 1];
409 std::array::from_fn(|c| {
410 (f64::from(a[c]) + (f64::from(b[c]) - f64::from(a[c])) * frac).round() as u8
411 })
412}
413
414const VIRIDIS: [[u8; 3]; 9] = [
416 [68, 1, 84],
417 [72, 40, 120],
418 [62, 74, 137],
419 [49, 104, 142],
420 [38, 130, 142],
421 [31, 158, 137],
422 [53, 183, 121],
423 [110, 206, 88],
424 [253, 231, 37],
425];
426
427const TURBO: [[u8; 3]; 11] = [
428 [48, 18, 59],
429 [61, 79, 195],
430 [54, 138, 247],
431 [33, 192, 225],
432 [39, 232, 166],
433 [127, 251, 86],
434 [191, 235, 49],
435 [240, 190, 50],
436 [251, 128, 44],
437 [225, 58, 20],
438 [122, 4, 3],
439];
440
441const RAW_POSITIVE: [u8; 3] = [0xff, 0x49, 0x6c];
444const RAW_NEGATIVE: [u8; 3] = [0x27, 0xc2, 0xff];
445
446const DECAY_LUT_LEN: usize = 4096;
460
461#[derive(Clone, Debug)]
462pub struct RawSurface {
463 width: usize,
464 height: usize,
465 last_t_ms: Vec<f64>,
467 last_positive: Vec<bool>,
468 latest_ms: f64,
471 decay_lut: Vec<f32>,
476 decay_cutoff: f64,
477 decay_lut_step: f64,
478}
479
480impl RawSurface {
481 pub fn new(width: usize, height: usize, decay_ms: f64) -> Self {
484 let pixels = width * height;
485 let decay_ms = decay_ms.max(1e-6);
486 let decay_cutoff = decay_ms * 6.5;
487 let decay_lut_step = decay_cutoff / (DECAY_LUT_LEN - 1) as f64;
488 let decay_lut = (0..DECAY_LUT_LEN)
489 .map(|i| (-(i as f64 * decay_lut_step) / decay_ms).exp() as f32)
490 .collect();
491 Self {
492 width,
493 height,
494 last_t_ms: vec![f64::NEG_INFINITY; pixels],
495 last_positive: vec![false; pixels],
496 latest_ms: 0.0,
497 decay_lut,
498 decay_cutoff,
499 decay_lut_step,
500 }
501 }
502
503 pub fn dimensions(&self) -> (usize, usize) {
505 (self.width, self.height)
506 }
507
508 pub fn clear(&mut self) {
513 self.last_t_ms.fill(f64::NEG_INFINITY);
514 self.last_positive.fill(false);
515 self.latest_ms = 0.0;
516 }
517
518 pub fn update(&mut self, stream: &EventStream) {
521 let scale = stream.timestamp_scale_ms();
522 let xs = stream.xs();
523 let ys = stream.ys();
524 let ts = stream.ts();
525 let ps = stream.ps();
526 for index in 0..xs.len() {
527 self.stamp(
528 xs[index] as usize,
529 ys[index] as usize,
530 ts[index] as f64 * scale,
531 ps[index],
532 );
533 }
534 }
535
536 pub fn stamp(&mut self, x: usize, y: usize, t_ms: f64, positive: bool) {
540 if x >= self.width || y >= self.height {
541 return;
542 }
543 let pixel = y * self.width + x;
544 if t_ms >= self.last_t_ms[pixel] {
546 self.last_t_ms[pixel] = t_ms;
547 self.last_positive[pixel] = positive;
548 }
549 if t_ms > self.latest_ms {
550 self.latest_ms = t_ms;
551 }
552 }
553
554 pub fn render(&self) -> Rgb8Image {
558 let count = self.width * self.height;
559 let mut pixels = vec![0u8; count * 3]; for index in 0..count {
575 let age = self.latest_ms - self.last_t_ms[index];
576 if !matches!(age.partial_cmp(&self.decay_cutoff), Some(std::cmp::Ordering::Less)) {
579 continue;
580 }
581 let bucket = ((age / self.decay_lut_step) as usize).min(DECAY_LUT_LEN - 1);
582 let intensity = self.decay_lut[bucket];
583 let color = if self.last_positive[index] {
584 RAW_POSITIVE
585 } else {
586 RAW_NEGATIVE
587 };
588 let base = index * 3;
589 for channel in 0..3 {
590 pixels[base + channel] = (f32::from(color[channel]) * intensity).round() as u8;
591 }
592 }
593 Rgb8Image {
594 width: self.width,
595 height: self.height,
596 pixels,
597 }
598 }
599}
600
601pub fn render_raw(stream: &EventStream, decay_ms: f64) -> Rgb8Image {
605 let (width, height) = stream.sensor_size();
606 let mut surface = RawSurface::new(width, height, decay_ms);
607 surface.update(stream);
608 surface.render()
609}
610
611#[cfg(test)]
612mod tests {
613 use super::{render_frame, render_raw, Colormap, RawSurface, Rgb8Image};
614 use crate::representation::{
615 AveragedTimeSurface, Binary, EventCount, EventFrame, EventFrameData, Representation,
616 RepresentationKind, Tencode,
617 };
618 use crate::EventStream;
619 use ndarray::array;
620
621 fn pixel(image: &Rgb8Image, x: usize, y: usize) -> [u8; 3] {
622 let i = (y * image.width + x) * 3;
623 [image.pixels[i], image.pixels[i + 1], image.pixels[i + 2]]
624 }
625
626 #[test]
627 fn raw_surface_colors_pixels_by_polarity() {
628 let stream = EventStream::from_array2(array![[0, 0, 10, 1], [1, 0, 10, 0]], 2, 1, 0.001);
630 let image = render_raw(&stream, 1000.0);
631
632 let [r0, _, b0] = pixel(&image, 0, 0);
633 let [r1, _, b1] = pixel(&image, 1, 0);
634 assert!(r0 > b0, "positive pixel is warm/red-dominant");
635 assert!(b1 > r1, "negative pixel is cool/blue-dominant");
636 }
637
638 #[test]
639 fn raw_surface_fades_older_events() {
640 let stream =
642 EventStream::from_array2(array![[0, 0, 0, 1], [1, 0, 100_000, 1]], 2, 1, 0.001);
643 let image = render_raw(&stream, 50.0); let old = pixel(&image, 0, 0)[0];
646 let new = pixel(&image, 1, 0)[0];
647 assert!(new > old, "newer event brighter than older");
648 assert!(old > 0, "older event still faintly visible");
649 }
650
651 #[test]
652 fn raw_surface_untouched_pixels_are_black() {
653 let stream = EventStream::from_array2(array![[0, 0, 10, 1]], 2, 1, 0.001);
654 let image = render_raw(&stream, 1000.0);
655
656 assert_eq!(pixel(&image, 1, 0), [0, 0, 0]);
657 assert_eq!((image.width, image.height), (2, 1));
658 }
659
660 #[test]
661 fn raw_surface_persists_across_updates() {
662 let (width, height) = (2, 1);
663 let mut surface = RawSurface::new(width, height, 50.0);
664 surface.update(&EventStream::from_array2(array![[0, 0, 0, 1]], width, height, 0.001));
665 surface.update(&EventStream::from_array2(
666 array![[1, 0, 100_000, 1]],
667 width,
668 height,
669 0.001,
670 ));
671 let image = surface.render();
672
673 assert!(pixel(&image, 0, 0)[0] > 0, "earlier event persists across updates");
675 assert!(pixel(&image, 1, 0)[0] > pixel(&image, 0, 0)[0], "newest event is brightest");
676 }
677
678 #[test]
679 fn count_frame_maps_the_busiest_pixel_to_the_colormap_top() {
680 let stream = EventStream::from_array2(
681 array![[0, 0, 1, 1], [0, 0, 2, 0], [1, 0, 3, 1]],
682 2,
683 1,
684 0.001,
685 );
686 let frame = EventCount::default().generate(&stream).unwrap();
687
688 let image = render_frame(&frame, Colormap::Grayscale, true);
689
690 assert_eq!(image.width, 2);
691 assert_eq!(image.height, 1);
692 assert_eq!(pixel(&image, 0, 0), [255, 255, 255]);
694 assert_eq!(pixel(&image, 1, 0), [128, 128, 128]);
695 }
696
697 #[test]
698 fn a_single_outlier_does_not_black_out_the_rest_of_the_field() {
699 let plane = 12 * 12;
703 let mut data = vec![10_u64; plane];
704 data[0] = 1000; let frame = EventFrame::from_parts(
706 EventFrameData::U64(data),
707 12,
708 12,
709 RepresentationKind::Count,
710 vec!["count".to_owned()],
711 );
712
713 let image = render_frame(&frame, Colormap::Grayscale, true);
714
715 assert!(
717 pixel(&image, 5, 5)[0] > 200,
718 "typical value must stay visible"
719 );
720 }
721
722 #[test]
723 fn flow_middlebury_encodes_direction_as_hue_and_zero_as_white() {
724 let plane = 12 * 12;
726 let mut data = vec![0.0_f32; plane * 2];
727 for y in 0..12 {
728 for x in 0..12 {
729 data[y * 12 + x] = if x < 6 { 1.0 } else { -1.0 }; }
731 }
732 data[0] = 0.0; let frame = EventFrame::from_parts(
734 EventFrameData::F32(data),
735 12,
736 12,
737 RepresentationKind::Flow,
738 vec!["flow_x".to_owned(), "flow_y".to_owned()],
739 );
740
741 let image = render_frame(&frame, Colormap::Viridis, true);
742
743 assert_ne!(
745 pixel(&image, 3, 5),
746 pixel(&image, 9, 5),
747 "opposite flow directions must differ in colour"
748 );
749 assert_eq!(pixel(&image, 0, 0), [255, 255, 255], "zero flow is white");
750 }
751
752 #[test]
753 fn signed_reprs_use_the_diverging_red_blue_map() {
754 let stream = EventStream::from_array2(array![[0, 0, 10, 1], [1, 0, 10, 0]], 2, 1, 0.001);
757 let frame = AveragedTimeSurface::default().generate(&stream).unwrap();
758
759 let image = render_frame(&frame, Colormap::Viridis, true);
760
761 let [r0, _, b0] = pixel(&image, 0, 0);
762 let [r1, _, b1] = pixel(&image, 1, 0);
763 assert!(r0 > b0, "positive pixel should be red-dominant");
764 assert!(b1 > r1, "negative pixel should be blue-dominant");
765 }
766
767 #[test]
768 fn tencode_passes_through_as_rgb() {
769 let stream = EventStream::from_array2(array![[0, 0, 10, 1]], 1, 1, 0.001);
770 let frame = Tencode::default().generate(&stream).unwrap();
771
772 let image = render_frame(&frame, Colormap::Turbo, false);
773
774 assert_eq!(image.pixels.len(), 3);
775 }
776
777 #[test]
778 fn empty_frame_renders_uniformly_at_the_colormap_floor() {
779 let stream = EventStream::from_array2(ndarray::Array2::zeros((0, 4)), 3, 2, 0.001);
780 let frame = Binary.generate(&stream).unwrap();
781
782 let image = render_frame(&frame, Colormap::Viridis, true);
783
784 assert_eq!(image.pixels.len(), 3 * 2 * 3);
786 assert_eq!(pixel(&image, 0, 0), [68, 1, 84]);
787 assert!(image
788 .pixels
789 .as_chunks::<3>()
790 .0
791 .iter()
792 .all(|rgb| *rgb == pixel(&image, 0, 0)));
793 }
794}