use archmage::autoversion;
use crate::metric::{FEATURES_PER_CHANNEL_BASIC, config_from_params, validate_pair};
use crate::source::ImageSource;
use crate::streaming::PrecomputedReference;
use crate::{ZensimError, ZensimResult};
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum DiffmapWeighting {
Trained,
Balanced,
Custom([f32; 3]),
}
impl Default for DiffmapWeighting {
fn default() -> Self {
Self::Trained
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DiffmapOptions {
pub weighting: DiffmapWeighting,
pub masking_strength: Option<f32>,
pub sqrt: bool,
pub include_edge_mse: bool,
pub include_hf: bool,
}
impl From<DiffmapWeighting> for DiffmapOptions {
fn from(w: DiffmapWeighting) -> Self {
Self {
weighting: w,
..Default::default()
}
}
}
#[derive(Clone, Copy, Default)]
pub(crate) struct PixelFeatureWeights {
pub ssim: f32,
pub art: f32,
pub det: f32,
pub mse: f32,
pub hf_loss: f32,
pub hf_mag: f32,
pub hf_gain: f32,
}
impl PixelFeatureWeights {
pub fn needs_edge_mse(&self) -> bool {
self.art != 0.0 || self.det != 0.0 || self.mse != 0.0
}
pub fn needs_hf(&self) -> bool {
self.hf_loss != 0.0 || self.hf_mag != 0.0 || self.hf_gain != 0.0
}
}
impl DiffmapWeighting {
fn resolve_multiscale(
self,
profile_weights: &[f64],
num_scales: usize,
include_edge_mse: bool,
include_hf: bool,
) -> (Vec<[PixelFeatureWeights; 3]>, Vec<f32>) {
match self {
Self::Trained => trained_multiscale_weights(
profile_weights,
num_scales,
include_edge_mse,
include_hf,
),
Self::Balanced => {
let pw = PixelFeatureWeights {
ssim: 1.0,
..Default::default()
};
let ch = [
PixelFeatureWeights { ssim: 0.15, ..pw },
PixelFeatureWeights { ssim: 0.70, ..pw },
PixelFeatureWeights { ssim: 0.15, ..pw },
];
let n_groups = 1 + include_edge_mse as usize * 3 + include_hf as usize * 3;
let per_group = 1.0 / n_groups as f32;
let ch = ch.map(|c| PixelFeatureWeights {
ssim: c.ssim * per_group,
art: if include_edge_mse {
c.ssim * per_group
} else {
0.0
},
det: if include_edge_mse {
c.ssim * per_group
} else {
0.0
},
mse: if include_edge_mse {
c.ssim * per_group
} else {
0.0
},
hf_loss: if include_hf { c.ssim * per_group } else { 0.0 },
hf_mag: if include_hf { c.ssim * per_group } else { 0.0 },
hf_gain: if include_hf { c.ssim * per_group } else { 0.0 },
});
let blend = 1.0 / num_scales as f32;
(vec![ch; num_scales], vec![blend; num_scales])
}
Self::Custom(w) => {
let nw = normalize_weights(w);
let ch = [nw[0], nw[1], nw[2]].map(|s| {
let n_groups = 1 + include_edge_mse as usize * 3 + include_hf as usize * 3;
let per_group = 1.0 / n_groups as f32;
PixelFeatureWeights {
ssim: s * per_group,
art: if include_edge_mse { s * per_group } else { 0.0 },
det: if include_edge_mse { s * per_group } else { 0.0 },
mse: if include_edge_mse { s * per_group } else { 0.0 },
hf_loss: if include_hf { s * per_group } else { 0.0 },
hf_mag: if include_hf { s * per_group } else { 0.0 },
hf_gain: if include_hf { s * per_group } else { 0.0 },
}
});
let blend = 1.0 / num_scales as f32;
(vec![ch; num_scales], vec![blend; num_scales])
}
}
}
}
fn trained_multiscale_weights(
weights: &[f64],
num_scales: usize,
include_edge_mse: bool,
include_hf: bool,
) -> (Vec<[PixelFeatureWeights; 3]>, Vec<f32>) {
const FPC: usize = FEATURES_PER_CHANNEL_BASIC;
const FPS: usize = FPC * 3;
let mut per_scale = Vec::with_capacity(num_scales);
let mut scale_totals = Vec::with_capacity(num_scales);
for s in 0..num_scales {
let scale_base = s * FPS;
let mut ssim_w = [0.0f64; 3];
let mut art_w = [0.0f64; 3];
let mut det_w = [0.0f64; 3];
let mut mse_w = [0.0f64; 3];
let mut hf_loss_w = [0.0f64; 3];
let mut hf_mag_w = [0.0f64; 3];
let mut hf_gain_w = [0.0f64; 3];
let mut scale_total = 0.0f64;
for c in 0..3 {
let base = scale_base + c * FPC;
if base + 2 < weights.len() {
ssim_w[c] = weights[base].abs() + weights[base + 1].abs() + weights[base + 2].abs();
}
if include_edge_mse && base + 9 < weights.len() {
art_w[c] =
weights[base + 3].abs() + weights[base + 4].abs() + weights[base + 5].abs();
det_w[c] =
weights[base + 6].abs() + weights[base + 7].abs() + weights[base + 8].abs();
mse_w[c] = weights[base + 9].abs();
}
if include_hf && base + 12 < weights.len() {
hf_loss_w[c] = weights[base + 10].abs();
hf_mag_w[c] = weights[base + 11].abs();
hf_gain_w[c] = weights[base + 12].abs();
}
for f in 0..FPC {
if base + f < weights.len() {
scale_total += weights[base + f].abs();
}
}
}
let feat_total: f64 = ssim_w.iter().sum::<f64>()
+ art_w.iter().sum::<f64>()
+ det_w.iter().sum::<f64>()
+ mse_w.iter().sum::<f64>()
+ hf_loss_w.iter().sum::<f64>()
+ hf_mag_w.iter().sum::<f64>()
+ hf_gain_w.iter().sum::<f64>();
let ch_weights = if feat_total > 0.0 {
core::array::from_fn(|c| PixelFeatureWeights {
ssim: (ssim_w[c] / feat_total) as f32,
art: (art_w[c] / feat_total) as f32,
det: (det_w[c] / feat_total) as f32,
mse: (mse_w[c] / feat_total) as f32,
hf_loss: (hf_loss_w[c] / feat_total) as f32,
hf_mag: (hf_mag_w[c] / feat_total) as f32,
hf_gain: (hf_gain_w[c] / feat_total) as f32,
})
} else {
let eq = 1.0 / 3.0;
[PixelFeatureWeights {
ssim: eq,
..Default::default()
}; 3]
};
per_scale.push(ch_weights);
scale_totals.push(scale_total);
}
let total: f64 = scale_totals.iter().sum();
let blend = if total > 0.0 {
scale_totals.iter().map(|&s| (s / total) as f32).collect()
} else {
let w = 1.0 / num_scales as f32;
vec![w; num_scales]
};
(per_scale, blend)
}
fn apply_contrast_masking(
diffmap: &mut [f32],
precomputed: &PrecomputedReference,
width: usize,
height: usize,
padded_width: usize,
strength: f32,
) {
let (ref planes, pw, ph) = precomputed.scales[0];
debug_assert_eq!(pw, padded_width);
debug_assert_eq!(ph, height);
let y_plane = &planes[1];
let r = 5usize;
let iw = width + 1;
let ih = height + 1;
let mut int_sum = vec![0.0f64; iw * ih];
let mut int_sq = vec![0.0f64; iw * ih];
for y in 0..height {
let mut row_sum = 0.0f64;
let mut row_sq = 0.0f64;
for x in 0..width {
let v = y_plane[y * padded_width + x] as f64;
row_sum += v;
row_sq += v * v;
let idx = (y + 1) * iw + (x + 1);
int_sum[idx] = row_sum + int_sum[idx - iw];
int_sq[idx] = row_sq + int_sq[idx - iw];
}
}
let dims = [width, iw, r];
for dy in 0..height {
apply_masking_row(
&mut diffmap[dy * width..(dy + 1) * width],
&int_sum,
&int_sq,
dy,
dims,
height,
strength,
);
}
}
#[autoversion]
fn apply_masking_row(
dm_row: &mut [f32],
int_sum: &[f64],
int_sq: &[f64],
dy: usize,
dims: [usize; 3],
height: usize,
strength: f32,
) {
let [width, iw, r] = dims;
let y0 = dy.saturating_sub(r);
let y1 = (dy + r + 1).min(height);
for (dx, dm_val) in dm_row[..width].iter_mut().enumerate() {
let x0 = dx.saturating_sub(r);
let x1 = (dx + r + 1).min(width);
let tl = y0 * iw + x0;
let tr = y0 * iw + x1;
let bl = y1 * iw + x0;
let br = y1 * iw + x1;
let sum = int_sum[br] - int_sum[tr] - int_sum[bl] + int_sum[tl];
let sq = int_sq[br] - int_sq[tr] - int_sq[bl] + int_sq[tl];
let count = ((y1 - y0) * (x1 - x0)) as f64;
let mean = sum / count;
let variance = (sq / count - mean * mean).max(0.0) as f32;
let mask = 1.0 + strength * variance;
*dm_val /= mask;
}
}
#[autoversion]
fn sqrt_inplace(data: &mut [f32]) {
for v in data.iter_mut() {
*v = v.sqrt();
}
}
fn normalize_weights(w: [f32; 3]) -> [f32; 3] {
let sum = w[0] + w[1] + w[2];
if sum > 0.0 {
[w[0] / sum, w[1] / sum, w[2] / sum]
} else {
[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]
}
}
#[non_exhaustive]
pub struct DiffmapResult {
result: ZensimResult,
diffmap: Vec<f32>,
width: usize,
height: usize,
}
impl DiffmapResult {
pub fn result(&self) -> &ZensimResult {
&self.result
}
pub fn score(&self) -> f64 {
self.result.score()
}
pub fn diffmap(&self) -> &[f32] {
&self.diffmap
}
pub fn into_parts(self) -> (ZensimResult, Vec<f32>, usize, usize) {
(self.result, self.diffmap, self.width, self.height)
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
}
impl crate::metric::Zensim {
pub fn compute_with_ref_and_diffmap(
&self,
precomputed: &PrecomputedReference,
distorted: &impl ImageSource,
options: impl Into<DiffmapOptions>,
) -> Result<DiffmapResult, ZensimError> {
let options = options.into();
let params = self.profile().params();
if distorted.width() < 8 || distorted.height() < 8 {
return Err(ZensimError::ImageTooSmall);
}
let width = distorted.width();
let height = distorted.height();
let config = config_from_params(params, self.parallel());
let (per_scale_ch, scale_blend) = options.weighting.resolve_multiscale(
params.weights,
config.num_scales,
options.include_edge_mse,
options.include_hf,
);
let (result, diffmap_padded, padded_width) =
crate::streaming::compute_zensim_streaming_with_ref_and_diffmap(
precomputed,
distorted,
&config,
params.weights,
&per_scale_ch,
&scale_blend,
);
let result = result.with_profile(self.profile());
let mut diffmap = if padded_width == width {
diffmap_padded
} else {
let mut out = Vec::with_capacity(width * height);
for y in 0..height {
out.extend_from_slice(&diffmap_padded[y * padded_width..y * padded_width + width]);
}
out
};
if let Some(strength) = options.masking_strength {
apply_contrast_masking(
&mut diffmap,
precomputed,
width,
height,
padded_width,
strength,
);
}
if options.sqrt {
sqrt_inplace(&mut diffmap);
}
Ok(DiffmapResult {
result,
diffmap,
width,
height,
})
}
pub fn compute_with_ref_and_diffmap_linear_planar(
&self,
precomputed: &PrecomputedReference,
planes: [&[f32]; 3],
width: usize,
height: usize,
stride: usize,
options: impl Into<DiffmapOptions>,
) -> Result<DiffmapResult, ZensimError> {
let options = options.into();
let params = self.profile().params();
if width < 8 || height < 8 {
return Err(ZensimError::ImageTooSmall);
}
let config = config_from_params(params, self.parallel());
let (per_scale_ch, scale_blend) = options.weighting.resolve_multiscale(
params.weights,
config.num_scales,
options.include_edge_mse,
options.include_hf,
);
let padded_width = crate::blur::simd_padded_width(width);
let (result, diffmap_padded, _) =
crate::streaming::compute_zensim_streaming_with_ref_and_diffmap_linear_planar(
precomputed,
planes,
width,
height,
stride,
&config,
params.weights,
&per_scale_ch,
&scale_blend,
);
let result = result.with_profile(self.profile());
let mut diffmap = if padded_width == width {
diffmap_padded
} else {
let mut out = Vec::with_capacity(width * height);
for y in 0..height {
out.extend_from_slice(&diffmap_padded[y * padded_width..y * padded_width + width]);
}
out
};
if let Some(strength) = options.masking_strength {
apply_contrast_masking(
&mut diffmap,
precomputed,
width,
height,
padded_width,
strength,
);
}
if options.sqrt {
sqrt_inplace(&mut diffmap);
}
Ok(DiffmapResult {
result,
diffmap,
width,
height,
})
}
pub fn compute_with_diffmap(
&self,
source: &impl ImageSource,
distorted: &impl ImageSource,
options: impl Into<DiffmapOptions>,
) -> Result<DiffmapResult, ZensimError> {
validate_pair(source, distorted)?;
let precomputed = self.precompute_reference(source)?;
self.compute_with_ref_and_diffmap(&precomputed, distorted, options)
}
}
#[cfg(test)]
mod tests {
use super::DiffmapWeighting;
use crate::{RgbSlice, ZensimProfile};
#[test]
fn test_diffmap_identical_images() {
let pixels: Vec<[u8; 3]> = (0..64)
.map(|i| [i as u8 * 4, 128, 255 - i as u8 * 4])
.collect();
let z = crate::Zensim::new(ZensimProfile::latest());
let src = RgbSlice::new(&pixels, 8, 8);
let result = z
.compute_with_diffmap(&src, &src, DiffmapWeighting::default())
.unwrap();
assert_eq!(result.width(), 8);
assert_eq!(result.height(), 8);
assert_eq!(result.diffmap().len(), 64);
let max_err = result.diffmap().iter().copied().fold(0.0f32, f32::max);
assert!(
max_err < 1e-4,
"max diffmap error for identical images: {max_err}"
);
assert!(result.score() > 95.0, "score: {}", result.score());
}
#[test]
fn test_diffmap_localized_error() {
let src_pixels: Vec<[u8; 3]> = vec![[128, 128, 128]; 16 * 16];
let mut dst_pixels = src_pixels.clone();
for y in 0..4 {
for x in 0..4 {
dst_pixels[y * 16 + x] = [255, 255, 255];
}
}
let z = crate::Zensim::new(ZensimProfile::latest());
let src = RgbSlice::new(&src_pixels, 16, 16);
let dst = RgbSlice::new(&dst_pixels, 16, 16);
let result = z
.compute_with_diffmap(&src, &dst, DiffmapWeighting::Balanced)
.unwrap();
let dm = result.diffmap();
assert_eq!(dm.len(), 256);
let mut distorted_sum = 0.0f32;
for y in 0..4 {
for x in 0..4 {
distorted_sum += dm[y * 16 + x];
}
}
let distorted_avg = distorted_sum / 16.0;
let mut clean_sum = 0.0f32;
for y in 8..12 {
for x in 8..12 {
clean_sum += dm[y * 16 + x];
}
}
let clean_avg = clean_sum / 16.0;
assert!(
distorted_avg > clean_avg,
"distorted region avg ({distorted_avg}) should exceed clean region avg ({clean_avg})"
);
}
#[test]
fn test_diffmap_masking_reduces_textured_error() {
let mut src_pixels: Vec<[u8; 3]> = Vec::with_capacity(16 * 16);
for y in 0..16 {
for x in 0..16 {
let v = if (x + y) % 2 == 0 { 200u8 } else { 60u8 };
src_pixels.push([v, v, v]);
}
}
let dst_pixels: Vec<[u8; 3]> = src_pixels
.iter()
.map(|p| {
[
p[0].saturating_add(30),
p[1].saturating_add(30),
p[2].saturating_add(30),
]
})
.collect();
let z = crate::Zensim::new(ZensimProfile::latest());
let src = RgbSlice::new(&src_pixels, 16, 16);
let dst = RgbSlice::new(&dst_pixels, 16, 16);
let raw = z
.compute_with_diffmap(&src, &dst, DiffmapWeighting::Balanced)
.unwrap();
let raw_max = raw.diffmap().iter().copied().fold(0.0f32, f32::max);
let masked = z
.compute_with_diffmap(
&src,
&dst,
super::DiffmapOptions {
weighting: DiffmapWeighting::Balanced,
masking_strength: Some(4.0),
sqrt: false,
include_edge_mse: false,
include_hf: false,
},
)
.unwrap();
let masked_max = masked.diffmap().iter().copied().fold(0.0f32, f32::max);
assert!(
masked_max < raw_max,
"masked max ({masked_max}) should be less than raw max ({raw_max})"
);
assert!(
(raw.score() - masked.score()).abs() < 0.01,
"scores should match: raw {} vs masked {}",
raw.score(),
masked.score()
);
}
#[test]
fn test_diffmap_sqrt_compresses_range() {
let src_pixels: Vec<[u8; 3]> = vec![[128, 128, 128]; 16 * 16];
let mut dst_pixels = src_pixels.clone();
for y in 0..4 {
for x in 0..4 {
dst_pixels[y * 16 + x] = [255, 255, 255];
}
}
let z = crate::Zensim::new(ZensimProfile::latest());
let src = RgbSlice::new(&src_pixels, 16, 16);
let dst = RgbSlice::new(&dst_pixels, 16, 16);
let raw = z
.compute_with_diffmap(&src, &dst, DiffmapWeighting::Balanced)
.unwrap();
let raw_max = raw.diffmap().iter().copied().fold(0.0f32, f32::max);
let sqrted = z
.compute_with_diffmap(
&src,
&dst,
super::DiffmapOptions {
weighting: DiffmapWeighting::Balanced,
masking_strength: None,
sqrt: true,
include_edge_mse: false,
include_hf: false,
},
)
.unwrap();
let sqrt_max = sqrted.diffmap().iter().copied().fold(0.0f32, f32::max);
let expected_sqrt_max = raw_max.sqrt();
assert!(
(sqrt_max - expected_sqrt_max).abs() < 1e-5,
"sqrt max ({sqrt_max}) should equal sqrt(raw_max) = {expected_sqrt_max}"
);
}
#[test]
fn test_diffmap_weighting_into_options() {
let z = crate::Zensim::new(ZensimProfile::latest());
let pixels: Vec<[u8; 3]> = vec![[100, 150, 200]; 8 * 8];
let src = RgbSlice::new(&pixels, 8, 8);
let _ = z
.compute_with_diffmap(&src, &src, DiffmapWeighting::Trained)
.unwrap();
let _ = z
.compute_with_diffmap(&src, &src, DiffmapWeighting::Balanced)
.unwrap();
let _ = z
.compute_with_diffmap(&src, &src, DiffmapWeighting::Custom([0.3, 0.5, 0.2]))
.unwrap();
}
#[test]
fn test_diffmap_edge_mse_produces_valid_signal() {
let src_pixels: Vec<[u8; 3]> = vec![[128, 128, 128]; 16 * 16];
let mut dst_pixels = src_pixels.clone();
for y in 0..8 {
for x in 0..16 {
dst_pixels[y * 16 + x] = [200, 200, 200];
}
}
let z = crate::Zensim::new(ZensimProfile::latest());
let src = RgbSlice::new(&src_pixels, 16, 16);
let dst = RgbSlice::new(&dst_pixels, 16, 16);
let with_edge = z
.compute_with_diffmap(
&src,
&dst,
super::DiffmapOptions {
weighting: DiffmapWeighting::Balanced,
masking_strength: None,
sqrt: false,
include_edge_mse: true,
include_hf: false,
},
)
.unwrap();
assert!(
with_edge.diffmap().iter().all(|&v| v >= 0.0),
"all diffmap values should be non-negative"
);
let max = with_edge.diffmap().iter().copied().fold(0.0f32, f32::max);
assert!(max > 0.0, "max should be > 0 for distorted image");
let top_avg: f32 = with_edge.diffmap()[..128].iter().sum::<f32>() / 128.0;
let bot_avg: f32 = with_edge.diffmap()[128..].iter().sum::<f32>() / 128.0;
assert!(
top_avg > bot_avg,
"distorted region ({top_avg}) should exceed clean region ({bot_avg})"
);
let ssim_only = z
.compute_with_diffmap(&src, &dst, DiffmapWeighting::Balanced)
.unwrap();
assert!(
(ssim_only.score() - with_edge.score()).abs() < 0.01,
"scores should match: {} vs {}",
ssim_only.score(),
with_edge.score()
);
}
#[test]
fn test_diffmap_edge_mse_trained_weights() {
let src_pixels: Vec<[u8; 3]> = (0..256).map(|i| [(i % 256) as u8, 128, 64]).collect();
let mut dst_pixels = src_pixels.clone();
for p in dst_pixels[..64].iter_mut() {
p[0] = p[0].wrapping_add(40);
}
let z = crate::Zensim::new(ZensimProfile::latest());
let src = RgbSlice::new(&src_pixels, 16, 16);
let dst = RgbSlice::new(&dst_pixels, 16, 16);
let result = z
.compute_with_diffmap(
&src,
&dst,
super::DiffmapOptions {
weighting: DiffmapWeighting::Trained,
masking_strength: None,
sqrt: false,
include_edge_mse: true,
include_hf: false,
},
)
.unwrap();
assert_eq!(result.diffmap().len(), 256);
assert!(
result.diffmap().iter().all(|&v| v >= 0.0),
"all diffmap values should be non-negative"
);
let max = result.diffmap().iter().copied().fold(0.0f32, f32::max);
assert!(max > 0.0, "max diffmap value should be > 0");
}
#[test]
fn test_diffmap_with_precomputed_ref() {
let pixels: Vec<[u8; 3]> = (0..256).map(|i| [(i % 256) as u8, 128, 64]).collect();
let mut dst = pixels.clone();
for p in dst[..32].iter_mut() {
p[0] = p[0].wrapping_add(50);
}
let z = crate::Zensim::new(ZensimProfile::latest());
let src = RgbSlice::new(&pixels, 16, 16);
let dst = RgbSlice::new(&dst, 16, 16);
let precomputed = z.precompute_reference(&src).unwrap();
let result = z
.compute_with_ref_and_diffmap(&precomputed, &dst, DiffmapWeighting::Trained)
.unwrap();
assert_eq!(result.width(), 16);
assert_eq!(result.height(), 16);
let regular = z.compute_with_ref(&precomputed, &dst).unwrap();
assert!(
(result.score() - regular.score()).abs() < 0.01,
"diffmap score {} vs regular score {}",
result.score(),
regular.score()
);
}
#[test]
fn test_diffmap_no_nan() {
let z = crate::Zensim::new(ZensimProfile::latest());
let weightings = [
DiffmapWeighting::Trained,
DiffmapWeighting::Balanced,
DiffmapWeighting::Custom([1.0, 0.0, 0.0]),
];
let options_list = [
super::DiffmapOptions::default(),
super::DiffmapOptions {
weighting: DiffmapWeighting::Trained,
masking_strength: Some(4.0),
sqrt: true,
include_edge_mse: true,
include_hf: false,
},
super::DiffmapOptions {
weighting: DiffmapWeighting::Trained,
masking_strength: None,
sqrt: false,
include_edge_mse: false,
include_hf: true,
},
super::DiffmapOptions {
weighting: DiffmapWeighting::Trained,
masking_strength: Some(4.0),
sqrt: true,
include_edge_mse: true,
include_hf: true,
},
];
#[allow(clippy::type_complexity)]
let cases: Vec<(&str, usize, usize, Vec<[u8; 3]>, Vec<[u8; 3]>)> = vec![
{
let w = 64;
let h = 64;
let src = vec![[128, 128, 128]; w * h];
let dst = vec![[128, 128, 128]; w * h];
("uniform_identical", w, h, src, dst)
},
{
let w = 64;
let h = 64;
let src = vec![[0, 0, 0]; w * h];
let dst = vec![[0, 0, 0]; w * h];
("black_identical", w, h, src, dst)
},
{
let w = 64;
let h = 64;
let src = vec![[255, 255, 255]; w * h];
let dst = vec![[255, 255, 255]; w * h];
("white_identical", w, h, src, dst)
},
{
let w = 64;
let h = 64;
let src = vec![[0, 0, 0]; w * h];
let dst = vec![[255, 255, 255]; w * h];
("black_vs_white", w, h, src, dst)
},
{
let w = 128;
let h = 128;
let src: Vec<[u8; 3]> = (0..w * h)
.map(|i| {
let v = (i % 256) as u8;
[v, v, v]
})
.collect();
let dst = src
.iter()
.map(|p| [p[0].wrapping_add(1), p[1], p[2]])
.collect();
("near_identical_128", w, h, src, dst)
},
{
let w = 64;
let h = 64;
let src: Vec<[u8; 3]> = (0..w * h)
.map(|i| {
let x = i % w;
let y = i / w;
if (x + y) % 2 == 0 {
[0, 0, 0]
} else {
[255, 255, 255]
}
})
.collect();
let dst = src
.iter()
.map(|p| {
[
p[0].saturating_add(10),
p[1].saturating_add(10),
p[2].saturating_add(10),
]
})
.collect();
("checkerboard", w, h, src, dst)
},
];
for (label, w, h, src, dst) in &cases {
let src_img = RgbSlice::new(src, *w, *h);
let dst_img = RgbSlice::new(dst, *w, *h);
for weighting in &weightings {
let result = z
.compute_with_diffmap(&src_img, &dst_img, *weighting)
.unwrap();
let nan_count = result.diffmap().iter().filter(|v| v.is_nan()).count();
let inf_count = result.diffmap().iter().filter(|v| v.is_infinite()).count();
assert!(
nan_count == 0 && inf_count == 0,
"{label}: {nan_count} NaN, {inf_count} Inf in diffmap (len={})",
result.diffmap().len()
);
}
for options in &options_list {
let result = z
.compute_with_diffmap(&src_img, &dst_img, *options)
.unwrap();
let nan_count = result.diffmap().iter().filter(|v| v.is_nan()).count();
let inf_count = result.diffmap().iter().filter(|v| v.is_infinite()).count();
assert!(
nan_count == 0 && inf_count == 0,
"{label} (options): {nan_count} NaN, {inf_count} Inf in diffmap (len={})",
result.diffmap().len()
);
}
}
}
}