/src/image/src/imageops/sample.rs
Line | Count | Source |
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 num_traits::{NumCast, ToPrimitive, Zero}; |
7 | | use std::f32; |
8 | | use std::ops::Mul; |
9 | | |
10 | | use crate::imageops::filter_1d::{ |
11 | | filter_2d_sep_la, filter_2d_sep_la_f32, filter_2d_sep_la_u16, filter_2d_sep_plane, |
12 | | filter_2d_sep_plane_f32, filter_2d_sep_plane_u16, filter_2d_sep_rgb, filter_2d_sep_rgb_f32, |
13 | | filter_2d_sep_rgb_u16, filter_2d_sep_rgba, filter_2d_sep_rgba_f32, filter_2d_sep_rgba_u16, |
14 | | FilterImageSize, |
15 | | }; |
16 | | use crate::images::buffer::{Gray16Image, GrayAlpha16Image, Rgb16Image, Rgba16Image}; |
17 | | use crate::traits::{Enlargeable, Pixel, Primitive}; |
18 | | use crate::utils::clamp; |
19 | | use crate::{ |
20 | | DynamicImage, GenericImage, GenericImageView, GrayAlphaImage, GrayImage, ImageBuffer, |
21 | | Rgb32FImage, RgbImage, Rgba32FImage, RgbaImage, |
22 | | }; |
23 | | |
24 | | /// Available Sampling Filters. |
25 | | /// |
26 | | /// ## Examples |
27 | | /// |
28 | | /// To test the different sampling filters on a real example, you can find two |
29 | | /// examples called |
30 | | /// [`scaledown`](https://github.com/image-rs/image/tree/main/examples/scaledown) |
31 | | /// and |
32 | | /// [`scaleup`](https://github.com/image-rs/image/tree/main/examples/scaleup) |
33 | | /// in the `examples` directory of the crate source code. |
34 | | /// |
35 | | /// Here is a 3.58 MiB |
36 | | /// [test image](https://github.com/image-rs/image/blob/main/examples/scaledown/test.jpg) |
37 | | /// that has been scaled down to 300x225 px: |
38 | | /// |
39 | | /// <!-- NOTE: To test new test images locally, replace the GitHub path with `../../../docs/` --> |
40 | | /// <div style="display: flex; flex-wrap: wrap; align-items: flex-start;"> |
41 | | /// <div style="margin: 0 8px 8px 0;"> |
42 | | /// <img src="https://raw.githubusercontent.com/image-rs/image/main/examples/scaledown/scaledown-test-near.png" title="Nearest"><br> |
43 | | /// Nearest Neighbor |
44 | | /// </div> |
45 | | /// <div style="margin: 0 8px 8px 0;"> |
46 | | /// <img src="https://raw.githubusercontent.com/image-rs/image/main/examples/scaledown/scaledown-test-tri.png" title="Triangle"><br> |
47 | | /// Linear: Triangle |
48 | | /// </div> |
49 | | /// <div style="margin: 0 8px 8px 0;"> |
50 | | /// <img src="https://raw.githubusercontent.com/image-rs/image/main/examples/scaledown/scaledown-test-cmr.png" title="CatmullRom"><br> |
51 | | /// Cubic: Catmull-Rom |
52 | | /// </div> |
53 | | /// <div style="margin: 0 8px 8px 0;"> |
54 | | /// <img src="https://raw.githubusercontent.com/image-rs/image/main/examples/scaledown/scaledown-test-gauss.png" title="Gaussian"><br> |
55 | | /// Gaussian |
56 | | /// </div> |
57 | | /// <div style="margin: 0 8px 8px 0;"> |
58 | | /// <img src="https://raw.githubusercontent.com/image-rs/image/main/examples/scaledown/scaledown-test-lcz2.png" title="Lanczos3"><br> |
59 | | /// Lanczos with window 3 |
60 | | /// </div> |
61 | | /// </div> |
62 | | /// |
63 | | /// ## Speed |
64 | | /// |
65 | | /// Time required to create each of the examples above, tested on an Intel |
66 | | /// i7-4770 CPU with Rust 1.37 in release mode: |
67 | | /// |
68 | | /// <table style="width: auto;"> |
69 | | /// <tr> |
70 | | /// <th>Nearest</th> |
71 | | /// <td>31 ms</td> |
72 | | /// </tr> |
73 | | /// <tr> |
74 | | /// <th>Triangle</th> |
75 | | /// <td>414 ms</td> |
76 | | /// </tr> |
77 | | /// <tr> |
78 | | /// <th>CatmullRom</th> |
79 | | /// <td>817 ms</td> |
80 | | /// </tr> |
81 | | /// <tr> |
82 | | /// <th>Gaussian</th> |
83 | | /// <td>1180 ms</td> |
84 | | /// </tr> |
85 | | /// <tr> |
86 | | /// <th>Lanczos3</th> |
87 | | /// <td>1170 ms</td> |
88 | | /// </tr> |
89 | | /// </table> |
90 | | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
91 | | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
92 | | pub enum FilterType { |
93 | | /// Nearest Neighbor |
94 | | Nearest, |
95 | | |
96 | | /// Linear Filter |
97 | | Triangle, |
98 | | |
99 | | /// Cubic Filter |
100 | | CatmullRom, |
101 | | |
102 | | /// Gaussian Filter |
103 | | Gaussian, |
104 | | |
105 | | /// Lanczos with window 3 |
106 | | Lanczos3, |
107 | | } |
108 | | |
109 | | /// A Representation of a separable filter. |
110 | | pub(crate) struct Filter<'a> { |
111 | | /// The filter's filter function. |
112 | | pub(crate) kernel: Box<dyn Fn(f32) -> f32 + 'a>, |
113 | | |
114 | | /// The window on which this filter operates. |
115 | | pub(crate) support: f32, |
116 | | } |
117 | | |
118 | | struct FloatNearest(f32); |
119 | | |
120 | | // to_i64, to_u64, and to_f64 implicitly affect all other lower conversions. |
121 | | // Note that to_f64 by default calls to_i64 and thus needs to be overridden. |
122 | | impl ToPrimitive for FloatNearest { |
123 | | // to_{i,u}64 is required, to_{i,u}{8,16} are useful. |
124 | | // If a usecase for full 32 bits is found its trivial to add |
125 | 0 | fn to_i8(&self) -> Option<i8> { |
126 | 0 | self.0.round().to_i8() |
127 | 0 | } |
128 | 0 | fn to_i16(&self) -> Option<i16> { |
129 | 0 | self.0.round().to_i16() |
130 | 0 | } |
131 | 0 | fn to_i64(&self) -> Option<i64> { |
132 | 0 | self.0.round().to_i64() |
133 | 0 | } |
134 | 0 | fn to_u8(&self) -> Option<u8> { |
135 | 0 | self.0.round().to_u8() |
136 | 0 | } |
137 | 0 | fn to_u16(&self) -> Option<u16> { |
138 | 0 | self.0.round().to_u16() |
139 | 0 | } |
140 | 0 | fn to_u64(&self) -> Option<u64> { |
141 | 0 | self.0.round().to_u64() |
142 | 0 | } |
143 | 0 | fn to_f64(&self) -> Option<f64> { |
144 | 0 | self.0.to_f64() |
145 | 0 | } |
146 | | } |
147 | | |
148 | | // sinc function: the ideal sampling filter. |
149 | 0 | fn sinc(t: f32) -> f32 { |
150 | 0 | let a = t * f32::consts::PI; |
151 | | |
152 | 0 | if t == 0.0 { |
153 | 0 | 1.0 |
154 | | } else { |
155 | 0 | a.sin() / a |
156 | | } |
157 | 0 | } |
158 | | |
159 | | // lanczos kernel function. A windowed sinc function. |
160 | 0 | fn lanczos(x: f32, t: f32) -> f32 { |
161 | 0 | if x.abs() < t { |
162 | 0 | sinc(x) * sinc(x / t) |
163 | | } else { |
164 | 0 | 0.0 |
165 | | } |
166 | 0 | } |
167 | | |
168 | | // Calculate a splice based on the b and c parameters. |
169 | | // from authors Mitchell and Netravali. |
170 | 0 | fn bc_cubic_spline(x: f32, b: f32, c: f32) -> f32 { |
171 | 0 | let a = x.abs(); |
172 | | |
173 | 0 | let k = if a < 1.0 { |
174 | 0 | (12.0 - 9.0 * b - 6.0 * c) * a.powi(3) |
175 | 0 | + (-18.0 + 12.0 * b + 6.0 * c) * a.powi(2) |
176 | 0 | + (6.0 - 2.0 * b) |
177 | 0 | } else if a < 2.0 { |
178 | 0 | (-b - 6.0 * c) * a.powi(3) |
179 | 0 | + (6.0 * b + 30.0 * c) * a.powi(2) |
180 | 0 | + (-12.0 * b - 48.0 * c) * a |
181 | 0 | + (8.0 * b + 24.0 * c) |
182 | | } else { |
183 | 0 | 0.0 |
184 | | }; |
185 | | |
186 | 0 | k / 6.0 |
187 | 0 | } |
188 | | |
189 | | /// The Gaussian Function. |
190 | | /// ```r``` is the standard deviation. |
191 | 0 | pub(crate) fn gaussian(x: f32, r: f32) -> f32 { |
192 | 0 | ((2.0 * f32::consts::PI).sqrt() * r).recip() * (-x.powi(2) / (2.0 * r.powi(2))).exp() |
193 | 0 | } |
194 | | |
195 | | /// Calculate the lanczos kernel with a window of 3 |
196 | 0 | pub(crate) fn lanczos3_kernel(x: f32) -> f32 { |
197 | 0 | lanczos(x, 3.0) |
198 | 0 | } |
199 | | |
200 | | /// Calculate the gaussian function with a |
201 | | /// standard deviation of 0.5 |
202 | 0 | pub(crate) fn gaussian_kernel(x: f32) -> f32 { |
203 | 0 | gaussian(x, 0.5) |
204 | 0 | } |
205 | | |
206 | | /// Calculate the Catmull-Rom cubic spline. |
207 | | /// Also known as a form of `BiCubic` sampling in two dimensions. |
208 | 0 | pub(crate) fn catmullrom_kernel(x: f32) -> f32 { |
209 | 0 | bc_cubic_spline(x, 0.0, 0.5) |
210 | 0 | } |
211 | | |
212 | | /// Calculate the triangle function. |
213 | | /// Also known as `BiLinear` sampling in two dimensions. |
214 | 0 | pub(crate) fn triangle_kernel(x: f32) -> f32 { |
215 | 0 | if x.abs() < 1.0 { |
216 | 0 | 1.0 - x.abs() |
217 | | } else { |
218 | 0 | 0.0 |
219 | | } |
220 | 0 | } |
221 | | |
222 | | /// Calculate the box kernel. |
223 | | /// Only pixels inside the box should be considered, and those |
224 | | /// contribute equally. So this method simply returns 1. |
225 | 0 | pub(crate) fn box_kernel(_x: f32) -> f32 { |
226 | 0 | 1.0 |
227 | 0 | } |
228 | | |
229 | | // Sample the rows of the supplied image using the provided filter. |
230 | | // The height of the image remains unchanged. |
231 | | // ```new_width``` is the desired width of the new image |
232 | | // ```filter``` is the filter to use for sampling. |
233 | | // ```image``` is not necessarily Rgba and the order of channels is passed through. |
234 | | // |
235 | | // Note: if an empty image is passed in, panics unless the image is truly empty. |
236 | 0 | fn horizontal_sample<P, S>( |
237 | 0 | image: &Rgba32FImage, |
238 | 0 | new_width: u32, |
239 | 0 | filter: &mut Filter, |
240 | 0 | ) -> ImageBuffer<P, Vec<S>> |
241 | 0 | where |
242 | 0 | P: Pixel<Subpixel = S> + 'static, |
243 | 0 | S: Primitive + 'static, |
244 | | { |
245 | 0 | let (width, height) = image.dimensions(); |
246 | | // This is protection against a memory usage similar to #2340. See `vertical_sample`. |
247 | 0 | assert!( |
248 | | // Checks the implication: (width == 0) -> (height == 0) |
249 | 0 | width != 0 || height == 0, |
250 | 0 | "Unexpected prior allocation size. This case should have been handled by the caller" |
251 | | ); |
252 | | |
253 | 0 | let mut out = ImageBuffer::new(new_width, height); |
254 | 0 | out.copy_color_space_from(image); |
255 | 0 | let mut ws = Vec::new(); |
256 | | |
257 | 0 | let max: f32 = NumCast::from(S::DEFAULT_MAX_VALUE).unwrap(); |
258 | 0 | let min: f32 = NumCast::from(S::DEFAULT_MIN_VALUE).unwrap(); |
259 | 0 | let ratio = width as f32 / new_width as f32; |
260 | 0 | let sratio = if ratio < 1.0 { 1.0 } else { ratio }; |
261 | 0 | let src_support = filter.support * sratio; |
262 | | |
263 | 0 | for outx in 0..new_width { |
264 | | // Find the point in the input image corresponding to the centre |
265 | | // of the current pixel in the output image. |
266 | 0 | let inputx = (outx as f32 + 0.5) * ratio; |
267 | | |
268 | | // Left and right are slice bounds for the input pixels relevant |
269 | | // to the output pixel we are calculating. Pixel x is relevant |
270 | | // if and only if (x >= left) && (x < right). |
271 | | |
272 | | // Invariant: 0 <= left < right <= width |
273 | | |
274 | 0 | let left = (inputx - src_support).floor() as i64; |
275 | 0 | let left = clamp(left, 0, <i64 as From<_>>::from(width) - 1) as u32; |
276 | | |
277 | 0 | let right = (inputx + src_support).ceil() as i64; |
278 | 0 | let right = clamp( |
279 | 0 | right, |
280 | 0 | <i64 as From<_>>::from(left) + 1, |
281 | 0 | <i64 as From<_>>::from(width), |
282 | 0 | ) as u32; |
283 | | |
284 | | // Go back to left boundary of pixel, to properly compare with i |
285 | | // below, as the kernel treats the centre of a pixel as 0. |
286 | 0 | let inputx = inputx - 0.5; |
287 | | |
288 | 0 | ws.clear(); |
289 | 0 | let mut sum = 0.0; |
290 | 0 | for i in left..right { |
291 | 0 | let w = (filter.kernel)((i as f32 - inputx) / sratio); |
292 | 0 | ws.push(w); |
293 | 0 | sum += w; |
294 | 0 | } |
295 | 0 | for w in ws.iter_mut() { |
296 | 0 | *w /= sum; |
297 | 0 | } |
298 | | |
299 | 0 | for y in 0..height { |
300 | 0 | let mut t = (0.0, 0.0, 0.0, 0.0); |
301 | | |
302 | 0 | for (i, w) in ws.iter().enumerate() { |
303 | 0 | let p = image.get_pixel(left + i as u32, y); |
304 | 0 |
|
305 | 0 | #[allow(deprecated)] |
306 | 0 | let vec = p.channels4(); |
307 | 0 |
|
308 | 0 | t.0 += vec.0 * w; |
309 | 0 | t.1 += vec.1 * w; |
310 | 0 | t.2 += vec.2 * w; |
311 | 0 | t.3 += vec.3 * w; |
312 | 0 | } |
313 | | |
314 | | #[allow(deprecated)] |
315 | 0 | let t = Pixel::from_channels( |
316 | 0 | NumCast::from(FloatNearest(clamp(t.0, min, max))).unwrap(), |
317 | 0 | NumCast::from(FloatNearest(clamp(t.1, min, max))).unwrap(), |
318 | 0 | NumCast::from(FloatNearest(clamp(t.2, min, max))).unwrap(), |
319 | 0 | NumCast::from(FloatNearest(clamp(t.3, min, max))).unwrap(), |
320 | | ); |
321 | | |
322 | 0 | out.put_pixel(outx, y, t); |
323 | | } |
324 | | } |
325 | | |
326 | 0 | out |
327 | 0 | } |
328 | | |
329 | | /// Linearly sample from an image using coordinates in [0, 1]. |
330 | 0 | pub fn sample_bilinear<P: Pixel>( |
331 | 0 | img: &impl GenericImageView<Pixel = P>, |
332 | 0 | u: f32, |
333 | 0 | v: f32, |
334 | 0 | ) -> Option<P> { |
335 | 0 | if ![u, v].iter().all(|c| (0.0..=1.0).contains(c)) { |
336 | 0 | return None; |
337 | 0 | } |
338 | | |
339 | 0 | let (w, h) = img.dimensions(); |
340 | 0 | if w == 0 || h == 0 { |
341 | 0 | return None; |
342 | 0 | } |
343 | | |
344 | 0 | let ui = w as f32 * u - 0.5; |
345 | 0 | let vi = h as f32 * v - 0.5; |
346 | 0 | interpolate_bilinear( |
347 | 0 | img, |
348 | 0 | ui.max(0.).min((w - 1) as f32), |
349 | 0 | vi.max(0.).min((h - 1) as f32), |
350 | | ) |
351 | 0 | } |
352 | | |
353 | | /// Sample from an image using coordinates in [0, 1], taking the nearest coordinate. |
354 | 0 | pub fn sample_nearest<P: Pixel>( |
355 | 0 | img: &impl GenericImageView<Pixel = P>, |
356 | 0 | u: f32, |
357 | 0 | v: f32, |
358 | 0 | ) -> Option<P> { |
359 | 0 | if ![u, v].iter().all(|c| (0.0..=1.0).contains(c)) { |
360 | 0 | return None; |
361 | 0 | } |
362 | | |
363 | 0 | let (w, h) = img.dimensions(); |
364 | 0 | let ui = w as f32 * u - 0.5; |
365 | 0 | let ui = ui.max(0.).min((w.saturating_sub(1)) as f32); |
366 | | |
367 | 0 | let vi = h as f32 * v - 0.5; |
368 | 0 | let vi = vi.max(0.).min((h.saturating_sub(1)) as f32); |
369 | 0 | interpolate_nearest(img, ui, vi) |
370 | 0 | } |
371 | | |
372 | | /// Sample from an image using coordinates in [0, w-1] and [0, h-1], taking the |
373 | | /// nearest pixel. |
374 | | /// |
375 | | /// Coordinates outside the image bounds will return `None`, however the |
376 | | /// behavior for points within half a pixel of the image bounds may change in |
377 | | /// the future. |
378 | 0 | pub fn interpolate_nearest<P: Pixel>( |
379 | 0 | img: &impl GenericImageView<Pixel = P>, |
380 | 0 | x: f32, |
381 | 0 | y: f32, |
382 | 0 | ) -> Option<P> { |
383 | 0 | let (w, h) = img.dimensions(); |
384 | 0 | if w == 0 || h == 0 { |
385 | 0 | return None; |
386 | 0 | } |
387 | 0 | if !(0.0..=((w - 1) as f32)).contains(&x) { |
388 | 0 | return None; |
389 | 0 | } |
390 | 0 | if !(0.0..=((h - 1) as f32)).contains(&y) { |
391 | 0 | return None; |
392 | 0 | } |
393 | | |
394 | 0 | Some(img.get_pixel(x.round() as u32, y.round() as u32)) |
395 | 0 | } |
396 | | |
397 | | /// Linearly sample from an image using coordinates in [0, w-1] and [0, h-1]. |
398 | 0 | pub fn interpolate_bilinear<P: Pixel>( |
399 | 0 | img: &impl GenericImageView<Pixel = P>, |
400 | 0 | x: f32, |
401 | 0 | y: f32, |
402 | 0 | ) -> Option<P> { |
403 | | // assumption needed for correctness of pixel creation |
404 | 0 | assert!(P::CHANNEL_COUNT <= 4); |
405 | | |
406 | 0 | let (w, h) = img.dimensions(); |
407 | 0 | if w == 0 || h == 0 { |
408 | 0 | return None; |
409 | 0 | } |
410 | 0 | if !(0.0..=((w - 1) as f32)).contains(&x) { |
411 | 0 | return None; |
412 | 0 | } |
413 | 0 | if !(0.0..=((h - 1) as f32)).contains(&y) { |
414 | 0 | return None; |
415 | 0 | } |
416 | | |
417 | | // keep these as integers, for fewer FLOPs |
418 | 0 | let uf = x.floor() as u32; |
419 | 0 | let vf = y.floor() as u32; |
420 | 0 | let uc = (uf + 1).min(w - 1); |
421 | 0 | let vc = (vf + 1).min(h - 1); |
422 | | |
423 | | // clamp coords to the range of the image |
424 | 0 | let mut sxx = [[0.; 4]; 4]; |
425 | | |
426 | | // do not use Array::map, as it can be slow with high stack usage, |
427 | | // for [[f32; 4]; 4]. |
428 | | |
429 | | // convert samples to f32 |
430 | | // currently rgba is the largest one, |
431 | | // so just store as many items as necessary, |
432 | | // because there's not a simple way to be generic over all of them. |
433 | 0 | let mut compute = |u: u32, v: u32, i| { |
434 | 0 | let s = img.get_pixel(u, v); |
435 | 0 | for (j, c) in s.channels().iter().enumerate() { |
436 | 0 | sxx[j][i] = c.to_f32().unwrap(); |
437 | 0 | } |
438 | 0 | s |
439 | 0 | }; |
440 | | |
441 | | // hacky reuse since cannot construct a generic Pixel |
442 | 0 | let mut out: P = compute(uf, vf, 0); |
443 | 0 | compute(uf, vc, 1); |
444 | 0 | compute(uc, vf, 2); |
445 | 0 | compute(uc, vc, 3); |
446 | | |
447 | | // weights, the later two are independent from the first 2 for better vectorization. |
448 | 0 | let ufw = x - uf as f32; |
449 | 0 | let vfw = y - vf as f32; |
450 | 0 | let ucw = (uf + 1) as f32 - x; |
451 | 0 | let vcw = (vf + 1) as f32 - y; |
452 | | |
453 | | // https://en.wikipedia.org/wiki/Bilinear_interpolation#Weighted_mean |
454 | | // the distance between pixels is 1 so there is no denominator |
455 | 0 | let wff = ucw * vcw; |
456 | 0 | let wfc = ucw * vfw; |
457 | 0 | let wcf = ufw * vcw; |
458 | 0 | let wcc = ufw * vfw; |
459 | | // was originally assert, but is actually not a cheap computation |
460 | 0 | debug_assert!(f32::abs((wff + wfc + wcf + wcc) - 1.) < 1e-3); |
461 | | |
462 | | // hack to see if primitive is an integer or a float |
463 | 0 | let is_float = P::Subpixel::DEFAULT_MAX_VALUE.to_f32().unwrap() == 1.0; |
464 | | |
465 | 0 | for (i, c) in out.channels_mut().iter_mut().enumerate() { |
466 | 0 | let v = wff * sxx[i][0] + wfc * sxx[i][1] + wcf * sxx[i][2] + wcc * sxx[i][3]; |
467 | | // this rounding may introduce quantization errors, |
468 | | // Specifically what is meant is that many samples may deviate |
469 | | // from the mean value of the originals, but it's not possible to fix that. |
470 | 0 | *c = <P::Subpixel as NumCast>::from(if is_float { v } else { v.round() }).unwrap_or({ |
471 | 0 | if v < 0.0 { |
472 | 0 | P::Subpixel::DEFAULT_MIN_VALUE |
473 | | } else { |
474 | 0 | P::Subpixel::DEFAULT_MAX_VALUE |
475 | | } |
476 | | }); |
477 | | } |
478 | | |
479 | 0 | Some(out) |
480 | 0 | } |
481 | | |
482 | | // Sample the columns of the supplied image using the provided filter. |
483 | | // The width of the image remains unchanged. |
484 | | // ```new_height``` is the desired height of the new image |
485 | | // ```filter``` is the filter to use for sampling. |
486 | | // The return value is not necessarily Rgba, the underlying order of channels in ```image``` is |
487 | | // preserved. |
488 | | // |
489 | | // Note: if an empty image is passed in, panics unless the image is truly empty. |
490 | 0 | fn vertical_sample<I, P, S>(image: &I, new_height: u32, filter: &mut Filter) -> Rgba32FImage |
491 | 0 | where |
492 | 0 | I: GenericImageView<Pixel = P>, |
493 | 0 | P: Pixel<Subpixel = S> + 'static, |
494 | 0 | S: Primitive + 'static, |
495 | | { |
496 | 0 | let (width, height) = image.dimensions(); |
497 | | |
498 | | // This is protection against a regression in memory usage such as #2340. Since the strategy to |
499 | | // deal with it depends on the caller it is a precondition of this function. |
500 | 0 | assert!( |
501 | | // Checks the implication: (height == 0) -> (width == 0) |
502 | 0 | height != 0 || width == 0, |
503 | 0 | "Unexpected prior allocation size. This case should have been handled by the caller" |
504 | | ); |
505 | | |
506 | 0 | let mut out = ImageBuffer::new(width, new_height); |
507 | 0 | out.copy_color_space_from(&image.buffer_with_dimensions(0, 0)); |
508 | 0 | let mut ws = Vec::new(); |
509 | | |
510 | 0 | let ratio = height as f32 / new_height as f32; |
511 | 0 | let sratio = if ratio < 1.0 { 1.0 } else { ratio }; |
512 | 0 | let src_support = filter.support * sratio; |
513 | | |
514 | 0 | for outy in 0..new_height { |
515 | | // For an explanation of this algorithm, see the comments |
516 | | // in horizontal_sample. |
517 | 0 | let inputy = (outy as f32 + 0.5) * ratio; |
518 | | |
519 | 0 | let left = (inputy - src_support).floor() as i64; |
520 | 0 | let left = clamp(left, 0, <i64 as From<_>>::from(height) - 1) as u32; |
521 | | |
522 | 0 | let right = (inputy + src_support).ceil() as i64; |
523 | 0 | let right = clamp( |
524 | 0 | right, |
525 | 0 | <i64 as From<_>>::from(left) + 1, |
526 | 0 | <i64 as From<_>>::from(height), |
527 | 0 | ) as u32; |
528 | | |
529 | 0 | let inputy = inputy - 0.5; |
530 | | |
531 | 0 | ws.clear(); |
532 | 0 | let mut sum = 0.0; |
533 | 0 | for i in left..right { |
534 | 0 | let w = (filter.kernel)((i as f32 - inputy) / sratio); |
535 | 0 | ws.push(w); |
536 | 0 | sum += w; |
537 | 0 | } |
538 | 0 | for w in ws.iter_mut() { |
539 | 0 | *w /= sum; |
540 | 0 | } |
541 | | |
542 | 0 | for x in 0..width { |
543 | 0 | let mut t = (0.0, 0.0, 0.0, 0.0); |
544 | | |
545 | 0 | for (i, w) in ws.iter().enumerate() { |
546 | 0 | let p = image.get_pixel(x, left + i as u32); |
547 | 0 |
|
548 | 0 | #[allow(deprecated)] |
549 | 0 | let (k1, k2, k3, k4) = p.channels4(); |
550 | 0 | let vec: (f32, f32, f32, f32) = ( |
551 | 0 | NumCast::from(k1).unwrap(), |
552 | 0 | NumCast::from(k2).unwrap(), |
553 | 0 | NumCast::from(k3).unwrap(), |
554 | 0 | NumCast::from(k4).unwrap(), |
555 | 0 | ); |
556 | 0 |
|
557 | 0 | t.0 += vec.0 * w; |
558 | 0 | t.1 += vec.1 * w; |
559 | 0 | t.2 += vec.2 * w; |
560 | 0 | t.3 += vec.3 * w; |
561 | 0 | } |
562 | | |
563 | | #[allow(deprecated)] |
564 | | // This is not necessarily Rgba. |
565 | 0 | let t = Pixel::from_channels(t.0, t.1, t.2, t.3); |
566 | | |
567 | 0 | out.put_pixel(x, outy, t); |
568 | | } |
569 | | } |
570 | | |
571 | 0 | out |
572 | 0 | } |
573 | | |
574 | | /// Local struct for keeping track of pixel sums for fast thumbnail averaging |
575 | | struct ThumbnailSum<S: Primitive + Enlargeable>(S::Larger, S::Larger, S::Larger, S::Larger); |
576 | | |
577 | | impl<S: Primitive + Enlargeable> ThumbnailSum<S> { |
578 | 0 | fn zeroed() -> Self { |
579 | 0 | ThumbnailSum( |
580 | 0 | S::Larger::zero(), |
581 | 0 | S::Larger::zero(), |
582 | 0 | S::Larger::zero(), |
583 | 0 | S::Larger::zero(), |
584 | 0 | ) |
585 | 0 | } Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<f32>>::zeroed Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u8>>::zeroed Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u16>>::zeroed |
586 | | |
587 | 0 | fn sample_val(val: S) -> S::Larger { |
588 | 0 | <S::Larger as NumCast>::from(val).unwrap() |
589 | 0 | } Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<f32>>::sample_val Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u8>>::sample_val Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u16>>::sample_val |
590 | | |
591 | 0 | fn add_pixel<P: Pixel<Subpixel = S>>(&mut self, pixel: P) { |
592 | | #[allow(deprecated)] |
593 | 0 | let pixel = pixel.channels4(); |
594 | 0 | self.0 += Self::sample_val(pixel.0); |
595 | 0 | self.1 += Self::sample_val(pixel.1); |
596 | 0 | self.2 += Self::sample_val(pixel.2); |
597 | 0 | self.3 += Self::sample_val(pixel.3); |
598 | 0 | } Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<f32>>::add_pixel::<image::color::Rgb<f32>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<f32>>::add_pixel::<image::color::Rgba<f32>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u8>>::add_pixel::<image::color::Rgb<u8>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u8>>::add_pixel::<image::color::Luma<u8>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u8>>::add_pixel::<image::color::Rgba<u8>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u8>>::add_pixel::<image::color::LumaA<u8>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u16>>::add_pixel::<image::color::Rgb<u16>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u16>>::add_pixel::<image::color::Luma<u16>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u16>>::add_pixel::<image::color::Rgba<u16>> Unexecuted instantiation: <image::imageops::sample::ThumbnailSum<u16>>::add_pixel::<image::color::LumaA<u16>> |
599 | | } |
600 | | |
601 | | /// Resize the supplied image to the specific dimensions. |
602 | | /// |
603 | | /// For downscaling, this method uses a fast integer algorithm where each source pixel contributes |
604 | | /// to exactly one target pixel. May give aliasing artifacts if new size is close to old size. |
605 | | /// |
606 | | /// In case the current width is smaller than the new width or similar for the height, another |
607 | | /// strategy is used instead. For each pixel in the output, a rectangular region of the input is |
608 | | /// determined, just as previously. But when no input pixel is part of this region, the nearest |
609 | | /// pixels are interpolated instead. |
610 | | /// |
611 | | /// For speed reasons, all interpolation is performed linearly over the colour values. It will not |
612 | | /// take the pixel colour spaces into account. |
613 | 0 | pub fn thumbnail<I, P, S>(image: &I, new_width: u32, new_height: u32) -> ImageBuffer<P, Vec<S>> |
614 | 0 | where |
615 | 0 | I: GenericImageView<Pixel = P>, |
616 | 0 | P: Pixel<Subpixel = S> + 'static, |
617 | 0 | S: Primitive + Enlargeable + 'static, |
618 | | { |
619 | 0 | let (width, height) = image.dimensions(); |
620 | 0 | let mut out = image.buffer_with_dimensions(new_width, new_height); |
621 | | |
622 | 0 | if height == 0 || width == 0 { |
623 | 0 | return out; |
624 | 0 | } |
625 | | |
626 | 0 | let x_ratio = width as f32 / new_width as f32; |
627 | 0 | let y_ratio = height as f32 / new_height as f32; |
628 | | |
629 | 0 | for outy in 0..new_height { |
630 | 0 | let bottomf = outy as f32 * y_ratio; |
631 | 0 | let topf = bottomf + y_ratio; |
632 | | |
633 | 0 | let bottom = clamp(bottomf.ceil() as u32, 0, height - 1); |
634 | 0 | let top = clamp(topf.ceil() as u32, bottom, height); |
635 | | |
636 | 0 | for outx in 0..new_width { |
637 | 0 | let leftf = outx as f32 * x_ratio; |
638 | 0 | let rightf = leftf + x_ratio; |
639 | | |
640 | 0 | let left = clamp(leftf.ceil() as u32, 0, width - 1); |
641 | 0 | let right = clamp(rightf.ceil() as u32, left, width); |
642 | | |
643 | 0 | let avg = if bottom != top && left != right { |
644 | 0 | thumbnail_sample_block(image, left, right, bottom, top) |
645 | 0 | } else if bottom != top { |
646 | | // && left == right |
647 | | // In the first column we have left == 0 and right > ceil(y_scale) > 0 so this |
648 | | // assertion can never trigger. |
649 | 0 | debug_assert!( |
650 | 0 | left > 0 && right > 0, |
651 | 0 | "First output column must have corresponding pixels" |
652 | | ); |
653 | | |
654 | 0 | let fraction_horizontal = (leftf.fract() + rightf.fract()) / 2.; |
655 | 0 | thumbnail_sample_fraction_horizontal( |
656 | 0 | image, |
657 | 0 | right - 1, |
658 | 0 | fraction_horizontal, |
659 | 0 | bottom, |
660 | 0 | top, |
661 | | ) |
662 | 0 | } else if left != right { |
663 | | // && bottom == top |
664 | | // In the first line we have bottom == 0 and top > ceil(x_scale) > 0 so this |
665 | | // assertion can never trigger. |
666 | 0 | debug_assert!( |
667 | 0 | bottom > 0 && top > 0, |
668 | 0 | "First output row must have corresponding pixels" |
669 | | ); |
670 | | |
671 | 0 | let fraction_vertical = (topf.fract() + bottomf.fract()) / 2.; |
672 | 0 | thumbnail_sample_fraction_vertical(image, left, right, top - 1, fraction_vertical) |
673 | | } else { |
674 | | // bottom == top && left == right |
675 | 0 | let fraction_horizontal = (topf.fract() + bottomf.fract()) / 2.; |
676 | 0 | let fraction_vertical = (leftf.fract() + rightf.fract()) / 2.; |
677 | | |
678 | 0 | thumbnail_sample_fraction_both( |
679 | 0 | image, |
680 | 0 | right - 1, |
681 | 0 | fraction_horizontal, |
682 | 0 | top - 1, |
683 | 0 | fraction_vertical, |
684 | | ) |
685 | | }; |
686 | | |
687 | | #[allow(deprecated)] |
688 | 0 | let pixel = Pixel::from_channels(avg.0, avg.1, avg.2, avg.3); |
689 | 0 | out.put_pixel(outx, outy, pixel); |
690 | | } |
691 | | } |
692 | | |
693 | 0 | out |
694 | 0 | } Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16> |
695 | | |
696 | | /// Get a pixel for a thumbnail where the input window encloses at least a full pixel. |
697 | 0 | fn thumbnail_sample_block<I, P, S>( |
698 | 0 | image: &I, |
699 | 0 | left: u32, |
700 | 0 | right: u32, |
701 | 0 | bottom: u32, |
702 | 0 | top: u32, |
703 | 0 | ) -> (S, S, S, S) |
704 | 0 | where |
705 | 0 | I: GenericImageView<Pixel = P>, |
706 | 0 | P: Pixel<Subpixel = S>, |
707 | 0 | S: Primitive + Enlargeable, |
708 | | { |
709 | 0 | let mut sum = ThumbnailSum::zeroed(); |
710 | | |
711 | 0 | for y in bottom..top { |
712 | 0 | for x in left..right { |
713 | 0 | let k = image.get_pixel(x, y); |
714 | 0 | sum.add_pixel(k); |
715 | 0 | } |
716 | | } |
717 | | |
718 | 0 | let n = <S::Larger as NumCast>::from((right - left) * (top - bottom)).unwrap(); |
719 | 0 | let round = <S::Larger as NumCast>::from(n / NumCast::from(2).unwrap()).unwrap(); |
720 | 0 | ( |
721 | 0 | S::clamp_from((sum.0 + round) / n), |
722 | 0 | S::clamp_from((sum.1 + round) / n), |
723 | 0 | S::clamp_from((sum.2 + round) / n), |
724 | 0 | S::clamp_from((sum.3 + round) / n), |
725 | 0 | ) |
726 | 0 | } Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_block::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16> |
727 | | |
728 | | /// Get a thumbnail pixel where the input window encloses at least a vertical pixel. |
729 | 0 | fn thumbnail_sample_fraction_horizontal<I, P, S>( |
730 | 0 | image: &I, |
731 | 0 | left: u32, |
732 | 0 | fraction_horizontal: f32, |
733 | 0 | bottom: u32, |
734 | 0 | top: u32, |
735 | 0 | ) -> (S, S, S, S) |
736 | 0 | where |
737 | 0 | I: GenericImageView<Pixel = P>, |
738 | 0 | P: Pixel<Subpixel = S>, |
739 | 0 | S: Primitive + Enlargeable, |
740 | | { |
741 | 0 | let fract = fraction_horizontal; |
742 | | |
743 | 0 | let mut sum_left = ThumbnailSum::zeroed(); |
744 | 0 | let mut sum_right = ThumbnailSum::zeroed(); |
745 | 0 | for x in bottom..top { |
746 | 0 | let k_left = image.get_pixel(left, x); |
747 | 0 | sum_left.add_pixel(k_left); |
748 | 0 |
|
749 | 0 | let k_right = image.get_pixel(left + 1, x); |
750 | 0 | sum_right.add_pixel(k_right); |
751 | 0 | } |
752 | | |
753 | | // Now we approximate: left/n*(1-fract) + right/n*fract |
754 | 0 | let fact_right = fract / ((top - bottom) as f32); |
755 | 0 | let fact_left = (1. - fract) / ((top - bottom) as f32); |
756 | | |
757 | 0 | let mix_left_and_right = |leftv: S::Larger, rightv: S::Larger| { |
758 | 0 | <S as NumCast>::from( |
759 | 0 | fact_left * leftv.to_f32().unwrap() + fact_right * rightv.to_f32().unwrap(), |
760 | | ) |
761 | 0 | .expect("Average sample value should fit into sample type") |
762 | 0 | }; Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16>::{closure#0} |
763 | | |
764 | 0 | ( |
765 | 0 | mix_left_and_right(sum_left.0, sum_right.0), |
766 | 0 | mix_left_and_right(sum_left.1, sum_right.1), |
767 | 0 | mix_left_and_right(sum_left.2, sum_right.2), |
768 | 0 | mix_left_and_right(sum_left.3, sum_right.3), |
769 | 0 | ) |
770 | 0 | } Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_horizontal::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16> |
771 | | |
772 | | /// Get a thumbnail pixel where the input window encloses at least a horizontal pixel. |
773 | 0 | fn thumbnail_sample_fraction_vertical<I, P, S>( |
774 | 0 | image: &I, |
775 | 0 | left: u32, |
776 | 0 | right: u32, |
777 | 0 | bottom: u32, |
778 | 0 | fraction_vertical: f32, |
779 | 0 | ) -> (S, S, S, S) |
780 | 0 | where |
781 | 0 | I: GenericImageView<Pixel = P>, |
782 | 0 | P: Pixel<Subpixel = S>, |
783 | 0 | S: Primitive + Enlargeable, |
784 | | { |
785 | 0 | let fract = fraction_vertical; |
786 | | |
787 | 0 | let mut sum_bot = ThumbnailSum::zeroed(); |
788 | 0 | let mut sum_top = ThumbnailSum::zeroed(); |
789 | 0 | for x in left..right { |
790 | 0 | let k_bot = image.get_pixel(x, bottom); |
791 | 0 | sum_bot.add_pixel(k_bot); |
792 | 0 |
|
793 | 0 | let k_top = image.get_pixel(x, bottom + 1); |
794 | 0 | sum_top.add_pixel(k_top); |
795 | 0 | } |
796 | | |
797 | | // Now we approximate: bot/n*fract + top/n*(1-fract) |
798 | 0 | let fact_top = fract / ((right - left) as f32); |
799 | 0 | let fact_bot = (1. - fract) / ((right - left) as f32); |
800 | | |
801 | 0 | let mix_bot_and_top = |botv: S::Larger, topv: S::Larger| { |
802 | 0 | <S as NumCast>::from(fact_bot * botv.to_f32().unwrap() + fact_top * topv.to_f32().unwrap()) |
803 | 0 | .expect("Average sample value should fit into sample type") |
804 | 0 | }; Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16>::{closure#0} |
805 | | |
806 | 0 | ( |
807 | 0 | mix_bot_and_top(sum_bot.0, sum_top.0), |
808 | 0 | mix_bot_and_top(sum_bot.1, sum_top.1), |
809 | 0 | mix_bot_and_top(sum_bot.2, sum_top.2), |
810 | 0 | mix_bot_and_top(sum_bot.3, sum_top.3), |
811 | 0 | ) |
812 | 0 | } Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_vertical::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16> |
813 | | |
814 | | /// Get a single pixel for a thumbnail where the input window does not enclose any full pixel. |
815 | 0 | fn thumbnail_sample_fraction_both<I, P, S>( |
816 | 0 | image: &I, |
817 | 0 | left: u32, |
818 | 0 | fraction_vertical: f32, |
819 | 0 | bottom: u32, |
820 | 0 | fraction_horizontal: f32, |
821 | 0 | ) -> (S, S, S, S) |
822 | 0 | where |
823 | 0 | I: GenericImageView<Pixel = P>, |
824 | 0 | P: Pixel<Subpixel = S>, |
825 | 0 | S: Primitive + Enlargeable, |
826 | | { |
827 | | #[allow(deprecated)] |
828 | 0 | let k_bl = image.get_pixel(left, bottom).channels4(); |
829 | | #[allow(deprecated)] |
830 | 0 | let k_tl = image.get_pixel(left, bottom + 1).channels4(); |
831 | | #[allow(deprecated)] |
832 | 0 | let k_br = image.get_pixel(left + 1, bottom).channels4(); |
833 | | #[allow(deprecated)] |
834 | 0 | let k_tr = image.get_pixel(left + 1, bottom + 1).channels4(); |
835 | | |
836 | 0 | let frac_v = fraction_vertical; |
837 | 0 | let frac_h = fraction_horizontal; |
838 | | |
839 | 0 | let fact_tr = frac_v * frac_h; |
840 | 0 | let fact_tl = frac_v * (1. - frac_h); |
841 | 0 | let fact_br = (1. - frac_v) * frac_h; |
842 | 0 | let fact_bl = (1. - frac_v) * (1. - frac_h); |
843 | | |
844 | 0 | let mix = |br: S, tr: S, bl: S, tl: S| { |
845 | 0 | <S as NumCast>::from( |
846 | 0 | fact_br * br.to_f32().unwrap() |
847 | 0 | + fact_tr * tr.to_f32().unwrap() |
848 | 0 | + fact_bl * bl.to_f32().unwrap() |
849 | 0 | + fact_tl * tl.to_f32().unwrap(), |
850 | | ) |
851 | 0 | .expect("Average sample value should fit into sample type") |
852 | 0 | }; Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16>::{closure#0} |
853 | | |
854 | 0 | ( |
855 | 0 | mix(k_br.0, k_tr.0, k_bl.0, k_tl.0), |
856 | 0 | mix(k_br.1, k_tr.1, k_bl.1, k_tl.1), |
857 | 0 | mix(k_br.2, k_tr.2, k_bl.2, k_tl.2), |
858 | 0 | mix(k_br.3, k_tr.3, k_bl.3, k_tl.3), |
859 | 0 | ) |
860 | 0 | } Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8> Unexecuted instantiation: image::imageops::sample::thumbnail_sample_fraction_both::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16> |
861 | | |
862 | | /// Perform a 3x3 box filter on the supplied image. |
863 | | /// |
864 | | /// # Arguments: |
865 | | /// |
866 | | /// * `image` - source image. |
867 | | /// * `kernel` - is an array of the filter weights of length 9. |
868 | | /// |
869 | | /// This method typically assumes that the input is scene-linear light. |
870 | | /// If it is not, color distortion may occur. |
871 | 0 | pub fn filter3x3<I, P, S>(image: &I, kernel: &[f32]) -> ImageBuffer<P, Vec<S>> |
872 | 0 | where |
873 | 0 | I: GenericImageView<Pixel = P>, |
874 | 0 | P: Pixel<Subpixel = S> + 'static, |
875 | 0 | S: Primitive + 'static, |
876 | | { |
877 | | // The kernel's input positions relative to the current pixel. |
878 | 0 | let taps: &[(isize, isize)] = &[ |
879 | 0 | (-1, -1), |
880 | 0 | (0, -1), |
881 | 0 | (1, -1), |
882 | 0 | (-1, 0), |
883 | 0 | (0, 0), |
884 | 0 | (1, 0), |
885 | 0 | (-1, 1), |
886 | 0 | (0, 1), |
887 | 0 | (1, 1), |
888 | 0 | ]; |
889 | | |
890 | 0 | let (width, height) = image.dimensions(); |
891 | 0 | let mut out = image.buffer_like(); |
892 | | |
893 | 0 | let max = S::DEFAULT_MAX_VALUE; |
894 | 0 | let max: f32 = NumCast::from(max).unwrap(); |
895 | | |
896 | 0 | let inverse_sum = match kernel.iter().sum() { |
897 | 0 | 0.0 => 1.0, |
898 | 0 | sum => 1.0 / sum, |
899 | | }; |
900 | | |
901 | 0 | for y in 1..height - 1 { |
902 | 0 | for x in 1..width - 1 { |
903 | 0 | let mut t = (0.0, 0.0, 0.0, 0.0); |
904 | | |
905 | | // TODO: There is no need to recalculate the kernel for each pixel. |
906 | | // Only a subtract and addition is needed for pixels after the first |
907 | | // in each row. |
908 | 0 | for (&k, &(a, b)) in kernel.iter().zip(taps.iter()) { |
909 | 0 | let x0 = x as isize + a; |
910 | 0 | let y0 = y as isize + b; |
911 | 0 |
|
912 | 0 | let p = image.get_pixel(x0 as u32, y0 as u32); |
913 | 0 |
|
914 | 0 | #[allow(deprecated)] |
915 | 0 | let (k1, k2, k3, k4) = p.channels4(); |
916 | 0 |
|
917 | 0 | let vec: (f32, f32, f32, f32) = ( |
918 | 0 | NumCast::from(k1).unwrap(), |
919 | 0 | NumCast::from(k2).unwrap(), |
920 | 0 | NumCast::from(k3).unwrap(), |
921 | 0 | NumCast::from(k4).unwrap(), |
922 | 0 | ); |
923 | 0 |
|
924 | 0 | t.0 += vec.0 * k; |
925 | 0 | t.1 += vec.1 * k; |
926 | 0 | t.2 += vec.2 * k; |
927 | 0 | t.3 += vec.3 * k; |
928 | 0 | } |
929 | | |
930 | 0 | let (t1, t2, t3, t4) = ( |
931 | 0 | t.0 * inverse_sum, |
932 | 0 | t.1 * inverse_sum, |
933 | 0 | t.2 * inverse_sum, |
934 | 0 | t.3 * inverse_sum, |
935 | 0 | ); |
936 | | |
937 | | #[allow(deprecated)] |
938 | 0 | let t = Pixel::from_channels( |
939 | 0 | NumCast::from(clamp(t1, 0.0, max)).unwrap(), |
940 | 0 | NumCast::from(clamp(t2, 0.0, max)).unwrap(), |
941 | 0 | NumCast::from(clamp(t3, 0.0, max)).unwrap(), |
942 | 0 | NumCast::from(clamp(t4, 0.0, max)).unwrap(), |
943 | | ); |
944 | | |
945 | 0 | out.put_pixel(x, y, t); |
946 | | } |
947 | | } |
948 | | |
949 | 0 | out |
950 | 0 | } Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8> Unexecuted instantiation: image::imageops::sample::filter3x3::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16> |
951 | | |
952 | | /// Resize the supplied image to the specified dimensions. |
953 | | /// |
954 | | /// # Arguments: |
955 | | /// |
956 | | /// * `nwidth` - new image width. |
957 | | /// * `nheight` - new image height. |
958 | | /// * `filter` - is the sampling filter to use, see [FilterType] for mor information. |
959 | | /// |
960 | | /// This method assumes alpha pre-multiplication for images that contain non-constant alpha. |
961 | | /// |
962 | | /// This method typically assumes that the input is scene-linear light. |
963 | | /// If it is not, color distortion may occur. |
964 | 0 | pub fn resize<I: GenericImageView>( |
965 | 0 | image: &I, |
966 | 0 | nwidth: u32, |
967 | 0 | nheight: u32, |
968 | 0 | filter: FilterType, |
969 | 0 | ) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>> |
970 | 0 | where |
971 | 0 | I::Pixel: 'static, |
972 | 0 | <I::Pixel as Pixel>::Subpixel: 'static, |
973 | | { |
974 | | // Check if there is nothing to sample from. |
975 | 0 | let is_empty = { |
976 | 0 | let (width, height) = image.dimensions(); |
977 | 0 | width == 0 || height == 0 |
978 | | }; |
979 | | |
980 | 0 | if is_empty { |
981 | 0 | return image.buffer_with_dimensions(nwidth, nheight); |
982 | 0 | } |
983 | | |
984 | | // check if the new dimensions are the same as the old. if they are, make a copy instead of resampling |
985 | 0 | if (nwidth, nheight) == image.dimensions() { |
986 | 0 | let mut tmp = image.buffer_like(); |
987 | 0 | tmp.copy_from(image, 0, 0).unwrap(); |
988 | 0 | return tmp; |
989 | 0 | } |
990 | | |
991 | 0 | let mut method = match filter { |
992 | 0 | FilterType::Nearest => Filter { |
993 | 0 | kernel: Box::new(box_kernel), |
994 | 0 | support: 0.0, |
995 | 0 | }, |
996 | 0 | FilterType::Triangle => Filter { |
997 | 0 | kernel: Box::new(triangle_kernel), |
998 | 0 | support: 1.0, |
999 | 0 | }, |
1000 | 0 | FilterType::CatmullRom => Filter { |
1001 | 0 | kernel: Box::new(catmullrom_kernel), |
1002 | 0 | support: 2.0, |
1003 | 0 | }, |
1004 | 0 | FilterType::Gaussian => Filter { |
1005 | 0 | kernel: Box::new(gaussian_kernel), |
1006 | 0 | support: 3.0, |
1007 | 0 | }, |
1008 | 0 | FilterType::Lanczos3 => Filter { |
1009 | 0 | kernel: Box::new(lanczos3_kernel), |
1010 | 0 | support: 3.0, |
1011 | 0 | }, |
1012 | | }; |
1013 | | |
1014 | | // Note: tmp is not necessarily actually Rgba |
1015 | 0 | let tmp: Rgba32FImage = vertical_sample(image, nheight, &mut method); |
1016 | 0 | horizontal_sample(&tmp, nwidth, &mut method) |
1017 | 0 | } |
1018 | | |
1019 | | /// Performs a Gaussian blur on the supplied image. |
1020 | | /// |
1021 | | /// # Arguments |
1022 | | /// |
1023 | | /// - `sigma` - gaussian bell flattening level. |
1024 | | /// |
1025 | | /// Use [`crate::imageops::fast_blur()`] for a faster but less |
1026 | | /// accurate version. |
1027 | | /// This method assumes alpha pre-multiplication for images that contain non-constant alpha. |
1028 | | /// This method typically assumes that the input is scene-linear light. |
1029 | | /// If it is not, color distortion may occur. |
1030 | 0 | pub fn blur<I: GenericImageView>( |
1031 | 0 | image: &I, |
1032 | 0 | sigma: f32, |
1033 | 0 | ) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>> |
1034 | 0 | where |
1035 | 0 | I::Pixel: 'static, |
1036 | | { |
1037 | 0 | gaussian_blur_indirect( |
1038 | 0 | image, |
1039 | 0 | GaussianBlurParameters::new_from_sigma(if sigma == 0.0 { 0.8 } else { sigma }), |
1040 | | ) |
1041 | 0 | } |
1042 | | |
1043 | | /// Performs a Gaussian blur on the supplied image. |
1044 | | /// |
1045 | | /// # Arguments |
1046 | | /// |
1047 | | /// - `parameters` - see [GaussianBlurParameters] for more info. |
1048 | | /// |
1049 | | /// This method assumes alpha pre-multiplication for images that contain non-constant alpha. |
1050 | | /// This method typically assumes that the input is scene-linear light. |
1051 | | /// If it is not, color distortion may occur. |
1052 | 0 | pub fn blur_advanced<I: GenericImageView>( |
1053 | 0 | image: &I, |
1054 | 0 | parameters: GaussianBlurParameters, |
1055 | 0 | ) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>> |
1056 | 0 | where |
1057 | 0 | I::Pixel: 'static, |
1058 | | { |
1059 | 0 | gaussian_blur_indirect(image, parameters) |
1060 | 0 | } Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::blur_advanced::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>> |
1061 | | |
1062 | 0 | fn get_gaussian_kernel_1d(width: usize, sigma: f32) -> Vec<f32> { |
1063 | 0 | let mut sum_norm: f32 = 0f32; |
1064 | 0 | let mut kernel = vec![0f32; width]; |
1065 | 0 | let scale = 1f32 / (f32::sqrt(2f32 * f32::consts::PI) * sigma); |
1066 | 0 | let mean = (width / 2) as f32; |
1067 | | |
1068 | 0 | for (x, weight) in kernel.iter_mut().enumerate() { |
1069 | 0 | let new_weight = f32::exp(-0.5f32 * f32::powf((x as f32 - mean) / sigma, 2.0f32)) * scale; |
1070 | 0 | *weight = new_weight; |
1071 | 0 | sum_norm += new_weight; |
1072 | 0 | } |
1073 | | |
1074 | 0 | if sum_norm != 0f32 { |
1075 | 0 | let sum_scale = 1f32 / sum_norm; |
1076 | 0 | for weight in &mut kernel { |
1077 | 0 | *weight = weight.mul(sum_scale); |
1078 | 0 | } |
1079 | 0 | } |
1080 | | |
1081 | 0 | kernel |
1082 | 0 | } |
1083 | | |
1084 | | /// Holds analytical gaussian blur representation |
1085 | | #[derive(Copy, Clone, PartialOrd, PartialEq)] |
1086 | | pub struct GaussianBlurParameters { |
1087 | | /// X-axis kernel, must be odd |
1088 | | x_axis_kernel_size: u32, |
1089 | | /// X-axis sigma, must > 0, not subnormal, and not NaN |
1090 | | x_axis_sigma: f32, |
1091 | | /// Y-axis kernel, must be odd |
1092 | | y_axis_kernel_size: u32, |
1093 | | /// Y-axis sigma, must > 0, not subnormal, and not NaN |
1094 | | y_axis_sigma: f32, |
1095 | | } |
1096 | | |
1097 | | impl GaussianBlurParameters { |
1098 | | /// Built-in smoothing kernel with size 3. |
1099 | | pub const SMOOTHING_3: GaussianBlurParameters = GaussianBlurParameters { |
1100 | | x_axis_kernel_size: 3, |
1101 | | x_axis_sigma: 0.8, |
1102 | | y_axis_kernel_size: 3, |
1103 | | y_axis_sigma: 0.8, |
1104 | | }; |
1105 | | |
1106 | | /// Built-in smoothing kernel with size 5. |
1107 | | pub const SMOOTHING_5: GaussianBlurParameters = GaussianBlurParameters { |
1108 | | x_axis_kernel_size: 5, |
1109 | | x_axis_sigma: 1.1, |
1110 | | y_axis_kernel_size: 5, |
1111 | | y_axis_sigma: 1.1, |
1112 | | }; |
1113 | | |
1114 | | /// Built-in smoothing kernel with size 7. |
1115 | | pub const SMOOTHING_7: GaussianBlurParameters = GaussianBlurParameters { |
1116 | | x_axis_kernel_size: 7, |
1117 | | x_axis_sigma: 1.4, |
1118 | | y_axis_kernel_size: 7, |
1119 | | y_axis_sigma: 1.4, |
1120 | | }; |
1121 | | |
1122 | | /// Creates a new parameters set from radius only. |
1123 | 0 | pub fn new_from_radius(radius: f32) -> GaussianBlurParameters { |
1124 | | // Previous implementation was allowing passing 0 so we'll allow here also. |
1125 | 0 | assert!(radius >= 0.0); |
1126 | 0 | if radius != 0. { |
1127 | 0 | assert!( |
1128 | 0 | radius.is_normal(), |
1129 | 0 | "Radius do not allow infinities, NaNs or subnormals" |
1130 | | ); |
1131 | 0 | } |
1132 | 0 | GaussianBlurParameters::new_from_kernel_size(radius * 2. + 1.) |
1133 | 0 | } |
1134 | | |
1135 | | /// Creates a new parameters set from kernel size only. |
1136 | | /// |
1137 | | /// Kernel size will be rounded to nearest odd, and used with fraction |
1138 | | /// to compute accurate required sigma. |
1139 | 0 | pub fn new_from_kernel_size(kernel_size: f32) -> GaussianBlurParameters { |
1140 | 0 | assert!( |
1141 | 0 | kernel_size > 0., |
1142 | 0 | "Kernel size do not allow infinities, zeros, NaNs or subnormals or negatives" |
1143 | | ); |
1144 | 0 | assert!( |
1145 | 0 | kernel_size.is_normal(), |
1146 | 0 | "Kernel size do not allow infinities, zeros, NaNs or subnormals or negatives" |
1147 | | ); |
1148 | 0 | let i_kernel_size = GaussianBlurParameters::round_to_nearest_odd(kernel_size); |
1149 | 0 | assert_ne!(i_kernel_size % 2, 0, "Kernel size must be odd"); |
1150 | 0 | let v_sigma = GaussianBlurParameters::sigma_size(kernel_size); |
1151 | 0 | GaussianBlurParameters { |
1152 | 0 | x_axis_kernel_size: i_kernel_size, |
1153 | 0 | x_axis_sigma: v_sigma, |
1154 | 0 | y_axis_kernel_size: i_kernel_size, |
1155 | 0 | y_axis_sigma: v_sigma, |
1156 | 0 | } |
1157 | 0 | } |
1158 | | |
1159 | | /// Creates a new anisotropic parameter set from kernel sizes |
1160 | | /// |
1161 | | /// Kernel size will be rounded to nearest odd, and used with fraction |
1162 | | /// to compute accurate required sigma. |
1163 | 0 | pub fn new_anisotropic_kernel_size( |
1164 | 0 | x_axis_kernel_size: f32, |
1165 | 0 | y_axis_kernel_size: f32, |
1166 | 0 | ) -> GaussianBlurParameters { |
1167 | 0 | assert!( |
1168 | 0 | x_axis_kernel_size > 0., |
1169 | 0 | "Kernel size do not allow infinities, zeros, NaNs or subnormals or negatives" |
1170 | | ); |
1171 | 0 | assert!( |
1172 | 0 | y_axis_kernel_size.is_normal(), |
1173 | 0 | "Kernel size do not allow infinities, zeros, NaNs or subnormals or negatives" |
1174 | | ); |
1175 | 0 | assert!( |
1176 | 0 | y_axis_kernel_size > 0., |
1177 | 0 | "Kernel size do not allow infinities, zeros, NaNs or subnormals or negatives" |
1178 | | ); |
1179 | 0 | assert!( |
1180 | 0 | y_axis_kernel_size.is_normal(), |
1181 | 0 | "Kernel size do not allow infinities, zeros, NaNs or subnormals or negatives" |
1182 | | ); |
1183 | 0 | let x_kernel_size = GaussianBlurParameters::round_to_nearest_odd(x_axis_kernel_size); |
1184 | 0 | assert_ne!(x_kernel_size % 2, 0, "Kernel size must be odd"); |
1185 | 0 | let y_kernel_size = GaussianBlurParameters::round_to_nearest_odd(y_axis_kernel_size); |
1186 | 0 | assert_ne!(y_kernel_size % 2, 0, "Kernel size must be odd"); |
1187 | 0 | let x_sigma = GaussianBlurParameters::sigma_size(x_axis_kernel_size); |
1188 | 0 | let y_sigma = GaussianBlurParameters::sigma_size(y_axis_kernel_size); |
1189 | 0 | GaussianBlurParameters { |
1190 | 0 | x_axis_kernel_size: x_kernel_size, |
1191 | 0 | x_axis_sigma: x_sigma, |
1192 | 0 | y_axis_kernel_size: y_kernel_size, |
1193 | 0 | y_axis_sigma: y_sigma, |
1194 | 0 | } |
1195 | 0 | } |
1196 | | |
1197 | | /// Creates a new parameters set from sigma only |
1198 | 0 | pub fn new_from_sigma(sigma: f32) -> GaussianBlurParameters { |
1199 | 0 | assert!( |
1200 | 0 | sigma.is_normal(), |
1201 | 0 | "Sigma cannot be NaN, Infinities, subnormal or zero" |
1202 | | ); |
1203 | 0 | assert!(sigma > 0.0, "Sigma must be positive"); |
1204 | 0 | let kernel_size = GaussianBlurParameters::kernel_size_from_sigma(sigma); |
1205 | 0 | GaussianBlurParameters { |
1206 | 0 | x_axis_kernel_size: kernel_size, |
1207 | 0 | x_axis_sigma: sigma, |
1208 | 0 | y_axis_kernel_size: kernel_size, |
1209 | 0 | y_axis_sigma: sigma, |
1210 | 0 | } |
1211 | 0 | } |
1212 | | |
1213 | | #[inline] |
1214 | 0 | fn round_to_nearest_odd(x: f32) -> u32 { |
1215 | 0 | let n = x.round() as u32; |
1216 | 0 | if n % 2 != 0 { |
1217 | 0 | n |
1218 | | } else { |
1219 | 0 | let lower = n - 1; |
1220 | 0 | let upper = n + 1; |
1221 | | |
1222 | 0 | let dist_lower = (x - lower as f32).abs(); |
1223 | 0 | let dist_upper = (x - upper as f32).abs(); |
1224 | | |
1225 | 0 | if dist_lower <= dist_upper { |
1226 | 0 | lower |
1227 | | } else { |
1228 | 0 | upper |
1229 | | } |
1230 | | } |
1231 | 0 | } |
1232 | | |
1233 | 0 | fn sigma_size(kernel_size: f32) -> f32 { |
1234 | 0 | let safe_kernel_size = if kernel_size <= 1. { 0.8 } else { kernel_size }; |
1235 | 0 | 0.3 * ((safe_kernel_size - 1.) * 0.5 - 1.) + 0.8 |
1236 | 0 | } |
1237 | | |
1238 | 0 | fn kernel_size_from_sigma(sigma: f32) -> u32 { |
1239 | 0 | let possible_size = (((((sigma - 0.8) / 0.3) + 1.) * 2.) + 1.).max(3.) as u32; |
1240 | 0 | if possible_size % 2 == 0 { |
1241 | 0 | return possible_size + 1; |
1242 | 0 | } |
1243 | 0 | possible_size |
1244 | 0 | } |
1245 | | } |
1246 | | |
1247 | 0 | pub(crate) fn gaussian_blur_dyn_image( |
1248 | 0 | image: &DynamicImage, |
1249 | 0 | parameters: GaussianBlurParameters, |
1250 | 0 | ) -> DynamicImage { |
1251 | 0 | let x_axis_kernel = get_gaussian_kernel_1d( |
1252 | 0 | parameters.x_axis_kernel_size as usize, |
1253 | 0 | parameters.x_axis_sigma, |
1254 | | ); |
1255 | | |
1256 | 0 | let y_axis_kernel = get_gaussian_kernel_1d( |
1257 | 0 | parameters.y_axis_kernel_size as usize, |
1258 | 0 | parameters.y_axis_sigma, |
1259 | | ); |
1260 | | |
1261 | 0 | let filter_image_size = FilterImageSize { |
1262 | 0 | width: image.width() as usize, |
1263 | 0 | height: image.height() as usize, |
1264 | 0 | }; |
1265 | | |
1266 | 0 | let mut target = match image { |
1267 | 0 | DynamicImage::ImageLuma8(img) => { |
1268 | 0 | let mut dest_image = vec![0u8; img.len()]; |
1269 | 0 | filter_2d_sep_plane( |
1270 | 0 | img.as_raw(), |
1271 | 0 | &mut dest_image, |
1272 | 0 | filter_image_size, |
1273 | 0 | &x_axis_kernel, |
1274 | 0 | &y_axis_kernel, |
1275 | | ) |
1276 | 0 | .unwrap(); |
1277 | 0 | DynamicImage::ImageLuma8( |
1278 | 0 | GrayImage::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1279 | 0 | ) |
1280 | | } |
1281 | 0 | DynamicImage::ImageLumaA8(img) => { |
1282 | 0 | let mut dest_image = vec![0u8; img.len()]; |
1283 | 0 | filter_2d_sep_la( |
1284 | 0 | img.as_raw(), |
1285 | 0 | &mut dest_image, |
1286 | 0 | filter_image_size, |
1287 | 0 | &x_axis_kernel, |
1288 | 0 | &y_axis_kernel, |
1289 | | ) |
1290 | 0 | .unwrap(); |
1291 | 0 | DynamicImage::ImageLumaA8( |
1292 | 0 | GrayAlphaImage::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1293 | 0 | ) |
1294 | | } |
1295 | 0 | DynamicImage::ImageRgb8(img) => { |
1296 | 0 | let mut dest_image = vec![0u8; img.len()]; |
1297 | 0 | filter_2d_sep_rgb( |
1298 | 0 | img.as_raw(), |
1299 | 0 | &mut dest_image, |
1300 | 0 | filter_image_size, |
1301 | 0 | &x_axis_kernel, |
1302 | 0 | &y_axis_kernel, |
1303 | | ) |
1304 | 0 | .unwrap(); |
1305 | 0 | DynamicImage::ImageRgb8( |
1306 | 0 | RgbImage::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1307 | 0 | ) |
1308 | | } |
1309 | 0 | DynamicImage::ImageRgba8(img) => { |
1310 | 0 | let mut dest_image = vec![0u8; img.len()]; |
1311 | 0 | filter_2d_sep_rgba( |
1312 | 0 | img.as_raw(), |
1313 | 0 | &mut dest_image, |
1314 | 0 | filter_image_size, |
1315 | 0 | &x_axis_kernel, |
1316 | 0 | &y_axis_kernel, |
1317 | | ) |
1318 | 0 | .unwrap(); |
1319 | 0 | DynamicImage::ImageRgba8( |
1320 | 0 | RgbaImage::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1321 | 0 | ) |
1322 | | } |
1323 | 0 | DynamicImage::ImageLuma16(img) => { |
1324 | 0 | let mut dest_image = vec![0u16; img.len()]; |
1325 | 0 | filter_2d_sep_plane_u16( |
1326 | 0 | img.as_raw(), |
1327 | 0 | &mut dest_image, |
1328 | 0 | filter_image_size, |
1329 | 0 | &x_axis_kernel, |
1330 | 0 | &y_axis_kernel, |
1331 | | ) |
1332 | 0 | .unwrap(); |
1333 | 0 | DynamicImage::ImageLuma16( |
1334 | 0 | Gray16Image::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1335 | 0 | ) |
1336 | | } |
1337 | 0 | DynamicImage::ImageLumaA16(img) => { |
1338 | 0 | let mut dest_image = vec![0u16; img.len()]; |
1339 | 0 | filter_2d_sep_la_u16( |
1340 | 0 | img.as_raw(), |
1341 | 0 | &mut dest_image, |
1342 | 0 | filter_image_size, |
1343 | 0 | &x_axis_kernel, |
1344 | 0 | &y_axis_kernel, |
1345 | | ) |
1346 | 0 | .unwrap(); |
1347 | 0 | DynamicImage::ImageLumaA16( |
1348 | 0 | GrayAlpha16Image::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1349 | 0 | ) |
1350 | | } |
1351 | 0 | DynamicImage::ImageRgb16(img) => { |
1352 | 0 | let mut dest_image = vec![0u16; img.len()]; |
1353 | 0 | filter_2d_sep_rgb_u16( |
1354 | 0 | img.as_raw(), |
1355 | 0 | &mut dest_image, |
1356 | 0 | filter_image_size, |
1357 | 0 | &x_axis_kernel, |
1358 | 0 | &y_axis_kernel, |
1359 | | ) |
1360 | 0 | .unwrap(); |
1361 | 0 | DynamicImage::ImageRgb16( |
1362 | 0 | Rgb16Image::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1363 | 0 | ) |
1364 | | } |
1365 | 0 | DynamicImage::ImageRgba16(img) => { |
1366 | 0 | let mut dest_image = vec![0u16; img.len()]; |
1367 | 0 | filter_2d_sep_rgba_u16( |
1368 | 0 | img.as_raw(), |
1369 | 0 | &mut dest_image, |
1370 | 0 | filter_image_size, |
1371 | 0 | &x_axis_kernel, |
1372 | 0 | &y_axis_kernel, |
1373 | | ) |
1374 | 0 | .unwrap(); |
1375 | 0 | DynamicImage::ImageRgba16( |
1376 | 0 | Rgba16Image::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1377 | 0 | ) |
1378 | | } |
1379 | 0 | DynamicImage::ImageRgb32F(img) => { |
1380 | 0 | let mut dest_image = vec![0f32; img.len()]; |
1381 | 0 | filter_2d_sep_rgb_f32( |
1382 | 0 | img.as_raw(), |
1383 | 0 | &mut dest_image, |
1384 | 0 | filter_image_size, |
1385 | 0 | &x_axis_kernel, |
1386 | 0 | &y_axis_kernel, |
1387 | | ) |
1388 | 0 | .unwrap(); |
1389 | 0 | DynamicImage::ImageRgb32F( |
1390 | 0 | Rgb32FImage::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1391 | 0 | ) |
1392 | | } |
1393 | 0 | DynamicImage::ImageRgba32F(img) => { |
1394 | 0 | let mut dest_image = vec![0f32; img.len()]; |
1395 | 0 | filter_2d_sep_rgba_f32( |
1396 | 0 | img.as_raw(), |
1397 | 0 | &mut dest_image, |
1398 | 0 | filter_image_size, |
1399 | 0 | &x_axis_kernel, |
1400 | 0 | &y_axis_kernel, |
1401 | | ) |
1402 | 0 | .unwrap(); |
1403 | 0 | DynamicImage::ImageRgba32F( |
1404 | 0 | Rgba32FImage::from_raw(img.width(), img.height(), dest_image).unwrap(), |
1405 | 0 | ) |
1406 | | } |
1407 | | }; |
1408 | | |
1409 | | // Must succeed. |
1410 | 0 | let _ = target.set_color_space(image.color_space()); |
1411 | 0 | target |
1412 | 0 | } |
1413 | | |
1414 | 0 | fn gaussian_blur_indirect<I: GenericImageView>( |
1415 | 0 | image: &I, |
1416 | 0 | parameters: GaussianBlurParameters, |
1417 | 0 | ) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>> |
1418 | 0 | where |
1419 | 0 | I::Pixel: 'static, |
1420 | | { |
1421 | 0 | match I::Pixel::CHANNEL_COUNT { |
1422 | 0 | 1 => gaussian_blur_indirect_impl::<I, 1>(image, parameters), |
1423 | 0 | 2 => gaussian_blur_indirect_impl::<I, 2>(image, parameters), |
1424 | 0 | 3 => gaussian_blur_indirect_impl::<I, 3>(image, parameters), |
1425 | 0 | 4 => gaussian_blur_indirect_impl::<I, 4>(image, parameters), |
1426 | 0 | _ => unimplemented!(), |
1427 | | } |
1428 | 0 | } Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>> |
1429 | | |
1430 | 0 | fn gaussian_blur_indirect_impl<I: GenericImageView, const CN: usize>( |
1431 | 0 | image: &I, |
1432 | 0 | parameters: GaussianBlurParameters, |
1433 | 0 | ) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>> |
1434 | 0 | where |
1435 | 0 | I::Pixel: 'static, |
1436 | | { |
1437 | 0 | let mut transient = vec![0f32; image.width() as usize * image.height() as usize * CN]; |
1438 | 0 | for (pixel, dst) in image.pixels().zip(transient.chunks_exact_mut(CN)) { |
1439 | 0 | let px = pixel.2.channels(); |
1440 | 0 | match CN { |
1441 | 0 | 1 => { |
1442 | 0 | dst[0] = NumCast::from(px[0]).unwrap(); |
1443 | 0 | } |
1444 | 0 | 2 => { |
1445 | 0 | dst[0] = NumCast::from(px[0]).unwrap(); |
1446 | 0 | dst[1] = NumCast::from(px[1]).unwrap(); |
1447 | 0 | } |
1448 | 0 | 3 => { |
1449 | 0 | dst[0] = NumCast::from(px[0]).unwrap(); |
1450 | 0 | dst[1] = NumCast::from(px[1]).unwrap(); |
1451 | 0 | dst[2] = NumCast::from(px[2]).unwrap(); |
1452 | 0 | } |
1453 | 0 | 4 => { |
1454 | 0 | dst[0] = NumCast::from(px[0]).unwrap(); |
1455 | 0 | dst[1] = NumCast::from(px[1]).unwrap(); |
1456 | 0 | dst[2] = NumCast::from(px[2]).unwrap(); |
1457 | 0 | dst[3] = NumCast::from(px[3]).unwrap(); |
1458 | 0 | } |
1459 | 0 | _ => unreachable!(), |
1460 | | } |
1461 | | } |
1462 | | |
1463 | 0 | let mut transient_dst = vec![0.; image.width() as usize * image.height() as usize * CN]; |
1464 | | |
1465 | 0 | let x_axis_kernel = get_gaussian_kernel_1d( |
1466 | 0 | parameters.x_axis_kernel_size as usize, |
1467 | 0 | parameters.x_axis_sigma, |
1468 | | ); |
1469 | 0 | let y_axis_kernel = get_gaussian_kernel_1d( |
1470 | 0 | parameters.y_axis_kernel_size as usize, |
1471 | 0 | parameters.y_axis_sigma, |
1472 | | ); |
1473 | | |
1474 | 0 | let filter_image_size = FilterImageSize { |
1475 | 0 | width: image.width() as usize, |
1476 | 0 | height: image.height() as usize, |
1477 | 0 | }; |
1478 | | |
1479 | 0 | match CN { |
1480 | 0 | 1 => { |
1481 | 0 | filter_2d_sep_plane_f32( |
1482 | 0 | &transient, |
1483 | 0 | &mut transient_dst, |
1484 | 0 | filter_image_size, |
1485 | 0 | &x_axis_kernel, |
1486 | 0 | &y_axis_kernel, |
1487 | 0 | ) |
1488 | 0 | .unwrap(); |
1489 | 0 | } |
1490 | 0 | 2 => { |
1491 | 0 | filter_2d_sep_la_f32( |
1492 | 0 | &transient, |
1493 | 0 | &mut transient_dst, |
1494 | 0 | filter_image_size, |
1495 | 0 | &x_axis_kernel, |
1496 | 0 | &y_axis_kernel, |
1497 | 0 | ) |
1498 | 0 | .unwrap(); |
1499 | 0 | } |
1500 | 0 | 3 => { |
1501 | 0 | filter_2d_sep_rgb_f32( |
1502 | 0 | &transient, |
1503 | 0 | &mut transient_dst, |
1504 | 0 | filter_image_size, |
1505 | 0 | &x_axis_kernel, |
1506 | 0 | &y_axis_kernel, |
1507 | 0 | ) |
1508 | 0 | .unwrap(); |
1509 | 0 | } |
1510 | 0 | 4 => { |
1511 | 0 | filter_2d_sep_rgba_f32( |
1512 | 0 | &transient, |
1513 | 0 | &mut transient_dst, |
1514 | 0 | filter_image_size, |
1515 | 0 | &x_axis_kernel, |
1516 | 0 | &y_axis_kernel, |
1517 | 0 | ) |
1518 | 0 | .unwrap(); |
1519 | 0 | } |
1520 | 0 | _ => unreachable!(), |
1521 | | } |
1522 | | |
1523 | 0 | let mut out = image.buffer_like(); |
1524 | 0 | for (dst, src) in out.pixels_mut().zip(transient_dst.chunks_exact_mut(CN)) { |
1525 | 0 | match CN { |
1526 | 0 | 1 => { |
1527 | 0 | let v0 = NumCast::from(FloatNearest(src[0])).unwrap(); |
1528 | 0 | #[allow(deprecated)] |
1529 | 0 | let t = Pixel::from_channels(v0, v0, v0, v0); |
1530 | 0 | *dst = t; |
1531 | 0 | } |
1532 | 0 | 2 => { |
1533 | 0 | let v0 = NumCast::from(FloatNearest(src[0])).unwrap(); |
1534 | 0 | let v1 = NumCast::from(FloatNearest(src[1])).unwrap(); |
1535 | 0 | #[allow(deprecated)] |
1536 | 0 | let t = Pixel::from_channels(v0, v1, v0, v0); |
1537 | 0 | *dst = t; |
1538 | 0 | } |
1539 | 0 | 3 => { |
1540 | 0 | let v0 = NumCast::from(FloatNearest(src[0])).unwrap(); |
1541 | 0 | let v1 = NumCast::from(FloatNearest(src[1])).unwrap(); |
1542 | 0 | let v2 = NumCast::from(FloatNearest(src[2])).unwrap(); |
1543 | 0 | #[allow(deprecated)] |
1544 | 0 | let t = Pixel::from_channels(v0, v1, v2, v0); |
1545 | 0 | *dst = t; |
1546 | 0 | } |
1547 | 0 | 4 => { |
1548 | 0 | let v0 = NumCast::from(FloatNearest(src[0])).unwrap(); |
1549 | 0 | let v1 = NumCast::from(FloatNearest(src[1])).unwrap(); |
1550 | 0 | let v2 = NumCast::from(FloatNearest(src[2])).unwrap(); |
1551 | 0 | let v3 = NumCast::from(FloatNearest(src[3])).unwrap(); |
1552 | 0 | #[allow(deprecated)] |
1553 | 0 | let t = Pixel::from_channels(v0, v1, v2, v3); |
1554 | 0 | *dst = t; |
1555 | 0 | } |
1556 | 0 | _ => unreachable!(), |
1557 | | } |
1558 | | } |
1559 | | |
1560 | 0 | out |
1561 | 0 | } Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, 3> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, 3> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, 3> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, 1> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, 1> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, 4> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, 4> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, 4> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, 2> Unexecuted instantiation: image::imageops::sample::gaussian_blur_indirect_impl::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, 2> |
1562 | | |
1563 | | /// Performs an unsharpen mask on the supplied image. |
1564 | | /// |
1565 | | /// # Arguments: |
1566 | | /// |
1567 | | /// * `sigma` - is the amount to blur the image by. |
1568 | | /// * `threshold` - is the threshold for minimal brightness change that will be sharpened. |
1569 | | /// |
1570 | | /// This method typically assumes that the input is scene-linear light. |
1571 | | /// If it is not, color distortion may occur. |
1572 | | /// |
1573 | | /// See [Digital unsharp masking](https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking) for more information. |
1574 | 0 | pub fn unsharpen<I, P, S>(image: &I, sigma: f32, threshold: i32) -> ImageBuffer<P, Vec<S>> |
1575 | 0 | where |
1576 | 0 | I: GenericImageView<Pixel = P>, |
1577 | 0 | P: Pixel<Subpixel = S> + 'static, |
1578 | 0 | S: Primitive + 'static, |
1579 | | { |
1580 | 0 | let mut tmp = blur_advanced(image, GaussianBlurParameters::new_from_sigma(sigma)); |
1581 | | |
1582 | 0 | let max = S::DEFAULT_MAX_VALUE; |
1583 | 0 | let max: i32 = NumCast::from(max).unwrap(); |
1584 | 0 | let (width, height) = image.dimensions(); |
1585 | | |
1586 | 0 | for y in 0..height { |
1587 | 0 | for x in 0..width { |
1588 | 0 | let a = image.get_pixel(x, y); |
1589 | 0 | let b = tmp.get_pixel_mut(x, y); |
1590 | | |
1591 | 0 | let p = a.map2(b, |c, d| { |
1592 | 0 | let ic: i32 = NumCast::from(c).unwrap(); |
1593 | 0 | let id: i32 = NumCast::from(d).unwrap(); |
1594 | | |
1595 | 0 | let diff = ic - id; |
1596 | | |
1597 | 0 | if diff.abs() > threshold { |
1598 | 0 | let e = clamp(ic + diff, 0, max); // FIXME what does this do for f32? clamp 0-1 integers?? |
1599 | | |
1600 | 0 | NumCast::from(e).unwrap() |
1601 | | } else { |
1602 | 0 | c |
1603 | | } |
1604 | 0 | }); Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8>::{closure#0}Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16>::{closure#0} |
1605 | | |
1606 | 0 | *b = p; |
1607 | | } |
1608 | | } |
1609 | | |
1610 | 0 | tmp |
1611 | 0 | } Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgb<f32>, alloc::vec::Vec<f32>>, image::color::Rgb<f32>, f32> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgb<u8>, alloc::vec::Vec<u8>>, image::color::Rgb<u8>, u8> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgb<u16>, alloc::vec::Vec<u16>>, image::color::Rgb<u16>, u16> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Luma<u8>, alloc::vec::Vec<u8>>, image::color::Luma<u8>, u8> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Luma<u16>, alloc::vec::Vec<u16>>, image::color::Luma<u16>, u16> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgba<f32>, alloc::vec::Vec<f32>>, image::color::Rgba<f32>, f32> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgba<u8>, alloc::vec::Vec<u8>>, image::color::Rgba<u8>, u8> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::Rgba<u16>, alloc::vec::Vec<u16>>, image::color::Rgba<u16>, u16> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::LumaA<u8>, alloc::vec::Vec<u8>>, image::color::LumaA<u8>, u8> Unexecuted instantiation: image::imageops::sample::unsharpen::<image::images::buffer::ImageBuffer<image::color::LumaA<u16>, alloc::vec::Vec<u16>>, image::color::LumaA<u16>, u16> |
1612 | | |
1613 | | #[cfg(test)] |
1614 | | mod tests { |
1615 | | use super::{resize, sample_bilinear, sample_nearest, FilterType}; |
1616 | | use crate::{GenericImageView, ImageBuffer, RgbImage}; |
1617 | | #[cfg(feature = "benchmarks")] |
1618 | | use test; |
1619 | | |
1620 | | #[bench] |
1621 | | #[cfg(all(feature = "benchmarks", feature = "png"))] |
1622 | | fn bench_resize(b: &mut test::Bencher) { |
1623 | | use std::path::Path; |
1624 | | let img = crate::open(Path::new("./examples/fractal.png")).unwrap(); |
1625 | | b.iter(|| { |
1626 | | test::black_box(resize(&img, 200, 200, FilterType::Nearest)); |
1627 | | }); |
1628 | | b.bytes = 800 * 800 * 3 + 200 * 200 * 3; |
1629 | | } |
1630 | | |
1631 | | #[test] |
1632 | | #[cfg(feature = "png")] |
1633 | | fn test_resize_same_size() { |
1634 | | use std::path::Path; |
1635 | | let img = crate::open(Path::new("./examples/fractal.png")).unwrap(); |
1636 | | let mut resized = img.clone(); |
1637 | | resized.resize(img.width(), img.height(), FilterType::Triangle); |
1638 | | assert!(img.pixels().eq(resized.pixels())); |
1639 | | } |
1640 | | |
1641 | | #[test] |
1642 | | #[cfg(feature = "png")] |
1643 | | fn test_sample_bilinear() { |
1644 | | use std::path::Path; |
1645 | | let img = crate::open(Path::new("./examples/fractal.png")).unwrap(); |
1646 | | assert!(sample_bilinear(&img, 0., 0.).is_some()); |
1647 | | assert!(sample_bilinear(&img, 1., 0.).is_some()); |
1648 | | assert!(sample_bilinear(&img, 0., 1.).is_some()); |
1649 | | assert!(sample_bilinear(&img, 1., 1.).is_some()); |
1650 | | assert!(sample_bilinear(&img, 0.5, 0.5).is_some()); |
1651 | | |
1652 | | assert!(sample_bilinear(&img, 1.2, 0.5).is_none()); |
1653 | | assert!(sample_bilinear(&img, 0.5, 1.2).is_none()); |
1654 | | assert!(sample_bilinear(&img, 1.2, 1.2).is_none()); |
1655 | | |
1656 | | assert!(sample_bilinear(&img, -0.1, 0.2).is_none()); |
1657 | | assert!(sample_bilinear(&img, 0.2, -0.1).is_none()); |
1658 | | assert!(sample_bilinear(&img, -0.1, -0.1).is_none()); |
1659 | | } |
1660 | | #[test] |
1661 | | #[cfg(feature = "png")] |
1662 | | fn test_sample_nearest() { |
1663 | | use std::path::Path; |
1664 | | let img = crate::open(Path::new("./examples/fractal.png")).unwrap(); |
1665 | | assert!(sample_nearest(&img, 0., 0.).is_some()); |
1666 | | assert!(sample_nearest(&img, 1., 0.).is_some()); |
1667 | | assert!(sample_nearest(&img, 0., 1.).is_some()); |
1668 | | assert!(sample_nearest(&img, 1., 1.).is_some()); |
1669 | | assert!(sample_nearest(&img, 0.5, 0.5).is_some()); |
1670 | | |
1671 | | assert!(sample_nearest(&img, 1.2, 0.5).is_none()); |
1672 | | assert!(sample_nearest(&img, 0.5, 1.2).is_none()); |
1673 | | assert!(sample_nearest(&img, 1.2, 1.2).is_none()); |
1674 | | |
1675 | | assert!(sample_nearest(&img, -0.1, 0.2).is_none()); |
1676 | | assert!(sample_nearest(&img, 0.2, -0.1).is_none()); |
1677 | | assert!(sample_nearest(&img, -0.1, -0.1).is_none()); |
1678 | | } |
1679 | | #[test] |
1680 | | fn test_sample_bilinear_correctness() { |
1681 | | use crate::Rgba; |
1682 | | let img = ImageBuffer::from_fn(2, 2, |x, y| match (x, y) { |
1683 | | (0, 0) => Rgba([255, 0, 0, 0]), |
1684 | | (0, 1) => Rgba([0, 255, 0, 0]), |
1685 | | (1, 0) => Rgba([0, 0, 255, 0]), |
1686 | | (1, 1) => Rgba([0, 0, 0, 255]), |
1687 | | _ => panic!(), |
1688 | | }); |
1689 | | assert_eq!(sample_bilinear(&img, 0.5, 0.5), Some(Rgba([64; 4]))); |
1690 | | assert_eq!(sample_bilinear(&img, 0.0, 0.0), Some(Rgba([255, 0, 0, 0]))); |
1691 | | assert_eq!(sample_bilinear(&img, 0.0, 1.0), Some(Rgba([0, 255, 0, 0]))); |
1692 | | assert_eq!(sample_bilinear(&img, 1.0, 0.0), Some(Rgba([0, 0, 255, 0]))); |
1693 | | assert_eq!(sample_bilinear(&img, 1.0, 1.0), Some(Rgba([0, 0, 0, 255]))); |
1694 | | |
1695 | | assert_eq!( |
1696 | | sample_bilinear(&img, 0.5, 0.0), |
1697 | | Some(Rgba([128, 0, 128, 0])) |
1698 | | ); |
1699 | | assert_eq!( |
1700 | | sample_bilinear(&img, 0.0, 0.5), |
1701 | | Some(Rgba([128, 128, 0, 0])) |
1702 | | ); |
1703 | | assert_eq!( |
1704 | | sample_bilinear(&img, 0.5, 1.0), |
1705 | | Some(Rgba([0, 128, 0, 128])) |
1706 | | ); |
1707 | | assert_eq!( |
1708 | | sample_bilinear(&img, 1.0, 0.5), |
1709 | | Some(Rgba([0, 0, 128, 128])) |
1710 | | ); |
1711 | | } |
1712 | | #[bench] |
1713 | | #[cfg(feature = "benchmarks")] |
1714 | | fn bench_sample_bilinear(b: &mut test::Bencher) { |
1715 | | use crate::Rgba; |
1716 | | let img = ImageBuffer::from_fn(2, 2, |x, y| match (x, y) { |
1717 | | (0, 0) => Rgba([255, 0, 0, 0]), |
1718 | | (0, 1) => Rgba([0, 255, 0, 0]), |
1719 | | (1, 0) => Rgba([0, 0, 255, 0]), |
1720 | | (1, 1) => Rgba([0, 0, 0, 255]), |
1721 | | _ => panic!(), |
1722 | | }); |
1723 | | b.iter(|| { |
1724 | | sample_bilinear(&img, test::black_box(0.5), test::black_box(0.5)); |
1725 | | }); |
1726 | | } |
1727 | | #[test] |
1728 | | fn test_sample_nearest_correctness() { |
1729 | | use crate::Rgba; |
1730 | | let img = ImageBuffer::from_fn(2, 2, |x, y| match (x, y) { |
1731 | | (0, 0) => Rgba([255, 0, 0, 0]), |
1732 | | (0, 1) => Rgba([0, 255, 0, 0]), |
1733 | | (1, 0) => Rgba([0, 0, 255, 0]), |
1734 | | (1, 1) => Rgba([0, 0, 0, 255]), |
1735 | | _ => panic!(), |
1736 | | }); |
1737 | | |
1738 | | assert_eq!(sample_nearest(&img, 0.0, 0.0), Some(Rgba([255, 0, 0, 0]))); |
1739 | | assert_eq!(sample_nearest(&img, 0.0, 1.0), Some(Rgba([0, 255, 0, 0]))); |
1740 | | assert_eq!(sample_nearest(&img, 1.0, 0.0), Some(Rgba([0, 0, 255, 0]))); |
1741 | | assert_eq!(sample_nearest(&img, 1.0, 1.0), Some(Rgba([0, 0, 0, 255]))); |
1742 | | |
1743 | | assert_eq!(sample_nearest(&img, 0.5, 0.5), Some(Rgba([0, 0, 0, 255]))); |
1744 | | assert_eq!(sample_nearest(&img, 0.5, 0.0), Some(Rgba([0, 0, 255, 0]))); |
1745 | | assert_eq!(sample_nearest(&img, 0.0, 0.5), Some(Rgba([0, 255, 0, 0]))); |
1746 | | assert_eq!(sample_nearest(&img, 0.5, 1.0), Some(Rgba([0, 0, 0, 255]))); |
1747 | | assert_eq!(sample_nearest(&img, 1.0, 0.5), Some(Rgba([0, 0, 0, 255]))); |
1748 | | } |
1749 | | |
1750 | | #[bench] |
1751 | | #[cfg(all(feature = "benchmarks", feature = "tiff"))] |
1752 | | fn bench_resize_same_size(b: &mut test::Bencher) { |
1753 | | let path = concat!( |
1754 | | env!("CARGO_MANIFEST_DIR"), |
1755 | | "/tests/images/tiff/testsuite/mandrill.tiff" |
1756 | | ); |
1757 | | let image = crate::open(path).unwrap(); |
1758 | | b.iter(|| { |
1759 | | test::black_box(image.clone()).resize( |
1760 | | image.width(), |
1761 | | image.height(), |
1762 | | FilterType::CatmullRom, |
1763 | | ); |
1764 | | }); |
1765 | | b.bytes = u64::from(image.width() * image.height() * 3); |
1766 | | } |
1767 | | |
1768 | | #[test] |
1769 | | fn test_issue_186() { |
1770 | | let img: RgbImage = ImageBuffer::new(100, 100); |
1771 | | let _ = resize(&img, 50, 50, FilterType::Lanczos3); |
1772 | | } |
1773 | | |
1774 | | #[bench] |
1775 | | #[cfg(all(feature = "benchmarks", feature = "tiff"))] |
1776 | | fn bench_thumbnail(b: &mut test::Bencher) { |
1777 | | let path = concat!( |
1778 | | env!("CARGO_MANIFEST_DIR"), |
1779 | | "/tests/images/tiff/testsuite/mandrill.tiff" |
1780 | | ); |
1781 | | let image = crate::open(path).unwrap(); |
1782 | | b.iter(|| { |
1783 | | test::black_box(image.thumbnail(256, 256)); |
1784 | | }); |
1785 | | b.bytes = 512 * 512 * 4 + 256 * 256 * 4; |
1786 | | } |
1787 | | |
1788 | | #[bench] |
1789 | | #[cfg(all(feature = "benchmarks", feature = "tiff"))] |
1790 | | fn bench_thumbnail_upsize(b: &mut test::Bencher) { |
1791 | | let path = concat!( |
1792 | | env!("CARGO_MANIFEST_DIR"), |
1793 | | "/tests/images/tiff/testsuite/mandrill.tiff" |
1794 | | ); |
1795 | | let image = crate::open(path).unwrap().thumbnail(256, 256); |
1796 | | b.iter(|| { |
1797 | | test::black_box(image.thumbnail(512, 512)); |
1798 | | }); |
1799 | | b.bytes = 512 * 512 * 4 + 256 * 256 * 4; |
1800 | | } |
1801 | | |
1802 | | #[bench] |
1803 | | #[cfg(all(feature = "benchmarks", feature = "tiff"))] |
1804 | | fn bench_thumbnail_upsize_irregular(b: &mut test::Bencher) { |
1805 | | let path = concat!( |
1806 | | env!("CARGO_MANIFEST_DIR"), |
1807 | | "/tests/images/tiff/testsuite/mandrill.tiff" |
1808 | | ); |
1809 | | let image = crate::open(path).unwrap().thumbnail(193, 193); |
1810 | | b.iter(|| { |
1811 | | test::black_box(image.thumbnail(256, 256)); |
1812 | | }); |
1813 | | b.bytes = 193 * 193 * 4 + 256 * 256 * 4; |
1814 | | } |
1815 | | |
1816 | | #[test] |
1817 | | #[cfg(feature = "png")] |
1818 | | fn resize_transparent_image() { |
1819 | | use super::FilterType::{CatmullRom, Gaussian, Lanczos3, Nearest, Triangle}; |
1820 | | use crate::imageops::crop_imm; |
1821 | | use crate::RgbaImage; |
1822 | | |
1823 | | fn assert_resize(image: &RgbaImage, filter: FilterType) { |
1824 | | let resized = resize(image, 16, 16, filter); |
1825 | | let cropped = crop_imm(&resized, 5, 5, 6, 6).to_image(); |
1826 | | for pixel in cropped.pixels() { |
1827 | | let alpha = pixel.0[3]; |
1828 | | assert!( |
1829 | | alpha != 254 && alpha != 253, |
1830 | | "alpha value: {alpha}, {filter:?}" |
1831 | | ); |
1832 | | } |
1833 | | } |
1834 | | |
1835 | | let path = concat!( |
1836 | | env!("CARGO_MANIFEST_DIR"), |
1837 | | "/tests/images/png/transparency/tp1n3p08.png" |
1838 | | ); |
1839 | | let img = crate::open(path).unwrap(); |
1840 | | let rgba8 = img.as_rgba8().unwrap(); |
1841 | | let filters = &[Nearest, Triangle, CatmullRom, Gaussian, Lanczos3]; |
1842 | | for filter in filters { |
1843 | | assert_resize(rgba8, *filter); |
1844 | | } |
1845 | | } |
1846 | | |
1847 | | #[test] |
1848 | | fn bug_1600() { |
1849 | | let image = crate::RgbaImage::from_raw(629, 627, vec![255; 629 * 627 * 4]).unwrap(); |
1850 | | let result = resize(&image, 22, 22, FilterType::Lanczos3); |
1851 | | assert!(result.into_raw().into_iter().any(|c| c != 0)); |
1852 | | } |
1853 | | |
1854 | | #[test] |
1855 | | fn issue_2340() { |
1856 | | let empty = crate::GrayImage::from_raw(1 << 31, 0, vec![]).unwrap(); |
1857 | | // Really we're checking that no overflow / outsized allocation happens here. |
1858 | | let result = resize(&empty, 1, 1, FilterType::Lanczos3); |
1859 | | assert!(result.into_raw().into_iter().all(|c| c == 0)); |
1860 | | // With the previous strategy before the regression this would allocate 1TB of memory for a |
1861 | | // temporary during the sampling evaluation. |
1862 | | let result = resize(&empty, 256, 256, FilterType::Lanczos3); |
1863 | | assert!(result.into_raw().into_iter().all(|c| c == 0)); |
1864 | | } |
1865 | | |
1866 | | #[test] |
1867 | | fn issue_2340_refl() { |
1868 | | // Tests the swapped coordinate version of `issue_2340`. |
1869 | | let empty = crate::GrayImage::from_raw(0, 1 << 31, vec![]).unwrap(); |
1870 | | let result = resize(&empty, 1, 1, FilterType::Lanczos3); |
1871 | | assert!(result.into_raw().into_iter().all(|c| c == 0)); |
1872 | | let result = resize(&empty, 256, 256, FilterType::Lanczos3); |
1873 | | assert!(result.into_raw().into_iter().all(|c| c == 0)); |
1874 | | } |
1875 | | } |