Skip to main content

oxigdal_algorithms/resampling/
nearest.rs

1//! Nearest neighbor resampling algorithm
2//!
3//! This is the fastest resampling method, which assigns each output pixel
4//! the value of the nearest input pixel. No interpolation is performed.
5//!
6//! # Characteristics
7//!
8//! - **Speed**: Fastest (O(1) per pixel)
9//! - **Quality**: Lowest (blocky for upsampling)
10//! - **Preservation**: Exact input values preserved
11//! - **Best for**: Categorical data, classifications, masks
12//!
13//! # Algorithm
14//!
15//! For each output pixel at (dst_x, dst_y):
16//! 1. Compute source position: src_x = dst_x * scale_x, src_y = dst_y * scale_y
17//! 2. Round to nearest integer: src_x = round(src_x), src_y = round(src_y)
18//! 3. Copy value: dst[dst_y][dst_x] = src[src_y][src_x]
19
20use crate::error::{AlgorithmError, Result};
21use oxigdal_core::buffer::RasterBuffer;
22
23/// Nearest neighbor resampler
24#[derive(Debug, Clone, Copy, Default)]
25pub struct NearestResampler;
26
27impl NearestResampler {
28    /// Creates a new nearest neighbor resampler
29    #[must_use]
30    pub const fn new() -> Self {
31        Self
32    }
33
34    /// Resamples a raster buffer using nearest neighbor
35    ///
36    /// # Arguments
37    ///
38    /// * `src` - Source raster buffer
39    /// * `dst_width` - Target width in pixels
40    /// * `dst_height` - Target height in pixels
41    ///
42    /// # Errors
43    ///
44    /// Returns an error if:
45    /// - Target dimensions are zero
46    /// - Source buffer is invalid
47    /// - Memory allocation fails
48    pub fn resample(
49        &self,
50        src: &RasterBuffer,
51        dst_width: u64,
52        dst_height: u64,
53    ) -> Result<RasterBuffer> {
54        if dst_width == 0 || dst_height == 0 {
55            return Err(AlgorithmError::InvalidParameter {
56                parameter: "dimensions",
57                message: "Target dimensions must be non-zero".to_string(),
58            });
59        }
60
61        let src_width = src.width();
62        let src_height = src.height();
63
64        if src_width == 0 || src_height == 0 {
65            return Err(AlgorithmError::EmptyInput {
66                operation: "nearest neighbor resampling",
67            });
68        }
69
70        // Create output buffer with same data type as source
71        let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
72
73        // Compute scaling factors
74        let scale_x = src_width as f64 / dst_width as f64;
75        let scale_y = src_height as f64 / dst_height as f64;
76
77        // For each output pixel
78        for dst_y in 0..dst_height {
79            for dst_x in 0..dst_width {
80                // Find nearest source pixel
81                let src_x = self.map_coordinate(dst_x as f64, scale_x, src_width);
82                let src_y = self.map_coordinate(dst_y as f64, scale_y, src_height);
83
84                // Copy value
85                let value = src.get_pixel(src_x, src_y).map_err(AlgorithmError::Core)?;
86                dst.set_pixel(dst_x, dst_y, value)
87                    .map_err(AlgorithmError::Core)?;
88            }
89        }
90
91        Ok(dst)
92    }
93
94    /// Maps a destination coordinate to the nearest source coordinate
95    ///
96    /// Uses pixel-center registration: pixel coordinates are at integer positions,
97    /// and pixel centers are at (i + 0.5, j + 0.5).
98    ///
99    /// # Arguments
100    ///
101    /// * `dst_coord` - Destination coordinate
102    /// * `scale` - Scaling factor (src_size / dst_size)
103    /// * `src_size` - Source dimension size
104    ///
105    /// # Returns
106    ///
107    /// The nearest source coordinate, clamped to [0, src_size)
108    #[inline]
109    fn map_coordinate(&self, dst_coord: f64, scale: f64, src_size: u64) -> u64 {
110        // Map from destination pixel center to source coordinate
111        let src_coord = (dst_coord + 0.5) * scale - 0.5;
112
113        // Round to nearest integer
114        let src_coord_rounded = src_coord.round();
115
116        // Clamp to valid range
117        let clamped = src_coord_rounded.max(0.0).min((src_size - 1) as f64);
118
119        clamped as u64
120    }
121
122    /// Resamples with explicit scaling factors
123    ///
124    /// This variant allows precise control over the resampling transformation.
125    ///
126    /// # Arguments
127    ///
128    /// * `src` - Source raster buffer
129    /// * `dst_width` - Target width
130    /// * `dst_height` - Target height
131    /// * `scale_x` - Horizontal scaling factor
132    /// * `scale_y` - Vertical scaling factor
133    /// * `offset_x` - Horizontal offset in source coordinates
134    /// * `offset_y` - Vertical offset in source coordinates
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if parameters are invalid
139    #[allow(clippy::too_many_arguments)]
140    pub fn resample_with_transform(
141        &self,
142        src: &RasterBuffer,
143        dst_width: u64,
144        dst_height: u64,
145        scale_x: f64,
146        scale_y: f64,
147        offset_x: f64,
148        offset_y: f64,
149    ) -> Result<RasterBuffer> {
150        if dst_width == 0 || dst_height == 0 {
151            return Err(AlgorithmError::InvalidParameter {
152                parameter: "dimensions",
153                message: "Target dimensions must be non-zero".to_string(),
154            });
155        }
156
157        if scale_x <= 0.0 || scale_y <= 0.0 {
158            return Err(AlgorithmError::InvalidParameter {
159                parameter: "scale",
160                message: "Scale factors must be positive".to_string(),
161            });
162        }
163
164        let src_width = src.width();
165        let src_height = src.height();
166
167        let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
168
169        for dst_y in 0..dst_height {
170            for dst_x in 0..dst_width {
171                // Apply transform
172                let src_x_f64 = dst_x as f64 * scale_x + offset_x;
173                let src_y_f64 = dst_y as f64 * scale_y + offset_y;
174
175                // Round and clamp
176                let src_x = src_x_f64.round().max(0.0).min((src_width - 1) as f64) as u64;
177                let src_y = src_y_f64.round().max(0.0).min((src_height - 1) as f64) as u64;
178
179                // Copy value
180                let value = src.get_pixel(src_x, src_y).map_err(AlgorithmError::Core)?;
181                dst.set_pixel(dst_x, dst_y, value)
182                    .map_err(AlgorithmError::Core)?;
183            }
184        }
185
186        Ok(dst)
187    }
188
189    /// Resamples with repeat edge mode (instead of clamp)
190    ///
191    /// When sampling outside the source bounds, this wraps coordinates
192    /// rather than clamping them. Useful for tiled or periodic data.
193    pub fn resample_repeat(
194        &self,
195        src: &RasterBuffer,
196        dst_width: u64,
197        dst_height: u64,
198    ) -> Result<RasterBuffer> {
199        if dst_width == 0 || dst_height == 0 {
200            return Err(AlgorithmError::InvalidParameter {
201                parameter: "dimensions",
202                message: "Target dimensions must be non-zero".to_string(),
203            });
204        }
205
206        let src_width = src.width();
207        let src_height = src.height();
208
209        if src_width == 0 || src_height == 0 {
210            return Err(AlgorithmError::EmptyInput {
211                operation: "nearest neighbor resampling",
212            });
213        }
214
215        let mut dst = RasterBuffer::zeros(dst_width, dst_height, src.data_type());
216
217        let scale_x = src_width as f64 / dst_width as f64;
218        let scale_y = src_height as f64 / dst_height as f64;
219
220        for dst_y in 0..dst_height {
221            for dst_x in 0..dst_width {
222                let src_coord_x = (dst_x as f64 + 0.5) * scale_x - 0.5;
223                let src_coord_y = (dst_y as f64 + 0.5) * scale_y - 0.5;
224
225                // Wrap coordinates
226                let src_x = (src_coord_x.round() as i64).rem_euclid(src_width as i64) as u64;
227                let src_y = (src_coord_y.round() as i64).rem_euclid(src_height as i64) as u64;
228
229                let value = src.get_pixel(src_x, src_y).map_err(AlgorithmError::Core)?;
230                dst.set_pixel(dst_x, dst_y, value)
231                    .map_err(AlgorithmError::Core)?;
232            }
233        }
234
235        Ok(dst)
236    }
237}
238
239#[cfg(feature = "simd")]
240mod simd_impl {
241    //! SIMD-accelerated nearest neighbor resampling
242    //!
243    //! While nearest neighbor doesn't benefit as much from SIMD as interpolation methods,
244    //! we can still vectorize the coordinate calculation and memory access patterns.
245
246    use super::*;
247
248    impl NearestResampler {
249        /// SIMD-accelerated resampling (when available)
250        ///
251        /// This uses platform-specific SIMD instructions to process multiple pixels
252        /// at once. Falls back to scalar implementation if SIMD is not available.
253        #[cfg(target_arch = "x86_64")]
254        pub fn resample_simd(
255            &self,
256            src: &RasterBuffer,
257            dst_width: u64,
258            dst_height: u64,
259        ) -> Result<RasterBuffer> {
260            // For nearest neighbor, SIMD doesn't provide huge benefits
261            // since we're just copying values. The main optimization is
262            // vectorizing the coordinate calculations.
263            //
264            // In a production implementation, we would:
265            // 1. Vectorize the scale_x/scale_y calculations
266            // 2. Use gather instructions for non-contiguous memory access
267            // 3. Batch pixel copies when possible
268            //
269            // For now, fall back to scalar
270            self.resample(src, dst_width, dst_height)
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use approx::assert_abs_diff_eq;
279    use oxigdal_core::types::RasterDataType;
280
281    #[test]
282    fn test_nearest_identity() {
283        // Resampling to same size should be identity
284        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
285
286        // Fill with test pattern
287        for y in 0..10 {
288            for x in 0..10 {
289                src.set_pixel(x, y, (y * 10 + x) as f64).ok();
290            }
291        }
292
293        let resampler = NearestResampler::new();
294        let result = resampler.resample(&src, 10, 10);
295        assert!(result.is_ok());
296
297        if let Ok(dst) = result {
298            for y in 0..10 {
299                for x in 0..10 {
300                    if let (Ok(sv), Ok(dv)) = (src.get_pixel(x, y), dst.get_pixel(x, y)) {
301                        assert_abs_diff_eq!(sv, dv, epsilon = 1e-10);
302                    }
303                }
304            }
305        }
306    }
307
308    #[test]
309    fn test_nearest_downsample() {
310        // Create 4x4 checkerboard
311        let mut src = RasterBuffer::zeros(4, 4, RasterDataType::Float32);
312        for y in 0..4 {
313            for x in 0..4 {
314                let value = if (x + y) % 2 == 0 { 1.0 } else { 0.0 };
315                src.set_pixel(x, y, value).ok();
316            }
317        }
318
319        let resampler = NearestResampler::new();
320        let dst = resampler.resample(&src, 2, 2);
321        assert!(dst.is_ok());
322    }
323
324    #[test]
325    fn test_nearest_upsample() {
326        // Create 2x2 source
327        let mut src = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
328        src.set_pixel(0, 0, 1.0).ok();
329        src.set_pixel(1, 0, 2.0).ok();
330        src.set_pixel(0, 1, 3.0).ok();
331        src.set_pixel(1, 1, 4.0).ok();
332
333        let resampler = NearestResampler::new();
334        let dst = resampler.resample(&src, 4, 4);
335        assert!(dst.is_ok());
336
337        // Values should be replicated (blocky)
338        if let Ok(dst) = dst {
339            // Top-left quadrant should be mostly 1.0
340            let val = dst.get_pixel(0, 0).ok();
341            assert!(val.is_some());
342        }
343    }
344
345    #[test]
346    fn test_nearest_zero_dimensions() {
347        let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
348        let resampler = NearestResampler::new();
349
350        assert!(resampler.resample(&src, 0, 10).is_err());
351        assert!(resampler.resample(&src, 10, 0).is_err());
352    }
353
354    #[test]
355    fn test_map_coordinate() {
356        let resampler = NearestResampler::new();
357
358        // Downsampling 2:1 (scale = 2.0)
359        // Formula: src_coord = (dst_coord + 0.5) * scale - 0.5, then round
360
361        // dst=0.0: (0.0 + 0.5) * 2.0 - 0.5 = 0.5 → rounds to 1 (Rust rounds 0.5 away from zero)
362        assert_eq!(resampler.map_coordinate(0.0, 2.0, 10), 1);
363
364        // dst=1.0: (1.0 + 0.5) * 2.0 - 0.5 = 2.5 → rounds to 3
365        assert_eq!(resampler.map_coordinate(1.0, 2.0, 10), 3);
366
367        // dst=2.0: (2.0 + 0.5) * 2.0 - 0.5 = 4.5 → rounds to 5
368        assert_eq!(resampler.map_coordinate(2.0, 2.0, 10), 5);
369
370        // dst=3.0: (3.0 + 0.5) * 2.0 - 0.5 = 6.5 → rounds to 7
371        assert_eq!(resampler.map_coordinate(3.0, 2.0, 10), 7);
372
373        // Upsampling 1:2 (scale = 0.5)
374        // dst=0.0: (0.0 + 0.5) * 0.5 - 0.5 = -0.25 → rounds to 0, clamped to 0
375        assert_eq!(resampler.map_coordinate(0.0, 0.5, 10), 0);
376
377        // dst=1.0: (1.0 + 0.5) * 0.5 - 0.5 = 0.25 → rounds to 0
378        assert_eq!(resampler.map_coordinate(1.0, 0.5, 10), 0);
379
380        // dst=2.0: (2.0 + 0.5) * 0.5 - 0.5 = 0.75 → rounds to 1
381        assert_eq!(resampler.map_coordinate(2.0, 0.5, 10), 1);
382
383        // Test clamping at upper bound
384        // dst=20.0: (20.0 + 0.5) * 2.0 - 0.5 = 40.5 → rounds to 41, clamped to 9
385        assert_eq!(resampler.map_coordinate(20.0, 2.0, 10), 9);
386    }
387
388    #[test]
389    fn test_nearest_with_transform() {
390        let src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
391        let resampler = NearestResampler::new();
392
393        // No offset, 1:1 scale
394        let result = resampler.resample_with_transform(&src, 10, 10, 1.0, 1.0, 0.0, 0.0);
395        assert!(result.is_ok());
396
397        // Invalid scale
398        let result = resampler.resample_with_transform(&src, 10, 10, 0.0, 1.0, 0.0, 0.0);
399        assert!(result.is_err());
400
401        let result = resampler.resample_with_transform(&src, 10, 10, 1.0, -1.0, 0.0, 0.0);
402        assert!(result.is_err());
403    }
404
405    #[test]
406    fn test_nearest_repeat() {
407        let mut src = RasterBuffer::zeros(3, 3, RasterDataType::Float32);
408        for y in 0..3 {
409            for x in 0..3 {
410                src.set_pixel(x, y, (y * 3 + x) as f64).ok();
411            }
412        }
413
414        let resampler = NearestResampler::new();
415        let result = resampler.resample_repeat(&src, 6, 6);
416        assert!(result.is_ok());
417    }
418}