Skip to main content

a_sixel/
bit.rs

1//! Encodes a palette by bucketing bit ranges into a power of two number of
2//! buckets. This is very fast and produces ok results for most images at larger
3//! palette sizes (e.g. 256).
4
5use std::cell::RefCell;
6
7use dilate::DilateExpand;
8use image::Rgba;
9use palette::IntoColor;
10use palette::Lab;
11use palette::Srgb;
12use rayon::iter::ParallelIterator;
13
14use crate::PaletteBuilder;
15use crate::dither::PaletteBucketer;
16use crate::private;
17
18pub struct BitPaletteBuilder {
19    pub(crate) shift: usize,
20}
21
22impl BitPaletteBuilder {
23    pub(crate) fn new(palette_size: usize) -> Self {
24        BitPaletteBuilder {
25            shift: Self::shift(palette_size),
26        }
27    }
28
29    pub(crate) fn shift(palette_size: usize) -> usize {
30        24 - palette_size.ilog2() as usize
31    }
32
33    pub(crate) fn index(
34        color: Srgb<u8>,
35        shift: usize,
36    ) -> usize {
37        let r = color.red.dilate_expand::<3>().value();
38        let g = color.green.dilate_expand::<3>().value();
39        let b = color.blue.dilate_expand::<3>().value();
40
41        // Since elements to the right will get shifted off first, we put them in grb
42        // order (order of most-least significant for luminance). This probably doesn't
43        // make a huge difference, but the theory is nice.
44        let rgb = g << 2 | r << 1 | b;
45
46        (rgb >> shift) as usize
47    }
48}
49
50impl private::Sealed for BitPaletteBuilder {}
51
52impl PaletteBuilder for BitPaletteBuilder {
53    const NAME: &'static str = "Bit";
54
55    fn build_palette(
56        image: &image::RgbaImage,
57        palette_size: usize,
58    ) -> Vec<Lab> {
59        let builder = Self::new(palette_size);
60
61        thread_local! {
62            static PALETTE: RefCell<Vec<(u64, u64, u64, u64)>> = RefCell::default();
63        }
64
65        let pool = rayon::ThreadPoolBuilder::new()
66            .num_threads(rayon::current_num_threads())
67            .build()
68            .unwrap();
69
70        pool.install(|| {
71            image.par_pixels().for_each(|pixel| {
72                PALETTE.with_borrow_mut(|palette| {
73                    palette.resize(palette_size, (0, 0, 0, 0));
74
75                    let pixel = Srgb::<u8>::new(pixel[0], pixel[1], pixel[2]);
76                    let index = Self::index(pixel, builder.shift);
77                    palette[index].0 += pixel.red as u64;
78                    palette[index].1 += pixel.green as u64;
79                    palette[index].2 += pixel.blue as u64;
80                    palette[index].3 += 1;
81                });
82            });
83        });
84
85        let per_thread_palettes = pool.broadcast(|_ctx| PALETTE.with_borrow_mut(std::mem::take));
86
87        let mut final_palette = vec![(0, 0, 0, 0); palette_size];
88        for palette in per_thread_palettes {
89            for (dest, src) in final_palette.iter_mut().zip(palette) {
90                dest.0 += src.0;
91                dest.1 += src.1;
92                dest.2 += src.2;
93                dest.3 += src.3;
94            }
95        }
96
97        final_palette
98            .into_iter()
99            .filter(|node| node.3 > 0)
100            .map(|node| {
101                let rgb = Srgb::new(
102                    (node.0 / node.3) as u8,
103                    (node.1 / node.3) as u8,
104                    (node.2 / node.3) as u8,
105                );
106                rgb.into_format().into_color()
107            })
108            .collect::<Vec<_>>()
109    }
110
111    fn build_bucketer(
112        palette: &[Lab],
113        palette_size: usize,
114    ) -> impl PaletteBucketer {
115        BitPaletteBucketer::new(palette, palette_size)
116    }
117}
118
119/// A [`PaletteBucketer`] that maps pixels directly to palette entries via the
120/// same bit-dilation bucketing used by [`BitPaletteBuilder`]. This avoids both
121/// the Lab color-space conversion and the KD-tree query for each pixel, making
122/// it significantly faster than
123/// [`KdTreeBucketer`](crate::dither::KdTreeBucketer) when used with
124/// [`NoDither`](crate::dither::NoDither).
125pub struct BitPaletteBucketer {
126    lut: Vec<(usize, usize, f32, f32)>,
127    shift: usize,
128}
129
130impl BitPaletteBucketer {
131    /// Build a lookup table that maps each bit-bucket index to its two nearest
132    /// palette entries with distances. Each palette Lab entry is converted to
133    /// Srgb to determine which bucket it belongs to, giving us a representative
134    /// color per bucket without needing to re-scan the image.
135    fn new(
136        palette: &[Lab],
137        palette_size: usize,
138    ) -> Self {
139        let shift = BitPaletteBuilder::shift(palette_size);
140        let n_bits = palette_size.ilog2() as usize;
141        let masks = morton_dim_masks(n_bits);
142        let all_mask = palette_size - 1;
143
144        let mut bucket_entries: Vec<Vec<usize>> = vec![vec![]; palette_size];
145        for (pi, &lab) in palette.iter().enumerate() {
146            let rgb: Srgb = lab.into_color();
147            let rgb: Srgb<u8> = rgb.into_format();
148            let idx = BitPaletteBuilder::index(rgb, shift);
149            bucket_entries[idx].push(pi);
150        }
151
152        let lut = (0..palette_size)
153            .map(|bucket_idx| {
154                if bucket_entries[bucket_idx].is_empty() {
155                    return (0, 0, 0.0, 0.0);
156                }
157
158                let nearest = bucket_entries[bucket_idx][0];
159                let second = find_second_nearest(
160                    nearest,
161                    bucket_idx,
162                    &bucket_entries,
163                    palette,
164                    masks,
165                    all_mask,
166                );
167                (nearest, second.0, 0.0, second.1)
168            })
169            .collect();
170
171        Self { lut, shift }
172    }
173}
174
175/// Bit masks for each dimension (B, R, G) in an `n`-bit Morton code
176/// with the interleaving order `...g r b g r b`.
177fn morton_dim_masks(n: usize) -> [usize; 3] {
178    let mut masks = [0usize; 3]; // [B, R, G]
179    for i in 0..n {
180        masks[i % 3] |= 1 << i;
181    }
182    masks
183}
184
185/// Increment one dimension of a Morton code. Returns `None` on overflow.
186fn morton_inc(
187    z: usize,
188    dim_mask: usize,
189    all_mask: usize,
190) -> Option<usize> {
191    let not_dim = all_mask & !dim_mask;
192    let t = (z | not_dim) + 1;
193    if t > all_mask {
194        return None;
195    }
196    Some((t & dim_mask) | (z & not_dim))
197}
198
199/// Decrement one dimension of a Morton code. Returns `None` on underflow.
200fn morton_dec(
201    z: usize,
202    dim_mask: usize,
203    all_mask: usize,
204) -> Option<usize> {
205    if z & dim_mask == 0 {
206        return None;
207    }
208    let not_dim = all_mask & !dim_mask;
209    let t = (z & dim_mask).wrapping_sub(1);
210    Some((t & dim_mask) | (z & not_dim))
211}
212
213/// Apply ±1 offsets in each Morton dimension. Returns `None` if any
214/// dimension would go out of bounds.
215fn morton_neighbor(
216    z: usize,
217    deltas: [i32; 3],
218    masks: &[usize; 3],
219    all_mask: usize,
220) -> Option<usize> {
221    let mut result = z;
222    for (delta, &mask) in deltas.iter().zip(masks.iter()) {
223        match delta.signum() {
224            1 => result = morton_inc(result, mask, all_mask)?,
225            -1 => result = morton_dec(result, mask, all_mask)?,
226            _ => {}
227        }
228    }
229    Some(result)
230}
231
232/// Find the second-nearest palette entry to `palette[nearest]` by searching
233/// the 26 neighboring Morton cells. Falls back to a full palette scan when
234/// no neighbor contains a candidate.
235fn find_second_nearest(
236    nearest: usize,
237    bucket_idx: usize,
238    bucket_entries: &[Vec<usize>],
239    palette: &[Lab],
240    masks: [usize; 3],
241    all_mask: usize,
242) -> (usize, f32) {
243    use palette::color_difference::EuclideanDistance;
244
245    let target = &palette[nearest];
246    let mut best = (0usize, f32::INFINITY);
247
248    for db in -1i32..=1 {
249        for dr in -1i32..=1 {
250            for dg in -1i32..=1 {
251                let nb = if db == 0 && dr == 0 && dg == 0 {
252                    Some(bucket_idx)
253                } else {
254                    morton_neighbor(bucket_idx, [db, dr, dg], &masks, all_mask)
255                };
256                if let Some(nb) = nb {
257                    for &pi in &bucket_entries[nb] {
258                        if pi == nearest {
259                            continue;
260                        }
261                        let dist = target.distance_squared(palette[pi]);
262                        if dist < best.1 {
263                            best = (pi, dist);
264                        }
265                    }
266                }
267            }
268        }
269    }
270
271    if best.1.is_finite() {
272        return best;
273    }
274
275    for (pi, color) in palette.iter().enumerate() {
276        if pi == nearest {
277            continue;
278        }
279        let dist = target.distance_squared(*color);
280        if dist < best.1 {
281            best = (pi, dist);
282        }
283    }
284
285    best
286}
287
288impl private::Sealed for BitPaletteBucketer {}
289
290impl PaletteBucketer for BitPaletteBucketer {
291    fn nearest(
292        &self,
293        point: &[f32; 3],
294    ) -> usize {
295        let lab = Lab::new(point[0], point[1], point[2]);
296        let rgb: Srgb = lab.into_color();
297        let rgb: Srgb<u8> = rgb.into_format();
298        self.lut[BitPaletteBuilder::index(rgb, self.shift)].0
299    }
300
301    fn nearest_two(
302        &self,
303        point: Rgba<u8>,
304    ) -> [(usize, f32); 2] {
305        let (n1, n2, d1, d2) =
306            self.lut[BitPaletteBuilder::index(Srgb::new(point[0], point[1], point[2]), self.shift)];
307        [(n1, d1), (n2, d2)]
308    }
309
310    fn nearest_rgb(
311        &self,
312        pixel: Rgba<u8>,
313    ) -> usize {
314        let index = BitPaletteBuilder::index(Srgb::new(pixel[0], pixel[1], pixel[2]), self.shift);
315        self.lut[index].0
316    }
317}