diff --git a/src/imageops/mod.rs b/src/imageops/mod.rs index 57761c4878..1e82c494a8 100644 --- a/src/imageops/mod.rs +++ b/src/imageops/mod.rs @@ -16,7 +16,7 @@ pub use self::affine::{ }; /// Image sampling -pub use self::sample::{blur, filter3x3, resize, thumbnail, unsharpen}; +pub use self::sample::{bilinear_sample, blur, filter3x3, resize, thumbnail, unsharpen}; /// Color operations pub use self::colorops::{ diff --git a/src/imageops/sample.rs b/src/imageops/sample.rs index 1aed9bb327..7009594d76 100644 --- a/src/imageops/sample.rs +++ b/src/imageops/sample.rs @@ -304,6 +304,71 @@ where out } +/// Linearly bisample from an image using coordinates in [0,1]. +pub fn bilinear_sample( + img: &impl GenericImageView, + uv: [f32; 2], +) -> Option

{ + if !uv.iter().all(|c| (0.0..=1.0).contains(c)) { + return None; + } + let (w, h) = img.dimensions(); + + let [u, v] = uv; + let ui = (w - 1) as f32 * u; + let vi = (h - 1) as f32 * v; + + fn add2(a: [f32; 4], b: [f32; 4]) -> [f32; 4] { + let mut out = [0.; 4]; + for i in 0..4 { + out[i] = a[i] + b[i]; + } + out + } + + let uf = ui.floor(); + let vf = vi.floor(); + let uc = uf + 1.; + let vc = vf + 1.; + + // clamp coords to the range of the image + let coords = [[uf, vf], [uf, vc], [uc, vf], [uc, vc]] + .map(|[u, v]| [u.min((w - 1) as f32).max(0.), v.min((h - 1) as f32).max(0.)]); + + 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); + + let max: f32 = P::Subpixel::DEFAULT_MAX_VALUE.to_f32().unwrap(); + // 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 [s0, s1, s2, s3] = samples.map(|s| { + let mut out = [0.; 4]; + for (i, c) in s.channels().iter().enumerate() { + out[i] = c.to_f32().unwrap() / max; + } + out + }); + // weights + let [ufw, vfw] = [ui - uf, vi - vf]; + let [ucw, vcw] = [1. - ufw, 1. - vfw]; + + let u02 = add2(s0.map(|c| c * ucw), s2.map(|c| c * ufw)); + let u13 = add2(s1.map(|c| c * ucw), s3.map(|c| c * ufw)); + let interp = add2(u02.map(|c| c * vcw), u13.map(|c| c * vfw)); + + // 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() { + *c = ::from(interp[i] * max).unwrap(); + } + Some(out) +} + // Sample the columns of the supplied image using the provided filter. // The width of the image remains unchanged. // ```new_height``` is the desired height of the new image @@ -866,7 +931,7 @@ where #[cfg(test)] mod tests { - use super::{resize, FilterType}; + use super::{bilinear_sample, resize, FilterType}; use crate::{GenericImageView, ImageBuffer, RgbImage}; #[cfg(feature = "benchmarks")] use test; @@ -891,6 +956,26 @@ mod tests { assert!(img.pixels().eq(resize.pixels())) } + #[test] + #[cfg(feature = "png")] + fn test_sample_bilinear() { + use std::path::Path; + let img = crate::open(&Path::new("./examples/fractal.png")).unwrap(); + assert!(bilinear_sample(&img, [0., 0.]).is_some()); + assert!(bilinear_sample(&img, [1., 0.]).is_some()); + assert!(bilinear_sample(&img, [0., 1.]).is_some()); + assert!(bilinear_sample(&img, [1., 1.]).is_some()); + assert!(bilinear_sample(&img, [0.5, 0.5]).is_some()); + + assert!(bilinear_sample(&img, [1.2, 0.5]).is_none()); + assert!(bilinear_sample(&img, [0.5, 1.2]).is_none()); + assert!(bilinear_sample(&img, [1.2, 1.2]).is_none()); + + assert!(bilinear_sample(&img, [-0.1, 0.2]).is_none()); + assert!(bilinear_sample(&img, [0.2, -0.1]).is_none()); + assert!(bilinear_sample(&img, [-0.1, -0.1]).is_none()); + } + #[bench] #[cfg(all(feature = "benchmarks", feature = "tiff"))] fn bench_resize_same_size(b: &mut test::Bencher) {