From 9c498009cbacd0ebf88036ac9410b51a5e471c79 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/image.rs | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/image.rs b/src/image.rs index 58e90b135d..b505c812d3 100644 --- a/src/image.rs +++ b/src/image.rs @@ -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( + img_view: &impl GenericImageView, + 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 = ::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.