#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::unreadable_literal)]
#![allow(clippy::inconsistent_digit_grouping)]
#![allow(clippy::excessive_precision)]
#![allow(clippy::suboptimal_flops)]
#![allow(clippy::many_single_char_names)]
#![allow(clippy::needless_range_loop)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::items_after_statements)]
#![allow(clippy::manual_saturating_arithmetic)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::missing_const_for_fn)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::if_not_else)]
#![allow(clippy::imprecise_flops)]
#![allow(clippy::implicit_saturating_sub)]
#![allow(clippy::useless_let_if_seq)]
#[cfg(feature = "internals")]
pub mod blur;
#[cfg(not(feature = "internals"))]
pub(crate) mod blur;
#[cfg(feature = "internals")]
pub mod consts;
#[cfg(not(feature = "internals"))]
pub(crate) mod consts;
mod diff;
#[cfg(feature = "internals")]
pub mod image;
#[cfg(not(feature = "internals"))]
pub(crate) mod image;
pub(crate) mod image_aligned;
#[cfg(feature = "internals")]
pub mod malta;
#[cfg(not(feature = "internals"))]
pub(crate) mod malta;
#[cfg(feature = "internals")]
pub mod mask;
#[cfg(not(feature = "internals"))]
pub(crate) mod mask;
#[cfg(feature = "internals")]
pub mod opsin;
#[cfg(not(feature = "internals"))]
pub(crate) mod opsin;
pub mod precompute;
pub use precompute::ButteraugliReference;
#[cfg(feature = "internals")]
pub mod psycho;
#[cfg(not(feature = "internals"))]
pub(crate) mod psycho;
pub(crate) mod xyb;
#[doc(hidden)]
pub mod reference_data;
pub use imgref::{Img, ImgRef, ImgVec};
pub use rgb::{RGB, RGB8};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ButteraugliError {
ImageTooSmall {
width: usize,
height: usize,
},
DimensionMismatch {
w1: usize,
h1: usize,
w2: usize,
h2: usize,
},
#[doc(hidden)]
InvalidDimensions {
width: usize,
height: usize,
},
#[doc(hidden)]
InvalidBufferSize {
expected: usize,
actual: usize,
},
}
impl std::fmt::Display for ButteraugliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ImageTooSmall { width, height } => {
write!(f, "image too small: {width}x{height} (minimum 8x8)")
}
Self::DimensionMismatch { w1, h1, w2, h2 } => {
write!(f, "image dimensions don't match: {w1}x{h1} vs {w2}x{h2}")
}
Self::InvalidDimensions { width, height } => {
write!(f, "invalid dimensions: {width}x{height} (minimum 8x8)")
}
Self::InvalidBufferSize { expected, actual } => {
write!(
f,
"buffer size {actual} doesn't match expected size {expected}"
)
}
}
}
}
impl std::error::Error for ButteraugliError {}
#[derive(Debug, Clone)]
pub struct ButteraugliParams {
hf_asymmetry: f32,
xmul: f32,
intensity_target: f32,
compute_diffmap: bool,
single_resolution: bool,
}
impl Default for ButteraugliParams {
fn default() -> Self {
Self {
hf_asymmetry: 1.0,
xmul: 1.0,
intensity_target: 80.0,
compute_diffmap: false,
single_resolution: false,
}
}
}
impl ButteraugliParams {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_intensity_target(mut self, intensity_target: f32) -> Self {
self.intensity_target = intensity_target;
self
}
#[must_use]
pub fn with_hf_asymmetry(mut self, hf_asymmetry: f32) -> Self {
self.hf_asymmetry = hf_asymmetry;
self
}
#[must_use]
pub fn with_xmul(mut self, xmul: f32) -> Self {
self.xmul = xmul;
self
}
#[must_use]
pub fn with_compute_diffmap(mut self, compute_diffmap: bool) -> Self {
self.compute_diffmap = compute_diffmap;
self
}
#[must_use]
pub fn hf_asymmetry(&self) -> f32 {
self.hf_asymmetry
}
#[must_use]
pub fn xmul(&self) -> f32 {
self.xmul
}
#[must_use]
pub fn intensity_target(&self) -> f32 {
self.intensity_target
}
#[must_use]
pub fn compute_diffmap(&self) -> bool {
self.compute_diffmap
}
#[must_use]
pub fn with_single_resolution(mut self, single_resolution: bool) -> Self {
self.single_resolution = single_resolution;
self
}
#[must_use]
pub fn single_resolution(&self) -> bool {
self.single_resolution
}
}
pub const BUTTERAUGLI_GOOD: f64 = 1.0;
pub const BUTTERAUGLI_BAD: f64 = 2.0;
#[derive(Debug, Clone)]
pub struct ButteraugliResult {
pub score: f64,
pub diffmap: Option<ImgVec<f32>>,
}
pub fn butteraugli(
img1: ImgRef<RGB8>,
img2: ImgRef<RGB8>,
params: &ButteraugliParams,
) -> Result<ButteraugliResult, ButteraugliError> {
let (w1, h1) = (img1.width(), img1.height());
let (w2, h2) = (img2.width(), img2.height());
if w1 < 8 || h1 < 8 {
return Err(ButteraugliError::ImageTooSmall {
width: w1,
height: h1,
});
}
if w1 != w2 || h1 != h2 {
return Err(ButteraugliError::DimensionMismatch { w1, h1, w2, h2 });
}
let result = diff::compute_butteraugli_imgref(img1, img2, params, params.compute_diffmap);
Ok(ButteraugliResult {
score: result.score,
diffmap: result.diffmap.map(image::ImageF::into_imgvec),
})
}
pub fn butteraugli_linear(
img1: ImgRef<RGB<f32>>,
img2: ImgRef<RGB<f32>>,
params: &ButteraugliParams,
) -> Result<ButteraugliResult, ButteraugliError> {
let (w1, h1) = (img1.width(), img1.height());
let (w2, h2) = (img2.width(), img2.height());
if w1 < 8 || h1 < 8 {
return Err(ButteraugliError::ImageTooSmall {
width: w1,
height: h1,
});
}
if w1 != w2 || h1 != h2 {
return Err(ButteraugliError::DimensionMismatch { w1, h1, w2, h2 });
}
let result =
diff::compute_butteraugli_linear_imgref(img1, img2, params, params.compute_diffmap);
Ok(ButteraugliResult {
score: result.score,
diffmap: result.diffmap.map(image::ImageF::into_imgvec),
})
}
#[must_use]
pub fn srgb_to_linear(v: u8) -> f32 {
opsin::srgb_to_linear(v)
}
#[doc(hidden)]
pub struct LegacyButteraugliResult {
pub score: f64,
pub diffmap: Option<image::ImageF>,
}
#[deprecated(since = "0.4.0", note = "Use butteraugli() with ImgRef<RGB8> instead")]
#[allow(clippy::missing_errors_doc)]
pub fn compute_butteraugli(
rgb1: &[u8],
rgb2: &[u8],
width: usize,
height: usize,
params: &ButteraugliParams,
) -> Result<LegacyButteraugliResult, ButteraugliError> {
let expected_size = width * height * 3;
if width < 8 || height < 8 {
return Err(ButteraugliError::ImageTooSmall { width, height });
}
if rgb1.len() != expected_size || rgb2.len() != expected_size {
return Err(ButteraugliError::DimensionMismatch {
w1: width,
h1: height,
w2: if rgb2.len() == expected_size {
width
} else {
0
},
h2: if rgb2.len() == expected_size {
height
} else {
0
},
});
}
let pixels1: Vec<RGB8> = rgb1
.chunks_exact(3)
.map(|c| RGB8::new(c[0], c[1], c[2]))
.collect();
let pixels2: Vec<RGB8> = rgb2
.chunks_exact(3)
.map(|c| RGB8::new(c[0], c[1], c[2]))
.collect();
let img1 = Img::new(pixels1, width, height);
let img2 = Img::new(pixels2, width, height);
let result = diff::compute_butteraugli_imgref(img1.as_ref(), img2.as_ref(), params, true);
Ok(LegacyButteraugliResult {
score: result.score,
diffmap: result.diffmap,
})
}
#[deprecated(
since = "0.4.0",
note = "Use butteraugli_linear() with ImgRef<RGB<f32>> instead"
)]
#[allow(clippy::missing_errors_doc)]
pub fn compute_butteraugli_linear(
rgb1: &[f32],
rgb2: &[f32],
width: usize,
height: usize,
params: &ButteraugliParams,
) -> Result<LegacyButteraugliResult, ButteraugliError> {
let expected_size = width * height * 3;
if width < 8 || height < 8 {
return Err(ButteraugliError::ImageTooSmall { width, height });
}
if rgb1.len() != expected_size || rgb2.len() != expected_size {
return Err(ButteraugliError::DimensionMismatch {
w1: width,
h1: height,
w2: if rgb2.len() == expected_size {
width
} else {
0
},
h2: if rgb2.len() == expected_size {
height
} else {
0
},
});
}
let pixels1: Vec<RGB<f32>> = rgb1
.chunks_exact(3)
.map(|c| RGB::new(c[0], c[1], c[2]))
.collect();
let pixels2: Vec<RGB<f32>> = rgb2
.chunks_exact(3)
.map(|c| RGB::new(c[0], c[1], c[2]))
.collect();
let img1 = Img::new(pixels1, width, height);
let img2 = Img::new(pixels2, width, height);
let result =
diff::compute_butteraugli_linear_imgref(img1.as_ref(), img2.as_ref(), params, true);
Ok(LegacyButteraugliResult {
score: result.score,
diffmap: result.diffmap,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identical_images() {
let width = 16;
let height = 16;
let pixels: Vec<RGB8> = (0..width * height)
.map(|i| {
RGB8::new(
(i % 256) as u8,
((i * 2) % 256) as u8,
((i * 3) % 256) as u8,
)
})
.collect();
let img = Img::new(pixels, width, height);
let result = butteraugli(img.as_ref(), img.as_ref(), &ButteraugliParams::default())
.expect("valid input");
assert!(
result.score < 0.001,
"Identical images should have score ~0, got {}",
result.score
);
}
#[test]
fn test_different_images() {
let width = 16;
let height = 16;
let pixels1: Vec<RGB8> = vec![RGB8::new(0, 0, 0); width * height];
let pixels2: Vec<RGB8> = vec![RGB8::new(255, 255, 255); width * height];
let img1 = Img::new(pixels1, width, height);
let img2 = Img::new(pixels2, width, height);
let result = butteraugli(img1.as_ref(), img2.as_ref(), &ButteraugliParams::default())
.expect("valid input");
assert!(
result.score > 0.01,
"Different images should have non-zero score, got {}",
result.score
);
}
#[test]
fn test_dimension_mismatch() {
let pixels1: Vec<RGB8> = vec![RGB8::new(0, 0, 0); 16 * 16];
let pixels2: Vec<RGB8> = vec![RGB8::new(0, 0, 0); 8 * 8];
let img1 = Img::new(pixels1, 16, 16);
let img2 = Img::new(pixels2, 8, 8);
let result = butteraugli(img1.as_ref(), img2.as_ref(), &ButteraugliParams::default());
assert!(matches!(
result,
Err(ButteraugliError::DimensionMismatch { .. })
));
}
#[test]
fn test_too_small_dimensions() {
let pixels: Vec<RGB8> = vec![RGB8::new(0, 0, 0); 4 * 4];
let img = Img::new(pixels, 4, 4);
let result = butteraugli(img.as_ref(), img.as_ref(), &ButteraugliParams::default());
assert!(matches!(
result,
Err(ButteraugliError::ImageTooSmall { .. })
));
}
#[test]
fn test_compute_diffmap_flag() {
let width = 16;
let height = 16;
let pixels: Vec<RGB8> = vec![RGB8::new(128, 128, 128); width * height];
let img = Img::new(pixels, width, height);
let params = ButteraugliParams::default();
let result = butteraugli(img.as_ref(), img.as_ref(), ¶ms).unwrap();
assert!(result.diffmap.is_none());
let params = ButteraugliParams::default().with_compute_diffmap(true);
let result = butteraugli(img.as_ref(), img.as_ref(), ¶ms).unwrap();
assert!(result.diffmap.is_some());
let diffmap = result.diffmap.unwrap();
assert_eq!(diffmap.width(), width);
assert_eq!(diffmap.height(), height);
}
}