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#[cfg(feature = "strum")]
22use strum::Display;
23#[cfg(feature = "strum")]
24use strum::EnumString;
25
26use crate::rgba_to_lab;
27
28/// Abstracts nearest-neighbor palette lookup, allowing different strategies
29/// (e.g. KD-tree vs. direct bucket lookup) to be used interchangeably.
30pub enum PaletteBucketer {
31    /// Backed by a KD-tree for nearest-neighbor queries in Lab color space.
32    KdTree(KdTreeBucketer),
33    /// Direct bit-dilation bucketing for fast lookups without color-space
34    /// conversion.
35    Bit(crate::bit::BitPaletteBucketer),
36}
37
38impl PaletteBucketer {
39    /// Find the nearest palette entry for a point in Lab color space.
40    fn nearest(
41        &self,
42        point: &[f32; 3],
43    ) -> usize {
44        match self {
45            Self::KdTree(b) => b.nearest(point),
46            Self::Bit(b) => b.nearest(point),
47        }
48    }
49
50    /// Find the two nearest palette entries for a point in Lab color space,
51    /// returned as `(palette_index, squared_distance)` pairs ordered nearest
52    /// first. Used by ordered-dithering algorithms that interpolate between
53    /// the two closest colors.
54    fn nearest_two(
55        &self,
56        point: Rgba<u8>,
57    ) -> [(usize, f32); 2] {
58        match self {
59            Self::KdTree(b) => b.nearest_two(point),
60            Self::Bit(b) => b.nearest_two(point),
61        }
62    }
63
64    /// Find the nearest palette entry for an RGB pixel.
65    fn nearest_rgb(
66        &self,
67        pixel: Rgba<u8>,
68    ) -> usize {
69        match self {
70            Self::KdTree(b) => b.nearest_rgb(pixel),
71            Self::Bit(b) => b.nearest_rgb(pixel),
72        }
73    }
74}
75
76/// A [`PaletteBucketer`] variant backed by a KD-tree for nearest-neighbor
77/// queries in Lab color space.
78pub struct KdTreeBucketer(KdTree<f32, usize, 3, 257, u32>);
79
80impl KdTreeBucketer {
81    /// Build a KD-tree from the given palette for nearest-neighbor lookups.
82    pub fn new(palette: &[Lab]) -> Self {
83        let mut tree = KdTree::with_capacity(palette.len());
84        for (idx, color) in palette.iter().enumerate() {
85            tree.add(color.as_ref(), idx);
86        }
87        Self(tree)
88    }
89
90    fn nearest(
91        &self,
92        point: &[f32; 3],
93    ) -> usize {
94        self.0.nearest_one::<kiddo::SquaredEuclidean>(point).item
95    }
96
97    fn nearest_two(
98        &self,
99        point: Rgba<u8>,
100    ) -> [(usize, f32); 2] {
101        let point = rgba_to_lab(point);
102        let point = [point.l, point.a, point.b];
103        let [l1, l2] = self
104            .0
105            .nearest_n::<kiddo::SquaredEuclidean>(&point, 2)
106            .try_into()
107            .unwrap();
108        [(l1.item, l1.distance), (l2.item, l2.distance)]
109    }
110
111    fn nearest_rgb(
112        &self,
113        pixel: Rgba<u8>,
114    ) -> usize {
115        let lab = rgba_to_lab(pixel);
116        self.nearest(lab.as_ref())
117    }
118}
119
120/// The dithering algorithm to apply during sixel encoding.
121///
122/// # Choosing an algorithm
123/// - [`Dither::Sierra`] is a good default choice for dithering, as it produces
124///   high-quality results with minimal artifacts.
125/// - [`Dither::None`] can be used if performance is a concern.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127#[cfg_attr(feature = "strum", derive(EnumString, Display))]
128#[cfg_attr(
129    feature = "strum",
130    strum(ascii_case_insensitive, serialize_all = "kebab-case")
131)]
132#[non_exhaustive]
133pub enum Dither {
134    /// Do not perform dithering. This is the fastest possible palette encoding
135    /// option.
136    None,
137    /// Sierra dithering (3-row).
138    Sierra,
139    /// Sierra-2 dithering (2-row).
140    Sierra2,
141    /// Sierra Lite dithering.
142    SierraLite,
143    /// Floyd-Steinberg dithering.
144    FloydSteinberg,
145    /// Jarvis, Judice, and Ninke dithering.
146    JJN,
147    /// Stucki dithering.
148    Stucki,
149    /// Atkinson dithering.
150    Atkinson,
151    /// Burkes dithering.
152    Burkes,
153    /// Bayer ordered dithering.
154    Bayer,
155    /// Uses the Sobol sequence to perturb the colors in a quasi-random manner.
156    /// This is somewhere between ordered dithering and stochastic dithering.
157    /// The results are generally decent, although they may appear textured like
158    /// paper.
159    Sobol,
160}
161
162impl Dither {
163    /// Take the input image and convert it to the input palette, applying a
164    /// dithering algorithm to the result.
165    pub fn dither_and_palettize(
166        &self,
167        image: &RgbaImage,
168        in_palette: &[Lab],
169        bucketer: &PaletteBucketer,
170    ) -> Vec<usize> {
171        match self {
172            Self::None => no_dither(image, bucketer),
173            Self::Sierra => {
174                error_diffusion_dither(image, in_palette, bucketer, SIERRA_KERNEL, 32.0)
175            }
176            Self::Sierra2 => {
177                error_diffusion_dither(image, in_palette, bucketer, SIERRA2_KERNEL, 16.0)
178            }
179            Self::SierraLite => {
180                error_diffusion_dither(image, in_palette, bucketer, SIERRA_LITE_KERNEL, 4.0)
181            }
182            Self::FloydSteinberg => {
183                error_diffusion_dither(image, in_palette, bucketer, FLOYD_STEINBERG_KERNEL, 16.0)
184            }
185            Self::JJN => error_diffusion_dither(image, in_palette, bucketer, JJN_KERNEL, 48.0),
186            Self::Stucki => {
187                error_diffusion_dither(image, in_palette, bucketer, STUCKI_KERNEL, 42.0)
188            }
189            Self::Atkinson => {
190                error_diffusion_dither(image, in_palette, bucketer, ATKINSON_KERNEL, 8.0)
191            }
192            Self::Burkes => {
193                error_diffusion_dither(image, in_palette, bucketer, BURKES_KERNEL, 32.0)
194            }
195            Self::Bayer => bayer_dither(image, in_palette, bucketer),
196            Self::Sobol => sobol_dither(image, in_palette, bucketer),
197        }
198    }
199}
200
201// --- Kernel constants ---
202
203// _ _ X 5 3
204// 2 4 5 4 2
205// _ 2 3 2 _
206const SIERRA_KERNEL: &[(isize, isize, f32)] = &[
207    (1, 0, 5.0),
208    (2, 0, 3.0),
209    //
210    (-2, 1, 2.0),
211    (-1, 1, 4.0),
212    (0, 1, 5.0),
213    (1, 1, 4.0),
214    (2, 1, 2.0),
215    //
216    (-1, 2, 2.0),
217    (0, 2, 3.0),
218    (1, 2, 2.0),
219];
220
221// _ _ X 4 3
222// 1 2 3 2 1
223const SIERRA2_KERNEL: &[(isize, isize, f32)] = &[
224    //x y  m
225    (1, 0, 4.0),
226    (2, 0, 3.0),
227    //
228    (-2, 1, 1.0),
229    (-1, 1, 2.0),
230    (0, 1, 3.0),
231    (1, 1, 2.0),
232    (2, 1, 1.0),
233];
234
235//  _ X 2
236//  1 1 _
237const SIERRA_LITE_KERNEL: &[(isize, isize, f32)] = &[
238    //x y  m
239    (1, 0, 2.0),
240    //
241    (-1, 1, 1.0),
242    (0, 1, 1.0),
243];
244
245// _ X 7
246// 3 5 3
247const FLOYD_STEINBERG_KERNEL: &[(isize, isize, f32)] = &[
248    //x y  m
249    (1, 0, 7.0),
250    //
251    (-1, 1, 3.0),
252    (0, 1, 5.0),
253    (1, 1, 1.0),
254];
255
256// _ _ X 7 5
257// 3 5 7 5 3
258// 1 2 5 3 1
259const JJN_KERNEL: &[(isize, isize, f32)] = &[
260    //x y  m
261    (1, 0, 7.0),
262    (2, 0, 5.0),
263    //
264    (-2, 1, 3.0),
265    (-1, 1, 5.0),
266    (0, 1, 7.0),
267    (1, 1, 5.0),
268    (2, 1, 3.0),
269    //
270    (-2, 2, 1.0),
271    (-1, 2, 2.0),
272    (0, 2, 5.0),
273    (1, 2, 3.0),
274    (2, 2, 1.0),
275];
276
277// _ _ X 8 4
278// 2 4 8 4 2
279// 1 2 4 2 1
280const STUCKI_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// _ X 1 1
299// 1 1 1 _
300// _ 1 _ _
301const ATKINSON_KERNEL: &[(isize, isize, f32)] = &[
302    //x y  m
303    (1, 0, 1.0),
304    (2, 0, 1.0),
305    //
306    (-1, 1, 1.0),
307    (0, 1, 1.0),
308    (1, 1, 1.0),
309    //
310    (0, 2, 1.0),
311];
312
313// _ _ X 8 4
314// 2 4 8 4 2
315const BURKES_KERNEL: &[(isize, isize, f32)] = &[
316    //x y  m
317    (1, 0, 8.0),
318    (2, 0, 4.0),
319    //
320    (-2, 1, 2.0),
321    (-1, 1, 4.0),
322    (0, 1, 8.0),
323    (1, 1, 4.0),
324    (2, 1, 2.0),
325];
326
327// --- Dither implementations ---
328
329fn no_dither(
330    image: &RgbaImage,
331    bucketer: &PaletteBucketer,
332) -> Vec<usize> {
333    let mut result = vec![0; image.width() as usize * image.height() as usize];
334    image.par_pixels().zip(&mut result).for_each(|(p, dest)| {
335        *dest = bucketer.nearest_rgb(*p);
336    });
337
338    result
339}
340
341fn error_diffusion_dither(
342    image: &RgbaImage,
343    in_palette: &[Lab],
344    bucketer: &PaletteBucketer,
345    kernel: &[(isize, isize, f32)],
346    div: f32,
347) -> Vec<usize> {
348    let pixels = image.pixels().copied().map(rgba_to_lab).collect::<Vec<_>>();
349
350    let mut result = vec![0; image.width() as usize * image.height() as usize];
351
352    let mut spills = vec![[0.0; 3]; image.width() as usize * image.height() as usize];
353
354    for (idx, p) in pixels.iter().copied().enumerate() {
355        let pixel = <Lab>::new(
356            p.l + spills[idx][0],
357            p.a + spills[idx][1],
358            p.b + spills[idx][2],
359        );
360
361        let palette_idx = bucketer.nearest(&[pixel.l, pixel.a, pixel.b]);
362
363        let error0 = pixel.l - in_palette[palette_idx].l;
364        let error1 = pixel.a - in_palette[palette_idx].a;
365        let error2 = pixel.b - in_palette[palette_idx].b;
366        let spill = [error0, error1, error2];
367
368        result[idx] = palette_idx;
369
370        for (dx, dy, m) in kernel {
371            let x = idx as isize % image.width() as isize + dx;
372            let y = idx as isize / image.width() as isize + dy;
373            if x < 0 || y < 0 || x >= image.width() as isize || y >= image.height() as isize {
374                continue;
375            }
376
377            let jdx = y * image.width() as isize + x;
378            let target = &mut spills[jdx as usize];
379            *target = [
380                target[0] + (spill[0] * m) / div / 2.0,
381                target[1] + (spill[1] * m) / div / 2.0,
382                target[2] + (spill[2] * m) / div / 2.0,
383            ];
384        }
385    }
386
387    result
388}
389
390fn bayer_dither(
391    image: &RgbaImage,
392    in_palette: &[Lab],
393    bucketer: &PaletteBucketer,
394) -> Vec<usize> {
395    let order = image.width().min(image.height()).ilog2().max(2) as usize;
396    let matrix_size = 1 << order;
397    let total_bits = 2 * order;
398    let mut matrix = vec![0.0; matrix_size * matrix_size];
399
400    (0..matrix_size as u32)
401        .into_par_iter()
402        .zip(matrix.par_chunks_mut(matrix_size))
403        .for_each(|(y, row)| {
404            for (x, cell) in row.iter_mut().enumerate() {
405                let x = x as u32;
406                let bits =
407                    (x ^ y).dilate_expand::<2>().value() | (y.dilate_expand::<2>().value() << 1);
408                let bits = bits.reverse_bits() >> (u32::BITS - total_bits as u32);
409                *cell = bits as f32 / matrix_size.pow(2) as f32;
410            }
411        });
412
413    let max_matrix = matrix
414        .iter()
415        .copied()
416        .max_by(|l, r| l.total_cmp(r))
417        .unwrap_or(1.0);
418    let min_matrix = matrix
419        .iter()
420        .copied()
421        .min_by(|l, r| l.total_cmp(r))
422        .unwrap_or(0.0);
423    matrix.par_iter_mut().for_each(|cell| {
424        *cell = (*cell - min_matrix) / (max_matrix - min_matrix);
425    });
426
427    let mut result = vec![0; image.width() as usize * image.height() as usize];
428    image
429        .par_pixels()
430        .enumerate()
431        .zip(&mut result)
432        .for_each(|((idx, p), dest)| {
433            let x = (idx % image.width() as usize) % matrix_size;
434            let y = (idx / image.width() as usize) % matrix_size;
435
436            let [(l1_item, l1_dist), (l2_item, l2_dist)] = bucketer.nearest_two(*p);
437
438            let p_dist = in_palette[l1_item].distance_squared(in_palette[l2_item]);
439            let t = ((l1_dist - l2_dist + p_dist) / (2.0 * p_dist)).clamp(0.0, 1.0);
440
441            let m_idx = x + y * matrix_size;
442            if t > matrix[m_idx] {
443                *dest = l2_item;
444            } else {
445                *dest = l1_item;
446            }
447        });
448
449    result
450}
451
452fn sobol_dither(
453    image: &RgbaImage,
454    in_palette: &[Lab],
455    bucketer: &PaletteBucketer,
456) -> Vec<usize> {
457    let mut result = vec![0; image.width() as usize * image.height() as usize];
458    image
459        .par_pixels()
460        .enumerate()
461        .zip(&mut result)
462        .for_each(|((idx, p), dest)| {
463            let thresh = sample(idx as u32 % (1 << 16), 0, idx as u32 / (1 << 16));
464
465            let [(l1_item, l1_dist), (l2_item, l2_dist)] = bucketer.nearest_two(*p);
466
467            let p_dist = in_palette[l1_item].distance_squared(in_palette[l2_item]);
468            let t = ((l1_dist - l2_dist + p_dist) / (2.0 * p_dist)).clamp(0.0, 1.0);
469
470            if t > thresh {
471                *dest = l2_item;
472            } else {
473                *dest = l1_item;
474            }
475        });
476
477    result
478}