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