1 | //! Functions and filters for the sampling of pixels. |
2 | |
3 | // See http://cs.brown.edu/courses/cs123/lectures/08_Image_Processing_IV.pdf |
4 | // for some of the theory behind image scaling and convolution |
5 | |
6 | use std::f32; |
7 | |
8 | use num_traits::{NumCast, ToPrimitive, Zero}; |
9 | |
10 | use crate::image::{GenericImage, GenericImageView}; |
11 | use crate::traits::{Enlargeable, Pixel, Primitive}; |
12 | use crate::utils::clamp; |
13 | use crate::{ImageBuffer, Rgba32FImage}; |
14 | |
15 | /// Available Sampling Filters. |
16 | /// |
17 | /// ## Examples |
18 | /// |
19 | /// To test the different sampling filters on a real example, you can find two |
20 | /// examples called |
21 | /// [`scaledown`](https://github.com/image-rs/image/tree/master/examples/scaledown) |
22 | /// and |
23 | /// [`scaleup`](https://github.com/image-rs/image/tree/master/examples/scaleup) |
24 | /// in the `examples` directory of the crate source code. |
25 | /// |
26 | /// Here is a 3.58 MiB |
27 | /// [test image](https://github.com/image-rs/image/blob/master/examples/scaledown/test.jpg) |
28 | /// that has been scaled down to 300x225 px: |
29 | /// |
30 | /// <!-- NOTE: To test new test images locally, replace the GitHub path with `../../../docs/` --> |
31 | /// <div style="display: flex; flex-wrap: wrap; align-items: flex-start;"> |
32 | /// <div style="margin: 0 8px 8px 0;"> |
33 | /// <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-near.png" title="Nearest"><br> |
34 | /// Nearest Neighbor |
35 | /// </div> |
36 | /// <div style="margin: 0 8px 8px 0;"> |
37 | /// <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-tri.png" title="Triangle"><br> |
38 | /// Linear: Triangle |
39 | /// </div> |
40 | /// <div style="margin: 0 8px 8px 0;"> |
41 | /// <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-cmr.png" title="CatmullRom"><br> |
42 | /// Cubic: Catmull-Rom |
43 | /// </div> |
44 | /// <div style="margin: 0 8px 8px 0;"> |
45 | /// <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-gauss.png" title="Gaussian"><br> |
46 | /// Gaussian |
47 | /// </div> |
48 | /// <div style="margin: 0 8px 8px 0;"> |
49 | /// <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-lcz2.png" title="Lanczos3"><br> |
50 | /// Lanczos with window 3 |
51 | /// </div> |
52 | /// </div> |
53 | /// |
54 | /// ## Speed |
55 | /// |
56 | /// Time required to create each of the examples above, tested on an Intel |
57 | /// i7-4770 CPU with Rust 1.37 in release mode: |
58 | /// |
59 | /// <table style="width: auto;"> |
60 | /// <tr> |
61 | /// <th>Nearest</th> |
62 | /// <td>31 ms</td> |
63 | /// </tr> |
64 | /// <tr> |
65 | /// <th>Triangle</th> |
66 | /// <td>414 ms</td> |
67 | /// </tr> |
68 | /// <tr> |
69 | /// <th>CatmullRom</th> |
70 | /// <td>817 ms</td> |
71 | /// </tr> |
72 | /// <tr> |
73 | /// <th>Gaussian</th> |
74 | /// <td>1180 ms</td> |
75 | /// </tr> |
76 | /// <tr> |
77 | /// <th>Lanczos3</th> |
78 | /// <td>1170 ms</td> |
79 | /// </tr> |
80 | /// </table> |
81 | #[derive (Clone, Copy, Debug, PartialEq)] |
82 | pub enum FilterType { |
83 | /// Nearest Neighbor |
84 | Nearest, |
85 | |
86 | /// Linear Filter |
87 | Triangle, |
88 | |
89 | /// Cubic Filter |
90 | CatmullRom, |
91 | |
92 | /// Gaussian Filter |
93 | Gaussian, |
94 | |
95 | /// Lanczos with window 3 |
96 | Lanczos3, |
97 | } |
98 | |
99 | /// A Representation of a separable filter. |
100 | pub(crate) struct Filter<'a> { |
101 | /// The filter's filter function. |
102 | pub(crate) kernel: Box<dyn Fn(f32) -> f32 + 'a>, |
103 | |
104 | /// The window on which this filter operates. |
105 | pub(crate) support: f32, |
106 | } |
107 | |
108 | struct FloatNearest(f32); |
109 | |
110 | // to_i64, to_u64, and to_f64 implicitly affect all other lower conversions. |
111 | // Note that to_f64 by default calls to_i64 and thus needs to be overridden. |
112 | impl ToPrimitive for FloatNearest { |
113 | // to_{i,u}64 is required, to_{i,u}{8,16} are useful. |
114 | // If a usecase for full 32 bits is found its trivial to add |
115 | fn to_i8(&self) -> Option<i8> { |
116 | self.0.round().to_i8() |
117 | } |
118 | fn to_i16(&self) -> Option<i16> { |
119 | self.0.round().to_i16() |
120 | } |
121 | fn to_i64(&self) -> Option<i64> { |
122 | self.0.round().to_i64() |
123 | } |
124 | fn to_u8(&self) -> Option<u8> { |
125 | self.0.round().to_u8() |
126 | } |
127 | fn to_u16(&self) -> Option<u16> { |
128 | self.0.round().to_u16() |
129 | } |
130 | fn to_u64(&self) -> Option<u64> { |
131 | self.0.round().to_u64() |
132 | } |
133 | fn to_f64(&self) -> Option<f64> { |
134 | self.0.to_f64() |
135 | } |
136 | } |
137 | |
138 | // sinc function: the ideal sampling filter. |
139 | fn sinc(t: f32) -> f32 { |
140 | let a: f32 = t * f32::consts::PI; |
141 | |
142 | if t == 0.0 { |
143 | 1.0 |
144 | } else { |
145 | a.sin() / a |
146 | } |
147 | } |
148 | |
149 | // lanczos kernel function. A windowed sinc function. |
150 | fn lanczos(x: f32, t: f32) -> f32 { |
151 | if x.abs() < t { |
152 | sinc(x) * sinc(x / t) |
153 | } else { |
154 | 0.0 |
155 | } |
156 | } |
157 | |
158 | // Calculate a splice based on the b and c parameters. |
159 | // from authors Mitchell and Netravali. |
160 | fn bc_cubic_spline(x: f32, b: f32, c: f32) -> f32 { |
161 | let a: f32 = x.abs(); |
162 | |
163 | let k: f32 = if a < 1.0 { |
164 | (12.0 - 9.0 * b - 6.0 * c) * a.powi(3) |
165 | + (-18.0 + 12.0 * b + 6.0 * c) * a.powi(2) |
166 | + (6.0 - 2.0 * b) |
167 | } else if a < 2.0 { |
168 | (-b - 6.0 * c) * a.powi(3) |
169 | + (6.0 * b + 30.0 * c) * a.powi(2) |
170 | + (-12.0 * b - 48.0 * c) * a |
171 | + (8.0 * b + 24.0 * c) |
172 | } else { |
173 | 0.0 |
174 | }; |
175 | |
176 | k / 6.0 |
177 | } |
178 | |
179 | /// The Gaussian Function. |
180 | /// ```r``` is the standard deviation. |
181 | pub(crate) fn gaussian(x: f32, r: f32) -> f32 { |
182 | ((2.0 * f32::consts::PI).sqrt() * r).recip() * (-x.powi(2) / (2.0 * r.powi(2))).exp() |
183 | } |
184 | |
185 | /// Calculate the lanczos kernel with a window of 3 |
186 | pub(crate) fn lanczos3_kernel(x: f32) -> f32 { |
187 | lanczos(x, t:3.0) |
188 | } |
189 | |
190 | /// Calculate the gaussian function with a |
191 | /// standard deviation of 0.5 |
192 | pub(crate) fn gaussian_kernel(x: f32) -> f32 { |
193 | gaussian(x, r:0.5) |
194 | } |
195 | |
196 | /// Calculate the Catmull-Rom cubic spline. |
197 | /// Also known as a form of `BiCubic` sampling in two dimensions. |
198 | pub(crate) fn catmullrom_kernel(x: f32) -> f32 { |
199 | bc_cubic_spline(x, b:0.0, c:0.5) |
200 | } |
201 | |
202 | /// Calculate the triangle function. |
203 | /// Also known as `BiLinear` sampling in two dimensions. |
204 | pub(crate) fn triangle_kernel(x: f32) -> f32 { |
205 | if x.abs() < 1.0 { |
206 | 1.0 - x.abs() |
207 | } else { |
208 | 0.0 |
209 | } |
210 | } |
211 | |
212 | /// Calculate the box kernel. |
213 | /// Only pixels inside the box should be considered, and those |
214 | /// contribute equally. So this method simply returns 1. |
215 | pub(crate) fn box_kernel(_x: f32) -> f32 { |
216 | 1.0 |
217 | } |
218 | |
219 | // Sample the rows of the supplied image using the provided filter. |
220 | // The height of the image remains unchanged. |
221 | // ```new_width``` is the desired width of the new image |
222 | // ```filter``` is the filter to use for sampling. |
223 | // ```image``` is not necessarily Rgba and the order of channels is passed through. |
224 | fn horizontal_sample<P, S>( |
225 | image: &Rgba32FImage, |
226 | new_width: u32, |
227 | filter: &mut Filter, |
228 | ) -> ImageBuffer<P, Vec<S>> |
229 | where |
230 | P: Pixel<Subpixel = S> + 'static, |
231 | S: Primitive + 'static, |
232 | { |
233 | let (width, height) = image.dimensions(); |
234 | let mut out = ImageBuffer::new(new_width, height); |
235 | let mut ws = Vec::new(); |
236 | |
237 | let max: f32 = NumCast::from(S::DEFAULT_MAX_VALUE).unwrap(); |
238 | let min: f32 = NumCast::from(S::DEFAULT_MIN_VALUE).unwrap(); |
239 | let ratio = width as f32 / new_width as f32; |
240 | let sratio = if ratio < 1.0 { 1.0 } else { ratio }; |
241 | let src_support = filter.support * sratio; |
242 | |
243 | for outx in 0..new_width { |
244 | // Find the point in the input image corresponding to the centre |
245 | // of the current pixel in the output image. |
246 | let inputx = (outx as f32 + 0.5) * ratio; |
247 | |
248 | // Left and right are slice bounds for the input pixels relevant |
249 | // to the output pixel we are calculating. Pixel x is relevant |
250 | // if and only if (x >= left) && (x < right). |
251 | |
252 | // Invariant: 0 <= left < right <= width |
253 | |
254 | let left = (inputx - src_support).floor() as i64; |
255 | let left = clamp(left, 0, <i64 as From<_>>::from(width) - 1) as u32; |
256 | |
257 | let right = (inputx + src_support).ceil() as i64; |
258 | let right = clamp( |
259 | right, |
260 | <i64 as From<_>>::from(left) + 1, |
261 | <i64 as From<_>>::from(width), |
262 | ) as u32; |
263 | |
264 | // Go back to left boundary of pixel, to properly compare with i |
265 | // below, as the kernel treats the centre of a pixel as 0. |
266 | let inputx = inputx - 0.5; |
267 | |
268 | ws.clear(); |
269 | let mut sum = 0.0; |
270 | for i in left..right { |
271 | let w = (filter.kernel)((i as f32 - inputx) / sratio); |
272 | ws.push(w); |
273 | sum += w; |
274 | } |
275 | ws.iter_mut().for_each(|w| *w /= sum); |
276 | |
277 | for y in 0..height { |
278 | let mut t = (0.0, 0.0, 0.0, 0.0); |
279 | |
280 | for (i, w) in ws.iter().enumerate() { |
281 | let p = image.get_pixel(left + i as u32, y); |
282 | |
283 | #[allow (deprecated)] |
284 | let vec = p.channels4(); |
285 | |
286 | t.0 += vec.0 * w; |
287 | t.1 += vec.1 * w; |
288 | t.2 += vec.2 * w; |
289 | t.3 += vec.3 * w; |
290 | } |
291 | |
292 | #[allow (deprecated)] |
293 | let t = Pixel::from_channels( |
294 | NumCast::from(FloatNearest(clamp(t.0, min, max))).unwrap(), |
295 | NumCast::from(FloatNearest(clamp(t.1, min, max))).unwrap(), |
296 | NumCast::from(FloatNearest(clamp(t.2, min, max))).unwrap(), |
297 | NumCast::from(FloatNearest(clamp(t.3, min, max))).unwrap(), |
298 | ); |
299 | |
300 | out.put_pixel(outx, y, t); |
301 | } |
302 | } |
303 | |
304 | out |
305 | } |
306 | |
307 | /// Linearly sample from an image using coordinates in [0, 1]. |
308 | pub fn sample_bilinear<P: Pixel>( |
309 | img: &impl GenericImageView<Pixel = P>, |
310 | u: f32, |
311 | v: f32, |
312 | ) -> Option<P> { |
313 | if ![u, v].iter().all(|c: &f32| (0.0..=1.0).contains(item:c)) { |
314 | return None; |
315 | } |
316 | |
317 | let (w: u32, h: u32) = img.dimensions(); |
318 | if w == 0 || h == 0 { |
319 | return None; |
320 | } |
321 | |
322 | let ui: f32 = w as f32 * u - 0.5; |
323 | let vi: f32 = h as f32 * v - 0.5; |
324 | interpolate_bilinear( |
325 | img, |
326 | x:ui.max(0.).min((w - 1) as f32), |
327 | y:vi.max(0.).min((h - 1) as f32), |
328 | ) |
329 | } |
330 | |
331 | /// Sample from an image using coordinates in [0, 1], taking the nearest coordinate. |
332 | pub fn sample_nearest<P: Pixel>( |
333 | img: &impl GenericImageView<Pixel = P>, |
334 | u: f32, |
335 | v: f32, |
336 | ) -> Option<P> { |
337 | if ![u, v].iter().all(|c: &f32| (0.0..=1.0).contains(item:c)) { |
338 | return None; |
339 | } |
340 | |
341 | let (w: u32, h: u32) = img.dimensions(); |
342 | let ui: f32 = w as f32 * u - 0.5; |
343 | let ui: f32 = ui.max(0.).min((w.saturating_sub(1)) as f32); |
344 | |
345 | let vi: f32 = h as f32 * v - 0.5; |
346 | let vi: f32 = vi.max(0.).min((h.saturating_sub(1)) as f32); |
347 | interpolate_nearest(img, x:ui, y:vi) |
348 | } |
349 | |
350 | /// Sample from an image using coordinates in [0, w-1] and [0, h-1], taking the |
351 | /// nearest pixel. |
352 | /// |
353 | /// Coordinates outside the image bounds will return `None`, however the |
354 | /// behavior for points within half a pixel of the image bounds may change in |
355 | /// the future. |
356 | pub fn interpolate_nearest<P: Pixel>( |
357 | img: &impl GenericImageView<Pixel = P>, |
358 | x: f32, |
359 | y: f32, |
360 | ) -> Option<P> { |
361 | let (w: u32, h: u32) = img.dimensions(); |
362 | if w == 0 || h == 0 { |
363 | return None; |
364 | } |
365 | if !(0.0..=((w - 1) as f32)).contains(&x) { |
366 | return None; |
367 | } |
368 | if !(0.0..=((h - 1) as f32)).contains(&y) { |
369 | return None; |
370 | } |
371 | |
372 | Some(img.get_pixel(x:x.round() as u32, y:y.round() as u32)) |
373 | } |
374 | |
375 | /// Linearly sample from an image using coordinates in [0, w-1] and [0, h-1]. |
376 | pub fn interpolate_bilinear<P: Pixel>( |
377 | img: &impl GenericImageView<Pixel = P>, |
378 | x: f32, |
379 | y: f32, |
380 | ) -> Option<P> { |
381 | // assumption needed for correctness of pixel creation |
382 | assert!(P::CHANNEL_COUNT <= 4); |
383 | |
384 | let (w, h) = img.dimensions(); |
385 | if w == 0 || h == 0 { |
386 | return None; |
387 | } |
388 | if !(0.0..=((w - 1) as f32)).contains(&x) { |
389 | return None; |
390 | } |
391 | if !(0.0..=((h - 1) as f32)).contains(&y) { |
392 | return None; |
393 | } |
394 | |
395 | // keep these as integers, for fewer FLOPs |
396 | let uf = x.floor() as u32; |
397 | let vf = y.floor() as u32; |
398 | let uc = (uf + 1).min(w - 1); |
399 | let vc = (vf + 1).min(h - 1); |
400 | |
401 | // clamp coords to the range of the image |
402 | let mut sxx = [[0.; 4]; 4]; |
403 | |
404 | // do not use Array::map, as it can be slow with high stack usage, |
405 | // for [[f32; 4]; 4]. |
406 | |
407 | // convert samples to f32 |
408 | // currently rgba is the largest one, |
409 | // so just store as many items as necessary, |
410 | // because there's not a simple way to be generic over all of them. |
411 | let mut compute = |u: u32, v: u32, i| { |
412 | let s = img.get_pixel(u, v); |
413 | for (j, c) in s.channels().iter().enumerate() { |
414 | sxx[j][i] = c.to_f32().unwrap(); |
415 | } |
416 | s |
417 | }; |
418 | |
419 | // hacky reuse since cannot construct a generic Pixel |
420 | let mut out: P = compute(uf, vf, 0); |
421 | compute(uf, vc, 1); |
422 | compute(uc, vf, 2); |
423 | compute(uc, vc, 3); |
424 | |
425 | // weights, the later two are independent from the first 2 for better vectorization. |
426 | let ufw = x - uf as f32; |
427 | let vfw = y - vf as f32; |
428 | let ucw = (uf + 1) as f32 - x; |
429 | let vcw = (vf + 1) as f32 - y; |
430 | |
431 | // https://en.wikipedia.org/wiki/Bilinear_interpolation#Weighted_mean |
432 | // the distance between pixels is 1 so there is no denominator |
433 | let wff = ucw * vcw; |
434 | let wfc = ucw * vfw; |
435 | let wcf = ufw * vcw; |
436 | let wcc = ufw * vfw; |
437 | // was originally assert, but is actually not a cheap computation |
438 | debug_assert!(f32::abs((wff + wfc + wcf + wcc) - 1.) < 1e-3); |
439 | |
440 | // hack to see if primitive is an integer or a float |
441 | let is_float = P::Subpixel::DEFAULT_MAX_VALUE.to_f32().unwrap() == 1.0; |
442 | |
443 | for (i, c) in out.channels_mut().iter_mut().enumerate() { |
444 | let v = wff * sxx[i][0] + wfc * sxx[i][1] + wcf * sxx[i][2] + wcc * sxx[i][3]; |
445 | // this rounding may introduce quantization errors, |
446 | // Specifically what is meant is that many samples may deviate |
447 | // from the mean value of the originals, but it's not possible to fix that. |
448 | *c = <P::Subpixel as NumCast>::from(if is_float { v } else { v.round() }).unwrap_or({ |
449 | if v < 0.0 { |
450 | P::Subpixel::DEFAULT_MIN_VALUE |
451 | } else { |
452 | P::Subpixel::DEFAULT_MAX_VALUE |
453 | } |
454 | }); |
455 | } |
456 | |
457 | Some(out) |
458 | } |
459 | |
460 | // Sample the columns of the supplied image using the provided filter. |
461 | // The width of the image remains unchanged. |
462 | // ```new_height``` is the desired height of the new image |
463 | // ```filter``` is the filter to use for sampling. |
464 | // The return value is not necessarily Rgba, the underlying order of channels in ```image``` is |
465 | // preserved. |
466 | fn vertical_sample<I, P, S>(image: &I, new_height: u32, filter: &mut Filter) -> Rgba32FImage |
467 | where |
468 | I: GenericImageView<Pixel = P>, |
469 | P: Pixel<Subpixel = S> + 'static, |
470 | S: Primitive + 'static, |
471 | { |
472 | let (width, height) = image.dimensions(); |
473 | let mut out = ImageBuffer::new(width, new_height); |
474 | let mut ws = Vec::new(); |
475 | |
476 | let ratio = height as f32 / new_height as f32; |
477 | let sratio = if ratio < 1.0 { 1.0 } else { ratio }; |
478 | let src_support = filter.support * sratio; |
479 | |
480 | for outy in 0..new_height { |
481 | // For an explanation of this algorithm, see the comments |
482 | // in horizontal_sample. |
483 | let inputy = (outy as f32 + 0.5) * ratio; |
484 | |
485 | let left = (inputy - src_support).floor() as i64; |
486 | let left = clamp(left, 0, <i64 as From<_>>::from(height) - 1) as u32; |
487 | |
488 | let right = (inputy + src_support).ceil() as i64; |
489 | let right = clamp( |
490 | right, |
491 | <i64 as From<_>>::from(left) + 1, |
492 | <i64 as From<_>>::from(height), |
493 | ) as u32; |
494 | |
495 | let inputy = inputy - 0.5; |
496 | |
497 | ws.clear(); |
498 | let mut sum = 0.0; |
499 | for i in left..right { |
500 | let w = (filter.kernel)((i as f32 - inputy) / sratio); |
501 | ws.push(w); |
502 | sum += w; |
503 | } |
504 | ws.iter_mut().for_each(|w| *w /= sum); |
505 | |
506 | for x in 0..width { |
507 | let mut t = (0.0, 0.0, 0.0, 0.0); |
508 | |
509 | for (i, w) in ws.iter().enumerate() { |
510 | let p = image.get_pixel(x, left + i as u32); |
511 | |
512 | #[allow (deprecated)] |
513 | let (k1, k2, k3, k4) = p.channels4(); |
514 | let vec: (f32, f32, f32, f32) = ( |
515 | NumCast::from(k1).unwrap(), |
516 | NumCast::from(k2).unwrap(), |
517 | NumCast::from(k3).unwrap(), |
518 | NumCast::from(k4).unwrap(), |
519 | ); |
520 | |
521 | t.0 += vec.0 * w; |
522 | t.1 += vec.1 * w; |
523 | t.2 += vec.2 * w; |
524 | t.3 += vec.3 * w; |
525 | } |
526 | |
527 | #[allow (deprecated)] |
528 | // This is not necessarily Rgba. |
529 | let t = Pixel::from_channels(t.0, t.1, t.2, t.3); |
530 | |
531 | out.put_pixel(x, outy, t); |
532 | } |
533 | } |
534 | |
535 | out |
536 | } |
537 | |
538 | /// Local struct for keeping track of pixel sums for fast thumbnail averaging |
539 | struct ThumbnailSum<S: Primitive + Enlargeable>(S::Larger, S::Larger, S::Larger, S::Larger); |
540 | |
541 | impl<S: Primitive + Enlargeable> ThumbnailSum<S> { |
542 | fn zeroed() -> Self { |
543 | ThumbnailSum( |
544 | S::Larger::zero(), |
545 | S::Larger::zero(), |
546 | S::Larger::zero(), |
547 | S::Larger::zero(), |
548 | ) |
549 | } |
550 | |
551 | fn sample_val(val: S) -> S::Larger { |
552 | <S::Larger as NumCast>::from(val).unwrap() |
553 | } |
554 | |
555 | fn add_pixel<P: Pixel<Subpixel = S>>(&mut self, pixel: P) { |
556 | #[allow (deprecated)] |
557 | let pixel: (S, S, S, S) = pixel.channels4(); |
558 | self.0 += Self::sample_val(pixel.0); |
559 | self.1 += Self::sample_val(pixel.1); |
560 | self.2 += Self::sample_val(pixel.2); |
561 | self.3 += Self::sample_val(pixel.3); |
562 | } |
563 | } |
564 | |
565 | /// Resize the supplied image to the specific dimensions. |
566 | /// |
567 | /// For downscaling, this method uses a fast integer algorithm where each source pixel contributes |
568 | /// to exactly one target pixel. May give aliasing artifacts if new size is close to old size. |
569 | /// |
570 | /// In case the current width is smaller than the new width or similar for the height, another |
571 | /// strategy is used instead. For each pixel in the output, a rectangular region of the input is |
572 | /// determined, just as previously. But when no input pixel is part of this region, the nearest |
573 | /// pixels are interpolated instead. |
574 | /// |
575 | /// For speed reasons, all interpolation is performed linearly over the colour values. It will not |
576 | /// take the pixel colour spaces into account. |
577 | pub fn thumbnail<I, P, S>(image: &I, new_width: u32, new_height: u32) -> ImageBuffer<P, Vec<S>> |
578 | where |
579 | I: GenericImageView<Pixel = P>, |
580 | P: Pixel<Subpixel = S> + 'static, |
581 | S: Primitive + Enlargeable + 'static, |
582 | { |
583 | let (width, height) = image.dimensions(); |
584 | let mut out = ImageBuffer::new(new_width, new_height); |
585 | if height == 0 || width == 0 { |
586 | return out; |
587 | } |
588 | |
589 | let x_ratio = width as f32 / new_width as f32; |
590 | let y_ratio = height as f32 / new_height as f32; |
591 | |
592 | for outy in 0..new_height { |
593 | let bottomf = outy as f32 * y_ratio; |
594 | let topf = bottomf + y_ratio; |
595 | |
596 | let bottom = clamp(bottomf.ceil() as u32, 0, height - 1); |
597 | let top = clamp(topf.ceil() as u32, bottom, height); |
598 | |
599 | for outx in 0..new_width { |
600 | let leftf = outx as f32 * x_ratio; |
601 | let rightf = leftf + x_ratio; |
602 | |
603 | let left = clamp(leftf.ceil() as u32, 0, width - 1); |
604 | let right = clamp(rightf.ceil() as u32, left, width); |
605 | |
606 | let avg = if bottom != top && left != right { |
607 | thumbnail_sample_block(image, left, right, bottom, top) |
608 | } else if bottom != top { |
609 | // && left == right |
610 | // In the first column we have left == 0 and right > ceil(y_scale) > 0 so this |
611 | // assertion can never trigger. |
612 | debug_assert!( |
613 | left > 0 && right > 0, |
614 | "First output column must have corresponding pixels" |
615 | ); |
616 | |
617 | let fraction_horizontal = (leftf.fract() + rightf.fract()) / 2.; |
618 | thumbnail_sample_fraction_horizontal( |
619 | image, |
620 | right - 1, |
621 | fraction_horizontal, |
622 | bottom, |
623 | top, |
624 | ) |
625 | } else if left != right { |
626 | // && bottom == top |
627 | // In the first line we have bottom == 0 and top > ceil(x_scale) > 0 so this |
628 | // assertion can never trigger. |
629 | debug_assert!( |
630 | bottom > 0 && top > 0, |
631 | "First output row must have corresponding pixels" |
632 | ); |
633 | |
634 | let fraction_vertical = (topf.fract() + bottomf.fract()) / 2.; |
635 | thumbnail_sample_fraction_vertical(image, left, right, top - 1, fraction_vertical) |
636 | } else { |
637 | // bottom == top && left == right |
638 | let fraction_horizontal = (topf.fract() + bottomf.fract()) / 2.; |
639 | let fraction_vertical = (leftf.fract() + rightf.fract()) / 2.; |
640 | |
641 | thumbnail_sample_fraction_both( |
642 | image, |
643 | right - 1, |
644 | fraction_horizontal, |
645 | top - 1, |
646 | fraction_vertical, |
647 | ) |
648 | }; |
649 | |
650 | #[allow (deprecated)] |
651 | let pixel = Pixel::from_channels(avg.0, avg.1, avg.2, avg.3); |
652 | out.put_pixel(outx, outy, pixel); |
653 | } |
654 | } |
655 | |
656 | out |
657 | } |
658 | |
659 | /// Get a pixel for a thumbnail where the input window encloses at least a full pixel. |
660 | fn thumbnail_sample_block<I, P, S>( |
661 | image: &I, |
662 | left: u32, |
663 | right: u32, |
664 | bottom: u32, |
665 | top: u32, |
666 | ) -> (S, S, S, S) |
667 | where |
668 | I: GenericImageView<Pixel = P>, |
669 | P: Pixel<Subpixel = S>, |
670 | S: Primitive + Enlargeable, |
671 | { |
672 | let mut sum: ThumbnailSum = ThumbnailSum::zeroed(); |
673 | |
674 | for y: u32 in bottom..top { |
675 | for x: u32 in left..right { |
676 | let k: P = image.get_pixel(x, y); |
677 | sum.add_pixel(k); |
678 | } |
679 | } |
680 | |
681 | let n: ::Larger = <S::Larger as NumCast>::from((right - left) * (top - bottom)).unwrap(); |
682 | let round: ::Larger = <S::Larger as NumCast>::from(n / NumCast::from(2).unwrap()).unwrap(); |
683 | ( |
684 | S::clamp_from((sum.0 + round) / n), |
685 | S::clamp_from((sum.1 + round) / n), |
686 | S::clamp_from((sum.2 + round) / n), |
687 | S::clamp_from((sum.3 + round) / n), |
688 | ) |
689 | } |
690 | |
691 | /// Get a thumbnail pixel where the input window encloses at least a vertical pixel. |
692 | fn thumbnail_sample_fraction_horizontal<I, P, S>( |
693 | image: &I, |
694 | left: u32, |
695 | fraction_horizontal: f32, |
696 | bottom: u32, |
697 | top: u32, |
698 | ) -> (S, S, S, S) |
699 | where |
700 | I: GenericImageView<Pixel = P>, |
701 | P: Pixel<Subpixel = S>, |
702 | S: Primitive + Enlargeable, |
703 | { |
704 | let fract = fraction_horizontal; |
705 | |
706 | let mut sum_left = ThumbnailSum::zeroed(); |
707 | let mut sum_right = ThumbnailSum::zeroed(); |
708 | for x in bottom..top { |
709 | let k_left = image.get_pixel(left, x); |
710 | sum_left.add_pixel(k_left); |
711 | |
712 | let k_right = image.get_pixel(left + 1, x); |
713 | sum_right.add_pixel(k_right); |
714 | } |
715 | |
716 | // Now we approximate: left/n*(1-fract) + right/n*fract |
717 | let fact_right = fract / ((top - bottom) as f32); |
718 | let fact_left = (1. - fract) / ((top - bottom) as f32); |
719 | |
720 | let mix_left_and_right = |leftv: S::Larger, rightv: S::Larger| { |
721 | <S as NumCast>::from( |
722 | fact_left * leftv.to_f32().unwrap() + fact_right * rightv.to_f32().unwrap(), |
723 | ) |
724 | .expect("Average sample value should fit into sample type" ) |
725 | }; |
726 | |
727 | ( |
728 | mix_left_and_right(sum_left.0, sum_right.0), |
729 | mix_left_and_right(sum_left.1, sum_right.1), |
730 | mix_left_and_right(sum_left.2, sum_right.2), |
731 | mix_left_and_right(sum_left.3, sum_right.3), |
732 | ) |
733 | } |
734 | |
735 | /// Get a thumbnail pixel where the input window encloses at least a horizontal pixel. |
736 | fn thumbnail_sample_fraction_vertical<I, P, S>( |
737 | image: &I, |
738 | left: u32, |
739 | right: u32, |
740 | bottom: u32, |
741 | fraction_vertical: f32, |
742 | ) -> (S, S, S, S) |
743 | where |
744 | I: GenericImageView<Pixel = P>, |
745 | P: Pixel<Subpixel = S>, |
746 | S: Primitive + Enlargeable, |
747 | { |
748 | let fract = fraction_vertical; |
749 | |
750 | let mut sum_bot = ThumbnailSum::zeroed(); |
751 | let mut sum_top = ThumbnailSum::zeroed(); |
752 | for x in left..right { |
753 | let k_bot = image.get_pixel(x, bottom); |
754 | sum_bot.add_pixel(k_bot); |
755 | |
756 | let k_top = image.get_pixel(x, bottom + 1); |
757 | sum_top.add_pixel(k_top); |
758 | } |
759 | |
760 | // Now we approximate: bot/n*fract + top/n*(1-fract) |
761 | let fact_top = fract / ((right - left) as f32); |
762 | let fact_bot = (1. - fract) / ((right - left) as f32); |
763 | |
764 | let mix_bot_and_top = |botv: S::Larger, topv: S::Larger| { |
765 | <S as NumCast>::from(fact_bot * botv.to_f32().unwrap() + fact_top * topv.to_f32().unwrap()) |
766 | .expect("Average sample value should fit into sample type" ) |
767 | }; |
768 | |
769 | ( |
770 | mix_bot_and_top(sum_bot.0, sum_top.0), |
771 | mix_bot_and_top(sum_bot.1, sum_top.1), |
772 | mix_bot_and_top(sum_bot.2, sum_top.2), |
773 | mix_bot_and_top(sum_bot.3, sum_top.3), |
774 | ) |
775 | } |
776 | |
777 | /// Get a single pixel for a thumbnail where the input window does not enclose any full pixel. |
778 | fn thumbnail_sample_fraction_both<I, P, S>( |
779 | image: &I, |
780 | left: u32, |
781 | fraction_vertical: f32, |
782 | bottom: u32, |
783 | fraction_horizontal: f32, |
784 | ) -> (S, S, S, S) |
785 | where |
786 | I: GenericImageView<Pixel = P>, |
787 | P: Pixel<Subpixel = S>, |
788 | S: Primitive + Enlargeable, |
789 | { |
790 | #[allow (deprecated)] |
791 | let k_bl = image.get_pixel(left, bottom).channels4(); |
792 | #[allow (deprecated)] |
793 | let k_tl = image.get_pixel(left, bottom + 1).channels4(); |
794 | #[allow (deprecated)] |
795 | let k_br = image.get_pixel(left + 1, bottom).channels4(); |
796 | #[allow (deprecated)] |
797 | let k_tr = image.get_pixel(left + 1, bottom + 1).channels4(); |
798 | |
799 | let frac_v = fraction_vertical; |
800 | let frac_h = fraction_horizontal; |
801 | |
802 | let fact_tr = frac_v * frac_h; |
803 | let fact_tl = frac_v * (1. - frac_h); |
804 | let fact_br = (1. - frac_v) * frac_h; |
805 | let fact_bl = (1. - frac_v) * (1. - frac_h); |
806 | |
807 | let mix = |br: S, tr: S, bl: S, tl: S| { |
808 | <S as NumCast>::from( |
809 | fact_br * br.to_f32().unwrap() |
810 | + fact_tr * tr.to_f32().unwrap() |
811 | + fact_bl * bl.to_f32().unwrap() |
812 | + fact_tl * tl.to_f32().unwrap(), |
813 | ) |
814 | .expect("Average sample value should fit into sample type" ) |
815 | }; |
816 | |
817 | ( |
818 | mix(k_br.0, k_tr.0, k_bl.0, k_tl.0), |
819 | mix(k_br.1, k_tr.1, k_bl.1, k_tl.1), |
820 | mix(k_br.2, k_tr.2, k_bl.2, k_tl.2), |
821 | mix(k_br.3, k_tr.3, k_bl.3, k_tl.3), |
822 | ) |
823 | } |
824 | |
825 | /// Perform a 3x3 box filter on the supplied image. |
826 | /// ```kernel``` is an array of the filter weights of length 9. |
827 | pub fn filter3x3<I, P, S>(image: &I, kernel: &[f32]) -> ImageBuffer<P, Vec<S>> |
828 | where |
829 | I: GenericImageView<Pixel = P>, |
830 | P: Pixel<Subpixel = S> + 'static, |
831 | S: Primitive + 'static, |
832 | { |
833 | // The kernel's input positions relative to the current pixel. |
834 | let taps: &[(isize, isize)] = &[ |
835 | (-1, -1), |
836 | (0, -1), |
837 | (1, -1), |
838 | (-1, 0), |
839 | (0, 0), |
840 | (1, 0), |
841 | (-1, 1), |
842 | (0, 1), |
843 | (1, 1), |
844 | ]; |
845 | |
846 | let (width, height) = image.dimensions(); |
847 | |
848 | let mut out = ImageBuffer::new(width, height); |
849 | |
850 | let max = S::DEFAULT_MAX_VALUE; |
851 | let max: f32 = NumCast::from(max).unwrap(); |
852 | |
853 | let sum = match kernel.iter().fold(0.0, |s, &item| s + item) { |
854 | x if x == 0.0 => 1.0, |
855 | sum => sum, |
856 | }; |
857 | let sum = (sum, sum, sum, sum); |
858 | |
859 | for y in 1..height - 1 { |
860 | for x in 1..width - 1 { |
861 | let mut t = (0.0, 0.0, 0.0, 0.0); |
862 | |
863 | // TODO: There is no need to recalculate the kernel for each pixel. |
864 | // Only a subtract and addition is needed for pixels after the first |
865 | // in each row. |
866 | for (&k, &(a, b)) in kernel.iter().zip(taps.iter()) { |
867 | let k = (k, k, k, k); |
868 | let x0 = x as isize + a; |
869 | let y0 = y as isize + b; |
870 | |
871 | let p = image.get_pixel(x0 as u32, y0 as u32); |
872 | |
873 | #[allow (deprecated)] |
874 | let (k1, k2, k3, k4) = p.channels4(); |
875 | |
876 | let vec: (f32, f32, f32, f32) = ( |
877 | NumCast::from(k1).unwrap(), |
878 | NumCast::from(k2).unwrap(), |
879 | NumCast::from(k3).unwrap(), |
880 | NumCast::from(k4).unwrap(), |
881 | ); |
882 | |
883 | t.0 += vec.0 * k.0; |
884 | t.1 += vec.1 * k.1; |
885 | t.2 += vec.2 * k.2; |
886 | t.3 += vec.3 * k.3; |
887 | } |
888 | |
889 | let (t1, t2, t3, t4) = (t.0 / sum.0, t.1 / sum.1, t.2 / sum.2, t.3 / sum.3); |
890 | |
891 | #[allow (deprecated)] |
892 | let t = Pixel::from_channels( |
893 | NumCast::from(clamp(t1, 0.0, max)).unwrap(), |
894 | NumCast::from(clamp(t2, 0.0, max)).unwrap(), |
895 | NumCast::from(clamp(t3, 0.0, max)).unwrap(), |
896 | NumCast::from(clamp(t4, 0.0, max)).unwrap(), |
897 | ); |
898 | |
899 | out.put_pixel(x, y, t); |
900 | } |
901 | } |
902 | |
903 | out |
904 | } |
905 | |
906 | /// Resize the supplied image to the specified dimensions. |
907 | /// ```nwidth``` and ```nheight``` are the new dimensions. |
908 | /// ```filter``` is the sampling filter to use. |
909 | pub fn resize<I: GenericImageView>( |
910 | image: &I, |
911 | nwidth: u32, |
912 | nheight: u32, |
913 | filter: FilterType, |
914 | ) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>> |
915 | where |
916 | I::Pixel: 'static, |
917 | <I::Pixel as Pixel>::Subpixel: 'static, |
918 | { |
919 | // check if the new dimensions are the same as the old. if they are, make a copy instead of resampling |
920 | if (nwidth, nheight) == image.dimensions() { |
921 | let mut tmp = ImageBuffer::new(image.width(), image.height()); |
922 | tmp.copy_from(image, 0, 0).unwrap(); |
923 | return tmp; |
924 | } |
925 | |
926 | let mut method = match filter { |
927 | FilterType::Nearest => Filter { |
928 | kernel: Box::new(box_kernel), |
929 | support: 0.0, |
930 | }, |
931 | FilterType::Triangle => Filter { |
932 | kernel: Box::new(triangle_kernel), |
933 | support: 1.0, |
934 | }, |
935 | FilterType::CatmullRom => Filter { |
936 | kernel: Box::new(catmullrom_kernel), |
937 | support: 2.0, |
938 | }, |
939 | FilterType::Gaussian => Filter { |
940 | kernel: Box::new(gaussian_kernel), |
941 | support: 3.0, |
942 | }, |
943 | FilterType::Lanczos3 => Filter { |
944 | kernel: Box::new(lanczos3_kernel), |
945 | support: 3.0, |
946 | }, |
947 | }; |
948 | |
949 | // Note: tmp is not necessarily actually Rgba |
950 | let tmp: Rgba32FImage = vertical_sample(image, nheight, &mut method); |
951 | horizontal_sample(&tmp, nwidth, &mut method) |
952 | } |
953 | |
954 | /// Performs a Gaussian blur on the supplied image. |
955 | /// ```sigma``` is a measure of how much to blur by. |
956 | pub fn blur<I: GenericImageView>( |
957 | image: &I, |
958 | sigma: f32, |
959 | ) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>> |
960 | where |
961 | I::Pixel: 'static, |
962 | { |
963 | let sigma: f32 = if sigma <= 0.0 { 1.0 } else { sigma }; |
964 | |
965 | let mut method: Filter<'_> = Filter { |
966 | kernel: Box::new(|x: f32| gaussian(x, r:sigma)), |
967 | support: 2.0 * sigma, |
968 | }; |
969 | |
970 | let (width: u32, height: u32) = image.dimensions(); |
971 | |
972 | // Keep width and height the same for horizontal and |
973 | // vertical sampling. |
974 | // Note: tmp is not necessarily actually Rgba |
975 | let tmp: Rgba32FImage = vertical_sample(image, new_height:height, &mut method); |
976 | horizontal_sample(&tmp, new_width:width, &mut method) |
977 | } |
978 | |
979 | /// Performs an unsharpen mask on the supplied image. |
980 | /// ```sigma``` is the amount to blur the image by. |
981 | /// ```threshold``` is the threshold for minimal brightness change that will be sharpened. |
982 | /// |
983 | /// See <https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking> |
984 | pub fn unsharpen<I, P, S>(image: &I, sigma: f32, threshold: i32) -> ImageBuffer<P, Vec<S>> |
985 | where |
986 | I: GenericImageView<Pixel = P>, |
987 | P: Pixel<Subpixel = S> + 'static, |
988 | S: Primitive + 'static, |
989 | { |
990 | let mut tmp = blur(image, sigma); |
991 | |
992 | let max = S::DEFAULT_MAX_VALUE; |
993 | let max: i32 = NumCast::from(max).unwrap(); |
994 | let (width, height) = image.dimensions(); |
995 | |
996 | for y in 0..height { |
997 | for x in 0..width { |
998 | let a = image.get_pixel(x, y); |
999 | let b = tmp.get_pixel_mut(x, y); |
1000 | |
1001 | let p = a.map2(b, |c, d| { |
1002 | let ic: i32 = NumCast::from(c).unwrap(); |
1003 | let id: i32 = NumCast::from(d).unwrap(); |
1004 | |
1005 | let diff = ic - id; |
1006 | |
1007 | if diff.abs() > threshold { |
1008 | let e = clamp(ic + diff, 0, max); // FIXME what does this do for f32? clamp 0-1 integers?? |
1009 | |
1010 | NumCast::from(e).unwrap() |
1011 | } else { |
1012 | c |
1013 | } |
1014 | }); |
1015 | |
1016 | *b = p; |
1017 | } |
1018 | } |
1019 | |
1020 | tmp |
1021 | } |
1022 | |
1023 | #[cfg (test)] |
1024 | mod tests { |
1025 | use super::{resize, sample_bilinear, sample_nearest, FilterType}; |
1026 | use crate::{GenericImageView, ImageBuffer, RgbImage}; |
1027 | #[cfg (feature = "benchmarks" )] |
1028 | use test ; |
1029 | |
1030 | #[bench ] |
1031 | #[cfg (all(feature = "benchmarks" , feature = "png" ))] |
1032 | fn bench_resize(b: &mut test::Bencher) { |
1033 | use std::path::Path; |
1034 | let img = crate::open(Path::new("./examples/fractal.png" )).unwrap(); |
1035 | b.iter(|| { |
1036 | test::black_box(resize(&img, 200, 200, FilterType::Nearest)); |
1037 | }); |
1038 | b.bytes = 800 * 800 * 3 + 200 * 200 * 3; |
1039 | } |
1040 | |
1041 | #[test ] |
1042 | #[cfg (feature = "png" )] |
1043 | fn test_resize_same_size() { |
1044 | use std::path::Path; |
1045 | let img = crate::open(Path::new("./examples/fractal.png" )).unwrap(); |
1046 | let resize = img.resize(img.width(), img.height(), FilterType::Triangle); |
1047 | assert!(img.pixels().eq(resize.pixels())) |
1048 | } |
1049 | |
1050 | #[test ] |
1051 | #[cfg (feature = "png" )] |
1052 | fn test_sample_bilinear() { |
1053 | use std::path::Path; |
1054 | let img = crate::open(Path::new("./examples/fractal.png" )).unwrap(); |
1055 | assert!(sample_bilinear(&img, 0., 0.).is_some()); |
1056 | assert!(sample_bilinear(&img, 1., 0.).is_some()); |
1057 | assert!(sample_bilinear(&img, 0., 1.).is_some()); |
1058 | assert!(sample_bilinear(&img, 1., 1.).is_some()); |
1059 | assert!(sample_bilinear(&img, 0.5, 0.5).is_some()); |
1060 | |
1061 | assert!(sample_bilinear(&img, 1.2, 0.5).is_none()); |
1062 | assert!(sample_bilinear(&img, 0.5, 1.2).is_none()); |
1063 | assert!(sample_bilinear(&img, 1.2, 1.2).is_none()); |
1064 | |
1065 | assert!(sample_bilinear(&img, -0.1, 0.2).is_none()); |
1066 | assert!(sample_bilinear(&img, 0.2, -0.1).is_none()); |
1067 | assert!(sample_bilinear(&img, -0.1, -0.1).is_none()); |
1068 | } |
1069 | #[test ] |
1070 | #[cfg (feature = "png" )] |
1071 | fn test_sample_nearest() { |
1072 | use std::path::Path; |
1073 | let img = crate::open(Path::new("./examples/fractal.png" )).unwrap(); |
1074 | assert!(sample_nearest(&img, 0., 0.).is_some()); |
1075 | assert!(sample_nearest(&img, 1., 0.).is_some()); |
1076 | assert!(sample_nearest(&img, 0., 1.).is_some()); |
1077 | assert!(sample_nearest(&img, 1., 1.).is_some()); |
1078 | assert!(sample_nearest(&img, 0.5, 0.5).is_some()); |
1079 | |
1080 | assert!(sample_nearest(&img, 1.2, 0.5).is_none()); |
1081 | assert!(sample_nearest(&img, 0.5, 1.2).is_none()); |
1082 | assert!(sample_nearest(&img, 1.2, 1.2).is_none()); |
1083 | |
1084 | assert!(sample_nearest(&img, -0.1, 0.2).is_none()); |
1085 | assert!(sample_nearest(&img, 0.2, -0.1).is_none()); |
1086 | assert!(sample_nearest(&img, -0.1, -0.1).is_none()); |
1087 | } |
1088 | #[test ] |
1089 | fn test_sample_bilinear_correctness() { |
1090 | use crate::Rgba; |
1091 | let img = ImageBuffer::from_fn(2, 2, |x, y| match (x, y) { |
1092 | (0, 0) => Rgba([255, 0, 0, 0]), |
1093 | (0, 1) => Rgba([0, 255, 0, 0]), |
1094 | (1, 0) => Rgba([0, 0, 255, 0]), |
1095 | (1, 1) => Rgba([0, 0, 0, 255]), |
1096 | _ => panic!(), |
1097 | }); |
1098 | assert_eq!(sample_bilinear(&img, 0.5, 0.5), Some(Rgba([64; 4]))); |
1099 | assert_eq!(sample_bilinear(&img, 0.0, 0.0), Some(Rgba([255, 0, 0, 0]))); |
1100 | assert_eq!(sample_bilinear(&img, 0.0, 1.0), Some(Rgba([0, 255, 0, 0]))); |
1101 | assert_eq!(sample_bilinear(&img, 1.0, 0.0), Some(Rgba([0, 0, 255, 0]))); |
1102 | assert_eq!(sample_bilinear(&img, 1.0, 1.0), Some(Rgba([0, 0, 0, 255]))); |
1103 | |
1104 | assert_eq!( |
1105 | sample_bilinear(&img, 0.5, 0.0), |
1106 | Some(Rgba([128, 0, 128, 0])) |
1107 | ); |
1108 | assert_eq!( |
1109 | sample_bilinear(&img, 0.0, 0.5), |
1110 | Some(Rgba([128, 128, 0, 0])) |
1111 | ); |
1112 | assert_eq!( |
1113 | sample_bilinear(&img, 0.5, 1.0), |
1114 | Some(Rgba([0, 128, 0, 128])) |
1115 | ); |
1116 | assert_eq!( |
1117 | sample_bilinear(&img, 1.0, 0.5), |
1118 | Some(Rgba([0, 0, 128, 128])) |
1119 | ); |
1120 | } |
1121 | #[bench ] |
1122 | #[cfg (feature = "benchmarks" )] |
1123 | fn bench_sample_bilinear(b: &mut test::Bencher) { |
1124 | use crate::Rgba; |
1125 | let img = ImageBuffer::from_fn(2, 2, |x, y| match (x, y) { |
1126 | (0, 0) => Rgba([255, 0, 0, 0]), |
1127 | (0, 1) => Rgba([0, 255, 0, 0]), |
1128 | (1, 0) => Rgba([0, 0, 255, 0]), |
1129 | (1, 1) => Rgba([0, 0, 0, 255]), |
1130 | _ => panic!(), |
1131 | }); |
1132 | b.iter(|| { |
1133 | sample_bilinear(&img, test::black_box(0.5), test::black_box(0.5)); |
1134 | }); |
1135 | } |
1136 | #[test ] |
1137 | fn test_sample_nearest_correctness() { |
1138 | use crate::Rgba; |
1139 | let img = ImageBuffer::from_fn(2, 2, |x, y| match (x, y) { |
1140 | (0, 0) => Rgba([255, 0, 0, 0]), |
1141 | (0, 1) => Rgba([0, 255, 0, 0]), |
1142 | (1, 0) => Rgba([0, 0, 255, 0]), |
1143 | (1, 1) => Rgba([0, 0, 0, 255]), |
1144 | _ => panic!(), |
1145 | }); |
1146 | |
1147 | assert_eq!(sample_nearest(&img, 0.0, 0.0), Some(Rgba([255, 0, 0, 0]))); |
1148 | assert_eq!(sample_nearest(&img, 0.0, 1.0), Some(Rgba([0, 255, 0, 0]))); |
1149 | assert_eq!(sample_nearest(&img, 1.0, 0.0), Some(Rgba([0, 0, 255, 0]))); |
1150 | assert_eq!(sample_nearest(&img, 1.0, 1.0), Some(Rgba([0, 0, 0, 255]))); |
1151 | |
1152 | assert_eq!(sample_nearest(&img, 0.5, 0.5), Some(Rgba([0, 0, 0, 255]))); |
1153 | assert_eq!(sample_nearest(&img, 0.5, 0.0), Some(Rgba([0, 0, 255, 0]))); |
1154 | assert_eq!(sample_nearest(&img, 0.0, 0.5), Some(Rgba([0, 255, 0, 0]))); |
1155 | assert_eq!(sample_nearest(&img, 0.5, 1.0), Some(Rgba([0, 0, 0, 255]))); |
1156 | assert_eq!(sample_nearest(&img, 1.0, 0.5), Some(Rgba([0, 0, 0, 255]))); |
1157 | } |
1158 | |
1159 | #[bench ] |
1160 | #[cfg (all(feature = "benchmarks" , feature = "tiff" ))] |
1161 | fn bench_resize_same_size(b: &mut test::Bencher) { |
1162 | let path = concat!( |
1163 | env!("CARGO_MANIFEST_DIR" ), |
1164 | "/tests/images/tiff/testsuite/mandrill.tiff" |
1165 | ); |
1166 | let image = crate::open(path).unwrap(); |
1167 | b.iter(|| { |
1168 | test::black_box(image.resize(image.width(), image.height(), FilterType::CatmullRom)); |
1169 | }); |
1170 | b.bytes = (image.width() * image.height() * 3) as u64; |
1171 | } |
1172 | |
1173 | #[test ] |
1174 | fn test_issue_186() { |
1175 | let img: RgbImage = ImageBuffer::new(100, 100); |
1176 | let _ = resize(&img, 50, 50, FilterType::Lanczos3); |
1177 | } |
1178 | |
1179 | #[bench ] |
1180 | #[cfg (all(feature = "benchmarks" , feature = "tiff" ))] |
1181 | fn bench_thumbnail(b: &mut test::Bencher) { |
1182 | let path = concat!( |
1183 | env!("CARGO_MANIFEST_DIR" ), |
1184 | "/tests/images/tiff/testsuite/mandrill.tiff" |
1185 | ); |
1186 | let image = crate::open(path).unwrap(); |
1187 | b.iter(|| { |
1188 | test::black_box(image.thumbnail(256, 256)); |
1189 | }); |
1190 | b.bytes = 512 * 512 * 4 + 256 * 256 * 4; |
1191 | } |
1192 | |
1193 | #[bench ] |
1194 | #[cfg (all(feature = "benchmarks" , feature = "tiff" ))] |
1195 | fn bench_thumbnail_upsize(b: &mut test::Bencher) { |
1196 | let path = concat!( |
1197 | env!("CARGO_MANIFEST_DIR" ), |
1198 | "/tests/images/tiff/testsuite/mandrill.tiff" |
1199 | ); |
1200 | let image = crate::open(path).unwrap().thumbnail(256, 256); |
1201 | b.iter(|| { |
1202 | test::black_box(image.thumbnail(512, 512)); |
1203 | }); |
1204 | b.bytes = 512 * 512 * 4 + 256 * 256 * 4; |
1205 | } |
1206 | |
1207 | #[bench ] |
1208 | #[cfg (all(feature = "benchmarks" , feature = "tiff" ))] |
1209 | fn bench_thumbnail_upsize_irregular(b: &mut test::Bencher) { |
1210 | let path = concat!( |
1211 | env!("CARGO_MANIFEST_DIR" ), |
1212 | "/tests/images/tiff/testsuite/mandrill.tiff" |
1213 | ); |
1214 | let image = crate::open(path).unwrap().thumbnail(193, 193); |
1215 | b.iter(|| { |
1216 | test::black_box(image.thumbnail(256, 256)); |
1217 | }); |
1218 | b.bytes = 193 * 193 * 4 + 256 * 256 * 4; |
1219 | } |
1220 | |
1221 | #[test ] |
1222 | #[cfg (feature = "png" )] |
1223 | fn resize_transparent_image() { |
1224 | use super::FilterType::{CatmullRom, Gaussian, Lanczos3, Nearest, Triangle}; |
1225 | use crate::imageops::crop_imm; |
1226 | use crate::RgbaImage; |
1227 | |
1228 | fn assert_resize(image: &RgbaImage, filter: FilterType) { |
1229 | let resized = resize(image, 16, 16, filter); |
1230 | let cropped = crop_imm(&resized, 5, 5, 6, 6).to_image(); |
1231 | for pixel in cropped.pixels() { |
1232 | let alpha = pixel.0[3]; |
1233 | assert!( |
1234 | alpha != 254 && alpha != 253, |
1235 | "alpha value: {}, {:?}" , |
1236 | alpha, |
1237 | filter |
1238 | ); |
1239 | } |
1240 | } |
1241 | |
1242 | let path = concat!( |
1243 | env!("CARGO_MANIFEST_DIR" ), |
1244 | "/tests/images/png/transparency/tp1n3p08.png" |
1245 | ); |
1246 | let img = crate::open(path).unwrap(); |
1247 | let rgba8 = img.as_rgba8().unwrap(); |
1248 | let filters = &[Nearest, Triangle, CatmullRom, Gaussian, Lanczos3]; |
1249 | for filter in filters { |
1250 | assert_resize(rgba8, *filter); |
1251 | } |
1252 | } |
1253 | |
1254 | #[test ] |
1255 | fn bug_1600() { |
1256 | let image = crate::RgbaImage::from_raw(629, 627, vec![255; 629 * 627 * 4]).unwrap(); |
1257 | let result = resize(&image, 22, 22, FilterType::Lanczos3); |
1258 | assert!(result.into_raw().into_iter().any(|c| c != 0)); |
1259 | } |
1260 | } |
1261 | |