const WEIGHTS: [f64; 5] = [0.0448, 0.2856, 0.3001, 0.2363, 0.1333];
const K1: f64 = 0.01;
const K2: f64 = 0.03;
const L: f64 = 255.0;
const C1: f64 = (K1 * L) * (K1 * L); const C2: f64 = (K2 * L) * (K2 * L);
#[must_use]
pub fn downsample_2x(pixels: &[u8], width: u32, height: u32) -> (Vec<u8>, u32, u32) {
let w = width as usize;
let h = height as usize;
let new_w = w / 2;
let new_h = h / 2;
if new_w == 0 || new_h == 0 {
return (pixels.to_vec(), width, height);
}
let mut out = Vec::with_capacity(new_w * new_h);
for by in 0..new_h {
for bx in 0..new_w {
let y0 = by * 2;
let y1 = y0 + 1;
let x0 = bx * 2;
let x1 = x0 + 1;
let sum = pixels[y0 * w + x0] as u32
+ pixels[y0 * w + x1] as u32
+ pixels[y1 * w + x0] as u32
+ pixels[y1 * w + x1] as u32;
out.push((sum / 4) as u8);
}
}
(out, new_w as u32, new_h as u32)
}
fn ssim_components(ref_frame: &[u8], cmp_frame: &[u8], n: usize) -> (f64, f64) {
if n == 0 {
return (1.0, 1.0);
}
let inv_n = 1.0 / n as f64;
let mu_x: f64 = ref_frame.iter().map(|&v| v as f64).sum::<f64>() * inv_n;
let mu_y: f64 = cmp_frame.iter().map(|&v| v as f64).sum::<f64>() * inv_n;
let mut var_x = 0.0_f64;
let mut var_y = 0.0_f64;
let mut cov_xy = 0.0_f64;
for (&x, &y) in ref_frame.iter().zip(cmp_frame.iter()) {
let dx = x as f64 - mu_x;
let dy = y as f64 - mu_y;
var_x += dx * dx;
var_y += dy * dy;
cov_xy += dx * dy;
}
var_x *= inv_n;
var_y *= inv_n;
cov_xy *= inv_n;
let luminance = (2.0 * mu_x * mu_y + C1) / (mu_x * mu_x + mu_y * mu_y + C1);
let contrast_structure = (2.0 * cov_xy + C2) / (var_x + var_y + C2);
(
luminance.clamp(0.0, 1.0),
contrast_structure.clamp(0.0, 1.0),
)
}
#[must_use]
pub fn ms_ssim(ref_frame: &[u8], cmp_frame: &[u8], width: u32, height: u32) -> f32 {
const NUM_SCALES: usize = 5;
if ref_frame.is_empty() || cmp_frame.is_empty() {
return 1.0;
}
let mut ref_scales: Vec<(Vec<u8>, u32, u32)> = Vec::with_capacity(NUM_SCALES);
let mut cmp_scales: Vec<(Vec<u8>, u32, u32)> = Vec::with_capacity(NUM_SCALES);
ref_scales.push((ref_frame.to_vec(), width, height));
cmp_scales.push((cmp_frame.to_vec(), width, height));
for s in 1..NUM_SCALES {
let (prev_ref, pw, ph) = &ref_scales[s - 1];
let (prev_cmp, _, _) = &cmp_scales[s - 1];
let (down_ref, nw, nh) = downsample_2x(prev_ref, *pw, *ph);
let (down_cmp, _, _) = downsample_2x(prev_cmp, *pw, *ph);
if nw == *pw && nh == *ph {
break;
}
ref_scales.push((down_ref, nw, nh));
cmp_scales.push((down_cmp, nw, nh));
}
let actual_scales = ref_scales.len();
let mut luminance = 1.0_f64;
let mut ms_ssim_acc = 1.0_f64;
for s in 0..actual_scales {
let (ref_s, w_s, h_s) = &ref_scales[s];
let (cmp_s, _, _) = &cmp_scales[s];
let n = (*w_s as usize) * (*h_s as usize);
let (l_s, cs_s) = ssim_components(ref_s, cmp_s, n);
let weight_idx = NUM_SCALES - 1 - (NUM_SCALES - 1 - s).min(NUM_SCALES - 1);
let w = WEIGHTS[weight_idx];
ms_ssim_acc *= cs_s.powf(w);
if s == actual_scales - 1 {
luminance = l_s.powf(w);
}
}
(luminance * ms_ssim_acc).clamp(0.0, 1.0) as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downsample_2x_uniform() {
let pixels = vec![200u8; 4 * 4];
let (out, nw, nh) = downsample_2x(&pixels, 4, 4);
assert_eq!(nw, 2);
assert_eq!(nh, 2);
assert_eq!(out.len(), 4);
for &v in &out {
assert_eq!(v, 200);
}
}
#[test]
fn test_downsample_2x_known_block() {
let pixels = vec![100u8, 200, 100, 200];
let (out, nw, nh) = downsample_2x(&pixels, 2, 2);
assert_eq!(nw, 1);
assert_eq!(nh, 1);
assert_eq!(out.len(), 1);
assert_eq!(out[0], 150);
}
#[test]
fn test_downsample_2x_dimensions() {
let pixels = vec![128u8; 8 * 6]; let (_, nw, nh) = downsample_2x(&pixels, 8, 6);
assert_eq!(nw, 4);
assert_eq!(nh, 3);
}
#[test]
fn test_downsample_2x_too_small_passthrough() {
let pixels = vec![42u8];
let (out, nw, nh) = downsample_2x(&pixels, 1, 1);
assert_eq!(nw, 1);
assert_eq!(nh, 1);
assert_eq!(out, vec![42u8]);
}
#[test]
fn test_ms_ssim_identical_frames() {
let w = 32u32;
let h = 32u32;
let pixels: Vec<u8> = (0..(w * h) as usize).map(|i| (i % 256) as u8).collect();
let score = ms_ssim(&pixels, &pixels, w, h);
assert!(
(score - 1.0).abs() < 0.01,
"Identical frames should score ≈ 1.0, got {score}"
);
}
#[test]
fn test_ms_ssim_completely_different() {
let w = 32u32;
let h = 32u32;
let ref_pixels = vec![0u8; (w * h) as usize];
let cmp_pixels = vec![255u8; (w * h) as usize];
let score = ms_ssim(&ref_pixels, &cmp_pixels, w, h);
assert!(
score < 0.9,
"Maximally different frames should score < 0.9, got {score}"
);
}
#[test]
fn test_ms_ssim_slightly_degraded() {
let w = 64u32;
let h = 64u32;
let ref_pixels: Vec<u8> = (0..(w * h) as usize)
.map(|i| ((i as f64 / (w * h) as f64) * 200.0) as u8)
.collect();
let cmp_pixels: Vec<u8> = ref_pixels
.iter()
.enumerate()
.map(|(i, &v)| {
let noise = (i % 5) as i16 - 2;
(v as i16 + noise).clamp(0, 255) as u8
})
.collect();
let score = ms_ssim(&ref_pixels, &cmp_pixels, w, h);
assert!(
score > 0.5,
"Slightly degraded should score > 0.5, got {score}"
);
assert!(score < 1.0, "Degraded should score < 1.0, got {score}");
}
#[test]
fn test_ms_ssim_score_in_range() {
let w = 16u32;
let h = 16u32;
let ref_pixels: Vec<u8> = (0..(w * h) as usize).map(|i| (i % 256) as u8).collect();
let cmp_pixels: Vec<u8> = ref_pixels.iter().map(|&v| v.saturating_add(10)).collect();
let score = ms_ssim(&ref_pixels, &cmp_pixels, w, h);
assert!(score >= 0.0 && score <= 1.0, "Score out of [0,1]: {score}");
}
#[test]
fn test_ms_ssim_empty_returns_one() {
let score = ms_ssim(&[], &[], 0, 0);
assert_eq!(score, 1.0);
}
#[test]
fn test_ms_ssim_degraded_lower_than_identical() {
let w = 32u32;
let h = 32u32;
let ref_pixels: Vec<u8> = (0..(w * h) as usize).map(|i| (i % 256) as u8).collect();
let perfect_score = ms_ssim(&ref_pixels, &ref_pixels, w, h);
let degraded: Vec<u8> = ref_pixels.iter().map(|&v| v.saturating_add(50)).collect();
let degraded_score = ms_ssim(&ref_pixels, °raded, w, h);
assert!(
degraded_score <= perfect_score,
"Degraded ({degraded_score}) should be ≤ perfect ({perfect_score})"
);
}
}