#![cfg(feature = "ssim")]
use iqa::{Gray8, Image, Srgb8, Srgb16, SsimMode, SsimOptions, ssim};
const RGB_AVERAGED: SsimOptions = SsimOptions {
mode: SsimMode::RgbAveraged,
};
const LUMA709: SsimOptions = SsimOptions {
mode: SsimMode::Luma709,
};
const WINDOW: usize = 11;
const SIGMA: f64 = 1.5;
const K1: f64 = 0.01;
const K2: f64 = 0.03;
fn gaussian() -> [[f64; WINDOW]; WINDOW] {
let center = (WINDOW as f64 - 1.0) / 2.0;
let mut w = [[0.0f64; WINDOW]; WINDOW];
let mut sum = 0.0;
for (j, row) in w.iter_mut().enumerate() {
for (i, cell) in row.iter_mut().enumerate() {
let dx = i as f64 - center;
let dy = j as f64 - center;
*cell = (-(dx * dx + dy * dy) / (2.0 * SIGMA * SIGMA)).exp();
sum += *cell;
}
}
for row in &mut w {
for cell in row {
*cell /= sum;
}
}
w
}
fn mean_ssim(width: usize, height: usize, a: &[f64], b: &[f64], l: f64) -> f64 {
let w = gaussian();
let c1 = (K1 * l).powi(2);
let c2 = (K2 * l).powi(2);
let mut total = 0.0;
let mut count = 0usize;
for y0 in 0..=(height - WINDOW) {
for x0 in 0..=(width - WINDOW) {
let mut mu_a = 0.0;
let mut mu_b = 0.0;
for (j, row) in w.iter().enumerate() {
for (i, &wij) in row.iter().enumerate() {
let idx = (y0 + j) * width + (x0 + i);
mu_a += wij * a[idx];
mu_b += wij * b[idx];
}
}
let mut var_a = 0.0;
let mut var_b = 0.0;
let mut cov = 0.0;
for (j, row) in w.iter().enumerate() {
for (i, &wij) in row.iter().enumerate() {
let idx = (y0 + j) * width + (x0 + i);
let da = a[idx] - mu_a;
let db = b[idx] - mu_b;
var_a += wij * da * da;
var_b += wij * db * db;
cov += wij * da * db;
}
}
let s = ((2.0 * mu_a * mu_b + c1) * (2.0 * cov + c2))
/ ((mu_a * mu_a + mu_b * mu_b + c1) * (var_a + var_b + c2));
total += s;
count += 1;
}
}
total / count as f64
}
fn ref_sample(x: u32, y: u32, c: usize) -> u8 {
let v = match c {
0 => 40 + (x * 7 + y * 3) % 120,
1 => 30 + (x * 3 + y * 11) % 140,
_ => 50 + (x * 13 + y * 5) % 100,
};
v as u8
}
fn dist_sample(x: u32, y: u32, c: usize) -> u8 {
let perturb = match c {
0 => ((x + 2 * y) % 5) as i32 * 6 - 12,
1 => ((3 * x + y) % 7) as i32 * 4 - 12,
_ => ((x + y) % 3) as i32 * 9 - 9,
};
(ref_sample(x, y, c) as i32 + perturb).clamp(0, 255) as u8
}
fn build_srgb8(width: u32, height: u32, f: impl Fn(u32, u32, usize) -> u8) -> Image<Srgb8> {
let mut data = Vec::with_capacity((width * height) as usize * 3);
for y in 0..height {
for x in 0..width {
for c in 0..3 {
data.push(f(x, y, c));
}
}
}
Image::srgb8(width, height, data).unwrap()
}
fn build_srgb16(width: u32, height: u32, f: impl Fn(u32, u32, usize) -> u8) -> Image<Srgb16> {
let mut data = Vec::with_capacity((width * height) as usize * 3);
for y in 0..height {
for x in 0..width {
for c in 0..3 {
data.push(f(x, y, c) as u16 * 257);
}
}
}
Image::srgb16(width, height, data).unwrap()
}
fn build_gray8(width: u32, height: u32, f: impl Fn(u32, u32) -> u8) -> Image<Gray8> {
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
data.push(f(x, y));
}
}
Image::gray8(width, height, data).unwrap()
}
fn channel_plane(img: &Image<Srgb8>, c: usize) -> Vec<f64> {
let (w, h) = (img.width() as usize, img.height() as usize);
let s = img.samples();
let mut p = Vec::with_capacity(w * h);
for px in 0..w * h {
p.push(s[px * 3 + c] as f64);
}
p
}
fn luma_plane(img: &Image<Srgb8>) -> Vec<f64> {
let (w, h) = (img.width() as usize, img.height() as usize);
let s = img.samples();
let mut p = Vec::with_capacity(w * h);
for px in 0..w * h {
let i = px * 3;
p.push(0.2126 * s[i] as f64 + 0.7152 * s[i + 1] as f64 + 0.0722 * s[i + 2] as f64);
}
p
}
fn channel_plane16(img: &Image<Srgb16>, c: usize) -> Vec<f64> {
let (w, h) = (img.width() as usize, img.height() as usize);
let s = img.samples();
let mut p = Vec::with_capacity(w * h);
for px in 0..w * h {
p.push(s[px * 3 + c] as f64);
}
p
}
fn gray_plane(img: &Image<Gray8>) -> Vec<f64> {
img.samples().iter().map(|&v| v as f64).collect()
}
fn assert_close(got: f64, want: f64, context: &str) {
assert!(
(got - want).abs() <= 1e-9,
"{context}: iqa::ssim = {got}, independent reference = {want} (delta {})",
(got - want).abs(),
);
}
#[test]
fn rgb_averaged_matches_independent_reference() {
for (w, h) in [(11, 11), (16, 18)] {
let reference = build_srgb8(w, h, ref_sample);
let distorted = build_srgb8(w, h, dist_sample);
let got = ssim(&reference, &distorted, RGB_AVERAGED).unwrap();
let want = (0..3)
.map(|c| {
mean_ssim(
w as usize,
h as usize,
&channel_plane(&reference, c),
&channel_plane(&distorted, c),
255.0,
)
})
.sum::<f64>()
/ 3.0;
assert_close(got, want, &format!("rgb-averaged {w}x{h}"));
assert!(got < 1.0 && got > -1.0, "{w}x{h}: score {got} out of range");
}
}
#[test]
fn luma_matches_independent_reference() {
for (w, h) in [(11, 11), (16, 18)] {
let reference = build_srgb8(w, h, ref_sample);
let distorted = build_srgb8(w, h, dist_sample);
let got = ssim(&reference, &distorted, LUMA709).unwrap();
let want = mean_ssim(
w as usize,
h as usize,
&luma_plane(&reference),
&luma_plane(&distorted),
255.0,
);
assert_close(got, want, &format!("luma709 {w}x{h}"));
}
}
#[test]
fn grayscale_matches_independent_reference() {
let (w, h) = (14, 14);
let reference = build_gray8(w, h, |x, y| ref_sample(x, y, 0));
let distorted = build_gray8(w, h, |x, y| dist_sample(x, y, 0));
let got = ssim(&reference, &distorted, RGB_AVERAGED).unwrap();
let want = mean_ssim(
w as usize,
h as usize,
&gray_plane(&reference),
&gray_plane(&distorted),
255.0,
);
assert_close(got, want, "grayscale 14x14");
}
#[test]
fn sixteen_bit_matches_independent_reference() {
let (w, h) = (13, 15);
let reference = build_srgb16(w, h, ref_sample);
let distorted = build_srgb16(w, h, dist_sample);
let got = ssim(&reference, &distorted, RGB_AVERAGED).unwrap();
let want = (0..3)
.map(|c| {
mean_ssim(
w as usize,
h as usize,
&channel_plane16(&reference, c),
&channel_plane16(&distorted, c),
65535.0,
)
})
.sum::<f64>()
/ 3.0;
assert_close(got, want, "rgb-averaged 16-bit 13x15");
}