use crate::error::{CalibrationError, CalibrationResult};
use crate::lut::LutMeasurement;
use crate::Rgb;
use oximedia_lut::{Lut3d, LutInterpolation};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct VerificationResult {
pub average_error: f64,
pub max_error: f64,
pub min_error: f64,
pub point_count: usize,
pub within_1_percent: f64,
pub within_2_percent: f64,
pub within_3_percent: f64,
}
impl VerificationResult {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
average_error: f64,
max_error: f64,
min_error: f64,
point_count: usize,
within_1_percent: f64,
within_2_percent: f64,
within_3_percent: f64,
) -> Self {
Self {
average_error,
max_error,
min_error,
point_count,
within_1_percent,
within_2_percent,
within_3_percent,
}
}
#[must_use]
pub fn passes(&self, max_average_error: f64, max_single_error: f64) -> bool {
self.average_error <= max_average_error && self.max_error <= max_single_error
}
#[must_use]
pub fn grade(&self) -> char {
if self.average_error < 1.0 {
'A'
} else if self.average_error < 2.0 {
'B'
} else if self.average_error < 3.0 {
'C'
} else if self.average_error < 5.0 {
'D'
} else if self.average_error < 10.0 {
'E'
} else {
'F'
}
}
}
pub struct LutVerifier;
impl LutVerifier {
pub fn verify_lut(
lut: &Lut3d,
measurements: &LutMeasurement,
interpolation: LutInterpolation,
) -> CalibrationResult<VerificationResult> {
measurements.validate()?;
if measurements.points.is_empty() {
return Err(CalibrationError::InsufficientData(
"No measurement points for verification".to_string(),
));
}
let mut errors = Vec::with_capacity(measurements.points.len());
for point in &measurements.points {
let lut_output = lut.apply(&point.input, interpolation);
let error = Self::calculate_delta_e(&lut_output, &point.output);
errors.push(error);
}
let average_error = errors.iter().sum::<f64>() / errors.len() as f64;
let max_error = errors.iter().copied().fold(0.0_f64, f64::max);
let min_error = errors.iter().copied().fold(f64::MAX, f64::min);
let within_1 = errors.iter().filter(|&&e| e < 1.0).count();
let within_2 = errors.iter().filter(|&&e| e < 2.0).count();
let within_3 = errors.iter().filter(|&&e| e < 3.0).count();
let total = errors.len() as f64;
Ok(VerificationResult::new(
average_error,
max_error,
min_error,
errors.len(),
(within_1 as f64 / total) * 100.0,
(within_2 as f64 / total) * 100.0,
(within_3 as f64 / total) * 100.0,
))
}
fn calculate_delta_e(rgb1: &Rgb, rgb2: &Rgb) -> f64 {
let dr = rgb1[0] - rgb2[0];
let dg = rgb1[1] - rgb2[1];
let db = rgb1[2] - rgb2[2];
(dr * dr + dg * dg + db * db).sqrt() * 100.0
}
pub fn verify_consistency(_lut: &Lut3d) -> CalibrationResult<()> {
Ok(())
}
#[must_use]
pub fn generate_report(result: &VerificationResult) -> String {
format!(
"LUT Verification Report\n\
=======================\n\
Average Error: {:.2} Delta E\n\
Maximum Error: {:.2} Delta E\n\
Minimum Error: {:.2} Delta E\n\
Points Tested: {}\n\
Grade: {}\n\
\n\
Error Distribution:\n\
- Within Delta E 1.0: {:.1}%\n\
- Within Delta E 2.0: {:.1}%\n\
- Within Delta E 3.0: {:.1}%",
result.average_error,
result.max_error,
result.min_error,
result.point_count,
result.grade(),
result.within_1_percent,
result.within_2_percent,
result.within_3_percent
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use oximedia_lut::LutSize;
#[test]
fn test_verification_result_new() {
let result = VerificationResult::new(1.5, 5.0, 0.5, 100, 40.0, 80.0, 95.0);
assert!((result.average_error - 1.5).abs() < 1e-10);
assert!((result.max_error - 5.0).abs() < 1e-10);
assert!((result.min_error - 0.5).abs() < 1e-10);
assert_eq!(result.point_count, 100);
}
#[test]
fn test_verification_result_passes() {
let result = VerificationResult::new(1.5, 5.0, 0.5, 100, 40.0, 80.0, 95.0);
assert!(result.passes(2.0, 6.0));
assert!(!result.passes(1.0, 6.0));
assert!(!result.passes(2.0, 4.0));
}
#[test]
fn test_verification_result_grade() {
let result_a = VerificationResult::new(0.5, 2.0, 0.1, 100, 90.0, 100.0, 100.0);
assert_eq!(result_a.grade(), 'A');
let result_b = VerificationResult::new(1.5, 5.0, 0.5, 100, 40.0, 80.0, 95.0);
assert_eq!(result_b.grade(), 'B');
let result_f = VerificationResult::new(15.0, 30.0, 5.0, 100, 0.0, 10.0, 20.0);
assert_eq!(result_f.grade(), 'F');
}
#[test]
fn test_calculate_delta_e() {
let rgb1 = [0.5, 0.5, 0.5];
let rgb2 = [0.5, 0.5, 0.5];
let delta_e = LutVerifier::calculate_delta_e(&rgb1, &rgb2);
assert!((delta_e - 0.0).abs() < 1e-10);
}
#[test]
fn test_generate_report() {
let result = VerificationResult::new(1.5, 5.0, 0.5, 100, 40.0, 80.0, 95.0);
let report = LutVerifier::generate_report(&result);
assert!(report.contains("1.5"));
assert!(report.contains("5.0"));
assert!(report.contains("0.5"));
assert!(report.contains("100"));
assert!(report.contains("Grade: B"));
}
#[test]
fn test_verify_lut_empty_measurements() {
let lut = Lut3d::new(LutSize::Size17);
let measurements = LutMeasurement::new("Empty".to_string());
let result = LutVerifier::verify_lut(&lut, &measurements, LutInterpolation::Trilinear);
assert!(result.is_err());
}
}