use crate::delta_e::delta_e_2000;
use crate::error::CalibrationResult;
use crate::{CalibrationError, Lab, Rgb};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CameraMeasurement {
pub camera_id: String,
pub patch_rgb: Vec<Rgb>,
pub reference_lab: Vec<Lab>,
}
impl CameraMeasurement {
pub fn new(
camera_id: impl Into<String>,
patch_rgb: Vec<Rgb>,
reference_lab: Vec<Lab>,
) -> CalibrationResult<Self> {
if patch_rgb.is_empty() {
return Err(CalibrationError::InvalidMeasurementData(
"patch_rgb must not be empty".to_string(),
));
}
if patch_rgb.len() != reference_lab.len() {
return Err(CalibrationError::InvalidMeasurementData(format!(
"patch_rgb ({}) and reference_lab ({}) lengths must match",
patch_rgb.len(),
reference_lab.len()
)));
}
Ok(Self {
camera_id: camera_id.into(),
patch_rgb,
reference_lab,
})
}
#[must_use]
pub fn patch_count(&self) -> usize {
self.patch_rgb.len()
}
#[must_use]
pub fn mean_delta_e(&self) -> f64 {
if self.patch_rgb.is_empty() {
return 0.0;
}
let total: f64 = self
.patch_rgb
.iter()
.zip(self.reference_lab.iter())
.map(|(rgb, ref_lab)| {
let lab = rgb_to_lab_approx(*rgb);
delta_e_2000(lab, *ref_lab)
})
.sum();
total / self.patch_rgb.len() as f64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SingleCameraCalibration {
pub camera_id: String,
pub is_reference: bool,
pub raw_delta_e: f64,
pub corrected_delta_e: f64,
pub correction_matrix: [[f64; 3]; 3],
pub gain_offsets: Rgb,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchCalibrationResult {
pub session_id: String,
pub reference_camera_id: String,
pub cameras: HashMap<String, SingleCameraCalibration>,
}
impl BatchCalibrationResult {
#[must_use]
pub fn best_match_camera(&self) -> Option<&str> {
self.cameras
.iter()
.filter(|(id, _)| id.as_str() != self.reference_camera_id.as_str())
.min_by(|(_, a), (_, b)| {
a.corrected_delta_e
.partial_cmp(&b.corrected_delta_e)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(id, _)| id.as_str())
}
#[must_use]
pub fn camera_count(&self) -> usize {
self.cameras.len()
}
#[must_use]
pub fn camera_calibration(&self, camera_id: &str) -> Option<&SingleCameraCalibration> {
self.cameras.get(camera_id)
}
}
pub struct BatchCalibrator {
session_id: String,
reference_camera_id: Option<String>,
measurements: Vec<CameraMeasurement>,
}
impl BatchCalibrator {
#[must_use]
pub fn new(session_id: impl Into<String>) -> Self {
Self {
session_id: session_id.into(),
reference_camera_id: None,
measurements: Vec::new(),
}
}
pub fn add_measurement(&mut self, measurement: CameraMeasurement) -> CalibrationResult<()> {
if self
.measurements
.iter()
.any(|m| m.camera_id == measurement.camera_id)
{
return Err(CalibrationError::InvalidMeasurementData(format!(
"Camera '{}' was already added",
measurement.camera_id
)));
}
self.measurements.push(measurement);
Ok(())
}
pub fn set_reference(&mut self, camera_id: impl Into<String>) {
self.reference_camera_id = Some(camera_id.into());
}
pub fn calibrate(&self) -> CalibrationResult<BatchCalibrationResult> {
if self.measurements.len() < 2 {
return Err(CalibrationError::InsufficientData(
"Batch calibration requires at least 2 cameras".to_string(),
));
}
let ref_id = self
.reference_camera_id
.clone()
.unwrap_or_else(|| self.measurements[0].camera_id.clone());
let reference = self
.measurements
.iter()
.find(|m| m.camera_id == ref_id)
.ok_or_else(|| {
CalibrationError::InvalidMeasurementData(format!(
"Reference camera '{ref_id}' not found in measurements"
))
})?;
let mut cameras = HashMap::new();
cameras.insert(
ref_id.clone(),
SingleCameraCalibration {
camera_id: ref_id.clone(),
is_reference: true,
raw_delta_e: reference.mean_delta_e(),
corrected_delta_e: reference.mean_delta_e(),
correction_matrix: identity_3x3(),
gain_offsets: [0.0, 0.0, 0.0],
},
);
for measurement in &self.measurements {
if measurement.camera_id == ref_id {
continue;
}
let matrix = compute_correction_matrix(&measurement.patch_rgb, &reference.patch_rgb);
let gain_offsets =
compute_gain_offsets(&measurement.patch_rgb, &reference.patch_rgb, &matrix);
let corrected_de = corrected_delta_e(
measurement,
&matrix,
&gain_offsets,
&reference.reference_lab,
);
cameras.insert(
measurement.camera_id.clone(),
SingleCameraCalibration {
camera_id: measurement.camera_id.clone(),
is_reference: false,
raw_delta_e: measurement.mean_delta_e(),
corrected_delta_e: corrected_de,
correction_matrix: matrix,
gain_offsets,
},
);
}
Ok(BatchCalibrationResult {
session_id: self.session_id.clone(),
reference_camera_id: ref_id,
cameras,
})
}
#[must_use]
pub fn camera_count(&self) -> usize {
self.measurements.len()
}
}
fn identity_3x3() -> [[f64; 3]; 3] {
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
}
fn compute_correction_matrix(src: &[Rgb], dst: &[Rgb]) -> [[f64; 3]; 3] {
let n = src.len();
if n < 3 {
return identity_3x3();
}
let mut sts = [[0.0f64; 3]; 3];
for p in src {
for i in 0..3 {
for j in 0..3 {
sts[i][j] += p[i] * p[j];
}
}
}
let sts_inv = match invert_3x3(sts) {
Some(inv) => inv,
None => return identity_3x3(),
};
let mut matrix = [[0.0f64; 3]; 3];
for c in 0..3 {
let mut rhs = [0.0f64; 3];
for (s, d) in src.iter().zip(dst.iter()) {
for i in 0..3 {
rhs[i] += s[i] * d[c];
}
}
for i in 0..3 {
let mut sum = 0.0;
for j in 0..3 {
sum += sts_inv[i][j] * rhs[j];
}
matrix[c][i] = sum;
}
}
matrix
}
fn compute_gain_offsets(src: &[Rgb], dst: &[Rgb], matrix: &[[f64; 3]; 3]) -> Rgb {
if src.is_empty() {
return [0.0, 0.0, 0.0];
}
let n = src.len() as f64;
let mut offset = [0.0f64; 3];
for (s, d) in src.iter().zip(dst.iter()) {
let corrected = apply_3x3(matrix, *s);
for i in 0..3 {
offset[i] += d[i] - corrected[i];
}
}
[offset[0] / n, offset[1] / n, offset[2] / n]
}
fn corrected_delta_e(
measurement: &CameraMeasurement,
matrix: &[[f64; 3]; 3],
gain_offsets: &Rgb,
reference_lab: &[Lab],
) -> f64 {
if measurement.patch_rgb.is_empty() {
return 0.0;
}
let total: f64 = measurement
.patch_rgb
.iter()
.zip(reference_lab.iter())
.map(|(rgb, ref_lab)| {
let corrected = apply_3x3(matrix, *rgb);
let gained: Rgb = [
(corrected[0] + gain_offsets[0]).clamp(0.0, 1.0),
(corrected[1] + gain_offsets[1]).clamp(0.0, 1.0),
(corrected[2] + gain_offsets[2]).clamp(0.0, 1.0),
];
let lab = rgb_to_lab_approx(gained);
delta_e_2000(lab, *ref_lab)
})
.sum();
total / measurement.patch_rgb.len() as f64
}
fn apply_3x3(m: &[[f64; 3]; 3], v: Rgb) -> Rgb {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
fn invert_3x3(m: [[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
if det.abs() < 1e-12 {
return None;
}
let inv_det = 1.0 / det;
Some([
[
(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
(m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
(m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
],
[
(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
(m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
(m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
],
[
(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
(m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
(m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
],
])
}
fn rgb_to_lab_approx(rgb: Rgb) -> Lab {
let r = rgb[0];
let g = rgb[1];
let b = rgb[2];
let x = 0.412_453 * r + 0.357_580 * g + 0.180_423 * b;
let y = 0.212_671 * r + 0.715_160 * g + 0.072_169 * b;
let z = 0.019_334 * r + 0.119_193 * g + 0.950_227 * b;
let xn = 0.950_47;
let yn = 1.0;
let zn = 1.088_83;
fn f(t: f64) -> f64 {
if t > 0.008_856 {
t.cbrt()
} else {
7.787 * t + 16.0 / 116.0
}
}
let fx = f(x / xn);
let fy = f(y / yn);
let fz = f(z / zn);
let l = 116.0 * fy - 16.0;
let a = 500.0 * (fx - fy);
let bb = 200.0 * (fy - fz);
[l, a, bb]
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn make_identity_patches(n: usize) -> (Vec<Rgb>, Vec<Lab>) {
let patches: Vec<Rgb> = (0..n)
.map(|i| {
let v = i as f64 / (n - 1).max(1) as f64;
[v, v, v]
})
.collect();
let lab: Vec<Lab> = patches.iter().map(|p| rgb_to_lab_approx(*p)).collect();
(patches, lab)
}
#[test]
fn test_camera_measurement_new_valid() -> TestResult {
let (patches, labs) = make_identity_patches(6);
let m = CameraMeasurement::new("CamA", patches, labs)?;
assert_eq!(m.patch_count(), 6);
Ok(())
}
#[test]
fn test_camera_measurement_new_length_mismatch() {
let patches = vec![[0.5f64; 3]; 4];
let labs = vec![[50.0f64, 0.0, 0.0]; 5];
assert!(CameraMeasurement::new("X", patches, labs).is_err());
}
#[test]
fn test_camera_measurement_new_empty() {
let m = CameraMeasurement::new("X", vec![], vec![]);
assert!(m.is_err());
}
#[test]
fn test_mean_delta_e_identical_patches() -> TestResult {
let (patches, labs) = make_identity_patches(8);
let m = CameraMeasurement::new("Cam", patches.clone(), labs.clone())?;
assert!(m.mean_delta_e() >= 0.0);
Ok(())
}
#[test]
fn test_batch_calibrator_requires_two_cameras() -> TestResult {
let mut cal = BatchCalibrator::new("session1");
let (p, l) = make_identity_patches(6);
cal.add_measurement(CameraMeasurement::new("A", p, l)?)?;
assert!(cal.calibrate().is_err());
Ok(())
}
#[test]
fn test_batch_calibrator_duplicate_camera() -> TestResult {
let mut cal = BatchCalibrator::new("session1");
let (p, l) = make_identity_patches(6);
cal.add_measurement(CameraMeasurement::new("A", p.clone(), l.clone())?)?;
let result = cal.add_measurement(CameraMeasurement::new("A", p, l)?);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_batch_calibrator_two_identical_cameras() -> TestResult {
let mut cal = BatchCalibrator::new("s");
let (p, l) = make_identity_patches(8);
cal.set_reference("Ref");
cal.add_measurement(CameraMeasurement::new("Ref", p.clone(), l.clone())?)?;
cal.add_measurement(CameraMeasurement::new("B", p, l)?)?;
let result = cal.calibrate()?;
assert_eq!(result.reference_camera_id, "Ref");
assert_eq!(result.camera_count(), 2);
let b = result
.camera_calibration("B")
.ok_or("camera B not found in result")?;
for i in 0..3 {
assert!((b.correction_matrix[i][i] - 1.0).abs() < 0.1);
}
Ok(())
}
#[test]
fn test_batch_calibrator_first_camera_as_reference() -> TestResult {
let mut cal = BatchCalibrator::new("s");
let (p, l) = make_identity_patches(6);
cal.add_measurement(CameraMeasurement::new("First", p.clone(), l.clone())?)?;
cal.add_measurement(CameraMeasurement::new("Second", p, l)?)?;
let result = cal.calibrate()?;
assert_eq!(result.reference_camera_id, "First");
Ok(())
}
#[test]
fn test_batch_result_camera_count() -> TestResult {
let mut cal = BatchCalibrator::new("s");
for i in 0..4 {
let (p, l) = make_identity_patches(6);
cal.add_measurement(CameraMeasurement::new(format!("Cam{i}"), p, l)?)?;
}
let result = cal.calibrate()?;
assert_eq!(result.camera_count(), 4);
Ok(())
}
#[test]
fn test_invert_identity() -> TestResult {
let id = identity_3x3();
let inv = invert_3x3(id).ok_or("invert_3x3 returned None for identity matrix")?;
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!((inv[i][j] - expected).abs() < 1e-9);
}
}
Ok(())
}
#[test]
fn test_invert_singular_returns_none() {
let singular = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
assert!(invert_3x3(singular).is_none());
}
}