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