use crate::core::image_view::OwnedImage;
use crate::core::scalar::Scalar;
#[derive(Debug, Clone)]
pub struct DiagnosticImage {
pub(crate) data: Vec<u8>,
width: usize,
height: usize,
}
impl DiagnosticImage {
pub fn new(width: usize, height: usize) -> Self {
Self {
data: vec![0u8; width * height * 4],
width,
height,
}
}
#[inline]
pub fn width(&self) -> usize {
self.width
}
#[inline]
pub fn height(&self) -> usize {
self.height
}
#[inline]
pub fn data(&self) -> &[u8] {
&self.data
}
#[inline]
pub fn into_data(self) -> Vec<u8> {
self.data
}
#[inline]
pub fn set_pixel(&mut self, x: usize, y: usize, rgba: [u8; 4]) {
if x < self.width && y < self.height {
let idx = (y * self.width + x) * 4;
self.data[idx..idx + 4].copy_from_slice(&rgba);
}
}
#[inline]
pub fn get_pixel(&self, x: usize, y: usize) -> [u8; 4] {
if x < self.width && y < self.height {
let idx = (y * self.width + x) * 4;
[
self.data[idx],
self.data[idx + 1],
self.data[idx + 2],
self.data[idx + 3],
]
} else {
[0, 0, 0, 0]
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Colormap {
Jet,
Hot,
Magma,
}
fn colormap_value(t: Scalar, cmap: Colormap) -> [u8; 4] {
let t = t.clamp(0.0, 1.0);
let (r, g, b) = match cmap {
Colormap::Jet => {
let r = (1.5 - (t - 0.75).abs() * 4.0).clamp(0.0, 1.0);
let g = (1.5 - (t - 0.5).abs() * 4.0).clamp(0.0, 1.0);
let b = (1.5 - (t - 0.25).abs() * 4.0).clamp(0.0, 1.0);
(r, g, b)
}
Colormap::Hot => {
let r = (t * 3.0).clamp(0.0, 1.0);
let g = ((t - 0.333) * 3.0).clamp(0.0, 1.0);
let b = ((t - 0.666) * 3.0).clamp(0.0, 1.0);
(r, g, b)
}
Colormap::Magma => {
let r = (t * 2.0).clamp(0.0, 1.0);
let g = ((t - 0.5) * 2.0).clamp(0.0, 1.0);
let b = (t * 1.5).clamp(0.0, 1.0);
(r, g, b)
}
};
[(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, 255]
}
pub fn response_heatmap(response: &OwnedImage<Scalar>, colormap: Colormap) -> DiagnosticImage {
let w = response.width();
let h = response.height();
let data = response.data();
let max = data.iter().copied().fold(0.0f32, Scalar::max);
let min = data.iter().copied().fold(Scalar::INFINITY, Scalar::min);
let range = (max - min).max(1e-8);
let mut img = DiagnosticImage::new(w, h);
for y in 0..h {
for x in 0..w {
let val = data[y * w + x];
let t = (val - min) / range;
img.set_pixel(x, y, colormap_value(t, colormap));
}
}
img
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagnostic_image_set_get() {
let mut img = DiagnosticImage::new(4, 4);
img.set_pixel(1, 2, [255, 0, 128, 255]);
assert_eq!(img.get_pixel(1, 2), [255, 0, 128, 255]);
assert_eq!(img.get_pixel(0, 0), [0, 0, 0, 0]);
}
#[test]
fn heatmap_dimensions() {
let response = OwnedImage::from_vec(vec![0.0f32; 16], 4, 4).unwrap();
let hm = response_heatmap(&response, Colormap::Jet);
assert_eq!(hm.width(), 4);
assert_eq!(hm.height(), 4);
assert_eq!(hm.data().len(), 4 * 4 * 4);
}
#[test]
fn heatmap_gradient_produces_varying_colors() {
let data: Vec<Scalar> = (0..100).map(|i| i as Scalar / 99.0).collect();
let response = OwnedImage::from_vec(data, 10, 10).unwrap();
let hm = response_heatmap(&response, Colormap::Hot);
let lo = hm.get_pixel(0, 0);
let hi = hm.get_pixel(9, 9);
let lo_sum: u32 = lo[0] as u32 + lo[1] as u32 + lo[2] as u32;
let hi_sum: u32 = hi[0] as u32 + hi[1] as u32 + hi[2] as u32;
assert!(hi_sum > lo_sum, "high value should be brighter");
}
}