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