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