mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 10:36:56 -06:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
58 lines
2.4 KiB
Rust
58 lines
2.4 KiB
Rust
//! Deterministic rendered-frame comparison helpers for the fidelity corpus.
|
||
|
||
use effectcraft_raster::Image;
|
||
|
||
/// How far a rendered frame is from its reference.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
pub struct FrameScore {
|
||
/// Pixels compared (the frames' width × height).
|
||
pub compared_pixels: usize,
|
||
/// Mean absolute difference over every channel sample.
|
||
pub mean_absolute_error: f64,
|
||
/// Largest absolute difference of any channel sample.
|
||
pub max_error: f32,
|
||
}
|
||
|
||
/// Compare two premultiplied rendered frames. A size mismatch is reported as an error instead of
|
||
/// silently producing a misleading score.
|
||
pub fn compare_frames(actual: &Image, reference: &Image) -> Result<FrameScore, String> {
|
||
if actual.width != reference.width || actual.height != reference.height || actual.data.len() != reference.data.len() {
|
||
return Err(format!("frame size mismatch: {}x{} vs {}x{}", actual.width, actual.height, reference.width, reference.height));
|
||
}
|
||
let mut sum = 0.0f64;
|
||
let mut max = 0.0f32;
|
||
for (a, b) in actual.data.iter().zip(&reference.data) {
|
||
for (&x, &y) in a.iter().zip(b) {
|
||
let error = (x - y).abs();
|
||
// A NaN would make the mean NaN, which a `mean > tolerance` check reads as a pass.
|
||
if !error.is_finite() {
|
||
return Err("frame has a non-finite sample".into());
|
||
}
|
||
sum += f64::from(error);
|
||
max = max.max(error);
|
||
}
|
||
}
|
||
let compared_pixels = actual.data.len();
|
||
let samples = compared_pixels.saturating_mul(4).max(1);
|
||
Ok(FrameScore { compared_pixels, mean_absolute_error: sum / samples as f64, max_error: max })
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn reports_pixel_error_and_rejects_size_mismatch() {
|
||
let mut actual = Image::filled(2, 1, [0.0, 0.0, 0.0, 1.0]);
|
||
let reference = Image::filled(2, 1, [0.0, 0.0, 0.0, 1.0]);
|
||
actual.data[1][0] = 0.5;
|
||
let score = compare_frames(&actual, &reference).expect("same size");
|
||
assert_eq!(score.compared_pixels, 2);
|
||
assert!((score.mean_absolute_error - 0.0625).abs() < f64::EPSILON);
|
||
assert_eq!(score.max_error, 0.5);
|
||
assert!(compare_frames(&actual, &Image::new(1, 1)).is_err());
|
||
actual.data[0][1] = f32::NAN;
|
||
assert!(compare_frames(&actual, &reference).is_err(), "NaN is an error, not a pass");
|
||
}
|
||
}
|