use crate::error::{Error, Result};
use crate::format::{PixelFormat, Sample};
use crate::image::Image;
use crate::ssim::{
K1, K2, SsimMode, WINDOW, color_channels, gaussian_window, luma, ssim_cs_map_means,
};
const MAX_SCALES: usize = 5;
const WEIGHTS: [f64; MAX_SCALES] = [0.0448, 0.2856, 0.3001, 0.2363, 0.1333];
const TERM_FLOOR: f64 = 1e-12;
#[derive(Debug, Clone, Copy, Default)]
pub struct MsssimOptions {
pub mode: SsimMode,
}
pub fn msssim<F: PixelFormat>(
reference: &Image<F>,
distorted: &Image<F>,
opts: MsssimOptions,
) -> 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 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| {
let r = channel_buffer(reference, c);
let d = channel_buffer(distorted, c);
msssim_of_signal(&r, &d, width, height, c1, c2)
})
.sum();
total / channels.len() as f64
}
SsimMode::Luma709 => {
let r = luma_buffer(reference);
let d = luma_buffer(distorted);
msssim_of_signal(&r, &d, width, height, c1, c2)
}
};
Ok(score)
}
fn channel_buffer<F: PixelFormat>(img: &Image<F>, c: usize) -> Vec<f64> {
let (width, height) = img.dimensions();
let mut buf = Vec::with_capacity(width as usize * height as usize);
for y in 0..height {
for x in 0..width {
buf.push(img.sample_at(x, y, c));
}
}
buf
}
fn luma_buffer<F: PixelFormat>(img: &Image<F>) -> Vec<f64> {
let (width, height) = img.dimensions();
let mut buf = Vec::with_capacity(width as usize * height as usize);
for y in 0..height {
for x in 0..width {
buf.push(luma(img, x, y));
}
}
buf
}
fn msssim_of_signal(
reference: &[f64],
distorted: &[f64],
width: u32,
height: u32,
c1: f64,
c2: f64,
) -> f64 {
let window = gaussian_window();
let scales = num_scales(width, height);
let weights = scale_weights(scales);
let mut r = reference.to_vec();
let mut d = distorted.to_vec();
let (mut w, mut h) = (width, height);
let mut product = 1.0;
for (scale, &weight) in weights.iter().enumerate() {
let stride = w as usize;
let (full, cs) = ssim_cs_map_means(
w,
h,
&window,
c1,
c2,
|x, y| r[y as usize * stride + x as usize],
|x, y| d[y as usize * stride + x as usize],
);
let term = if scale == scales - 1 { full } else { cs };
product *= term.max(TERM_FLOOR).powf(weight);
if scale + 1 < scales {
let (rn, nw, nh) = downsample(&r, w, h);
let (dn, _, _) = downsample(&d, w, h);
r = rn;
d = dn;
w = nw;
h = nh;
}
}
product
}
fn num_scales(mut width: u32, mut height: u32) -> usize {
let mut scales = 1;
while scales < MAX_SCALES {
let (nw, nh) = (width / 2, height / 2);
if nw < WINDOW as u32 || nh < WINDOW as u32 {
break;
}
width = nw;
height = nh;
scales += 1;
}
scales
}
fn scale_weights(scales: usize) -> Vec<f64> {
let used = &WEIGHTS[..scales];
let sum: f64 = used.iter().sum();
used.iter().map(|w| w / sum).collect()
}
fn downsample(src: &[f64], width: u32, height: u32) -> (Vec<f64>, u32, u32) {
let (nw, nh) = (width / 2, height / 2);
let stride = width as usize;
let out_stride = nw as usize;
let mut out = vec![0.0; out_stride * nh as usize];
for j in 0..nh as usize {
for i in 0..nw as usize {
let (x0, y0) = (2 * i, 2 * j);
let sum = src[y0 * stride + x0]
+ src[y0 * stride + x0 + 1]
+ src[(y0 + 1) * stride + x0]
+ src[(y0 + 1) * stride + x0 + 1];
out[j * out_stride + i] = sum / 4.0;
}
}
(out, nw, nh)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::Image;
fn solid_srgb8(width: u32, height: u32, value: u8) -> Image<crate::format::Srgb8> {
Image::srgb8(width, height, vec![value; (width * height * 3) as usize]).unwrap()
}
#[test]
fn identical_images_are_one() {
let img = solid_srgb8(64, 64, 100);
let score = msssim(&img, &img, MsssimOptions::default()).unwrap();
assert!((score - 1.0).abs() < 1e-12, "expected 1.0, got {score}");
}
#[test]
fn uses_five_scales_only_once_large_enough() {
assert_eq!(num_scales(175, 175), 4);
assert_eq!(num_scales(176, 176), 5);
assert_eq!(num_scales(11, 11), 1);
}
#[test]
fn single_scale_matches_plain_ssim() {
let reference = solid_srgb8(16, 16, 100);
let distorted = solid_srgb8(16, 16, 120);
assert_eq!(num_scales(16, 16), 1);
let ms = msssim(&reference, &distorted, MsssimOptions::default()).unwrap();
let plain =
crate::ssim::ssim(&reference, &distorted, crate::ssim::SsimOptions::default()).unwrap();
assert!((ms - plain).abs() < 1e-12, "ms={ms}, plain={plain}");
}
#[test]
fn distortion_lowers_the_score() {
let reference = solid_srgb8(64, 64, 128);
let mut data = vec![128u8; 64 * 64 * 3];
for (i, sample) in data.iter_mut().enumerate() {
if i % 7 == 0 {
*sample = 170;
}
}
let distorted = Image::srgb8(64, 64, data).unwrap();
let score = msssim(&reference, &distorted, MsssimOptions::default()).unwrap();
assert!(score < 1.0, "distorted image scored {score}");
}
#[test]
fn image_below_window_is_rejected() {
let img = Image::srgb8(10, 10, vec![0; 10 * 10 * 3]).unwrap();
let err = msssim(&img, &img, MsssimOptions::default()).unwrap_err();
assert!(matches!(err, Error::ImageTooSmall(10, 10, 11)));
}
#[test]
fn dimension_mismatch_is_an_error() {
let a = Image::srgb8(16, 16, vec![0; 16 * 16 * 3]).unwrap();
let b = Image::srgb8(16, 12, vec![0; 16 * 12 * 3]).unwrap();
let err = msssim(&a, &b, MsssimOptions::default()).unwrap_err();
assert!(matches!(err, Error::DimensionMismatch { .. }));
}
}