1 | //! Image Processing Functions |
2 | use std::cmp; |
3 | |
4 | use crate::image::{GenericImage, GenericImageView, SubImage}; |
5 | use crate::traits::{Lerp, Pixel, Primitive}; |
6 | |
7 | pub use self::sample::FilterType; |
8 | |
9 | pub use self::sample::FilterType::{CatmullRom, Gaussian, Lanczos3, Nearest, Triangle}; |
10 | |
11 | /// Affine transformations |
12 | pub use self::affine::{ |
13 | flip_horizontal, flip_horizontal_in, flip_horizontal_in_place, flip_vertical, flip_vertical_in, |
14 | flip_vertical_in_place, rotate180, rotate180_in, rotate180_in_place, rotate270, rotate270_in, |
15 | rotate90, rotate90_in, |
16 | }; |
17 | |
18 | /// Image sampling |
19 | pub use self::sample::{ |
20 | blur, filter3x3, interpolate_bilinear, interpolate_nearest, resize, sample_bilinear, |
21 | sample_nearest, thumbnail, unsharpen, |
22 | }; |
23 | |
24 | /// Color operations |
25 | pub use self::colorops::{ |
26 | brighten, contrast, dither, grayscale, grayscale_alpha, grayscale_with_type, |
27 | grayscale_with_type_alpha, huerotate, index_colors, invert, BiLevel, ColorMap, |
28 | }; |
29 | |
30 | mod affine; |
31 | // Public only because of Rust bug: |
32 | // https://github.com/rust-lang/rust/issues/18241 |
33 | pub mod colorops; |
34 | mod fast_blur; |
35 | mod sample; |
36 | |
37 | pub use fast_blur::fast_blur; |
38 | |
39 | /// Return a mutable view into an image |
40 | /// The coordinates set the position of the top left corner of the crop. |
41 | pub fn crop<I: GenericImageView>( |
42 | image: &mut I, |
43 | x: u32, |
44 | y: u32, |
45 | width: u32, |
46 | height: u32, |
47 | ) -> SubImage<&mut I> { |
48 | let (x: u32, y: u32, width: u32, height: u32) = crop_dimms(image, x, y, width, height); |
49 | SubImage::new(image, x, y, width, height) |
50 | } |
51 | |
52 | /// Return an immutable view into an image |
53 | /// The coordinates set the position of the top left corner of the crop. |
54 | pub fn crop_imm<I: GenericImageView>( |
55 | image: &I, |
56 | x: u32, |
57 | y: u32, |
58 | width: u32, |
59 | height: u32, |
60 | ) -> SubImage<&I> { |
61 | let (x: u32, y: u32, width: u32, height: u32) = crop_dimms(image, x, y, width, height); |
62 | SubImage::new(image, x, y, width, height) |
63 | } |
64 | |
65 | fn crop_dimms<I: GenericImageView>( |
66 | image: &I, |
67 | x: u32, |
68 | y: u32, |
69 | width: u32, |
70 | height: u32, |
71 | ) -> (u32, u32, u32, u32) { |
72 | let (iwidth: u32, iheight: u32) = image.dimensions(); |
73 | |
74 | let x: u32 = cmp::min(v1:x, v2:iwidth); |
75 | let y: u32 = cmp::min(v1:y, v2:iheight); |
76 | |
77 | let height: u32 = cmp::min(v1:height, v2:iheight - y); |
78 | let width: u32 = cmp::min(v1:width, v2:iwidth - x); |
79 | |
80 | (x, y, width, height) |
81 | } |
82 | |
83 | /// Calculate the region that can be copied from top to bottom. |
84 | /// |
85 | /// Given image size of bottom and top image, and a point at which we want to place the top image |
86 | /// onto the bottom image, how large can we be? Have to wary of the following issues: |
87 | /// * Top might be larger than bottom |
88 | /// * Overflows in the computation |
89 | /// * Coordinates could be completely out of bounds |
90 | /// |
91 | /// The main idea is to make use of inequalities provided by the nature of `saturating_add` and |
92 | /// `saturating_sub`. These intrinsically validate that all resulting coordinates will be in bounds |
93 | /// for both images. |
94 | /// |
95 | /// We want that all these coordinate accesses are safe: |
96 | /// 1. `bottom.get_pixel(x + [0..x_range), y + [0..y_range))` |
97 | /// 2. `top.get_pixel([0..x_range), [0..y_range))` |
98 | /// |
99 | /// Proof that the function provides the necessary bounds for width. Note that all unaugmented math |
100 | /// operations are to be read in standard arithmetic, not integer arithmetic. Since no direct |
101 | /// integer arithmetic occurs in the implementation, this is unambiguous. |
102 | /// |
103 | /// ```text |
104 | /// Three short notes/lemmata: |
105 | /// - Iff `(a - b) <= 0` then `a.saturating_sub(b) = 0` |
106 | /// - Iff `(a - b) >= 0` then `a.saturating_sub(b) = a - b` |
107 | /// - If `a <= c` then `a.saturating_sub(b) <= c.saturating_sub(b)` |
108 | /// |
109 | /// 1.1 We show that if `bottom_width <= x`, then `x_range = 0` therefore `x + [0..x_range)` is empty. |
110 | /// |
111 | /// x_range |
112 | /// = (top_width.saturating_add(x).min(bottom_width)).saturating_sub(x) |
113 | /// <= bottom_width.saturating_sub(x) |
114 | /// |
115 | /// bottom_width <= x |
116 | /// <==> bottom_width - x <= 0 |
117 | /// <==> bottom_width.saturating_sub(x) = 0 |
118 | /// ==> x_range <= 0 |
119 | /// ==> x_range = 0 |
120 | /// |
121 | /// 1.2 If `x < bottom_width` then `x + x_range < bottom_width` |
122 | /// |
123 | /// x + x_range |
124 | /// <= x + bottom_width.saturating_sub(x) |
125 | /// = x + (bottom_width - x) |
126 | /// = bottom_width |
127 | /// |
128 | /// 2. We show that `x_range <= top_width` |
129 | /// |
130 | /// x_range |
131 | /// = (top_width.saturating_add(x).min(bottom_width)).saturating_sub(x) |
132 | /// <= top_width.saturating_add(x).saturating_sub(x) |
133 | /// <= (top_wdith + x).saturating_sub(x) |
134 | /// = top_width (due to `top_width >= 0` and `x >= 0`) |
135 | /// ``` |
136 | /// |
137 | /// Proof is the same for height. |
138 | #[must_use ] |
139 | pub fn overlay_bounds( |
140 | (bottom_width: u32, bottom_height: u32): (u32, u32), |
141 | (top_width: u32, top_height: u32): (u32, u32), |
142 | x: u32, |
143 | y: u32, |
144 | ) -> (u32, u32) { |
145 | let x_range: u32 = top_widthu32 |
146 | .saturating_add(x) // Calculate max coordinate |
147 | .min(bottom_width) // Restrict to lower width |
148 | .saturating_sub(x); // Determinate length from start `x` |
149 | let y_range: u32 = top_heightu32 |
150 | .saturating_add(y) |
151 | .min(bottom_height) |
152 | .saturating_sub(y); |
153 | (x_range, y_range) |
154 | } |
155 | |
156 | /// Calculate the region that can be copied from top to bottom. |
157 | /// |
158 | /// Given image size of bottom and top image, and a point at which we want to place the top image |
159 | /// onto the bottom image, how large can we be? Have to wary of the following issues: |
160 | /// * Top might be larger than bottom |
161 | /// * Overflows in the computation |
162 | /// * Coordinates could be completely out of bounds |
163 | /// |
164 | /// The returned value is of the form: |
165 | /// |
166 | /// `(origin_bottom_x, origin_bottom_y, origin_top_x, origin_top_y, x_range, y_range)` |
167 | /// |
168 | /// The main idea is to do computations on i64's and then clamp to image dimensions. |
169 | /// In particular, we want to ensure that all these coordinate accesses are safe: |
170 | /// 1. `bottom.get_pixel(origin_bottom_x + [0..x_range), origin_bottom_y + [0..y_range))` |
171 | /// 2. `top.get_pixel(origin_top_y + [0..x_range), origin_top_y + [0..y_range))` |
172 | fn overlay_bounds_ext( |
173 | (bottom_width: u32, bottom_height: u32): (u32, u32), |
174 | (top_width: u32, top_height: u32): (u32, u32), |
175 | x: i64, |
176 | y: i64, |
177 | ) -> (u32, u32, u32, u32, u32, u32) { |
178 | // Return a predictable value if the two images don't overlap at all. |
179 | if x > i64::from(bottom_width) |
180 | || y > i64::from(bottom_height) |
181 | || x.saturating_add(i64::from(top_width)) <= 0 |
182 | || y.saturating_add(i64::from(top_height)) <= 0 |
183 | { |
184 | return (0, 0, 0, 0, 0, 0); |
185 | } |
186 | |
187 | // Find the maximum x and y coordinates in terms of the bottom image. |
188 | let max_x = x.saturating_add(i64::from(top_width)); |
189 | let max_y = y.saturating_add(i64::from(top_height)); |
190 | |
191 | // Clip the origin and maximum coordinates to the bounds of the bottom image. |
192 | // Casting to a u32 is safe because both 0 and `bottom_{width,height}` fit |
193 | // into 32-bits. |
194 | let max_inbounds_x = max_x.clamp(0, i64::from(bottom_width)) as u32; |
195 | let max_inbounds_y = max_y.clamp(0, i64::from(bottom_height)) as u32; |
196 | let origin_bottom_x = x.clamp(0, i64::from(bottom_width)) as u32; |
197 | let origin_bottom_y = y.clamp(0, i64::from(bottom_height)) as u32; |
198 | |
199 | // The range is the difference between the maximum inbounds coordinates and |
200 | // the clipped origin. Unchecked subtraction is safe here because both are |
201 | // always positive and `max_inbounds_{x,y}` >= `origin_{x,y}` due to |
202 | // `top_{width,height}` being >= 0. |
203 | let x_range = max_inbounds_x - origin_bottom_x; |
204 | let y_range = max_inbounds_y - origin_bottom_y; |
205 | |
206 | // If x (or y) is negative, then the origin of the top image is shifted by -x (or -y). |
207 | let origin_top_x = x.saturating_mul(-1).clamp(0, i64::from(top_width)) as u32; |
208 | let origin_top_y = y.saturating_mul(-1).clamp(0, i64::from(top_height)) as u32; |
209 | |
210 | ( |
211 | origin_bottom_x, |
212 | origin_bottom_y, |
213 | origin_top_x, |
214 | origin_top_y, |
215 | x_range, |
216 | y_range, |
217 | ) |
218 | } |
219 | |
220 | /// Overlay an image at a given coordinate (x, y) |
221 | pub fn overlay<I, J>(bottom: &mut I, top: &J, x: i64, y: i64) |
222 | where |
223 | I: GenericImage, |
224 | J: GenericImageView<Pixel = I::Pixel>, |
225 | { |
226 | let bottom_dims: (u32, u32) = bottom.dimensions(); |
227 | let top_dims: (u32, u32) = top.dimensions(); |
228 | |
229 | // Crop our top image if we're going out of bounds |
230 | let (origin_bottom_x: u32, origin_bottom_y: u32, origin_top_x: u32, origin_top_y: u32, range_width: u32, range_height: u32) = |
231 | overlay_bounds_ext(bottom_dims, top_dims, x, y); |
232 | |
233 | for y: u32 in 0..range_height { |
234 | for x: u32 in 0..range_width { |
235 | let p: ::Pixel = top.get_pixel(x:origin_top_x + x, y:origin_top_y + y); |
236 | let mut bottom_pixel: ::Pixel = bottom.get_pixel(x:origin_bottom_x + x, y:origin_bottom_y + y); |
237 | bottom_pixel.blend(&p); |
238 | |
239 | bottom.put_pixel(x:origin_bottom_x + x, y:origin_bottom_y + y, bottom_pixel); |
240 | } |
241 | } |
242 | } |
243 | |
244 | /// Tile an image by repeating it multiple times |
245 | /// |
246 | /// # Examples |
247 | /// ```no_run |
248 | /// use image::RgbaImage; |
249 | /// |
250 | /// let mut img = RgbaImage::new(1920, 1080); |
251 | /// let tile = image::open("tile.png" ).unwrap(); |
252 | /// |
253 | /// image::imageops::tile(&mut img, &tile); |
254 | /// img.save("tiled_wallpaper.png" ).unwrap(); |
255 | /// ``` |
256 | pub fn tile<I, J>(bottom: &mut I, top: &J) |
257 | where |
258 | I: GenericImage, |
259 | J: GenericImageView<Pixel = I::Pixel>, |
260 | { |
261 | for x: u32 in (0..bottom.width()).step_by(step:top.width() as usize) { |
262 | for y: u32 in (0..bottom.height()).step_by(step:top.height() as usize) { |
263 | overlay(bottom, top, x:i64::from(x), y:i64::from(y)); |
264 | } |
265 | } |
266 | } |
267 | |
268 | /// Fill the image with a linear vertical gradient |
269 | /// |
270 | /// This function assumes a linear color space. |
271 | /// |
272 | /// # Examples |
273 | /// ```no_run |
274 | /// use image::{Rgba, RgbaImage, Pixel}; |
275 | /// |
276 | /// let mut img = RgbaImage::new(100, 100); |
277 | /// let start = Rgba::from_slice(&[0, 128, 0, 0]); |
278 | /// let end = Rgba::from_slice(&[255, 255, 255, 255]); |
279 | /// |
280 | /// image::imageops::vertical_gradient(&mut img, start, end); |
281 | /// img.save("vertical_gradient.png" ).unwrap(); |
282 | pub fn vertical_gradient<S, P, I>(img: &mut I, start: &P, stop: &P) |
283 | where |
284 | I: GenericImage<Pixel = P>, |
285 | P: Pixel<Subpixel = S> + 'static, |
286 | S: Primitive + Lerp + 'static, |
287 | { |
288 | for y: u32 in 0..img.height() { |
289 | let pixel: P = start.map2(other:stop, |a: S, b: S| { |
290 | let y: ::Ratio = <S::Ratio as num_traits::NumCast>::from(y).unwrap(); |
291 | let height: ::Ratio = <S::Ratio as num_traits::NumCast>::from(img.height() - 1).unwrap(); |
292 | S::lerp(a, b, ratio:y / height) |
293 | }); |
294 | |
295 | for x: u32 in 0..img.width() { |
296 | img.put_pixel(x, y, pixel); |
297 | } |
298 | } |
299 | } |
300 | |
301 | /// Fill the image with a linear horizontal gradient |
302 | /// |
303 | /// This function assumes a linear color space. |
304 | /// |
305 | /// # Examples |
306 | /// ```no_run |
307 | /// use image::{Rgba, RgbaImage, Pixel}; |
308 | /// |
309 | /// let mut img = RgbaImage::new(100, 100); |
310 | /// let start = Rgba::from_slice(&[0, 128, 0, 0]); |
311 | /// let end = Rgba::from_slice(&[255, 255, 255, 255]); |
312 | /// |
313 | /// image::imageops::horizontal_gradient(&mut img, start, end); |
314 | /// img.save("horizontal_gradient.png" ).unwrap(); |
315 | pub fn horizontal_gradient<S, P, I>(img: &mut I, start: &P, stop: &P) |
316 | where |
317 | I: GenericImage<Pixel = P>, |
318 | P: Pixel<Subpixel = S> + 'static, |
319 | S: Primitive + Lerp + 'static, |
320 | { |
321 | for x: u32 in 0..img.width() { |
322 | let pixel: P = start.map2(other:stop, |a: S, b: S| { |
323 | let x: ::Ratio = <S::Ratio as num_traits::NumCast>::from(x).unwrap(); |
324 | let width: ::Ratio = <S::Ratio as num_traits::NumCast>::from(img.width() - 1).unwrap(); |
325 | S::lerp(a, b, ratio:x / width) |
326 | }); |
327 | |
328 | for y: u32 in 0..img.height() { |
329 | img.put_pixel(x, y, pixel); |
330 | } |
331 | } |
332 | } |
333 | |
334 | /// Replace the contents of an image at a given coordinate (x, y) |
335 | pub fn replace<I, J>(bottom: &mut I, top: &J, x: i64, y: i64) |
336 | where |
337 | I: GenericImage, |
338 | J: GenericImageView<Pixel = I::Pixel>, |
339 | { |
340 | let bottom_dims: (u32, u32) = bottom.dimensions(); |
341 | let top_dims: (u32, u32) = top.dimensions(); |
342 | |
343 | // Crop our top image if we're going out of bounds |
344 | let (origin_bottom_x: u32, origin_bottom_y: u32, origin_top_x: u32, origin_top_y: u32, range_width: u32, range_height: u32) = |
345 | overlay_bounds_ext(bottom_dims, top_dims, x, y); |
346 | |
347 | for y: u32 in 0..range_height { |
348 | for x: u32 in 0..range_width { |
349 | let p: ::Pixel = top.get_pixel(x:origin_top_x + x, y:origin_top_y + y); |
350 | bottom.put_pixel(x:origin_bottom_x + x, y:origin_bottom_y + y, pixel:p); |
351 | } |
352 | } |
353 | } |
354 | |
355 | #[cfg (test)] |
356 | mod tests { |
357 | |
358 | use super::*; |
359 | use crate::color::Rgb; |
360 | use crate::GrayAlphaImage; |
361 | use crate::GrayImage; |
362 | use crate::ImageBuffer; |
363 | use crate::RgbImage; |
364 | use crate::RgbaImage; |
365 | |
366 | #[test ] |
367 | fn test_overlay_bounds_ext() { |
368 | assert_eq!( |
369 | overlay_bounds_ext((10, 10), (10, 10), 0, 0), |
370 | (0, 0, 0, 0, 10, 10) |
371 | ); |
372 | assert_eq!( |
373 | overlay_bounds_ext((10, 10), (10, 10), 1, 0), |
374 | (1, 0, 0, 0, 9, 10) |
375 | ); |
376 | assert_eq!( |
377 | overlay_bounds_ext((10, 10), (10, 10), 0, 11), |
378 | (0, 0, 0, 0, 0, 0) |
379 | ); |
380 | assert_eq!( |
381 | overlay_bounds_ext((10, 10), (10, 10), -1, 0), |
382 | (0, 0, 1, 0, 9, 10) |
383 | ); |
384 | assert_eq!( |
385 | overlay_bounds_ext((10, 10), (10, 10), -10, 0), |
386 | (0, 0, 0, 0, 0, 0) |
387 | ); |
388 | assert_eq!( |
389 | overlay_bounds_ext((10, 10), (10, 10), 1i64 << 50, 0), |
390 | (0, 0, 0, 0, 0, 0) |
391 | ); |
392 | assert_eq!( |
393 | overlay_bounds_ext((10, 10), (10, 10), -(1i64 << 50), 0), |
394 | (0, 0, 0, 0, 0, 0) |
395 | ); |
396 | assert_eq!( |
397 | overlay_bounds_ext((10, 10), (u32::MAX, 10), 10 - i64::from(u32::MAX), 0), |
398 | (0, 0, u32::MAX - 10, 0, 10, 10) |
399 | ); |
400 | } |
401 | |
402 | #[test ] |
403 | /// Test that images written into other images works |
404 | fn test_image_in_image() { |
405 | let mut target = ImageBuffer::new(32, 32); |
406 | let source = ImageBuffer::from_pixel(16, 16, Rgb([255u8, 0, 0])); |
407 | overlay(&mut target, &source, 0, 0); |
408 | assert!(*target.get_pixel(0, 0) == Rgb([255u8, 0, 0])); |
409 | assert!(*target.get_pixel(15, 0) == Rgb([255u8, 0, 0])); |
410 | assert!(*target.get_pixel(16, 0) == Rgb([0u8, 0, 0])); |
411 | assert!(*target.get_pixel(0, 15) == Rgb([255u8, 0, 0])); |
412 | assert!(*target.get_pixel(0, 16) == Rgb([0u8, 0, 0])); |
413 | } |
414 | |
415 | #[test ] |
416 | /// Test that images written outside of a frame doesn't blow up |
417 | fn test_image_in_image_outside_of_bounds() { |
418 | let mut target = ImageBuffer::new(32, 32); |
419 | let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0])); |
420 | overlay(&mut target, &source, 1, 1); |
421 | assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0])); |
422 | assert!(*target.get_pixel(1, 1) == Rgb([255u8, 0, 0])); |
423 | assert!(*target.get_pixel(31, 31) == Rgb([255u8, 0, 0])); |
424 | } |
425 | |
426 | #[test ] |
427 | /// Test that images written to coordinates out of the frame doesn't blow up |
428 | /// (issue came up in #848) |
429 | fn test_image_outside_image_no_wrap_around() { |
430 | let mut target = ImageBuffer::new(32, 32); |
431 | let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0])); |
432 | overlay(&mut target, &source, 33, 33); |
433 | assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0])); |
434 | assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0])); |
435 | assert!(*target.get_pixel(31, 31) == Rgb([0, 0, 0])); |
436 | } |
437 | |
438 | #[test ] |
439 | /// Test that images written to coordinates with overflow works |
440 | fn test_image_coordinate_overflow() { |
441 | let mut target = ImageBuffer::new(16, 16); |
442 | let source = ImageBuffer::from_pixel(32, 32, Rgb([255u8, 0, 0])); |
443 | // Overflows to 'sane' coordinates but top is larger than bot. |
444 | overlay( |
445 | &mut target, |
446 | &source, |
447 | i64::from(u32::MAX - 31), |
448 | i64::from(u32::MAX - 31), |
449 | ); |
450 | assert!(*target.get_pixel(0, 0) == Rgb([0, 0, 0])); |
451 | assert!(*target.get_pixel(1, 1) == Rgb([0, 0, 0])); |
452 | assert!(*target.get_pixel(15, 15) == Rgb([0, 0, 0])); |
453 | } |
454 | |
455 | use super::{horizontal_gradient, vertical_gradient}; |
456 | |
457 | #[test ] |
458 | /// Test that horizontal gradients are correctly generated |
459 | fn test_image_horizontal_gradient_limits() { |
460 | let mut img = ImageBuffer::new(100, 1); |
461 | |
462 | let start = Rgb([0u8, 128, 0]); |
463 | let end = Rgb([255u8, 255, 255]); |
464 | |
465 | horizontal_gradient(&mut img, &start, &end); |
466 | |
467 | assert_eq!(img.get_pixel(0, 0), &start); |
468 | assert_eq!(img.get_pixel(img.width() - 1, 0), &end); |
469 | } |
470 | |
471 | #[test ] |
472 | /// Test that vertical gradients are correctly generated |
473 | fn test_image_vertical_gradient_limits() { |
474 | let mut img = ImageBuffer::new(1, 100); |
475 | |
476 | let start = Rgb([0u8, 128, 0]); |
477 | let end = Rgb([255u8, 255, 255]); |
478 | |
479 | vertical_gradient(&mut img, &start, &end); |
480 | |
481 | assert_eq!(img.get_pixel(0, 0), &start); |
482 | assert_eq!(img.get_pixel(0, img.height() - 1), &end); |
483 | } |
484 | |
485 | #[test ] |
486 | /// Test blur doesn't panic when passed 0.0 |
487 | fn test_blur_zero() { |
488 | let image = RgbaImage::new(50, 50); |
489 | let _ = blur(&image, 0.0); |
490 | } |
491 | |
492 | #[test ] |
493 | /// Test fast blur doesn't panic when passed 0.0 |
494 | fn test_fast_blur_zero() { |
495 | let image = RgbaImage::new(50, 50); |
496 | let _ = fast_blur(&image, 0.0); |
497 | } |
498 | |
499 | #[test ] |
500 | /// Test fast blur doesn't panic when passed negative numbers |
501 | fn test_fast_blur_negative() { |
502 | let image = RgbaImage::new(50, 50); |
503 | let _ = fast_blur(&image, -1.0); |
504 | } |
505 | |
506 | #[test ] |
507 | /// Test fast blur doesn't panic when sigma produces boxes larger than the image |
508 | fn test_fast_large_sigma() { |
509 | let image = RgbaImage::new(1, 1); |
510 | let _ = fast_blur(&image, 50.0); |
511 | } |
512 | |
513 | #[test ] |
514 | /// Test blur doesn't panic when passed an empty image (any direction) |
515 | fn test_fast_blur_empty() { |
516 | let image = RgbaImage::new(0, 0); |
517 | let _ = fast_blur(&image, 1.0); |
518 | let image = RgbaImage::new(20, 0); |
519 | let _ = fast_blur(&image, 1.0); |
520 | let image = RgbaImage::new(0, 20); |
521 | let _ = fast_blur(&image, 1.0); |
522 | } |
523 | |
524 | #[test ] |
525 | /// Test fast blur works with 3 channels |
526 | fn test_fast_blur_3_channels() { |
527 | let image = RgbImage::new(50, 50); |
528 | let _ = fast_blur(&image, 1.0); |
529 | } |
530 | |
531 | #[test ] |
532 | /// Test fast blur works with 2 channels |
533 | fn test_fast_blur_2_channels() { |
534 | let image = GrayAlphaImage::new(50, 50); |
535 | let _ = fast_blur(&image, 1.0); |
536 | } |
537 | |
538 | #[test ] |
539 | /// Test fast blur works with 1 channel |
540 | fn test_fast_blur_1_channels() { |
541 | let image = GrayImage::new(50, 50); |
542 | let _ = fast_blur(&image, 1.0); |
543 | } |
544 | |
545 | #[test ] |
546 | #[cfg (feature = "tiff" )] |
547 | fn fast_blur_approximates_gaussian_blur_well() { |
548 | let path = concat!( |
549 | env!("CARGO_MANIFEST_DIR" ), |
550 | "/tests/images/tiff/testsuite/rgb-3c-16b.tiff" |
551 | ); |
552 | let image = crate::open(path).unwrap(); |
553 | let image_blurred_gauss = image.blur(50.0).to_rgb8(); |
554 | let image_blurred_gauss_samples = image_blurred_gauss.as_flat_samples(); |
555 | let image_blurred_gauss_bytes = image_blurred_gauss_samples.as_slice(); |
556 | let image_blurred_fast = image.fast_blur(50.0).to_rgb8(); |
557 | let image_blurred_fast_samples = image_blurred_fast.as_flat_samples(); |
558 | let image_blurred_fast_bytes = image_blurred_fast_samples.as_slice(); |
559 | |
560 | let error = image_blurred_gauss_bytes |
561 | .iter() |
562 | .zip(image_blurred_fast_bytes.iter()) |
563 | .map(|(a, b)| ((f32::from(*a) - f32::from(*b)) / f32::from(*a))) |
564 | .sum::<f32>() |
565 | / (image_blurred_gauss_bytes.len() as f32); |
566 | assert!(error < 0.05); |
567 | } |
568 | } |
569 | |