Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removed allocation from filter_pixel() #656

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 110 additions & 86 deletions src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod box_filter;
pub use self::box_filter::box_filter;

use image::{GenericImageView, GrayImage, Luma, Pixel, Primitive};
use itertools::Itertools;

use crate::definitions::{Clamp, Image};
use crate::kernel::{self, Kernel};
Expand All @@ -21,107 +22,131 @@ use num::Num;
use std::cmp::{max, min};
use std::f32;

/// Returns 2d correlation of an image. Intermediate calculations are performed
/// at type K, and the results converted to pixel Q via f. Pads by continuity.
/// Calculates the new pixel value for a particular pixel and kernel.
///
/// # Panics
///
/// If `P::CHANNEL_COUNT != Q::CHANNEL_COUNT`
pub fn filter<P, K, F, Q>(image: &Image<P>, kernel: Kernel<K>, mut f: F) -> Image<Q>
fn filter_pixel<P, K, F, Q>(x: u32, y: u32, kernel: Kernel<K>, f: F, image: &Image<P>) -> Q
where
P: Pixel,
<P as Pixel>::Subpixel: Into<K>,
Q: Pixel,
F: FnMut(K) -> Q::Subpixel,
K: Copy + Num,
F: Fn(K) -> Q::Subpixel,
K: num::Num + Copy + From<P::Subpixel>,
{
assert_eq!(P::CHANNEL_COUNT, Q::CHANNEL_COUNT);
let num_channels = P::CHANNEL_COUNT as usize;
let zero = K::zero();

let (width, height) = (image.width() as i64, image.height() as i64);
let (k_width, k_height) = (kernel.width as i64, kernel.height as i64);
let (x, y) = (i64::from(x), i64::from(y));

let weighted_pixels = (0..kernel.height as i64)
.cartesian_product(0..kernel.width as i64)
.map(|(k_y, k_x)| {
let kernel_weight = *kernel.at(k_x as u32, k_y as u32);

let window_y = (y + k_y - k_height / 2).clamp(0, height - 1);
let window_x = (x + k_x - k_width / 2).clamp(0, width - 1);

debug_assert!(image.in_bounds(window_x as u32, window_y as u32));

// Safety: we clamped `window_x` and `window_y` to be in bounds.
let window_pixel = unsafe { image.unsafe_get_pixel(window_x as u32, window_y as u32) };

if P::CHANNEL_COUNT == 1 {
[
kernel_weight * K::from(window_pixel.channels()[0]),
K::zero(),
K::zero(),
K::zero(),
]
} else if P::CHANNEL_COUNT == 2 {
[
kernel_weight * K::from(window_pixel.channels()[0]),
kernel_weight * K::from(window_pixel.channels()[1]),
K::zero(),
K::zero(),
]
} else if P::CHANNEL_COUNT == 3 {
[
kernel_weight * K::from(window_pixel.channels()[0]),
kernel_weight * K::from(window_pixel.channels()[1]),
kernel_weight * K::from(window_pixel.channels()[2]),
K::zero(),
]
} else if P::CHANNEL_COUNT == 4 {
[
kernel_weight * K::from(window_pixel.channels()[0]),
kernel_weight * K::from(window_pixel.channels()[1]),
kernel_weight * K::from(window_pixel.channels()[2]),
kernel_weight * K::from(window_pixel.channels()[3]),
]
} else {
panic!("P::CHANNEL_COUNT must be smaller than or equal to 4");
}
});

let final_channel_sum: [K; 4] =
weighted_pixels.fold([K::zero(); 4], |mut accumulator, weighted_pixel| {
for (i, weighted_subpixel) in weighted_pixel.into_iter().enumerate() {
accumulator[i] = accumulator[i] + weighted_subpixel;
}

accumulator
});

let mapped_final = final_channel_sum.map(f);

*Q::from_slice(&mapped_final[0..Q::CHANNEL_COUNT as usize])
}

/// Returns 2d correlation of an image. Intermediate calculations are performed
/// at type K, and the results converted to pixel Q via f. Pads by continuity.
pub fn filter<P, K, F, Q>(image: &Image<P>, kernel: Kernel<K>, f: F) -> Image<Q>
where
P: Pixel,
Q: Pixel,
F: Fn(K) -> Q::Subpixel,
K: num::Num + Copy + From<P::Subpixel>,
{
let (width, height) = image.dimensions();

let mut out = Image::<Q>::new(width, height);
let mut acc = vec![zero; num_channels];
let (k_width, k_height) = (kernel.width as i64, kernel.height as i64);
let (width, height) = (width as i64, height as i64);

for y in 0..height {
for x in 0..width {
for k_y in 0..k_height {
let y_p = min(height - 1, max(0, y + k_y - k_height / 2)) as u32;
for k_x in 0..k_width {
let x_p = min(width - 1, max(0, x + k_x - k_width / 2)) as u32;

debug_assert!(image.in_bounds(x_p, y_p));
accumulate(
&mut acc,
unsafe { &image.unsafe_get_pixel(x_p, y_p) },
unsafe { kernel.get_unchecked(k_x as u32, k_y as u32) },
);
}
}
let out_channels = out.get_pixel_mut(x as u32, y as u32).channels_mut();
for (a, c) in acc.iter_mut().zip(out_channels) {
*c = f(*a);
*a = zero;
}
out.put_pixel(x, y, filter_pixel(x, y, kernel, &f, image));
}
}

out
}

