use crate::error::{CalibrationError, CalibrationResult};
use crate::{Illuminant, Matrix3x3, Rgb, Xyz};
use std::collections::HashMap;
use std::sync::Mutex;
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[allow(unsafe_code)]
unsafe fn mat3x3_mul_vec3_avx2(mat: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
use std::arch::x86_64::*;
let _vv0 = _mm256_set1_pd(v[0]);
let _vv1 = _mm256_set1_pd(v[1]);
let _vv2 = _mm256_set1_pd(v[2]);
let mut out = [0.0_f64; 3];
for (i, row) in mat.iter().enumerate() {
let row_reg = _mm256_set_pd(0.0, row[2], row[1], row[0]);
let v_vec = _mm256_set_pd(0.0, v[2], v[1], v[0]);
let products = _mm256_mul_pd(row_reg, v_vec);
let h1 = _mm256_hadd_pd(products, products);
let lo = _mm256_castpd256_pd128(h1); let hi = _mm256_extractf128_pd(h1, 1); let sum128 = _mm_add_pd(lo, hi);
out[i] = _mm_cvtsd_f64(sum128);
}
out
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
#[allow(unsafe_code)]
unsafe fn mat3x3_mul_vec3_neon(mat: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
use std::arch::aarch64::*;
let vv0 = vdupq_n_f64(v[0]);
let vv1 = vdupq_n_f64(v[1]);
let vv2 = vdupq_n_f64(v[2]);
let mut out = [0.0_f64; 3];
for (i, row) in mat.iter().enumerate() {
let r01 = vld1q_f64(row.as_ptr());
let r22 = vdupq_n_f64(row[2]);
let acc = vmulq_f64(r01, vv0); let p0 = vmulq_f64(r01, vv1); let _ = (acc, p0);
let v01 = vcombine_f64(vdup_n_f64(v[0]), vdup_n_f64(v[1]));
let products01 = vmulq_f64(r01, v01);
let p2 = vmulq_f64(r22, vv2);
let half = vaddq_f64(products01, p2); let sum2 = vaddvq_f64(products01); let _ = half; out[i] = sum2 + row[2] * v[2]; }
out
}
#[inline(always)]
fn mat3x3_mul_vec3_scalar(mat: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
[
mat[0][0] * v[0] + mat[0][1] * v[1] + mat[0][2] * v[2],
mat[1][0] * v[0] + mat[1][1] * v[1] + mat[1][2] * v[2],
mat[2][0] * v[0] + mat[2][1] * v[1] + mat[2][2] * v[2],
]
}
pub fn mat3x3_mul_vec3_simd(mat: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") {
#[allow(unsafe_code)]
return unsafe { mat3x3_mul_vec3_avx2(mat, v) };
}
}
#[cfg(target_arch = "aarch64")]
{
if std::arch::is_aarch64_feature_detected!("neon") {
#[allow(unsafe_code)]
return unsafe { mat3x3_mul_vec3_neon(mat, v) };
}
}
mat3x3_mul_vec3_scalar(mat, v)
}
type CacheKey = (Illuminant, Illuminant, ChromaticAdaptationMethod);
type CacheMap = Mutex<HashMap<CacheKey, Matrix3x3>>;
static ADAPT_MATRIX_CACHE: std::sync::OnceLock<CacheMap> = std::sync::OnceLock::new();
fn cache() -> &'static CacheMap {
ADAPT_MATRIX_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ChromaticAdaptationMethod {
Bradford,
VonKries,
Cat02,
XyzScaling,
}
pub struct ChromaticAdaptation {
method: ChromaticAdaptationMethod,
source_illuminant: Illuminant,
target_illuminant: Illuminant,
transform_matrix: Matrix3x3,
}
impl ChromaticAdaptation {
pub fn new(
method: ChromaticAdaptationMethod,
source_illuminant: Illuminant,
target_illuminant: Illuminant,
) -> CalibrationResult<Self> {
let transform_matrix =
Self::cached_transform_matrix(method, source_illuminant, target_illuminant)?;
Ok(Self {
method,
source_illuminant,
target_illuminant,
transform_matrix,
})
}
fn cached_transform_matrix(
method: ChromaticAdaptationMethod,
source: Illuminant,
target: Illuminant,
) -> CalibrationResult<Matrix3x3> {
let key: CacheKey = (source, target, method);
{
let guard = cache().lock().map_err(|_| {
CalibrationError::ChromaticAdaptationFailed("cache lock poisoned".to_string())
})?;
if let Some(&mat) = guard.get(&key) {
return Ok(mat);
}
}
let mat = Self::compute_transform_matrix(method, source, target)?;
{
let mut guard = cache().lock().map_err(|_| {
CalibrationError::ChromaticAdaptationFailed("cache lock poisoned".to_string())
})?;
guard.entry(key).or_insert(mat);
}
Ok(mat)
}
fn compute_transform_matrix(
method: ChromaticAdaptationMethod,
source: Illuminant,
target: Illuminant,
) -> CalibrationResult<Matrix3x3> {
let source_xyz = source.xyz();
let target_xyz = target.xyz();
match method {
ChromaticAdaptationMethod::Bradford => {
Self::bradford_transform(&source_xyz, &target_xyz)
}
ChromaticAdaptationMethod::VonKries => {
Self::von_kries_transform(&source_xyz, &target_xyz)
}
ChromaticAdaptationMethod::Cat02 => Self::cat02_transform(&source_xyz, &target_xyz),
ChromaticAdaptationMethod::XyzScaling => {
Self::xyz_scaling_transform(&source_xyz, &target_xyz)
}
}
}
fn bradford_transform(source_xyz: &Xyz, target_xyz: &Xyz) -> CalibrationResult<Matrix3x3> {
let bradford = [
[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],
];
let bradford_inv = [
[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],
];
let source_lms = Self::apply_matrix(&bradford, source_xyz);
let target_lms = Self::apply_matrix(&bradford, target_xyz);
if source_lms[0] < 1e-10 || source_lms[1] < 1e-10 || source_lms[2] < 1e-10 {
return Err(CalibrationError::ChromaticAdaptationFailed(
"Invalid source illuminant".to_string(),
));
}
let scale = [
[target_lms[0] / source_lms[0], 0.0, 0.0],
[0.0, target_lms[1] / source_lms[1], 0.0],
[0.0, 0.0, target_lms[2] / source_lms[2]],
];
let temp = Self::multiply_matrices(&scale, &bradford);
Ok(Self::multiply_matrices(&bradford_inv, &temp))
}
fn von_kries_transform(source_xyz: &Xyz, target_xyz: &Xyz) -> CalibrationResult<Matrix3x3> {
let von_kries = [
[0.400_24, 0.707_6, -0.080_8],
[-0.226_3, 1.165_3, 0.045_7],
[0.0, 0.0, 0.918_2],
];
let von_kries_inv = [
[1.859_936, -1.129_382, 0.219_897],
[0.361_191, 0.638_812, -0.000_006],
[0.0, 0.0, 1.089_064],
];
let source_lms = Self::apply_matrix(&von_kries, source_xyz);
let target_lms = Self::apply_matrix(&von_kries, target_xyz);
if source_lms[0] < 1e-10 || source_lms[1] < 1e-10 || source_lms[2] < 1e-10 {
return Err(CalibrationError::ChromaticAdaptationFailed(
"Invalid source illuminant".to_string(),
));
}
let scale = [
[target_lms[0] / source_lms[0], 0.0, 0.0],
[0.0, target_lms[1] / source_lms[1], 0.0],
[0.0, 0.0, target_lms[2] / source_lms[2]],
];
let temp = Self::multiply_matrices(&scale, &von_kries);
Ok(Self::multiply_matrices(&von_kries_inv, &temp))
}
fn cat02_transform(source_xyz: &Xyz, target_xyz: &Xyz) -> CalibrationResult<Matrix3x3> {
let cat02 = [
[0.732_8, 0.429_6, -0.162_4],
[-0.703_6, 1.697_5, 0.006_1],
[0.003_0, 0.013_6, 0.983_4],
];
let cat02_inv = [
[1.096_124, -0.278_869, 0.182_745],
[0.454_369, 0.473_533, 0.072_098],
[-0.009_628, -0.005_698, 1.015_326],
];
let source_rgb = Self::apply_matrix(&cat02, source_xyz);
let target_rgb = Self::apply_matrix(&cat02, target_xyz);
if source_rgb[0] < 1e-10 || source_rgb[1] < 1e-10 || source_rgb[2] < 1e-10 {
return Err(CalibrationError::ChromaticAdaptationFailed(
"Invalid source illuminant".to_string(),
));
}
let scale = [
[target_rgb[0] / source_rgb[0], 0.0, 0.0],
[0.0, target_rgb[1] / source_rgb[1], 0.0],
[0.0, 0.0, target_rgb[2] / source_rgb[2]],
];
let temp = Self::multiply_matrices(&scale, &cat02);
Ok(Self::multiply_matrices(&cat02_inv, &temp))
}
fn xyz_scaling_transform(source_xyz: &Xyz, target_xyz: &Xyz) -> CalibrationResult<Matrix3x3> {
if source_xyz[0] < 1e-10 || source_xyz[1] < 1e-10 || source_xyz[2] < 1e-10 {
return Err(CalibrationError::ChromaticAdaptationFailed(
"Invalid source illuminant".to_string(),
));
}
Ok([
[target_xyz[0] / source_xyz[0], 0.0, 0.0],
[0.0, target_xyz[1] / source_xyz[1], 0.0],
[0.0, 0.0, target_xyz[2] / source_xyz[2]],
])
}
fn apply_matrix(matrix: &Matrix3x3, color: &[f64; 3]) -> [f64; 3] {
mat3x3_mul_vec3_simd(matrix, color)
}
fn multiply_matrices(a: &Matrix3x3, b: &Matrix3x3) -> Matrix3x3 {
let mut result = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
result[i][j] += a[i][k] * b[k][j];
}
}
}
result
}
#[must_use]
pub fn adapt_xyz(&self, xyz: &Xyz) -> Xyz {
Self::apply_matrix(&self.transform_matrix, xyz)
}
#[must_use]
pub fn adapt_rgb(&self, rgb: &Rgb) -> Rgb {
self.adapt_xyz(rgb)
}
#[must_use]
pub fn source_illuminant(&self) -> Illuminant {
self.source_illuminant
}
#[must_use]
pub fn target_illuminant(&self) -> Illuminant {
self.target_illuminant
}
#[must_use]
pub fn method(&self) -> ChromaticAdaptationMethod {
self.method
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chromatic_adaptation_new() {
let result = ChromaticAdaptation::new(
ChromaticAdaptationMethod::Bradford,
Illuminant::D50,
Illuminant::D65,
);
assert!(result.is_ok());
let ca = result.expect("expected successful result");
assert_eq!(ca.source_illuminant(), Illuminant::D50);
assert_eq!(ca.target_illuminant(), Illuminant::D65);
assert_eq!(ca.method(), ChromaticAdaptationMethod::Bradford);
}
#[test]
fn test_bradford_same_illuminant() {
let result = ChromaticAdaptation::new(
ChromaticAdaptationMethod::Bradford,
Illuminant::D65,
Illuminant::D65,
);
assert!(result.is_ok());
let ca = result.expect("expected successful result");
let xyz = [0.5, 0.5, 0.5];
let adapted = ca.adapt_xyz(&xyz);
assert!((adapted[0] - xyz[0]).abs() < 0.01);
assert!((adapted[1] - xyz[1]).abs() < 0.01);
assert!((adapted[2] - xyz[2]).abs() < 0.01);
}
#[test]
fn test_von_kries_adaptation() {
let result = ChromaticAdaptation::new(
ChromaticAdaptationMethod::VonKries,
Illuminant::D50,
Illuminant::D65,
);
assert!(result.is_ok());
}
#[test]
fn test_cat02_adaptation() {
let result = ChromaticAdaptation::new(
ChromaticAdaptationMethod::Cat02,
Illuminant::A,
Illuminant::D65,
);
assert!(result.is_ok());
}
#[test]
fn test_xyz_scaling() {
let result = ChromaticAdaptation::new(
ChromaticAdaptationMethod::XyzScaling,
Illuminant::D50,
Illuminant::D65,
);
assert!(result.is_ok());
}
#[test]
fn test_adapt_xyz() {
let ca = ChromaticAdaptation::new(
ChromaticAdaptationMethod::Bradford,
Illuminant::D50,
Illuminant::D65,
)
.expect("unexpected None/Err");
let xyz = [0.5, 0.5, 0.5];
let adapted = ca.adapt_xyz(&xyz);
assert!(adapted[0] > 0.0 && adapted[0] <= 1.0);
assert!(adapted[1] > 0.0 && adapted[1] <= 1.0);
assert!(adapted[2] > 0.0 && adapted[2] <= 1.0);
}
#[test]
fn test_multiply_matrices() {
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let scale = [[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]];
let result = ChromaticAdaptation::multiply_matrices(&identity, &scale);
assert!((result[0][0] - 2.0).abs() < 1e-10);
assert!((result[1][1] - 2.0).abs() < 1e-10);
assert!((result[2][2] - 2.0).abs() < 1e-10);
}
#[test]
fn test_mat3x3_simd_matches_scalar() {
let mat: 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],
];
let v = [0.964_22, 1.0, 0.825_21];
let scalar = mat3x3_mul_vec3_scalar(&mat, &v);
let simd = mat3x3_mul_vec3_simd(&mat, &v);
for i in 0..3 {
assert!(
(simd[i] - scalar[i]).abs() < 1e-10,
"element {i}: SIMD={} scalar={}",
simd[i],
scalar[i]
);
}
}
#[test]
fn test_mat3x3_simd_identity() {
let identity: Matrix3x3 = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let v = [0.3, 0.6, 0.9];
let result = mat3x3_mul_vec3_simd(&identity, &v);
for i in 0..3 {
assert!(
(result[i] - v[i]).abs() < 1e-15,
"element {i}: expected {}, got {}",
v[i],
result[i]
);
}
}
#[test]
fn test_mat3x3_simd_zero_matrix() {
let zero: Matrix3x3 = [[0.0; 3]; 3];
let v = [1.0, 2.0, 3.0];
let result = mat3x3_mul_vec3_simd(&zero, &v);
for i in 0..3 {
assert!(result[i].abs() < 1e-15, "element {i} should be 0.0");
}
}
#[test]
fn test_bradford_uses_simd_apply_matrix() {
let ca = ChromaticAdaptation::new(
ChromaticAdaptationMethod::Bradford,
Illuminant::D65,
Illuminant::D65,
)
.expect("Bradford D65→D65 must succeed");
let xyz = [0.4505, 0.3290, 0.0736];
let adapted = ca.adapt_xyz(&xyz);
for i in 0..3 {
assert!(
(adapted[i] - xyz[i]).abs() < 1e-4,
"Bradford D65→D65: element {i} expected ~{}, got {}",
xyz[i],
adapted[i]
);
}
}
}