#![doc(html_root_url = "https://docs.rs/image-pyramid/0.5.1")]
#![doc(issue_tracker_base_url = "https://github.com/jnickg/image-pyramid/issues")]
#![deny(
nonstandard_style,
// unused,
unsafe_code,
future_incompatible,
rust_2018_idioms,
clippy::all,
clippy::nursery,
clippy::pedantic
)]
use std::fmt::Debug;
use image::{DynamicImage, GenericImage, GenericImageView, Pixel};
use num_traits::{clamp, Num, NumCast};
use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ImagePyramidError {
#[error("Invalid scale_factor value {0} (expected: 0.0 < scale_factor < 1.0)")]
BadScaleFactor(f32),
#[error("Functionality \"{0}\" is not yet implemented.")]
NotImplemented(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[derive(Debug, Copy, Clone)]
pub struct UnitIntervalValue(f32);
pub trait IntoUnitInterval {
fn into_unit_interval(self) -> Result<UnitIntervalValue, ImagePyramidError>;
}
impl IntoUnitInterval for f32 {
fn into_unit_interval(self) -> Result<UnitIntervalValue, ImagePyramidError> {
match self {
v if v <= 0.0 || v >= 1.0 => Err(ImagePyramidError::BadScaleFactor(v)),
_ => Ok(UnitIntervalValue(self)),
}
}
}
impl UnitIntervalValue {
pub fn new<T: IntoUnitInterval>(val: T) -> Result<Self, ImagePyramidError> {
val.into_unit_interval()
}
#[must_use]
pub const fn get(self) -> f32 { self.0 }
}
fn accumulate<P, K>(acc: &mut [K], pixel: &P, weight: K)
where
P: Pixel,
<P as Pixel>::Subpixel: Into<K>,
K: Num + Copy + Debug,
{
acc
.iter_mut()
.zip(pixel.channels().iter())
.for_each(|(a, c)| {
let new_val = <<P as Pixel>::Subpixel as Into<K>>::into(*c) * weight;
*a = *a + new_val;
});
}
struct Kernel<K> {
data: Vec<K>,
width: u32,
height: u32,
}
impl<K: Num + Copy + Debug> Kernel<K> {
pub fn new(data: &[K], width: u32, height: u32) -> Result<Self, ImagePyramidError> {
debug_assert!(width > 0 && height > 0, "width and height must be non-zero");
debug_assert!(
(width * height) as usize == data.len(),
"Invalid kernel len: expecting {}, found {}",
width * height,
data.len()
);
if width == 0 || height == 0 {
return Err(ImagePyramidError::Internal(
"width and height must be non-zero".to_string(),
));
}
if (width * height) as usize != data.len() {
return Err(ImagePyramidError::Internal(format!(
"Invalid kernel len: expecting {}, found {}",
width * height,
data.len()
)));
}
Ok(Self {
data: data.to_vec(),
width,
height,
})
}
pub fn new_normalized(data: &[K], width: u32, height: u32) -> Result<Kernel<f32>, ImagePyramidError>
where K: Into<f32>
{
let mut sum = K::zero();
for i in data {
sum = sum + *i;
}
let data_norm: Vec<f32> = data.iter().map(|x| <K as Into<f32>>::into(*x) / <K as Into<f32>>::into(sum)).collect();
Kernel::<f32>::new(&data_norm, width, height)
}
#[allow(unsafe_code)]
#[allow(unused)]
pub fn filter_in_place<I, F>(&self, image: &mut I, mut f: F)
where
I: GenericImage + Clone,
<<I as GenericImageView>::Pixel as Pixel>::Subpixel: Into<K>,
F: FnMut(&mut <<I as GenericImageView>::Pixel as Pixel>::Subpixel, K),
{
use core::cmp::{max, min};
let (width, height) = image.dimensions();
let num_channels = <<I as GenericImageView>::Pixel as Pixel>::CHANNEL_COUNT as usize;
let zero = K::zero();
let mut acc = vec![zero; num_channels];
#[allow(clippy::cast_lossless)]
let (k_width, k_height) = (self.width as i64, self.height as i64);
#[allow(clippy::cast_lossless)]
let (width, height) = (width as i64, height as i64);
for y in 0..height {
for x in 0..width {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let x_u32 = x as u32;
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let y_u32 = y as u32;
for k_y in 0..k_height {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let y_p = clamp(y + k_y - k_height / 2, 0, height - 1) as u32;
for k_x in 0..k_width {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let x_p = clamp(x + k_x - k_width / 2, 0, width - 1) as u32;
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let k_idx = (k_y * k_width + k_x) as usize;
accumulate(
&mut acc,
unsafe { &image.unsafe_get_pixel(x_p, y_p) },
unsafe { *self.data.get_unchecked(k_idx) },
);
}
}
let mut out_pel = image.get_pixel(x_u32, y_u32);
let out_channels = out_pel.channels_mut();
for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
f(c, *a);
*a = zero;
}
image.put_pixel(x_u32, y_u32, out_pel);
}
}
}
}
pub struct ImageToProcess<'a>(pub &'a DynamicImage);
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SmoothingType {
Gaussian,
Box,
Triangle,
}
#[derive(Debug, Clone)]
pub enum ImagePyramidType {
Lowpass,
Bandpass,
Steerable,
}
#[derive(Debug, Clone)]
pub struct ImagePyramidParams {
pub scale_factor: UnitIntervalValue,
pub pyramid_type: ImagePyramidType,
pub smoothing_type: SmoothingType,
}
impl Default for ImagePyramidParams {
fn default() -> Self {
Self {
scale_factor: UnitIntervalValue::new(0.5).unwrap(),
pyramid_type: ImagePyramidType::Lowpass,
smoothing_type: SmoothingType::Gaussian,
}
}
}
pub struct ImagePyramid {
pub levels: Vec<DynamicImage>,
pub params: ImagePyramidParams,
}
impl ImagePyramid {
pub fn create(
image: &DynamicImage,
params: Option<&ImagePyramidParams>,
) -> Result<Self, ImagePyramidError> {
let image_to_process = ImageToProcess(image);
let pyramid = image_to_process.compute_image_pyramid(params)?;
Ok(pyramid)
}
}
pub trait CanComputePyramid {
fn compute_image_pyramid(
&self,
params: Option<&ImagePyramidParams>,
) -> Result<ImagePyramid, ImagePyramidError>;
}
impl<'a> CanComputePyramid for ImageToProcess<'a> {
fn compute_image_pyramid(
&self,
params: Option<&ImagePyramidParams>,
) -> Result<ImagePyramid, ImagePyramidError> {
fn compute_lowpass_pyramid(
image: &DynamicImage,
params: &ImagePyramidParams,
) -> Result<Vec<DynamicImage>, ImagePyramidError> {
let mut levels = vec![image.clone()];
let kernel = match params.smoothing_type {
SmoothingType::Gaussian => Kernel::new_normalized(&[1u8, 2, 3, 2, 4, 2, 1, 2, 1], 3, 3)?,
SmoothingType::Box => Kernel::new_normalized(&[1u8, 1, 1, 1, 1, 1, 1, 1, 1], 3, 3)?,
SmoothingType::Triangle => Kernel::new_normalized(&[1u8, 2, 1, 2, 4, 2, 1, 2, 1], 3, 3)?,
};
let mut current_level = image.clone();
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_sign_loss)]
while current_level.width() > 1 && current_level.height() > 1 {
kernel.filter_in_place(&mut current_level, |c, a| *c = a as u8);
current_level = current_level.resize_exact(
(current_level.width() as f32 * params.scale_factor.get()) as u32,
(current_level.height() as f32 * params.scale_factor.get()) as u32,
image::imageops::FilterType::Gaussian,
);
levels.push(current_level.clone());
}
Ok(levels)
}
fn bandpass_in_place<I>(image: &mut I, other: &I)
where I: GenericImage {
use image::Primitive;
type Subpixel<I> = <<I as GenericImageView>::Pixel as Pixel>::Subpixel;
let mid_val = ((Subpixel::<I>::DEFAULT_MAX_VALUE - Subpixel::<I>::DEFAULT_MIN_VALUE)
/ NumCast::from(2).unwrap())
+ Subpixel::<I>::DEFAULT_MIN_VALUE;
debug_assert_eq!(image.dimensions(), other.dimensions());
let (width, height) = image.dimensions();
for y in 0..height {
for x in 0..width {
let other_p = other.get_pixel(x, y);
let mut p = image.get_pixel(x, y);
p.apply2(&other_p, |b1, b2| {
let diff = <f32 as NumCast>::from(b1).unwrap() - <f32 as NumCast>::from(b2).unwrap();
let new_val = <f32 as NumCast>::from(mid_val).unwrap() + diff;
NumCast::from(new_val).unwrap_or(mid_val)
});
image.put_pixel(x, y, p);
}
}
}
let params = params.map_or_else(ImagePyramidParams::default, std::clone::Clone::clone);
match params.pyramid_type {
ImagePyramidType::Lowpass =>
Ok(ImagePyramid {
levels: compute_lowpass_pyramid(self.0, ¶ms)?,
params: params.clone(),
}),
ImagePyramidType::Bandpass => {
let mut levels = compute_lowpass_pyramid(self.0, ¶ms)?;
for i in 0..levels.len() - 1 {
let next_level = levels[i + 1].resize_exact(
levels[i].width(),
levels[i].height(),
image::imageops::FilterType::Nearest,
);
bandpass_in_place(&mut levels[i], &next_level);
}
Ok(ImagePyramid {
levels,
params,
})
}
ImagePyramidType::Steerable =>
Err(ImagePyramidError::NotImplemented(
"ImagePyramidType::Steerable".to_string(),
)),
}
}
}
#[cfg(test)]
mod tests {
use test_case::test_matrix;
use super::*;
#[test]
fn kernel_filter_in_place() {
let mut image = DynamicImage::new_rgb8(3, 3);
let mut other = DynamicImage::new_rgb8(3, 3);
let mut i = 0;
for y in 0..3 {
for x in 0..3 {
let mut pel = image.get_pixel(x, y);
pel.apply_without_alpha(|_| i);
image.put_pixel(x, y, pel);
let mut pel = other.get_pixel(x, y);
pel.apply_without_alpha(|_| i + 1);
other.put_pixel(x, y, pel);
i += 1;
}
}
let kernel = Kernel::new_normalized(&[1u8, 2, 1, 2, 4, 2, 1, 2, 1], 3, 3).unwrap();
kernel.filter_in_place(&mut image, |c, a| *c = a as u8);
assert_eq!(image.get_pixel(1, 1), image::Rgba::<u8>([4, 4, 4, 255]));
}
#[test]
fn compute_image_pyramid_imagepyramidtype_steerable_unimplemented() {
let image = DynamicImage::new_rgb8(640, 480);
let ipr = ImageToProcess(&image);
let params = ImagePyramidParams {
pyramid_type: ImagePyramidType::Steerable,
..Default::default()
};
let pyramid = ipr.compute_image_pyramid(Some(¶ms));
assert!(pyramid.is_err());
}
#[test_matrix(
[ImagePyramidType::Lowpass, ImagePyramidType::Bandpass],
[SmoothingType::Gaussian, SmoothingType::Triangle, SmoothingType::Box]
)]
#[allow(clippy::needless_pass_by_value)]
fn compute_image_pyramid_every_type(
pyramid_type: ImagePyramidType,
smoothing_type: SmoothingType,
) {
let functors = vec![
DynamicImage::new_luma16,
DynamicImage::new_luma8,
DynamicImage::new_luma_a16,
DynamicImage::new_luma_a8,
DynamicImage::new_rgb16,
DynamicImage::new_rgb8,
DynamicImage::new_rgb32f,
DynamicImage::new_rgba16,
DynamicImage::new_rgba8,
DynamicImage::new_rgba32f,
];
for functor in functors {
let image = functor(128, 128);
let ipr = ImageToProcess(&image);
let params = ImagePyramidParams {
pyramid_type: pyramid_type.clone(),
smoothing_type: smoothing_type.clone(),
..Default::default()
};
let pyramid = ipr.compute_image_pyramid(Some(¶ms));
assert!(pyramid.is_ok());
let pyramid = pyramid.unwrap();
assert_eq!(pyramid.levels.len(), 8);
}
}
#[test]
fn into_unit_interval_f32() {
let i = 0.5f32.into_unit_interval();
assert!(i.is_ok());
assert_eq!(0.5f32, i.unwrap().get());
}
#[test]
fn into_unit_interval_err_when_0_0f32() {
let i = 0.0f32.into_unit_interval();
assert!(i.is_err());
}
#[test]
fn into_unit_interval_err_when_1_0f32() {
let i = 1.0f32.into_unit_interval();
assert!(i.is_err());
}
}