pub mod butteraugli;
pub mod dssim;
pub mod icc;
pub mod prelude;
pub mod ssimulacra2;
pub mod xyb;
pub use icc::{ColorProfile, prepare_for_comparison, transform_to_srgb};
use serde::{Deserialize, Serialize};
pub use xyb::xyb_roundtrip;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MetricConfig {
pub dssim: bool,
pub ssimulacra2: bool,
pub butteraugli: bool,
pub psnr: bool,
pub xyb_roundtrip: bool,
}
impl MetricConfig {
#[must_use]
pub fn all() -> Self {
Self {
dssim: true,
ssimulacra2: true,
butteraugli: true,
psnr: true,
xyb_roundtrip: false,
}
}
#[must_use]
pub fn fast() -> Self {
Self {
dssim: false,
ssimulacra2: false,
butteraugli: false,
psnr: true,
xyb_roundtrip: false,
}
}
#[must_use]
pub fn perceptual() -> Self {
Self {
dssim: true,
ssimulacra2: true,
butteraugli: true,
psnr: false,
xyb_roundtrip: false,
}
}
#[must_use]
pub fn perceptual_xyb() -> Self {
Self {
dssim: true,
ssimulacra2: true,
butteraugli: true,
psnr: false,
xyb_roundtrip: true,
}
}
#[must_use]
pub fn ssimulacra2_only() -> Self {
Self {
dssim: false,
ssimulacra2: true,
butteraugli: false,
psnr: false,
xyb_roundtrip: false,
}
}
#[must_use]
pub fn with_xyb_roundtrip(mut self) -> Self {
self.xyb_roundtrip = true;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MetricResult {
pub dssim: Option<f64>,
pub ssimulacra2: Option<f64>,
pub butteraugli: Option<f64>,
pub psnr: Option<f64>,
}
impl MetricResult {
#[must_use]
pub fn perception_level(&self) -> Option<PerceptionLevel> {
self.dssim.map(PerceptionLevel::from_dssim)
}
#[must_use]
pub fn perception_level_ssimulacra2(&self) -> Option<PerceptionLevel> {
self.ssimulacra2.map(PerceptionLevel::from_ssimulacra2)
}
#[must_use]
pub fn perception_level_butteraugli(&self) -> Option<PerceptionLevel> {
self.butteraugli.map(PerceptionLevel::from_butteraugli)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PerceptionLevel {
Imperceptible,
Marginal,
Subtle,
Noticeable,
Degraded,
}
impl PerceptionLevel {
#[must_use]
pub fn from_dssim(dssim: f64) -> Self {
if dssim < 0.0003 {
Self::Imperceptible
} else if dssim < 0.0007 {
Self::Marginal
} else if dssim < 0.0015 {
Self::Subtle
} else if dssim < 0.003 {
Self::Noticeable
} else {
Self::Degraded
}
}
#[must_use]
pub fn from_ssimulacra2(score: f64) -> Self {
if score > 90.0 {
Self::Imperceptible
} else if score > 80.0 {
Self::Marginal
} else if score > 70.0 {
Self::Subtle
} else if score > 50.0 {
Self::Noticeable
} else {
Self::Degraded
}
}
#[must_use]
pub fn from_butteraugli(score: f64) -> Self {
if score < 1.0 {
Self::Imperceptible
} else if score < 2.0 {
Self::Marginal
} else if score < 3.0 {
Self::Subtle
} else if score < 5.0 {
Self::Noticeable
} else {
Self::Degraded
}
}
#[must_use]
pub fn max_dssim(self) -> f64 {
match self {
Self::Imperceptible => 0.0003,
Self::Marginal => 0.0007,
Self::Subtle => 0.0015,
Self::Noticeable => 0.003,
Self::Degraded => f64::INFINITY,
}
}
#[must_use]
pub fn min_ssimulacra2(self) -> f64 {
match self {
Self::Imperceptible => 90.0,
Self::Marginal => 80.0,
Self::Subtle => 70.0,
Self::Noticeable => 50.0,
Self::Degraded => f64::NEG_INFINITY,
}
}
#[must_use]
pub fn max_butteraugli(self) -> f64 {
match self {
Self::Imperceptible => 1.0,
Self::Marginal => 2.0,
Self::Subtle => 3.0,
Self::Noticeable => 5.0,
Self::Degraded => f64::INFINITY,
}
}
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::Imperceptible => "IMP",
Self::Marginal => "MAR",
Self::Subtle => "SUB",
Self::Noticeable => "NOT",
Self::Degraded => "DEG",
}
}
}
impl std::fmt::Display for PerceptionLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Imperceptible => write!(f, "Imperceptible"),
Self::Marginal => write!(f, "Marginal"),
Self::Subtle => write!(f, "Subtle"),
Self::Noticeable => write!(f, "Noticeable"),
Self::Degraded => write!(f, "Degraded"),
}
}
}
#[must_use]
pub fn calculate_psnr(reference: &[u8], test: &[u8], width: usize, height: usize) -> f64 {
assert_eq!(reference.len(), test.len());
assert_eq!(reference.len(), width * height * 3);
let mut mse_sum: f64 = 0.0;
let pixel_count = (width * height * 3) as f64;
for (r, t) in reference.iter().zip(test.iter()) {
let diff = f64::from(*r) - f64::from(*t);
mse_sum += diff * diff;
}
let mse = mse_sum / pixel_count;
if mse == 0.0 {
f64::INFINITY
} else {
10.0 * (255.0_f64 * 255.0 / mse).log10()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perception_level_thresholds() {
assert_eq!(
PerceptionLevel::from_dssim(0.0001),
PerceptionLevel::Imperceptible
);
assert_eq!(
PerceptionLevel::from_dssim(0.0003),
PerceptionLevel::Marginal
);
assert_eq!(
PerceptionLevel::from_dssim(0.0005),
PerceptionLevel::Marginal
);
assert_eq!(PerceptionLevel::from_dssim(0.0007), PerceptionLevel::Subtle);
assert_eq!(PerceptionLevel::from_dssim(0.001), PerceptionLevel::Subtle);
assert_eq!(
PerceptionLevel::from_dssim(0.0015),
PerceptionLevel::Noticeable
);
assert_eq!(
PerceptionLevel::from_dssim(0.002),
PerceptionLevel::Noticeable
);
assert_eq!(
PerceptionLevel::from_dssim(0.003),
PerceptionLevel::Degraded
);
assert_eq!(PerceptionLevel::from_dssim(0.01), PerceptionLevel::Degraded);
}
#[test]
fn test_psnr_identical() {
let data = vec![128u8; 100 * 100 * 3];
let psnr = calculate_psnr(&data, &data, 100, 100);
assert!(psnr.is_infinite());
}
#[test]
fn test_psnr_different() {
let reference = vec![100u8; 100 * 100 * 3];
let test = vec![110u8; 100 * 100 * 3];
let psnr = calculate_psnr(&reference, &test, 100, 100);
assert!(psnr > 28.0);
assert!(psnr < 29.0);
}
#[test]
fn test_metric_config_all() {
let config = MetricConfig::all();
assert!(config.dssim);
assert!(config.psnr);
}
#[test]
fn test_metric_config_fast() {
let config = MetricConfig::fast();
assert!(!config.dssim);
assert!(config.psnr);
}
}