1#![allow(clippy::many_single_char_names)]
18#![allow(clippy::unreadable_literal)]
19#![allow(clippy::excessive_precision)]
20
21use std::f64::consts::PI;
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct Lab {
26 pub l: f64,
28 pub a: f64,
30 pub b: f64,
32}
33
34impl Lab {
35 pub const fn new(l: f64, a: f64, b: f64) -> Self {
37 Self { l, a, b }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct Rgb {
44 pub r: u8,
45 pub g: u8,
46 pub b: u8,
47}
48
49impl Rgb {
50 pub const fn new(r: u8, g: u8, b: u8) -> Self {
52 Self { r, g, b }
53 }
54}
55
56const D65_XN: f64 = 0.95047;
58const D65_YN: f64 = 1.00000;
59const D65_ZN: f64 = 1.08883;
60
61pub fn rgb_to_lab(rgb: Rgb) -> Lab {
65 let r = srgb_to_linear(rgb.r as f64 / 255.0);
67 let g = srgb_to_linear(rgb.g as f64 / 255.0);
68 let b = srgb_to_linear(rgb.b as f64 / 255.0);
69
70 let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
72 let y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
73 let z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041;
74
75 let fx = lab_f(x / D65_XN);
77 let fy = lab_f(y / D65_YN);
78 let fz = lab_f(z / D65_ZN);
79
80 Lab {
81 l: 116.0 * fy - 16.0,
82 a: 500.0 * (fx - fy),
83 b: 200.0 * (fy - fz),
84 }
85}
86
87fn srgb_to_linear(c: f64) -> f64 {
89 if c <= 0.04045 {
90 c / 12.92
91 } else {
92 ((c + 0.055) / 1.055).powf(2.4)
93 }
94}
95
96fn lab_f(t: f64) -> f64 {
98 const DELTA: f64 = 6.0 / 29.0;
99 const DELTA_CUBE: f64 = DELTA * DELTA * DELTA;
100
101 if t > DELTA_CUBE {
102 t.cbrt()
103 } else {
104 t / (3.0 * DELTA * DELTA) + 4.0 / 29.0
105 }
106}
107
108pub fn ciede2000(lab1: Lab, lab2: Lab) -> f64 {
120 const KL: f64 = 1.0;
122 const KC: f64 = 1.0;
123 const KH: f64 = 1.0;
124
125 let l1 = lab1.l;
126 let a1 = lab1.a;
127 let b1 = lab1.b;
128 let l2 = lab2.l;
129 let a2 = lab2.a;
130 let b2 = lab2.b;
131
132 let c1_ab = a1.hypot(b1);
134 let c2_ab = a2.hypot(b2);
135 let c_ab_mean = (c1_ab + c2_ab) / 2.0;
136
137 let c_ab_mean_pow7 = c_ab_mean.powi(7);
139 let g = 0.5 * (1.0 - (c_ab_mean_pow7 / (c_ab_mean_pow7 + 6103515625.0_f64)).sqrt()); let a1_prime = a1 * (1.0 + g);
143 let a2_prime = a2 * (1.0 + g);
144
145 let c1_prime = a1_prime.hypot(b1);
147 let c2_prime = a2_prime.hypot(b2);
148
149 let h1_prime = hue_angle(a1_prime, b1);
151 let h2_prime = hue_angle(a2_prime, b2);
152
153 let delta_l_prime = l2 - l1;
155 let delta_c_prime = c2_prime - c1_prime;
156
157 let delta_h_prime = if c1_prime * c2_prime == 0.0 {
158 0.0
159 } else {
160 let delta_h = h2_prime - h1_prime;
161 if delta_h.abs() <= 180.0 {
162 delta_h
163 } else if delta_h > 180.0 {
164 delta_h - 360.0
165 } else {
166 delta_h + 360.0
167 }
168 };
169
170 let delta_h_prime_rad = delta_h_prime * PI / 180.0;
172 let delta_big_h_prime = 2.0 * (c1_prime * c2_prime).sqrt() * (delta_h_prime_rad / 2.0).sin();
173
174 let l_prime_mean = (l1 + l2) / 2.0;
176 let c_prime_mean = (c1_prime + c2_prime) / 2.0;
177
178 let h_prime_mean = if c1_prime * c2_prime == 0.0 {
179 h1_prime + h2_prime
180 } else {
181 let h_diff = (h1_prime - h2_prime).abs();
182 if h_diff <= 180.0 {
183 (h1_prime + h2_prime) / 2.0
184 } else if h1_prime + h2_prime < 360.0 {
185 (h1_prime + h2_prime + 360.0) / 2.0
186 } else {
187 (h1_prime + h2_prime - 360.0) / 2.0
188 }
189 };
190
191 let h_prime_mean_rad = h_prime_mean * PI / 180.0;
193 let t = 1.0 - 0.17 * (h_prime_mean_rad - PI / 6.0).cos()
194 + 0.24 * (2.0 * h_prime_mean_rad).cos()
195 + 0.32 * (3.0 * h_prime_mean_rad + PI / 30.0).cos()
196 - 0.20 * (4.0 * h_prime_mean_rad - 63.0 * PI / 180.0).cos();
197
198 let l_prime_mean_minus_50_sq = (l_prime_mean - 50.0).powi(2);
200 let sl = 1.0 + (0.015 * l_prime_mean_minus_50_sq) / (20.0 + l_prime_mean_minus_50_sq).sqrt();
201 let sc = 1.0 + 0.045 * c_prime_mean;
202 let sh = 1.0 + 0.015 * c_prime_mean * t;
203
204 let delta_theta = 30.0 * (-((h_prime_mean - 275.0) / 25.0).powi(2)).exp();
207 let c_prime_mean_pow7 = c_prime_mean.powi(7);
208 let rc = 2.0 * (c_prime_mean_pow7 / (c_prime_mean_pow7 + 6103515625.0_f64)).sqrt();
209 let rt = -(2.0 * delta_theta * PI / 180.0).sin() * rc;
211
212 let term_l = delta_l_prime / (KL * sl);
214 let term_c = delta_c_prime / (KC * sc);
215 let term_h = delta_big_h_prime / (KH * sh);
216
217 (term_l * term_l + term_c * term_c + term_h * term_h + rt * term_c * term_h).sqrt()
218}
219
220fn hue_angle(a: f64, b: f64) -> f64 {
222 if a == 0.0 && b == 0.0 {
223 0.0
224 } else {
225 let mut h = b.atan2(a) * 180.0 / PI;
226 if h < 0.0 {
227 h += 360.0;
228 }
229 h
230 }
231}
232
233pub fn average_delta_e(colors1: &[Lab], colors2: &[Lab]) -> f64 {
235 if colors1.is_empty() || colors1.len() != colors2.len() {
236 return f64::MAX;
237 }
238
239 let total: f64 = colors1
240 .iter()
241 .zip(colors2.iter())
242 .map(|(c1, c2)| ciede2000(*c1, *c2))
243 .sum();
244
245 total / colors1.len() as f64
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub enum DeltaECategory {
251 Imperceptible,
253 BarelyPerceptible,
255 Noticeable,
257 Distinct,
259 VeryDistinct,
261}
262
263impl DeltaECategory {
264 pub fn from_delta_e(de: f64) -> Self {
266 if de < 1.0 {
267 Self::Imperceptible
268 } else if de < 2.0 {
269 Self::BarelyPerceptible
270 } else if de < 10.0 {
271 Self::Noticeable
272 } else if de < 50.0 {
273 Self::Distinct
274 } else {
275 Self::VeryDistinct
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
287 fn test_ciede2000_cie_reference_vectors() {
288 let test_cases = [
291 (50.0, 2.6772, -79.7751, 50.0, 0.0, -82.7485, 2.0425),
293 (50.0, 3.1571, -77.2803, 50.0, 0.0, -82.7485, 2.8615),
295 (50.0, 2.8361, -74.0200, 50.0, 0.0, -82.7485, 3.4412),
297 (50.0, -1.3802, -84.2814, 50.0, 0.0, -82.7485, 1.0),
299 (50.0, -1.1848, -84.8006, 50.0, 0.0, -82.7485, 1.0),
301 (50.0, -0.9009, -85.5211, 50.0, 0.0, -82.7485, 1.0),
303 (50.0, 0.0, 0.0, 50.0, -1.0, 2.0, 2.3669),
305 (50.0, -1.0, 2.0, 50.0, 0.0, 0.0, 2.3669),
307 (50.0, 2.49, -0.001, 50.0, -2.49, 0.0009, 7.1792),
309 (50.0, 2.49, -0.001, 50.0, -2.49, 0.001, 7.1792),
311 (50.0, 2.49, -0.001, 50.0, -2.49, 0.0011, 7.2195),
313 (50.0, 2.49, -0.001, 50.0, -2.49, 0.0012, 7.2195),
315 (50.0, -0.001, 2.49, 50.0, 0.0009, -2.49, 4.8045),
317 (50.0, -0.001, 2.49, 50.0, 0.001, -2.49, 4.8045),
319 (50.0, -0.001, 2.49, 50.0, 0.0011, -2.49, 4.7461),
321 (50.0, 2.5, 0.0, 50.0, 0.0, -2.5, 4.3065),
323 (50.0, 2.5, 0.0, 73.0, 25.0, -18.0, 27.1492),
325 (50.0, 2.5, 0.0, 61.0, -5.0, 29.0, 22.8977),
327 (50.0, 2.5, 0.0, 56.0, -27.0, -3.0, 31.9030),
329 (50.0, 2.5, 0.0, 58.0, 24.0, 15.0, 19.4535),
331 (50.0, 2.5, 0.0, 50.0, 3.1736, 0.5854, 1.0),
333 (50.0, 2.5, 0.0, 50.0, 3.2972, 0.0, 1.0),
335 (50.0, 2.5, 0.0, 50.0, 1.8634, 0.5757, 1.0),
337 (50.0, 2.5, 0.0, 50.0, 3.2592, 0.335, 1.0),
339 (
341 60.2574, -34.0099, 36.2677, 60.4626, -34.1751, 39.4387, 1.2644,
342 ),
343 (
345 63.0109, -31.0961, -5.8663, 62.8187, -29.7946, -4.0864, 1.263,
346 ),
347 (61.2901, 3.7196, -5.3901, 61.4292, 2.248, -4.962, 1.8731),
349 (35.0831, -44.1164, 3.7933, 35.0232, -40.0716, 1.5901, 1.8645),
351 (22.7233, 20.0904, -46.694, 23.0331, 14.973, -42.5619, 2.0373),
353 (36.4612, 47.858, 18.3852, 36.2715, 50.5065, 21.2231, 1.4146),
355 (90.8027, -2.0831, 1.441, 91.1528, -1.6435, 0.0447, 1.4441),
357 (90.9257, -0.5406, -0.9208, 88.6381, -0.8985, -0.7239, 1.5381),
359 (6.7747, -0.2908, -2.4247, 5.8714, -0.0985, -2.2286, 0.6377),
361 (2.0776, 0.0795, -1.135, 0.9033, -0.0636, -0.5514, 0.9082),
363 ];
364
365 for (i, &(l1, a1, b1, l2, a2, b2, expected)) in test_cases.iter().enumerate() {
366 let lab1 = Lab::new(l1, a1, b1);
367 let lab2 = Lab::new(l2, a2, b2);
368 let result = ciede2000(lab1, lab2);
369
370 let diff = (result - expected).abs();
372 assert!(
373 diff < 0.005,
374 "Test pair {}: expected {:.4}, got {:.4}, diff {:.4}",
375 i + 1,
376 expected,
377 result,
378 diff
379 );
380 }
381 }
382
383 #[test]
384 fn test_identical_colors() {
385 let lab = Lab::new(50.0, 25.0, -30.0);
386 assert!((ciede2000(lab, lab)).abs() < 0.0001);
387 }
388
389 #[test]
390 fn test_black_and_white() {
391 let black = Lab::new(0.0, 0.0, 0.0);
392 let white = Lab::new(100.0, 0.0, 0.0);
393 let de = ciede2000(black, white);
394 assert!(de > 50.0);
396 }
397
398 #[test]
399 fn test_gray_scale() {
400 let gray1 = Lab::new(50.0, 0.0, 0.0);
402 let gray2 = Lab::new(60.0, 0.0, 0.0);
403 let de = ciede2000(gray1, gray2);
404 assert!(de > 5.0 && de < 15.0);
406 }
407
408 #[test]
409 fn test_rgb_to_lab_white() {
410 let white = rgb_to_lab(Rgb::new(255, 255, 255));
411 assert!((white.l - 100.0).abs() < 0.1);
412 assert!(white.a.abs() < 0.1);
413 assert!(white.b.abs() < 0.1);
414 }
415
416 #[test]
417 fn test_rgb_to_lab_black() {
418 let black = rgb_to_lab(Rgb::new(0, 0, 0));
419 assert!(black.l.abs() < 0.1);
420 assert!(black.a.abs() < 0.1);
421 assert!(black.b.abs() < 0.1);
422 }
423
424 #[test]
425 fn test_rgb_to_lab_red() {
426 let red = rgb_to_lab(Rgb::new(255, 0, 0));
427 assert!(red.l > 50.0 && red.l < 56.0);
429 assert!(red.a > 75.0 && red.a < 85.0);
430 assert!(red.b > 60.0 && red.b < 70.0);
431 }
432
433 #[test]
434 fn test_delta_e_category() {
435 assert_eq!(
436 DeltaECategory::from_delta_e(0.5),
437 DeltaECategory::Imperceptible
438 );
439 assert_eq!(
440 DeltaECategory::from_delta_e(1.5),
441 DeltaECategory::BarelyPerceptible
442 );
443 assert_eq!(
444 DeltaECategory::from_delta_e(5.0),
445 DeltaECategory::Noticeable
446 );
447 assert_eq!(DeltaECategory::from_delta_e(25.0), DeltaECategory::Distinct);
448 assert_eq!(
449 DeltaECategory::from_delta_e(60.0),
450 DeltaECategory::VeryDistinct
451 );
452 }
453
454 #[test]
455 fn test_symmetry() {
456 let lab1 = Lab::new(50.0, 25.0, -30.0);
458 let lab2 = Lab::new(60.0, -10.0, 15.0);
459 let de1 = ciede2000(lab1, lab2);
460 let de2 = ciede2000(lab2, lab1);
461 assert!((de1 - de2).abs() < 0.0001);
462 }
463
464 #[test]
465 fn test_average_delta_e_empty() {
466 let empty: Vec<Lab> = vec![];
467 let result = average_delta_e(&empty, &empty);
468 assert_eq!(result, f64::MAX);
469 }
470
471 #[test]
472 fn test_average_delta_e_different_lengths() {
473 let colors1 = vec![Lab::new(50.0, 0.0, 0.0)];
474 let colors2 = vec![Lab::new(50.0, 0.0, 0.0), Lab::new(60.0, 0.0, 0.0)];
475 let result = average_delta_e(&colors1, &colors2);
476 assert_eq!(result, f64::MAX);
477 }
478
479 #[test]
480 fn test_average_delta_e_identical() {
481 let colors1 = vec![Lab::new(50.0, 0.0, 0.0), Lab::new(60.0, 10.0, -10.0)];
482 let colors2 = colors1.clone();
483 let result = average_delta_e(&colors1, &colors2);
484 assert!(result < 0.001); }
486
487 #[test]
488 fn test_average_delta_e_different() {
489 let colors1 = vec![Lab::new(50.0, 0.0, 0.0), Lab::new(50.0, 0.0, 0.0)];
490 let colors2 = vec![Lab::new(70.0, 20.0, 20.0), Lab::new(70.0, 20.0, 20.0)];
491 let result = average_delta_e(&colors1, &colors2);
492 assert!(result > 0.0);
493 }
494
495 #[test]
496 fn test_lab_new() {
497 let lab = Lab::new(50.0, 25.0, -30.0);
498 assert_eq!(lab.l, 50.0);
499 assert_eq!(lab.a, 25.0);
500 assert_eq!(lab.b, -30.0);
501 }
502
503 #[test]
504 fn test_lab_clone() {
505 let lab1 = Lab::new(50.0, 25.0, -30.0);
506 let lab2 = lab1;
507 assert_eq!(lab1, lab2);
508 }
509
510 #[test]
511 fn test_lab_copy() {
512 let lab1 = Lab::new(50.0, 25.0, -30.0);
513 let lab2 = lab1; assert_eq!(lab1, lab2);
515 }
516
517 #[test]
518 fn test_rgb_new() {
519 let rgb = Rgb::new(255, 128, 64);
520 assert_eq!(rgb.r, 255);
521 assert_eq!(rgb.g, 128);
522 assert_eq!(rgb.b, 64);
523 }
524
525 #[test]
526 fn test_rgb_clone() {
527 let rgb1 = Rgb::new(100, 150, 200);
528 let rgb2 = rgb1;
529 assert_eq!(rgb1, rgb2);
530 }
531
532 #[test]
533 fn test_rgb_to_lab_green() {
534 let green = rgb_to_lab(Rgb::new(0, 255, 0));
535 assert!(green.l > 85.0 && green.l < 90.0);
537 assert!(green.a < -80.0);
538 assert!(green.b > 75.0);
539 }
540
541 #[test]
542 fn test_rgb_to_lab_blue() {
543 let blue = rgb_to_lab(Rgb::new(0, 0, 255));
544 assert!(blue.l > 30.0 && blue.l < 35.0);
546 assert!(blue.a > 75.0);
547 assert!(blue.b < -100.0);
548 }
549
550 #[test]
551 fn test_rgb_to_lab_gray() {
552 let gray = rgb_to_lab(Rgb::new(128, 128, 128));
553 assert!(gray.a.abs() < 1.0);
555 assert!(gray.b.abs() < 1.0);
556 assert!(gray.l > 50.0 && gray.l < 55.0);
557 }
558
559 #[test]
560 fn test_delta_e_category_boundary_values() {
561 assert_eq!(
563 DeltaECategory::from_delta_e(0.0),
564 DeltaECategory::Imperceptible
565 );
566 assert_eq!(
567 DeltaECategory::from_delta_e(0.9999),
568 DeltaECategory::Imperceptible
569 );
570 assert_eq!(
571 DeltaECategory::from_delta_e(1.0),
572 DeltaECategory::BarelyPerceptible
573 );
574 assert_eq!(
575 DeltaECategory::from_delta_e(1.9999),
576 DeltaECategory::BarelyPerceptible
577 );
578 assert_eq!(
579 DeltaECategory::from_delta_e(2.0),
580 DeltaECategory::Noticeable
581 );
582 assert_eq!(
583 DeltaECategory::from_delta_e(9.9999),
584 DeltaECategory::Noticeable
585 );
586 assert_eq!(DeltaECategory::from_delta_e(10.0), DeltaECategory::Distinct);
587 assert_eq!(
588 DeltaECategory::from_delta_e(49.9999),
589 DeltaECategory::Distinct
590 );
591 assert_eq!(
592 DeltaECategory::from_delta_e(50.0),
593 DeltaECategory::VeryDistinct
594 );
595 }
596
597 #[test]
598 fn test_delta_e_category_debug() {
599 let cat = DeltaECategory::Noticeable;
600 let debug = format!("{:?}", cat);
601 assert!(debug.contains("Noticeable"));
602 }
603
604 #[test]
605 fn test_delta_e_category_clone() {
606 let cat1 = DeltaECategory::Distinct;
607 let cat2 = cat1;
608 assert_eq!(cat1, cat2);
609 }
610
611 #[test]
612 fn test_hue_angle_quadrants() {
613 let h1 = hue_angle(1.0, 1.0);
616 assert!(h1 > 0.0 && h1 < 90.0);
617
618 let h2 = hue_angle(-1.0, 1.0);
620 assert!(h2 > 90.0 && h2 < 180.0);
621
622 let h3 = hue_angle(-1.0, -1.0);
624 assert!(h3 > 180.0 && h3 < 270.0);
625
626 let h4 = hue_angle(1.0, -1.0);
628 assert!(h4 > 270.0 && h4 < 360.0);
629 }
630
631 #[test]
632 fn test_hue_angle_axes() {
633 let h_pos_a = hue_angle(1.0, 0.0);
634 assert!(h_pos_a.abs() < 0.01); let h_pos_b = hue_angle(0.0, 1.0);
637 assert!((h_pos_b - 90.0).abs() < 0.01); let h_neg_a = hue_angle(-1.0, 0.0);
640 assert!((h_neg_a - 180.0).abs() < 0.01); let h_neg_b = hue_angle(0.0, -1.0);
643 assert!((h_neg_b - 270.0).abs() < 0.01); }
645
646 #[test]
647 fn test_srgb_to_linear_threshold() {
648 let below = srgb_to_linear(0.04);
650 let above = srgb_to_linear(0.05);
651
652 assert!(below < above);
654 assert!(below > 0.0);
655 }
656
657 #[test]
658 fn test_srgb_to_linear_endpoints() {
659 let at_zero = srgb_to_linear(0.0);
660 let at_one = srgb_to_linear(1.0);
661
662 assert!(at_zero.abs() < 0.0001);
663 assert!((at_one - 1.0).abs() < 0.0001);
664 }
665
666 #[test]
667 fn test_lab_f_threshold() {
668 let delta = 6.0_f64 / 29.0;
670 let delta_cube = delta * delta * delta;
671 let below = lab_f(delta_cube * 0.9);
672 let above = lab_f(delta_cube * 1.1);
673
674 assert!(above > below);
676 }
677
678 #[test]
679 fn test_ciede2000_large_difference() {
680 let red = Lab::new(53.0, 80.0, 67.0);
682 let cyan = Lab::new(91.0, -48.0, -14.0);
683
684 let de = ciede2000(red, cyan);
685 assert!(de > 50.0); }
687
688 #[test]
689 fn test_ciede2000_similar_colors() {
690 let lab1 = Lab::new(50.0, 10.0, 10.0);
692 let lab2 = Lab::new(50.5, 10.0, 10.0);
693
694 let de = ciede2000(lab1, lab2);
695 assert!(de < 1.0); }
697
698 #[test]
699 fn test_average_delta_e_single() {
700 let colors1 = vec![Lab::new(50.0, 0.0, 0.0)];
701 let colors2 = vec![Lab::new(60.0, 0.0, 0.0)];
702 let result = average_delta_e(&colors1, &colors2);
703
704 let expected = ciede2000(colors1[0], colors2[0]);
706 assert!((result - expected).abs() < 0.0001);
707 }
708
709 #[test]
710 fn test_lab_debug() {
711 let lab = Lab::new(50.0, 25.0, -30.0);
712 let debug = format!("{:?}", lab);
713 assert!(debug.contains("Lab"));
714 assert!(debug.contains("50"));
715 }
716
717 #[test]
718 fn test_rgb_debug() {
719 let rgb = Rgb::new(255, 128, 0);
720 let debug = format!("{:?}", rgb);
721 assert!(debug.contains("Rgb"));
722 assert!(debug.contains("255"));
723 }
724}