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