image/imageops/
colorops.rs

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