use crate::error::{Error, Result};
use crate::format::{PixelFormat, Sample};
use crate::image::{Channels, Image};
pub(crate) const WINDOW: usize = 11;
const SIGMA: f64 = 1.5;
pub(crate) const K1: f64 = 0.01;
pub(crate) const K2: f64 = 0.03;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SsimMode {
#[default]
RgbAveraged,
Luma709,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SsimOptions {
pub mode: SsimMode,
}
pub fn ssim<F: PixelFormat>(
reference: &Image<F>,
distorted: &Image<F>,
opts: SsimOptions,
) -> Result<f64> {
if reference.dimensions() != distorted.dimensions() {
return Err(Error::DimensionMismatch {
a: reference.dimensions(),
b: distorted.dimensions(),
});
}
let (width, height) = reference.dimensions();
if width < WINDOW as u32 || height < WINDOW as u32 {
return Err(Error::ImageTooSmall(width, height, WINDOW as u32));
}
let window = gaussian_window();
let l = <F::Sample as Sample>::MAX;
let c1 = (K1 * l).powi(2);
let c2 = (K2 * l).powi(2);
let score = match opts.mode {
SsimMode::RgbAveraged => {
let channels = color_channels(F::CHANNELS);
let total: f64 = channels
.iter()
.map(|&c| {
ssim_map_mean(
width,
height,
&window,
c1,
c2,
|x, y| reference.sample_at(x, y, c),
|x, y| distorted.sample_at(x, y, c),
)
})
.sum();
total / channels.len() as f64
}
SsimMode::Luma709 => ssim_map_mean(
width,
height,
&window,
c1,
c2,
|x, y| luma(reference, x, y),
|x, y| luma(distorted, x, y),
),
};
Ok(score)
}
pub(crate) fn color_channels(channels: Channels) -> &'static [usize] {
match channels {
Channels::Gray => &[0],
Channels::Rgb | Channels::Rgba => &[0, 1, 2],
}
}
pub(crate) fn luma<F: PixelFormat>(img: &Image<F>, x: u32, y: u32) -> f64 {
match F::CHANNELS {
Channels::Gray => img.sample_at(x, y, 0),
Channels::Rgb | Channels::Rgba => {
0.2126 * img.sample_at(x, y, 0)
+ 0.7152 * img.sample_at(x, y, 1)
+ 0.0722 * img.sample_at(x, y, 2)
}
}
}
pub(crate) fn gaussian_window() -> [[f64; WINDOW]; WINDOW] {
let center = (WINDOW as f64 - 1.0) / 2.0;
let mut window = [[0.0f64; WINDOW]; WINDOW];
let mut sum = 0.0;
for (j, row) in window.iter_mut().enumerate() {
for (i, w) in row.iter_mut().enumerate() {
let dx = i as f64 - center;
let dy = j as f64 - center;
*w = (-(dx * dx + dy * dy) / (2.0 * (SIGMA * SIGMA))).exp();
sum += *w;
}
}
for row in &mut window {
for w in row {
*w /= sum;
}
}
window
}
fn ssim_map_mean(
width: u32,
height: u32,
window: &[[f64; WINDOW]; WINDOW],
c1: f64,
c2: f64,
a: impl Fn(u32, u32) -> f64,
b: impl Fn(u32, u32) -> f64,
) -> f64 {
ssim_cs_map_means(width, height, window, c1, c2, a, b).0
}
pub(crate) fn ssim_cs_map_means(
width: u32,
height: u32,
window: &[[f64; WINDOW]; WINDOW],
c1: f64,
c2: f64,
a: impl Fn(u32, u32) -> f64,
b: impl Fn(u32, u32) -> f64,
) -> (f64, f64) {
let (full, cs, map_w, map_h) = ssim_cs_maps(width, height, window, c1, c2, a, b);
let count = (map_w * map_h) as f64;
(
full.iter().sum::<f64>() / count,
cs.iter().sum::<f64>() / count,
)
}
pub(crate) fn ssim_cs_maps(
width: u32,
height: u32,
window: &[[f64; WINDOW]; WINDOW],
c1: f64,
c2: f64,
a: impl Fn(u32, u32) -> f64,
b: impl Fn(u32, u32) -> f64,
) -> (Vec<f64>, Vec<f64>, u32, u32) {
let map_w = width - WINDOW as u32 + 1;
let map_h = height - WINDOW as u32 + 1;
let mut full = Vec::with_capacity((map_w * map_h) as usize);
let mut cs = Vec::with_capacity((map_w * map_h) as usize);
for y0 in 0..map_h {
for x0 in 0..map_w {
let (f, c) = ssim_at(window, c1, c2, x0, y0, &a, &b);
full.push(f);
cs.push(c);
}
}
(full, cs, map_w, map_h)
}
fn ssim_at<A, B>(
window: &[[f64; WINDOW]; WINDOW],
c1: f64,
c2: f64,
x0: u32,
y0: u32,
a: &A,
b: &B,
) -> (f64, f64)
where
A: Fn(u32, u32) -> f64,
B: Fn(u32, u32) -> f64,
{
let mut mu_x = 0.0;
let mut mu_y = 0.0;
for (j, row) in window.iter().enumerate() {
for (i, &w) in row.iter().enumerate() {
let x = x0 + i as u32;
let y = y0 + j as u32;
mu_x += w * a(x, y);
mu_y += w * b(x, y);
}
}
let mut var_x = 0.0;
let mut var_y = 0.0;
let mut cov = 0.0;
for (j, row) in window.iter().enumerate() {
for (i, &w) in row.iter().enumerate() {
let x = x0 + i as u32;
let y = y0 + j as u32;
let dx = a(x, y) - mu_x;
let dy = b(x, y) - mu_y;
var_x += w * (dx * dx);
var_y += w * (dy * dy);
cov += w * (dx * dy);
}
}
let full = ((2.0 * (mu_x * mu_y) + c1) * (2.0 * cov + c2))
/ ((mu_x * mu_x + mu_y * mu_y + c1) * (var_x + var_y + c2));
let cs = (2.0 * cov + c2) / (var_x + var_y + c2);
(full, cs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::Image;
fn solid_srgb8(value: u8) -> Image<crate::format::Srgb8> {
Image::srgb8(11, 11, vec![value; 11 * 11 * 3]).unwrap()
}
#[test]
fn identical_images_are_one() {
let img = solid_srgb8(100);
let score = ssim(&img, &img, SsimOptions::default()).unwrap();
assert!((score - 1.0).abs() < 1e-12, "expected 1.0, got {score}");
}
#[test]
fn gaussian_window_sums_to_one_and_is_symmetric() {
let window = gaussian_window();
let sum: f64 = window.iter().flatten().sum();
assert!((sum - 1.0).abs() < 1e-12, "window sum was {sum}");
for j in 0..WINDOW {
for i in 0..WINDOW {
assert!((window[j][i] - window[i][j]).abs() < 1e-18);
assert!(window[5][5] >= window[j][i]);
}
}
}
#[test]
fn solid_offset_matches_closed_form() {
let reference = solid_srgb8(100);
let distorted = solid_srgb8(120);
let score = ssim(&reference, &distorted, SsimOptions::default()).unwrap();
let c1 = (K1 * 255.0_f64).powi(2);
let expected = (2.0 * 100.0 * 120.0 + c1) / (100.0 * 100.0 + 120.0 * 120.0 + c1);
assert!(
(score - expected).abs() < 1e-9,
"got {score}, want {expected}"
);
}
#[test]
fn distortion_lowers_the_score() {
let reference = solid_srgb8(128);
let mut data = vec![128u8; 11 * 11 * 3];
for (i, sample) in data.iter_mut().enumerate() {
if i % 7 == 0 {
*sample = 160;
}
}
let distorted = Image::srgb8(11, 11, data).unwrap();
let score = ssim(&reference, &distorted, SsimOptions::default()).unwrap();
assert!(score < 1.0, "distorted image scored {score}");
}
#[test]
fn luma_weights_blue_channel_lightly() {
let reference = solid_srgb8(128);
let mut data = vec![128u8; 11 * 11 * 3];
for px in data.chunks_mut(3) {
px[2] = 180; }
let distorted = Image::srgb8(11, 11, data).unwrap();
let rgb = ssim(&reference, &distorted, SsimOptions::default()).unwrap();
let luma = ssim(
&reference,
&distorted,
SsimOptions {
mode: SsimMode::Luma709,
},
)
.unwrap();
assert!(luma > rgb, "rgb={rgb}, luma={luma}");
}
#[test]
fn image_below_window_is_rejected() {
let img = Image::srgb8(10, 10, vec![0; 10 * 10 * 3]).unwrap();
let err = ssim(&img, &img, SsimOptions::default()).unwrap_err();
assert!(matches!(err, Error::ImageTooSmall(10, 10, 11)));
}
#[test]
fn dimension_mismatch_is_an_error() {
let a = Image::srgb8(11, 11, vec![0; 11 * 11 * 3]).unwrap();
let b = Image::srgb8(12, 11, vec![0; 12 * 11 * 3]).unwrap();
let err = ssim(&a, &b, SsimOptions::default()).unwrap_err();
assert!(matches!(err, Error::DimensionMismatch { .. }));
}
}