Skip to main content

ai_image/imageops/
colorops.rs

1//! Functions for altering and converting the color of pixelbufs
2
3use alloc::vec::Vec;
4#[cfg(not(feature = "std"))]
5use num_traits::Float as _;
6use num_traits::NumCast;
7
8use crate::color::{FromColor, IntoColor, Luma, LumaA};
9use crate::metadata::{CicpColorPrimaries, CicpTransferCharacteristics};
10use crate::traits::{Pixel, Primitive};
11use crate::utils::clamp;
12use crate::{GenericImage, GenericImageView, ImageBuffer};
13
14type Subpixel<I> = <<I as GenericImageView>::Pixel as Pixel>::Subpixel;
15
16/// Convert the supplied image to grayscale. Alpha channel is discarded.
17pub fn grayscale<I: GenericImageView>(
18    image: &I,
19) -> ImageBuffer<Luma<Subpixel<I>>, Vec<Subpixel<I>>> {
20    grayscale_with_type(image)
21}
22
23/// Convert the supplied image to grayscale. Alpha channel is preserved.
24pub fn grayscale_alpha<I: GenericImageView>(
25    image: &I,
26) -> ImageBuffer<LumaA<Subpixel<I>>, Vec<Subpixel<I>>> {
27    grayscale_with_type_alpha(image)
28}
29
30/// Convert the supplied image to a grayscale image with the specified pixel type. Alpha channel is discarded.
31pub fn grayscale_with_type<NewPixel, I: GenericImageView>(
32    image: &I,
33) -> ImageBuffer<NewPixel, Vec<NewPixel::Subpixel>>
34where
35    NewPixel: Pixel + FromColor<Luma<Subpixel<I>>>,
36{
37    let (width, height) = image.dimensions();
38    let mut out = ImageBuffer::new(width, height);
39    out.copy_color_space_from(&image.buffer_with_dimensions(0, 0));
40
41    for (x, y, pixel) in image.pixels() {
42        let grayscale = pixel.to_luma();
43        let new_pixel = grayscale.into_color(); // no-op for luma->luma
44
45        out.put_pixel(x, y, new_pixel);
46    }
47
48    out
49}
50
51/// Convert the supplied image to a grayscale image with the specified pixel type. Alpha channel is preserved.
52pub fn grayscale_with_type_alpha<NewPixel, I: GenericImageView>(
53    image: &I,
54) -> ImageBuffer<NewPixel, Vec<NewPixel::Subpixel>>
55where
56    NewPixel: Pixel + FromColor<LumaA<Subpixel<I>>>,
57{
58    let (width, height) = image.dimensions();
59    let mut out = ImageBuffer::new(width, height);
60    out.copy_color_space_from(&image.buffer_with_dimensions(0, 0));
61
62    for (x, y, pixel) in image.pixels() {
63        let grayscale = pixel.to_luma_alpha();
64        let new_pixel = grayscale.into_color(); // no-op for luma->luma
65
66        out.put_pixel(x, y, new_pixel);
67    }
68
69    out
70}
71
72/// Invert each pixel within the supplied image.
73/// This function operates in place.
74pub fn invert<I: GenericImage>(image: &mut I) {
75    // TODO find a way to use pixels?
76    let (width, height) = image.dimensions();
77
78    for y in 0..height {
79        for x in 0..width {
80            let mut p = image.get_pixel(x, y);
81            p.invert();
82
83            image.put_pixel(x, y, p);
84        }
85    }
86}
87
88/// Adjust the contrast of the supplied image.
89/// ```contrast``` is the amount to adjust the contrast by.
90/// Negative values decrease the contrast and positive values increase the contrast.
91///
92/// *[See also `contrast_in_place`.][contrast_in_place]*
93pub fn contrast<I, P, S>(image: &I, contrast: f32) -> ImageBuffer<P, Vec<S>>
94where
95    I: GenericImageView<Pixel = P>,
96    P: Pixel<Subpixel = S> + 'static,
97    S: Primitive + 'static,
98{
99    let mut out = image.buffer_like();
100
101    let max = S::DEFAULT_MAX_VALUE;
102    let max: f32 = NumCast::from(max).unwrap();
103
104    let percent = ((100.0 + contrast) / 100.0).powi(2);
105
106    for (x, y, pixel) in image.pixels() {
107        let f = pixel.map(|b| {
108            let c: f32 = NumCast::from(b).unwrap();
109
110            let d = ((c / max - 0.5) * percent + 0.5) * max;
111            let e = clamp(d, 0.0, max);
112
113            NumCast::from(e).unwrap()
114        });
115        out.put_pixel(x, y, f);
116    }
117
118    out
119}
120
121/// Adjust the contrast of the supplied image in place.
122/// ```contrast``` is the amount to adjust the contrast by.
123/// Negative values decrease the contrast and positive values increase the contrast.
124///
125/// *[See also `contrast`.][contrast]*
126pub fn contrast_in_place<I>(image: &mut I, contrast: f32)
127where
128    I: GenericImage,
129{
130    let (width, height) = image.dimensions();
131
132    let max = <I::Pixel as Pixel>::Subpixel::DEFAULT_MAX_VALUE;
133    let max: f32 = NumCast::from(max).unwrap();
134
135    let percent = ((100.0 + contrast) / 100.0).powi(2);
136
137    // TODO find a way to use pixels?
138    for y in 0..height {
139        for x in 0..width {
140            let f = image.get_pixel(x, y).map(|b| {
141                let c: f32 = NumCast::from(b).unwrap();
142
143                let d = ((c / max - 0.5) * percent + 0.5) * max;
144                let e = clamp(d, 0.0, max);
145
146                NumCast::from(e).unwrap()
147            });
148
149            image.put_pixel(x, y, f);
150        }
151    }
152}
153
154/// Brighten the supplied image.
155/// ```value``` is the amount to brighten each pixel by.
156/// Negative values decrease the brightness and positive values increase it.
157///
158/// *[See also `brighten_in_place`.][brighten_in_place]*
159pub fn brighten<I, P, S>(image: &I, value: i32) -> ImageBuffer<P, Vec<S>>
160where
161    I: GenericImageView<Pixel = P>,
162    P: Pixel<Subpixel = S> + 'static,
163    S: Primitive + 'static,
164{
165    let mut out = image.buffer_like();
166
167    let max = S::DEFAULT_MAX_VALUE;
168    let max: i32 = NumCast::from(max).unwrap();
169
170    for (x, y, pixel) in image.pixels() {
171        let e = pixel.map_with_alpha(
172            |b| {
173                let c: i32 = NumCast::from(b).unwrap();
174                let d = clamp(c + value, 0, max);
175
176                NumCast::from(d).unwrap()
177            },
178            |alpha| alpha,
179        );
180        out.put_pixel(x, y, e);
181    }
182
183    out
184}
185
186/// Brighten the supplied image in place.
187/// ```value``` is the amount to brighten each pixel by.
188/// Negative values decrease the brightness and positive values increase it.
189///
190/// *[See also `brighten`.][brighten]*
191pub fn brighten_in_place<I>(image: &mut I, value: i32)
192where
193    I: GenericImage,
194{
195    let (width, height) = image.dimensions();
196
197    let max = <I::Pixel as Pixel>::Subpixel::DEFAULT_MAX_VALUE;
198    let max: i32 = NumCast::from(max).unwrap(); // TODO what does this do for f32? clamp at 1??
199
200    // TODO find a way to use pixels?
201    for y in 0..height {
202        for x in 0..width {
203            let e = image.get_pixel(x, y).map_with_alpha(
204                |b| {
205                    let c: i32 = NumCast::from(b).unwrap();
206                    let d = clamp(c + value, 0, max);
207
208                    NumCast::from(d).unwrap()
209                },
210                |alpha| alpha,
211            );
212
213            image.put_pixel(x, y, e);
214        }
215    }
216}
217
218/// Hue rotate the supplied image.
219/// `value` is the degrees to rotate each pixel by.
220/// 0 and 360 do nothing, the rest rotates by the given degree value.
221/// just like the css webkit filter hue-rotate(180)
222///
223/// *[See also `huerotate_in_place`.][huerotate_in_place]*
224pub fn huerotate<I, P, S>(image: &I, value: i32) -> ImageBuffer<P, Vec<S>>
225where
226    I: GenericImageView<Pixel = P>,
227    P: Pixel<Subpixel = S> + 'static,
228    S: Primitive + 'static,
229{
230    let mut out = image.buffer_like();
231
232    let angle: f64 = NumCast::from(value).unwrap();
233
234    let cosv = angle.to_radians().cos();
235    let sinv = angle.to_radians().sin();
236    let matrix: [f64; 9] = [
237        // Reds
238        0.213 + cosv * 0.787 - sinv * 0.213,
239        0.715 - cosv * 0.715 - sinv * 0.715,
240        0.072 - cosv * 0.072 + sinv * 0.928,
241        // Greens
242        0.213 - cosv * 0.213 + sinv * 0.143,
243        0.715 + cosv * 0.285 + sinv * 0.140,
244        0.072 - cosv * 0.072 - sinv * 0.283,
245        // Blues
246        0.213 - cosv * 0.213 - sinv * 0.787,
247        0.715 - cosv * 0.715 + sinv * 0.715,
248        0.072 + cosv * 0.928 + sinv * 0.072,
249    ];
250    for (x, y, pixel) in out.enumerate_pixels_mut() {
251        let p = image.get_pixel(x, y);
252
253        #[allow(deprecated)]
254        let (k1, k2, k3, k4) = p.channels4();
255        let vec: (f64, f64, f64, f64) = (
256            NumCast::from(k1).unwrap(),
257            NumCast::from(k2).unwrap(),
258            NumCast::from(k3).unwrap(),
259            NumCast::from(k4).unwrap(),
260        );
261
262        let r = vec.0;
263        let g = vec.1;
264        let b = vec.2;
265
266        let new_r = matrix[0] * r + matrix[1] * g + matrix[2] * b;
267        let new_g = matrix[3] * r + matrix[4] * g + matrix[5] * b;
268        let new_b = matrix[6] * r + matrix[7] * g + matrix[8] * b;
269        let max = 255f64;
270
271        #[allow(deprecated)]
272        let outpixel = Pixel::from_channels(
273            NumCast::from(clamp(new_r, 0.0, max)).unwrap(),
274            NumCast::from(clamp(new_g, 0.0, max)).unwrap(),
275            NumCast::from(clamp(new_b, 0.0, max)).unwrap(),
276            NumCast::from(clamp(vec.3, 0.0, max)).unwrap(),
277        );
278        *pixel = outpixel;
279    }
280    out
281}
282
283/// Hue rotate the supplied image in place.
284///
285/// `value` is the degrees to rotate each pixel by.
286/// 0 and 360 do nothing, the rest rotates by the given degree value.
287/// just like the css webkit filter hue-rotate(180)
288///
289/// *[See also `huerotate`.][huerotate]*
290pub fn huerotate_in_place<I>(image: &mut I, value: i32)
291where
292    I: GenericImage,
293{
294    let (width, height) = image.dimensions();
295
296    let angle: f64 = NumCast::from(value).unwrap();
297
298    let cosv = angle.to_radians().cos();
299    let sinv = angle.to_radians().sin();
300    let matrix: [f64; 9] = [
301        // Reds
302        0.213 + cosv * 0.787 - sinv * 0.213,
303        0.715 - cosv * 0.715 - sinv * 0.715,
304        0.072 - cosv * 0.072 + sinv * 0.928,
305        // Greens
306        0.213 - cosv * 0.213 + sinv * 0.143,
307        0.715 + cosv * 0.285 + sinv * 0.140,
308        0.072 - cosv * 0.072 - sinv * 0.283,
309        // Blues
310        0.213 - cosv * 0.213 - sinv * 0.787,
311        0.715 - cosv * 0.715 + sinv * 0.715,
312        0.072 + cosv * 0.928 + sinv * 0.072,
313    ];
314
315    // TODO find a way to use pixels?
316    for y in 0..height {
317        for x in 0..width {
318            let pixel = image.get_pixel(x, y);
319
320            #[allow(deprecated)]
321            let (k1, k2, k3, k4) = pixel.channels4();
322
323            let vec: (f64, f64, f64, f64) = (
324                NumCast::from(k1).unwrap(),
325                NumCast::from(k2).unwrap(),
326                NumCast::from(k3).unwrap(),
327                NumCast::from(k4).unwrap(),
328            );
329
330            let r = vec.0;
331            let g = vec.1;
332            let b = vec.2;
333
334            let new_r = matrix[0] * r + matrix[1] * g + matrix[2] * b;
335            let new_g = matrix[3] * r + matrix[4] * g + matrix[5] * b;
336            let new_b = matrix[6] * r + matrix[7] * g + matrix[8] * b;
337            let max = 255f64;
338
339            #[allow(deprecated)]
340            let outpixel = Pixel::from_channels(
341                NumCast::from(clamp(new_r, 0.0, max)).unwrap(),
342                NumCast::from(clamp(new_g, 0.0, max)).unwrap(),
343                NumCast::from(clamp(new_b, 0.0, max)).unwrap(),
344                NumCast::from(clamp(vec.3, 0.0, max)).unwrap(),
345            );
346
347            image.put_pixel(x, y, outpixel);
348        }
349    }
350}
351
352/// A color map
353pub trait ColorMap {
354    /// The color type on which the map operates on
355    type Color;
356    /// Returns the index of the closest match of `color`
357    /// in the color map.
358    fn index_of(&self, color: &Self::Color) -> usize;
359    /// Looks up color by index in the color map.  If `idx` is out of range for the color map, or
360    /// `ColorMap` doesn't implement `lookup` `None` is returned.
361    fn lookup(&self, index: usize) -> Option<Self::Color> {
362        let _ = index;
363        None
364    }
365    /// Determine if this implementation of `ColorMap` overrides the default `lookup`.
366    fn has_lookup(&self) -> bool {
367        false
368    }
369    /// Maps `color` to the closest color in the color map.
370    fn map_color(&self, color: &mut Self::Color);
371}
372
373/// A bi-level color map
374///
375/// # Examples
376/// ```
377/// use ai_image::imageops::colorops::{index_colors, BiLevel, ColorMap};
378/// use ai_image::{ImageBuffer, Luma};
379///
380/// let (w, h) = (16, 16);
381/// // Create an image with a smooth horizontal gradient from black (0) to white (255).
382/// let gray = ImageBuffer::from_fn(w, h, |x, y| -> Luma<u8> { [(255 * x / w) as u8].into() });
383/// // Mapping the gray image through the `BiLevel` filter should map gray pixels less than half
384/// // intensity (127) to black (0), and anything greater to white (255).
385/// let cmap = BiLevel;
386/// let palletized = index_colors(&gray, &cmap);
387/// let mapped = ImageBuffer::from_fn(w, h, |x, y| {
388///     let p = palletized.get_pixel(x, y);
389///     cmap.lookup(p.0[0] as usize)
390///         .expect("indexed color out-of-range")
391/// });
392/// // Create an black and white image of expected output.
393/// let bw = ImageBuffer::from_fn(w, h, |x, y| -> Luma<u8> {
394///     if x <= (w / 2) {
395///         [0].into()
396///     } else {
397///         [255].into()
398///     }
399/// });
400/// assert_eq!(mapped, bw);
401/// ```
402#[derive(Clone, Copy)]
403pub struct BiLevel;
404
405impl ColorMap for BiLevel {
406    type Color = Luma<u8>;
407
408    #[inline(always)]
409    fn index_of(&self, color: &Luma<u8>) -> usize {
410        let luma = color.0;
411        if luma[0] > 127 {
412            1
413        } else {
414            0
415        }
416    }
417
418    #[inline(always)]
419    fn lookup(&self, idx: usize) -> Option<Self::Color> {
420        match idx {
421            0 => Some([0].into()),
422            1 => Some([255].into()),
423            _ => None,
424        }
425    }
426
427    /// Indicate `NeuQuant` implements `lookup`.
428    fn has_lookup(&self) -> bool {
429        true
430    }
431
432    #[inline(always)]
433    fn map_color(&self, color: &mut Luma<u8>) {
434        let new_color = 0xFF * self.index_of(color) as u8;
435        let luma = &mut color.0;
436        luma[0] = new_color;
437    }
438}
439
440#[cfg(feature = "color_quant")]
441impl ColorMap for color_quant::NeuQuant {
442    type Color = crate::color::Rgba<u8>;
443
444    #[inline(always)]
445    fn index_of(&self, color: &Self::Color) -> usize {
446        self.index_of(color.channels())
447    }
448
449    #[inline(always)]
450    fn lookup(&self, idx: usize) -> Option<Self::Color> {
451        self.lookup(idx).map(|p| p.into())
452    }
453
454    /// Indicate NeuQuant implements `lookup`.
455    fn has_lookup(&self) -> bool {
456        true
457    }
458
459    #[inline(always)]
460    fn map_color(&self, color: &mut Self::Color) {
461        self.map_pixel(color.channels_mut());
462    }
463}
464
465/// Floyd-Steinberg error diffusion
466fn diffuse_err<P: Pixel<Subpixel = u8>>(pixel: &mut P, error: [i16; 3], factor: i16) {
467    for (e, c) in error.iter().zip(pixel.channels_mut().iter_mut()) {
468        *c = match <i16 as From<_>>::from(*c) + e * factor / 16 {
469            val if val < 0 => 0,
470            val if val > 0xFF => 0xFF,
471            val => val as u8,
472        }
473    }
474}
475
476macro_rules! do_dithering(
477    ($map:expr, $image:expr, $err:expr, $x:expr, $y:expr) => (
478        {
479            let old_pixel = $image[($x, $y)];
480            let new_pixel = $image.get_pixel_mut($x, $y);
481            $map.map_color(new_pixel);
482            for ((e, &old), &new) in $err.iter_mut()
483                                        .zip(old_pixel.channels().iter())
484                                        .zip(new_pixel.channels().iter())
485            {
486                *e = <i16 as From<_>>::from(old) - <i16 as From<_>>::from(new)
487            }
488        }
489    )
490);
491
492/// Reduces the colors of the image using the supplied `color_map` while applying
493/// Floyd-Steinberg dithering to improve the visual conception
494pub fn dither<Pix, Map>(image: &mut ImageBuffer<Pix, Vec<u8>>, color_map: &Map)
495where
496    Map: ColorMap<Color = Pix> + ?Sized,
497    Pix: Pixel<Subpixel = u8> + 'static,
498{
499    let (width, height) = image.dimensions();
500    let mut err: [i16; 3] = [0; 3];
501    for y in 0..height - 1 {
502        let x = 0;
503        do_dithering!(color_map, image, err, x, y);
504        diffuse_err(image.get_pixel_mut(x + 1, y), err, 7);
505        diffuse_err(image.get_pixel_mut(x, y + 1), err, 5);
506        diffuse_err(image.get_pixel_mut(x + 1, y + 1), err, 1);
507        for x in 1..width - 1 {
508            do_dithering!(color_map, image, err, x, y);
509            diffuse_err(image.get_pixel_mut(x + 1, y), err, 7);
510            diffuse_err(image.get_pixel_mut(x - 1, y + 1), err, 3);
511            diffuse_err(image.get_pixel_mut(x, y + 1), err, 5);
512            diffuse_err(image.get_pixel_mut(x + 1, y + 1), err, 1);
513        }
514        let x = width - 1;
515        do_dithering!(color_map, image, err, x, y);
516        diffuse_err(image.get_pixel_mut(x - 1, y + 1), err, 3);
517        diffuse_err(image.get_pixel_mut(x, y + 1), err, 5);
518    }
519    let y = height - 1;
520    let x = 0;
521    do_dithering!(color_map, image, err, x, y);
522    diffuse_err(image.get_pixel_mut(x + 1, y), err, 7);
523    for x in 1..width - 1 {
524        do_dithering!(color_map, image, err, x, y);
525        diffuse_err(image.get_pixel_mut(x + 1, y), err, 7);
526    }
527    let x = width - 1;
528    do_dithering!(color_map, image, err, x, y);
529}
530
531/// Reduces the colors using the supplied `color_map` and returns an image of the indices
532pub fn index_colors<Pix, Map>(
533    image: &ImageBuffer<Pix, Vec<u8>>,
534    color_map: &Map,
535) -> ImageBuffer<Luma<u8>, Vec<u8>>
536where
537    Map: ColorMap<Color = Pix> + ?Sized,
538    Pix: Pixel<Subpixel = u8> + 'static,
539{
540    // Special case, we do *not* want to copy the color space here.
541    let mut indices = ImageBuffer::new(image.width(), image.height());
542    indices.set_rgb_primaries(CicpColorPrimaries::Unspecified);
543    indices.set_transfer_function(CicpTransferCharacteristics::Unspecified);
544    for (pixel, idx) in image.pixels().zip(indices.pixels_mut()) {
545        *idx = Luma([color_map.index_of(pixel) as u8]);
546    }
547    indices
548}
549
550#[cfg(test)]
551mod test {
552
553    use super::*;
554    use crate::GrayImage;
555
556    macro_rules! assert_pixels_eq {
557        ($actual:expr, $expected:expr) => {{
558            let actual_dim = $actual.dimensions();
559            let expected_dim = $expected.dimensions();
560
561            if actual_dim != expected_dim {
562                panic!(
563                    "dimensions do not match. \
564                     actual: {:?}, expected: {:?}",
565                    actual_dim, expected_dim
566                )
567            }
568
569            let diffs = pixel_diffs($actual, $expected);
570
571            if !diffs.is_empty() {
572                let mut err = "".to_string();
573
574                let diff_messages = diffs
575                    .iter()
576                    .take(5)
577                    .map(|d| format!("\nactual: {:?}, expected {:?} ", d.0, d.1))
578                    .collect::<Vec<_>>()
579                    .join("");
580
581                err.push_str(&diff_messages);
582                panic!("pixels do not match. {:?}", err)
583            }
584        }};
585    }
586
587    #[test]
588    fn test_dither() {
589        let mut image = ImageBuffer::from_raw(2, 2, vec![127, 127, 127, 127]).unwrap();
590        let cmap = BiLevel;
591        dither(&mut image, &cmap);
592        assert_eq!(&*image, &[0, 0xFF, 0xFF, 0]);
593        assert_eq!(index_colors(&image, &cmap).into_raw(), vec![0, 1, 1, 0]);
594    }
595
596    #[test]
597    fn test_grayscale() {
598        let image: GrayImage =
599            ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap();
600
601        let expected: GrayImage =
602            ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap();
603
604        assert_pixels_eq!(&grayscale(&image), &expected);
605    }
606
607    #[test]
608    fn test_invert() {
609        let mut image: GrayImage =
610            ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap();
611
612        let expected: GrayImage =
613            ImageBuffer::from_raw(3, 2, vec![255u8, 254u8, 253u8, 245u8, 244u8, 243u8]).unwrap();
614
615        invert(&mut image);
616        assert_pixels_eq!(&image, &expected);
617    }
618    #[test]
619    fn test_brighten() {
620        let image: GrayImage =
621            ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap();
622
623        let expected: GrayImage =
624            ImageBuffer::from_raw(3, 2, vec![10u8, 11u8, 12u8, 20u8, 21u8, 22u8]).unwrap();
625
626        assert_pixels_eq!(&brighten(&image, 10), &expected);
627    }
628
629    #[test]
630    fn test_brighten_place() {
631        let mut image: GrayImage =
632            ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap();
633
634        let expected: GrayImage =
635            ImageBuffer::from_raw(3, 2, vec![10u8, 11u8, 12u8, 20u8, 21u8, 22u8]).unwrap();
636
637        brighten_in_place(&mut image, 10);
638        assert_pixels_eq!(&image, &expected);
639    }
640
641    #[allow(clippy::type_complexity)]
642    fn pixel_diffs<I, J, P>(left: &I, right: &J) -> Vec<((u32, u32, P), (u32, u32, P))>
643    where
644        I: GenericImage<Pixel = P>,
645        J: GenericImage<Pixel = P>,
646        P: Pixel + Eq,
647    {
648        left.pixels()
649            .zip(right.pixels())
650            .filter(|&(p, q)| p != q)
651            .collect::<Vec<_>>()
652    }
653}