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