Skip to main content

codec_eval/metrics/
butteraugli.rs

1//! Butteraugli metric calculation.
2//!
3//! Butteraugli is a perceptual image quality metric developed by Google.
4//! Lower scores indicate better quality (more similar to reference).
5//!
6//! Score interpretation:
7//! - < 1.0: Imperceptible difference
8//! - < 2.0: Marginal difference
9//! - < 3.0: Subtle difference
10//! - < 5.0: Noticeable difference
11//! - >= 5.0: Degraded
12//!
13//! # ICC Profile Support
14//!
15//! Use [`calculate_butteraugli_icc`] for images with non-sRGB color profiles.
16
17use butteraugli::{ButteraugliParams, compute_butteraugli};
18
19use super::icc::ColorProfile;
20use crate::error::{Error, Result};
21
22/// Calculate Butteraugli distance between two images.
23///
24/// # Arguments
25///
26/// * `reference` - Reference image as RGB8 pixel data.
27/// * `test` - Test image as RGB8 pixel data.
28/// * `width` - Image width in pixels.
29/// * `height` - Image height in pixels.
30///
31/// # Returns
32///
33/// Butteraugli score where lower is better (<1.0 = imperceptible).
34///
35/// # Errors
36///
37/// Returns an error if the images have different sizes or if calculation fails.
38pub fn calculate_butteraugli(
39    reference: &[u8],
40    test: &[u8],
41    width: usize,
42    height: usize,
43) -> Result<f64> {
44    if reference.len() != test.len() {
45        return Err(Error::DimensionMismatch {
46            expected: (width, height),
47            actual: (test.len() / 3 / height, height),
48        });
49    }
50
51    let expected_len = width * height * 3;
52    if reference.len() != expected_len {
53        return Err(Error::MetricCalculation {
54            metric: "Butteraugli".to_string(),
55            reason: format!(
56                "Invalid image size: expected {} bytes, got {}",
57                expected_len,
58                reference.len()
59            ),
60        });
61    }
62
63    let params = ButteraugliParams::default();
64    let result = compute_butteraugli(reference, test, width, height, &params).map_err(|e| {
65        Error::MetricCalculation {
66            metric: "Butteraugli".to_string(),
67            reason: e.to_string(),
68        }
69    })?;
70
71    Ok(result.score)
72}
73
74/// Calculate Butteraugli with custom intensity target.
75///
76/// The intensity target affects how the metric perceives differences
77/// at different brightness levels.
78///
79/// # Arguments
80///
81/// * `reference` - Reference image as RGB8 pixel data.
82/// * `test` - Test image as RGB8 pixel data.
83/// * `width` - Image width in pixels.
84/// * `height` - Image height in pixels.
85/// * `intensity_target` - Target display intensity in nits (default: 80.0).
86///
87/// # Returns
88///
89/// Butteraugli score where lower is better.
90pub fn calculate_butteraugli_with_intensity(
91    reference: &[u8],
92    test: &[u8],
93    width: usize,
94    height: usize,
95    intensity_target: f32,
96) -> Result<f64> {
97    if reference.len() != test.len() {
98        return Err(Error::DimensionMismatch {
99            expected: (width, height),
100            actual: (test.len() / 3 / height, height),
101        });
102    }
103
104    let expected_len = width * height * 3;
105    if reference.len() != expected_len {
106        return Err(Error::MetricCalculation {
107            metric: "Butteraugli".to_string(),
108            reason: format!(
109                "Invalid image size: expected {} bytes, got {}",
110                expected_len,
111                reference.len()
112            ),
113        });
114    }
115
116    let params = ButteraugliParams::default().with_intensity_target(intensity_target);
117    let result = compute_butteraugli(reference, test, width, height, &params).map_err(|e| {
118        Error::MetricCalculation {
119            metric: "Butteraugli".to_string(),
120            reason: e.to_string(),
121        }
122    })?;
123
124    Ok(result.score)
125}
126
127/// Calculate Butteraugli with ICC profile support.
128///
129/// This function transforms both images to sRGB before comparison.
130///
131/// # Arguments
132///
133/// * `reference` - Reference image as RGB8 pixel data.
134/// * `reference_profile` - Color profile of the reference image.
135/// * `test` - Test image as RGB8 pixel data.
136/// * `test_profile` - Color profile of the test image.
137/// * `width` - Image width in pixels.
138/// * `height` - Image height in pixels.
139pub fn calculate_butteraugli_icc(
140    reference: &[u8],
141    reference_profile: &ColorProfile,
142    test: &[u8],
143    test_profile: &ColorProfile,
144    width: usize,
145    height: usize,
146) -> Result<f64> {
147    let (ref_srgb, test_srgb) =
148        super::icc::prepare_for_comparison(reference, reference_profile, test, test_profile)?;
149
150    calculate_butteraugli(&ref_srgb, &test_srgb, width, height)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_identical_images() {
159        let data: Vec<u8> = (0..100 * 100 * 3).map(|i| (i % 256) as u8).collect();
160        let score = calculate_butteraugli(&data, &data, 100, 100).unwrap();
161        // Identical images should have score close to 0
162        assert!(
163            score < 0.01,
164            "Identical images should have score ~0, got {score}"
165        );
166    }
167
168    #[test]
169    fn test_different_images() {
170        let ref_data: Vec<u8> = vec![100u8; 100 * 100 * 3];
171        let test_data: Vec<u8> = vec![200u8; 100 * 100 * 3];
172        let score = calculate_butteraugli(&ref_data, &test_data, 100, 100).unwrap();
173        // Very different images should have high score
174        assert!(
175            score > 1.0,
176            "Very different images should have high score, got {score}"
177        );
178    }
179
180    #[test]
181    fn test_size_mismatch() {
182        let small: Vec<u8> = vec![128u8; 50 * 50 * 3];
183        let large: Vec<u8> = vec![128u8; 100 * 100 * 3];
184        let result = calculate_butteraugli(&small, &large, 100, 100);
185        assert!(result.is_err());
186    }
187
188    #[test]
189    fn test_custom_intensity() {
190        let data: Vec<u8> = (0..100 * 100 * 3).map(|i| (i % 256) as u8).collect();
191        let score = calculate_butteraugli_with_intensity(&data, &data, 100, 100, 250.0).unwrap();
192        assert!(
193            score < 0.01,
194            "Identical images should have score ~0 at any intensity"
195        );
196    }
197}