use crate::ButteraugliParams;
use crate::consts::{
NORM1_HF, NORM1_HF_X, NORM1_MF, NORM1_MF_X, NORM1_UHF, NORM1_UHF_X, W_HF_MALTA, W_HF_MALTA_X,
W_MF_MALTA, W_MF_MALTA_X, W_UHF_MALTA, W_UHF_MALTA_X, WMUL,
};
use crate::image::{BufferPool, Image3F, ImageF};
use crate::malta::malta_diff_map;
use crate::mask::compute_mask_from_hf_uhf;
use crate::opsin::linear_rgb_to_xyb_butteraugli;
use crate::psycho::{PsychoImage, separate_frequencies};
use imgref::ImgRef;
use rgb::{RGB, RGB8};
#[cfg(feature = "rayon")]
pub(crate) fn maybe_join<A, B, RA, RB>(a: A, b: B) -> (RA, RB)
where
A: FnOnce() -> RA + Send,
B: FnOnce() -> RB + Send,
RA: Send,
RB: Send,
{
rayon::join(a, b)
}
#[cfg(not(feature = "rayon"))]
pub(crate) fn maybe_join<A, B, RA, RB>(a: A, b: B) -> (RA, RB)
where
A: FnOnce() -> RA,
B: FnOnce() -> RB,
{
(a(), b())
}
pub(crate) struct InternalResult {
pub score: f64,
pub pnorm_3: f64,
pub diffmap: Option<ImageF>,
}
const MIN_SIZE_FOR_MULTIRESOLUTION: usize = 8;
fn linear_rgb_to_xyb_image(
rgb: &[f32],
width: usize,
height: usize,
intensity_target: f32,
pool: &BufferPool,
) -> Image3F {
linear_rgb_to_xyb_butteraugli(rgb, width, height, intensity_target, pool)
}
#[cfg(test)]
fn srgb_u8_to_linear_f32(rgb: &[u8]) -> Vec<f32> {
let lut = &*crate::opsin::SRGB_TO_LINEAR_LUT;
rgb.iter().map(|&v| lut[v as usize]).collect()
}
#[archmage::autoversion]
fn add_supersampled_2x(_token: archmage::SimdToken, src: &ImageF, weight: f32, dest: &mut ImageF) {
let dest_width = dest.width();
let dest_height = dest.height();
const K_HEURISTIC_MIXING_VALUE: f32 = 0.3;
let blend = 1.0 - K_HEURISTIC_MIXING_VALUE * weight;
let src_w = src.width();
let src_h = src.height();
for y in 0..dest_height {
let src_y = (y / 2).min(src_h - 1);
let src_row = src.row(src_y);
let dst_row = dest.row_mut(y);
let n_pairs = (dest_width / 2).min(src_w);
for (pair, &sv) in dst_row[..n_pairs * 2]
.chunks_exact_mut(2)
.zip(src_row[..n_pairs].iter())
{
let ws = weight * sv;
pair[0] = pair[0].mul_add(blend, ws);
pair[1] = pair[1].mul_add(blend, ws);
}
if dest_width > n_pairs * 2 {
let sv = src_row[(dest_width / 2).min(src_w - 1)];
dst_row[dest_width - 1] = dst_row[dest_width - 1].mul_add(blend, weight * sv);
}
}
}
#[archmage::autoversion]
fn accumulate_two(_token: archmage::SimdToken, a: &ImageF, b: &ImageF, dst: &mut ImageF) {
let height = a.height();
for y in 0..height {
let ra = a.row(y);
let rb = b.row(y);
let rd = dst.row_mut(y);
for ((d, &va), &vb) in rd.iter_mut().zip(ra.iter()).zip(rb.iter()) {
*d += va + vb;
}
}
}
#[archmage::autoversion]
fn l2_diff(_token: archmage::SimdToken, i0: &ImageF, i1: &ImageF, w: f32, diffmap: &mut ImageF) {
let height = i0.height();
for y in 0..height {
let row0 = i0.row(y);
let row1 = i1.row(y);
let row_diff = diffmap.row_mut(y);
for ((d, &v0), &v1) in row_diff.iter_mut().zip(row0.iter()).zip(row1.iter()) {
let diff = v0 - v1;
*d = (diff * diff).mul_add(w, *d);
}
}
}
#[archmage::autoversion]
fn l2_diff_write(
_token: archmage::SimdToken,
i0: &ImageF,
i1: &ImageF,
w: f32,
diffmap: &mut ImageF,
) {
let height = i0.height();
for y in 0..height {
let row0 = i0.row(y);
let row1 = i1.row(y);
let row_diff = diffmap.row_mut(y);
for ((d, &v0), &v1) in row_diff.iter_mut().zip(row0.iter()).zip(row1.iter()) {
let diff = v0 - v1;
*d = diff * diff * w;
}
}
}
#[archmage::autoversion]
fn l2_diff_asymmetric(
_token: archmage::SimdToken,
i0: &ImageF,
i1: &ImageF,
w_0gt1: f32,
w_0lt1: f32,
diffmap: &mut ImageF,
) {
if w_0gt1 == 0.0 && w_0lt1 == 0.0 {
return;
}
let width = i0.width();
let height = i0.height();
let vw_0gt1 = w_0gt1 * 0.8;
let vw_0lt1 = w_0lt1 * 0.8;
for y in 0..height {
let row0 = i0.row(y);
let row1 = i1.row(y);
let row_diff = diffmap.row_mut(y);
for x in 0..width {
let val0 = row0[x];
let val1 = row1[x];
let diff = val0 - val1;
let total = (diff * diff).mul_add(vw_0gt1, row_diff[x]);
let fabs0 = val0.abs();
let too_small = 0.4 * fabs0;
let sign = 1.0f32.copysign(val0);
let sv1 = val1 * sign;
let v = (too_small - sv1).max(0.0) + (sv1 - fabs0).max(0.0);
row_diff[x] = (v * v).mul_add(vw_0lt1, total);
}
}
}
fn compute_psycho_diff_malta(
ps0: &PsychoImage,
ps1: &PsychoImage,
hf_asymmetry: f32,
_xmul: f32,
pool: &BufferPool,
) -> Image3F {
let width = ps0.width();
let height = ps0.height();
let sqrt_hf_asym = hf_asymmetry.sqrt();
let ((uhf_y_diff, uhf_x_diff), ((hf_y_diff, hf_x_diff), (mf_y_diff, mf_x_diff))) = maybe_join(
|| {
maybe_join(
|| {
malta_diff_map(
&ps0.uhf[1],
&ps1.uhf[1],
W_UHF_MALTA * hf_asymmetry as f64,
W_UHF_MALTA / hf_asymmetry as f64,
NORM1_UHF,
false,
pool,
)
},
|| {
malta_diff_map(
&ps0.uhf[0],
&ps1.uhf[0],
W_UHF_MALTA_X * hf_asymmetry as f64,
W_UHF_MALTA_X / hf_asymmetry as f64,
NORM1_UHF_X,
false,
pool,
)
},
)
},
|| {
maybe_join(
|| {
maybe_join(
|| {
malta_diff_map(
&ps0.hf[1],
&ps1.hf[1],
W_HF_MALTA * sqrt_hf_asym as f64,
W_HF_MALTA / sqrt_hf_asym as f64,
NORM1_HF,
true,
pool,
)
},
|| {
malta_diff_map(
&ps0.hf[0],
&ps1.hf[0],
W_HF_MALTA_X * sqrt_hf_asym as f64,
W_HF_MALTA_X / sqrt_hf_asym as f64,
NORM1_HF_X,
true,
pool,
)
},
)
},
|| {
maybe_join(
|| {
malta_diff_map(
ps0.mf.plane(1),
ps1.mf.plane(1),
W_MF_MALTA,
W_MF_MALTA,
NORM1_MF,
true,
pool,
)
},
|| {
malta_diff_map(
ps0.mf.plane(0),
ps1.mf.plane(0),
W_MF_MALTA_X,
W_MF_MALTA_X,
NORM1_MF_X,
true,
pool,
)
},
)
},
)
},
);
let mut plane_x = uhf_x_diff;
let mut plane_y = uhf_y_diff;
accumulate_two(&hf_y_diff, &mf_y_diff, &mut plane_y);
accumulate_two(&hf_x_diff, &mf_x_diff, &mut plane_x);
l2_diff_asymmetric(
&ps0.hf[0],
&ps1.hf[0],
WMUL[0] as f32 * hf_asymmetry,
WMUL[0] as f32 / hf_asymmetry,
&mut plane_x,
);
l2_diff_asymmetric(
&ps0.hf[1],
&ps1.hf[1],
WMUL[1] as f32 * hf_asymmetry,
WMUL[1] as f32 / hf_asymmetry,
&mut plane_y,
);
l2_diff(
ps0.mf.plane(0),
ps1.mf.plane(0),
WMUL[3] as f32,
&mut plane_x,
);
l2_diff(
ps0.mf.plane(1),
ps1.mf.plane(1),
WMUL[4] as f32,
&mut plane_y,
);
let mut plane_b = ImageF::new_uninit(width, height);
l2_diff_write(
ps0.mf.plane(2),
ps1.mf.plane(2),
WMUL[5] as f32,
&mut plane_b,
);
Image3F::from_planes(plane_x, plane_y, plane_b)
}
fn mask_psycho_image(
ps0: &PsychoImage,
ps1: &PsychoImage,
diff_ac: Option<&mut ImageF>,
pool: &BufferPool,
) -> ImageF {
compute_mask_from_hf_uhf(&ps0.hf, &ps0.uhf, &ps1.hf, &ps1.uhf, diff_ac, pool)
}
#[archmage::autoversion]
fn combine_channels_to_diffmap_fused(
_token: archmage::SimdToken,
mask: &ImageF,
lf1: &Image3F,
lf2: &Image3F,
block_diff_ac: &Image3F,
xmul: f32,
) -> ImageF {
let width = mask.width();
let height = mask.height();
let mut diffmap = ImageF::new_uninit(width, height);
let dc_w0 = WMUL[6] as f32;
let dc_w1 = WMUL[7] as f32;
let dc_w2 = WMUL[8] as f32;
let global_scale = crate::consts::GLOBAL_SCALE;
let my_mul = crate::consts::MASK_Y_MUL as f32;
let my_scaler = crate::consts::MASK_Y_SCALER as f32;
let my_offset = crate::consts::MASK_Y_OFFSET as f32;
let mdc_mul = crate::consts::MASK_DC_Y_MUL as f32;
let mdc_scaler = crate::consts::MASK_DC_Y_SCALER as f32;
let mdc_offset = crate::consts::MASK_DC_Y_OFFSET as f32;
for y in 0..height {
let mask_row = mask.row(y);
let lf1_0 = lf1.plane(0).row(y);
let lf1_1 = lf1.plane(1).row(y);
let lf1_2 = lf1.plane(2).row(y);
let lf2_0 = lf2.plane(0).row(y);
let lf2_1 = lf2.plane(1).row(y);
let lf2_2 = lf2.plane(2).row(y);
let ac0 = block_diff_ac.plane(0).row(y);
let ac1 = block_diff_ac.plane(1).row(y);
let ac2 = block_diff_ac.plane(2).row(y);
let out = diffmap.row_mut(y);
for x in 0..width {
let val = mask_row[x];
let c_y = my_mul / my_scaler.mul_add(val, my_offset);
let r_y = global_scale.mul_add(c_y, global_scale);
let maskval = r_y * r_y;
let c_dc = mdc_mul / mdc_scaler.mul_add(val, mdc_offset);
let r_dc = global_scale.mul_add(c_dc, global_scale);
let dc_maskval = r_dc * r_dc;
let d0 = lf1_0[x] - lf2_0[x];
let d1 = lf1_1[x] - lf2_1[x];
let d2 = lf1_2[x] - lf2_2[x];
let dc_masked = (d0 * d0 * dc_w0 * xmul).mul_add(
dc_maskval,
(d1 * d1 * dc_w1).mul_add(dc_maskval, d2 * d2 * dc_w2 * dc_maskval),
);
let ac_masked =
(ac0[x] * xmul).mul_add(maskval, ac1[x].mul_add(maskval, ac2[x] * maskval));
out[x] = (dc_masked + ac_masked).sqrt();
}
}
diffmap
}
#[archmage::autoversion]
fn compute_score_from_diffmap(_token: archmage::SimdToken, diffmap: &ImageF) -> (f64, f64) {
let width = diffmap.width();
let height = diffmap.height();
if width * height == 0 {
return (0.0, 0.0);
}
let mut max_lanes = [0.0f32; 8];
let mut sum_p3 = [0.0f64; 8];
let mut sum_p6 = [0.0f64; 8];
let mut sum_p12 = [0.0f64; 8];
for y in 0..height {
let row = diffmap.row(y);
for chunk in row.chunks_exact(8) {
for i in 0..8 {
let v = chunk[i];
if v > max_lanes[i] {
max_lanes[i] = v;
}
let d = v as f64;
let d3 = d * d * d;
sum_p3[i] += d3;
let d6 = d3 * d3;
sum_p6[i] += d6;
sum_p12[i] += d6 * d6;
}
}
for &v in row.chunks_exact(8).remainder() {
if v > max_lanes[0] {
max_lanes[0] = v;
}
let d = v as f64;
let d3 = d * d * d;
sum_p3[0] += d3;
let d6 = d3 * d3;
sum_p6[0] += d6;
sum_p12[0] += d6 * d6;
}
}
let mut max_val = max_lanes[0];
for &m in &max_lanes[1..] {
if m > max_val {
max_val = m;
}
}
let total_p3: f64 = sum_p3.iter().sum();
let total_p6: f64 = sum_p6.iter().sum();
let total_p12: f64 = sum_p12.iter().sum();
let one_per_pixels = 1.0_f64 / ((width * height) as f64);
let v0 = (one_per_pixels * total_p3).powf(1.0 / 3.0);
let v1 = (one_per_pixels * total_p6).powf(1.0 / 6.0);
let v2 = (one_per_pixels * total_p12).powf(1.0 / 12.0);
let pnorm_3 = (v0 + v1 + v2) / 3.0;
(max_val as f64, pnorm_3)
}
fn subsample_linear_rgb_2x(rgb: &[f32], width: usize, height: usize) -> (Vec<f32>, usize, usize) {
let out_width = width.div_ceil(2);
let out_height = height.div_ceil(2);
let mut output = vec![0.0f32; out_width * out_height * 3];
let interior_w = width / 2;
let interior_h = height / 2;
let inv4 = 0.25f32;
for oy in 0..interior_h {
let iy = oy * 2;
let row0 = iy * width * 3;
let row1 = (iy + 1) * width * 3;
for ox in 0..interior_w {
let ix = ox * 2;
let i00 = row0 + ix * 3;
let i10 = row0 + (ix + 1) * 3;
let i01 = row1 + ix * 3;
let i11 = row1 + (ix + 1) * 3;
let out_idx = (oy * out_width + ox) * 3;
output[out_idx] = (rgb[i00] + rgb[i10] + rgb[i01] + rgb[i11]) * inv4;
output[out_idx + 1] =
(rgb[i00 + 1] + rgb[i10 + 1] + rgb[i01 + 1] + rgb[i11 + 1]) * inv4;
output[out_idx + 2] =
(rgb[i00 + 2] + rgb[i10 + 2] + rgb[i01 + 2] + rgb[i11 + 2]) * inv4;
}
}
if out_width > interior_w {
let ox = interior_w;
let ix = ox * 2;
for oy in 0..interior_h {
let iy = oy * 2;
let i00 = (iy * width + ix) * 3;
let i01 = ((iy + 1) * width + ix) * 3;
let out_idx = (oy * out_width + ox) * 3;
let inv2 = 0.5f32;
output[out_idx] = (rgb[i00] + rgb[i01]) * inv2;
output[out_idx + 1] = (rgb[i00 + 1] + rgb[i01 + 1]) * inv2;
output[out_idx + 2] = (rgb[i00 + 2] + rgb[i01 + 2]) * inv2;
}
}
if out_height > interior_h {
let oy = interior_h;
let iy = oy * 2;
let row0 = iy * width * 3;
for ox in 0..interior_w {
let ix = ox * 2;
let i00 = row0 + ix * 3;
let i10 = row0 + (ix + 1) * 3;
let out_idx = (oy * out_width + ox) * 3;
let inv2 = 0.5f32;
output[out_idx] = (rgb[i00] + rgb[i10]) * inv2;
output[out_idx + 1] = (rgb[i00 + 1] + rgb[i10 + 1]) * inv2;
output[out_idx + 2] = (rgb[i00 + 2] + rgb[i10 + 2]) * inv2;
}
if out_width > interior_w {
let ox = interior_w;
let ix = ox * 2;
let i00 = row0 + ix * 3;
let out_idx = (oy * out_width + ox) * 3;
output[out_idx] = rgb[i00];
output[out_idx + 1] = rgb[i00 + 1];
output[out_idx + 2] = rgb[i00 + 2];
}
}
(output, out_width, out_height)
}
fn compute_diffmap_single_resolution_linear(
rgb1: &[f32],
rgb2: &[f32],
width: usize,
height: usize,
params: &ButteraugliParams,
) -> ImageF {
let intensity_target = params.intensity_target();
let (ps1, ps2) = maybe_join(
|| {
let pool = BufferPool::new();
let xyb = linear_rgb_to_xyb_image(rgb1, width, height, intensity_target, &pool);
separate_frequencies(&xyb, &pool)
},
|| {
let pool = BufferPool::new();
let xyb = linear_rgb_to_xyb_image(rgb2, width, height, intensity_target, &pool);
separate_frequencies(&xyb, &pool)
},
);
let pool = BufferPool::new();
let mut block_diff_ac =
compute_psycho_diff_malta(&ps1, &ps2, params.hf_asymmetry(), params.xmul(), &pool);
let mask = mask_psycho_image(&ps1, &ps2, Some(block_diff_ac.plane_mut(1)), &pool);
combine_channels_to_diffmap_fused(&mask, &ps1.lf, &ps2.lf, &block_diff_ac, params.xmul())
}
fn compute_diffmap_multiresolution_linear(
rgb1: &[f32],
rgb2: &[f32],
width: usize,
height: usize,
params: &ButteraugliParams,
) -> ImageF {
const MIN_SIZE_FOR_SUBSAMPLE: usize = 15;
let need_sub = !params.single_resolution()
&& width >= MIN_SIZE_FOR_SUBSAMPLE
&& height >= MIN_SIZE_FOR_SUBSAMPLE;
if need_sub {
let (sub_diffmap, mut diffmap) = maybe_join(
|| {
let (sub_rgb1, sw, sh) = subsample_linear_rgb_2x(rgb1, width, height);
let (sub_rgb2, _, _) = subsample_linear_rgb_2x(rgb2, width, height);
compute_diffmap_single_resolution_linear(&sub_rgb1, &sub_rgb2, sw, sh, params)
},
|| compute_diffmap_single_resolution_linear(rgb1, rgb2, width, height, params),
);
add_supersampled_2x(&sub_diffmap, 0.5, &mut diffmap);
diffmap
} else {
compute_diffmap_single_resolution_linear(rgb1, rgb2, width, height, params)
}
}
#[cfg(test)]
pub fn compute_butteraugli_impl(
rgb1: &[u8],
rgb2: &[u8],
width: usize,
height: usize,
params: &ButteraugliParams,
) -> InternalResult {
assert_eq!(rgb1.len(), width * height * 3);
assert_eq!(rgb2.len(), width * height * 3);
if rgb1 == rgb2 {
return InternalResult {
score: 0.0,
pnorm_3: 0.0,
diffmap: Some(ImageF::new(width, height)),
};
}
let linear1 = srgb_u8_to_linear_f32(rgb1);
let linear2 = srgb_u8_to_linear_f32(rgb2);
compute_butteraugli_linear_impl(&linear1, &linear2, width, height, params)
}
pub fn compute_butteraugli_linear_impl(
rgb1: &[f32],
rgb2: &[f32],
width: usize,
height: usize,
params: &ButteraugliParams,
) -> InternalResult {
assert_eq!(rgb1.len(), width * height * 3);
assert_eq!(rgb2.len(), width * height * 3);
if rgb1 == rgb2 {
return InternalResult {
score: 0.0,
pnorm_3: 0.0,
diffmap: Some(ImageF::new(width, height)),
};
}
let diffmap = if width < MIN_SIZE_FOR_MULTIRESOLUTION || height < MIN_SIZE_FOR_MULTIRESOLUTION {
compute_diffmap_single_resolution_linear(rgb1, rgb2, width, height, params)
} else {
compute_diffmap_multiresolution_linear(rgb1, rgb2, width, height, params)
};
let (score, pnorm_3) = compute_score_from_diffmap(&diffmap);
InternalResult {
score,
pnorm_3,
diffmap: Some(diffmap),
}
}
pub(crate) fn compute_butteraugli_imgref(
img1: ImgRef<RGB8>,
img2: ImgRef<RGB8>,
params: &ButteraugliParams,
compute_diffmap: bool,
) -> InternalResult {
let width = img1.width();
let height = img1.height();
let linear1 = imgref_srgb_to_linear_f32(img1);
let linear2 = imgref_srgb_to_linear_f32(img2);
let mut result = compute_butteraugli_linear_impl(&linear1, &linear2, width, height, params);
if !compute_diffmap {
result.diffmap = None;
}
result
}
pub(crate) fn imgref_srgb_to_linear_f32(img: ImgRef<RGB8>) -> Vec<f32> {
let lut = &*crate::opsin::SRGB_TO_LINEAR_LUT;
let width = img.width();
let height = img.height();
let mut result = Vec::with_capacity(width * height * 3);
for row in img.rows() {
for px in row {
result.push(lut[px.r as usize]);
result.push(lut[px.g as usize]);
result.push(lut[px.b as usize]);
}
}
result
}
pub(crate) fn compute_butteraugli_linear_imgref(
img1: ImgRef<RGB<f32>>,
img2: ImgRef<RGB<f32>>,
params: &ButteraugliParams,
compute_diffmap: bool,
) -> InternalResult {
let width = img1.width();
let height = img1.height();
let rgb1 = imgref_rgbf32_to_f32_vec(img1);
let rgb2 = imgref_rgbf32_to_f32_vec(img2);
let mut result = compute_butteraugli_linear_impl(&rgb1, &rgb2, width, height, params);
if !compute_diffmap {
result.diffmap = None;
}
result
}
pub(crate) fn imgref_rgbf32_to_f32_vec(img: ImgRef<RGB<f32>>) -> Vec<f32> {
let width = img.width();
let height = img.height();
let mut out = Vec::with_capacity(width * height * 3);
for row in img.rows() {
for px in row {
out.push(px.r);
out.push(px.g);
out.push(px.b);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identical_images() {
let width = 32;
let height = 32;
let rgb: Vec<u8> = (0..width * height * 3).map(|i| (i % 256) as u8).collect();
let result =
compute_butteraugli_impl(&rgb, &rgb, width, height, &ButteraugliParams::default());
assert!(
result.score < 0.001,
"Identical images should have score ~0, got {}",
result.score
);
}
#[test]
fn test_slightly_different_images() {
let width = 32;
let height = 32;
let rgb1: Vec<u8> = vec![128; width * height * 3];
let mut rgb2 = rgb1.clone();
rgb2[0] = 129;
rgb2[1] = 129;
rgb2[2] = 129;
let result =
compute_butteraugli_impl(&rgb1, &rgb2, width, height, &ButteraugliParams::default());
assert!(
result.score < 2.0,
"Small difference should have low score, got {}",
result.score
);
}
#[test]
fn test_very_different_images() {
let width = 32;
let height = 32;
let rgb1: Vec<u8> = vec![0; width * height * 3];
let rgb2: Vec<u8> = vec![255; width * height * 3];
let result =
compute_butteraugli_impl(&rgb1, &rgb2, width, height, &ButteraugliParams::default());
assert!(
result.score > 0.01,
"Very different images should have non-zero score, got {}",
result.score
);
}
#[test]
fn test_diffmap_dimensions() {
let width = 64;
let height = 48;
let rgb1: Vec<u8> = vec![100; width * height * 3];
let rgb2: Vec<u8> = vec![150; width * height * 3];
let result =
compute_butteraugli_impl(&rgb1, &rgb2, width, height, &ButteraugliParams::default());
let diffmap = result.diffmap.unwrap();
assert_eq!(diffmap.width(), width);
assert_eq!(diffmap.height(), height);
}
#[test]
fn test_l2_diff_asymmetric() {
let width = 16;
let height = 16;
let i0 = ImageF::filled(width, height, 1.0);
let i1 = ImageF::filled(width, height, 0.5);
let mut diffmap = ImageF::new(width, height);
l2_diff_asymmetric(&i0, &i1, 1.0, 1.0, &mut diffmap);
let mut sum = 0.0;
for y in 0..height {
for x in 0..width {
sum += diffmap.get(x, y);
}
}
assert!(sum > 0.0, "L2 diff should be non-zero for different images");
}
#[test]
fn test_add_supersampled_2x() {
let src = ImageF::filled(4, 4, 1.0);
let mut dest = ImageF::filled(8, 8, 2.0);
add_supersampled_2x(&src, 0.5, &mut dest);
let val = dest.get(0, 0);
assert!((val - 2.2).abs() < 0.01, "Expected ~2.2, got {val}");
}
#[test]
fn test_multiresolution_small_image() {
let width = 4;
let height = 4;
let rgb1: Vec<u8> = vec![128; width * height * 3];
let rgb2: Vec<u8> = vec![140; width * height * 3];
let result =
compute_butteraugli_impl(&rgb1, &rgb2, width, height, &ButteraugliParams::default());
assert!(result.score > 0.0, "Should have non-zero score");
}
}