Skip to main content

oxigdal_algorithms/resampling/
lanczos.rs

1//! Lanczos resampling algorithm
2//!
3//! Lanczos resampling provides the highest quality results using a windowed sinc filter.
4//! It preserves sharp edges better than bicubic while avoiding excessive ringing.
5//!
6//! # Characteristics
7//!
8//! - **Speed**: Slowest (36-100 samples per pixel)
9//! - **Quality**: Highest, excellent edge preservation
10//! - **Best for**: High-quality imagery, when quality is paramount
11//!
12//! # Algorithm
13//!
14//! For each output pixel:
15//! 1. Sample (2a) x (2a) neighborhood (where a = lobes, typically 2 or 3)
16//! 2. Apply Lanczos kernel: L(x) = sinc(x) * sinc(x/a)
17//! 3. Normalize weights and sum
18
19use crate::error::{AlgorithmError, Result};
20use crate::resampling::kernel::{lanczos, normalize_weights};
21use oxigdal_core::buffer::RasterBuffer;
22
23/// Lanczos resampler with configurable lobe count
24#[derive(Debug, Clone, Copy)]
25pub struct LanczosResampler {
26    /// Number of lobes (typically 2 or 3)
27    lobes: usize,
28}
29
30impl Default for LanczosResampler {
31    fn default() -> Self {
32        Self::new(3)
33    }
34}
35
36impl LanczosResampler {
37    /// Creates a new Lanczos resampler with specified lobe count
38    ///
39    /// # Arguments
40    ///
41    /// * `lobes` - Number of lobes (2 = faster, 3 = higher quality)
42    ///
43    /// Common values:
44    /// - 2: Lanczos2 - faster, still good quality
45    /// - 3: Lanczos3 - standard, excellent quality (default)
46    #[must_use]
47    pub const fn new(lobes: usize) -> Self {
48        Self { lobes }
49    }
50
51    /// Returns the lobe count
52    #[must_use]
53    pub const fn lobes(&self) -> usize {
54        self.lobes
55    }
56
57    /// Returns the kernel radius (same as lobe count)
58    #[must_use]
59    pub const fn radius(&self) -> usize {
60        self.lobes
61    }
62
63    /// Resamples a raster buffer using Lanczos interpolation
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if dimensions are invalid
68    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    /// Interpolates value at fractional source coordinates using Lanczos
110    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        // Clamp to valid range
115        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        // Get integer center
119        let x_center = src_x_clamped.floor() as i64;
120        let y_center = src_y_clamped.floor() as i64;
121
122        // Compute kernel range
123        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        // Allocate weight and value buffers
133        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        // Sample neighborhood and compute weights
138        let mut idx = 0;
139        for sy in y_start..y_end {
140            for sx in x_start..x_end {
141                // Clamp to valid range
142                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                // Get value
146                values[idx] = src
147                    .get_pixel(sample_x, sample_y)
148                    .map_err(AlgorithmError::Core)?;
149
150                // Compute kernel weight
151                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
162        normalize_weights(&mut weights);
163
164        // Compute weighted sum
165        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    /// Resamples with edge handling
174    ///
175    /// This variant allows specifying how to handle edge pixels.
176    ///
177    /// # Arguments
178    ///
179    /// * `src` - Source buffer
180    /// * `dst_width` - Destination width
181    /// * `dst_height` - Destination height
182    /// * `edge_mode` - How to handle edge pixels
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if parameters are invalid
187    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        // For now, only clamp mode is implemented
195        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    /// Separable Lanczos resampling (optimized 2-pass)
207    ///
208    /// This performs resampling in two passes (horizontal then vertical)
209    /// which is more cache-friendly and can be faster for large kernels.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if parameters are invalid
214    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        // Pass 1: Horizontal resampling
231        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        // Pass 2: Vertical resampling
244        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    /// Horizontal 1D interpolation
260    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    /// Vertical 1D interpolation
288    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/// Edge handling modes for resampling
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub enum EdgeMode {
319    /// Clamp coordinates to edge (default)
320    Clamp,
321    /// Wrap coordinates (for tiled data)
322    Wrap,
323    /// Mirror coordinates at edges
324    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        // Lanczos should produce high-quality smooth results
360        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        // Result should be smooth
372        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        // Results should be similar (not identical due to rounding)
397    }
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        // Wrap and Mirror not yet implemented
422        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}