#[cfg(feature = "rayon")]
#[doc = generate_parallel_doc_comment!("filter")]
pub fn filter_parallel<P, K, F, Q>(image: &Image<P>, kernel: Kernel<K>, f: F) -> Image<Q>
where
P: Pixel + Sync,
P::Subpixel: Into<K> + Sync,
Q: Pixel + Send,
F: Sync + Fn(K) -> Q::Subpixel,
K: Copy + Num + Sync,
Q: Pixel + Send + Sync,
P::Subpixel: Sync,
Q::Subpixel: Send + Sync,
F: Fn(K) -> Q::Subpixel + Send + Sync,
K: Num + Copy + From<P::Subpixel> + Sync,
{
use num::Zero;
use rayon::prelude::*;
use rayon::iter::IndexedParallelIterator;
use rayon::iter::ParallelIterator;

assert_eq!(P::CHANNEL_COUNT, Q::CHANNEL_COUNT);
let num_channels = P::CHANNEL_COUNT as usize;

let (k_width, k_height) = (kernel.width as i64, kernel.height as i64);
let (width, height) = (image.width() as i64, image.height() as i64);
let (width, height) = image.dimensions();

let out_rows: Vec<Vec<_>> = (0..height)
.into_par_iter()
.map(|y| {
let mut out_row = Vec::with_capacity(image.width() as usize);
let mut out_pixel = vec![Q::Subpixel::zero(); num_channels];
let mut acc = vec![K::zero(); num_channels];
let mut out: Image<Q> = Image::new(width, height);

for x in 0..width {
for k_y in 0..k_height {
let y_p = min(height - 1, max(0, y + k_y - k_height / 2)) as u32;
for k_x in 0..k_width {
let x_p = min(width - 1, max(0, x + k_x - k_width / 2)) as u32;

debug_assert!(image.in_bounds(x_p, y_p));
accumulate(
&mut acc,
unsafe { &image.unsafe_get_pixel(x_p, y_p) },
unsafe { kernel.get_unchecked(k_x as u32, k_y as u32) },
);
}
}
for (a, c) in acc.iter_mut().zip(out_pixel.iter_mut()) {
*c = f(*a);
*a = K::zero();
}
out_row.push(*Q::from_slice(&out_pixel));
}
out_row
})
.collect();
image
.par_enumerate_pixels()
.zip_eq(out.par_pixels_mut())
.for_each(move |((x, y, _), output_pixel)| {
*output_pixel = filter_pixel(x, y, kernel, &f, image);
});

Image::from_fn(image.width(), image.height(), |x, y| {
out_rows[y as usize][x as usize]
})
out
}

#[inline]
Expand Down Expand Up @@ -203,26 +228,26 @@ where
/// the crate `rayon` feature is enabled.
pub fn filter_clamped<P, K, S>(image: &Image<P>, kernel: Kernel<K>) -> Image<ChannelMap<P, S>>
where
P: WithChannel<S>,
P::Subpixel: Into<K>,
K: Num + Copy + From<<P as image::Pixel>::Subpixel>,
S: Clamp<K> + Primitive,
P: WithChannel<S>,
K: Num + Copy + From<<P as image::Pixel>::Subpixel>,
{
filter(image, kernel, S::clamp)
}

#[cfg(feature = "rayon")]
#[doc = generate_parallel_doc_comment!("filter_clamped")]
pub fn filter_clamped_parallel<P, K, S>(
image: &Image<P>,
kernel: Kernel<K>,
) -> Image<ChannelMap<P, S>>
where
P: Sync + WithChannel<S>,
P::Subpixel: Into<K> + Sync,
<P as WithChannel<S>>::Pixel: Send,
K: Num + Copy + From<<P as image::Pixel>::Subpixel> + Sync,
S: Clamp<K> + Primitive + Send,
P: Sync,
P::Subpixel: Send + Sync,
<P as WithChannel<S>>::Pixel: Send + Sync,
S: Clamp<K> + Primitive + Send + Sync,
P: WithChannel<S>,
K: Num + Copy + Send + Sync + From<P::Subpixel>,
{
filter_parallel(image, kernel, S::clamp)
}
Expand Down Expand Up @@ -409,12 +434,12 @@ where
fn accumulate<P, K>(acc: &mut [K], pixel: &P, weight: K)
where
P: Pixel,
P::Subpixel: Into<K>,
<P as Pixel>::Subpixel: Into<K>,
K: Num + Copy,
{
acc.iter_mut().zip(pixel.channels()).for_each(|(a, &c)| {
*a = *a + c.into() * weight;
});
for i in 0..(P::CHANNEL_COUNT as usize) {
acc[i] = acc[i] + pixel.channels()[i].into() * weight;
}
}

