#![allow(dead_code)]
use crate::Matrix3x3;
pub const XYZ_TO_AP1: Matrix3x3 = [
[1.641_023_4, -0.324_803_3, -0.236_408_6],
[-0.663_662_4, 1.615_300_8, 0.016_756_7],
[0.011_721_9, -0.008_284_4, 0.988_606_0],
];
pub const AP1_TO_XYZ: Matrix3x3 = [
[0.662_455_496_937, 0.134_006_798_567, 0.156_143_766_952],
[0.272_234_247_337, 0.674_095_649_956, 0.053_674_465_568],
[-0.005_573_443_506, 0.004_059_922_467, 1.010_123_708_657],
];
pub const ACES_D60_WHITE: [f64; 3] = [0.952_65, 1.0, 1.008_82];
const BRADFORD: Matrix3x3 = [
[0.895_1, 0.266_4, -0.161_4],
[-0.750_2, 1.713_5, 0.036_7],
[0.038_9, -0.068_5, 1.029_6],
];
const BRADFORD_INV: Matrix3x3 = [
[0.986_993, -0.147_054, 0.159_963],
[0.432_305, 0.518_360, 0.049_291],
[-0.008_529, 0.040_043, 0.968_487],
];
#[inline]
#[must_use]
fn mat_vec(m: &Matrix3x3, v: &[f64; 3]) -> [f64; 3] {
[
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],
]
}
#[must_use]
fn mat3x3_mul(a: &Matrix3x3, b: &Matrix3x3) -> Matrix3x3 {
let mut out = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
out[i][j] += a[i][k] * b[k][j];
}
}
}
out
}
#[must_use]
fn mat3x3_inv(m: &Matrix3x3) -> Option<Matrix3x3> {
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-15 {
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,
],
])
}
#[must_use]
pub fn bradford_cat(src_white: &[f64; 3], dst_white: &[f64; 3]) -> Matrix3x3 {
let src_lms = mat_vec(&BRADFORD, src_white);
let dst_lms = mat_vec(&BRADFORD, dst_white);
if src_lms[0].abs() <= f64::EPSILON
|| src_lms[1].abs() <= f64::EPSILON
|| src_lms[2].abs() <= f64::EPSILON
{
return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
}
let scale: Matrix3x3 = [
[dst_lms[0] / src_lms[0], 0.0, 0.0],
[0.0, dst_lms[1] / src_lms[1], 0.0],
[0.0, 0.0, dst_lms[2] / src_lms[2]],
];
let tmp = mat3x3_mul(&scale, &BRADFORD);
mat3x3_mul(&BRADFORD_INV, &tmp)
}
#[derive(Debug, Clone)]
pub struct AcesIdtConfig {
pub camera_make: String,
pub camera_model: String,
pub color_matrix: Matrix3x3,
pub camera_white: [f64; 3],
}
impl AcesIdtConfig {
#[must_use]
pub fn new_d65(
camera_make: impl Into<String>,
camera_model: impl Into<String>,
color_matrix: Matrix3x3,
) -> Self {
Self {
camera_make: camera_make.into(),
camera_model: camera_model.into(),
color_matrix,
camera_white: [0.950_47, 1.0, 1.088_83], }
}
#[must_use]
pub fn new(
camera_make: impl Into<String>,
camera_model: impl Into<String>,
color_matrix: Matrix3x3,
camera_white: [f64; 3],
) -> Self {
Self {
camera_make: camera_make.into(),
camera_model: camera_model.into(),
color_matrix,
camera_white,
}
}
}
#[derive(Debug, Clone)]
pub struct AcesIdt {
pub name: String,
pub combined_matrix: Matrix3x3,
pub cat_matrix: Matrix3x3,
}
impl AcesIdt {
#[must_use]
pub fn apply(&self, rgb: &[f32; 3]) -> [f32; 3] {
let r = f64::from(rgb[0]);
let g = f64::from(rgb[1]);
let b = f64::from(rgb[2]);
let m = &self.combined_matrix;
let out_r = m[0][0] * r + m[0][1] * g + m[0][2] * b;
let out_g = m[1][0] * r + m[1][1] * g + m[1][2] * b;
let out_b = m[2][0] * r + m[2][1] * g + m[2][2] * b;
[out_r as f32, out_g as f32, out_b as f32]
}
#[must_use]
pub fn apply_f64(&self, rgb: &[f64; 3]) -> [f64; 3] {
mat_vec(&self.combined_matrix, rgb)
}
pub fn apply_batch(&self, pixels: &mut [[f32; 3]]) {
for px in pixels.iter_mut() {
*px = self.apply(px);
}
}
}
pub struct AcesIdtGenerator;
impl AcesIdtGenerator {
#[must_use]
pub fn generate(config: &AcesIdtConfig) -> AcesIdt {
let cat = bradford_cat(&config.camera_white, &ACES_D60_WHITE);
let xyz_adapted = mat3x3_mul(&cat, &config.color_matrix);
let combined = mat3x3_mul(&XYZ_TO_AP1, &xyz_adapted);
AcesIdt {
name: format!("{} {}", config.camera_make, config.camera_model),
combined_matrix: combined,
cat_matrix: cat,
}
}
#[must_use]
pub fn generate_with_diagnostics(config: &AcesIdtConfig) -> (AcesIdt, Matrix3x3) {
let cat = bradford_cat(&config.camera_white, &ACES_D60_WHITE);
let xyz_d60 = mat3x3_mul(&cat, &config.color_matrix);
let combined = mat3x3_mul(&XYZ_TO_AP1, &xyz_d60);
let idt = AcesIdt {
name: format!("{} {}", config.camera_make, config.camera_model),
combined_matrix: combined,
cat_matrix: cat,
};
(idt, xyz_d60)
}
#[must_use]
pub fn verify_white_point(idt: &AcesIdt, config: &AcesIdtConfig) -> f64 {
let cam_rgb_white = match mat3x3_inv(&config.color_matrix) {
Some(inv) => mat_vec(&inv, &config.camera_white),
None => return f64::INFINITY,
};
let aces_white = idt.apply_f64(&cam_rgb_white);
let expected = mat_vec(&XYZ_TO_AP1, &ACES_D60_WHITE);
(0..3)
.map(|i| (aces_white[i] - expected[i]).abs())
.fold(0.0_f64, f64::max)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity_matrix() -> Matrix3x3 {
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
}
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_generate_identity_camera() {
let config = AcesIdtConfig::new_d65("Test", "Camera", identity_matrix());
let idt = AcesIdtGenerator::generate(&config);
assert_eq!(idt.name, "Test Camera");
let sum: f64 = idt.combined_matrix.iter().flat_map(|r| r.iter()).sum();
assert!(sum.abs() > 0.1, "combined_matrix should not be trivial");
}
#[test]
fn test_aces_d60_white_luminance() {
assert!(approx(ACES_D60_WHITE[1], 1.0, 1e-6));
}
#[test]
fn test_apply_neutral_grey_positive() {
let config = AcesIdtConfig::new_d65("Test", "Camera", identity_matrix());
let idt = AcesIdtGenerator::generate(&config);
let out = idt.apply(&[0.18, 0.18, 0.18]);
assert!(
out[1] > 0.0,
"Green channel (luminance proxy) must be positive: {out:?}"
);
}
#[test]
fn test_apply_black_maps_to_zero() {
let config = AcesIdtConfig::new_d65("Test", "Camera", identity_matrix());
let idt = AcesIdtGenerator::generate(&config);
let out = idt.apply(&[0.0, 0.0, 0.0]);
assert!(
out[0].abs() < 1e-7 && out[1].abs() < 1e-7 && out[2].abs() < 1e-7,
"Black should map to near-zero: {out:?}"
);
}
#[test]
fn test_apply_f64_consistent_with_apply_f32() {
let config = AcesIdtConfig::new_d65("Canon", "EOS R5", identity_matrix());
let idt = AcesIdtGenerator::generate(&config);
let rgb = [0.5_f32, 0.4, 0.6];
let out_f32 = idt.apply(&rgb);
let out_f64 = idt.apply_f64(&[0.5, 0.4, 0.6]);
for ch in 0..3 {
assert!(
(f64::from(out_f32[ch]) - out_f64[ch]).abs() < 1e-5,
"ch={ch} mismatch: f32={}, f64={}",
out_f32[ch],
out_f64[ch]
);
}
}
#[test]
fn test_apply_batch_consistent() {
let config = AcesIdtConfig::new_d65("Sony", "α7 IV", identity_matrix());
let idt = AcesIdtGenerator::generate(&config);
let pixel = [0.3_f32, 0.5, 0.2];
let expected = idt.apply(&pixel);
let mut batch = vec![pixel; 4];
idt.apply_batch(&mut batch);
for out in &batch {
for ch in 0..3 {
assert!(
(out[ch] - expected[ch]).abs() < 1e-6,
"Batch mismatch at ch={ch}"
);
}
}
}
#[test]
fn test_bradford_cat_identity_same_white() {
let d65 = [0.950_47_f64, 1.0, 1.088_83];
let cat = bradford_cat(&d65, &d65);
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(cat[i][j] - expected).abs() < 1e-6,
"cat[{i}][{j}]={} expected {expected}",
cat[i][j]
);
}
}
}
#[test]
fn test_xyz_ap1_round_trip() {
let prod = mat3x3_mul(&XYZ_TO_AP1, &AP1_TO_XYZ);
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(prod[i][j] - expected).abs() < 1e-6,
"XYZ_TO_AP1*AP1_TO_XYZ[{i}][{j}]={} expected {expected}",
prod[i][j]
);
}
}
}
#[test]
fn test_generate_with_diagnostics_returns_xyz_matrix() {
let config = AcesIdtConfig::new_d65("Nikon", "Z9", identity_matrix());
let (idt, xyz_mat) = AcesIdtGenerator::generate_with_diagnostics(&config);
let recon = mat3x3_mul(&XYZ_TO_AP1, &xyz_mat);
for i in 0..3 {
for j in 0..3 {
assert!(
(recon[i][j] - idt.combined_matrix[i][j]).abs() < 1e-10,
"Reconstruction mismatch [{i}][{j}]"
);
}
}
}
#[test]
fn test_custom_white_point_d50() {
let d50 = [0.964_22_f64, 1.0, 0.825_21];
let config = AcesIdtConfig::new("Arri", "Alexa Mini LF", identity_matrix(), d50);
let idt = AcesIdtGenerator::generate(&config);
let is_identity = (0..3).all(|i| {
(0..3).all(|j| {
let expected = if i == j { 1.0 } else { 0.0 };
(idt.cat_matrix[i][j] - expected).abs() < 1e-4
})
});
assert!(!is_identity, "D50 → D60 CAT should not be identity");
}
}