Skip to content

Commit

Permalink
Add sampling using [0,1] for GenericImageView
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JulianKnodt committed Jul 3, 2023
1 parent d5b8bdf commit 2945419
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/imageops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
87 changes: 86 additions & 1 deletion src/imageops/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,71 @@ where
out
}

/// Linearly bisample from an image using coordinates in [0,1].
pub fn bilinear_sample<P: Pixel>(
img: &impl GenericImageView<Pixel = P>,
uv: [f32; 2],
) -> Option<P> {
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 = <P::Subpixel as NumCast>::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
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down

0 comments on commit 2945419

Please sign in to comment.