Skip to main content

scirs2_vision/color/
quantization.rs

1//! Color quantization algorithms
2//!
3//! This module provides methods for reducing the number of distinct colors
4//! in an image while preserving visual quality.
5
6use crate::error::Result;
7use image::{DynamicImage, Rgb, RgbImage};
8use scirs2_core::parallel_ops::*;
9use scirs2_core::random::prelude::*;
10use scirs2_core::random::seq::SliceRandom;
11use scirs2_core::random::{Rng, RngExt};
12use std::collections::HashMap;
13
14/// K-means color quantization parameters
15#[derive(Debug, Copy, Clone)]
16pub struct KMeansParams {
17    /// Number of colors in the palette
18    pub k: usize,
19    /// Maximum iterations
20    pub max_iterations: usize,
21    /// Convergence threshold
22    pub epsilon: f32,
23    /// Initialization method
24    pub init_method: InitMethod,
25}
26
27impl Default for KMeansParams {
28    fn default() -> Self {
29        Self {
30            k: 16,
31            max_iterations: 100,
32            epsilon: 1.0,
33            init_method: InitMethod::KMeansPlusPlus,
34        }
35    }
36}
37
38/// Initialization method for k-means
39#[derive(Debug, Clone, Copy)]
40pub enum InitMethod {
41    /// Random initialization
42    Random,
43    /// K-means++ initialization
44    KMeansPlusPlus,
45    /// Use most frequent colors
46    Frequency,
47}
48
49/// Perform k-means color quantization
50///
51/// # Arguments
52///
53/// * `img` - Input image
54/// * `params` - K-means parameters
55///
56/// # Returns
57///
58/// * Result containing quantized image
59///
60/// # Example
61///
62/// ```rust
63/// use scirs2_vision::color::{kmeans_quantize, KMeansParams};
64/// use image::DynamicImage;
65///
66/// # fn main() -> scirs2_vision::error::Result<()> {
67/// let img = image::open("examples/input/input.jpg").expect("Operation failed");
68/// let quantized = kmeans_quantize(&img, &KMeansParams::default())?;
69/// # Ok(())
70/// # }
71/// ```
72#[allow(dead_code)]
73pub fn kmeans_quantize(img: &DynamicImage, params: &KMeansParams) -> Result<DynamicImage> {
74    let rgb = img.to_rgb8();
75    let (width, height) = rgb.dimensions();
76
77    // Extract color samples
78    let mut colors = Vec::new();
79    for pixel in rgb.pixels() {
80        colors.push([pixel[0] as f32, pixel[1] as f32, pixel[2] as f32]);
81    }
82
83    // Initialize cluster centers
84    let mut centers = initialize_centers(&colors, params);
85
86    // K-means iterations
87    for _iter in 0..params.max_iterations {
88        // Assign pixels to clusters
89        let assignments: Vec<usize> = colors
90            .par_iter()
91            .map(|color| {
92                let mut min_dist = f32::INFINITY;
93                let mut best_cluster = 0;
94
95                for (i, center) in centers.iter().enumerate() {
96                    let dist = color_distance(color, center);
97                    if dist < min_dist {
98                        min_dist = dist;
99                        best_cluster = i;
100                    }
101                }
102
103                best_cluster
104            })
105            .collect();
106
107        // Update centers
108        let new_centers = update_centers(&colors, &assignments, params.k);
109
110        // Check convergence
111        let mut max_change = 0.0f32;
112        for (old, new) in centers.iter().zip(new_centers.iter()) {
113            let change = color_distance(old, new);
114            if change > max_change {
115                max_change = change;
116            }
117        }
118
119        centers = new_centers;
120
121        if max_change < params.epsilon {
122            break;
123        }
124    }
125
126    // Create quantized image
127    let mut result = RgbImage::new(width, height);
128    let mut idx = 0;
129
130    for y in 0..height {
131        for x in 0..width {
132            let color = &colors[idx];
133            let mut min_dist = f32::INFINITY;
134            let mut best_center = &centers[0];
135
136            for center in &centers {
137                let dist = color_distance(color, center);
138                if dist < min_dist {
139                    min_dist = dist;
140                    best_center = center;
141                }
142            }
143
144            result.put_pixel(
145                x,
146                y,
147                Rgb([
148                    best_center[0] as u8,
149                    best_center[1] as u8,
150                    best_center[2] as u8,
151                ]),
152            );
153
154            idx += 1;
155        }
156    }
157
158    Ok(DynamicImage::ImageRgb8(result))
159}
160
161/// Initialize cluster centers
162#[allow(dead_code)]
163fn initialize_centers(colors: &[[f32; 3]], params: &KMeansParams) -> Vec<[f32; 3]> {
164    match params.init_method {
165        InitMethod::Random => initialize_random(colors, params.k),
166        InitMethod::KMeansPlusPlus => initialize_kmeans_plus_plus(colors, params.k),
167        InitMethod::Frequency => initialize_frequency(colors, params.k),
168    }
169}
170
171/// Random initialization
172#[allow(dead_code)]
173fn initialize_random(colors: &[[f32; 3]], k: usize) -> Vec<[f32; 3]> {
174    let mut rng = scirs2_core::random::rng();
175    let mut centers = Vec::new();
176    let mut indices: Vec<_> = (0..colors.len()).collect();
177    indices.shuffle(&mut rng);
178
179    centers.extend(indices.iter().take(k.min(colors.len())).map(|&i| colors[i]));
180
181    centers
182}
183
184/// K-means++ initialization
185#[allow(dead_code)]
186fn initialize_kmeans_plus_plus(colors: &[[f32; 3]], k: usize) -> Vec<[f32; 3]> {
187    let mut rng = scirs2_core::random::rng();
188    let mut centers = Vec::new();
189
190    // Choose first center randomly
191    centers.push(colors[rng.random_range(0..colors.len())]);
192
193    // Choose remaining centers
194    for _ in 1..k {
195        let mut distances = vec![0.0f32; colors.len()];
196        let mut sum = 0.0f32;
197
198        // Compute distances to nearest center
199        for (i, color) in colors.iter().enumerate() {
200            let mut min_dist = f32::INFINITY;
201            for center in &centers {
202                let dist = color_distance(color, center);
203                if dist < min_dist {
204                    min_dist = dist;
205                }
206            }
207            distances[i] = min_dist * min_dist; // Square for probability
208            sum += distances[i];
209        }
210
211        // Choose next center with probability proportional to squared distance
212        let mut threshold = rng.random::<f32>() * sum;
213        let mut chosen = 0;
214
215        for (i, &dist) in distances.iter().enumerate() {
216            threshold -= dist;
217            if threshold <= 0.0 {
218                chosen = i;
219                break;
220            }
221        }
222
223        centers.push(colors[chosen]);
224    }
225
226    centers
227}
228
229/// Frequency-based initialization
230#[allow(dead_code)]
231fn initialize_frequency(colors: &[[f32; 3]], k: usize) -> Vec<[f32; 3]> {
232    // Count color frequencies
233    let mut color_counts = HashMap::new();
234
235    for color in colors {
236        let key = (color[0] as u8, color[1] as u8, color[2] as u8);
237        *color_counts.entry(key).or_insert(0) += 1;
238    }
239
240    // Sort by frequency
241    let mut sorted: Vec<_> = color_counts.into_iter().collect();
242    sorted.sort_by_key(|(_, count)| -count);
243
244    // Take top k colors
245    let mut centers = Vec::new();
246    for ((r, g, b), _) in sorted.iter().take(k.min(sorted.len())) {
247        centers.push([*r as f32, *g as f32, *b as f32]);
248    }
249
250    // Fill remaining with random if needed
251    let mut rng = scirs2_core::random::rng();
252    while centers.len() < k {
253        centers.push(colors[rng.random_range(0..colors.len())]);
254    }
255
256    centers
257}
258
259/// Update cluster centers
260#[allow(dead_code)]
261fn update_centers(colors: &[[f32; 3]], assignments: &[usize], k: usize) -> Vec<[f32; 3]> {
262    let mut new_centers = vec![[0.0, 0.0, 0.0]; k];
263    let mut counts = vec![0; k];
264
265    // Accumulate colors
266    for (color, &cluster) in colors.iter().zip(assignments.iter()) {
267        new_centers[cluster][0] += color[0];
268        new_centers[cluster][1] += color[1];
269        new_centers[cluster][2] += color[2];
270        counts[cluster] += 1;
271    }
272
273    // Compute means
274    new_centers
275        .iter_mut()
276        .zip(counts.iter())
277        .filter(|(_, &count)| count > 0)
278        .for_each(|(center, &count)| {
279            let count_f32 = count as f32;
280            center[0] /= count_f32;
281            center[1] /= count_f32;
282            center[2] /= count_f32;
283        });
284
285    new_centers
286}
287
288/// Compute squared Euclidean distance between colors
289#[allow(dead_code)]
290fn color_distance(a: &[f32; 3], b: &[f32; 3]) -> f32 {
291    let dr = a[0] - b[0];
292    let dg = a[1] - b[1];
293    let db = a[2] - b[2];
294    dr * dr + dg * dg + db * db
295}
296
297/// Median cut color quantization
298///
299/// # Arguments
300///
301/// * `img` - Input image
302/// * `ncolors` - Number of colors in palette
303///
304/// # Returns
305///
306/// * Result containing quantized image
307#[allow(dead_code)]
308pub fn median_cut_quantize(img: &DynamicImage, ncolors: usize) -> Result<DynamicImage> {
309    let rgb = img.to_rgb8();
310    let (width, height) = rgb.dimensions();
311
312    // Extract colors
313    let mut colors = Vec::new();
314    for pixel in rgb.pixels() {
315        colors.push([pixel[0], pixel[1], pixel[2]]);
316    }
317
318    // Build initial box
319    let mut boxes = vec![ColorBox::new(&colors)];
320
321    // Recursively split boxes
322    while boxes.len() < ncolors && boxes.iter().any(|b| b.can_split()) {
323        // Find box with largest volume
324        let mut max_volume = 0;
325        let mut split_idx = 0;
326
327        for (i, box_) in boxes.iter().enumerate() {
328            if box_.can_split() && box_.volume() > max_volume {
329                max_volume = box_.volume();
330                split_idx = i;
331            }
332        }
333
334        // Split the box
335        let box_to_split = boxes.remove(split_idx);
336        let (box1, box2) = box_to_split.split();
337        boxes.push(box1);
338        boxes.push(box2);
339    }
340
341    // Get palette colors (average of each box)
342    let palette: Vec<[u8; 3]> = boxes.iter().map(|b| b.average()).collect();
343
344    // Create quantized image
345    let mut result = RgbImage::new(width, height);
346
347    for (pixel, result_pixel) in rgb.pixels().zip(result.pixels_mut()) {
348        let color = [pixel[0], pixel[1], pixel[2]];
349        let mut min_dist = u32::MAX;
350        let mut best_color = palette[0];
351
352        for &palette_color in &palette {
353            let dist = color_distance_u8(&color, &palette_color);
354            if dist < min_dist {
355                min_dist = dist;
356                best_color = palette_color;
357            }
358        }
359
360        *result_pixel = Rgb(best_color);
361    }
362
363    Ok(DynamicImage::ImageRgb8(result))
364}
365
366/// Color box for median cut algorithm
367struct ColorBox {
368    colors: Vec<[u8; 3]>,
369    min: [u8; 3],
370    max: [u8; 3],
371}
372
373impl ColorBox {
374    fn new(colors: &[[u8; 3]]) -> Self {
375        let mut min = [255u8; 3];
376        let mut max = [0u8; 3];
377
378        for color in colors {
379            for i in 0..3 {
380                min[i] = min[i].min(color[i]);
381                max[i] = max[i].max(color[i]);
382            }
383        }
384
385        Self {
386            colors: colors.to_vec(),
387            min,
388            max,
389        }
390    }
391
392    fn can_split(&self) -> bool {
393        self.colors.len() > 1
394    }
395
396    fn volume(&self) -> u32 {
397        let r = (self.max[0] - self.min[0]) as u32;
398        let g = (self.max[1] - self.min[1]) as u32;
399        let b = (self.max[2] - self.min[2]) as u32;
400        r * g * b
401    }
402
403    fn split(mut self) -> (ColorBox, ColorBox) {
404        // Find longest axis
405        let r_range = self.max[0] - self.min[0];
406        let g_range = self.max[1] - self.min[1];
407        let b_range = self.max[2] - self.min[2];
408
409        let axis = if r_range >= g_range && r_range >= b_range {
410            0
411        } else if g_range >= b_range {
412            1
413        } else {
414            2
415        };
416
417        // Sort by the longest axis
418        self.colors.sort_by_key(|c| c[axis]);
419
420        // Split at median
421        let mid = self.colors.len() / 2;
422        let colors2 = self.colors.split_off(mid);
423
424        (ColorBox::new(&self.colors), ColorBox::new(&colors2))
425    }
426
427    fn average(&self) -> [u8; 3] {
428        let mut sum = [0u32; 3];
429
430        for color in &self.colors {
431            sum[0] += color[0] as u32;
432            sum[1] += color[1] as u32;
433            sum[2] += color[2] as u32;
434        }
435
436        let n = self.colors.len() as u32;
437        [(sum[0] / n) as u8, (sum[1] / n) as u8, (sum[2] / n) as u8]
438    }
439}
440
441/// Compute squared distance between u8 colors
442#[allow(dead_code)]
443fn color_distance_u8(a: &[u8; 3], b: &[u8; 3]) -> u32 {
444    let dr = a[0] as i32 - b[0] as i32;
445    let dg = a[1] as i32 - b[1] as i32;
446    let db = a[2] as i32 - b[2] as i32;
447    (dr * dr + dg * dg + db * db) as u32
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_kmeans_quantize() {
456        let img = DynamicImage::new_rgb8(20, 20);
457        let params = KMeansParams {
458            k: 4,
459            max_iterations: 10,
460            ..Default::default()
461        };
462
463        let result = kmeans_quantize(&img, &params);
464        assert!(result.is_ok());
465
466        let quantized = result.expect("Operation failed");
467        assert_eq!(quantized.width(), 20);
468        assert_eq!(quantized.height(), 20);
469    }
470
471    #[test]
472    fn test_median_cut() {
473        let img = DynamicImage::new_rgb8(20, 20);
474        let result = median_cut_quantize(&img, 8);
475        assert!(result.is_ok());
476    }
477
478    #[test]
479    fn test_color_box() {
480        let colors = vec![[0, 0, 0], [255, 255, 255], [128, 128, 128]];
481
482        let box_ = ColorBox::new(&colors);
483        assert_eq!(box_.min, [0, 0, 0]);
484        assert_eq!(box_.max, [255, 255, 255]);
485        assert!(box_.can_split());
486    }
487}