#![allow(dead_code)]
use iqa::{Error, PixelFormat};
pub use iqa::{Gray8, Gray16, Image, Rgba8, Rgba16, Srgb8, Srgb16};
trait ScaledSample: Copy + Into<f64> {
const SCALE: f64;
const MAXV: f64;
fn from_8bit(value8: f64) -> Self;
fn from_full(value: f64) -> Self;
}
impl ScaledSample for u8 {
const SCALE: f64 = 1.0;
const MAXV: f64 = 255.0;
fn from_8bit(value8: f64) -> Self {
Self::from_full(value8 * Self::SCALE)
}
fn from_full(value: f64) -> Self {
value.round().clamp(0.0, Self::MAXV) as u8
}
}
impl ScaledSample for u16 {
const SCALE: f64 = 257.0;
const MAXV: f64 = 65_535.0;
fn from_8bit(value8: f64) -> Self {
Self::from_full(value8 * Self::SCALE)
}
fn from_full(value: f64) -> Self {
value.round().clamp(0.0, Self::MAXV) as u16
}
}
fn grid<S>(
width: u32,
height: u32,
channels: usize,
mut f: impl FnMut(u32, u32, usize) -> S,
) -> Vec<S> {
let mut data = Vec::with_capacity(width as usize * height as usize * channels);
for y in 0..height {
for x in 0..width {
for c in 0..channels {
data.push(f(x, y, c));
}
}
}
data
}
pub fn srgb8(width: u32, height: u32, f: impl FnMut(u32, u32, usize) -> u8) -> Image<Srgb8> {
Image::srgb8(width, height, grid(width, height, 3, f)).unwrap()
}
pub fn srgb16(width: u32, height: u32, f: impl FnMut(u32, u32, usize) -> u16) -> Image<Srgb16> {
Image::srgb16(width, height, grid(width, height, 3, f)).unwrap()
}
pub fn gray8(width: u32, height: u32, mut f: impl FnMut(u32, u32) -> u8) -> Image<Gray8> {
Image::gray8(width, height, grid(width, height, 1, |x, y, _| f(x, y))).unwrap()
}
pub fn gray16(width: u32, height: u32, mut f: impl FnMut(u32, u32) -> u16) -> Image<Gray16> {
Image::gray16(width, height, grid(width, height, 1, |x, y, _| f(x, y))).unwrap()
}
pub fn rgba8(width: u32, height: u32, f: impl FnMut(u32, u32, usize) -> u8) -> Image<Rgba8> {
Image::rgba8(width, height, grid(width, height, 4, f)).unwrap()
}
pub fn rgba16(width: u32, height: u32, f: impl FnMut(u32, u32, usize) -> u16) -> Image<Rgba16> {
Image::rgba16(width, height, grid(width, height, 4, f)).unwrap()
}
fn hash3(x: u32, y: u32, c: u32) -> u32 {
let mut h =
x.wrapping_mul(0x9E37_79B1) ^ y.wrapping_mul(0x85EB_CA77) ^ c.wrapping_mul(0xC2B2_AE3D);
h ^= h >> 15;
h = h.wrapping_mul(0x2545_F491);
h ^= h >> 13;
h
}
fn delta(x: u32, y: u32, c: usize) -> f64 {
(hash3(x, y, c as u32) % 20_001) as f64 / 10_000.0 - 1.0
}
fn gradient_8bit(x: u32, y: u32, c: usize) -> f64 {
(96 + ((x * 2 + y * 3 + c as u32 * 13) % 64)) as f64
}
pub trait Fixture: PixelFormat + Sized {
fn base(width: u32, height: u32) -> Image<Self>;
fn solid(width: u32, height: u32, value8: f64) -> Image<Self>;
fn distort(base: &Image<Self>, amplitude: f64) -> Image<Self>;
fn alpha_variant(base: &Image<Self>) -> Option<Image<Self>>;
}
macro_rules! impl_fixture {
($fmt:ty, $ctor:ident, $sample:ty) => {
impl Fixture for $fmt {
fn base(width: u32, height: u32) -> Image<$fmt> {
let ch = <$fmt as PixelFormat>::CHANNELS.count();
let data = grid::<$sample>(width, height, ch, |x, y, c| {
if c == 3 {
<$sample as ScaledSample>::from_8bit(255.0)
} else {
<$sample as ScaledSample>::from_8bit(gradient_8bit(x, y, c))
}
});
Image::$ctor(width, height, data).unwrap()
}
fn solid(width: u32, height: u32, value8: f64) -> Image<$fmt> {
let ch = <$fmt as PixelFormat>::CHANNELS.count();
let data = grid::<$sample>(width, height, ch, |_, _, c| {
let v = if c == 3 { 255.0 } else { value8 };
<$sample as ScaledSample>::from_8bit(v)
});
Image::$ctor(width, height, data).unwrap()
}
fn distort(base: &Image<$fmt>, amplitude: f64) -> Image<$fmt> {
let ch = <$fmt as PixelFormat>::CHANNELS.count();
let w = base.width();
let src = base.samples();
let scale = <$sample as ScaledSample>::SCALE;
let data = grid::<$sample>(base.width(), base.height(), ch, |x, y, c| {
let idx = (y as usize * w as usize + x as usize) * ch + c;
if c == 3 {
src[idx]
} else {
let orig: f64 = src[idx].into();
<$sample as ScaledSample>::from_full(
orig + amplitude * scale * delta(x, y, c),
)
}
});
Image::$ctor(base.width(), base.height(), data).unwrap()
}
fn alpha_variant(base: &Image<$fmt>) -> Option<Image<$fmt>> {
let ch = <$fmt as PixelFormat>::CHANNELS.count();
if ch < 4 {
return None;
}
let w = base.width();
let src = base.samples();
let data = grid::<$sample>(base.width(), base.height(), ch, |x, y, c| {
let idx = (y as usize * w as usize + x as usize) * ch + c;
if c == 3 {
<$sample as ScaledSample>::from_8bit(((x * 7 + y) % 256) as f64)
} else {
src[idx]
}
});
Some(Image::$ctor(base.width(), base.height(), data).unwrap())
}
}
};
}
impl_fixture!(Srgb8, srgb8, u8);
impl_fixture!(Srgb16, srgb16, u16);
impl_fixture!(Gray8, gray8, u8);
impl_fixture!(Gray16, gray16, u16);
impl_fixture!(Rgba8, rgba8, u8);
impl_fixture!(Rgba16, rgba16, u16);
pub trait Metric<F: PixelFormat> {
const NAME: &'static str;
const IDENTITY_SCORE: f64;
const HIGHER_IS_BETTER: bool;
const SYMMETRIC: bool;
const MIN_DIM: u32;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64>;
}
#[cfg(feature = "psnr")]
pub struct PsnrRgbAvg;
#[cfg(feature = "psnr")]
impl<F: PixelFormat> Metric<F> for PsnrRgbAvg {
const NAME: &'static str = "psnr (rgb-averaged)";
const IDENTITY_SCORE: f64 = f64::INFINITY;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 1;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::psnr(
reference,
distorted,
iqa::PsnrOptions {
mode: iqa::PsnrMode::RgbAveraged,
},
)
}
}
#[cfg(feature = "psnr")]
pub struct PsnrLuma;
#[cfg(feature = "psnr")]
impl<F: PixelFormat> Metric<F> for PsnrLuma {
const NAME: &'static str = "psnr (luma709)";
const IDENTITY_SCORE: f64 = f64::INFINITY;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 1;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::psnr(
reference,
distorted,
iqa::PsnrOptions {
mode: iqa::PsnrMode::Luma709,
},
)
}
}
#[cfg(feature = "ssim")]
pub struct SsimRgbAvg;
#[cfg(feature = "ssim")]
impl<F: PixelFormat> Metric<F> for SsimRgbAvg {
const NAME: &'static str = "ssim (rgb-averaged)";
const IDENTITY_SCORE: f64 = 1.0;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 11;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::ssim(
reference,
distorted,
iqa::SsimOptions {
mode: iqa::SsimMode::RgbAveraged,
},
)
}
}
#[cfg(feature = "ssim")]
pub struct SsimLuma;
#[cfg(feature = "ssim")]
impl<F: PixelFormat> Metric<F> for SsimLuma {
const NAME: &'static str = "ssim (luma709)";
const IDENTITY_SCORE: f64 = 1.0;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 11;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::ssim(
reference,
distorted,
iqa::SsimOptions {
mode: iqa::SsimMode::Luma709,
},
)
}
}
#[cfg(feature = "dssim")]
pub struct DssimRgbAvg;
#[cfg(feature = "dssim")]
impl<F: PixelFormat> Metric<F> for DssimRgbAvg {
const NAME: &'static str = "dssim (rgb-averaged)";
const IDENTITY_SCORE: f64 = 0.0;
const HIGHER_IS_BETTER: bool = false;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 11;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::dssim(
reference,
distorted,
iqa::DssimOptions {
mode: iqa::SsimMode::RgbAveraged,
},
)
}
}
#[cfg(feature = "dssim")]
pub struct DssimLuma;
#[cfg(feature = "dssim")]
impl<F: PixelFormat> Metric<F> for DssimLuma {
const NAME: &'static str = "dssim (luma709)";
const IDENTITY_SCORE: f64 = 0.0;
const HIGHER_IS_BETTER: bool = false;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 11;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::dssim(
reference,
distorted,
iqa::DssimOptions {
mode: iqa::SsimMode::Luma709,
},
)
}
}
#[cfg(feature = "ms-ssim")]
pub struct MsssimRgbAvg;
#[cfg(feature = "ms-ssim")]
impl<F: PixelFormat> Metric<F> for MsssimRgbAvg {
const NAME: &'static str = "ms-ssim (rgb-averaged)";
const IDENTITY_SCORE: f64 = 1.0;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 11;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::msssim(
reference,
distorted,
iqa::MsssimOptions {
mode: iqa::SsimMode::RgbAveraged,
},
)
}
}
#[cfg(feature = "psnr-hvs-m")]
pub struct PsnrHvsMRgbAvg;
#[cfg(feature = "psnr-hvs-m")]
impl<F: PixelFormat> Metric<F> for PsnrHvsMRgbAvg {
const NAME: &'static str = "psnr-hvs-m (rgb-averaged)";
const IDENTITY_SCORE: f64 = f64::INFINITY;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 8;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::psnr_hvs_m(
reference,
distorted,
iqa::PsnrHvsOptions {
mode: iqa::PsnrHvsMode::RgbAveraged,
},
)
}
}
#[cfg(feature = "ms-ssim")]
pub struct MsssimLuma;
#[cfg(feature = "ms-ssim")]
impl<F: PixelFormat> Metric<F> for MsssimLuma {
const NAME: &'static str = "ms-ssim (luma709)";
const IDENTITY_SCORE: f64 = 1.0;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 11;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::msssim(
reference,
distorted,
iqa::MsssimOptions {
mode: iqa::SsimMode::Luma709,
},
)
}
}
#[cfg(feature = "psnr-hvs-m")]
pub struct PsnrHvsMLuma;
#[cfg(feature = "psnr-hvs-m")]
impl<F: PixelFormat> Metric<F> for PsnrHvsMLuma {
const NAME: &'static str = "psnr-hvs-m (luma709)";
const IDENTITY_SCORE: f64 = f64::INFINITY;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 8;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::psnr_hvs_m(
reference,
distorted,
iqa::PsnrHvsOptions {
mode: iqa::PsnrHvsMode::Luma709,
},
)
}
}
#[cfg(feature = "ciede2000")]
pub struct Ciede2000;
#[cfg(feature = "ciede2000")]
impl<F: PixelFormat> Metric<F> for Ciede2000 {
const NAME: &'static str = "ciede2000";
const IDENTITY_SCORE: f64 = 0.0;
const HIGHER_IS_BETTER: bool = false;
const SYMMETRIC: bool = true;
const MIN_DIM: u32 = 1;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::ciede2000(reference, distorted, iqa::Ciede2000Options::default())
}
}
#[cfg(feature = "ssimulacra2")]
pub struct Ssim2;
#[cfg(feature = "ssimulacra2")]
impl<F: iqa::Ssimulacra2Input> Metric<F> for Ssim2 {
const NAME: &'static str = "ssimulacra2";
const IDENTITY_SCORE: f64 = 100.0;
const HIGHER_IS_BETTER: bool = true;
const SYMMETRIC: bool = false;
const MIN_DIM: u32 = 8;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::ssimulacra2(reference, distorted)
}
}
#[cfg(feature = "butteraugli")]
pub struct Butteraugli;
#[cfg(feature = "butteraugli")]
impl<F: iqa::ButteraugliInput> Metric<F> for Butteraugli {
const NAME: &'static str = "butteraugli";
const IDENTITY_SCORE: f64 = 0.0;
const HIGHER_IS_BETTER: bool = false;
const SYMMETRIC: bool = false;
const MIN_DIM: u32 = 8;
fn compute(reference: &Image<F>, distorted: &Image<F>) -> iqa::Result<f64> {
iqa::butteraugli(reference, distorted, iqa::ButteraugliOptions::default())
}
}
pub fn matches_identity(identity: f64, score: f64) -> bool {
if identity.is_infinite() {
score.is_infinite() && score.is_sign_positive() == identity.is_sign_positive()
} else {
(score - identity).abs() <= 1e-9
}
}
pub fn strictly_better(higher_is_better: bool, a: f64, b: f64) -> bool {
if higher_is_better { a > b } else { a < b }
}
pub fn not_better(higher_is_better: bool, a: f64, b: f64) -> bool {
!strictly_better(higher_is_better, a, b)
}
fn fmt_name<F: 'static>() -> &'static str {
std::any::type_name::<F>()
}
pub fn run_property_suite<F, M>()
where
F: Fixture,
M: Metric<F>,
{
check_identity::<F, M>();
check_determinism::<F, M>();
check_dimension_mismatch::<F, M>();
check_min_dimension::<F, M>();
check_monotonicity::<F, M>();
check_finite_and_bounded::<F, M>();
check_alpha_ignored::<F, M>();
if M::SYMMETRIC {
check_symmetry::<F, M>();
}
}
fn check_identity<F: Fixture, M: Metric<F>>() {
for (label, img) in [
("gradient", F::base(24, 24)),
("solid", F::solid(24, 24, 128.0)),
] {
let score = M::compute(&img, &img).unwrap_or_else(|e| {
panic!(
"{} [{}/{label}]: identical image errored: {e}",
M::NAME,
fmt_name::<F>()
)
});
assert!(
matches_identity(M::IDENTITY_SCORE, score),
"{} [{}/{label}]: identical inputs scored {score}, expected {}",
M::NAME,
fmt_name::<F>(),
M::IDENTITY_SCORE,
);
}
}
fn check_determinism<F: Fixture, M: Metric<F>>() {
let reference = F::base(32, 32);
let distorted = F::distort(&reference, 20.0);
let first = M::compute(&reference, &distorted).unwrap();
let second = M::compute(&reference, &distorted).unwrap();
assert_eq!(
first.to_bits(),
second.to_bits(),
"{} [{}]: nondeterministic: {first} then {second}",
M::NAME,
fmt_name::<F>(),
);
}
fn check_dimension_mismatch<F: Fixture, M: Metric<F>>() {
let a = F::base(32, 32);
let b = F::base(32, 24);
assert!(
matches!(M::compute(&a, &b), Err(Error::DimensionMismatch { .. })),
"{} [{}]: 32x32 vs 32x24 should fail with DimensionMismatch",
M::NAME,
fmt_name::<F>(),
);
}
fn check_min_dimension<F: Fixture, M: Metric<F>>() {
if M::MIN_DIM <= 1 {
return;
}
let d = M::MIN_DIM - 1;
let tiny = F::base(d, d);
assert!(
M::compute(&tiny, &tiny).is_err(),
"{} [{}]: accepted a {d}x{d} image below the {min}x{min} minimum",
M::NAME,
fmt_name::<F>(),
min = M::MIN_DIM,
);
}
fn check_monotonicity<F: Fixture, M: Metric<F>>() {
let amplitudes = [0.0, 8.0, 22.0, 55.0];
let base = F::base(32, 32);
let scores: Vec<f64> = amplitudes
.iter()
.map(|&| M::compute(&base, &F::distort(&base, amp)).unwrap())
.collect();
assert!(
matches_identity(M::IDENTITY_SCORE, scores[0]),
"{} [{}]: zero distortion scored {}, expected the perfect score",
M::NAME,
fmt_name::<F>(),
scores[0],
);
for pair in scores.windows(2) {
assert!(
not_better(M::HIGHER_IS_BETTER, pair[1], pair[0]),
"{} [{}]: increasing distortion improved the score: {scores:?}",
M::NAME,
fmt_name::<F>(),
);
}
assert!(
strictly_better(M::HIGHER_IS_BETTER, scores[1], scores[3]),
"{} [{}]: light distortion ({}) did not beat heavy distortion ({})",
M::NAME,
fmt_name::<F>(),
scores[1],
scores[3],
);
}
fn check_finite_and_bounded<F: Fixture, M: Metric<F>>() {
let base = F::base(24, 24);
for amplitude in [5.0, 25.0, 70.0] {
let score = M::compute(&base, &F::distort(&base, amplitude)).unwrap();
assert!(
score.is_finite(),
"{} [{}]: amplitude {amplitude} produced a non-finite score {score}",
M::NAME,
fmt_name::<F>(),
);
assert!(
not_better(M::HIGHER_IS_BETTER, score, M::IDENTITY_SCORE),
"{} [{}]: distorted score {score} beat the perfect score {}",
M::NAME,
fmt_name::<F>(),
M::IDENTITY_SCORE,
);
}
}
fn check_alpha_ignored<F: Fixture, M: Metric<F>>() {
let base = F::base(24, 24);
if let Some(variant) = F::alpha_variant(&base) {
let score = M::compute(&base, &variant).unwrap();
assert!(
matches_identity(M::IDENTITY_SCORE, score),
"{} [{}]: changing only the alpha channel changed the score to {score}",
M::NAME,
fmt_name::<F>(),
);
}
}
fn check_symmetry<F: Fixture, M: Metric<F>>() {
let a = F::base(24, 24);
let b = F::distort(&a, 18.0);
let forward = M::compute(&a, &b).unwrap();
let backward = M::compute(&b, &a).unwrap();
assert_eq!(
forward.to_bits(),
backward.to_bits(),
"{} [{}]: declared symmetric but compute(a,b)={forward} != compute(b,a)={backward}",
M::NAME,
fmt_name::<F>(),
);
}