use crate::error::{AlgorithmError, Result};
use crate::resampling::kernel::{lanczos, normalize_weights};
use oxigeo_core::buffer::RasterBuffer;
#[derive(Debug, Clone, Copy)]
pub struct LanczosResampler {
lobes: usize,
}
impl Default for LanczosResampler {
fn default() -> Self {
Self::new(3)
}
}
impl LanczosResampler {
#[must_use]
pub const fn new(lobes: usize) -> Self {
Self { lobes }
}
#[must_use]
pub const fn lobes(&self) -> usize {
self.lobes
}
#[must_use]
pub const fn radius(&self) -> usize {
self.lobes
}
pub fn resample(
&self,
src: &RasterBuffer,
dst_width: u64,
dst_height: u64,
) -> Result<RasterBuffer> {
self.resample_with_edge_mode(src, dst_width, dst_height, EdgeMode::Clamp)
}
fn interpolate_at(
&self,
src: &RasterBuffer,
src_x: f64,
src_y: f64,
edge_mode: EdgeMode,
) -> Result<f64> {
let src_width = src.width();
let src_height = src.height();
let src_x_clamped = src_x.max(0.0).min((src_width - 1) as f64);
let src_y_clamped = src_y.max(0.0).min((src_height - 1) as f64);
let x_center = src_x_clamped.floor() as i64;
let y_center = src_y_clamped.floor() as i64;
let radius = self.radius() as i64;
let x_start = x_center - radius + 1;
let x_end = x_center + radius + 1;
let y_start = y_center - radius + 1;
let y_end = y_center + radius + 1;
let kernel_width = (x_end - x_start) as usize;
let kernel_height = (y_end - y_start) as usize;
let total_samples = kernel_width * kernel_height;
let mut values = vec![0.0f64; total_samples];
let mut weights = vec![0.0f64; total_samples];
let mut idx = 0;
for sy in y_start..y_end {
for sx in x_start..x_end {
let sample_x = map_edge_coord(sx, src_width, edge_mode);
let sample_y = map_edge_coord(sy, src_height, edge_mode);
values[idx] = src
.get_pixel(sample_x, sample_y)
.map_err(AlgorithmError::Core)?;
let dx = sx as f64 - src_x_clamped;
let dy = sy as f64 - src_y_clamped;
let wx = lanczos(dx, self.lobes);
let wy = lanczos(dy, self.lobes);
weights[idx] = wx * wy;
idx += 1;
}
}
normalize_weights(&mut weights);
let mut result = 0.0;
for (value, weight) in values.iter().zip(weights.iter()) {
result += value * weight;
}
Ok(result)
}
pub fn resample_with_edge_mode(
&self,
src: &RasterBuffer,
dst_width: u64,
dst_height: u64,
edge_mode: EdgeMode,
) -> Result<RasterBuffer> {
if dst_width == 0 || dst_height == 0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "dimensions",
message: "Target dimensions must be non-zero".to_string(),
});
}
let src_width = src.width();
let src_height = src.height();
if src_width == 0 || src_height == 0 {
return Err(AlgorithmError::EmptyInput {
operation: "Lanczos resampling",
});
}
let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
let scale_x = src_width as f64 / dst_width as f64;
let scale_y = src_height as f64 / dst_height as f64;
for dst_y in 0..dst_height {
for dst_x in 0..dst_width {
let src_x = (dst_x as f64 + 0.5) * scale_x - 0.5;
let src_y = (dst_y as f64 + 0.5) * scale_y - 0.5;
let value = self.interpolate_at(src, src_x, src_y, edge_mode)?;
dst.set_pixel(dst_x, dst_y, value)
.map_err(AlgorithmError::Core)?;
}
}
Ok(dst)
}
pub fn resample_separable(
&self,
src: &RasterBuffer,
dst_width: u64,
dst_height: u64,
) -> Result<RasterBuffer> {
if dst_width == 0 || dst_height == 0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "dimensions",
message: "Target dimensions must be non-zero".to_string(),
});
}
let src_width = src.width();
let src_height = src.height();
let mut temp = RasterBuffer::zeros(dst_width, src_height, src.data_type());
let scale_x = src_width as f64 / dst_width as f64;
for src_y in 0..src_height {
for dst_x in 0..dst_width {
let src_x = (dst_x as f64 + 0.5) * scale_x - 0.5;
let value = self.interpolate_horizontal(src, src_x, src_y)?;
temp.set_pixel(dst_x, src_y, value)
.map_err(AlgorithmError::Core)?;
}
}
let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
let scale_y = src_height as f64 / dst_height as f64;
for dst_x in 0..dst_width {
for dst_y in 0..dst_height {
let src_y = (dst_y as f64 + 0.5) * scale_y - 0.5;
let value = self.interpolate_vertical(&temp, dst_x, src_y)?;
dst.set_pixel(dst_x, dst_y, value)
.map_err(AlgorithmError::Core)?;
}
}
Ok(dst)
}
fn interpolate_horizontal(&self, src: &RasterBuffer, src_x: f64, y: u64) -> Result<f64> {
let src_width = src.width();
let src_x_clamped = src_x.max(0.0).min((src_width - 1) as f64);
let x_center = src_x_clamped.floor() as i64;
let radius = self.radius() as i64;
let x_start = x_center - radius + 1;
let x_end = x_center + radius + 1;
let kernel_width = (x_end - x_start) as usize;
let mut values = vec![0.0f64; kernel_width];
let mut weights = vec![0.0f64; kernel_width];
for (idx, sx) in (x_start..x_end).enumerate() {
let sample_x = sx.max(0).min(src_width as i64 - 1) as u64;
values[idx] = src.get_pixel(sample_x, y).map_err(AlgorithmError::Core)?;
let dx = sx as f64 - src_x_clamped;
weights[idx] = lanczos(dx, self.lobes);
}
normalize_weights(&mut weights);
let result = values.iter().zip(weights.iter()).map(|(v, w)| v * w).sum();
Ok(result)
}
fn interpolate_vertical(&self, src: &RasterBuffer, x: u64, src_y: f64) -> Result<f64> {
let src_height = src.height();
let src_y_clamped = src_y.max(0.0).min((src_height - 1) as f64);
let y_center = src_y_clamped.floor() as i64;
let radius = self.radius() as i64;
let y_start = y_center - radius + 1;
let y_end = y_center + radius + 1;
let kernel_height = (y_end - y_start) as usize;
let mut values = vec![0.0f64; kernel_height];
let mut weights = vec![0.0f64; kernel_height];
for (idx, sy) in (y_start..y_end).enumerate() {
let sample_y = sy.max(0).min(src_height as i64 - 1) as u64;
values[idx] = src.get_pixel(x, sample_y).map_err(AlgorithmError::Core)?;
let dy = sy as f64 - src_y_clamped;
weights[idx] = lanczos(dy, self.lobes);
}
normalize_weights(&mut weights);
let result = values.iter().zip(weights.iter()).map(|(v, w)| v * w).sum();
Ok(result)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeMode {
Clamp,
Wrap,
Mirror,
}
#[must_use]
fn map_edge_coord(c: i64, size: u64, mode: EdgeMode) -> u64 {
if size == 0 {
return 0;
}
let size_i = size as i64;
match mode {
EdgeMode::Clamp => c.clamp(0, size_i - 1) as u64,
EdgeMode::Wrap => c.rem_euclid(size_i) as u64,
EdgeMode::Mirror => {
if size_i == 1 {
return 0;
}
let period = 2 * (size_i - 1);
let m = c.rem_euclid(period);
if m < size_i {
m as u64
} else {
(period - m) as u64
}
}
}
}
#[cfg(test)]
#[allow(clippy::panic)]
mod tests {
use super::*;
use oxigeo_core::types::RasterDataType;
#[test]
fn test_lanczos_creation() {
let l2 = LanczosResampler::new(2);
assert_eq!(l2.lobes(), 2);
assert_eq!(l2.radius(), 2);
let l3 = LanczosResampler::new(3);
assert_eq!(l3.lobes(), 3);
assert_eq!(l3.radius(), 3);
}
#[test]
fn test_lanczos_identity() {
let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
src.set_pixel(x, y, (y * 10 + x) as f64).ok();
}
}
let resampler = LanczosResampler::new(3);
let dst = resampler.resample(&src, 10, 10);
assert!(dst.is_ok());
}
#[test]
fn test_lanczos_quality() {
let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
src.set_pixel(x, y, ((x + y) * (x + y)) as f64).ok();
}
}
let resampler = LanczosResampler::new(3);
let dst = resampler.resample(&src, 10, 10);
assert!(dst.is_ok());
if let Ok(dst) = dst {
let v1 = dst.get_pixel(4, 4).ok();
let v2 = dst.get_pixel(5, 5).ok();
assert!(v1.is_some());
assert!(v2.is_some());
}
}
#[test]
fn test_lanczos_separable() {
let mut src = RasterBuffer::zeros(8, 8, RasterDataType::Float32);
for y in 0..8 {
for x in 0..8 {
src.set_pixel(x, y, (x + y) as f64).ok();
}
}
let resampler = LanczosResampler::new(3);
let dst1 = resampler.resample(&src, 16, 16);
let dst2 = resampler.resample_separable(&src, 16, 16);
assert!(dst1.is_ok());
assert!(dst2.is_ok());
}
#[test]
fn test_lanczos_lobes() {
let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
let l2 = LanczosResampler::new(2);
let l3 = LanczosResampler::new(3);
let dst2 = l2.resample(&src, 20, 20);
let dst3 = l3.resample(&src, 20, 20);
assert!(dst2.is_ok());
assert!(dst3.is_ok());
}
#[test]
fn test_map_edge_coord() {
assert_eq!(map_edge_coord(-1, 5, EdgeMode::Clamp), 0);
assert_eq!(map_edge_coord(-100, 5, EdgeMode::Clamp), 0);
assert_eq!(map_edge_coord(2, 5, EdgeMode::Clamp), 2);
assert_eq!(map_edge_coord(5, 5, EdgeMode::Clamp), 4);
assert_eq!(map_edge_coord(100, 5, EdgeMode::Clamp), 4);
assert_eq!(map_edge_coord(-1, 5, EdgeMode::Wrap), 4);
assert_eq!(map_edge_coord(-6, 5, EdgeMode::Wrap), 4);
assert_eq!(map_edge_coord(5, 5, EdgeMode::Wrap), 0);
assert_eq!(map_edge_coord(7, 5, EdgeMode::Wrap), 2);
let expected = [4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0];
for (i, c) in (-4i64..=8).enumerate() {
assert_eq!(
map_edge_coord(c, 5, EdgeMode::Mirror),
expected[i],
"mirror mismatch at c={c}"
);
}
for mode in [EdgeMode::Clamp, EdgeMode::Wrap, EdgeMode::Mirror] {
assert_eq!(map_edge_coord(-3, 1, mode), 0);
assert_eq!(map_edge_coord(0, 1, mode), 0);
assert_eq!(map_edge_coord(3, 1, mode), 0);
}
}
#[test]
fn test_edge_modes_constant_image() {
let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
src.set_pixel(x, y, 42.0).ok();
}
}
let resampler = LanczosResampler::new(3);
for mode in [EdgeMode::Clamp, EdgeMode::Wrap, EdgeMode::Mirror] {
let dst = resampler
.resample_with_edge_mode(&src, 10, 10, mode)
.unwrap_or_else(|e| panic!("resample with {mode:?} failed: {e}"));
for y in 0..10 {
for x in 0..10 {
let v = dst
.get_pixel(x, y)
.unwrap_or_else(|e| panic!("get_pixel({x},{y}) failed: {e}"));
assert!(
(v - 42.0).abs() < 1e-6,
"mode {mode:?}: pixel ({x},{y}) = {v}, expected 42.0"
);
}
}
}
}
#[test]
fn test_edge_modes_ramp_spot_check() {
let width = 8u64;
let height = 3u64;
let mut src = RasterBuffer::zeros(width, height, RasterDataType::Float32);
for y in 0..height {
for x in 0..width {
src.set_pixel(x, y, x as f64).ok();
}
}
let resampler = LanczosResampler::new(2);
let dst_width = 5u64;
let dst_height = 3u64;
let expected: &[(EdgeMode, f64)] = &[
(EdgeMode::Clamp, 0.243_460_891_937_171_1),
(EdgeMode::Wrap, -0.353_871_041_402_479_4),
(EdgeMode::Mirror, 0.158_127_758_602_935_3),
];
for &(mode, expected_value) in expected {
let dst = resampler
.resample_with_edge_mode(&src, dst_width, dst_height, mode)
.unwrap_or_else(|e| panic!("resample with {mode:?} failed: {e}"));
let v = dst
.get_pixel(0, 1)
.unwrap_or_else(|e| panic!("get_pixel(0,1) failed: {e}"));
assert!(
(v - expected_value).abs() < 1e-6,
"mode {mode:?}: pixel (0,1) = {v}, expected {expected_value}"
);
}
assert!((expected[0].1 - expected[1].1).abs() > 0.1);
assert!((expected[0].1 - expected[2].1).abs() > 0.05);
}
#[test]
fn test_lanczos_zero_dimensions() {
let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
let resampler = LanczosResampler::new(3);
assert!(resampler.resample(&src, 0, 10).is_err());
assert!(resampler.resample(&src, 10, 0).is_err());
}
}