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 May 17, 2023
1 parent d5b8bdf commit 9c49800
Showing 1 changed file with 104 additions and 0 deletions.
104 changes: 104 additions & 0 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,110 @@ pub trait GenericImageView {
}
}

/// How to sample image,
/// taken from OpenGL's wrap mode.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SampleMode {
/// Sample the nearest integer coordinate.
Nearest,
/// Linearly bisample nearby coordinates.
Bilinear,
}

/// This explicitly does not have a "checked"
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WrapKind {
/// Clamp input coordinates to [0,1]
Clamp,
/// Mod input coordinates, i.e. `uv % 1`
Repeat,
/// Mod input coordinates with alternating size, i.e. `uv % 1`
MirrorRepeat,
}

impl WrapKind {
fn apply(self, v: f32) -> f32 {
match self {
Self::Clamp => v.min(1.).max(0.),
Self::Repeat => v.rem_euclid(1.),
Self::MirrorRepeat => {
let wr = v % 2.;
if wr > 1. {
2. - wr
} else {
wr
}
}
}
}
}

fn sample_generic_image_view<P: Pixel>(
img_view: &impl GenericImageView<Pixel = P>,
uv: [f32; 2],
sample_mode: SampleMode,
wrap_kind: WrapKind,
) -> P {
let [u, v] = uv.map(|v| wrap_kind.apply(v));
assert!(0. <= u && u <= 1.);
let (w, h) = img_view.dimensions();

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
}

match sample_mode {
SampleMode::Nearest => img_view.get_pixel(ui.round() as u32, vi.round() as u32),
SampleMode::Bilinear => {
let uf = ui.floor();
let uc = ui.ceil();
let vf = vi.floor();
let vc = vi.ceil();

let coords = [[uf, vf], [uf, vc], [uc, vf], [uc, vc]];
let samples = coords.map(|[u, v]| img_view.get_pixel(u as u32, v as u32));
use crate::traits::Primitive;
use num_traits::{NumCast, ToPrimitive};
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, ucw, vfw, vcw] = [ui - uf, uc - ui, vi - vf, vc - vi];

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();
}
out
}
}
}

/// A trait for manipulating images.
pub trait GenericImage: GenericImageView {
/// Gets a reference to the mutable pixel at location `(x, y)`. Indexed from top left.
Expand Down

0 comments on commit 9c49800

Please sign in to comment.