1use super::spectral_kernel::{
22 spd_to_xyz, Spd, Xyz, CIE_1931_CMF_X, CIE_1931_CMF_Y, CIE_1931_CMF_Z, SPD_SAMPLES,
23};
24
25const Y_NORM: f32 = {
28 let mut sum = 0.0f32;
29 let mut i = 0;
30 while i < CIE_1931_CMF_Y.len() {
31 sum += CIE_1931_CMF_Y[i];
32 i += 1;
33 }
34 1.0 / sum
35};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MetamerError {
44 ZeroTarget,
46 BufferTooSmall { needed: usize, have: usize },
48}
49
50impl core::fmt::Display for MetamerError {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 match self {
53 Self::ZeroTarget => write!(f, "metamer: target XYZ is zero"),
54 Self::BufferTooSmall { needed, have } => {
55 write!(f, "metamer: buffer too small, need {needed}, have {have}")
56 }
57 }
58 }
59}
60
61impl std::error::Error for MetamerError {}
62
63pub fn metamer_kernel_basis(out_basis: &mut [f32]) -> Result<usize, MetamerError> {
75 let n = SPD_SAMPLES;
76 let ker_dim = n - 3;
77 if out_basis.len() < ker_dim * n {
78 return Err(MetamerError::BufferTooSmall {
79 needed: ker_dim * n,
80 have: out_basis.len(),
81 });
82 }
83
84 let cmf_f64: [[f64; SPD_SAMPLES]; 3] = [
86 CIE_1931_CMF_X.map(|v| v as f64),
87 CIE_1931_CMF_Y.map(|v| v as f64),
88 CIE_1931_CMF_Z.map(|v| v as f64),
89 ];
90
91 let mut ppt = [[0.0f64; 3]; 3];
93 for i in 0..3 {
94 for j in 0..3 {
95 ppt[i][j] = cmf_f64[i]
96 .iter()
97 .zip(cmf_f64[j].iter())
98 .map(|(a, b)| a * b)
99 .sum();
100 }
101 }
102
103 let det = ppt[0][0] * (ppt[1][1] * ppt[2][2] - ppt[1][2] * ppt[2][1])
105 - ppt[0][1] * (ppt[1][0] * ppt[2][2] - ppt[1][2] * ppt[2][0])
106 + ppt[0][2] * (ppt[1][0] * ppt[2][1] - ppt[1][1] * ppt[2][0]);
107 let inv_det = 1.0 / det;
108 let inv = [
109 [
110 (ppt[1][1] * ppt[2][2] - ppt[1][2] * ppt[2][1]) * inv_det,
111 (ppt[0][2] * ppt[2][1] - ppt[0][1] * ppt[2][2]) * inv_det,
112 (ppt[0][1] * ppt[1][2] - ppt[0][2] * ppt[1][1]) * inv_det,
113 ],
114 [
115 (ppt[1][2] * ppt[2][0] - ppt[1][0] * ppt[2][2]) * inv_det,
116 (ppt[0][0] * ppt[2][2] - ppt[0][2] * ppt[2][0]) * inv_det,
117 (ppt[0][2] * ppt[1][0] - ppt[0][0] * ppt[1][2]) * inv_det,
118 ],
119 [
120 (ppt[1][0] * ppt[2][1] - ppt[1][1] * ppt[2][0]) * inv_det,
121 (ppt[0][1] * ppt[2][0] - ppt[0][0] * ppt[2][1]) * inv_det,
122 (ppt[0][0] * ppt[1][1] - ppt[0][1] * ppt[1][0]) * inv_det,
123 ],
124 ];
125
126 let mut basis_idx = 0usize;
132 for i in 3..n {
133 let pe = [cmf_f64[0][i], cmf_f64[1][i], cmf_f64[2][i]];
135
136 let mut inv_pe = [0.0f64; 3];
138 for j in 0..3 {
139 inv_pe[j] = inv[j][0] * pe[0] + inv[j][1] * pe[1] + inv[j][2] * pe[2];
140 }
141
142 let mut proj = [0.0f64; SPD_SAMPLES];
144 for k in 0..n {
145 for j in 0..3 {
146 proj[k] += cmf_f64[j][k] * inv_pe[j];
147 }
148 }
149
150 let mut v = [0.0f64; SPD_SAMPLES];
152 v[i] = 1.0;
153 for k in 0..n {
154 v[k] -= proj[k];
155 }
156
157 let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
159 if norm > 1e-10 {
160 for j in 0..n {
161 v[j] /= norm;
162 }
163 for j in 0..n {
164 out_basis[basis_idx * n + j] = v[j] as f32;
165 }
166 basis_idx += 1;
167 }
168
169 if basis_idx >= ker_dim {
170 break;
171 }
172 }
173
174 Ok(basis_idx)
175}
176
177pub fn min_norm_spd_for_xyz(target: &Xyz) -> Spd {
188 let cmf_x: [f32; SPD_SAMPLES] = CIE_1931_CMF_X.map(|v| v * Y_NORM);
191 let cmf_y: [f32; SPD_SAMPLES] = CIE_1931_CMF_Y.map(|v| v * Y_NORM);
192 let cmf_z: [f32; SPD_SAMPLES] = CIE_1931_CMF_Z.map(|v| v * Y_NORM);
193 let cmf = [cmf_x, cmf_y, cmf_z];
194
195 let mut ppt = [[0.0f32; 3]; 3];
197 for i in 0..3 {
198 for j in 0..3 {
199 ppt[i][j] = cmf[i].iter().zip(cmf[j].iter()).map(|(a, b)| a * b).sum();
200 }
201 }
202
203 let inv = invert_3x3(&ppt);
205
206 let target_vec = [target.x, target.y, target.z];
208 let mut spd = Spd::default();
209 for i in 0..SPD_SAMPLES {
210 let mut val = 0.0f32;
211 for j in 0..3 {
212 val += cmf[j][i]
213 * (inv[j][0] * target_vec[0]
214 + inv[j][1] * target_vec[1]
215 + inv[j][2] * target_vec[2]);
216 }
217 spd.samples[i] = val;
218 }
219
220 spd
221}
222
223fn invert_3x3(m: &[[f32; 3]; 3]) -> [[f32; 3]; 3] {
225 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
226 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
227 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
228
229 if det.abs() < 1e-20 {
230 return [[0.0; 3]; 3];
231 }
232
233 let inv_det = 1.0 / det;
234 [
235 [
236 (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
237 (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
238 (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
239 ],
240 [
241 (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
242 (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
243 (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
244 ],
245 [
246 (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
247 (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
248 (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
249 ],
250 ]
251}
252
253pub fn fibre_spd(particular: &Spd, basis: &[f32], coeffs: &[f32]) -> Spd {
262 let mut spd = *particular;
263 let n = SPD_SAMPLES;
264 for (k, &c) in coeffs.iter().enumerate() {
265 if k * n + n > basis.len() {
266 break;
267 }
268 for i in 0..n {
269 spd.samples[i] += c * basis[k * n + i];
270 }
271 }
272 spd
273}
274
275pub fn is_metameric(spd: &Spd, target: &Xyz, tolerance: f32) -> bool {
277 let xyz = spd_to_xyz(spd);
278 (xyz.x - target.x).abs() < tolerance
279 && (xyz.y - target.y).abs() < tolerance
280 && (xyz.z - target.z).abs() < tolerance
281}
282
283pub fn is_metameric_black(spd: &Spd, tolerance: f32) -> bool {
285 let xyz = spd_to_xyz(spd);
286 xyz.x.abs() < tolerance && xyz.y.abs() < tolerance && xyz.z.abs() < tolerance
287}
288
289#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn kernel_basis_projects_to_zero() {
299 let ker_dim = SPD_SAMPLES - 3;
300 let mut basis = vec![0.0f32; ker_dim * SPD_SAMPLES];
301 let count = metamer_kernel_basis(&mut basis).unwrap();
302 assert!(count > 0, "should produce at least one kernel vector");
303
304 for k in 0..count {
306 let mut spd = Spd::default();
307 spd.samples
308 .copy_from_slice(&basis[k * SPD_SAMPLES..(k + 1) * SPD_SAMPLES]);
309 let xyz = spd_to_xyz(&spd);
310 assert!(
311 is_metameric_black(&spd, 5e-2),
312 "kernel vector {} should be metameric-black, got XYZ=({:.6}, {:.6}, {:.6})",
313 k,
314 xyz.x,
315 xyz.y,
316 xyz.z
317 );
318 }
319 }
320
321 #[test]
322 fn min_norm_spd_reprojects_to_target() {
323 let target = Xyz::new(0.5, 0.6, 0.4);
324 let spd = min_norm_spd_for_xyz(&target);
325 let reprojected = spd_to_xyz(&spd);
326 assert!(
327 (reprojected.x - target.x).abs() < 0.05,
328 "X reprojection mismatch: {} vs {}",
329 reprojected.x,
330 target.x
331 );
332 assert!(
333 (reprojected.y - target.y).abs() < 0.05,
334 "Y reprojection mismatch: {} vs {}",
335 reprojected.y,
336 target.y
337 );
338 assert!(
339 (reprojected.z - target.z).abs() < 0.05,
340 "Z reprojection mismatch: {} vs {}",
341 reprojected.z,
342 target.z
343 );
344 }
345
346 #[test]
347 fn fibre_invariance() {
348 let target = Xyz::new(0.3, 0.5, 0.2);
350 let particular = min_norm_spd_for_xyz(&target);
351
352 let ker_dim = SPD_SAMPLES - 3;
353 let mut basis = vec![0.0f32; ker_dim * SPD_SAMPLES];
354 let count = metamer_kernel_basis(&mut basis).unwrap();
355
356 let coeffs: Vec<f32> = (0..count).map(|i| (i as f32 * 0.1).sin()).collect();
358 let fibre = fibre_spd(&particular, &basis, &coeffs);
359
360 assert!(
361 is_metameric(&fibre, &target, 5e-2),
362 "fibre element must be metameric to the target"
363 );
364 }
365
366 #[test]
367 fn min_norm_spd_determinism() {
368 let target = Xyz::new(0.4, 0.7, 0.3);
369 let spd1 = min_norm_spd_for_xyz(&target);
370 let spd2 = min_norm_spd_for_xyz(&target);
371 assert_eq!(spd1, spd2, "min-norm SPD must be deterministic");
372 }
373
374 #[test]
375 fn kernel_basis_determinism() {
376 let ker_dim = SPD_SAMPLES - 3;
377 let mut b1 = vec![0.0f32; ker_dim * SPD_SAMPLES];
378 let mut b2 = vec![0.0f32; ker_dim * SPD_SAMPLES];
379 let c1 = metamer_kernel_basis(&mut b1).unwrap();
380 let c2 = metamer_kernel_basis(&mut b2).unwrap();
381 assert_eq!(c1, c2);
382 assert_eq!(b1, b2, "kernel basis must be deterministic");
383 }
384}