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 16, 2023
1 parent d5b8bdf commit 7fb55c4
Showing 1 changed file with 100 additions and 0 deletions.
100 changes: 100 additions & 0 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,106 @@ 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] {
std::array::from_fn(|i| a[i] + b[i])
}

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 7fb55c4

Please sign in to comment.