use core::f32::consts::PI;
use libm::{atan2f, cosf, expf, sinf, sqrtf};
use super::{LABS_A, LABS_B, LABS_C, LABS_L};
const RAD_TO_DEG: f32 = 180.0 / PI;
const DEG_TO_RAD: f32 = PI / 180.0;
const TWENTY_FIVE_POW_7: f32 = 25.0_f32 * 25.0 * 25.0 * 25.0 * 25.0 * 25.0 * 25.0;
#[inline]
fn pow7(x: f32) -> f32 {
let x2 = x * x;
let x4 = x2 * x2;
x4 * x2 * x
}
#[allow(dead_code)]
#[inline]
pub fn delta_e_2000_sq(lab1: [f32; 3], lab2: [f32; 3]) -> f32 {
let c1 = sqrtf(lab1[1] * lab1[1] + lab1[2] * lab1[2]);
let c2 = sqrtf(lab2[1] * lab2[1] + lab2[2] * lab2[2]);
delta_e_2000_sq_with_chromas(lab1, c1, lab2, c2)
}
#[inline(always)]
fn delta_e_2000_sq_with_chromas(lab1: [f32; 3], c1: f32, lab2: [f32; 3], c2: f32) -> f32 {
let [l1, a1, b1] = lab1;
let [l2, a2, b2] = lab2;
let cbar = 0.5 * (c1 + c2);
let cbar7 = pow7(cbar);
let g = 0.5 * (1.0 - sqrtf(cbar7 / (cbar7 + TWENTY_FIVE_POW_7)));
let one_plus_g = 1.0 + g;
let a1p = a1 * one_plus_g;
let a2p = a2 * one_plus_g;
let c1p = sqrtf(a1p * a1p + b1 * b1);
let c2p = sqrtf(a2p * a2p + b2 * b2);
let h1p = hue_atan2_deg(b1, a1p);
let h2p = hue_atan2_deg(b2, a2p);
let dlp = l2 - l1;
let dcp = c2p - c1p;
let dhp = if c1p * c2p == 0.0 {
0.0
} else {
let diff = h2p - h1p;
if diff > 180.0 {
diff - 360.0
} else if diff < -180.0 {
diff + 360.0
} else {
diff
}
};
let dh_cap = 2.0 * sqrtf(c1p * c2p) * sinf(0.5 * dhp * DEG_TO_RAD);
let lp_bar = 0.5 * (l1 + l2);
let cp_bar = 0.5 * (c1p + c2p);
let hp_bar = if c1p * c2p == 0.0 {
h1p + h2p
} else {
let abs_diff = (h1p - h2p).abs();
let sum = h1p + h2p;
if abs_diff <= 180.0 {
0.5 * sum
} else if sum < 360.0 {
0.5 * (sum + 360.0)
} else {
0.5 * (sum - 360.0)
}
};
let hp_rad = hp_bar * DEG_TO_RAD;
let t = 1.0 - 0.17 * cosf(hp_rad - 30.0 * DEG_TO_RAD)
+ 0.24 * cosf(2.0 * hp_rad)
+ 0.32 * cosf(3.0 * hp_rad + 6.0 * DEG_TO_RAD)
- 0.20 * cosf(4.0 * hp_rad - 63.0 * DEG_TO_RAD);
let d_theta_arg = (hp_bar - 275.0) / 25.0;
let d_theta = 30.0 * expf(-(d_theta_arg * d_theta_arg));
let cp_bar7 = pow7(cp_bar);
let rc = 2.0 * sqrtf(cp_bar7 / (cp_bar7 + TWENTY_FIVE_POW_7));
let lp_minus_50 = lp_bar - 50.0;
let sl = 1.0 + 0.015 * lp_minus_50 * lp_minus_50 / sqrtf(20.0 + lp_minus_50 * lp_minus_50);
let sc = 1.0 + 0.045 * cp_bar;
let sh = 1.0 + 0.015 * cp_bar * t;
let rt = -sinf(2.0 * d_theta * DEG_TO_RAD) * rc;
let dl_term = dlp / sl;
let dc_term = dcp / sc;
let dh_term = dh_cap / sh;
dl_term * dl_term + dc_term * dc_term + dh_term * dh_term + rt * dc_term * dh_term
}
#[inline]
fn hue_atan2_deg(y: f32, x: f32) -> f32 {
if y == 0.0 && x == 0.0 {
return 0.0;
}
let h = atan2f(y, x) * RAD_TO_DEG;
if h < 0.0 { h + 360.0 } else { h }
}
pub fn nearest_idx(query: [f32; 3]) -> usize {
let n = LABS_L.len();
let q_c = sqrtf(query[1] * query[1] + query[2] * query[2]);
let mut best_idx = 0usize;
let mut best_d2 = f32::INFINITY;
for i in 0..n {
let entry_lab = [LABS_L[i], LABS_A[i], LABS_B[i]];
let entry_c = LABS_C[i];
let d2 = delta_e_2000_sq_with_chromas(entry_lab, entry_c, query, q_c);
if d2 < best_d2 {
best_d2 = d2;
best_idx = i;
}
}
best_idx
}
pub const PREFILTER_K: usize = 96;
pub fn nearest_idx_prefiltered(query: [f32; 3]) -> usize {
let mut top: [(f32, usize); PREFILTER_K] = [(f32::INFINITY, 0); PREFILTER_K];
let n = LABS_L.len();
let [ql, qa, qb] = query;
for i in 0..n {
let dl = ql - LABS_L[i];
let da = qa - LABS_A[i];
let db = qb - LABS_B[i];
let d2 = (dl * dl + da * da) + db * db;
let worst = top[PREFILTER_K - 1].0;
if d2 >= worst {
continue;
}
let mut j = PREFILTER_K - 1;
while j > 0 && top[j - 1].0 > d2 {
top[j] = top[j - 1];
j -= 1;
}
top[j] = (d2, i);
}
let q_c = sqrtf(query[1] * query[1] + query[2] * query[2]);
let first_idx = top[0].1;
let first_lab = [LABS_L[first_idx], LABS_A[first_idx], LABS_B[first_idx]];
let mut best_idx = first_idx;
let mut best_d2 = delta_e_2000_sq_with_chromas(first_lab, LABS_C[first_idx], query, q_c);
for &(_, idx) in &top[1..] {
let entry_lab = [LABS_L[idx], LABS_A[idx], LABS_B[idx]];
let d2 = delta_e_2000_sq_with_chromas(entry_lab, LABS_C[idx], query, q_c);
if d2 < best_d2 {
best_d2 = d2;
best_idx = idx;
}
}
best_idx
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_de2000(lab1: [f32; 3], lab2: [f32; 3], expected: f32, tol: f32, label: &str) {
let d2 = delta_e_2000_sq(lab1, lab2);
let d = sqrtf(d2.max(0.0));
assert!(
(d - expected).abs() < tol,
"{label}: expected ΔE00={expected:.4}, got {d:.4} \
(sq={d2:.6}); lab1={lab1:?} lab2={lab2:?}"
);
}
#[test]
fn sharma_table_1_row_1() {
assert_de2000(
[50.0, 2.6772, -79.7751],
[50.0, 0.0, -82.7485],
2.0425,
1e-2,
"row 1",
);
}
#[test]
fn sharma_table_1_row_2() {
assert_de2000(
[50.0, 3.1571, -77.2803],
[50.0, 0.0, -82.7485],
2.8615,
1e-2,
"row 2",
);
}
#[test]
fn sharma_table_1_row_14() {
assert_de2000(
[50.0, 2.5, 0.0],
[73.0, 25.0, -18.0],
27.1492,
1e-2,
"row 14",
);
}
#[test]
fn self_distance_is_zero() {
for lab in [[50.0, 0.0, 0.0], [80.0, 20.0, -30.0], [0.0, 0.0, 0.0]] {
let d2 = delta_e_2000_sq(lab, lab);
assert!(d2.abs() < 1e-5, "self-distance {d2} for {lab:?}");
}
}
#[test]
fn black_vs_black_handled() {
let d2 = delta_e_2000_sq([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
assert_eq!(d2, 0.0);
}
#[test]
fn symmetric_in_arguments() {
let pairs = [
([50.0, 2.6772, -79.7751], [50.0, 0.0, -82.7485]),
([60.0, 30.0, 40.0], [40.0, -10.0, 5.0]),
([0.0, 0.0, 0.0], [100.0, 0.0, 0.0]),
];
for (a, b) in pairs {
let d_ab = delta_e_2000_sq(a, b);
let d_ba = delta_e_2000_sq(b, a);
assert!(
(d_ab - d_ba).abs() < 1e-4,
"asymmetric: ΔE²(A→B)={d_ab}, ΔE²(B→A)={d_ba}; A={a:?} B={b:?}"
);
}
}
#[test]
#[cfg(feature = "std")]
#[cfg_attr(
miri,
ignore = "17³ grid × CIEDE2000 transcendentals too slow under miri"
)]
fn prefilter_matches_full_scan_across_grid() {
let mut mismatches: Vec<([u8; 3], usize, usize)> = Vec::new();
for r in (0..256u32).step_by(16) {
for g in (0..256u32).step_by(16) {
for b in (0..256u32).step_by(16) {
let rgb = [r as u8, g as u8, b as u8];
let q = crate::rgb_to_lab(rgb);
let exact = nearest_idx(q);
let prefiltered = nearest_idx_prefiltered(q);
if exact != prefiltered {
mismatches.push((rgb, exact, prefiltered));
}
}
}
}
assert!(
mismatches.is_empty(),
"{} prefilter divergences from full-scan CIEDE2000 across the \
17³ RGB grid (K = {}); first few: {:?}",
mismatches.len(),
PREFILTER_K,
&mismatches[..mismatches.len().min(5)]
);
}
#[test]
#[cfg_attr(
miri,
ignore = "125-query grid × 949-entry CIEDE2000 transcendentals too slow under miri"
)]
fn lookup_returns_valid_index_across_grid() {
for r in (0..=255u32).step_by(64) {
for g in (0..=255u32).step_by(64) {
for b in (0..=255u32).step_by(64) {
let q = crate::rgb_to_lab([r as u8, g as u8, b as u8]);
let idx = nearest_idx(q);
assert!(idx < LABS_L.len());
let entry = [LABS_L[idx], LABS_A[idx], LABS_B[idx]];
let d2 = delta_e_2000_sq(q, entry);
assert!(
d2.is_finite() && d2 >= 0.0,
"non-finite/negative ΔE00² at rgb=[{r},{g},{b}]: d2={d2}"
);
}
}
}
}
}