1use crate::error::{AlgorithmError, Result};
20use crate::resampling::kernel::{lanczos, normalize_weights};
21use oxigeo_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(
74 &self,
75 src: &RasterBuffer,
76 dst_width: u64,
77 dst_height: u64,
78 ) -> Result<RasterBuffer> {
79 self.resample_with_edge_mode(src, dst_width, dst_height, EdgeMode::Clamp)
80 }
81
82 fn interpolate_at(
88 &self,
89 src: &RasterBuffer,
90 src_x: f64,
91 src_y: f64,
92 edge_mode: EdgeMode,
93 ) -> Result<f64> {
94 let src_width = src.width();
95 let src_height = src.height();
96
97 let src_x_clamped = src_x.max(0.0).min((src_width - 1) as f64);
99 let src_y_clamped = src_y.max(0.0).min((src_height - 1) as f64);
100
101 let x_center = src_x_clamped.floor() as i64;
103 let y_center = src_y_clamped.floor() as i64;
104
105 let radius = self.radius() as i64;
107 let x_start = x_center - radius + 1;
108 let x_end = x_center + radius + 1;
109 let y_start = y_center - radius + 1;
110 let y_end = y_center + radius + 1;
111
112 let kernel_width = (x_end - x_start) as usize;
113 let kernel_height = (y_end - y_start) as usize;
114
115 let total_samples = kernel_width * kernel_height;
117 let mut values = vec![0.0f64; total_samples];
118 let mut weights = vec![0.0f64; total_samples];
119
120 let mut idx = 0;
122 for sy in y_start..y_end {
123 for sx in x_start..x_end {
124 let sample_x = map_edge_coord(sx, src_width, edge_mode);
127 let sample_y = map_edge_coord(sy, src_height, edge_mode);
128
129 values[idx] = src
131 .get_pixel(sample_x, sample_y)
132 .map_err(AlgorithmError::Core)?;
133
134 let dx = sx as f64 - src_x_clamped;
136 let dy = sy as f64 - src_y_clamped;
137 let wx = lanczos(dx, self.lobes);
138 let wy = lanczos(dy, self.lobes);
139 weights[idx] = wx * wy;
140
141 idx += 1;
142 }
143 }
144
145 normalize_weights(&mut weights);
147
148 let mut result = 0.0;
150 for (value, weight) in values.iter().zip(weights.iter()) {
151 result += value * weight;
152 }
153
154 Ok(result)
155 }
156
157 pub fn resample_with_edge_mode(
174 &self,
175 src: &RasterBuffer,
176 dst_width: u64,
177 dst_height: u64,
178 edge_mode: EdgeMode,
179 ) -> Result<RasterBuffer> {
180 if dst_width == 0 || dst_height == 0 {
181 return Err(AlgorithmError::InvalidParameter {
182 parameter: "dimensions",
183 message: "Target dimensions must be non-zero".to_string(),
184 });
185 }
186
187 let src_width = src.width();
188 let src_height = src.height();
189
190 if src_width == 0 || src_height == 0 {
191 return Err(AlgorithmError::EmptyInput {
192 operation: "Lanczos resampling",
193 });
194 }
195
196 let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
197
198 let scale_x = src_width as f64 / dst_width as f64;
199 let scale_y = src_height as f64 / dst_height as f64;
200
201 for dst_y in 0..dst_height {
202 for dst_x in 0..dst_width {
203 let src_x = (dst_x as f64 + 0.5) * scale_x - 0.5;
204 let src_y = (dst_y as f64 + 0.5) * scale_y - 0.5;
205
206 let value = self.interpolate_at(src, src_x, src_y, edge_mode)?;
207 dst.set_pixel(dst_x, dst_y, value)
208 .map_err(AlgorithmError::Core)?;
209 }
210 }
211
212 Ok(dst)
213 }
214
215 pub fn resample_separable(
224 &self,
225 src: &RasterBuffer,
226 dst_width: u64,
227 dst_height: u64,
228 ) -> Result<RasterBuffer> {
229 if dst_width == 0 || dst_height == 0 {
230 return Err(AlgorithmError::InvalidParameter {
231 parameter: "dimensions",
232 message: "Target dimensions must be non-zero".to_string(),
233 });
234 }
235
236 let src_width = src.width();
237 let src_height = src.height();
238
239 let mut temp = RasterBuffer::zeros(dst_width, src_height, src.data_type());
241 let scale_x = src_width as f64 / dst_width as f64;
242
243 for src_y in 0..src_height {
244 for dst_x in 0..dst_width {
245 let src_x = (dst_x as f64 + 0.5) * scale_x - 0.5;
246 let value = self.interpolate_horizontal(src, src_x, src_y)?;
247 temp.set_pixel(dst_x, src_y, value)
248 .map_err(AlgorithmError::Core)?;
249 }
250 }
251
252 let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
254 let scale_y = src_height as f64 / dst_height as f64;
255
256 for dst_x in 0..dst_width {
257 for dst_y in 0..dst_height {
258 let src_y = (dst_y as f64 + 0.5) * scale_y - 0.5;
259 let value = self.interpolate_vertical(&temp, dst_x, src_y)?;
260 dst.set_pixel(dst_x, dst_y, value)
261 .map_err(AlgorithmError::Core)?;
262 }
263 }
264
265 Ok(dst)
266 }
267
268 fn interpolate_horizontal(&self, src: &RasterBuffer, src_x: f64, y: u64) -> Result<f64> {
270 let src_width = src.width();
271 let src_x_clamped = src_x.max(0.0).min((src_width - 1) as f64);
272 let x_center = src_x_clamped.floor() as i64;
273
274 let radius = self.radius() as i64;
275 let x_start = x_center - radius + 1;
276 let x_end = x_center + radius + 1;
277
278 let kernel_width = (x_end - x_start) as usize;
279 let mut values = vec![0.0f64; kernel_width];
280 let mut weights = vec![0.0f64; kernel_width];
281
282 for (idx, sx) in (x_start..x_end).enumerate() {
283 let sample_x = sx.max(0).min(src_width as i64 - 1) as u64;
284 values[idx] = src.get_pixel(sample_x, y).map_err(AlgorithmError::Core)?;
285 let dx = sx as f64 - src_x_clamped;
286 weights[idx] = lanczos(dx, self.lobes);
287 }
288
289 normalize_weights(&mut weights);
290
291 let result = values.iter().zip(weights.iter()).map(|(v, w)| v * w).sum();
292
293 Ok(result)
294 }
295
296 fn interpolate_vertical(&self, src: &RasterBuffer, x: u64, src_y: f64) -> Result<f64> {
298 let src_height = src.height();
299 let src_y_clamped = src_y.max(0.0).min((src_height - 1) as f64);
300 let y_center = src_y_clamped.floor() as i64;
301
302 let radius = self.radius() as i64;
303 let y_start = y_center - radius + 1;
304 let y_end = y_center + radius + 1;
305
306 let kernel_height = (y_end - y_start) as usize;
307 let mut values = vec![0.0f64; kernel_height];
308 let mut weights = vec![0.0f64; kernel_height];
309
310 for (idx, sy) in (y_start..y_end).enumerate() {
311 let sample_y = sy.max(0).min(src_height as i64 - 1) as u64;
312 values[idx] = src.get_pixel(x, sample_y).map_err(AlgorithmError::Core)?;
313 let dy = sy as f64 - src_y_clamped;
314 weights[idx] = lanczos(dy, self.lobes);
315 }
316
317 normalize_weights(&mut weights);
318
319 let result = values.iter().zip(weights.iter()).map(|(v, w)| v * w).sum();
320
321 Ok(result)
322 }
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum EdgeMode {
328 Clamp,
330 Wrap,
332 Mirror,
334}
335
336#[must_use]
351fn map_edge_coord(c: i64, size: u64, mode: EdgeMode) -> u64 {
352 if size == 0 {
353 return 0;
354 }
355 let size_i = size as i64;
356
357 match mode {
358 EdgeMode::Clamp => c.clamp(0, size_i - 1) as u64,
359 EdgeMode::Wrap => c.rem_euclid(size_i) as u64,
360 EdgeMode::Mirror => {
361 if size_i == 1 {
362 return 0;
363 }
364 let period = 2 * (size_i - 1);
366 let m = c.rem_euclid(period);
367 if m < size_i {
368 m as u64
369 } else {
370 (period - m) as u64
371 }
372 }
373 }
374}
375
376#[cfg(test)]
377#[allow(clippy::panic)]
378mod tests {
379 use super::*;
380 use oxigeo_core::types::RasterDataType;
381
382 #[test]
383 fn test_lanczos_creation() {
384 let l2 = LanczosResampler::new(2);
385 assert_eq!(l2.lobes(), 2);
386 assert_eq!(l2.radius(), 2);
387
388 let l3 = LanczosResampler::new(3);
389 assert_eq!(l3.lobes(), 3);
390 assert_eq!(l3.radius(), 3);
391 }
392
393 #[test]
394 fn test_lanczos_identity() {
395 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
396 for y in 0..10 {
397 for x in 0..10 {
398 src.set_pixel(x, y, (y * 10 + x) as f64).ok();
399 }
400 }
401
402 let resampler = LanczosResampler::new(3);
403 let dst = resampler.resample(&src, 10, 10);
404 assert!(dst.is_ok());
405 }
406
407 #[test]
408 fn test_lanczos_quality() {
409 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
411 for y in 0..5 {
412 for x in 0..5 {
413 src.set_pixel(x, y, ((x + y) * (x + y)) as f64).ok();
414 }
415 }
416
417 let resampler = LanczosResampler::new(3);
418 let dst = resampler.resample(&src, 10, 10);
419 assert!(dst.is_ok());
420
421 if let Ok(dst) = dst {
423 let v1 = dst.get_pixel(4, 4).ok();
424 let v2 = dst.get_pixel(5, 5).ok();
425 assert!(v1.is_some());
426 assert!(v2.is_some());
427 }
428 }
429
430 #[test]
431 fn test_lanczos_separable() {
432 let mut src = RasterBuffer::zeros(8, 8, RasterDataType::Float32);
433 for y in 0..8 {
434 for x in 0..8 {
435 src.set_pixel(x, y, (x + y) as f64).ok();
436 }
437 }
438
439 let resampler = LanczosResampler::new(3);
440 let dst1 = resampler.resample(&src, 16, 16);
441 let dst2 = resampler.resample_separable(&src, 16, 16);
442
443 assert!(dst1.is_ok());
444 assert!(dst2.is_ok());
445
446 }
448
449 #[test]
450 fn test_lanczos_lobes() {
451 let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
452
453 let l2 = LanczosResampler::new(2);
454 let l3 = LanczosResampler::new(3);
455
456 let dst2 = l2.resample(&src, 20, 20);
457 let dst3 = l3.resample(&src, 20, 20);
458
459 assert!(dst2.is_ok());
460 assert!(dst3.is_ok());
461 }
462
463 #[test]
464 fn test_map_edge_coord() {
465 assert_eq!(map_edge_coord(-1, 5, EdgeMode::Clamp), 0);
467 assert_eq!(map_edge_coord(-100, 5, EdgeMode::Clamp), 0);
468 assert_eq!(map_edge_coord(2, 5, EdgeMode::Clamp), 2);
469 assert_eq!(map_edge_coord(5, 5, EdgeMode::Clamp), 4);
470 assert_eq!(map_edge_coord(100, 5, EdgeMode::Clamp), 4);
471
472 assert_eq!(map_edge_coord(-1, 5, EdgeMode::Wrap), 4);
474 assert_eq!(map_edge_coord(-6, 5, EdgeMode::Wrap), 4);
475 assert_eq!(map_edge_coord(5, 5, EdgeMode::Wrap), 0);
476 assert_eq!(map_edge_coord(7, 5, EdgeMode::Wrap), 2);
477
478 let expected = [4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0];
481 for (i, c) in (-4i64..=8).enumerate() {
482 assert_eq!(
483 map_edge_coord(c, 5, EdgeMode::Mirror),
484 expected[i],
485 "mirror mismatch at c={c}"
486 );
487 }
488
489 for mode in [EdgeMode::Clamp, EdgeMode::Wrap, EdgeMode::Mirror] {
491 assert_eq!(map_edge_coord(-3, 1, mode), 0);
492 assert_eq!(map_edge_coord(0, 1, mode), 0);
493 assert_eq!(map_edge_coord(3, 1, mode), 0);
494 }
495 }
496
497 #[test]
498 fn test_edge_modes_constant_image() {
499 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
504 for y in 0..5 {
505 for x in 0..5 {
506 src.set_pixel(x, y, 42.0).ok();
507 }
508 }
509 let resampler = LanczosResampler::new(3);
510
511 for mode in [EdgeMode::Clamp, EdgeMode::Wrap, EdgeMode::Mirror] {
512 let dst = resampler
513 .resample_with_edge_mode(&src, 10, 10, mode)
514 .unwrap_or_else(|e| panic!("resample with {mode:?} failed: {e}"));
515 for y in 0..10 {
516 for x in 0..10 {
517 let v = dst
518 .get_pixel(x, y)
519 .unwrap_or_else(|e| panic!("get_pixel({x},{y}) failed: {e}"));
520 assert!(
521 (v - 42.0).abs() < 1e-6,
522 "mode {mode:?}: pixel ({x},{y}) = {v}, expected 42.0"
523 );
524 }
525 }
526 }
527 }
528
529 #[test]
530 fn test_edge_modes_ramp_spot_check() {
531 let width = 8u64;
543 let height = 3u64;
544 let mut src = RasterBuffer::zeros(width, height, RasterDataType::Float32);
545 for y in 0..height {
546 for x in 0..width {
547 src.set_pixel(x, y, x as f64).ok();
548 }
549 }
550
551 let resampler = LanczosResampler::new(2);
552 let dst_width = 5u64;
553 let dst_height = 3u64;
554
555 let expected: &[(EdgeMode, f64)] = &[
556 (EdgeMode::Clamp, 0.243_460_891_937_171_1),
557 (EdgeMode::Wrap, -0.353_871_041_402_479_4),
558 (EdgeMode::Mirror, 0.158_127_758_602_935_3),
559 ];
560
561 for &(mode, expected_value) in expected {
562 let dst = resampler
563 .resample_with_edge_mode(&src, dst_width, dst_height, mode)
564 .unwrap_or_else(|e| panic!("resample with {mode:?} failed: {e}"));
565 let v = dst
566 .get_pixel(0, 1)
567 .unwrap_or_else(|e| panic!("get_pixel(0,1) failed: {e}"));
568 assert!(
569 (v - expected_value).abs() < 1e-6,
570 "mode {mode:?}: pixel (0,1) = {v}, expected {expected_value}"
571 );
572 }
573
574 assert!((expected[0].1 - expected[1].1).abs() > 0.1);
578 assert!((expected[0].1 - expected[2].1).abs() > 0.05);
579 }
580
581 #[test]
582 fn test_lanczos_zero_dimensions() {
583 let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
584 let resampler = LanczosResampler::new(3);
585
586 assert!(resampler.resample(&src, 0, 10).is_err());
587 assert!(resampler.resample(&src, 10, 0).is_err());
588 }
589}