use super::simd::{sum, sum_squared_deviation};
use super::{Plane, SsimOptions, SsimOutcome};
use crate::SsimError;
#[derive(Clone, Copy, Debug)]
pub struct HitchhikersOptions {
pub window_stride: Option<usize>,
pub cov_pooling: bool,
}
impl Default for HitchhikersOptions {
fn default() -> Self {
Self {
window_stride: None,
cov_pooling: true,
}
}
}
pub fn hitchhikers_ssim(
image1: &Plane,
image2: &Plane,
options: &SsimOptions,
hitchhikers_options: &HitchhikersOptions,
) -> Result<SsimOutcome, SsimError> {
Plane::validate_pair(image1, image2)?;
let (width, height) = (image1.width, image1.height);
let window_size = options.window_size;
if width < window_size || height < window_size {
return Err(SsimError::InputTooSmall {
width: width as u32,
height: height as u32,
minimum: window_size as u32,
});
}
let stride = hitchhikers_options.window_stride.unwrap_or(window_size);
if stride == 0 {
return Err(SsimError::Options(
"hitchhikers-ssim window stride must be at least 1".to_string(),
));
}
let dynamic_range = options.dynamic_range();
let c1 = (options.k1 * dynamic_range).powi(2);
let c2 = (options.k2 * dynamic_range).powi(2);
let luma1 = &image1.samples;
let luma2 = &image2.samples;
let integral1 = IntegralImage::build(width, height, |i| luma1[i]);
let integral2 = IntegralImage::build(width, height, |i| luma2[i]);
let integral1_sq = IntegralImage::build(width, height, |i| luma1[i] * luma1[i]);
let integral2_sq = IntegralImage::build(width, height, |i| luma2[i] * luma2[i]);
let integral12 = IntegralImage::build(width, height, |i| luma1[i] * luma2[i]);
let map_width = (width - window_size) / stride + 1;
let map_height = (height - window_size) / stride + 1;
let mut map = vec![0f32; map_width * map_height];
let window_area = (window_size * window_size) as f64;
for y in 0..map_height {
let top = y * stride;
let bottom = top + window_size;
for x in 0..map_width {
let left = x * stride;
let right = left + window_size;
let mu1 = integral1.window_sum(left, top, right, bottom) / window_area;
let mu2 = integral2.window_sum(left, top, right, bottom) / window_area;
let mu1_sq = mu1 * mu1;
let mu2_sq = mu2 * mu2;
let mu1_mu2 = mu1 * mu2;
let sigma1_sq =
integral1_sq.window_sum(left, top, right, bottom) / window_area - mu1_sq;
let sigma2_sq =
integral2_sq.window_sum(left, top, right, bottom) / window_area - mu2_sq;
let sigma12 = integral12.window_sum(left, top, right, bottom) / window_area - mu1_mu2;
let numerator = (2.0 * mu1_mu2 + c1) * (2.0 * sigma12 + c2);
let denominator = (mu1_sq + mu2_sq + c1) * (sigma1_sq + sigma2_sq + c2);
map[y * map_width + x] = (numerator / denominator) as f32;
}
}
let score = if hitchhikers_options.cov_pooling {
coefficient_of_variation_score(&map)
} else {
sum(&map) / map.len() as f64
};
Ok(SsimOutcome {
score,
map,
map_width,
map_height,
})
}
struct IntegralImage {
data: Vec<f64>,
stride: usize,
}
impl IntegralImage {
fn build(width: usize, height: usize, sample: impl Fn(usize) -> f32) -> Self {
let stride = width + 1;
let mut data = vec![0f64; stride * (height + 1)];
let mut prefix = vec![0f64; width];
for y in 0..height {
let mut running = 0f64;
for (x, slot) in prefix.iter_mut().enumerate() {
running += sample(y * width + x) as f64;
*slot = running;
}
let (above, current) = data.split_at_mut((y + 1) * stride);
let above = &above[y * stride + 1..][..width];
let current = &mut current[1..][..width];
for ((slot, up), left) in current.iter_mut().zip(above).zip(&prefix) {
*slot = up + left;
}
}
Self { data, stride }
}
#[inline]
fn window_sum(&self, left: usize, top: usize, right: usize, bottom: usize) -> f64 {
self.data[bottom * self.stride + right]
- self.data[top * self.stride + right]
- self.data[bottom * self.stride + left]
+ self.data[top * self.stride + left]
}
}
fn coefficient_of_variation_score(map: &[f32]) -> f64 {
let mean = sum(map) / map.len() as f64;
let variance = sum_squared_deviation(map, mean as f32) / map.len() as f64;
if mean > 0.0 {
1.0 - variance.sqrt() / mean
} else {
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plane(width: usize, height: usize, f: impl Fn(usize, usize) -> f32) -> Plane {
Plane {
samples: (0..width * height)
.map(|i| f(i % width, i / width))
.collect(),
width,
height,
}
}
#[test]
fn integral_window_sums_match_direct_summation() {
let (width, height) = (17, 13);
let samples: Vec<f32> = (0..width * height).map(|i| (i % 29) as f32).collect();
let integral = IntegralImage::build(width, height, |i| samples[i]);
for (left, top, right, bottom) in [(0, 0, 17, 13), (3, 2, 14, 11), (5, 5, 6, 6)] {
let expected: f64 = (top..bottom)
.flat_map(|y| (left..right).map(move |x| (y, x)))
.map(|(y, x)| samples[y * width + x] as f64)
.sum();
let actual = integral.window_sum(left, top, right, bottom);
assert!((actual - expected).abs() < 1e-6, "{actual} vs {expected}");
}
}
#[test]
fn identical_images_score_one() {
let image = plane(64, 48, |x, y| ((x * 7 + y * 13) % 256) as f32);
let outcome = hitchhikers_ssim(
&image,
&image,
&SsimOptions::default(),
&HitchhikersOptions::default(),
)
.unwrap();
assert!((outcome.score - 1.0).abs() < 1e-12, "{}", outcome.score);
assert!(outcome.map.iter().all(|value| (value - 1.0).abs() < 1e-6));
}
#[test]
fn map_covers_non_overlapping_windows_by_default() {
let image = plane(64, 48, |x, y| ((x + y) % 256) as f32);
let outcome = hitchhikers_ssim(
&image,
&image,
&SsimOptions::default(),
&HitchhikersOptions::default(),
)
.unwrap();
assert_eq!(outcome.map_width, (64 - 11) / 11 + 1);
assert_eq!(outcome.map_height, (48 - 11) / 11 + 1);
}
#[test]
fn stride_one_produces_a_dense_map() {
let image = plane(64, 48, |x, y| ((x + y) % 256) as f32);
let outcome = hitchhikers_ssim(
&image,
&image,
&SsimOptions::default(),
&HitchhikersOptions {
window_stride: Some(1),
..Default::default()
},
)
.unwrap();
assert_eq!(outcome.map_width, 64 - 11 + 1);
assert_eq!(outcome.map_height, 48 - 11 + 1);
}
#[test]
fn mean_pooling_is_available_and_differs_from_cov() {
let base = plane(96, 96, |x, y| ((x * 5 + y * 3) % 240) as f32);
let scrambled = plane(96, 96, |x, y| (((x * 91 + y * 173) % 256) ^ 0x5a) as f32);
let options = SsimOptions::default();
let cov = hitchhikers_ssim(&base, &scrambled, &options, &HitchhikersOptions::default())
.unwrap()
.score;
let mean = hitchhikers_ssim(
&base,
&scrambled,
&options,
&HitchhikersOptions {
cov_pooling: false,
..Default::default()
},
)
.unwrap()
.score;
assert!((cov - mean).abs() > 1e-6);
}
#[test]
fn images_below_the_window_size_are_rejected() {
let tiny = plane(8, 8, |_, _| 128.0);
assert!(matches!(
hitchhikers_ssim(
&tiny,
&tiny,
&SsimOptions::default(),
&HitchhikersOptions::default()
),
Err(SsimError::InputTooSmall { .. })
));
}
}