//! Functions and filters for the sampling of pixels. // See http://cs.brown.edu/courses/cs123/lectures/08_Image_Processing_IV.pdf // for some of the theory behind image scaling and convolution use std::f32; use num_traits::{NumCast, ToPrimitive, Zero}; use crate::image::{GenericImage, GenericImageView}; use crate::traits::{Enlargeable, Pixel, Primitive}; use crate::utils::clamp; use crate::{ImageBuffer, Rgba32FImage}; /// Available Sampling Filters. /// /// ## Examples /// /// To test the different sampling filters on a real example, you can find two /// examples called /// [`scaledown`](https://github.com/image-rs/image/tree/master/examples/scaledown) /// and /// [`scaleup`](https://github.com/image-rs/image/tree/master/examples/scaleup) /// in the `examples` directory of the crate source code. /// /// Here is a 3.58 MiB /// [test image](https://github.com/image-rs/image/blob/master/examples/scaledown/test.jpg) /// that has been scaled down to 300x225 px: /// /// ///
Nearest | ///31 ms | ///
---|---|
Triangle | ///414 ms | ///
CatmullRom | ///817 ms | ///
Gaussian | ///1180 ms | ///
Lanczos3 | ///1170 ms | ///
( image: &Rgba32FImage, new_width: u32, filter: &mut Filter, ) -> ImageBuffer
>
where
P: Pixel {
if ![u, v].iter().all(|c| (0.0..=1.0).contains(c)) {
return None;
}
let (w, h) = img.dimensions();
if w == 0 || h == 0 {
return None;
}
let ui = w as f32 * u - 0.5;
let vi = h as f32 * v - 0.5;
interpolate_bilinear(
img,
ui.max(0.).min((w - 1) as f32),
vi.max(0.).min((h - 1) as f32),
)
}
/// Sample from an image using coordinates in [0, 1], taking the nearest coordinate.
pub fn sample_nearest {
if ![u, v].iter().all(|c| (0.0..=1.0).contains(c)) {
return None;
}
let (w, h) = img.dimensions();
let ui = w as f32 * u - 0.5;
let ui = ui.max(0.).min((w.saturating_sub(1)) as f32);
let vi = h as f32 * v - 0.5;
let vi = vi.max(0.).min((h.saturating_sub(1)) as f32);
interpolate_nearest(img, ui, vi)
}
/// Sample from an image using coordinates in [0, w-1] and [0, h-1], taking the
/// nearest pixel.
///
/// Coordinates outside the image bounds will return `None`, however the
/// behavior for points within half a pixel of the image bounds may change in
/// the future.
pub fn interpolate_nearest {
let (w, h) = img.dimensions();
if w == 0 || h == 0 {
return None;
}
if !(0.0..=((w - 1) as f32)).contains(&x) {
return None;
}
if !(0.0..=((h - 1) as f32)).contains(&y) {
return None;
}
Some(img.get_pixel(x.round() as u32, y.round() as u32))
}
/// Linearly sample from an image using coordinates in [0, w-1] and [0, h-1].
pub fn interpolate_bilinear {
let (w, h) = img.dimensions();
if w == 0 || h == 0 {
return None;
}
if !(0.0..=((w - 1) as f32)).contains(&x) {
return None;
}
if !(0.0..=((h - 1) as f32)).contains(&y) {
return None;
}
let uf = x.floor();
let vf = y.floor();
let uc = (x + 1.).min((w - 1) as f32);
let vc = (y + 1.).min((h - 1) as f32);
// clamp coords to the range of the image
let coords = [[uf, vf], [uf, vc], [uc, vf], [uc, vc]];
assert!(coords
.iter()
.all(|&[u, v]| { img.in_bounds(u as u32, v as u32) }));
let samples = coords.map(|[u, v]| img.get_pixel(u as u32, v as u32));
assert!(P::CHANNEL_COUNT <= 4);
// convert samples to f32
// currently rgba is the largest one,
// so just store as many items as necessary,
// because there's not a simple way to be generic over all of them.
let [sff, sfc, scf, scc] = samples.map(|s| {
let mut out = [0.; 4];
for (i, c) in s.channels().iter().enumerate() {
out[i] = c.to_f32().unwrap();
}
out
});
// weights
let [ufw, vfw] = [x - uf, y - vf];
let [ucw, vcw] = [1. - ufw, 1. - vfw];
// https://en.wikipedia.org/wiki/Bilinear_interpolation#Weighted_mean
// the distance between pixels is 1 so there is no denominator
let wff = ucw * vcw;
let wfc = ucw * vfw;
let wcf = ufw * vcw;
let wcc = ufw * vfw;
assert!(f32::abs((wff + wfc + wcf + wcc) - 1.) < 1e-3);
// hack to get around not being able to construct a generic Pixel
let mut out = samples[0];
for (i, c) in out.channels_mut().iter_mut().enumerate() {
let v = wff * sff[i] + wfc * sfc[i] + wcf * scf[i] + wcc * scc[i];
// this rounding may introduce quantization errors,
// but cannot do anything about it.
*c = >
where
I: GenericImageView >
where
I: GenericImageView >
where
I: GenericImageView {
fn zeroed() -> Self {
ThumbnailSum(
S::Larger::zero(),
S::Larger::zero(),
S::Larger::zero(),
S::Larger::zero(),
)
}
fn sample_val(val: S) -> S::Larger {
::from(
fact_left * leftv.to_f32().unwrap() + fact_right * rightv.to_f32().unwrap(),
)
.expect("Average sample value should fit into sample type")
};
(
mix_left_and_right(sum_left.0, sum_right.0),
mix_left_and_right(sum_left.1, sum_right.1),
mix_left_and_right(sum_left.2, sum_right.2),
mix_left_and_right(sum_left.3, sum_right.3),
)
}
/// Get a thumbnail pixel where the input window encloses at least a horizontal pixel.
fn thumbnail_sample_fraction_vertical(
image: &I,
left: u32,
right: u32,
bottom: u32,
fraction_vertical: f32,
) -> (S, S, S, S)
where
I: GenericImageView::from(fact_bot * botv.to_f32().unwrap() + fact_top * topv.to_f32().unwrap())
.expect("Average sample value should fit into sample type")
};
(
mix_bot_and_top(sum_bot.0, sum_top.0),
mix_bot_and_top(sum_bot.1, sum_top.1),
mix_bot_and_top(sum_bot.2, sum_top.2),
mix_bot_and_top(sum_bot.3, sum_top.3),
)
}
/// Get a single pixel for a thumbnail where the input window does not enclose any full pixel.
fn thumbnail_sample_fraction_both(
image: &I,
left: u32,
fraction_vertical: f32,
bottom: u32,
fraction_horizontal: f32,
) -> (S, S, S, S)
where
I: GenericImageView::from(
fact_br * br.to_f32().unwrap()
+ fact_tr * tr.to_f32().unwrap()
+ fact_bl * bl.to_f32().unwrap()
+ fact_tl * tl.to_f32().unwrap(),
)
.expect("Average sample value should fit into sample type")
};
(
mix(k_br.0, k_tr.0, k_bl.0, k_tl.0),
mix(k_br.1, k_tr.1, k_bl.1, k_tl.1),
mix(k_br.2, k_tr.2, k_bl.2, k_tl.2),
mix(k_br.3, k_tr.3, k_bl.3, k_tl.3),
)
}
/// Perform a 3x3 box filter on the supplied image.
/// ```kernel``` is an array of the filter weights of length 9.
pub fn filter3x3(image: &I, kernel: &[f32]) -> ImageBuffer