pixie_anim_lib/quant/
mod.rs1pub mod dither;
6pub mod zeng;
7
8use crate::error::Result;
9
10#[cfg(feature = "rayon")]
11use rayon::prelude::*;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Rgb {
16 pub r: u8,
18 pub g: u8,
20 pub b: u8,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum DitherType {
27 None,
29 FloydSteinberg,
31 BlueNoise,
33 Ordered,
35}
36
37pub struct Palette {
39 pub colors: Vec<Rgb>,
41}
42
43pub struct QuantizationResult {
46 pub palette: Palette,
48 pub index_mapping: Vec<u8>,
50}
51
52pub trait Quantizer {
54 fn quantize(&self, pixels: &[Rgb], max_colors: usize) -> Result<QuantizationResult>;
56}
57
58pub struct KMeansQuantizer {
60 pub max_iterations: usize,
62 pub sample_rate: usize,
64 pub dither: bool,
66}
67
68impl KMeansQuantizer {
69 pub fn new(max_iterations: usize) -> Self {
71 Self {
72 max_iterations,
73 sample_rate: 10,
74 dither: true,
75 }
76 }
77
78 fn distance_sq(c1: crate::color::Lab, c2: crate::color::Lab) -> f32 {
79 crate::color::lab_distance_sq(c1, c2)
80 }
81
82 fn initialize_centroids(
84 pixels: &[crate::color::Lab],
85 max_colors: usize,
86 ) -> Vec<crate::color::Lab> {
87 if pixels.is_empty() {
88 return Vec::new();
89 }
90
91 let mut centroids = Vec::with_capacity(max_colors);
92 centroids.push(pixels[0]);
93
94 let mut min_distances = vec![f32::MAX; pixels.len()];
95
96 while centroids.len() < max_colors {
97 let last_centroid = centroids.last().unwrap();
98 let mut total_dist: f64 = 0.0;
99
100 for (i, &pixel) in pixels.iter().enumerate() {
101 let dist = Self::distance_sq(pixel, *last_centroid);
102 if dist < min_distances[i] {
103 min_distances[i] = dist;
104 }
105 total_dist += min_distances[i] as f64;
106 }
107
108 if total_dist == 0.0 {
109 break;
110 }
111
112 let mut best_pixel_idx = 0;
113 let mut max_d = -1.0;
114 for (i, &d) in min_distances.iter().enumerate() {
115 if d > max_d {
116 max_d = d;
117 best_pixel_idx = i;
118 }
119 }
120 centroids.push(pixels[best_pixel_idx]);
121 }
122
123 centroids
124 }
125}
126
127impl Quantizer for KMeansQuantizer {
128 fn quantize(&self, pixels: &[Rgb], max_colors: usize) -> Result<QuantizationResult> {
129 if pixels.is_empty() {
130 return Ok(QuantizationResult {
131 palette: Palette { colors: Vec::new() },
132 index_mapping: Vec::new(),
133 });
134 }
135
136 let sampled_pixels: Vec<crate::color::Lab> = pixels
138 .iter()
139 .step_by(self.sample_rate)
140 .map(|p| crate::color::rgb_to_lab(p.r, p.g, p.b))
141 .collect();
142
143 let mut centroids = Self::initialize_centroids(&sampled_pixels, max_colors);
145
146 let mut assignments = vec![0usize; sampled_pixels.len()];
147
148 for _ in 0..self.max_iterations {
149 let mut changed = false;
150
151 let planar_centroids = crate::simd::PlanarLabPalette::from_lab(¢roids);
153
154 #[cfg(feature = "rayon")]
155 let new_assignments: Vec<usize> = sampled_pixels
156 .par_iter()
157 .map(|&pixel_lab| crate::simd::find_nearest_color_lab(pixel_lab, &planar_centroids))
158 .collect();
159
160 #[cfg(not(feature = "rayon"))]
161 let new_assignments: Vec<usize> = sampled_pixels
162 .iter()
163 .map(|&pixel_lab| crate::simd::find_nearest_color_lab(pixel_lab, &planar_centroids))
164 .collect();
165
166 if assignments != new_assignments {
167 assignments = new_assignments;
168 changed = true;
169 }
170
171 if !changed {
172 break;
173 }
174
175 let mut sums = vec![(0.0f32, 0.0f32, 0.0f32, 0usize); centroids.len()];
177 for (i, &pixel_lab) in sampled_pixels.iter().enumerate() {
178 let a = assignments[i];
179 sums[a].0 += pixel_lab.l;
180 sums[a].1 += pixel_lab.a;
181 sums[a].2 += pixel_lab.b;
182 sums[a].3 += 1;
183 }
184
185 for (c_idx, sum) in sums.iter().enumerate() {
186 if sum.3 > 0 {
187 centroids[c_idx] = crate::color::Lab {
188 l: sum.0 / sum.3 as f32,
189 a: sum.1 / sum.3 as f32,
190 b: sum.2 / sum.3 as f32,
191 };
192 }
193 }
194 }
195
196 let rgb_centroids: Vec<Rgb> = centroids
198 .iter()
199 .map(|&lab| {
200 let mut min_dist = f32::MAX;
201 let mut best_rgb = Rgb { r: 0, g: 0, b: 0 };
202 for &p in pixels.iter().step_by(10) {
204 let p_lab = crate::color::rgb_to_lab(p.r, p.g, p.b);
205 let dist = crate::color::lab_distance_sq(lab, p_lab);
206 if dist < min_dist {
207 min_dist = dist;
208 best_rgb = p;
209 }
210 }
211 best_rgb
212 })
213 .collect();
214
215 let (final_palette, index_mapping) = zeng::reorder_palette(&Palette {
217 colors: rgb_centroids,
218 });
219
220 Ok(QuantizationResult {
221 palette: final_palette,
222 index_mapping,
223 })
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_kmeans_basic() {
233 let pixels = vec![
234 Rgb { r: 255, g: 0, b: 0 },
235 Rgb { r: 254, g: 1, b: 0 },
236 Rgb { r: 0, g: 255, b: 0 },
237 Rgb { r: 1, g: 254, b: 0 },
238 ];
239 let mut quantizer = KMeansQuantizer::new(10);
240 quantizer.sample_rate = 1;
241 let result = quantizer.quantize(&pixels, 2).unwrap();
242 assert_eq!(result.palette.colors.len(), 2);
243 }
244}