1use crate::WAVELENGTHS;
2use crate::colorimetry::{X_BAR_2, X_BAR_10, XYZ, Y_BAR_2, Y_BAR_10, Z_BAR_2, Z_BAR_10, weighting};
3use crate::{Illuminant, Observer};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
7pub enum MeasurementMode {
8 #[default]
11 Reflective,
12 Emissive,
15 Ambient,
17}
18
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct MeasurementResult {
24 pub spectrum: SpectralData,
25 pub xyz: XYZ,
27 pub lab: crate::colorimetry::Lab,
29 pub rgb: (f32, f32, f32),
31 pub cct: f32,
33 pub cri: Option<f32>,
35}
36
37impl MeasurementResult {
38 pub fn rgb_u8(&self) -> (u8, u8, u8) {
40 (
41 (self.rgb.0.clamp(0.0, 1.0) * 255.0).round() as u8,
42 (self.rgb.1.clamp(0.0, 1.0) * 255.0).round() as u8,
43 (self.rgb.2.clamp(0.0, 1.0) * 255.0).round() as u8,
44 )
45 }
46
47 pub fn peak_wavelength(&self) -> f32 {
49 let (idx, _) = self.spectrum.values.iter().enumerate().fold(
50 (0, 0.0f32),
51 |(max_idx, max_val), (idx, &val)| {
52 if val > max_val {
53 (idx, val)
54 } else {
55 (max_idx, max_val)
56 }
57 },
58 );
59 380.0 + idx as f32 * 10.0
60 }
61
62 pub fn centroid_wavelength(&self) -> f32 {
64 let total: f32 = self.spectrum.values.iter().sum();
65 if total < 1e-6 {
66 return 550.0;
67 }
68 let weighted_sum: f32 = self
69 .spectrum
70 .values
71 .iter()
72 .enumerate()
73 .map(|(i, v)| (380.0 + i as f32 * 10.0) * v)
74 .sum();
75 weighted_sum / total
76 }
77}
78
79#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
80pub struct SpectralData {
81 pub wavelengths: Vec<f32>,
82 pub values: Vec<f32>,
83 pub mode: MeasurementMode,
85}
86
87impl SpectralData {
88 pub fn new(mut values: Vec<f32>) -> Self {
89 while values.len() < 41 {
91 values.push(0.0);
92 }
93 Self {
94 wavelengths: WAVELENGTHS.to_vec(),
95 values,
96 mode: MeasurementMode::default(),
97 }
98 }
99
100 pub fn with_mode(mut values: Vec<f32>, mode: MeasurementMode) -> Self {
102 while values.len() < 41 {
103 values.push(0.0);
104 }
105 Self {
106 wavelengths: WAVELENGTHS.to_vec(),
107 values,
108 mode,
109 }
110 }
111
112 pub fn set_mode(&mut self, mode: MeasurementMode) {
114 self.mode = mode;
115 }
116
117 pub fn to_xyz(&self) -> XYZ {
120 self.to_xyz_ext(Illuminant::D65, Observer::CIE1931_2)
121 }
122
123 pub fn to_xyz_ext(&self, source: Illuminant, obs: Observer) -> XYZ {
128 match self.mode {
129 MeasurementMode::Reflective => {
130 match (source, obs) {
131 (Illuminant::D65, Observer::CIE1931_2) => self.to_xyz_reflective_weighted(
132 &weighting::WX_D65_2_10,
133 &weighting::WY_D65_2_10,
134 &weighting::WZ_D65_2_10,
135 weighting::SUM_WY_D65_2_10,
136 ),
137 (Illuminant::D50, Observer::CIE1931_2) => self.to_xyz_reflective_weighted(
138 &weighting::WX_D50_2_10,
139 &weighting::WY_D50_2_10,
140 &weighting::WZ_D50_2_10,
141 weighting::SUM_WY_D50_2_10,
142 ),
143 _ => {
145 let spd = source.get_spd();
146 let (xb, yb, zb) = obs.get_cmfs();
147 let mut wx = [0.0f32; 41];
148 let mut wy = [0.0f32; 41];
149 let mut wz = [0.0f32; 41];
150 let mut sum_wy = 0.0f32;
151
152 for i in 0..41 {
153 wx[i] = spd[i] * xb[i];
154 wy[i] = spd[i] * yb[i];
155 wz[i] = spd[i] * zb[i];
156 sum_wy += wy[i];
157 }
158
159 self.to_xyz_reflective_weighted(&wx, &wy, &wz, sum_wy)
160 }
161 }
162 }
163 MeasurementMode::Emissive | MeasurementMode::Ambient => self.to_xyz_emissive_ext(obs),
164 }
165 }
166
167 fn to_xyz_reflective_weighted(
169 &self,
170 wx: &[f32; 41],
171 wy: &[f32; 41],
172 wz: &[f32; 41],
173 sum_wy: f32,
174 ) -> XYZ {
175 let mut x = 0.0f32;
176 let mut y = 0.0f32;
177 let mut z = 0.0f32;
178
179 for i in 0..41 {
180 x += self.values[i] * wx[i];
181 y += self.values[i] * wy[i];
182 z += self.values[i] * wz[i];
183 }
184
185 let scale = 100.0 / sum_wy;
187
188 XYZ {
189 x: x * scale,
190 y: y * scale,
191 z: z * scale,
192 }
193 }
194
195 pub fn resample(&self, start: f32, end: f32, step: f32) -> Self {
199 let mut new_values = Vec::new();
200 let mut current_wl = start;
201
202 let mut padded_values = Vec::with_capacity(self.values.len() + 5);
204 if !self.values.is_empty() {
205 padded_values.push(self.values[0]);
206 padded_values.push(self.values[0]);
207 padded_values.extend_from_slice(&self.values);
208 padded_values.push(*self.values.last().unwrap());
209 padded_values.push(*self.values.last().unwrap());
210 padded_values.push(*self.values.last().unwrap());
211 } else {
212 return Self {
213 wavelengths: Vec::new(),
214 values: Vec::new(),
215 mode: self.mode,
216 };
217 }
218
219 let orig_start = self.wavelengths[0];
220 let orig_step = if self.wavelengths.len() > 1 {
221 self.wavelengths[1] - self.wavelengths[0]
222 } else {
223 10.0
224 };
225
226 while current_wl <= end + 1e-3 {
227 let t = (current_wl - orig_start) / orig_step;
228 let i = t.floor() as i32;
229 let x = t - i as f32;
230
231 let idx = (i + 2) as usize;
234
235 if idx < 2 || idx + 3 >= padded_values.len() {
236 let val = if current_wl <= orig_start {
238 self.values[0]
239 } else if current_wl >= orig_start + (self.values.len() - 1) as f32 * orig_step {
240 *self.values.last().unwrap()
241 } else {
242 let i_idx = i.max(0) as usize;
243 let v0 = self.values[i_idx];
244 let v1 = self.values[(i_idx + 1).min(self.values.len() - 1)];
245 v0 + x * (v1 - v0)
246 };
247 new_values.push(val);
248 } else {
249 let y = [
250 padded_values[idx - 2],
251 padded_values[idx - 1],
252 padded_values[idx],
253 padded_values[idx + 1],
254 padded_values[idx + 2],
255 padded_values[idx + 3],
256 ];
257 new_values.push(Self::sprague_interpolate(x, &y));
258 }
259
260 current_wl += step;
261 }
262
263 let mut wavelengths = Vec::new();
264 let mut wl = start;
265 while wl <= end + 1e-3 {
266 wavelengths.push(wl);
267 wl += step;
268 }
269
270 Self {
271 wavelengths,
272 values: new_values,
273 mode: self.mode,
274 }
275 }
276
277 fn sprague_interpolate(x: f32, y: &[f32; 6]) -> f32 {
280 let x2 = x * x;
281 let x3 = x2 * x;
282 let x4 = x3 * x;
283 let x5 = x4 * x;
284
285 let a0 = y[2];
287 let a1 = (2.0 * y[0] - 16.0 * y[1] + 16.0 * y[3] - 2.0 * y[4]) / 24.0;
288 let a2 = (-y[0] + 16.0 * y[1] - 30.0 * y[2] + 16.0 * y[3] - y[4]) / 24.0;
289 let a3 = (-9.0 * y[0] + 39.0 * y[1] - 70.0 * y[2] + 66.0 * y[3] - 33.0 * y[4] + 7.0 * y[5])
290 / 120.0;
291 let a4 = (13.0 * y[0] - 64.0 * y[1] + 126.0 * y[2] - 124.0 * y[3] + 61.0 * y[4]
292 - 12.0 * y[5])
293 / 120.0;
294 let a5 = (-5.0 * y[0] + 25.0 * y[1] - 50.0 * y[2] + 50.0 * y[3] - 25.0 * y[4] + 5.0 * y[5])
295 / 120.0;
296
297 a0 + a1 * x + a2 * x2 + a3 * x3 + a4 * x4 + a5 * x5
298 }
299
300 pub fn to_xyz_emissive_ext(&self, obs: Observer) -> XYZ {
302 const STEP: f32 = 10.0;
303 let (xb, yb, zb) = obs.get_cmfs();
304
305 let mut x = 0.0f32;
306 let mut y = 0.0f32;
307 let mut z = 0.0f32;
308
309 for i in 0..41 {
310 x += self.values[i] * xb[i];
311 y += self.values[i] * yb[i];
312 z += self.values[i] * zb[i];
313 }
314
315 XYZ {
316 x: x * STEP,
317 y: y * STEP,
318 z: z * STEP,
319 }
320 }
321
322 pub fn get_wavelength_data(&self) -> (Vec<f32>, Vec<f32>) {
325 (self.wavelengths.clone(), self.values.clone())
326 }
327
328 pub fn to_xyz_reflective_2(&self) -> XYZ {
336 let mut x = 0.0f32;
337 let mut y = 0.0f32;
338 let mut z = 0.0f32;
339
340 for i in 0..41 {
341 x += self.values[i] * weighting::WX_D65_2_10[i];
342 y += self.values[i] * weighting::WY_D65_2_10[i];
343 z += self.values[i] * weighting::WZ_D65_2_10[i];
344 }
345
346 let scale = 100.0 / weighting::SUM_WY_D65_2_10;
349
350 XYZ {
351 x: x * scale,
352 y: y * scale,
353 z: z * scale,
354 }
355 }
356
357 pub fn to_xyz_emissive_2(&self) -> XYZ {
370 const STEP: f32 = 10.0; let mut x = 0.0f32;
373 let mut y = 0.0f32;
374 let mut z = 0.0f32;
375
376 for i in 0..41 {
377 x += self.values[i] * X_BAR_2[i];
378 y += self.values[i] * Y_BAR_2[i];
379 z += self.values[i] * Z_BAR_2[i];
380 }
381
382 XYZ {
385 x: x * STEP,
386 y: y * STEP,
387 z: z * STEP,
388 }
389 }
390
391 #[deprecated(
394 since = "0.2.0",
395 note = "Use to_xyz() with appropriate MeasurementMode"
396 )]
397 pub fn to_xyz_2(&self) -> XYZ {
398 self.to_xyz_emissive_2()
399 }
400
401 pub fn to_xyz_10(&self) -> XYZ {
404 const STEP: f32 = 10.0;
405
406 let mut x = 0.0f32;
407 let mut y = 0.0f32;
408 let mut z = 0.0f32;
409
410 for i in 0..41 {
411 x += self.values[i] * X_BAR_10[i];
412 y += self.values[i] * Y_BAR_10[i];
413 z += self.values[i] * Z_BAR_10[i];
414 }
415
416 XYZ {
417 x: x * STEP,
418 y: y * STEP,
419 z: z * STEP,
420 }
421 }
422
423 pub fn calculate_k(illuminant_spd: &[f32], y_bar: &[f32], step: f32) -> f32 {
434 let sum_s_y: f32 = illuminant_spd
435 .iter()
436 .zip(y_bar.iter())
437 .map(|(s, yb)| s * yb)
438 .sum();
439 100.0 / (sum_s_y * step)
440 }
441
442 pub fn to_result(&self) -> MeasurementResult {
446 let target_illuminant = Illuminant::D50;
448 let observer = Observer::CIE1931_2;
449
450 let xyz = if self.mode == MeasurementMode::Reflective {
454 self.to_xyz_ext(target_illuminant, observer)
455 } else {
456 self.to_xyz_ext(Illuminant::D65, observer)
457 };
458
459 let wp = target_illuminant.get_white_point(observer);
464
465 let lab = if self.mode == MeasurementMode::Reflective {
469 xyz.to_lab(wp)
470 } else {
471 let adapted_xyz = crate::colorimetry::chromatic_adaptation::bradford_adapt(
474 xyz,
475 Illuminant::D65.get_white_point(observer), wp,
477 );
478 adapted_xyz.to_lab(wp)
479 };
480
481 let srgb_xyz = if self.mode == MeasurementMode::Reflective {
484 crate::colorimetry::chromatic_adaptation::bradford_adapt(
486 xyz,
487 wp,
488 Illuminant::D65.get_white_point(observer),
489 )
490 } else {
491 xyz };
493 let (r, g, b) = srgb_xyz.to_srgb();
494 let rgb_float = (r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
495
496 let cct = xyz.to_cct();
498
499 let (ra, _) = crate::colorimetry::metrics::calculate_cri(self);
504
505 MeasurementResult {
506 spectrum: self.clone(),
507 xyz,
508 lab,
509 rgb: rgb_float,
510 cct,
511 cri: Some(ra),
512 }
513 }
514}
515
516impl XYZ {
517 pub fn to_chromaticity(&self) -> (f32, f32) {
518 let sum = self.x + self.y + self.z;
519 if sum < 1e-6 {
520 return (0.3127, 0.3290);
521 } (self.x / sum, self.y / sum)
523 }
524
525 pub fn to_cct(&self) -> f32 {
527 let (x, y) = self.to_chromaticity();
528 let n = (x - 0.3320) / (0.1858 - y);
529 449.0 * n.powi(3) + 3525.0 * n.powi(2) + 6823.3 * n + 5524.33
531 }
532}
533
534impl std::fmt::Display for SpectralData {
535 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 writeln!(f, "Spectral Data (380nm - 730nm, {:?} mode):", self.mode)?;
537 for (w, v) in self.wavelengths.iter().zip(self.values.iter()) {
538 writeln!(f, " {:.0}nm: {:.6}", w, v)?;
539 }
540 Ok(())
541 }
542}