use crate::error::{Error, Result};
use crate::format::{PixelFormat, Sample};
use crate::image::{Channels, Image};
const WINDOW: usize = 11;
const SIGMA: f64 = 1.5;
const K1: f64 = 0.01;
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)
}
fn color_channels(channels: Channels) -> &'static [usize] {
match channels {
Channels::Gray => &[0],
Channels::Rgb | Channels::Rgba => &[0, 1, 2],
}
}
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)
}
}
}
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 {
let mut sum = 0.0;
let mut count = 0usize;
for y0 in 0..=(height - WINDOW as u32) {
for x0 in 0..=(width - WINDOW as u32) {
sum += ssim_at(window, c1, c2, x0, y0, &a, &b);
count += 1;
}
}
sum / count as f64
}
fn ssim_at<A, B>(
window: &[[f64; WINDOW]; WINDOW],
c1: f64,
c2: f64,
x0: u32,
y0: u32,
a: &A,
b: &B,
) -> 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);
}
}
((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))
}
#[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 { .. }));
}
}