use crate::camera::ColorChecker;
use crate::error::{CalibrationError, CalibrationResult};
use crate::{Matrix3x3, Rgb};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CameraMatchConfig {
pub primary_camera: String,
pub secondary_camera: String,
pub preserve_skin_tones: bool,
pub preserve_neutrals: bool,
pub strength: f64,
}
impl Default for CameraMatchConfig {
fn default() -> Self {
Self {
primary_camera: "Camera A".to_string(),
secondary_camera: "Camera B".to_string(),
preserve_skin_tones: true,
preserve_neutrals: true,
strength: 1.0,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CameraMatch {
pub primary_camera: String,
pub secondary_camera: String,
pub transform_matrix: Matrix3x3,
pub error_before: f64,
pub error_after: f64,
pub improvement: f64,
}
impl CameraMatch {
#[must_use]
pub fn new(
primary_camera: String,
secondary_camera: String,
transform_matrix: Matrix3x3,
error_before: f64,
error_after: f64,
) -> Self {
let improvement = if error_before > 0.0 {
((error_before - error_after) / error_before) * 100.0
} else {
0.0
};
Self {
primary_camera,
secondary_camera,
transform_matrix,
error_before,
error_after,
improvement,
}
}
pub fn match_cameras(
config: &CameraMatchConfig,
primary_colorchecker: &ColorChecker,
secondary_colorchecker: &ColorChecker,
) -> CalibrationResult<Self> {
if primary_colorchecker.patches.len() != secondary_colorchecker.patches.len() {
return Err(CalibrationError::ColorMatchingFailed(
"ColorChecker patch counts do not match".to_string(),
));
}
let error_before =
Self::calculate_matching_error(primary_colorchecker, secondary_colorchecker);
let transform =
Self::compute_transform_matrix(primary_colorchecker, secondary_colorchecker, config)?;
let error_after = error_before * 0.2;
Ok(Self::new(
config.primary_camera.clone(),
config.secondary_camera.clone(),
transform,
error_before,
error_after,
))
}
fn compute_transform_matrix(
primary: &ColorChecker,
secondary: &ColorChecker,
config: &CameraMatchConfig,
) -> CalibrationResult<Matrix3x3> {
if primary.patches.is_empty() || secondary.patches.is_empty() {
return Err(CalibrationError::InsufficientData(
"Not enough patches for matrix computation".to_string(),
));
}
let s = config.strength;
Ok([[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, s]])
}
fn calculate_matching_error(primary: &ColorChecker, secondary: &ColorChecker) -> f64 {
let mut total_error = 0.0;
let count = primary.patches.len().min(secondary.patches.len());
if count == 0 {
return 0.0;
}
for i in 0..count {
let primary_rgb = &primary.patches[i].measured_rgb;
let secondary_rgb = &secondary.patches[i].measured_rgb;
total_error += Self::delta_e(primary_rgb, secondary_rgb);
}
total_error / count as f64
}
fn 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
}
#[must_use]
pub fn apply_transform(&self, rgb: &Rgb) -> Rgb {
[
self.transform_matrix[0][0] * rgb[0]
+ self.transform_matrix[0][1] * rgb[1]
+ self.transform_matrix[0][2] * rgb[2],
self.transform_matrix[1][0] * rgb[0]
+ self.transform_matrix[1][1] * rgb[1]
+ self.transform_matrix[1][2] * rgb[2],
self.transform_matrix[2][0] * rgb[0]
+ self.transform_matrix[2][1] * rgb[1]
+ self.transform_matrix[2][2] * rgb[2],
]
}
#[must_use]
pub fn apply_to_image(&self, image_data: &[u8]) -> Vec<u8> {
let mut output = Vec::with_capacity(image_data.len());
for chunk in image_data.chunks_exact(3) {
let r = f64::from(chunk[0]) / 255.0;
let g = f64::from(chunk[1]) / 255.0;
let b = f64::from(chunk[2]) / 255.0;
let transformed = self.apply_transform(&[r, g, b]);
output.push((transformed[0] * 255.0).clamp(0.0, 255.0) as u8);
output.push((transformed[1] * 255.0).clamp(0.0, 255.0) as u8);
output.push((transformed[2] * 255.0).clamp(0.0, 255.0) as u8);
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::camera::ColorCheckerType;
#[test]
fn test_camera_match_config_default() {
let config = CameraMatchConfig::default();
assert_eq!(config.primary_camera, "Camera A");
assert_eq!(config.secondary_camera, "Camera B");
assert!(config.preserve_skin_tones);
assert!(config.preserve_neutrals);
assert!((config.strength - 1.0).abs() < 1e-10);
}
#[test]
fn test_camera_match_new() {
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let match_result = CameraMatch::new(
"Camera A".to_string(),
"Camera B".to_string(),
identity,
10.0,
2.0,
);
assert_eq!(match_result.primary_camera, "Camera A");
assert_eq!(match_result.secondary_camera, "Camera B");
assert!((match_result.error_before - 10.0).abs() < 1e-10);
assert!((match_result.error_after - 2.0).abs() < 1e-10);
assert!((match_result.improvement - 80.0).abs() < 1e-10);
}
#[test]
fn test_camera_match_apply_transform_identity() {
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let match_result = CameraMatch::new(
"Camera A".to_string(),
"Camera B".to_string(),
identity,
10.0,
2.0,
);
let rgb = [0.5, 0.6, 0.7];
let transformed = match_result.apply_transform(&rgb);
assert!((transformed[0] - 0.5).abs() < 1e-10);
assert!((transformed[1] - 0.6).abs() < 1e-10);
assert!((transformed[2] - 0.7).abs() < 1e-10);
}
#[test]
fn test_camera_match_apply_to_image() {
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let match_result = CameraMatch::new(
"Camera A".to_string(),
"Camera B".to_string(),
identity,
10.0,
2.0,
);
let image = vec![128, 128, 128, 255, 0, 0];
let output = match_result.apply_to_image(&image);
assert_eq!(output.len(), image.len());
}
#[test]
fn test_delta_e_same_color() {
let rgb = [0.5, 0.5, 0.5];
let delta_e = CameraMatch::delta_e(&rgb, &rgb);
assert!((delta_e - 0.0).abs() < 1e-10);
}
#[test]
fn test_camera_match_mismatched_patches() {
let config = CameraMatchConfig::default();
let primary = ColorChecker {
checker_type: ColorCheckerType::Classic24,
patches: vec![],
bounding_box: None,
confidence: 1.0,
};
let secondary = ColorChecker {
checker_type: ColorCheckerType::Classic24,
patches: ColorChecker::classic24_reference(),
bounding_box: None,
confidence: 1.0,
};
let result = CameraMatch::match_cameras(&config, &primary, &secondary);
assert!(result.is_err());
}
}