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