pub fn vignetting_pa_apply_f32(
pixels: &mut [f32],
width: usize,
height: usize,
channels: usize,
k1: f32,
k2: f32,
k3: f32,
) {
walk(pixels, width, height, channels, k1, k2, k3, |p, c| *p *= c);
}
pub fn vignetting_pa_correct_f32(
pixels: &mut [f32],
width: usize,
height: usize,
channels: usize,
k1: f32,
k2: f32,
k3: f32,
) {
walk(pixels, width, height, channels, k1, k2, k3, |p, c| {
*p *= 1.0 / c
});
}
pub fn vignetting_pa_apply_u16(
pixels: &mut [u16],
width: usize,
height: usize,
channels: usize,
k1: f32,
k2: f32,
k3: f32,
) {
walk(pixels, width, height, channels, k1, k2, k3, |p, c| {
*p = clamp_u16(f32::from(*p) * c);
});
}
pub fn vignetting_pa_correct_u16(
pixels: &mut [u16],
width: usize,
height: usize,
channels: usize,
k1: f32,
k2: f32,
k3: f32,
) {
walk(pixels, width, height, channels, k1, k2, k3, |p, c| {
*p = clamp_u16(f32::from(*p) * (1.0 / c));
});
}
pub fn vignetting_pa_apply_u8(
pixels: &mut [u8],
width: usize,
height: usize,
channels: usize,
k1: f32,
k2: f32,
k3: f32,
) {
walk(pixels, width, height, channels, k1, k2, k3, |p, c| {
*p = clamp_u8(f32::from(*p) * c);
});
}
pub fn vignetting_pa_correct_u8(
pixels: &mut [u8],
width: usize,
height: usize,
channels: usize,
k1: f32,
k2: f32,
k3: f32,
) {
walk(pixels, width, height, channels, k1, k2, k3, |p, c| {
*p = clamp_u8(f32::from(*p) * (1.0 / c));
});
}
fn walk<T>(
pixels: &mut [T],
width: usize,
height: usize,
channels: usize,
k1: f32,
k2: f32,
k3: f32,
mut op: impl FnMut(&mut T, f32),
) {
if width == 0 || height == 0 || channels == 0 {
return;
}
debug_assert_eq!(
pixels.len(),
width * height * channels,
"buffer length must equal width * height * channels"
);
let denom = (width.max(height).saturating_sub(1)).max(1) as f32;
let norm_scale = 2.0_f32 / denom;
let d1 = 2.0_f32 * norm_scale;
let d2 = norm_scale * norm_scale;
let cx = (width.saturating_sub(1)) as f32 * 0.5 * norm_scale;
let cy = (height.saturating_sub(1)) as f32 * 0.5 * norm_scale;
let mut y = -cy;
for row in 0..height {
let x_start = -cx;
let mut r2 = x_start * x_start + y * y;
let mut x = x_start;
let row_off = row * width * channels;
for col in 0..width {
let r4 = r2 * r2;
let r6 = r4 * r2;
let c = 1.0 + k1 * r2 + k2 * r4 + k3 * r6;
let pix_off = row_off + col * channels;
for ch in 0..channels {
op(&mut pixels[pix_off + ch], c);
}
r2 += d1 * x + d2;
x += norm_scale;
}
y += norm_scale;
}
}
fn clamp_u8(v: f32) -> u8 {
if !v.is_finite() || v <= 0.0 {
return 0;
}
let r = (v + 0.5) as i32;
r.clamp(0, 255) as u8
}
fn clamp_u16(v: f32) -> u16 {
if !v.is_finite() || v <= 0.0 {
return 0;
}
let r = (v + 0.5) as i32;
r.clamp(0, 65535) as u16
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn center_pixel_gain_is_one_f32() {
let mut buf = vec![0.5_f32; 5 * 5];
vignetting_pa_apply_f32(&mut buf, 5, 5, 1, -0.5, 0.2, 0.1);
assert!((buf[2 * 5 + 2] - 0.5).abs() < 1e-6);
}
#[test]
fn zero_coefficients_is_identity_f32() {
let mut buf = vec![0.5_f32; 4 * 3 * 3];
let original = buf.clone();
vignetting_pa_apply_f32(&mut buf, 4, 3, 3, 0.0, 0.0, 0.0);
for (a, b) in buf.iter().zip(original.iter()) {
assert!((a - b).abs() < 1e-6);
}
}
#[test]
fn empty_buffers_are_noops() {
let mut empty: Vec<f32> = Vec::new();
vignetting_pa_apply_f32(&mut empty, 0, 0, 3, 0.1, 0.0, 0.0);
vignetting_pa_correct_f32(&mut empty, 0, 0, 3, 0.1, 0.0, 0.0);
assert!(empty.is_empty());
}
}