#![cfg(feature = "ssim")]
mod common;
use common::*;
use iqa::{Error, SsimMode, SsimOptions, ssim};
const RGB_AVERAGED: SsimOptions = SsimOptions {
mode: SsimMode::RgbAveraged,
};
const LUMA709: SsimOptions = SsimOptions {
mode: SsimMode::Luma709,
};
fn assert_close(actual: f64, expected: f64, context: &str) {
assert!(
(actual - expected).abs() <= 1e-9,
"{context}: got {actual}, expected {expected}",
);
}
#[test]
fn identical_images_score_one() {
let rgb8 = Srgb8::base(16, 16);
let rgb16 = Srgb16::base(16, 16);
let gray8 = Gray8::base(16, 16);
let gray16 = Gray16::base(16, 16);
let rgba8 = Rgba8::base(16, 16);
let rgba16 = Rgba16::base(16, 16);
for opts in [RGB_AVERAGED, LUMA709] {
assert_close(ssim(&rgb8, &rgb8, opts).unwrap(), 1.0, "srgb8");
assert_close(ssim(&rgb16, &rgb16, opts).unwrap(), 1.0, "srgb16");
assert_close(ssim(&gray8, &gray8, opts).unwrap(), 1.0, "gray8");
assert_close(ssim(&gray16, &gray16, opts).unwrap(), 1.0, "gray16");
assert_close(ssim(&rgba8, &rgba8, opts).unwrap(), 1.0, "rgba8");
assert_close(ssim(&rgba16, &rgba16, opts).unwrap(), 1.0, "rgba16");
}
}
#[test]
fn uniform_inputs_match_the_closed_form() {
let reference = srgb8(16, 16, |_, _, _| 100);
let distorted = srgb8(16, 16, |_, _, _| 120);
let c1 = (0.01 * 255.0_f64).powi(2);
let expected = (2.0 * 100.0 * 120.0 + c1) / (100.0 * 100.0 + 120.0 * 120.0 + c1);
assert_close(
ssim(&reference, &distorted, RGB_AVERAGED).unwrap(),
expected,
"uniform 100 vs 120",
);
}
#[test]
fn argument_order_does_not_matter() {
let a = Srgb8::base(20, 20);
let b = Srgb8::distort(&a, 18.0);
for opts in [RGB_AVERAGED, LUMA709] {
let forward = ssim(&a, &b, opts).unwrap();
let backward = ssim(&b, &a, opts).unwrap();
assert_eq!(
forward.to_bits(),
backward.to_bits(),
"asymmetric: {opts:?}"
);
}
}
#[test]
fn quality_decreases_with_distortion() {
let base = Srgb8::base(32, 32);
let scores: Vec<f64> = [0.0, 8.0, 24.0, 60.0]
.iter()
.map(|&| ssim(&base, &Srgb8::distort(&base, amp), RGB_AVERAGED).unwrap())
.collect();
assert_close(scores[0], 1.0, "zero distortion");
for pair in scores.windows(2) {
assert!(
pair[1] <= pair[0],
"score increased with distortion: {scores:?}"
);
}
assert!(scores[1] > scores[3], "light should beat heavy: {scores:?}");
}
#[test]
fn luma_mode_is_lenient_on_blue() {
let reference = srgb8(16, 16, |_, _, _| 128);
let distorted = srgb8(16, 16, |_, _, c| if c == 2 { 200 } else { 128 });
let rgb = ssim(&reference, &distorted, RGB_AVERAGED).unwrap();
let luma = ssim(&reference, &distorted, LUMA709).unwrap();
assert!(luma > rgb, "rgb={rgb}, luma={luma}");
}
#[test]
fn grayscale_modes_agree() {
let reference = Gray8::base(16, 16);
let distorted = Gray8::distort(&reference, 20.0);
let rgb = ssim(&reference, &distorted, RGB_AVERAGED).unwrap();
let luma = ssim(&reference, &distorted, LUMA709).unwrap();
assert_eq!(rgb.to_bits(), luma.to_bits(), "gray modes differ");
assert!(rgb < 1.0, "distorted grayscale scored {rgb}");
}
#[test]
fn alpha_channel_is_ignored() {
let base = Rgba8::base(16, 16);
let variant = Rgba8::alpha_variant(&base).expect("rgba8 has alpha");
for opts in [RGB_AVERAGED, LUMA709] {
assert_close(
ssim(&base, &variant, opts).unwrap(),
1.0,
"alpha changed score",
);
}
}
#[test]
fn scores_never_exceed_one() {
let base = Srgb8::base(24, 24);
for amplitude in [5.0, 25.0, 70.0] {
let score = ssim(&base, &Srgb8::distort(&base, amplitude), RGB_AVERAGED).unwrap();
assert!(
score <= 1.0 + 1e-9 && score.is_finite(),
"amplitude {amplitude}: score {score} is out of range",
);
}
}
#[test]
fn images_below_11x11_are_rejected() {
for (w, h) in [(10, 10), (11, 10), (10, 11)] {
let tiny = srgb8(w, h, |_, _, _| 0);
assert!(
matches!(ssim(&tiny, &tiny, RGB_AVERAGED), Err(Error::ImageTooSmall(tw, th, 11)) if tw == w && th == h),
"{w}x{h} should be rejected",
);
}
}
#[test]
fn dimension_mismatch_is_an_error() {
let a = srgb8(16, 16, |_, _, _| 0);
let b = srgb8(16, 12, |_, _, _| 0);
assert!(matches!(
ssim(&a, &b, RGB_AVERAGED),
Err(Error::DimensionMismatch { .. }),
));
}