image/imageops/
colorops.rs

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