oxigdal_algorithms/resampling/
lanczos.rs1use crate::error::{AlgorithmError, Result};
20use crate::resampling::kernel::{lanczos, normalize_weights};
21use oxigdal_core::buffer::RasterBuffer;
22
23#[derive(Debug, Clone, Copy)]
25pub struct LanczosResampler {
26 lobes: usize,
28}
29
30impl Default for LanczosResampler {
31 fn default() -> Self {
32 Self::new(3)
33 }
34}
35
36impl LanczosResampler {
37 #[must_use]
47 pub const fn new(lobes: usize) -> Self {
48 Self { lobes }
49 }
50
51 #[must_use]
53 pub const fn lobes(&self) -> usize {
54 self.lobes
55 }
56
57 #[must_use]
59 pub const fn radius(&self) -> usize {
60 self.lobes
61 }
62
63 pub fn resample(
69 &self,
70 src: &RasterBuffer,
71 dst_width: u64,
72 dst_height: u64,
73 ) -> Result<RasterBuffer> {
74 if dst_width == 0 || dst_height == 0 {
75 return Err(AlgorithmError::InvalidParameter {
76 parameter: "dimensions",
77 message: "Target dimensions must be non-zero".to_string(),
78 });
79 }
80
81 let src_width = src.width();
82 let src_height = src.height();
83
84 if src_width == 0 || src_height == 0 {
85 return Err(AlgorithmError::EmptyInput {
86 operation: "Lanczos resampling",
87 });
88 }
89
90 let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
91
92 let scale_x = src_width as f64 / dst_width as f64;
93 let scale_y = src_height as f64 / dst_height as f64;
94
95 for dst_y in 0..dst_height {
96 for dst_x in 0..dst_width {
97 let src_x = (dst_x as f64 + 0.5) * scale_x - 0.5;
98 let src_y = (dst_y as f64 + 0.5) * scale_y - 0.5;
99
100 let value = self.interpolate_at(src, src_x, src_y)?;
101 dst.set_pixel(dst_x, dst_y, value)
102 .map_err(AlgorithmError::Core)?;
103 }
104 }
105
106 Ok(dst)
107 }
108
109 fn interpolate_at(&self, src: &RasterBuffer, src_x: f64, src_y: f64) -> Result<f64> {
111 let src_width = src.width();
112 let src_height = src.height();
113
114 let src_x_clamped = src_x.max(0.0).min((src_width - 1) as f64);
116 let src_y_clamped = src_y.max(0.0).min((src_height - 1) as f64);
117
118 let x_center = src_x_clamped.floor() as i64;
120 let y_center = src_y_clamped.floor() as i64;
121
122 let radius = self.radius() as i64;
124 let x_start = x_center - radius + 1;
125 let x_end = x_center + radius + 1;
126 let y_start = y_center - radius + 1;
127 let y_end = y_center + radius + 1;
128
129 let kernel_width = (x_end - x_start) as usize;
130 let kernel_height = (y_end - y_start) as usize;
131
132 let total_samples = kernel_width * kernel_height;
134 let mut values = vec![0.0f64; total_samples];
135 let mut weights = vec![0.0f64; total_samples];
136
137 let mut idx = 0;
139 for sy in y_start..y_end {
140 for sx in x_start..x_end {
141 let sample_x = sx.max(0).min(src_width as i64 - 1) as u64;
143 let sample_y = sy.max(0).min(src_height as i64 - 1) as u64;
144
145 values[idx] = src
147 .get_pixel(sample_x, sample_y)
148 .map_err(AlgorithmError::Core)?;
149
150 let dx = sx as f64 - src_x_clamped;
152 let dy = sy as f64 - src_y_clamped;
153 let wx = lanczos(dx, self.lobes);
154 let wy = lanczos(dy, self.lobes);
155 weights[idx] = wx * wy;
156
157 idx += 1;
158 }
159 }
160
161 normalize_weights(&mut weights);
163
164 let mut result = 0.0;
166 for (value, weight) in values.iter().zip(weights.iter()) {
167 result += value * weight;
168 }
169
170 Ok(result)
171 }
172
173 pub fn resample_with_edge_mode(
188 &self,
189 src: &RasterBuffer,
190 dst_width: u64,
191 dst_height: u64,
192 edge_mode: EdgeMode,
193 ) -> Result<RasterBuffer> {
194 match edge_mode {
196 EdgeMode::Clamp => self.resample(src, dst_width, dst_height),
197 EdgeMode::Wrap => Err(AlgorithmError::UnsupportedOperation {
198 operation: "Wrap edge mode not yet implemented".to_string(),
199 }),
200 EdgeMode::Mirror => Err(AlgorithmError::UnsupportedOperation {
201 operation: "Mirror edge mode not yet implemented".to_string(),
202 }),
203 }
204 }
205
206 pub fn resample_separable(
215 &self,
216 src: &RasterBuffer,
217 dst_width: u64,
218 dst_height: u64,
219 ) -> Result<RasterBuffer> {
220 if dst_width == 0 || dst_height == 0 {
221 return Err(AlgorithmError::InvalidParameter {
222 parameter: "dimensions",
223 message: "Target dimensions must be non-zero".to_string(),
224 });
225 }
226
227 let src_width = src.width();
228 let src_height = src.height();
229
230 let mut temp = RasterBuffer::zeros(dst_width, src_height, src.data_type());
232 let scale_x = src_width as f64 / dst_width as f64;
233
234 for src_y in 0..src_height {
235 for dst_x in 0..dst_width {
236 let src_x = (dst_x as f64 + 0.5) * scale_x - 0.5;
237 let value = self.interpolate_horizontal(src, src_x, src_y)?;
238 temp.set_pixel(dst_x, src_y, value)
239 .map_err(AlgorithmError::Core)?;
240 }
241 }
242
243 let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
245 let scale_y = src_height as f64 / dst_height as f64;
246
247 for dst_x in 0..dst_width {
248 for dst_y in 0..dst_height {
249 let src_y = (dst_y as f64 + 0.5) * scale_y - 0.5;
250 let value = self.interpolate_vertical(&temp, dst_x, src_y)?;
251 dst.set_pixel(dst_x, dst_y, value)
252 .map_err(AlgorithmError::Core)?;
253 }
254 }
255
256 Ok(dst)
257 }
258
259 fn interpolate_horizontal(&self, src: &RasterBuffer, src_x: f64, y: u64) -> Result<f64> {
261 let src_width = src.width();
262 let src_x_clamped = src_x.max(0.0).min((src_width - 1) as f64);
263 let x_center = src_x_clamped.floor() as i64;
264
265 let radius = self.radius() as i64;
266 let x_start = x_center - radius + 1;
267 let x_end = x_center + radius + 1;
268
269 let kernel_width = (x_end - x_start) as usize;
270 let mut values = vec![0.0f64; kernel_width];
271 let mut weights = vec![0.0f64; kernel_width];
272
273 for (idx, sx) in (x_start..x_end).enumerate() {
274 let sample_x = sx.max(0).min(src_width as i64 - 1) as u64;
275 values[idx] = src.get_pixel(sample_x, y).map_err(AlgorithmError::Core)?;
276 let dx = sx as f64 - src_x_clamped;
277 weights[idx] = lanczos(dx, self.lobes);
278 }
279
280 normalize_weights(&mut weights);
281
282 let result = values.iter().zip(weights.iter()).map(|(v, w)| v * w).sum();
283
284 Ok(result)
285 }
286
287 fn interpolate_vertical(&self, src: &RasterBuffer, x: u64, src_y: f64) -> Result<f64> {
289 let src_height = src.height();
290 let src_y_clamped = src_y.max(0.0).min((src_height - 1) as f64);
291 let y_center = src_y_clamped.floor() as i64;
292
293 let radius = self.radius() as i64;
294 let y_start = y_center - radius + 1;
295 let y_end = y_center + radius + 1;
296
297 let kernel_height = (y_end - y_start) as usize;
298 let mut values = vec![0.0f64; kernel_height];
299 let mut weights = vec![0.0f64; kernel_height];
300
301 for (idx, sy) in (y_start..y_end).enumerate() {
302 let sample_y = sy.max(0).min(src_height as i64 - 1) as u64;
303 values[idx] = src.get_pixel(x, sample_y).map_err(AlgorithmError::Core)?;
304 let dy = sy as f64 - src_y_clamped;
305 weights[idx] = lanczos(dy, self.lobes);
306 }
307
308 normalize_weights(&mut weights);
309
310 let result = values.iter().zip(weights.iter()).map(|(v, w)| v * w).sum();
311
312 Ok(result)
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub enum EdgeMode {
319 Clamp,
321 Wrap,
323 Mirror,
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use oxigdal_core::types::RasterDataType;
331
332 #[test]
333 fn test_lanczos_creation() {
334 let l2 = LanczosResampler::new(2);
335 assert_eq!(l2.lobes(), 2);
336 assert_eq!(l2.radius(), 2);
337
338 let l3 = LanczosResampler::new(3);
339 assert_eq!(l3.lobes(), 3);
340 assert_eq!(l3.radius(), 3);
341 }
342
343 #[test]
344 fn test_lanczos_identity() {
345 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
346 for y in 0..10 {
347 for x in 0..10 {
348 src.set_pixel(x, y, (y * 10 + x) as f64).ok();
349 }
350 }
351
352 let resampler = LanczosResampler::new(3);
353 let dst = resampler.resample(&src, 10, 10);
354 assert!(dst.is_ok());
355 }
356
357 #[test]
358 fn test_lanczos_quality() {
359 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
361 for y in 0..5 {
362 for x in 0..5 {
363 src.set_pixel(x, y, ((x + y) * (x + y)) as f64).ok();
364 }
365 }
366
367 let resampler = LanczosResampler::new(3);
368 let dst = resampler.resample(&src, 10, 10);
369 assert!(dst.is_ok());
370
371 if let Ok(dst) = dst {
373 let v1 = dst.get_pixel(4, 4).ok();
374 let v2 = dst.get_pixel(5, 5).ok();
375 assert!(v1.is_some());
376 assert!(v2.is_some());
377 }
378 }
379
380 #[test]
381 fn test_lanczos_separable() {
382 let mut src = RasterBuffer::zeros(8, 8, RasterDataType::Float32);
383 for y in 0..8 {
384 for x in 0..8 {
385 src.set_pixel(x, y, (x + y) as f64).ok();
386 }
387 }
388
389 let resampler = LanczosResampler::new(3);
390 let dst1 = resampler.resample(&src, 16, 16);
391 let dst2 = resampler.resample_separable(&src, 16, 16);
392
393 assert!(dst1.is_ok());
394 assert!(dst2.is_ok());
395
396 }
398
399 #[test]
400 fn test_lanczos_lobes() {
401 let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
402
403 let l2 = LanczosResampler::new(2);
404 let l3 = LanczosResampler::new(3);
405
406 let dst2 = l2.resample(&src, 20, 20);
407 let dst3 = l3.resample(&src, 20, 20);
408
409 assert!(dst2.is_ok());
410 assert!(dst3.is_ok());
411 }
412
413 #[test]
414 fn test_edge_modes() {
415 let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
416 let resampler = LanczosResampler::new(3);
417
418 let clamp = resampler.resample_with_edge_mode(&src, 10, 10, EdgeMode::Clamp);
419 assert!(clamp.is_ok());
420
421 let wrap = resampler.resample_with_edge_mode(&src, 10, 10, EdgeMode::Wrap);
423 assert!(wrap.is_err());
424
425 let mirror = resampler.resample_with_edge_mode(&src, 10, 10, EdgeMode::Mirror);
426 assert!(mirror.is_err());
427 }
428
429 #[test]
430 fn test_lanczos_zero_dimensions() {
431 let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
432 let resampler = LanczosResampler::new(3);
433
434 assert!(resampler.resample(&src, 0, 10).is_err());
435 assert!(resampler.resample(&src, 10, 0).is_err());
436 }
437}