From 08afcc6452cd36778597eb2a2bf1fcc5efc78883 Mon Sep 17 00:00:00 2001 From: julianknodt Date: Tue, 16 May 2023 00:44:38 -0700 Subject: [PATCH] Add sampling using [0,1] for GenericImageView Adds a way to sample images in [0,1], which is a common use case in graphics. Currently adds non-exhaustive enums based on OpenGL's that allow for simple sampling of nearby coordinates. Nearest sampling can produce artifacts depending on the sampling resolution of the UV whereas bilinear will smoothly interpolate. --- src/imageops/mod.rs | 2 +- src/imageops/sample.rs | 89 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/imageops/mod.rs b/src/imageops/mod.rs index 57761c4878..00cdc6213b 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::{blur, filter3x3, resize, sample_bilinear, thumbnail, unsharpen}; /// Color operations pub use self::colorops::{ diff --git a/src/imageops/sample.rs b/src/imageops/sample.rs index 1aed9bb327..b81ffbf0af 100644 --- a/src/imageops/sample.rs +++ b/src/imageops/sample.rs @@ -304,6 +304,73 @@ where out } +/// Linearly bisample from an image using coordinates in [0,1]. +pub fn sample_bilinear( + img: &impl GenericImageView, + x: f32, + y: f32, +) -> Option

{ + if ![x, y].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 - 1) as f32 * x; + let vi = (h - 1) as f32 * y; + + 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.).min((w - 1) as f32); + let vc = (vf + 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 [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(); + } + 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]).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 +933,7 @@ where #[cfg(test)] mod tests { - use super::{resize, FilterType}; + use super::{resize, sample_bilinear, FilterType}; use crate::{GenericImageView, ImageBuffer, RgbImage}; #[cfg(feature = "benchmarks")] use test; @@ -891,6 +958,26 @@ mod tests { assert!(img.pixels().eq(resize.pixels())) } + #[test] + #[cfg(feature = "png")] + fn test_bilinear_sample() { + use std::path::Path; + let img = crate::open(&Path::new("./examples/fractal.png")).unwrap(); + assert!(sample_bilinear(&img, 0., 0.).is_some()); + assert!(sample_bilinear(&img, 1., 0.).is_some()); + assert!(sample_bilinear(&img, 0., 1.).is_some()); + assert!(sample_bilinear(&img, 1., 1.).is_some()); + assert!(sample_bilinear(&img, 0.5, 0.5).is_some()); + + assert!(sample_bilinear(&img, 1.2, 0.5).is_none()); + assert!(sample_bilinear(&img, 0.5, 1.2).is_none()); + assert!(sample_bilinear(&img, 1.2, 1.2).is_none()); + + assert!(sample_bilinear(&img, -0.1, 0.2).is_none()); + assert!(sample_bilinear(&img, 0.2, -0.1).is_none()); + assert!(sample_bilinear(&img, -0.1, -0.1).is_none()); + } + #[bench] #[cfg(all(feature = "benchmarks", feature = "tiff"))] fn bench_resize_same_size(b: &mut test::Bencher) {