1pub mod butteraugli;
30pub mod dssim;
31pub mod icc;
32pub mod prelude;
33pub mod ssimulacra2;
34pub mod xyb;
35
36pub use icc::{ColorProfile, prepare_for_comparison, transform_to_srgb};
38
39use serde::{Deserialize, Serialize};
40
41pub use xyb::xyb_roundtrip;
43
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46pub struct MetricConfig {
47 pub dssim: bool,
49 pub ssimulacra2: bool,
51 pub butteraugli: bool,
53 pub psnr: bool,
55 pub xyb_roundtrip: bool,
63}
64
65impl MetricConfig {
66 #[must_use]
68 pub fn all() -> Self {
69 Self {
70 dssim: true,
71 ssimulacra2: true,
72 butteraugli: true,
73 psnr: true,
74 xyb_roundtrip: false,
75 }
76 }
77
78 #[must_use]
80 pub fn fast() -> Self {
81 Self {
82 dssim: false,
83 ssimulacra2: false,
84 butteraugli: false,
85 psnr: true,
86 xyb_roundtrip: false,
87 }
88 }
89
90 #[must_use]
92 pub fn perceptual() -> Self {
93 Self {
94 dssim: true,
95 ssimulacra2: true,
96 butteraugli: true,
97 psnr: false,
98 xyb_roundtrip: false,
99 }
100 }
101
102 #[must_use]
108 pub fn perceptual_xyb() -> Self {
109 Self {
110 dssim: true,
111 ssimulacra2: true,
112 butteraugli: true,
113 psnr: false,
114 xyb_roundtrip: true,
115 }
116 }
117
118 #[must_use]
120 pub fn ssimulacra2_only() -> Self {
121 Self {
122 dssim: false,
123 ssimulacra2: true,
124 butteraugli: false,
125 psnr: false,
126 xyb_roundtrip: false,
127 }
128 }
129
130 #[must_use]
132 pub fn with_xyb_roundtrip(mut self) -> Self {
133 self.xyb_roundtrip = true;
134 self
135 }
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140pub struct MetricResult {
141 pub dssim: Option<f64>,
143 pub ssimulacra2: Option<f64>,
145 pub butteraugli: Option<f64>,
147 pub psnr: Option<f64>,
149}
150
151impl MetricResult {
152 #[must_use]
154 pub fn perception_level(&self) -> Option<PerceptionLevel> {
155 self.dssim.map(PerceptionLevel::from_dssim)
156 }
157
158 #[must_use]
160 pub fn perception_level_ssimulacra2(&self) -> Option<PerceptionLevel> {
161 self.ssimulacra2.map(PerceptionLevel::from_ssimulacra2)
162 }
163
164 #[must_use]
166 pub fn perception_level_butteraugli(&self) -> Option<PerceptionLevel> {
167 self.butteraugli.map(PerceptionLevel::from_butteraugli)
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173pub enum PerceptionLevel {
174 Imperceptible,
176 Marginal,
178 Subtle,
180 Noticeable,
182 Degraded,
184}
185
186impl PerceptionLevel {
187 #[must_use]
189 pub fn from_dssim(dssim: f64) -> Self {
190 if dssim < 0.0003 {
191 Self::Imperceptible
192 } else if dssim < 0.0007 {
193 Self::Marginal
194 } else if dssim < 0.0015 {
195 Self::Subtle
196 } else if dssim < 0.003 {
197 Self::Noticeable
198 } else {
199 Self::Degraded
200 }
201 }
202
203 #[must_use]
206 pub fn from_ssimulacra2(score: f64) -> Self {
207 if score > 90.0 {
208 Self::Imperceptible
209 } else if score > 80.0 {
210 Self::Marginal
211 } else if score > 70.0 {
212 Self::Subtle
213 } else if score > 50.0 {
214 Self::Noticeable
215 } else {
216 Self::Degraded
217 }
218 }
219
220 #[must_use]
223 pub fn from_butteraugli(score: f64) -> Self {
224 if score < 1.0 {
225 Self::Imperceptible
226 } else if score < 2.0 {
227 Self::Marginal
228 } else if score < 3.0 {
229 Self::Subtle
230 } else if score < 5.0 {
231 Self::Noticeable
232 } else {
233 Self::Degraded
234 }
235 }
236
237 #[must_use]
239 pub fn max_dssim(self) -> f64 {
240 match self {
241 Self::Imperceptible => 0.0003,
242 Self::Marginal => 0.0007,
243 Self::Subtle => 0.0015,
244 Self::Noticeable => 0.003,
245 Self::Degraded => f64::INFINITY,
246 }
247 }
248
249 #[must_use]
251 pub fn min_ssimulacra2(self) -> f64 {
252 match self {
253 Self::Imperceptible => 90.0,
254 Self::Marginal => 80.0,
255 Self::Subtle => 70.0,
256 Self::Noticeable => 50.0,
257 Self::Degraded => f64::NEG_INFINITY,
258 }
259 }
260
261 #[must_use]
263 pub fn max_butteraugli(self) -> f64 {
264 match self {
265 Self::Imperceptible => 1.0,
266 Self::Marginal => 2.0,
267 Self::Subtle => 3.0,
268 Self::Noticeable => 5.0,
269 Self::Degraded => f64::INFINITY,
270 }
271 }
272
273 #[must_use]
275 pub fn code(self) -> &'static str {
276 match self {
277 Self::Imperceptible => "IMP",
278 Self::Marginal => "MAR",
279 Self::Subtle => "SUB",
280 Self::Noticeable => "NOT",
281 Self::Degraded => "DEG",
282 }
283 }
284}
285
286impl std::fmt::Display for PerceptionLevel {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 match self {
289 Self::Imperceptible => write!(f, "Imperceptible"),
290 Self::Marginal => write!(f, "Marginal"),
291 Self::Subtle => write!(f, "Subtle"),
292 Self::Noticeable => write!(f, "Noticeable"),
293 Self::Degraded => write!(f, "Degraded"),
294 }
295 }
296}
297
298#[must_use]
312pub fn calculate_psnr(reference: &[u8], test: &[u8], width: usize, height: usize) -> f64 {
313 assert_eq!(reference.len(), test.len());
314 assert_eq!(reference.len(), width * height * 3);
315
316 let mut mse_sum: f64 = 0.0;
317 let pixel_count = (width * height * 3) as f64;
318
319 for (r, t) in reference.iter().zip(test.iter()) {
320 let diff = f64::from(*r) - f64::from(*t);
321 mse_sum += diff * diff;
322 }
323
324 let mse = mse_sum / pixel_count;
325
326 if mse == 0.0 {
327 f64::INFINITY
328 } else {
329 10.0 * (255.0_f64 * 255.0 / mse).log10()
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_perception_level_thresholds() {
339 assert_eq!(
340 PerceptionLevel::from_dssim(0.0001),
341 PerceptionLevel::Imperceptible
342 );
343 assert_eq!(
344 PerceptionLevel::from_dssim(0.0003),
345 PerceptionLevel::Marginal
346 );
347 assert_eq!(
348 PerceptionLevel::from_dssim(0.0005),
349 PerceptionLevel::Marginal
350 );
351 assert_eq!(PerceptionLevel::from_dssim(0.0007), PerceptionLevel::Subtle);
352 assert_eq!(PerceptionLevel::from_dssim(0.001), PerceptionLevel::Subtle);
353 assert_eq!(
354 PerceptionLevel::from_dssim(0.0015),
355 PerceptionLevel::Noticeable
356 );
357 assert_eq!(
358 PerceptionLevel::from_dssim(0.002),
359 PerceptionLevel::Noticeable
360 );
361 assert_eq!(
362 PerceptionLevel::from_dssim(0.003),
363 PerceptionLevel::Degraded
364 );
365 assert_eq!(PerceptionLevel::from_dssim(0.01), PerceptionLevel::Degraded);
366 }
367
368 #[test]
369 fn test_psnr_identical() {
370 let data = vec![128u8; 100 * 100 * 3];
371 let psnr = calculate_psnr(&data, &data, 100, 100);
372 assert!(psnr.is_infinite());
373 }
374
375 #[test]
376 fn test_psnr_different() {
377 let reference = vec![100u8; 100 * 100 * 3];
378 let test = vec![110u8; 100 * 100 * 3];
379 let psnr = calculate_psnr(&reference, &test, 100, 100);
380 assert!(psnr > 28.0);
382 assert!(psnr < 29.0);
383 }
384
385 #[test]
386 fn test_metric_config_all() {
387 let config = MetricConfig::all();
388 assert!(config.dssim);
389 assert!(config.psnr);
390 }
391
392 #[test]
393 fn test_metric_config_fast() {
394 let config = MetricConfig::fast();
395 assert!(!config.dssim);
396 assert!(config.psnr);
397 }
398}