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//! 
25//! [](https://crates.io/crates/image-pyramid)
26//! [](https://docs.rs/image-pyramid)
27//! [](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(¶ms)) {
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, ¶ms)?,
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, ¶ms)?;
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(¶ms));
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(¶ms));
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}