Skip to content

Commit f25633a

Browse files
author
jnickg
committed
Get custom filters working (?)
1 parent 953c179 commit f25633a

File tree

1 file changed

+53
-25
lines changed

1 file changed

+53
-25
lines changed

src/lib.rs

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
9898
#![deny(
9999
nonstandard_style,
100-
unused,
100+
// unused,
101101
unsafe_code,
102102
future_incompatible,
103103
rust_2018_idioms,
@@ -106,8 +106,10 @@
106106
clippy::pedantic
107107
)]
108108

109+
use std::fmt::Debug;
110+
109111
use image::{DynamicImage, GenericImage, GenericImageView, Pixel};
110-
use num_traits::{Num, NumCast};
112+
use num_traits::{clamp, Num, NumCast};
111113
use thiserror::Error;
112114

113115
/// An enumeration of the errors that may be emitted from the `image_pyramid`
@@ -181,24 +183,24 @@ fn accumulate<P, K>(acc: &mut [K], pixel: &P, weight: K)
181183
where
182184
P: Pixel,
183185
<P as Pixel>::Subpixel: Into<K>,
184-
K: Num + Copy,
186+
K: Num + Copy + Debug,
185187
{
186188
acc
187189
.iter_mut()
188-
.enumerate()
189-
.take(P::CHANNEL_COUNT as usize)
190-
.for_each(|(i, a)| {
191-
*a = *a + pixel.channels()[i].into() * weight;
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;
192194
});
193195
}
194196

195-
pub struct Kernel<K> {
197+
struct Kernel<K> {
196198
data: Vec<K>,
197199
width: u32,
198200
height: u32,
199201
}
200202

201-
impl<K: Num + Copy> Kernel<K> {
203+
impl<K: Num + Copy + Debug> Kernel<K> {
202204
/// Construct a kernel from a slice and its dimensions. The input slice is
203205
/// in row-major form. For example, a 3x3 matrix with data
204206
/// `[0,1,0,1,2,1,0,1,0`] describes the following matrix:
@@ -261,6 +263,10 @@ impl<K: Num + Copy> Kernel<K> {
261263
/// | 0 1 0 |
262264
/// └ ┘
263265
/// ```
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+
///
264270
/// # Errors
265271
///
266272
/// - If `width == 0 || height == 0`, [`ImagePyramidError::Internal`] is
@@ -272,28 +278,30 @@ impl<K: Num + Copy> Kernel<K> {
272278
///
273279
/// In debug builds, this factory panics under the conditions that [`Err`] is
274280
/// returned for release builds.
275-
pub fn new_normalized(data: &[K], width: u32, height: u32) -> Result<Self, ImagePyramidError> {
281+
pub fn new_normalized(data: &[K], width: u32, height: u32) -> Result<Kernel<f32>, ImagePyramidError>
282+
where K: Into<f32>
283+
{
276284
let mut sum = K::zero();
277285
for i in data {
278286
sum = sum + *i;
279287
}
280-
let data_norm: Vec<K> = data.iter().map(|x| *x / sum).collect();
281-
Self::new(&data_norm, width, height)
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)
282290
}
283291

284292
/// Returns 2d correlation of an image. Intermediate calculations are
285293
/// performed at type K, and the results converted to pixel Q via f. Pads by
286294
/// continuity.
287295
#[allow(unsafe_code)]
288-
pub fn filter<I, F>(&self, image: &I, mut f: F) -> I
296+
#[allow(unused)]
297+
pub fn filter_in_place<I, F>(&self, image: &mut I, mut f: F)
289298
where
290299
I: GenericImage + Clone,
291300
<<I as GenericImageView>::Pixel as Pixel>::Subpixel: Into<K>,
292301
F: FnMut(&mut <<I as GenericImageView>::Pixel as Pixel>::Subpixel, K),
293302
{
294303
use core::cmp::{max, min};
295304
let (width, height) = image.dimensions();
296-
let mut out: I = image.clone();
297305
let num_channels = <<I as GenericImageView>::Pixel as Pixel>::CHANNEL_COUNT as usize;
298306
let zero = K::zero();
299307
let mut acc = vec![zero; num_channels];
@@ -313,11 +321,11 @@ impl<K: Num + Copy> Kernel<K> {
313321
for k_y in 0..k_height {
314322
#[allow(clippy::cast_possible_truncation)]
315323
#[allow(clippy::cast_sign_loss)]
316-
let y_p = min(height - 1, max(0, y + k_y - k_height / 2)) as u32;
324+
let y_p = clamp(y + k_y - k_height / 2, 0, height - 1) as u32;
317325
for k_x in 0..k_width {
318326
#[allow(clippy::cast_possible_truncation)]
319327
#[allow(clippy::cast_sign_loss)]
320-
let x_p = min(width - 1, max(0, x + k_x - k_width / 2)) as u32;
328+
let x_p = clamp(x + k_x - k_width / 2, 0, width - 1) as u32;
321329
#[allow(clippy::cast_possible_truncation)]
322330
#[allow(clippy::cast_sign_loss)]
323331
let k_idx = (k_y * k_width + k_x) as usize;
@@ -329,17 +337,15 @@ impl<K: Num + Copy> Kernel<K> {
329337
);
330338
}
331339
}
332-
let mut out_pel = out.get_pixel(x_u32, y_u32);
340+
let mut out_pel = image.get_pixel(x_u32, y_u32);
333341
let out_channels = out_pel.channels_mut();
334342
for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
335343
f(c, *a);
336344
*a = zero;
337345
}
338-
out.put_pixel(x_u32, y_u32, out_pel);
346+
image.put_pixel(x_u32, y_u32, out_pel);
339347
}
340348
}
341-
342-
out
343349
}
344350
}
345351

@@ -494,20 +500,20 @@ impl<'a> CanComputePyramid for ImageToProcess<'a> {
494500
) -> Result<Vec<DynamicImage>, ImagePyramidError> {
495501
let mut levels = vec![image.clone()];
496502
let kernel = match params.smoothing_type {
497-
SmoothingType::Gaussian => Kernel::new_normalized(&[1, 2, 3, 2, 4, 2, 1, 2, 1], 3, 3)?,
498-
SmoothingType::Box => Kernel::new_normalized(&[1, 1, 1, 1, 1, 1, 1, 1, 1], 3, 3)?,
499-
SmoothingType::Triangle => Kernel::new_normalized(&[1, 2, 1, 2, 4, 2, 1, 2, 1], 3, 3)?,
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)?,
500506
};
501507
let mut current_level = image.clone();
502508
#[allow(clippy::cast_possible_truncation)]
503509
#[allow(clippy::cast_precision_loss)]
504510
#[allow(clippy::cast_sign_loss)]
505511
while current_level.width() > 1 && current_level.height() > 1 {
506-
current_level = kernel.filter(&current_level, |c, a| *c = num_traits::clamp(a, 0, 255));
512+
kernel.filter_in_place(&mut current_level, |c, a| *c = a as u8);
507513
current_level = current_level.resize_exact(
508514
(current_level.width() as f32 * params.scale_factor.get()) as u32,
509515
(current_level.height() as f32 * params.scale_factor.get()) as u32,
510-
image::imageops::FilterType::Nearest,
516+
image::imageops::FilterType::Gaussian,
511517
);
512518
levels.push(current_level.clone());
513519
}
@@ -591,6 +597,28 @@ mod tests {
591597

592598
use super::*;
593599

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+
594622
#[test]
595623
fn compute_image_pyramid_imagepyramidtype_steerable_unimplemented() {
596624
let image = DynamicImage::new_rgb8(640, 480);

0 commit comments

Comments
 (0)