1use crate::{AlignError, AlignResult};
12#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct ColorRgb {
17 pub r: f32,
19 pub g: f32,
21 pub b: f32,
23}
24
25impl ColorRgb {
26 #[must_use]
28 pub fn new(r: f32, g: f32, b: f32) -> Self {
29 Self { r, g, b }
30 }
31
32 #[must_use]
34 pub fn from_u8(r: u8, g: u8, b: u8) -> Self {
35 Self {
36 r: f32::from(r) / 255.0,
37 g: f32::from(g) / 255.0,
38 b: f32::from(b) / 255.0,
39 }
40 }
41
42 #[must_use]
44 pub fn to_u8(&self) -> (u8, u8, u8) {
45 (
46 (self.r * 255.0).clamp(0.0, 255.0) as u8,
47 (self.g * 255.0).clamp(0.0, 255.0) as u8,
48 (self.b * 255.0).clamp(0.0, 255.0) as u8,
49 )
50 }
51
52 #[must_use]
54 pub fn to_lab(&self) -> ColorLab {
55 let r = Self::gamma_to_linear(self.r);
57 let g = Self::gamma_to_linear(self.g);
58 let b = Self::gamma_to_linear(self.b);
59
60 let x = r * 0.4124 + g * 0.3576 + b * 0.1805;
61 let y = r * 0.2126 + g * 0.7152 + b * 0.0722;
62 let z = r * 0.0193 + g * 0.1192 + b * 0.9505;
63
64 let xn = 0.95047; let yn = 1.0;
67 let zn = 1.08883;
68
69 let fx = Self::lab_f(x / xn);
70 let fy = Self::lab_f(y / yn);
71 let fz = Self::lab_f(z / zn);
72
73 let l = 116.0 * fy - 16.0;
74 let a = 500.0 * (fx - fy);
75 let b_lab = 200.0 * (fy - fz);
76
77 ColorLab::new(l, a, b_lab)
78 }
79
80 fn gamma_to_linear(v: f32) -> f32 {
81 if v <= 0.04045 {
82 v / 12.92
83 } else {
84 ((v + 0.055) / 1.055).powf(2.4)
85 }
86 }
87
88 fn lab_f(t: f32) -> f32 {
89 let delta = 6.0 / 29.0;
90 if t > delta * delta * delta {
91 t.powf(1.0 / 3.0)
92 } else {
93 t / (3.0 * delta * delta) + 4.0 / 29.0
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct ColorLab {
101 pub l: f32,
103 pub a: f32,
105 pub b: f32,
107}
108
109impl ColorLab {
110 #[must_use]
112 pub fn new(l: f32, a: f32, b: f32) -> Self {
113 Self { l, a, b }
114 }
115
116 #[must_use]
118 pub fn to_rgb(&self) -> ColorRgb {
119 let fy = (self.l + 16.0) / 116.0;
121 let fx = self.a / 500.0 + fy;
122 let fz = fy - self.b / 200.0;
123
124 let xn = 0.95047;
125 let yn = 1.0;
126 let zn = 1.08883;
127
128 let x = xn * Self::lab_f_inv(fx);
129 let y = yn * Self::lab_f_inv(fy);
130 let z = zn * Self::lab_f_inv(fz);
131
132 let r = x * 3.2406 + y * -1.5372 + z * -0.4986;
134 let g = x * -0.9689 + y * 1.8758 + z * 0.0415;
135 let b = x * 0.0557 + y * -0.2040 + z * 1.0570;
136
137 ColorRgb::new(
138 Self::linear_to_gamma(r),
139 Self::linear_to_gamma(g),
140 Self::linear_to_gamma(b),
141 )
142 }
143
144 fn lab_f_inv(t: f32) -> f32 {
145 let delta = 6.0 / 29.0;
146 if t > delta {
147 t * t * t
148 } else {
149 3.0 * delta * delta * (t - 4.0 / 29.0)
150 }
151 }
152
153 fn linear_to_gamma(v: f32) -> f32 {
154 if v <= 0.0031308 {
155 12.92 * v
156 } else {
157 1.055 * v.powf(1.0 / 2.4) - 0.055
158 }
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct ColorStatistics {
165 pub mean: ColorRgb,
167 pub std_dev: ColorRgb,
169 pub mean_lab: ColorLab,
171 pub std_dev_lab: ColorLab,
173}
174
175impl ColorStatistics {
176 #[must_use]
178 pub fn from_image(rgb: &[u8], width: usize, height: usize) -> Self {
179 let n = (width * height) as f32;
180 let mut sum_r = 0.0f32;
181 let mut sum_g = 0.0f32;
182 let mut sum_b = 0.0f32;
183
184 for pixel in rgb.chunks_exact(3) {
186 sum_r += f32::from(pixel[0]);
187 sum_g += f32::from(pixel[1]);
188 sum_b += f32::from(pixel[2]);
189 }
190
191 let mean = ColorRgb::new(
192 sum_r / (n * 255.0),
193 sum_g / (n * 255.0),
194 sum_b / (n * 255.0),
195 );
196
197 let mut var_r = 0.0f32;
199 let mut var_g = 0.0f32;
200 let mut var_b = 0.0f32;
201
202 for pixel in rgb.chunks_exact(3) {
203 let r = f32::from(pixel[0]) / 255.0 - mean.r;
204 let g = f32::from(pixel[1]) / 255.0 - mean.g;
205 let b = f32::from(pixel[2]) / 255.0 - mean.b;
206
207 var_r += r * r;
208 var_g += g * g;
209 var_b += b * b;
210 }
211
212 let std_dev = ColorRgb::new((var_r / n).sqrt(), (var_g / n).sqrt(), (var_b / n).sqrt());
213
214 let _mean_lab = mean.to_lab();
216
217 let mut sum_l = 0.0f32;
219 let mut sum_a = 0.0f32;
220 let mut sum_b_lab = 0.0f32;
221
222 for pixel in rgb.chunks_exact(3) {
223 let color = ColorRgb::from_u8(pixel[0], pixel[1], pixel[2]);
224 let lab = color.to_lab();
225 sum_l += lab.l;
226 sum_a += lab.a;
227 sum_b_lab += lab.b;
228 }
229
230 let mean_lab_actual = ColorLab::new(sum_l / n, sum_a / n, sum_b_lab / n);
231
232 let mut var_l = 0.0f32;
233 let mut var_a = 0.0f32;
234 let mut var_b_lab = 0.0f32;
235
236 for pixel in rgb.chunks_exact(3) {
237 let color = ColorRgb::from_u8(pixel[0], pixel[1], pixel[2]);
238 let lab = color.to_lab();
239 let dl = lab.l - mean_lab_actual.l;
240 let da = lab.a - mean_lab_actual.a;
241 let db = lab.b - mean_lab_actual.b;
242
243 var_l += dl * dl;
244 var_a += da * da;
245 var_b_lab += db * db;
246 }
247
248 let std_dev_lab = ColorLab::new(
249 (var_l / n).sqrt(),
250 (var_a / n).sqrt(),
251 (var_b_lab / n).sqrt(),
252 );
253
254 Self {
255 mean,
256 std_dev,
257 mean_lab: mean_lab_actual,
258 std_dev_lab,
259 }
260 }
261}
262
263pub struct ColorTransfer;
265
266impl ColorTransfer {
267 pub fn transfer(
272 source_rgb: &[u8],
273 target_rgb: &[u8],
274 width: usize,
275 height: usize,
276 ) -> AlignResult<Vec<u8>> {
277 let expected_size = width * height * 3;
278 if source_rgb.len() != expected_size || target_rgb.len() != expected_size {
279 return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
280 }
281
282 let source_stats = ColorStatistics::from_image(source_rgb, width, height);
283 let target_stats = ColorStatistics::from_image(target_rgb, width, height);
284
285 let mut output = vec![0u8; expected_size];
286
287 for (i, pixel) in target_rgb.chunks_exact(3).enumerate() {
288 let color = ColorRgb::from_u8(pixel[0], pixel[1], pixel[2]);
289 let lab = color.to_lab();
290
291 let l = (lab.l - target_stats.mean_lab.l)
293 * (source_stats.std_dev_lab.l / target_stats.std_dev_lab.l.max(1e-6))
294 + source_stats.mean_lab.l;
295 let a = (lab.a - target_stats.mean_lab.a)
296 * (source_stats.std_dev_lab.a / target_stats.std_dev_lab.a.max(1e-6))
297 + source_stats.mean_lab.a;
298 let b = (lab.b - target_stats.mean_lab.b)
299 * (source_stats.std_dev_lab.b / target_stats.std_dev_lab.b.max(1e-6))
300 + source_stats.mean_lab.b;
301
302 let transferred_lab = ColorLab::new(l, a, b);
303 let transferred_rgb = transferred_lab.to_rgb();
304 let (r, g, b_val) = transferred_rgb.to_u8();
305
306 output[i * 3] = r;
307 output[i * 3 + 1] = g;
308 output[i * 3 + 2] = b_val;
309 }
310
311 Ok(output)
312 }
313}
314
315pub struct HistogramMatcher;
317
318impl HistogramMatcher {
319 pub fn match_histogram(
324 source: &[u8],
325 target: &[u8],
326 width: usize,
327 height: usize,
328 ) -> AlignResult<Vec<u8>> {
329 let expected_size = width * height * 3;
330 if source.len() != expected_size || target.len() != expected_size {
331 return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
332 }
333
334 let mut output = vec![0u8; expected_size];
335
336 for channel in 0..3 {
338 let source_channel: Vec<u8> = source.iter().skip(channel).step_by(3).copied().collect();
339 let target_channel: Vec<u8> = target.iter().skip(channel).step_by(3).copied().collect();
340
341 let matched = Self::match_channel(&source_channel, &target_channel);
342
343 for (i, &value) in matched.iter().enumerate() {
344 output[i * 3 + channel] = value;
345 }
346 }
347
348 Ok(output)
349 }
350
351 fn match_channel(source: &[u8], target: &[u8]) -> Vec<u8> {
353 let source_hist = Self::compute_histogram(source);
355 let target_hist = Self::compute_histogram(target);
356
357 let source_cdf = Self::compute_cdf(&source_hist);
359 let target_cdf = Self::compute_cdf(&target_hist);
360
361 let lut = Self::build_lut(&source_cdf, &target_cdf);
363
364 target.iter().map(|&v| lut[v as usize]).collect()
366 }
367
368 fn compute_histogram(data: &[u8]) -> [u32; 256] {
370 let mut hist = [0u32; 256];
371 for &value in data {
372 hist[value as usize] += 1;
373 }
374 hist
375 }
376
377 fn compute_cdf(hist: &[u32; 256]) -> [f32; 256] {
379 let mut cdf = [0.0f32; 256];
380 let total: u32 = hist.iter().sum();
381
382 if total == 0 {
383 return cdf;
384 }
385
386 let mut cumsum = 0u32;
387 for (i, &count) in hist.iter().enumerate() {
388 cumsum += count;
389 cdf[i] = cumsum as f32 / total as f32;
390 }
391
392 cdf
393 }
394
395 fn build_lut(source_cdf: &[f32; 256], target_cdf: &[f32; 256]) -> [u8; 256] {
397 let mut lut = [0u8; 256];
398
399 for (target_val, &target_prob) in target_cdf.iter().enumerate() {
400 let mut best_idx = 0;
402 let mut best_diff = f32::INFINITY;
403
404 for (source_val, &source_prob) in source_cdf.iter().enumerate() {
405 let diff = (source_prob - target_prob).abs();
406 if diff < best_diff {
407 best_diff = diff;
408 best_idx = source_val;
409 }
410 }
411
412 lut[target_val] = best_idx as u8;
413 }
414
415 lut
416 }
417}
418
419pub struct WhiteBalanceEstimator;
421
422impl WhiteBalanceEstimator {
423 #[must_use]
425 pub fn estimate_gray_world(rgb: &[u8], width: usize, height: usize) -> ColorRgb {
426 let stats = ColorStatistics::from_image(rgb, width, height);
427
428 let avg = (stats.mean.r + stats.mean.g + stats.mean.b) / 3.0;
430
431 ColorRgb::new(
432 avg / stats.mean.r.max(1e-6),
433 avg / stats.mean.g.max(1e-6),
434 avg / stats.mean.b.max(1e-6),
435 )
436 }
437
438 #[must_use]
440 pub fn estimate_white_patch(rgb: &[u8], _width: usize, _height: usize) -> ColorRgb {
441 let mut max_r = 0u8;
442 let mut max_g = 0u8;
443 let mut max_b = 0u8;
444
445 for pixel in rgb.chunks_exact(3) {
446 max_r = max_r.max(pixel[0]);
447 max_g = max_g.max(pixel[1]);
448 max_b = max_b.max(pixel[2]);
449 }
450
451 let max_val = max_r.max(max_g).max(max_b);
452
453 ColorRgb::new(
454 f32::from(max_val) / f32::from(max_r).max(1.0),
455 f32::from(max_val) / f32::from(max_g).max(1.0),
456 f32::from(max_val) / f32::from(max_b).max(1.0),
457 )
458 }
459
460 #[must_use]
462 pub fn apply_white_balance(rgb: &[u8], gains: &ColorRgb) -> Vec<u8> {
463 rgb.chunks_exact(3)
464 .flat_map(|pixel| {
465 let r = (f32::from(pixel[0]) * gains.r).clamp(0.0, 255.0) as u8;
466 let g = (f32::from(pixel[1]) * gains.g).clamp(0.0, 255.0) as u8;
467 let b = (f32::from(pixel[2]) * gains.b).clamp(0.0, 255.0) as u8;
468 [r, g, b]
469 })
470 .collect()
471 }
472}
473
474pub struct ColorCheckerCalibrator {
476 reference: Vec<ColorRgb>,
478}
479
480impl Default for ColorCheckerCalibrator {
481 fn default() -> Self {
482 Self::new()
483 }
484}
485
486impl ColorCheckerCalibrator {
487 #[must_use]
489 pub fn new() -> Self {
490 let reference = vec![
492 ColorRgb::new(0.451, 0.315, 0.242), ColorRgb::new(0.773, 0.586, 0.502), ColorRgb::new(0.350, 0.439, 0.594), ColorRgb::new(0.329, 0.400, 0.241), ColorRgb::new(0.541, 0.548, 0.742), ColorRgb::new(0.492, 0.729, 0.636), ColorRgb::new(0.871, 0.482, 0.145), ColorRgb::new(0.299, 0.359, 0.635), ColorRgb::new(0.789, 0.347, 0.376), ColorRgb::new(0.353, 0.241, 0.410), ColorRgb::new(0.596, 0.730, 0.247), ColorRgb::new(0.914, 0.620, 0.145), ColorRgb::new(0.196, 0.263, 0.557), ColorRgb::new(0.329, 0.565, 0.298), ColorRgb::new(0.757, 0.243, 0.224), ColorRgb::new(0.918, 0.765, 0.094), ColorRgb::new(0.773, 0.310, 0.557), ColorRgb::new(0.149, 0.490, 0.624), ColorRgb::new(0.957, 0.957, 0.957), ColorRgb::new(0.788, 0.788, 0.788), ColorRgb::new(0.635, 0.635, 0.635), ColorRgb::new(0.478, 0.478, 0.478), ColorRgb::new(0.318, 0.318, 0.318), ColorRgb::new(0.200, 0.200, 0.200), ];
517
518 Self { reference }
519 }
520
521 #[allow(dead_code)]
526 pub fn compute_correction_matrix(&self, measured: &[ColorRgb]) -> AlignResult<[[f32; 3]; 3]> {
527 if measured.len() != 24 {
528 return Err(AlignError::InvalidConfig(
529 "Need exactly 24 ColorChecker measurements".to_string(),
530 ));
531 }
532
533 Ok([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
536 }
537
538 #[must_use]
540 pub fn get_reference(&self, patch_index: usize) -> Option<ColorRgb> {
541 self.reference.get(patch_index).copied()
542 }
543}
544
545pub struct GammaCorrection;
547
548impl GammaCorrection {
549 #[must_use]
551 pub fn apply(rgb: &[u8], gamma: f32) -> Vec<u8> {
552 let lut: Vec<u8> = (0..256)
553 .map(|i| {
554 let normalized = i as f32 / 255.0;
555 let corrected = normalized.powf(gamma);
556 (corrected * 255.0).clamp(0.0, 255.0) as u8
557 })
558 .collect();
559
560 rgb.iter().map(|&v| lut[v as usize]).collect()
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn test_color_rgb_creation() {
570 let color = ColorRgb::new(0.5, 0.6, 0.7);
571 assert_eq!(color.r, 0.5);
572 assert_eq!(color.g, 0.6);
573 assert_eq!(color.b, 0.7);
574 }
575
576 #[test]
577 fn test_color_rgb_u8_conversion() {
578 let color = ColorRgb::from_u8(128, 192, 255);
579 let (r, g, b) = color.to_u8();
580 assert_eq!(r, 128);
581 assert_eq!(g, 192);
582 assert_eq!(b, 255);
583 }
584
585 #[test]
586 fn test_rgb_lab_roundtrip() {
587 let rgb = ColorRgb::new(0.5, 0.6, 0.7);
588 let lab = rgb.to_lab();
589 let rgb2 = lab.to_rgb();
590
591 assert!((rgb.r - rgb2.r).abs() < 0.01);
592 assert!((rgb.g - rgb2.g).abs() < 0.01);
593 assert!((rgb.b - rgb2.b).abs() < 0.01);
594 }
595
596 #[test]
597 fn test_color_statistics() {
598 let rgb = vec![128u8; 300]; let stats = ColorStatistics::from_image(&rgb, 10, 10);
600
601 assert!((stats.mean.r - 0.5).abs() < 0.01);
602 assert!((stats.mean.g - 0.5).abs() < 0.01);
603 assert!((stats.mean.b - 0.5).abs() < 0.01);
604 }
605
606 #[test]
607 fn test_histogram_computation() {
608 let data = vec![0u8, 0, 128, 128, 255];
609 let hist = HistogramMatcher::compute_histogram(&data);
610 assert_eq!(hist[0], 2);
611 assert_eq!(hist[128], 2);
612 assert_eq!(hist[255], 1);
613 }
614
615 #[test]
616 fn test_white_balance_gray_world() {
617 let mut rgb = vec![0u8; 300];
618 for i in 0..100 {
620 rgb[i * 3] = 200;
621 rgb[i * 3 + 1] = 100;
622 rgb[i * 3 + 2] = 100;
623 }
624
625 let gains = WhiteBalanceEstimator::estimate_gray_world(&rgb, 10, 10);
626 assert!(gains.r < 1.0); assert!(gains.g > 1.0); assert!(gains.b > 1.0); }
630
631 #[test]
632 fn test_gamma_correction() {
633 let rgb = vec![128u8; 30];
634 let corrected = GammaCorrection::apply(&rgb, 2.2);
635 assert_eq!(corrected.len(), 30);
636 }
637
638 #[test]
639 fn test_colorchecker_calibrator() {
640 let calibrator = ColorCheckerCalibrator::new();
641 assert_eq!(calibrator.reference.len(), 24);
642
643 let white = calibrator.get_reference(18).expect("white should be valid");
644 assert!(white.r > 0.95);
645 assert!(white.g > 0.95);
646 assert!(white.b > 0.95);
647 }
648}