image_pyramid/
lib.rs

1// This file is part of the `image-pyramid` crate: <https://github.com/jnickg/image-pyramid>
2// Copyright (C) 2024 jnickg <jnickg83@gmail.com>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful, but
9// WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11// General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15//
16// Copyright (C) 2024 jnickg <jnickg83@gmail.com>
17// SPDX-License-Identifier: GPL-3.0-only
18
19#![doc(html_root_url = "https://docs.rs/image-pyramid/0.5.1")]
20#![doc(issue_tracker_base_url = "https://github.com/jnickg/image-pyramid/issues")]
21
22//! # Image Pyramid
23//!
24//! ![Maintenance](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg)
25//! [![crates-io](https://img.shields.io/crates/v/image-pyramid.svg)](https://crates.io/crates/image-pyramid)
26//! [![api-docs](https://docs.rs/image-pyramid/badge.svg)](https://docs.rs/image-pyramid)
27//! [![dependency-status](https://deps.rs/repo/github/jnickg/image-pyramid/status.svg)](https://deps.rs/repo/github/jnickg/image-pyramid)
28//!
29//! ## Overview
30//!
31//! This is a small Rust crate that facilitates quickly generating an image
32//! pyramid from a user-provided image.
33//!
34//! - See [OpenCV: Image Pyramids](https://docs.opencv.org/4.x/dc/dff/tutorial_py_pyramids.html)
35//!   for an overview of the two most common pyramid types, Lowpass (AKA
36//!   Gaussian) and Bandpass (AKA Laplacian).
37//! - The Tomasi paper [Lowpass and Bandpass Pyramids](https://courses.cs.duke.edu/cps274/fall14/notes/Pyramids.pdf)
38//!   has an authoritative explanation as well.
39//! - [Wikipedia](https://en.wikipedia.org/wiki/Pyramid_(image_processing)#Steerable_pyramid)
40//!   has a decent explanation of a steerable pyramid
41//!
42//! ## Usage
43//!
44//! See the [crates.io page](https://crates.io/crates/image-pyramid) for installation instructions, then check out the [examples directory](./examples/) for example code. Below is a simple illustrative example of computing a default pyramid (Gaussian where each level is half resolution).
45//!
46//! ```rust
47//! use image::DynamicImage;
48//! use image_pyramid::*;
49//!
50//! let image = DynamicImage::new_rgba8(640, 480); // Or load from file
51//! let pyramid = match ImagePyramid::create(&image, None) {
52//!   Ok(pyramid) => pyramid,
53//!   Err(e) => {
54//!     eprintln!("Error creating image pyramid: {}", e);
55//!     return;
56//!   }
57//! };
58//! ```
59//!
60//! Or a slightly more complex example, illustrating how to create a bandpass
61//! pyramid where each octave is 2/3 the resolution, smoothed using a triangle
62//! (linear) filter.
63//!
64//! ```rust
65//! use image::DynamicImage;
66//! use image_pyramid::*;
67//!
68//! let image = DynamicImage::new_rgba8(640, 480); // Or load from file
69//! let params = ImagePyramidParams {
70//!   scale_factor:   (2.0 / 3.0).into_unit_interval().unwrap(),
71//!   pyramid_type:   ImagePyramidType::Bandpass,
72//!   smoothing_type: SmoothingType::Triangle,
73//! };
74//! let pyramid = match ImagePyramid::create(&image, Some(&params)) {
75//!   Ok(pyramid) => pyramid,
76//!   Err(e) => {
77//!     eprintln!("Error creating image pyramid: {}", e);
78//!     return;
79//!   }
80//! };
81//! ```
82//!
83//! [`ImagePyramidParams::scale_factor`] field is a [`UnitIntervalValue`], which
84//! must be a floating-point value in the interval (0, 1). Creating a value of
85//! this type yields a [`Result`] and will contain an error if the value is not
86//! valid.
87//!
88//! ## Support
89//!
90//! Open an Issue with questions or bug reports, and feel free to open a PR with
91//! proposed changes.
92//!
93//! ## Contributing
94//!
95//! Follow standard Rust conventions, and be sure to add tests for any new code
96//! added.
97
98#![deny(
99  nonstandard_style,
100  // unused,
101  unsafe_code,
102  future_incompatible,
103  rust_2018_idioms,
104  clippy::all,
105  clippy::nursery,
106  clippy::pedantic
107)]
108
109use std::fmt::Debug;
110
111use image::{DynamicImage, GenericImage, GenericImageView, Pixel};
112use num_traits::{clamp, Num, NumCast};
113use thiserror::Error;
114
115/// An enumeration of the errors that may be emitted from the `image_pyramid`
116/// crate
117#[derive(Error, Debug)]
118#[non_exhaustive]
119pub enum ImagePyramidError {
120  /// Raised when the user provides an invalid scale value
121  #[error("Invalid scale_factor value {0} (expected: 0.0 < scale_factor < 1.0)")]
122  BadScaleFactor(f32),
123
124  /// Raised when the requested functionality is not yet supported.
125  #[error("Functionality \"{0}\" is not yet implemented.")]
126  NotImplemented(String),
127
128  /// Raised when something unexpected went wrong in the library.
129  #[error("Internal error: {0}")]
130  Internal(String),
131}
132
133/// A container for a value falling on the range (0.0, 1.0) (exclusive, meaning
134/// the values 0.0 and 1.0 are not valid)
135///
136/// This is useful for safely defining decreasing scale factors.
137///
138/// Because this type is currently used only for computing the resized
139/// dimensions of each level of the pyramid, the choice was made to support only
140/// [`f32`]. In the future support may be added for other floating-point types,
141/// such as [`f64`], rational values, or fixed-point, if there arises a need for
142/// such precision and/or performance.
143#[derive(Debug, Copy, Clone)]
144pub struct UnitIntervalValue(f32);
145
146/// A trait describing some floating-point type that can be converted to a
147/// unit-interval value (0.0 to 1.0, exclusive)
148pub trait IntoUnitInterval {
149  /// Attempts to convert this value into a guaranteed unit-interval value.
150  ///
151  /// Returns an error string if the value is not valid.
152  ///
153  /// # Errors
154  /// - The value is not within the unit range
155  fn into_unit_interval(self) -> Result<UnitIntervalValue, ImagePyramidError>;
156}
157
158impl IntoUnitInterval for f32 {
159  fn into_unit_interval(self) -> Result<UnitIntervalValue, ImagePyramidError> {
160    match self {
161      v if v <= 0.0 || v >= 1.0 => Err(ImagePyramidError::BadScaleFactor(v)),
162      _ => Ok(UnitIntervalValue(self)),
163    }
164  }
165}
166
167impl UnitIntervalValue {
168  /// Attempts to create a new instance from the provided value
169  ///
170  /// # Errors
171  /// - The value is not within the unit range
172  pub fn new<T: IntoUnitInterval>(val: T) -> Result<Self, ImagePyramidError> {
173    val.into_unit_interval()
174  }
175
176  /// Retrieves the stored value which is guaranteed to fall between 0.0 and 1.0
177  /// (exclusive)
178  #[must_use]
179  pub const fn get(self) -> f32 { self.0 }
180}
181
182fn accumulate<P, K>(acc: &mut [K], pixel: &P, weight: K)
183where
184  P: Pixel,
185  <P as Pixel>::Subpixel: Into<K>,
186  K: Num + Copy + Debug,
187{
188  acc
189    .iter_mut()
190    .zip(pixel.channels().iter())
191    .for_each(|(a, c)| {
192      let new_val = <<P as Pixel>::Subpixel as Into<K>>::into(*c) * weight;
193      *a = *a + new_val;
194    });
195}
196
197struct Kernel<K> {
198  data:   Vec<K>,
199  width:  u32,
200  height: u32,
201}
202
203impl<K: Num + Copy + Debug> Kernel<K> {
204  /// Construct a kernel from a slice and its dimensions. The input slice is
205  /// in row-major form. For example, a 3x3 matrix with data
206  /// `[0,1,0,1,2,1,0,1,0`] describes the following matrix:
207  ///
208  /// ```text
209  /// ┌       ┐
210  /// | 0 1 0 |  
211  /// | 1 2 1 |  
212  /// | 0 1 0 |
213  /// └       ┘
214  /// ```
215  ///
216  /// # Errors
217  ///
218  /// - If `width == 0 || height == 0`, [`ImagePyramidError::Internal`] is
219  ///   raised
220  /// - If the provided data does not match the size corresponding to the given
221  ///   dimensions
222  ///
223  /// # Panics
224  ///
225  /// In debug builds, this factory panics under the conditions that [`Err`] is
226  /// returned for release builds.
227  pub fn new(data: &[K], width: u32, height: u32) -> Result<Self, ImagePyramidError> {
228    debug_assert!(width > 0 && height > 0, "width and height must be non-zero");
229    debug_assert!(
230      (width * height) as usize == data.len(),
231      "Invalid kernel len: expecting {}, found {}",
232      width * height,
233      data.len()
234    );
235    // Take the above asserts and return Internal error when appropriate
236    if width == 0 || height == 0 {
237      return Err(ImagePyramidError::Internal(
238        "width and height must be non-zero".to_string(),
239      ));
240    }
241    if (width * height) as usize != data.len() {
242      return Err(ImagePyramidError::Internal(format!(
243        "Invalid kernel len: expecting {}, found {}",
244        width * height,
245        data.len()
246      )));
247    }
248
249    Ok(Self {
250      data: data.to_vec(),
251      width,
252      height,
253    })
254  }
255
256  /// Construct a kernel from a slice and its dimensions, normalizing the data
257  /// to sum to 1.0. The input slice is in row-major form. For example, a 3x3
258  /// matrix with data `[0,1,0,1,2,1,0,1,0`] describes the following matrix:
259  /// ```text
260  /// ┌       ┐
261  /// | 0 1 0 |
262  /// | 1 2 1 | / 6
263  /// | 0 1 0 |
264  /// └       ┘
265  /// ```
266  /// ...where `6` is computed dynamically by summing the elements of the
267  /// kernel. In other words, all the weights in a normalized kernel sum to
268  /// 1.0. This is useful, as many filters have this property
269  ///
270  /// # Errors
271  ///
272  /// - If `width == 0 || height == 0`, [`ImagePyramidError::Internal`] is
273  ///   raised
274  /// - If the provided data does not match the size corresponding to the given
275  ///   dimensions
276  ///
277  /// # Panics
278  ///
279  /// In debug builds, this factory panics under the conditions that [`Err`] is
280  /// returned for release builds.
281  pub fn new_normalized(data: &[K], width: u32, height: u32) -> Result<Kernel<f32>, ImagePyramidError>
282    where K: Into<f32>
283  {
284    let mut sum = K::zero();
285    for i in data {
286      sum = sum + *i;
287    }
288    let data_norm: Vec<f32> = data.iter().map(|x| <K as Into<f32>>::into(*x) / <K as Into<f32>>::into(sum)).collect();
289    Kernel::<f32>::new(&data_norm, width, height)
290  }
291
292  /// Returns 2d correlation of an image. Intermediate calculations are
293  /// performed at type K, and the results converted to pixel Q via f. Pads by
294  /// continuity.
295  #[allow(unsafe_code)]
296  #[allow(unused)]
297  pub fn filter_in_place<I, F>(&self, image: &mut I, mut f: F)
298  where
299    I: GenericImage + Clone,
300    <<I as GenericImageView>::Pixel as Pixel>::Subpixel: Into<K>,
301    F: FnMut(&mut <<I as GenericImageView>::Pixel as Pixel>::Subpixel, K),
302  {
303    use core::cmp::{max, min};
304    let (width, height) = image.dimensions();
305    let num_channels = <<I as GenericImageView>::Pixel as Pixel>::CHANNEL_COUNT as usize;
306    let zero = K::zero();
307    let mut acc = vec![zero; num_channels];
308    #[allow(clippy::cast_lossless)]
309    let (k_width, k_height) = (self.width as i64, self.height as i64);
310    #[allow(clippy::cast_lossless)]
311    let (width, height) = (width as i64, height as i64);
312
313    for y in 0..height {
314      for x in 0..width {
315        #[allow(clippy::cast_possible_truncation)]
316        #[allow(clippy::cast_sign_loss)]
317        let x_u32 = x as u32;
318        #[allow(clippy::cast_possible_truncation)]
319        #[allow(clippy::cast_sign_loss)]
320        let y_u32 = y as u32;
321        for k_y in 0..k_height {
322          #[allow(clippy::cast_possible_truncation)]
323          #[allow(clippy::cast_sign_loss)]
324          let y_p = clamp(y + k_y - k_height / 2, 0, height - 1) as u32;
325          for k_x in 0..k_width {
326            #[allow(clippy::cast_possible_truncation)]
327            #[allow(clippy::cast_sign_loss)]
328            let x_p = clamp(x + k_x - k_width / 2, 0, width - 1) as u32;
329            #[allow(clippy::cast_possible_truncation)]
330            #[allow(clippy::cast_sign_loss)]
331            let k_idx = (k_y * k_width + k_x) as usize;
332
333            accumulate(
334              &mut acc,
335              unsafe { &image.unsafe_get_pixel(x_p, y_p) },
336              unsafe { *self.data.get_unchecked(k_idx) },
337            );
338          }
339        }
340        let mut out_pel = image.get_pixel(x_u32, y_u32);
341        let out_channels = out_pel.channels_mut();
342        for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
343          f(c, *a);
344          *a = zero;
345        }
346        image.put_pixel(x_u32, y_u32, out_pel);
347      }
348    }
349  }
350}
351
352/// A simple wrapper extending the functionality of the given image with
353/// image-pyramid support
354pub struct ImageToProcess<'a>(pub &'a DynamicImage);
355
356/// How to smooth an image when downsampling
357///
358/// For now, these all use a 3x3 kernel for smoothing. As a consequence, the
359/// Gaussian and Triangle smoothing types produce identical results
360#[derive(Debug, Clone)]
361#[non_exhaustive]
362pub enum SmoothingType {
363  /// Use a Gaussian filter
364  /// `[[1,2,1],[2,4,2],[1,2,1]] * 1/16`
365  Gaussian,
366  /// Use a linear box filter
367  /// `[[1,1,1],[1,1,1],[1,1,1]] * 1/9`
368  Box,
369  /// Use a linear triangle filter:
370  /// `[[1,2,1],[2,4,2],[1,2,1]] * 1/16`
371  Triangle,
372}
373
374/// What type of pyramid to compute. Each has different properties,
375/// applications, and computation cost.
376#[derive(Debug, Clone)]
377pub enum ImagePyramidType {
378  /// Use smoothing & subsampling to compute pyramid. This is used to generate
379  /// mipmaps, thumbnails, display low-resolution previews of expensive image
380  /// processing operations, texture synthesis, and more.
381  Lowpass,
382
383  /// AKA Laplacian pyramid, where adjacent levels of the lowpass pyramid are
384  /// upscaled and their pixel differences are computed. This used in image
385  /// processing routines such as blending.
386  Bandpass,
387
388  /// Uses a bank of multi-orientation bandpass filters. Used for used for
389  /// applications including image compression, texture synthesis, and object
390  /// recognition.
391  Steerable,
392}
393
394/// The set of parameters required for computing an image pyramid. For most
395/// applications, the default set of parameters is correct.
396#[derive(Debug, Clone)]
397pub struct ImagePyramidParams {
398  /// The scale factor to use on image dimensions when downsampling. This is
399  /// most commonly 0.5
400  pub scale_factor: UnitIntervalValue,
401
402  /// What type of pyramid to compute. See [`ImagePyramidType`] for more
403  /// information.
404  pub pyramid_type: ImagePyramidType,
405
406  /// What type of smoothing to use when computing pyramid levels. See
407  /// [`SmoothingType`] for more information.
408  pub smoothing_type: SmoothingType,
409}
410
411/// Generates a useful default set of parameters.
412///
413/// Defaults to a traditional image pyramid: Gaussian lowpass image pyramid with
414/// scale factor of 0.5.
415impl Default for ImagePyramidParams {
416  fn default() -> Self {
417    Self {
418      scale_factor:   UnitIntervalValue::new(0.5).unwrap(),
419      pyramid_type:   ImagePyramidType::Lowpass,
420      smoothing_type: SmoothingType::Gaussian,
421    }
422  }
423}
424
425/// A computed image pyramid and its associated metadata.
426///
427/// Image pyramids consist of multiple, successively smaller-scale versions of
428/// an original image. These are called the _levels_ (sometimes called
429/// _octaves_) of the image pyramid.
430///
431/// Closely related to a traditional Gaussian image pyramid is a mipmap, which
432/// is a specific application of the more general image pyramid concept. A
433/// mipmap is essentially a way of storing a `scale=0.5` lowpass image pyramid
434/// such that an appropriate octave can be sampled by a graphics renderer, for
435/// the purpose of avoiding anti-aliasing.
436pub struct ImagePyramid {
437  /// The ordered levels of the pyramid. Index N refers to pyramid level N.
438  /// Depending on the scale factor S in `params`, and image dimensions `(W,
439  /// H)`, there will be `ceil(log_{1/S}(min(W, H)))` levels.
440  ///
441  /// For example, a `(800, 600)` image with scale factor `S=0.5` will have
442  /// `ceil(log_2(600))` levels, which comes out to `10`. Similarly, a `(640,
443  /// 480)` image would have `(ceil(log_2(480))` (`9`) levels.
444  pub levels: Vec<DynamicImage>,
445
446  /// A copy of the parameters used to compute the levels in this pyramid.
447  pub params: ImagePyramidParams,
448}
449
450impl ImagePyramid {
451  /// Create a new image pyramid for the given image, using the optionally
452  /// provided parameters.
453  ///
454  /// If no parameters are passed, the default parameters will be used.
455  ///
456  /// # Errors
457  /// See [`CanComputePyramid::compute_image_pyramid`] for errors that may be
458  /// raised
459  pub fn create(
460    image: &DynamicImage,
461    params: Option<&ImagePyramidParams>,
462  ) -> Result<Self, ImagePyramidError> {
463    let image_to_process = ImageToProcess(image);
464    let pyramid = image_to_process.compute_image_pyramid(params)?;
465
466    Ok(pyramid)
467  }
468}
469
470/// Describes types that can compute their own image pyramid
471pub trait CanComputePyramid {
472  /// Compute an image pyramid for this instance's data, using the optionally
473  /// provided parameters.
474  ///
475  /// If no parameters are passed, the default parameters will be used.
476  ///
477  /// # Errors
478  /// Errors of type [`ImagePyramidError::NotImplemented`] are raised for the
479  /// following parameter values, which are not yet implemented:
480  ///
481  /// - [`SmoothingType::Box`] - This smoothing type is not yet supported in the
482  ///   `image` crate and is also not yet implemented manually
483  /// - [`ImagePyramidType::Steerable`] - Not yet implemented
484  fn compute_image_pyramid(
485    &self,
486    params: Option<&ImagePyramidParams>,
487  ) -> Result<ImagePyramid, ImagePyramidError>;
488}
489
490impl<'a> CanComputePyramid for ImageToProcess<'a> {
491  fn compute_image_pyramid(
492    &self,
493    params: Option<&ImagePyramidParams>,
494  ) -> Result<ImagePyramid, ImagePyramidError> {
495    /// Compute a lowpass pyramid with the given params. Ignores
496    /// `params.pyramid_type`.
497    fn compute_lowpass_pyramid(
498      image: &DynamicImage,
499      params: &ImagePyramidParams,
500    ) -> Result<Vec<DynamicImage>, ImagePyramidError> {
501      let mut levels = vec![image.clone()];
502      let kernel = match params.smoothing_type {
503        SmoothingType::Gaussian => Kernel::new_normalized(&[1u8, 2, 3, 2, 4, 2, 1, 2, 1], 3, 3)?,
504        SmoothingType::Box => Kernel::new_normalized(&[1u8, 1, 1, 1, 1, 1, 1, 1, 1], 3, 3)?,
505        SmoothingType::Triangle => Kernel::new_normalized(&[1u8, 2, 1, 2, 4, 2, 1, 2, 1], 3, 3)?,
506      };
507      let mut current_level = image.clone();
508      #[allow(clippy::cast_possible_truncation)]
509      #[allow(clippy::cast_precision_loss)]
510      #[allow(clippy::cast_sign_loss)]
511      while current_level.width() > 1 && current_level.height() > 1 {
512        kernel.filter_in_place(&mut current_level, |c, a| *c = a as u8);
513        current_level = current_level.resize_exact(
514          (current_level.width() as f32 * params.scale_factor.get()) as u32,
515          (current_level.height() as f32 * params.scale_factor.get()) as u32,
516          image::imageops::FilterType::Gaussian,
517        );
518        levels.push(current_level.clone());
519      }
520      Ok(levels)
521    }
522
523    /// Takes the diference in pixel values between `image` and `other`, adds
524    /// that value to the center of the Subpixel container type's range, and
525    /// applies the result to `image`.
526    fn bandpass_in_place<I>(image: &mut I, other: &I)
527    where I: GenericImage {
528      use image::Primitive;
529      type Subpixel<I> = <<I as GenericImageView>::Pixel as Pixel>::Subpixel;
530      // The center value for the given container type. We leverage the `image`
531      // crate's definition of these values to be 1.0 and 0.0 respectively, for
532      // floating-point types (where we want `mid_val` to be 0.5). For unsigned
533      // integer types, we should get half the primitive container's capacity
534      // (e.g. 127 for a u8)
535      let mid_val = ((Subpixel::<I>::DEFAULT_MAX_VALUE - Subpixel::<I>::DEFAULT_MIN_VALUE)
536        / NumCast::from(2).unwrap())
537        + Subpixel::<I>::DEFAULT_MIN_VALUE;
538      debug_assert_eq!(image.dimensions(), other.dimensions());
539      // Iterate through pixels and compute difference. Add difference to
540      // mid_val and apply that to i1
541      let (width, height) = image.dimensions();
542      for y in 0..height {
543        for x in 0..width {
544          let other_p = other.get_pixel(x, y);
545          let mut p = image.get_pixel(x, y);
546          p.apply2(&other_p, |b1, b2| {
547            let diff = <f32 as NumCast>::from(b1).unwrap() - <f32 as NumCast>::from(b2).unwrap();
548            let new_val = <f32 as NumCast>::from(mid_val).unwrap() + diff;
549            NumCast::from(new_val).unwrap_or(mid_val)
550          });
551          image.put_pixel(x, y, p);
552        }
553      }
554    }
555
556    // If unspecified, use default parameters.
557    let params = params.map_or_else(ImagePyramidParams::default, std::clone::Clone::clone);
558
559    match params.pyramid_type {
560      ImagePyramidType::Lowpass =>
561        Ok(ImagePyramid {
562          levels: compute_lowpass_pyramid(self.0, &params)?,
563          params: params.clone(),
564        }),
565      ImagePyramidType::Bandpass => {
566        // First, we need a lowpass pyramid to work with.
567        let mut levels = compute_lowpass_pyramid(self.0, &params)?;
568
569        // For each index N, upscale the resolution N+1 to match N's resolution.
570        // Then we compute the pixel-wise difference between them, and
571        // store the result in the current level
572        for i in 0..levels.len() - 1 {
573          let next_level = levels[i + 1].resize_exact(
574            levels[i].width(),
575            levels[i].height(),
576            image::imageops::FilterType::Nearest,
577          );
578          bandpass_in_place(&mut levels[i], &next_level);
579        }
580
581        Ok(ImagePyramid {
582          levels,
583          params,
584        })
585      }
586      ImagePyramidType::Steerable =>
587        Err(ImagePyramidError::NotImplemented(
588          "ImagePyramidType::Steerable".to_string(),
589        )),
590    }
591  }
592}
593
594#[cfg(test)]
595mod tests {
596  use test_case::test_matrix;
597
598  use super::*;
599
600  #[test]
601  fn kernel_filter_in_place() {
602    let mut image = DynamicImage::new_rgb8(3, 3);
603    let mut other = DynamicImage::new_rgb8(3, 3);
604    let mut i = 0;
605    for y in 0..3 {
606      for x in 0..3 {
607        let mut pel = image.get_pixel(x, y);
608        pel.apply_without_alpha(|_| i);
609        image.put_pixel(x, y, pel);
610
611        let mut pel = other.get_pixel(x, y);
612        pel.apply_without_alpha(|_| i + 1);
613        other.put_pixel(x, y, pel);
614        i += 1;
615      }
616    }
617    let kernel = Kernel::new_normalized(&[1u8, 2, 1, 2, 4, 2, 1, 2, 1], 3, 3).unwrap();
618    kernel.filter_in_place(&mut image, |c, a| *c = a as u8);
619    assert_eq!(image.get_pixel(1, 1), image::Rgba::<u8>([4, 4, 4, 255]));
620  }
621
622  #[test]
623  fn compute_image_pyramid_imagepyramidtype_steerable_unimplemented() {
624    let image = DynamicImage::new_rgb8(640, 480);
625    let ipr = ImageToProcess(&image);
626
627    let params = ImagePyramidParams {
628      pyramid_type: ImagePyramidType::Steerable,
629      ..Default::default()
630    };
631
632    let pyramid = ipr.compute_image_pyramid(Some(&params));
633    assert!(pyramid.is_err());
634  }
635
636  #[test_matrix(
637    [ImagePyramidType::Lowpass, ImagePyramidType::Bandpass],
638    [SmoothingType::Gaussian, SmoothingType::Triangle, SmoothingType::Box]
639  )]
640  #[allow(clippy::needless_pass_by_value)]
641  fn compute_image_pyramid_every_type(
642    pyramid_type: ImagePyramidType,
643    smoothing_type: SmoothingType,
644  ) {
645    // test_case crate won't let these be parameterized so we loop through them
646    // here.
647    let functors = vec![
648      DynamicImage::new_luma16,
649      DynamicImage::new_luma8,
650      DynamicImage::new_luma_a16,
651      DynamicImage::new_luma_a8,
652      DynamicImage::new_rgb16,
653      DynamicImage::new_rgb8,
654      DynamicImage::new_rgb32f,
655      DynamicImage::new_rgba16,
656      DynamicImage::new_rgba8,
657      DynamicImage::new_rgba32f,
658    ];
659    for functor in functors {
660      let image = functor(128, 128);
661      let ipr = ImageToProcess(&image);
662
663      let params = ImagePyramidParams {
664        pyramid_type: pyramid_type.clone(),
665        smoothing_type: smoothing_type.clone(),
666        ..Default::default()
667      };
668
669      let pyramid = ipr.compute_image_pyramid(Some(&params));
670      assert!(pyramid.is_ok());
671      let pyramid = pyramid.unwrap();
672      assert_eq!(pyramid.levels.len(), 8);
673    }
674  }
675
676  #[test]
677  fn into_unit_interval_f32() {
678    let i = 0.5f32.into_unit_interval();
679    assert!(i.is_ok());
680    assert_eq!(0.5f32, i.unwrap().get());
681  }
682
683  #[test]
684  fn into_unit_interval_err_when_0_0f32() {
685    let i = 0.0f32.into_unit_interval();
686    assert!(i.is_err());
687  }
688
689  #[test]
690  fn into_unit_interval_err_when_1_0f32() {
691    let i = 1.0f32.into_unit_interval();
692    assert!(i.is_err());
693  }
694}