use crate::FixationPoint;
use anyhow::{Result, ensure};
use std::{fmt, str::FromStr};
#[derive(Clone, Debug, PartialEq)]
pub struct AutoGazeVisualization {
pub width: usize,
pub height: usize,
pub side_by_side_width: usize,
pub mask_rgba: Vec<u8>,
pub blend_rgba: Vec<u8>,
pub side_by_side_rgba: Vec<u8>,
pub mask_pixel_count: usize,
pub updated_pixel_count: usize,
}
impl AutoGazeVisualization {
pub fn output_rgba(&self) -> &[u8] {
&self.blend_rgba
}
pub fn mask_ratio(&self) -> f64 {
ratio(self.mask_pixel_count, self.width * self.height)
}
pub fn update_ratio(&self) -> f64 {
ratio(self.updated_pixel_count, self.width * self.height)
}
pub fn output_psnr_db(&self, input_rgba: &[u8]) -> Result<f64> {
rgba_psnr_db(input_rgba, &self.blend_rgba)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum AutoGazeVisualizationMode {
#[default]
FullBlend,
Interframe,
}
impl AutoGazeVisualizationMode {
pub const fn as_str(self) -> &'static str {
match self {
Self::FullBlend => "full-blend",
Self::Interframe => "interframe",
}
}
}
impl fmt::Display for AutoGazeVisualizationMode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for AutoGazeVisualizationMode {
type Err = String;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"blend" | "full" | "full-blend" | "alphablend" | "alpha-blend" | "alpha" => {
Ok(Self::FullBlend)
}
"interframe" | "inter-frame" | "video" | "video-encoding" | "delta" => {
Ok(Self::Interframe)
}
other => Err(format!("unsupported AutoGaze visualization mode `{other}`")),
}
}
}
#[derive(Clone, Debug)]
pub struct AutoGazeVisualizationState {
mode: AutoGazeVisualizationMode,
keyframe_duration: usize,
frame_index: usize,
interframe_output_rgba: Vec<u8>,
interframe_width: usize,
interframe_height: usize,
}
impl Default for AutoGazeVisualizationState {
fn default() -> Self {
Self::new(AutoGazeVisualizationMode::FullBlend, 30)
}
}
impl AutoGazeVisualizationState {
pub fn new(mode: AutoGazeVisualizationMode, keyframe_duration: usize) -> Self {
Self {
mode,
keyframe_duration: keyframe_duration.max(1),
frame_index: 0,
interframe_output_rgba: Vec::new(),
interframe_width: 0,
interframe_height: 0,
}
}
pub fn mode(&self) -> AutoGazeVisualizationMode {
self.mode
}
pub fn keyframe_duration(&self) -> usize {
self.keyframe_duration
}
pub fn configure(&mut self, mode: AutoGazeVisualizationMode, keyframe_duration: usize) {
if self.mode != mode {
self.reset();
}
self.mode = mode;
self.keyframe_duration = keyframe_duration.max(1);
}
pub fn reset(&mut self) {
self.frame_index = 0;
self.interframe_output_rgba.clear();
self.interframe_width = 0;
self.interframe_height = 0;
}
pub fn visualize_rgba(
&mut self,
rgba: &[u8],
width: usize,
height: usize,
points: &[FixationPoint],
cell_scale: f32,
blend_alpha: f32,
) -> Result<AutoGazeVisualization> {
let mask = mask_rgba_and_alpha(rgba, width, height, points, cell_scale)?;
let pixels = validate_rgba_dimensions(rgba, width, height)?;
let (output_rgba, updated_pixel_count) = match self.mode {
AutoGazeVisualizationMode::FullBlend => {
(blend_masked_rgba(rgba, &mask.alpha, blend_alpha), pixels)
}
AutoGazeVisualizationMode::Interframe => {
self.interframe_rgba(rgba, width, height, &mask.alpha)?
}
};
self.frame_index = self.frame_index.saturating_add(1);
build_visualization(
rgba,
width,
height,
mask.mask_rgba,
output_rgba,
mask.mask_pixel_count,
updated_pixel_count,
)
}
fn interframe_rgba(
&mut self,
rgba: &[u8],
width: usize,
height: usize,
alpha: &[u8],
) -> Result<(Vec<u8>, usize)> {
let pixels = validate_rgba_dimensions(rgba, width, height)?;
ensure!(
alpha.len() == pixels,
"interframe alpha mask length must match input frame"
);
let dimensions_changed = self.interframe_width != width || self.interframe_height != height;
let keyframe = dimensions_changed
|| self.interframe_output_rgba.len() != pixels * 4
|| self.frame_index == 0
|| self.frame_index.is_multiple_of(self.keyframe_duration);
let mut updated_pixel_count = if keyframe { pixels } else { 0 };
if keyframe {
self.interframe_output_rgba.clear();
self.interframe_output_rgba.extend_from_slice(rgba);
self.interframe_width = width;
self.interframe_height = height;
}
if !keyframe {
for (pixel, &mask) in alpha.iter().enumerate().take(pixels) {
let offset = pixel * 4;
if mask > 0 {
self.interframe_output_rgba[offset..offset + 4]
.copy_from_slice(&rgba[offset..offset + 4]);
updated_pixel_count += 1;
}
}
}
Ok((self.interframe_output_rgba.clone(), updated_pixel_count))
}
}
pub fn fixation_alpha_mask(
width: usize,
height: usize,
points: &[FixationPoint],
cell_scale: f32,
) -> Vec<u8> {
let width = width.max(1);
let height = height.max(1);
let mut alpha = vec![0u8; width * height];
for point in points {
if point.confidence <= 0.0 {
continue;
}
let bounds = point.scaled_bounds(cell_scale);
let (x0, x1) = pixel_range(bounds.x_min, bounds.x_max, width);
let (y0, y1) = pixel_range(bounds.y_min, bounds.y_max, height);
for y in y0..y1 {
let row = y * width;
for x in x0..x1 {
alpha[row + x] = 255;
}
}
}
alpha
}
pub fn fixation_scale_mask_rgba(
width: usize,
height: usize,
points: &[FixationPoint],
cell_scale: f32,
) -> Vec<u8> {
let width = width.max(1);
let height = height.max(1);
let mut rgba = vec![0u8; width * height * 4];
for pixel in rgba.chunks_exact_mut(4) {
pixel[3] = 255;
}
let mut ordered = points
.iter()
.copied()
.filter(|point| point.confidence > 0.0)
.collect::<Vec<_>>();
ordered.sort_by(|left, right| {
right
.cell_width()
.total_cmp(&left.cell_width())
.then_with(|| right.cell_height().total_cmp(&left.cell_height()))
});
for point in ordered {
let color = scale_color_for_point(point);
let bounds = point.scaled_bounds(cell_scale);
let (x0, x1) = pixel_range(bounds.x_min, bounds.x_max, width);
let (y0, y1) = pixel_range(bounds.y_min, bounds.y_max, height);
let rect = CellRect { x0, x1, y0, y1 };
fill_cell(&mut rgba, width, rect, color, 0.42);
stroke_cell(&mut rgba, width, rect, color);
}
rgba
}
#[derive(Clone, Copy)]
struct CellRect {
x0: usize,
x1: usize,
y0: usize,
y1: usize,
}
fn fill_cell(rgba: &mut [u8], width: usize, rect: CellRect, color: [u8; 3], opacity: f32) {
let opacity = opacity.clamp(0.0, 1.0);
for y in rect.y0..rect.y1 {
let row = y * width;
for x in rect.x0..rect.x1 {
let offset = (row + x) * 4;
for channel in 0..3 {
let overlay = color[channel] as f32;
rgba[offset + channel] = (overlay * opacity).round() as u8;
}
}
}
}
fn stroke_cell(rgba: &mut [u8], width: usize, rect: CellRect, color: [u8; 3]) {
if rect.x0 >= rect.x1 || rect.y0 >= rect.y1 {
return;
}
for x in rect.x0..rect.x1 {
write_mask_pixel(rgba, width, x, rect.y0, color);
write_mask_pixel(rgba, width, x, rect.y1 - 1, color);
}
for y in rect.y0..rect.y1 {
write_mask_pixel(rgba, width, rect.x0, y, color);
write_mask_pixel(rgba, width, rect.x1 - 1, y, color);
}
}
fn write_mask_pixel(rgba: &mut [u8], width: usize, x: usize, y: usize, color: [u8; 3]) {
let offset = (y * width + x) * 4;
if offset + 3 <= rgba.len() {
rgba[offset..offset + 3].copy_from_slice(&color);
}
}
pub fn visualize_fixations_rgba(
rgba: &[u8],
width: usize,
height: usize,
points: &[FixationPoint],
cell_scale: f32,
blend_alpha: f32,
) -> Result<AutoGazeVisualization> {
let mask_blend = mask_and_blend_rgba(rgba, width, height, points, cell_scale, blend_alpha)?;
let pixels = validate_rgba_dimensions(rgba, width, height)?;
build_visualization(
rgba,
width,
height,
mask_blend.mask_rgba,
mask_blend.blend_rgba,
mask_blend.mask_pixel_count,
pixels,
)
}
pub fn rgba_psnr_db(reference_rgba: &[u8], candidate_rgba: &[u8]) -> Result<f64> {
ensure!(
reference_rgba.len() == candidate_rgba.len(),
"PSNR inputs must have the same byte length"
);
ensure!(
reference_rgba.len().is_multiple_of(4),
"PSNR inputs must be RGBA buffers"
);
ensure!(!reference_rgba.is_empty(), "PSNR inputs must be nonempty");
let mut squared_error = 0.0f64;
let mut samples = 0usize;
for (reference, candidate) in reference_rgba
.chunks_exact(4)
.zip(candidate_rgba.chunks_exact(4))
{
for channel in 0..3 {
let diff = reference[channel] as f64 - candidate[channel] as f64;
squared_error += diff * diff;
samples += 1;
}
}
if squared_error == 0.0 {
return Ok(f64::INFINITY);
}
let mse = squared_error / samples.max(1) as f64;
Ok(10.0 * ((255.0 * 255.0) / mse).log10())
}
fn validate_rgba_dimensions(rgba: &[u8], width: usize, height: usize) -> Result<usize> {
let pixels = width
.checked_mul(height)
.ok_or_else(|| anyhow::anyhow!("visualization dimensions overflow"))?;
ensure!(
width > 0 && height > 0,
"visualization dimensions must be nonzero"
);
ensure!(
rgba.len() == pixels * 4,
"expected {} RGBA bytes for {}x{}, got {}",
pixels * 4,
width,
height,
rgba.len()
);
Ok(pixels)
}
struct MaskRgbaAndAlpha {
mask_rgba: Vec<u8>,
alpha: Vec<u8>,
mask_pixel_count: usize,
}
fn mask_rgba_and_alpha(
rgba: &[u8],
width: usize,
height: usize,
points: &[FixationPoint],
cell_scale: f32,
) -> Result<MaskRgbaAndAlpha> {
let _ = validate_rgba_dimensions(rgba, width, height)?;
let alpha = fixation_alpha_mask(width, height, points, cell_scale);
let mask_rgba = fixation_scale_mask_rgba(width, height, points, cell_scale);
let mask_pixel_count = alpha.iter().filter(|&&mask| mask > 0).count();
Ok(MaskRgbaAndAlpha {
mask_rgba,
alpha,
mask_pixel_count,
})
}
struct MaskBlendRgba {
mask_rgba: Vec<u8>,
blend_rgba: Vec<u8>,
mask_pixel_count: usize,
}
fn mask_and_blend_rgba(
rgba: &[u8],
width: usize,
height: usize,
points: &[FixationPoint],
cell_scale: f32,
blend_alpha: f32,
) -> Result<MaskBlendRgba> {
let mask = mask_rgba_and_alpha(rgba, width, height, points, cell_scale)?;
let blend_rgba = blend_masked_rgba(rgba, &mask.alpha, blend_alpha);
Ok(MaskBlendRgba {
mask_rgba: mask.mask_rgba,
blend_rgba,
mask_pixel_count: mask.mask_pixel_count,
})
}
fn blend_masked_rgba(rgba: &[u8], alpha: &[u8], blend_alpha: f32) -> Vec<u8> {
debug_assert_eq!(rgba.len(), alpha.len() * 4);
let pixels = alpha.len();
let mut blend_rgba = vec![0u8; pixels * 4];
let blend_alpha = blend_alpha.clamp(0.0, 1.0);
for (pixel, mask) in alpha.iter().copied().enumerate() {
let src = pixel * 4;
let overlay = if mask > 0 { blend_alpha } else { 0.0 };
for channel in 0..3 {
let base = rgba[src + channel] as f32;
blend_rgba[src + channel] = (base * (1.0 - overlay) + 255.0 * overlay).round() as u8;
}
blend_rgba[src + 3] = rgba[src + 3];
}
blend_rgba
}
fn build_visualization(
rgba: &[u8],
width: usize,
height: usize,
mask_rgba: Vec<u8>,
blend_rgba: Vec<u8>,
mask_pixel_count: usize,
updated_pixel_count: usize,
) -> Result<AutoGazeVisualization> {
let _ = validate_rgba_dimensions(rgba, width, height)?;
ensure!(
mask_rgba.len() == rgba.len(),
"mask RGBA byte length must match input frame"
);
ensure!(
blend_rgba.len() == rgba.len(),
"blend RGBA byte length must match input frame"
);
let side_by_side_width = width
.checked_mul(3)
.ok_or_else(|| anyhow::anyhow!("side-by-side visualization width overflow"))?;
let side_by_side_bytes = side_by_side_width
.checked_mul(height)
.and_then(|pixels| pixels.checked_mul(4))
.ok_or_else(|| anyhow::anyhow!("side-by-side visualization byte length overflow"))?;
let mut side_by_side_rgba = vec![0u8; side_by_side_bytes];
let row_bytes = width * 4;
let out_row_bytes = side_by_side_width * 4;
for y in 0..height {
let src = y * row_bytes;
let dst = y * out_row_bytes;
side_by_side_rgba[dst..dst + row_bytes].copy_from_slice(&rgba[src..src + row_bytes]);
side_by_side_rgba[dst + row_bytes..dst + 2 * row_bytes]
.copy_from_slice(&mask_rgba[src..src + row_bytes]);
side_by_side_rgba[dst + 2 * row_bytes..dst + 3 * row_bytes]
.copy_from_slice(&blend_rgba[src..src + row_bytes]);
}
Ok(AutoGazeVisualization {
width,
height,
side_by_side_width,
mask_rgba,
blend_rgba,
side_by_side_rgba,
mask_pixel_count,
updated_pixel_count,
})
}
fn ratio(count: usize, total: usize) -> f64 {
if total == 0 {
0.0
} else {
count as f64 / total as f64
}
}
fn scale_color_for_point(point: FixationPoint) -> [u8; 3] {
match point
.cell_grid()
.unwrap_or_else(|| nearest_scale_grid(point))
{
0..=2 => [255, 180, 0],
3..=4 => [60, 220, 120],
5..=7 => [0, 185, 255],
_ => [230, 110, 255],
}
}
fn nearest_scale_grid(point: FixationPoint) -> usize {
let recovered = 1.0 / point.cell_width().max(point.cell_height()).max(1.0e-6);
[2usize, 4, 7, 14]
.into_iter()
.min_by(|left, right| {
((*left as f32 - recovered).abs()).total_cmp(&(*right as f32 - recovered).abs())
})
.unwrap_or(14)
}
fn pixel_range(min: f32, max: f32, extent: usize) -> (usize, usize) {
let extent_f = extent as f32;
let mut start = (min.clamp(0.0, 1.0) * extent_f).floor() as usize;
let mut end = (max.clamp(0.0, 1.0) * extent_f).ceil() as usize;
start = start.min(extent.saturating_sub(1));
end = end.min(extent);
if end <= start {
end = (start + 1).min(extent);
}
(start, end)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draws_crisp_binary_cell_mask() {
let point = FixationPoint::with_extent(0.25, 0.25, 0.5, 0.5, 0.9);
let alpha = fixation_alpha_mask(8, 8, &[point], 1.0);
for y in 0..8 {
for x in 0..8 {
let expected = if x < 4 && y < 4 { 255 } else { 0 };
assert_eq!(alpha[y * 8 + x], expected, "pixel {x},{y}");
}
}
}
#[test]
fn draws_crisp_scale_colored_cells_with_fine_cells_on_top() {
let coarse = FixationPoint::with_grid_extent(0.5, 0.5, 1.0, 1.0, 0.9, 2);
let fine = FixationPoint::with_grid_extent(0.625, 0.625, 0.25, 0.25, 0.9, 4);
let rgba = fixation_scale_mask_rgba(8, 8, &[fine, coarse], 1.0);
assert_eq!(&rgba[0..4], &[255, 180, 0, 255]);
let fine_offset = (5 * 8 + 5) * 4;
assert_eq!(&rgba[fine_offset..fine_offset + 4], &[60, 220, 120, 255]);
}
#[test]
fn scale_colored_cell_fill_uses_topmost_scale_without_color_mixing() {
let coarse = FixationPoint::with_grid_extent(0.5, 0.5, 1.0, 1.0, 0.9, 2);
let mid = FixationPoint::with_grid_extent(0.5, 0.5, 0.5, 0.5, 0.9, 4);
let fine = FixationPoint::with_grid_extent(0.5, 0.5, 0.25, 0.25, 0.9, 7);
let rgba = fixation_scale_mask_rgba(16, 16, &[coarse, mid, fine], 1.0);
let fine_interior = (8 * 16 + 8) * 4;
assert_eq!(&rgba[fine_interior..fine_interior + 4], &[0, 78, 107, 255]);
let mid_only_interior = (5 * 16 + 5) * 4;
assert_eq!(
&rgba[mid_only_interior..mid_only_interior + 4],
&[25, 92, 50, 255]
);
}
#[test]
fn blends_selected_cells_with_white() {
let rgba = [100, 50, 0, 255, 10, 20, 30, 255];
let point = FixationPoint::with_extent(0.25, 0.5, 0.5, 1.0, 1.0);
let visualization =
visualize_fixations_rgba(&rgba, 2, 1, &[point], 1.0, 0.5).expect("visualize");
assert_eq!(&visualization.mask_rgba[0..4], &[255, 180, 0, 255]);
assert_eq!(&visualization.mask_rgba[4..8], &[0, 0, 0, 255]);
assert_eq!(&visualization.blend_rgba[0..4], &[178, 153, 128, 255]);
assert_eq!(&visualization.blend_rgba[4..8], &[10, 20, 30, 255]);
assert_eq!(visualization.mask_pixel_count, 1);
assert_eq!(visualization.updated_pixel_count, 2);
assert_eq!(visualization.mask_ratio(), 0.5);
assert_eq!(visualization.update_ratio(), 1.0);
}
#[test]
fn side_by_side_layout_uses_input_mask_output_columns() {
let rgba = [100, 50, 0, 255, 10, 20, 30, 255];
let point = FixationPoint::with_extent(0.25, 0.5, 0.5, 1.0, 1.0);
let visualization =
visualize_fixations_rgba(&rgba, 2, 1, &[point], 1.0, 0.5).expect("visualize");
assert_eq!(visualization.side_by_side_width, 6);
assert_eq!(&visualization.side_by_side_rgba[0..8], &rgba);
assert_eq!(
&visualization.side_by_side_rgba[8..16],
&visualization.mask_rgba
);
assert_eq!(
&visualization.side_by_side_rgba[16..24],
&visualization.blend_rgba
);
}
#[test]
fn side_by_side_buffer_has_exact_three_column_size_for_tall_frames() {
let rgba = vec![
10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 1, 2, 3, 255,
];
let visualization =
visualize_fixations_rgba(&rgba, 2, 2, &[], 1.0, 0.5).expect("visualize tall frame");
assert_eq!(visualization.side_by_side_width, 6);
assert_eq!(visualization.side_by_side_rgba.len(), 2 * 2 * 3 * 4);
}
#[test]
fn interframe_mode_preserves_unmasked_regions_until_keyframe() {
let point = FixationPoint::with_extent(0.25, 0.5, 0.5, 1.0, 1.0);
let mut state = AutoGazeVisualizationState::new(AutoGazeVisualizationMode::Interframe, 3);
let first = [10, 0, 0, 255, 20, 0, 0, 255];
let first_visualization = state
.visualize_rgba(&first, 2, 1, &[point], 1.0, 1.0)
.expect("first visualization");
assert_eq!(&first_visualization.blend_rgba[0..4], &[10, 0, 0, 255]);
assert_eq!(&first_visualization.blend_rgba[4..8], &[20, 0, 0, 255]);
assert_eq!(first_visualization.mask_ratio(), 0.5);
assert_eq!(first_visualization.update_ratio(), 1.0);
let second = [30, 0, 0, 255, 40, 0, 0, 255];
let second_visualization = state
.visualize_rgba(&second, 2, 1, &[point], 1.0, 1.0)
.expect("second visualization");
assert_eq!(
&second_visualization.blend_rgba[0..8],
&[30, 0, 0, 255, 20, 0, 0, 255]
);
assert_eq!(second_visualization.mask_ratio(), 0.5);
assert_eq!(second_visualization.update_ratio(), 0.5);
let third = [50, 0, 0, 255, 60, 0, 0, 255];
let third_visualization = state
.visualize_rgba(&third, 2, 1, &[], 1.0, 1.0)
.expect("third visualization");
assert_eq!(
&third_visualization.blend_rgba[0..8],
&[30, 0, 0, 255, 20, 0, 0, 255]
);
assert_eq!(third_visualization.mask_ratio(), 0.0);
assert_eq!(third_visualization.update_ratio(), 0.0);
let fourth = [70, 0, 0, 255, 80, 0, 0, 255];
let fourth_visualization = state
.visualize_rgba(&fourth, 2, 1, &[], 1.0, 1.0)
.expect("fourth visualization");
assert_eq!(
&fourth_visualization.blend_rgba[0..8],
&[70, 0, 0, 255, 80, 0, 0, 255]
);
assert_eq!(fourth_visualization.mask_ratio(), 0.0);
assert_eq!(fourth_visualization.update_ratio(), 1.0);
}
#[test]
fn interframe_updates_are_driven_by_alpha_not_visible_mask_color() {
let blue_scale = FixationPoint::with_grid_extent(0.5, 0.5, 1.0, 1.0, 1.0, 7);
let mut state = AutoGazeVisualizationState::new(AutoGazeVisualizationMode::Interframe, 10);
let first = [10, 0, 0, 255];
let first_visualization = state
.visualize_rgba(&first, 1, 1, &[blue_scale], 1.0, 1.0)
.expect("first visualization");
assert_eq!(&first_visualization.mask_rgba[0..4], &[0, 185, 255, 255]);
let second = [50, 0, 0, 255];
let second_visualization = state
.visualize_rgba(&second, 1, 1, &[blue_scale], 1.0, 1.0)
.expect("second visualization");
assert_eq!(&second_visualization.blend_rgba[0..4], &[50, 0, 0, 255]);
assert_eq!(second_visualization.update_ratio(), 1.0);
}
#[test]
fn full_blend_opacity_controls_white_overlay_strength() {
let point = FixationPoint::with_extent(0.5, 0.5, 1.0, 1.0, 1.0);
let rgba = [100, 50, 0, 200];
let mut state = AutoGazeVisualizationState::new(AutoGazeVisualizationMode::FullBlend, 10);
let transparent = state
.visualize_rgba(&rgba, 1, 1, &[point], 1.0, 0.0)
.expect("transparent visualization");
assert_eq!(&transparent.blend_rgba[0..4], &rgba);
let subtle = state
.visualize_rgba(&rgba, 1, 1, &[point], 1.0, 0.25)
.expect("subtle visualization");
assert_eq!(&subtle.blend_rgba[0..4], &[139, 101, 64, 200]);
}
#[test]
fn interframe_updates_copy_source_pixels_without_alpha_overlay() {
let point = FixationPoint::with_extent(0.5, 0.5, 1.0, 1.0, 1.0);
let mut state = AutoGazeVisualizationState::new(AutoGazeVisualizationMode::Interframe, 10);
let first = [10, 10, 10, 255];
state
.visualize_rgba(&first, 1, 1, &[], 1.0, 0.0)
.expect("initial keyframe");
let second = [100, 50, 0, 255];
let visualization = state
.visualize_rgba(&second, 1, 1, &[point], 1.0, 0.25)
.expect("interframe update");
assert_eq!(&visualization.blend_rgba[0..4], &second);
assert_eq!(visualization.update_ratio(), 1.0);
}
#[test]
fn psnr_is_infinite_for_identical_rgba_buffers() {
let rgba = [10, 20, 30, 255, 40, 50, 60, 255];
assert!(rgba_psnr_db(&rgba, &rgba).expect("psnr").is_infinite());
}
#[test]
fn psnr_uses_rgb_channels() {
let reference = [10, 20, 30, 0];
let candidate = [20, 20, 30, 255];
let psnr = rgba_psnr_db(&reference, &candidate).expect("psnr");
let expected = 10.0f64 * ((255.0f64 * 255.0f64) / (100.0f64 / 3.0f64)).log10();
assert!((psnr - expected).abs() < 1.0e-12);
}
}