1pub mod octree_quantization;
7pub mod quantization;
8
9use crate::error::Result;
10use image::{DynamicImage, ImageBuffer, Rgb};
11pub use octree_quantization::{adaptive_octree_quantize, extract_palette, octree_quantize};
14pub use quantization::{kmeans_quantize, median_cut_quantize, InitMethod, KMeansParams};
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum ColorSpace {
19 RGB,
21 HSV,
23 HSL,
25 LAB,
27 XYZ,
29 YCbCr,
31 Gray,
33}
34
35#[allow(dead_code)]
45pub fn rgb_to_hsv(img: &DynamicImage) -> Result<DynamicImage> {
46 let rgb_img = img.to_rgb8();
48 let (width, height) = rgb_img.dimensions();
49
50 let mut hsv_img = ImageBuffer::new(width, height);
52
53 for y in 0..height {
54 for x in 0..width {
55 let rgb = rgb_img.get_pixel(x, y);
56 let r = rgb[0] as f32 / 255.0;
57 let g = rgb[1] as f32 / 255.0;
58 let b = rgb[2] as f32 / 255.0;
59
60 let max = r.max(g).max(b);
61 let min = r.min(g).min(b);
62 let delta = max - min;
63
64 let h = if delta == 0.0 {
66 0.0
67 } else if max == r {
68 60.0 * (((g - b) / delta) % 6.0)
69 } else if max == g {
70 60.0 * (((b - r) / delta) + 2.0)
71 } else {
72 60.0 * (((r - g) / delta) + 4.0)
73 };
74
75 let h = if h < 0.0 { h + 360.0 } else { h };
77
78 let s = if max == 0.0 { 0.0 } else { delta / max };
80
81 let v = max;
83
84 hsv_img.put_pixel(
89 x,
90 y,
91 Rgb([
92 (h / 360.0 * 255.0) as u8,
93 (s * 255.0) as u8,
94 (v * 255.0) as u8,
95 ]),
96 );
97 }
98 }
99
100 Ok(DynamicImage::ImageRgb8(hsv_img))
101}
102
103#[allow(dead_code)]
113pub fn hsv_to_rgb(_hsvimg: &DynamicImage) -> Result<DynamicImage> {
114 let hsv = _hsvimg.to_rgb8();
115 let (width, height) = hsv.dimensions();
116
117 let mut rgb_img = ImageBuffer::new(width, height);
118
119 for y in 0..height {
120 for x in 0..width {
121 let hsv_pixel = hsv.get_pixel(x, y);
122
123 let h = hsv_pixel[0] as f32 / 255.0 * 360.0;
125 let s = hsv_pixel[1] as f32 / 255.0;
126 let v = hsv_pixel[2] as f32 / 255.0;
127
128 let c = v * s;
130 let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
131 let m = v - c;
132
133 let (r1, g1, b1) = if h < 60.0 {
134 (c, x, 0.0)
135 } else if h < 120.0 {
136 (x, c, 0.0)
137 } else if h < 180.0 {
138 (0.0, c, x)
139 } else if h < 240.0 {
140 (0.0, x, c)
141 } else if h < 300.0 {
142 (x, 0.0, c)
143 } else {
144 (c, 0.0, x)
145 };
146
147 let r = ((r1 + m) * 255.0) as u8;
148 let g = ((g1 + m) * 255.0) as u8;
149 let b = ((b1 + m) * 255.0) as u8;
150
151 #[allow(clippy::unnecessary_cast)]
152 rgb_img.put_pixel(x as u32, y as u32, Rgb([r, g, b]));
153 }
154 }
155
156 Ok(DynamicImage::ImageRgb8(rgb_img))
157}
158
159#[allow(dead_code)]
170pub fn rgb_to_grayscale(img: &DynamicImage, weights: Option<[f32; 3]>) -> Result<DynamicImage> {
171 let weights = weights.unwrap_or([0.2989, 0.5870, 0.1140]);
173
174 let rgb_img = img.to_rgb8();
176 let (width, height) = rgb_img.dimensions();
177
178 let mut gray_img = ImageBuffer::new(width, height);
180
181 for y in 0..height {
182 for x in 0..width {
183 let rgb = rgb_img.get_pixel(x, y);
184
185 let gray_value = (weights[0] * rgb[0] as f32
187 + weights[1] * rgb[1] as f32
188 + weights[2] * rgb[2] as f32)
189 .clamp(0.0, 255.0) as u8;
190
191 gray_img.put_pixel(x, y, image::Luma([gray_value]));
192 }
193 }
194
195 Ok(DynamicImage::ImageLuma8(gray_img))
196}
197
198#[allow(dead_code)]
208pub fn rgb_to_lab(img: &DynamicImage) -> Result<DynamicImage> {
209 let rgb_img = img.to_rgb8();
210 let (width, height) = rgb_img.dimensions();
211
212 let mut lab_img = ImageBuffer::new(width, height);
213
214 for y in 0..height {
215 for x in 0..width {
216 let rgb = rgb_img.get_pixel(x, y);
217
218 let r = rgb[0] as f32 / 255.0;
220 let g = rgb[1] as f32 / 255.0;
221 let b = rgb[2] as f32 / 255.0;
222
223 let r_lin = if r > 0.04045 {
225 ((r + 0.055) / 1.055).powf(2.4)
226 } else {
227 r / 12.92
228 };
229 let g_lin = if g > 0.04045 {
230 ((g + 0.055) / 1.055).powf(2.4)
231 } else {
232 g / 12.92
233 };
234 let b_lin = if b > 0.04045 {
235 ((b + 0.055) / 1.055).powf(2.4)
236 } else {
237 b / 12.92
238 };
239
240 let x = r_lin * 0.4124564 + g_lin * 0.3575761 + b_lin * 0.1804375;
242 let y = r_lin * 0.2126729 + g_lin * 0.7151522 + b_lin * 0.0721750;
243 let z = r_lin * 0.0193339 + g_lin * 0.119192 + b_lin * 0.9503041;
244
245 let x_n = 0.95047;
248 let y_n = 1.0;
249 let z_n = 1.08883;
250
251 let x_r = x / x_n;
253 let y_r = y / y_n;
254 let z_r = z / z_n;
255
256 let f = |t: f32| -> f32 {
258 if t > 0.008856 {
259 t.powf(1.0 / 3.0)
260 } else {
261 (7.787 * t) + (16.0 / 116.0)
262 }
263 };
264
265 let fx = f(x_r);
266 let fy = f(y_r);
267 let fz = f(z_r);
268
269 let l = (116.0 * fy) - 16.0;
271 let a = 500.0 * (fx - fy);
272 let b_val = 200.0 * (fy - fz);
273
274 let l_scaled = (l * 2.55).clamp(0.0, 255.0) as u8;
279 let a_scaled = ((a + 128.0).clamp(0.0, 255.0)) as u8;
280 let b_scaled = ((b_val + 128.0).clamp(0.0, 255.0)) as u8;
281
282 lab_img.put_pixel(x as u32, y as u32, Rgb([l_scaled, a_scaled, b_scaled]));
283 }
284 }
285
286 Ok(DynamicImage::ImageRgb8(lab_img))
287}
288
289#[allow(dead_code)]
299pub fn lab_to_rgb(_labimg: &DynamicImage) -> Result<DynamicImage> {
300 let lab = _labimg.to_rgb8();
301 let (width, height) = lab.dimensions();
302
303 let mut rgb_img = ImageBuffer::new(width, height);
304
305 for y in 0..height {
306 for x in 0..width {
307 let lab_pixel = lab.get_pixel(x, y);
308
309 let l = lab_pixel[0] as f32 / 2.55; let a = lab_pixel[1] as f32 - 128.0; let b = lab_pixel[2] as f32 - 128.0; let fy = (l + 16.0) / 116.0;
316 let fx = a / 500.0 + fy;
317 let fz = fy - b / 200.0;
318
319 let x_n = 0.95047;
321 let y_n = 1.0;
322 let z_n = 1.08883;
323
324 let f = |t: f32| -> f32 {
326 if t > 0.206893 {
327 t.powi(3)
328 } else {
329 (t - 16.0 / 116.0) / 7.787
330 }
331 };
332
333 let x = x_n * f(fx);
334 let y = y_n * f(fy);
335 let z = z_n * f(fz);
336
337 let r_lin = x * 3.2404542 - y * 1.5371385 - z * 0.4985314;
339 let g_lin = -x * 0.969266 + y * 1.8760108 + z * 0.0415560;
340 let b_lin = x * 0.0556434 - y * 0.2040259 + z * 1.0572252;
341
342 let r = if r_lin > 0.0031308 {
344 1.055 * r_lin.powf(1.0 / 2.4) - 0.055
345 } else {
346 12.92 * r_lin
347 };
348
349 let g = if g_lin > 0.0031308 {
350 1.055 * g_lin.powf(1.0 / 2.4) - 0.055
351 } else {
352 12.92 * g_lin
353 };
354
355 let b = if b_lin > 0.0031308 {
356 1.055 * b_lin.powf(1.0 / 2.4) - 0.055
357 } else {
358 12.92 * b_lin
359 };
360
361 let r = (r * 255.0).clamp(0.0, 255.0) as u8;
363 let g = (g * 255.0).clamp(0.0, 255.0) as u8;
364 let b = (b * 255.0).clamp(0.0, 255.0) as u8;
365
366 #[allow(clippy::unnecessary_cast)]
367 rgb_img.put_pixel(x as u32, y as u32, Rgb([r, g, b]));
368 }
369 }
370
371 Ok(DynamicImage::ImageRgb8(rgb_img))
372}
373
374#[allow(dead_code)]
384pub fn split_channels(img: &DynamicImage) -> Result<(DynamicImage, DynamicImage, DynamicImage)> {
385 let rgb_img = img.to_rgb8();
386 let (width, height) = rgb_img.dimensions();
387
388 let mut r_channel = ImageBuffer::new(width, height);
389 let mut g_channel = ImageBuffer::new(width, height);
390 let mut b_channel = ImageBuffer::new(width, height);
391
392 for y in 0..height {
393 for x in 0..width {
394 let rgb = rgb_img.get_pixel(x, y);
395
396 r_channel.put_pixel(x, y, image::Luma([rgb[0]]));
397 g_channel.put_pixel(x, y, image::Luma([rgb[1]]));
398 b_channel.put_pixel(x, y, image::Luma([rgb[2]]));
399 }
400 }
401
402 Ok((
403 DynamicImage::ImageLuma8(r_channel),
404 DynamicImage::ImageLuma8(g_channel),
405 DynamicImage::ImageLuma8(b_channel),
406 ))
407}
408
409#[allow(dead_code)]
421pub fn merge_channels(
422 r_channel: &DynamicImage,
423 g_channel: &DynamicImage,
424 b_channel: &DynamicImage,
425) -> Result<DynamicImage> {
426 let r_img = r_channel.to_luma8();
427 let g_img = g_channel.to_luma8();
428 let b_img = b_channel.to_luma8();
429
430 let (width, height) = r_img.dimensions();
431
432 if g_img.dimensions() != (width, height) || b_img.dimensions() != (width, height) {
434 return Err(crate::error::VisionError::InvalidParameter(
435 "Channel dimensions do not match".to_string(),
436 ));
437 }
438
439 let mut rgb_img = ImageBuffer::new(width, height);
440
441 for y in 0..height {
442 for x in 0..width {
443 let r = r_img.get_pixel(x, y)[0];
444 let g = g_img.get_pixel(x, y)[0];
445 let b = b_img.get_pixel(x, y)[0];
446
447 #[allow(clippy::unnecessary_cast)]
448 rgb_img.put_pixel(x as u32, y as u32, Rgb([r, g, b]));
449 }
450 }
451
452 Ok(DynamicImage::ImageRgb8(rgb_img))
453}
454
455#[allow(dead_code)]
466pub fn rgb_to_hsl(img: &DynamicImage) -> Result<DynamicImage> {
467 let rgb_img = img.to_rgb8();
468 let (width, height) = rgb_img.dimensions();
469 let mut hsl_img = ImageBuffer::new(width, height);
470
471 for y in 0..height {
472 for x in 0..width {
473 let px = rgb_img.get_pixel(x, y);
474 let r = px[0] as f32 / 255.0;
475 let g = px[1] as f32 / 255.0;
476 let b = px[2] as f32 / 255.0;
477
478 let max_c = r.max(g).max(b);
479 let min_c = r.min(g).min(b);
480 let delta = max_c - min_c;
481 let l = (max_c + min_c) / 2.0;
482
483 let s = if delta < 1e-6 {
484 0.0
485 } else {
486 delta / (1.0 - (2.0 * l - 1.0).abs())
487 };
488
489 let h = if delta < 1e-6 {
490 0.0
491 } else if (max_c - r).abs() < 1e-6 {
492 let mut hh = 60.0 * ((g - b) / delta);
493 if hh < 0.0 {
494 hh += 360.0;
495 }
496 hh
497 } else if (max_c - g).abs() < 1e-6 {
498 60.0 * ((b - r) / delta + 2.0)
499 } else {
500 60.0 * ((r - g) / delta + 4.0)
501 };
502
503 hsl_img.put_pixel(
504 x,
505 y,
506 Rgb([
507 (h / 360.0 * 255.0).clamp(0.0, 255.0) as u8,
508 (s * 255.0).clamp(0.0, 255.0) as u8,
509 (l * 255.0).clamp(0.0, 255.0) as u8,
510 ]),
511 );
512 }
513 }
514
515 Ok(DynamicImage::ImageRgb8(hsl_img))
516}
517
518#[allow(dead_code)]
522pub fn hsl_to_rgb(hsl_img: &DynamicImage) -> Result<DynamicImage> {
523 let hsl = hsl_img.to_rgb8();
524 let (width, height) = hsl.dimensions();
525 let mut rgb_out = ImageBuffer::new(width, height);
526
527 for y in 0..height {
528 for x in 0..width {
529 let px = hsl.get_pixel(x, y);
530 let h = px[0] as f32 / 255.0 * 360.0;
531 let s = px[1] as f32 / 255.0;
532 let l = px[2] as f32 / 255.0;
533
534 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
535 let hp = h / 60.0;
536 let x_val = c * (1.0 - (hp % 2.0 - 1.0).abs());
537 let m = l - c / 2.0;
538
539 let (r1, g1, b1) = if hp < 1.0 {
540 (c, x_val, 0.0)
541 } else if hp < 2.0 {
542 (x_val, c, 0.0)
543 } else if hp < 3.0 {
544 (0.0, c, x_val)
545 } else if hp < 4.0 {
546 (0.0, x_val, c)
547 } else if hp < 5.0 {
548 (x_val, 0.0, c)
549 } else {
550 (c, 0.0, x_val)
551 };
552
553 rgb_out.put_pixel(
554 x,
555 y,
556 Rgb([
557 ((r1 + m) * 255.0).clamp(0.0, 255.0) as u8,
558 ((g1 + m) * 255.0).clamp(0.0, 255.0) as u8,
559 ((b1 + m) * 255.0).clamp(0.0, 255.0) as u8,
560 ]),
561 );
562 }
563 }
564
565 Ok(DynamicImage::ImageRgb8(rgb_out))
566}
567
568#[allow(dead_code)]
574pub fn rgb_to_ycbcr(img: &DynamicImage) -> Result<DynamicImage> {
575 let rgb = img.to_rgb8();
576 let (width, height) = rgb.dimensions();
577 let mut out = ImageBuffer::new(width, height);
578
579 for y in 0..height {
580 for x in 0..width {
581 let px = rgb.get_pixel(x, y);
582 let r = px[0] as f32;
583 let g = px[1] as f32;
584 let b = px[2] as f32;
585
586 let y_val = 0.299 * r + 0.587 * g + 0.114 * b;
587 let cb = 128.0 + (-0.168736 * r - 0.331264 * g + 0.5 * b);
588 let cr = 128.0 + (0.5 * r - 0.418688 * g - 0.081312 * b);
589
590 out.put_pixel(
591 x,
592 y,
593 Rgb([
594 y_val.clamp(0.0, 255.0) as u8,
595 cb.clamp(0.0, 255.0) as u8,
596 cr.clamp(0.0, 255.0) as u8,
597 ]),
598 );
599 }
600 }
601
602 Ok(DynamicImage::ImageRgb8(out))
603}
604
605#[allow(dead_code)]
607pub fn ycbcr_to_rgb(ycbcr_img: &DynamicImage) -> Result<DynamicImage> {
608 let ycbcr = ycbcr_img.to_rgb8();
609 let (width, height) = ycbcr.dimensions();
610 let mut out = ImageBuffer::new(width, height);
611
612 for y in 0..height {
613 for x in 0..width {
614 let px = ycbcr.get_pixel(x, y);
615 let y_val = px[0] as f32;
616 let cb = px[1] as f32 - 128.0;
617 let cr = px[2] as f32 - 128.0;
618
619 let r = y_val + 1.402 * cr;
620 let g = y_val - 0.344136 * cb - 0.714136 * cr;
621 let b = y_val + 1.772 * cb;
622
623 out.put_pixel(
624 x,
625 y,
626 Rgb([
627 r.clamp(0.0, 255.0) as u8,
628 g.clamp(0.0, 255.0) as u8,
629 b.clamp(0.0, 255.0) as u8,
630 ]),
631 );
632 }
633 }
634
635 Ok(DynamicImage::ImageRgb8(out))
636}
637
638#[inline]
644fn srgb_to_linear(v: f32) -> f32 {
645 if v > 0.04045 {
646 ((v + 0.055) / 1.055).powf(2.4)
647 } else {
648 v / 12.92
649 }
650}
651
652#[inline]
654fn linear_to_srgb(v: f32) -> f32 {
655 if v > 0.0031308 {
656 1.055 * v.powf(1.0 / 2.4) - 0.055
657 } else {
658 12.92 * v
659 }
660}
661
662#[allow(dead_code)]
664pub fn rgb_to_xyz(img: &DynamicImage) -> Result<DynamicImage> {
665 let rgb = img.to_rgb8();
666 let (width, height) = rgb.dimensions();
667 let mut out = ImageBuffer::new(width, height);
668
669 for y in 0..height {
670 for x in 0..width {
671 let px = rgb.get_pixel(x, y);
672 let r = srgb_to_linear(px[0] as f32 / 255.0);
673 let g = srgb_to_linear(px[1] as f32 / 255.0);
674 let b = srgb_to_linear(px[2] as f32 / 255.0);
675
676 let x_val = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
677 let y_val = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
678 let z_val = r * 0.0193339 + g * 0.119_192 + b * 0.9503041;
679
680 out.put_pixel(
681 x,
682 y,
683 Rgb([
684 (x_val * 255.0).clamp(0.0, 255.0) as u8,
685 (y_val * 255.0).clamp(0.0, 255.0) as u8,
686 (z_val * 255.0).clamp(0.0, 255.0) as u8,
687 ]),
688 );
689 }
690 }
691
692 Ok(DynamicImage::ImageRgb8(out))
693}
694
695#[allow(dead_code)]
697pub fn xyz_to_rgb(xyz_img: &DynamicImage) -> Result<DynamicImage> {
698 let xyz = xyz_img.to_rgb8();
699 let (width, height) = xyz.dimensions();
700 let mut out = ImageBuffer::new(width, height);
701
702 for y in 0..height {
703 for x in 0..width {
704 let px = xyz.get_pixel(x, y);
705 let x_val = px[0] as f32 / 255.0;
706 let y_val = px[1] as f32 / 255.0;
707 let z_val = px[2] as f32 / 255.0;
708
709 let r_lin = x_val * 3.2404542 - y_val * 1.5371385 - z_val * 0.4985314;
710 let g_lin = -x_val * 0.969_266 + y_val * 1.8760108 + z_val * 0.0415560;
711 let b_lin = x_val * 0.0556434 - y_val * 0.2040259 + z_val * 1.0572252;
712
713 let r = linear_to_srgb(r_lin);
714 let g = linear_to_srgb(g_lin);
715 let b = linear_to_srgb(b_lin);
716
717 out.put_pixel(
718 x,
719 y,
720 Rgb([
721 (r * 255.0).clamp(0.0, 255.0) as u8,
722 (g * 255.0).clamp(0.0, 255.0) as u8,
723 (b * 255.0).clamp(0.0, 255.0) as u8,
724 ]),
725 );
726 }
727 }
728
729 Ok(DynamicImage::ImageRgb8(out))
730}
731
732#[derive(Debug, Clone)]
738pub struct DominantColor {
739 pub rgb: [u8; 3],
741 pub weight: f32,
743}
744
745pub fn extract_dominant_colors(
756 img: &DynamicImage,
757 num_colors: usize,
758) -> Result<Vec<DominantColor>> {
759 if num_colors == 0 {
760 return Err(crate::error::VisionError::InvalidParameter(
761 "Number of colors must be positive".to_string(),
762 ));
763 }
764
765 let rgb = img.to_rgb8();
766 let total_pixels = (rgb.width() as usize) * (rgb.height() as usize);
767 if total_pixels == 0 {
768 return Ok(Vec::new());
769 }
770
771 let mut pixels: Vec<[u8; 3]> = Vec::with_capacity(total_pixels);
772 for pixel in rgb.pixels() {
773 pixels.push([pixel[0], pixel[1], pixel[2]]);
774 }
775
776 let mut boxes: Vec<Vec<[u8; 3]>> = vec![pixels];
777
778 while boxes.len() < num_colors {
779 let mut best_idx = 0;
780 let mut best_range = 0u16;
781 for (i, bx) in boxes.iter().enumerate() {
782 if bx.len() < 2 {
783 continue;
784 }
785 let range = box_max_range(bx);
786 if range > best_range {
787 best_range = range;
788 best_idx = i;
789 }
790 }
791 if best_range == 0 {
792 break;
793 }
794
795 let bx = boxes.remove(best_idx);
796 let (a, b) = split_box(bx);
797 boxes.push(a);
798 boxes.push(b);
799 }
800
801 let mut result: Vec<DominantColor> = boxes
802 .iter()
803 .filter(|bx| !bx.is_empty())
804 .map(|bx| {
805 let count = bx.len();
806 let mut r_sum = 0u64;
807 let mut g_sum = 0u64;
808 let mut b_sum = 0u64;
809 for px in bx {
810 r_sum += px[0] as u64;
811 g_sum += px[1] as u64;
812 b_sum += px[2] as u64;
813 }
814 DominantColor {
815 rgb: [
816 (r_sum / count as u64) as u8,
817 (g_sum / count as u64) as u8,
818 (b_sum / count as u64) as u8,
819 ],
820 weight: count as f32 / total_pixels as f32,
821 }
822 })
823 .collect();
824
825 result.sort_by(|a, b| {
826 b.weight
827 .partial_cmp(&a.weight)
828 .unwrap_or(std::cmp::Ordering::Equal)
829 });
830 Ok(result)
831}
832
833fn box_max_range(pixels: &[[u8; 3]]) -> u16 {
835 let mut ranges = [0u16; 3];
836 for ch in 0..3 {
837 let min_v = pixels.iter().map(|p| p[ch]).min().unwrap_or(0);
838 let max_v = pixels.iter().map(|p| p[ch]).max().unwrap_or(0);
839 ranges[ch] = (max_v as u16).saturating_sub(min_v as u16);
840 }
841 *ranges.iter().max().unwrap_or(&0)
842}
843
844fn split_box(mut pixels: Vec<[u8; 3]>) -> (Vec<[u8; 3]>, Vec<[u8; 3]>) {
846 let mut best_ch = 0usize;
847 let mut best_range = 0u16;
848 for ch in 0..3 {
849 let min_v = pixels.iter().map(|p| p[ch]).min().unwrap_or(0);
850 let max_v = pixels.iter().map(|p| p[ch]).max().unwrap_or(0);
851 let range = (max_v as u16).saturating_sub(min_v as u16);
852 if range > best_range {
853 best_range = range;
854 best_ch = ch;
855 }
856 }
857
858 pixels.sort_by_key(|p| p[best_ch]);
859 let mid = pixels.len() / 2;
860 let second = pixels.split_off(mid);
861 (pixels, second)
862}
863
864#[cfg(test)]
869mod tests {
870 use super::*;
871
872 fn make_rgb(width: u32, height: u32, r: u8, g: u8, b: u8) -> DynamicImage {
873 let mut img = image::RgbImage::new(width, height);
874 for pixel in img.pixels_mut() {
875 *pixel = Rgb([r, g, b]);
876 }
877 DynamicImage::ImageRgb8(img)
878 }
879
880 #[test]
881 fn test_rgb_to_hsl_and_back() {
882 let img = make_rgb(10, 10, 200, 100, 50);
883 let hsl = rgb_to_hsl(&img).expect("rgb_to_hsl failed");
884 let recovered = hsl_to_rgb(&hsl).expect("hsl_to_rgb failed");
885 let orig = img.to_rgb8();
886 let rec = recovered.to_rgb8();
887 let px_o = orig.get_pixel(0, 0);
888 let px_r = rec.get_pixel(0, 0);
889 for ch in 0..3 {
890 assert!(
891 (px_o[ch] as i16 - px_r[ch] as i16).unsigned_abs() <= 3,
892 "Channel {ch} mismatch: orig={}, recovered={}",
893 px_o[ch],
894 px_r[ch]
895 );
896 }
897 }
898
899 #[test]
900 fn test_rgb_to_hsl_gray() {
901 let img = make_rgb(5, 5, 128, 128, 128);
902 let hsl = rgb_to_hsl(&img).expect("rgb_to_hsl failed");
903 let px = hsl.to_rgb8().get_pixel(0, 0).0;
904 assert_eq!(px[1], 0, "Saturation should be 0 for gray");
905 }
906
907 #[test]
908 fn test_rgb_to_ycbcr_and_back() {
909 let img = make_rgb(10, 10, 180, 90, 40);
910 let ycbcr = rgb_to_ycbcr(&img).expect("rgb_to_ycbcr failed");
911 let recovered = ycbcr_to_rgb(&ycbcr).expect("ycbcr_to_rgb failed");
912 let orig = img.to_rgb8();
913 let rec = recovered.to_rgb8();
914 let px_o = orig.get_pixel(0, 0);
915 let px_r = rec.get_pixel(0, 0);
916 for ch in 0..3 {
917 assert!(
918 (px_o[ch] as i16 - px_r[ch] as i16).unsigned_abs() <= 2,
919 "Channel {ch} mismatch: orig={}, recovered={}",
920 px_o[ch],
921 px_r[ch]
922 );
923 }
924 }
925
926 #[test]
927 fn test_rgb_to_ycbcr_white() {
928 let img = make_rgb(5, 5, 255, 255, 255);
929 let ycbcr = rgb_to_ycbcr(&img).expect("rgb_to_ycbcr failed");
930 let px = ycbcr.to_rgb8().get_pixel(0, 0).0;
931 assert!(px[0] >= 250, "Y of white should be near 255, got {}", px[0]);
932 assert!(
933 (px[1] as i16 - 128).unsigned_abs() <= 2,
934 "Cb of white should be ~128, got {}",
935 px[1]
936 );
937 }
938
939 #[test]
940 fn test_rgb_to_xyz_and_back() {
941 let img = make_rgb(10, 10, 150, 100, 80);
942 let xyz = rgb_to_xyz(&img).expect("rgb_to_xyz failed");
943 let recovered = xyz_to_rgb(&xyz).expect("xyz_to_rgb failed");
944 let orig = img.to_rgb8();
945 let rec = recovered.to_rgb8();
946 let px_o = orig.get_pixel(0, 0);
947 let px_r = rec.get_pixel(0, 0);
948 for ch in 0..3 {
949 assert!(
950 (px_o[ch] as i16 - px_r[ch] as i16).unsigned_abs() <= 3,
951 "Channel {ch} mismatch: orig={}, recovered={}",
952 px_o[ch],
953 px_r[ch]
954 );
955 }
956 }
957
958 #[test]
959 fn test_rgb_to_xyz_black() {
960 let img = make_rgb(5, 5, 0, 0, 0);
961 let xyz = rgb_to_xyz(&img).expect("rgb_to_xyz failed");
962 let px = xyz.to_rgb8().get_pixel(0, 0).0;
963 assert_eq!(px[0], 0);
964 assert_eq!(px[1], 0);
965 assert_eq!(px[2], 0);
966 }
967
968 #[test]
969 fn test_extract_dominant_colors_uniform() {
970 let img = make_rgb(20, 20, 100, 50, 200);
971 let colors = extract_dominant_colors(&img, 3).expect("dominant extraction failed");
972 assert!(!colors.is_empty());
973 let dc = &colors[0];
974 assert!((dc.rgb[0] as i16 - 100).unsigned_abs() <= 1);
975 assert!((dc.rgb[1] as i16 - 50).unsigned_abs() <= 1);
976 assert!((dc.rgb[2] as i16 - 200).unsigned_abs() <= 1);
977 }
978
979 #[test]
980 fn test_extract_dominant_colors_two_regions() {
981 let mut img = image::RgbImage::new(20, 10);
982 for y in 0..10 {
983 for x in 0..10 {
984 img.put_pixel(x, y, Rgb([255, 0, 0]));
985 }
986 for x in 10..20 {
987 img.put_pixel(x, y, Rgb([0, 0, 255]));
988 }
989 }
990 let dyn_img = DynamicImage::ImageRgb8(img);
991 let colors = extract_dominant_colors(&dyn_img, 2).expect("dominant extraction failed");
992 assert_eq!(colors.len(), 2);
993 for c in &colors {
994 assert!((c.weight - 0.5).abs() < 0.05);
995 }
996 }
997
998 #[test]
999 fn test_extract_dominant_colors_invalid() {
1000 let img = make_rgb(5, 5, 100, 100, 100);
1001 assert!(extract_dominant_colors(&img, 0).is_err());
1002 }
1003
1004 #[test]
1005 fn test_rgb_to_hsv_and_back() {
1006 let img = make_rgb(10, 10, 200, 100, 50);
1007 let hsv = rgb_to_hsv(&img).expect("rgb_to_hsv failed");
1008 let recovered = hsv_to_rgb(&hsv).expect("hsv_to_rgb failed");
1009 let orig = img.to_rgb8();
1010 let rec = recovered.to_rgb8();
1011 let px_o = orig.get_pixel(0, 0);
1012 let px_r = rec.get_pixel(0, 0);
1013 for ch in 0..3 {
1014 assert!(
1015 (px_o[ch] as i16 - px_r[ch] as i16).unsigned_abs() <= 5,
1016 "Channel {ch} mismatch: orig={}, recovered={}",
1017 px_o[ch],
1018 px_r[ch]
1019 );
1020 }
1021 }
1022
1023 #[test]
1024 fn test_rgb_to_grayscale_test() {
1025 let img = make_rgb(10, 10, 100, 100, 100);
1026 let gray = rgb_to_grayscale(&img, None).expect("grayscale failed");
1027 let luma = gray.to_luma8();
1028 let val = luma.get_pixel(0, 0)[0];
1029 assert!((val as i16 - 100).unsigned_abs() <= 2);
1030 }
1031
1032 #[test]
1033 fn test_split_merge_channels_roundtrip() {
1034 let img = make_rgb(5, 5, 100, 150, 200);
1035 let (r, g, b) = split_channels(&img).expect("split failed");
1036 let merged = merge_channels(&r, &g, &b).expect("merge failed");
1037 let orig = img.to_rgb8();
1038 let rec = merged.to_rgb8();
1039 for yy in 0..5u32 {
1040 for xx in 0..5u32 {
1041 assert_eq!(orig.get_pixel(xx, yy), rec.get_pixel(xx, yy));
1042 }
1043 }
1044 }
1045}