fn clamp_and_reset<P, K>(acc: &mut [K], out_channels: &mut [P::Subpixel], zero: K)
Expand Down Expand Up @@ -442,7 +467,6 @@ where
pub fn laplacian_filter(image: &GrayImage) -> Image<Luma<i16>> {
filter_clamped(image, kernel::FOUR_LAPLACIAN_3X3)
}

#[must_use = "the function does not modify the original image"]
#[cfg(feature = "rayon")]
#[doc = generate_parallel_doc_comment!("laplacian_filter")]
Expand Down
19 changes: 7 additions & 12 deletions src/gradients.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Functions for computing gradients of image intensities.

use crate::definitions::{HasBlack, Image};
use crate::filter::filter_clamped;
use crate::definitions::{Clamp, HasBlack, Image};
use crate::filter::{filter, filter_clamped};
use crate::kernel::{self, Kernel};
use crate::map::{ChannelMap, WithChannel};
use image::{GenericImage, GenericImageView, GrayImage, Luma, Pixel};
Expand Down Expand Up @@ -95,8 +95,8 @@ pub fn gradients_greyscale<P, F, Q>(
/// # }
pub fn gradients<P, F, Q>(
image: &Image<P>,
horizontal_kernel: Kernel<i32>,
vertical_kernel: Kernel<i32>,
kernel1: Kernel<i32>,
kernel2: Kernel<i32>,
f: F,
) -> Image<Q>
where
Expand All @@ -105,8 +105,8 @@ where
ChannelMap<P, u16>: HasBlack,
F: Fn(ChannelMap<P, u16>) -> Q,
{
let horizontal = filter_clamped::<_, _, i16>(image, horizontal_kernel);
let vertical = filter_clamped::<_, _, i16>(image, vertical_kernel);
let pass1: Image<ChannelMap<P, i16>> = filter(image, kernel1, <i16 as Clamp<i32>>::clamp);
let pass2: Image<ChannelMap<P, i16>> = filter(image, kernel2, <i16 as Clamp<i32>>::clamp);

let (width, height) = image.dimensions();
let mut out = Image::<Q>::new(width, height);
Expand All @@ -122,12 +122,7 @@ where
// x and y are in bounds for image by construction,
// vertical and horizontal are the result of calling filter_clamped on image,
// and filter_clamped returns an image of the same size as its input
let (h, v) = unsafe {
(
horizontal.unsafe_get_pixel(x, y),
vertical.unsafe_get_pixel(x, y),
)
};
let (h, v) = unsafe { (pass1.unsafe_get_pixel(x, y), pass2.unsafe_get_pixel(x, y)) };
let mut p = ChannelMap::<P, u16>::black();

for (h, v, p) in multizip((h.channels(), v.channels(), p.channels_mut())) {
Expand Down
34 changes: 5 additions & 29 deletions src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@

/// An borrowed 2D kernel, used to filter images via convolution.
#[derive(Debug, Copy, Clone)]
pub struct Kernel<'a, K>
where
K: Copy,
{
pub struct Kernel<'a, K> {
pub(crate) data: &'a [K],
pub(crate) width: u32,
pub(crate) height: u32,
}

impl<'a, K> Kernel<'a, K>
where
K: Copy,
{
impl<'a, K> Kernel<'a, K> {
/// Construct a kernel from a slice and its dimensions. The input slice is
/// in row-major order.
///
Expand All @@ -35,28 +29,10 @@ where
///
/// # Panics
///
/// If the `y * kernel.width + x` is outside of the kernel data.
/// If the `x` or `y` is outside of the width or height of the kernel.
#[inline]
pub fn get(&self, x: u32, y: u32) -> K {
debug_assert!(x < self.width);
debug_assert!(y < self.height);
let at = usize::try_from(y * self.width + x).unwrap();
self.data[at]
}

/// Get the value in the kernel at the given `x` and `y` position, without
/// doing bounds checking.
///
/// # Safety
/// The caller must ensure that `y * self.width + x` is in bounds of the
/// kernel data.
#[inline]
pub unsafe fn get_unchecked(&self, x: u32, y: u32) -> K {
debug_assert!(x < self.width);
debug_assert!(y < self.height);
let at = usize::try_from(y * self.width + x).unwrap();
debug_assert!(at < self.data.len());
*self.data.get_unchecked(at)
pub fn at(&self, x: u32, y: u32) -> &K {
&self.data[(y * self.width + x) as usize]
}
}

Expand Down
Loading