Skip to main content

a_sixel/
dither.rs

1//! A collection of various dithering algorithms that can be used with the
2//! [`SixelEncoder`](crate::SixelEncoder) to dither the result.
3//!
4//! Most of the dithering algorithms are based on the ones described in
5//! [this article](https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html).
6//! There is also an implementation of the Bayer matrix dithering algorithm
7//! and a Sobol sequence based dithering algorithm.
8
9use dilate::DilateExpand;
10use image::Rgba;
11use image::RgbaImage;
12use kiddo::float::kdtree::KdTree;
13use palette::Lab;
14use palette::color_difference::EuclideanDistance;
15use rayon::iter::IndexedParallelIterator;
16use rayon::iter::IntoParallelIterator;
17use rayon::iter::IntoParallelRefMutIterator;
18use rayon::iter::ParallelIterator;
19use rayon::slice::ParallelSliceMut;
20use sobol_burley::sample;
21
22use crate::private;
23use crate::rgba_to_lab;
24
25/// Abstracts nearest-neighbor palette lookup, allowing different strategies
26/// (e.g. KD-tree vs. direct bucket lookup) to be used interchangeably.
27pub trait PaletteBucketer: Sync + private::Sealed {
28    /// Find the nearest palette entry for a point in Lab color space.
29    fn nearest(
30        &self,
31        point: &[f32; 3],
32    ) -> usize;
33
34    /// Find the two nearest palette entries for a point in Lab color space,
35    /// returned as `(palette_index, squared_distance)` pairs ordered nearest
36    /// first. Used by ordered-dithering algorithms that interpolate between
37    /// the two closest colors.
38    fn nearest_two(
39        &self,
40        point: Rgba<u8>,
41    ) -> [(usize, f32); 2];
42
43    /// Find the nearest palette entry for an RGB pixel. The default
44    /// implementation converts to Lab and delegates to
45    /// [`nearest`](Self::nearest), but implementations may override this
46    /// for a faster path that avoids the color-space conversion entirely.
47    fn nearest_rgb(
48        &self,
49        pixel: Rgba<u8>,
50    ) -> usize {
51        let lab = rgba_to_lab(pixel);
52        self.nearest(lab.as_ref())
53    }
54}
55
56/// A [`PaletteBucketer`] backed by a KD-tree for nearest-neighbor queries in
57/// Lab color space.
58pub struct KdTreeBucketer(KdTree<f32, usize, 3, 257, u32>);
59
60impl KdTreeBucketer {
61    pub fn new(palette: &[Lab]) -> Self {
62        let mut tree = KdTree::with_capacity(palette.len());
63        for (idx, color) in palette.iter().enumerate() {
64            tree.add(color.as_ref(), idx);
65        }
66        Self(tree)
67    }
68}
69
70impl private::Sealed for KdTreeBucketer {}
71
72impl PaletteBucketer for KdTreeBucketer {
73    fn nearest(
74        &self,
75        point: &[f32; 3],
76    ) -> usize {
77        self.0.nearest_one::<kiddo::SquaredEuclidean>(point).item
78    }
79
80    fn nearest_two(
81        &self,
82        point: Rgba<u8>,
83    ) -> [(usize, f32); 2] {
84        let point = rgba_to_lab(point);
85        let point = [point.l, point.a, point.b];
86        let [l1, l2] = self
87            .0
88            .nearest_n::<kiddo::SquaredEuclidean>(&point, 2)
89            .try_into()
90            .unwrap();
91        [(l1.item, l1.distance), (l2.item, l2.distance)]
92    }
93}
94
95/// Each struct in this module implements this trait and can be combined with
96/// the [`SixelEncoder`](crate::SixelEncoder) struct to dither the result.
97pub trait Dither: private::Sealed {
98    const KERNEL: &[(isize, isize, f32)];
99    const DIV: f32;
100
101    /// Take the input image and convert it to the input palette, applying a
102    /// dithering algorithm to the result.
103    fn dither_and_palettize(
104        image: &RgbaImage,
105        in_palette: &[Lab],
106        bucketer: &impl PaletteBucketer,
107    ) -> Vec<usize> {
108        let pixels = image.pixels().copied().map(rgba_to_lab).collect::<Vec<_>>();
109
110        let mut result = vec![0; image.width() as usize * image.height() as usize];
111
112        let mut spills = vec![[0.0; 3]; image.width() as usize * image.height() as usize];
113
114        for (idx, p) in pixels.iter().copied().enumerate() {
115            let pixel = <Lab>::new(
116                p.l + spills[idx][0],
117                p.a + spills[idx][1],
118                p.b + spills[idx][2],
119            );
120
121            let palette_idx = bucketer.nearest(&[pixel.l, pixel.a, pixel.b]);
122
123            let error0 = pixel.l - in_palette[palette_idx].l;
124            let error1 = pixel.a - in_palette[palette_idx].a;
125            let error2 = pixel.b - in_palette[palette_idx].b;
126            let spill = [error0, error1, error2];
127
128            result[idx] = palette_idx;
129
130            for (dx, dy, m) in Self::KERNEL {
131                let x = idx as isize % image.width() as isize + dx;
132                let y = idx as isize / image.width() as isize + dy;
133                if x < 0 || y < 0 || x >= image.width() as isize || y >= image.height() as isize {
134                    continue;
135                }
136
137                let jdx = y * image.width() as isize + x;
138                let target = &mut spills[jdx as usize];
139                *target = [
140                    target[0] + (spill[0] * m) / Self::DIV / 2.0,
141                    target[1] + (spill[1] * m) / Self::DIV / 2.0,
142                    target[2] + (spill[2] * m) / Self::DIV / 2.0,
143                ];
144            }
145        }
146
147        result
148    }
149}
150
151/// Do not perform dithering. This is the fastest possible palette encoding
152/// option.
153pub struct NoDither;
154impl private::Sealed for NoDither {}
155impl Dither for NoDither {
156    const DIV: f32 = 1.0;
157    const KERNEL: &[(isize, isize, f32)] = &[];
158
159    fn dither_and_palettize(
160        image: &RgbaImage,
161        _in_palette: &[Lab],
162        bucketer: &impl PaletteBucketer,
163    ) -> Vec<usize> {
164        let mut result = vec![0; image.width() as usize * image.height() as usize];
165        image.par_pixels().zip(&mut result).for_each(|(p, dest)| {
166            *dest = bucketer.nearest_rgb(*p);
167        });
168
169        result
170    }
171}
172
173pub struct Sierra;
174impl private::Sealed for Sierra {}
175impl Dither for Sierra {
176    const DIV: f32 = 32.0;
177    // _ _ X 5 3
178    // 2 4 5 4 2
179    // _ 2 3 2 _
180    const KERNEL: &[(isize, isize, f32)] = &[
181        (1, 0, 5.0),
182        (2, 0, 3.0),
183        //
184        (-2, 1, 2.0),
185        (-1, 1, 4.0),
186        (0, 1, 5.0),
187        (1, 1, 4.0),
188        (2, 1, 2.0),
189        //
190        (-1, 2, 2.0),
191        (0, 2, 3.0),
192        (1, 2, 2.0),
193    ];
194}
195
196pub struct Sierra2;
197impl private::Sealed for Sierra2 {}
198impl Dither for Sierra2 {
199    const DIV: f32 = 16.0;
200    // _ _ X 4 3
201    // 1 2 3 2 1
202    // _ 1 2 1 _
203    const KERNEL: &[(isize, isize, f32)] = &[
204        //x y  m
205        (1, 0, 4.0),
206        (2, 0, 3.0),
207        //
208        (-2, 1, 1.0),
209        (-1, 1, 2.0),
210        (0, 1, 3.0),
211        (1, 1, 2.0),
212        (2, 1, 1.0),
213    ];
214}
215
216pub struct SierraLite;
217impl private::Sealed for SierraLite {}
218impl Dither for SierraLite {
219    const DIV: f32 = 4.0;
220    //  _ X 2
221    //  1 1 _
222    const KERNEL: &[(isize, isize, f32)] = &[
223        //x y  m
224        (1, 0, 2.0),
225        //
226        (-1, 1, 1.0),
227        (0, 1, 1.0),
228    ];
229}
230
231pub struct FloydSteinberg;
232impl private::Sealed for FloydSteinberg {}
233impl Dither for FloydSteinberg {
234    const DIV: f32 = 16.0;
235    // _ X 7
236    // 3 5 3
237    const KERNEL: &[(isize, isize, f32)] = &[
238        //x y  m
239        (1, 0, 7.0),
240        //
241        (-1, 1, 3.0),
242        (0, 1, 5.0),
243        (1, 1, 1.0),
244    ];
245}
246
247pub struct JJN;
248impl private::Sealed for JJN {}
249impl Dither for JJN {
250    const DIV: f32 = 48.0;
251    // _ _ X 7 5
252    // 3 5 7 5 3
253    // 1 2 5 3 1
254    const KERNEL: &[(isize, isize, f32)] = &[
255        //x y  m
256        (1, 0, 7.0),
257        (2, 0, 5.0),
258        //
259        (-2, 1, 3.0),
260        (-1, 1, 5.0),
261        (0, 1, 7.0),
262        (1, 1, 5.0),
263        (2, 1, 3.0),
264        //
265        (-2, 2, 1.0),
266        (-1, 2, 2.0),
267        (0, 2, 5.0),
268        (1, 2, 3.0),
269        (2, 2, 1.0),
270    ];
271}
272
273pub struct Stucki;
274impl private::Sealed for Stucki {}
275impl Dither for Stucki {
276    const DIV: f32 = 42.0;
277    // _ _ X 8 4
278    // 2 4 8 4 2
279    // 1 2 4 2 1
280    const KERNEL: &[(isize, isize, f32)] = &[
281        //x y  m
282        (1, 0, 8.0),
283        (2, 0, 4.0),
284        //
285        (-2, 1, 2.0),
286        (-1, 1, 4.0),
287        (0, 1, 8.0),
288        (1, 1, 4.0),
289        (2, 1, 2.0),
290        //
291        (-2, 2, 1.0),
292        (-1, 2, 2.0),
293        (0, 2, 4.0),
294        (1, 2, 2.0),
295        (2, 2, 1.0),
296    ];
297}
298
299pub struct Atkinson;
300impl private::Sealed for Atkinson {}
301impl Dither for Atkinson {
302    const DIV: f32 = 8.0;
303    // _ X 1 1
304    // 1 1 1 _
305    // _ 1 _ _
306    const KERNEL: &[(isize, isize, f32)] = &[
307        //x y  m
308        (1, 0, 1.0),
309        (2, 0, 1.0),
310        //
311        (-1, 1, 1.0),
312        (0, 1, 1.0),
313        (1, 1, 1.0),
314        //
315        (0, 2, 1.0),
316    ];
317}
318
319pub struct Burkes;
320impl private::Sealed for Burkes {}
321impl Dither for Burkes {
322    const DIV: f32 = 32.0;
323    // _ _ X 8 4
324    // 2 4 8 4 2
325    const KERNEL: &[(isize, isize, f32)] = &[
326        //x y  m
327        (1, 0, 8.0),
328        (2, 0, 4.0),
329        //
330        (-2, 1, 2.0),
331        (-1, 1, 4.0),
332        (0, 1, 8.0),
333        (1, 1, 4.0),
334        (2, 1, 2.0),
335    ];
336}
337
338pub struct Bayer;
339impl private::Sealed for Bayer {}
340impl Dither for Bayer {
341    const DIV: f32 = 0.0;
342    const KERNEL: &[(isize, isize, f32)] = &[];
343
344    fn dither_and_palettize(
345        image: &RgbaImage,
346        in_palette: &[Lab],
347        bucketer: &impl PaletteBucketer,
348    ) -> Vec<usize> {
349        let order = image.width().min(image.height()).ilog2().max(2) as usize;
350        let matrix_size = 1 << order;
351        let total_bits = 2 * order;
352        let mut matrix = vec![0.0; matrix_size * matrix_size];
353
354        (0..matrix_size as u32)
355            .into_par_iter()
356            .zip(matrix.par_chunks_mut(matrix_size))
357            .for_each(|(y, row)| {
358                for (x, cell) in row.iter_mut().enumerate() {
359                    let x = x as u32;
360                    let bits = (x ^ y).dilate_expand::<2>().value()
361                        | (y.dilate_expand::<2>().value() << 1);
362                    let bits = bits.reverse_bits() >> (u32::BITS - total_bits as u32);
363                    *cell = bits as f32 / matrix_size.pow(2) as f32;
364                }
365            });
366
367        let max_matrix = matrix
368            .iter()
369            .copied()
370            .max_by(|l, r| l.total_cmp(r))
371            .unwrap_or(1.0);
372        let min_matrix = matrix
373            .iter()
374            .copied()
375            .min_by(|l, r| l.total_cmp(r))
376            .unwrap_or(0.0);
377        matrix.par_iter_mut().for_each(|cell| {
378            *cell = (*cell - min_matrix) / (max_matrix - min_matrix);
379        });
380
381        let mut result = vec![0; image.width() as usize * image.height() as usize];
382        image
383            .par_pixels()
384            .enumerate()
385            .zip(&mut result)
386            .for_each(|((idx, p), dest)| {
387                let x = (idx % image.width() as usize) % matrix_size;
388                let y = (idx / image.width() as usize) % matrix_size;
389
390                let [(l1_item, l1_dist), (l2_item, l2_dist)] = bucketer.nearest_two(*p);
391
392                let p_dist = in_palette[l1_item].distance_squared(in_palette[l2_item]);
393                let t = ((l1_dist - l2_dist + p_dist) / (2.0 * p_dist)).clamp(0.0, 1.0);
394
395                let m_idx = x + y * matrix_size;
396                if t > matrix[m_idx] {
397                    *dest = l2_item;
398                } else {
399                    *dest = l1_item;
400                }
401            });
402
403        result
404    }
405}
406
407/// Uses the Sobol sequence to perturb the colors in a quasi-random manner. This
408/// is somewhere between ordered dithering and stochastic dithering. The results
409/// are generally decent, although they may appear textured like paper.
410pub struct Sobol;
411impl private::Sealed for Sobol {}
412impl Dither for Sobol {
413    const DIV: f32 = 0.0;
414    const KERNEL: &[(isize, isize, f32)] = &[];
415
416    fn dither_and_palettize(
417        image: &RgbaImage,
418        in_palette: &[Lab],
419        bucketer: &impl PaletteBucketer,
420    ) -> Vec<usize> {
421        let mut result = vec![0; image.width() as usize * image.height() as usize];
422        image
423            .par_pixels()
424            .enumerate()
425            .zip(&mut result)
426            .for_each(|((idx, p), dest)| {
427                let thresh = sample(idx as u32 % (1 << 16), 0, idx as u32 / (1 << 16));
428
429                let [(l1_item, l1_dist), (l2_item, l2_dist)] = bucketer.nearest_two(*p);
430
431                let p_dist = in_palette[l1_item].distance_squared(in_palette[l2_item]);
432                let t = ((l1_dist - l2_dist + p_dist) / (2.0 * p_dist)).clamp(0.0, 1.0);
433
434                if t > thresh {
435                    *dest = l2_item;
436                } else {
437                    *dest = l1_item;
438                }
439            });
440
441        result
442    }
443}