use imgref::ImgRef;
use rgb::{RGB, RGB8};
use crate::diff::{
self, compute_diffmap_multiresolution_linear, compute_diffmap_single_resolution_linear,
imgref_rgbf32_to_f32_vec, imgref_srgb_to_linear_f32,
};
use crate::image::ImageF;
use crate::precompute::ButteraugliReference;
use crate::{ButteraugliError, ButteraugliParams, ButteraugliResult, check_finite_f32};
pub const HALO_ROWS_DEFAULT: usize = 64;
const STRIP_ALIGNMENT: usize = 2;
pub const MIN_STRIP_HEIGHT: usize = 8;
#[derive(Debug, Clone, Copy)]
pub struct ButteraugliStripConfig {
pub halo_rows: usize,
}
impl Default for ButteraugliStripConfig {
fn default() -> Self {
Self {
halo_rows: HALO_ROWS_DEFAULT,
}
}
}
impl ButteraugliStripConfig {
#[must_use]
pub fn with_halo_rows(halo_rows: usize) -> Self {
Self { halo_rows }
}
}
#[derive(Debug, Default)]
struct StripReducer {
max_val: f32,
sum_p3: f64,
sum_p6: f64,
sum_p12: f64,
pixels: u64,
}
impl StripReducer {
fn add_strip(&mut self, diffmap: &ImageF, interior_y0: usize, interior_y1: usize) {
let width = diffmap.width();
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 interior_y0..interior_y1 {
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 strip_max = max_lanes[0];
for &m in &max_lanes[1..] {
if m > strip_max {
strip_max = m;
}
}
if strip_max > self.max_val {
self.max_val = strip_max;
}
self.sum_p3 += sum_p3.iter().sum::<f64>();
self.sum_p6 += sum_p6.iter().sum::<f64>();
self.sum_p12 += sum_p12.iter().sum::<f64>();
self.pixels += (interior_y1 - interior_y0) as u64 * width as u64;
}
fn finalise(&self, total_pixels: u64) -> (f64, f64) {
debug_assert_eq!(
self.pixels, total_pixels,
"strip reducer accumulated {} pixels but image has {}; \
strip walker has a coverage bug",
self.pixels, total_pixels
);
if total_pixels == 0 {
return (0.0, 0.0);
}
let one_per_pixels = 1.0_f64 / total_pixels as f64;
let v0 = (one_per_pixels * self.sum_p3).powf(1.0 / 3.0);
let v1 = (one_per_pixels * self.sum_p6).powf(1.0 / 6.0);
let v2 = (one_per_pixels * self.sum_p12).powf(1.0 / 12.0);
let pnorm_3 = (v0 + v1 + v2) / 3.0;
(self.max_val as f64, pnorm_3)
}
}
pub fn butteraugli_strip(
img1: ImgRef<RGB8>,
img2: ImgRef<RGB8>,
params: &ButteraugliParams,
strip_height: u32,
) -> Result<ButteraugliResult, ButteraugliError> {
butteraugli_strip_with_config(
img1,
img2,
params,
strip_height,
ButteraugliStripConfig::default(),
)
}
pub fn butteraugli_strip_with_config(
img1: ImgRef<RGB8>,
img2: ImgRef<RGB8>,
params: &ButteraugliParams,
strip_height: u32,
config: ButteraugliStripConfig,
) -> Result<ButteraugliResult, ButteraugliError> {
params.validate()?;
validate_image_pair(img1, img2, strip_height as usize)?;
let (width, height) = (img1.width(), img1.height());
width
.checked_mul(height)
.and_then(|wh| wh.checked_mul(3))
.ok_or(ButteraugliError::DimensionOverflow { width, height })?;
let linear1 = imgref_srgb_to_linear_f32(img1);
let linear2 = imgref_srgb_to_linear_f32(img2);
run_strip_walker_linear(
&linear1,
&linear2,
width,
height,
strip_height as usize,
params,
config.halo_rows,
params.compute_diffmap(),
)
}
pub fn butteraugli_linear_strip(
img1: ImgRef<RGB<f32>>,
img2: ImgRef<RGB<f32>>,
params: &ButteraugliParams,
strip_height: u32,
) -> Result<ButteraugliResult, ButteraugliError> {
butteraugli_linear_strip_with_config(
img1,
img2,
params,
strip_height,
ButteraugliStripConfig::default(),
)
}
pub fn butteraugli_linear_strip_with_config(
img1: ImgRef<RGB<f32>>,
img2: ImgRef<RGB<f32>>,
params: &ButteraugliParams,
strip_height: u32,
config: ButteraugliStripConfig,
) -> Result<ButteraugliResult, ButteraugliError> {
params.validate()?;
let (width, height) = (img1.width(), img1.height());
let (w2, h2) = (img2.width(), img2.height());
if width < 8 || height < 8 {
return Err(ButteraugliError::ImageTooSmall { width, height });
}
if width != w2 || height != h2 {
return Err(ButteraugliError::DimensionMismatch {
w1: width,
h1: height,
w2,
h2,
});
}
if (strip_height as usize) < MIN_STRIP_HEIGHT {
return Err(ButteraugliError::ImageTooSmall {
width: strip_height as usize,
height: MIN_STRIP_HEIGHT,
});
}
width
.checked_mul(height)
.and_then(|wh| wh.checked_mul(3))
.ok_or(ButteraugliError::DimensionOverflow { width, height })?;
let linear1 = imgref_rgbf32_to_f32_vec(img1);
let linear2 = imgref_rgbf32_to_f32_vec(img2);
check_finite_f32(&linear1, "linear rgb1")?;
check_finite_f32(&linear2, "linear rgb2")?;
run_strip_walker_linear(
&linear1,
&linear2,
width,
height,
strip_height as usize,
params,
config.halo_rows,
params.compute_diffmap(),
)
}
fn validate_image_pair(
img1: ImgRef<RGB8>,
img2: ImgRef<RGB8>,
strip_height: usize,
) -> Result<(), 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 });
}
if strip_height < MIN_STRIP_HEIGHT {
return Err(ButteraugliError::ImageTooSmall {
width: strip_height,
height: MIN_STRIP_HEIGHT,
});
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_strip_walker_linear(
rgb1: &[f32],
rgb2: &[f32],
width: usize,
height: usize,
strip_height: usize,
params: &ButteraugliParams,
halo: usize,
want_diffmap: bool,
) -> Result<ButteraugliResult, ButteraugliError> {
let mut full_diffmap: Option<Vec<f32>> = if want_diffmap {
Some(vec![0.0; width * height])
} else {
None
};
let mut reducer = StripReducer::default();
let mut y = 0usize;
while y < height {
let mut next_y = (y + strip_height).next_multiple_of(STRIP_ALIGNMENT);
if next_y >= height || height - next_y < STRIP_ALIGNMENT {
next_y = height;
}
let interior_start = y;
let interior_end = next_y;
let halo_above = halo.min(interior_start);
let halo_below = halo.min(height - interior_end);
let strip_y0 = interior_start - halo_above;
let strip_y1 = interior_end + halo_below;
let strip_h_full = strip_y1 - strip_y0;
let rgb_offset = strip_y0 * width * 3;
let rgb_len = strip_h_full * width * 3;
let strip_rgb1 = &rgb1[rgb_offset..rgb_offset + rgb_len];
let strip_rgb2 = &rgb2[rgb_offset..rgb_offset + rgb_len];
let diffmap = if width < diff::MIN_SIZE_FOR_MULTIRESOLUTION
|| strip_h_full < diff::MIN_SIZE_FOR_MULTIRESOLUTION
{
compute_diffmap_single_resolution_linear(
strip_rgb1,
strip_rgb2,
width,
strip_h_full,
params,
)
} else {
compute_diffmap_multiresolution_linear(
strip_rgb1,
strip_rgb2,
width,
strip_h_full,
params,
)
};
let interior_y0_in_strip = interior_start - strip_y0;
let interior_y1_in_strip = interior_end - strip_y0;
reducer.add_strip(&diffmap, interior_y0_in_strip, interior_y1_in_strip);
if let Some(out) = full_diffmap.as_mut() {
for (dst_y, src_y) in
(interior_start..interior_end).zip(interior_y0_in_strip..interior_y1_in_strip)
{
let dst_row = &mut out[dst_y * width..(dst_y + 1) * width];
let src_row = diffmap.row(src_y);
dst_row.copy_from_slice(src_row);
}
}
y = next_y;
}
let total_pixels = (width as u64) * (height as u64);
let (score, pnorm_3) = reducer.finalise(total_pixels);
if !score.is_finite() {
return Err(ButteraugliError::NonFiniteResult);
}
Ok(ButteraugliResult {
score,
pnorm_3,
diffmap: full_diffmap.map(|buf| imgref::ImgVec::new(buf, width, height)),
})
}
impl ButteraugliReference {
pub fn compare_strip(
&self,
rgb: &[u8],
strip_height: u32,
) -> Result<ButteraugliResult, ButteraugliError> {
self.compare_strip_with_config(rgb, strip_height, ButteraugliStripConfig::default())
}
pub fn compare_strip_with_config(
&self,
rgb: &[u8],
strip_height: u32,
config: ButteraugliStripConfig,
) -> Result<ButteraugliResult, ButteraugliError> {
let width = self.width();
let height = self.height();
let expected = width * height * 3;
if rgb.len() != expected {
return Err(ButteraugliError::InvalidBufferSize {
expected,
actual: rgb.len(),
});
}
if (strip_height as usize) < MIN_STRIP_HEIGHT {
return Err(ButteraugliError::ImageTooSmall {
width: strip_height as usize,
height: MIN_STRIP_HEIGHT,
});
}
let linear1 = self
.source_linear_rgb()
.ok_or(ButteraugliError::InvalidParameter {
name: "reference",
value: 0.0,
reason: "compare_strip requires a reference built via \
ButteraugliReference::new or new_linear (the planar \
constructor does not retain interleaved source data)",
})?;
let lut = &*crate::opsin::SRGB_TO_LINEAR_LUT;
let linear2: Vec<f32> = rgb.iter().map(|&v| lut[v as usize]).collect();
run_strip_walker_linear(
linear1,
&linear2,
width,
height,
strip_height as usize,
self.params(),
config.halo_rows,
self.params().compute_diffmap(),
)
}
pub fn compare_linear_strip(
&self,
rgb: &[f32],
strip_height: u32,
) -> Result<ButteraugliResult, ButteraugliError> {
self.compare_linear_strip_with_config(rgb, strip_height, ButteraugliStripConfig::default())
}
pub fn compare_linear_strip_with_config(
&self,
rgb: &[f32],
strip_height: u32,
config: ButteraugliStripConfig,
) -> Result<ButteraugliResult, ButteraugliError> {
let width = self.width();
let height = self.height();
let expected = width * height * 3;
if rgb.len() != expected {
return Err(ButteraugliError::InvalidBufferSize {
expected,
actual: rgb.len(),
});
}
if (strip_height as usize) < MIN_STRIP_HEIGHT {
return Err(ButteraugliError::ImageTooSmall {
width: strip_height as usize,
height: MIN_STRIP_HEIGHT,
});
}
check_finite_f32(rgb, "compare_linear_strip rgb")?;
let linear1 = self
.source_linear_rgb()
.ok_or(ButteraugliError::InvalidParameter {
name: "reference",
value: 0.0,
reason: "compare_linear_strip requires a reference built via \
ButteraugliReference::new or new_linear (the planar \
constructor does not retain interleaved source data)",
})?;
run_strip_walker_linear(
linear1,
rgb,
width,
height,
strip_height as usize,
self.params(),
config.halo_rows,
self.params().compute_diffmap(),
)
}
pub fn compare_strip_srgb(
&self,
img: ImgRef<RGB8>,
strip_height: u32,
) -> Result<ButteraugliResult, ButteraugliError> {
if img.width() != self.width() || img.height() != self.height() {
return Err(ButteraugliError::DimensionMismatch {
w1: self.width(),
h1: self.height(),
w2: img.width(),
h2: img.height(),
});
}
let linear = imgref_srgb_to_linear_f32(img);
self.compare_linear_strip(&linear, strip_height)
}
pub fn compare_strip_linear_imgref(
&self,
img: ImgRef<RGB<f32>>,
strip_height: u32,
) -> Result<ButteraugliResult, ButteraugliError> {
if img.width() != self.width() || img.height() != self.height() {
return Err(ButteraugliError::DimensionMismatch {
w1: self.width(),
h1: self.height(),
w2: img.width(),
h2: img.height(),
});
}
let linear = imgref_rgbf32_to_f32_vec(img);
check_finite_f32(&linear, "linear rgb")?;
self.compare_linear_strip(&linear, strip_height)
}
}