image/imageops/
sample.rs

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 = 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 = x.abs();
162
163    let k = 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, 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, 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, 0.0, 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| (0.0..=1.0).contains(c)) {
314        return None;
315    }
316
317    let (w, h) = img.dimensions();
318    if w == 0 || h == 0 {
319        return None;
320    }
321
322    let ui = w as f32 * u - 0.5;
323    let vi = h as f32 * v - 0.5;
324    interpolate_bilinear(
325        img,
326        ui.max(0.).min((w - 1) as f32),
327        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| (0.0..=1.0).contains(c)) {
338        return None;
339    }
340
341    let (w, h) = img.dimensions();
342    let ui = w as f32 * u - 0.5;
343    let ui = ui.max(0.).min((w.saturating_sub(1)) as f32);
344
345    let vi = h as f32 * v - 0.5;
346    let vi = vi.max(0.).min((h.saturating_sub(1)) as f32);
347    interpolate_nearest(img, ui, 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, h) = 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.round() as u32, 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 = 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::zeroed();
673
674    for y in bottom..top {
675        for x in left..right {
676            let k = image.get_pixel(x, y);
677            sum.add_pixel(k);
678        }
679    }
680
681    let n = <S::Larger as NumCast>::from((right - left) * (top - bottom)).unwrap();
682    let round = <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 = if sigma <= 0.0 { 1.0 } else { sigma };
964
965    let mut method = Filter {
966        kernel: Box::new(|x| gaussian(x, sigma)),
967        support: 2.0 * sigma,
968    };
969
970    let (width, height) = 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, height, &mut method);
976    horizontal_sample(&tmp, 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}