1use crate::error::{AlgorithmError, Result};
41use oxigeo_core::buffer::RasterBuffer;
42use oxigeo_core::types::RasterDataType;
43
44#[cfg(feature = "parallel")]
45use rayon::prelude::*;
46
47#[derive(Debug, Clone, PartialEq)]
49pub enum WindowShape {
50 Rectangular { width: usize, height: usize },
52
53 Circular { radius: f64 },
55
56 Custom {
58 mask: Vec<bool>,
59 width: usize,
60 height: usize,
61 },
62}
63
64impl WindowShape {
65 pub fn rectangular(width: usize, height: usize) -> Result<Self> {
76 use oxigeo_core::OxiGeoError;
77
78 if width == 0 || height == 0 {
79 return Err(OxiGeoError::invalid_parameter_builder(
80 "window_size",
81 format!(
82 "window dimensions must be greater than zero, got {}x{}",
83 width, height
84 ),
85 )
86 .with_parameter("width", width.to_string())
87 .with_parameter("height", height.to_string())
88 .with_parameter("min", "1")
89 .with_operation("focal_operation")
90 .with_suggestion("Use positive window dimensions. Common values: 3, 5, 7, 9, 11")
91 .build()
92 .into());
93 }
94 if width.is_multiple_of(2) || height.is_multiple_of(2) {
95 let suggested_width = if width.is_multiple_of(2) {
96 width + 1
97 } else {
98 width
99 };
100 let suggested_height = if height.is_multiple_of(2) {
101 height + 1
102 } else {
103 height
104 };
105 return Err(OxiGeoError::invalid_parameter_builder(
106 "window_size",
107 format!("window dimensions must be odd, got {}x{}", width, height),
108 )
109 .with_parameter("width", width.to_string())
110 .with_parameter("height", height.to_string())
111 .with_operation("focal_operation")
112 .with_suggestion(format!(
113 "Use odd window dimensions for symmetric neighborhood. Try {}x{} instead",
114 suggested_width, suggested_height
115 ))
116 .build()
117 .into());
118 }
119 Ok(Self::Rectangular { width, height })
120 }
121
122 pub fn circular(radius: f64) -> Result<Self> {
132 use oxigeo_core::OxiGeoError;
133
134 if radius <= 0.0 {
135 return Err(OxiGeoError::invalid_parameter_builder(
136 "radius",
137 format!("window radius must be positive, got {}", radius),
138 )
139 .with_parameter("value", radius.to_string())
140 .with_parameter("min", "0.0")
141 .with_operation("focal_operation")
142 .with_suggestion(
143 "Use positive radius value. Common values: 1.0, 1.5, 2.0, 3.0 (in pixels)",
144 )
145 .build()
146 .into());
147 }
148 Ok(Self::Circular { radius })
149 }
150
151 pub fn custom(mask: Vec<bool>, width: usize, height: usize) -> Result<Self> {
163 use oxigeo_core::OxiGeoError;
164
165 if mask.len() != width * height {
166 let expected = width * height;
167 return Err(OxiGeoError::invalid_parameter_builder(
168 "mask",
169 format!(
170 "mask size must match width * height, got {} but expected {}",
171 mask.len(),
172 expected
173 ),
174 )
175 .with_parameter("mask_length", mask.len().to_string())
176 .with_parameter("width", width.to_string())
177 .with_parameter("height", height.to_string())
178 .with_parameter("expected_length", expected.to_string())
179 .with_operation("focal_operation")
180 .with_suggestion(format!(
181 "Provide a mask with exactly {} elements ({}x{})",
182 expected, width, height
183 ))
184 .build()
185 .into());
186 }
187 if width.is_multiple_of(2) || height.is_multiple_of(2) {
188 let suggested_width = if width.is_multiple_of(2) {
189 width + 1
190 } else {
191 width
192 };
193 let suggested_height = if height.is_multiple_of(2) {
194 height + 1
195 } else {
196 height
197 };
198 return Err(OxiGeoError::invalid_parameter_builder(
199 "window_size",
200 format!("window dimensions must be odd, got {}x{}", width, height),
201 )
202 .with_parameter("width", width.to_string())
203 .with_parameter("height", height.to_string())
204 .with_operation("focal_operation")
205 .with_suggestion(format!(
206 "Use odd window dimensions for symmetric neighborhood. Try {}x{} instead",
207 suggested_width, suggested_height
208 ))
209 .build()
210 .into());
211 }
212 Ok(Self::Custom {
213 mask,
214 width,
215 height,
216 })
217 }
218
219 #[must_use]
221 pub fn dimensions(&self) -> (usize, usize) {
222 match self {
223 Self::Rectangular { width, height } => (*width, *height),
224 Self::Circular { radius } => {
225 let size = (radius.ceil() * 2.0 + 1.0) as usize;
226 (size, size)
227 }
228 Self::Custom { width, height, .. } => (*width, *height),
229 }
230 }
231
232 #[must_use]
234 pub fn includes(&self, dx: i64, dy: i64) -> bool {
235 match self {
236 Self::Rectangular { width, height } => {
237 let hw = (*width / 2) as i64;
238 let hh = (*height / 2) as i64;
239 dx.abs() <= hw && dy.abs() <= hh
240 }
241 Self::Circular { radius } => {
242 let dist = ((dx * dx + dy * dy) as f64).sqrt();
243 dist <= *radius
244 }
245 Self::Custom {
246 mask,
247 width,
248 height,
249 } => {
250 let hw = (*width / 2) as i64;
251 let hh = (*height / 2) as i64;
252 if dx.abs() > hw || dy.abs() > hh {
253 return false;
254 }
255 let x = (dx + hw) as usize;
256 let y = (dy + hh) as usize;
257 mask[y * width + x]
258 }
259 }
260 }
261
262 #[must_use]
264 pub fn offsets(&self) -> Vec<(i64, i64)> {
265 let (width, height) = self.dimensions();
266 let hw = (width / 2) as i64;
267 let hh = (height / 2) as i64;
268
269 let mut result = Vec::new();
270 for dy in -hh..=hh {
271 for dx in -hw..=hw {
272 if self.includes(dx, dy) {
273 result.push((dx, dy));
274 }
275 }
276 }
277 result
278 }
279
280 #[must_use]
282 pub fn cell_count(&self) -> usize {
283 self.offsets().len()
284 }
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum BoundaryMode {
290 Ignore,
292
293 Constant(i64),
295
296 Reflect,
298
299 Wrap,
301
302 Edge,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub enum FocalOperation {
309 Mean,
311
312 Median,
314
315 Range,
317
318 Variety,
320
321 Min,
323
324 Max,
326
327 Sum,
329
330 StdDev,
332
333 Majority,
335}
336
337pub fn focal_mean(
349 src: &RasterBuffer,
350 window: &WindowShape,
351 boundary: &BoundaryMode,
352) -> Result<RasterBuffer> {
353 focal_operation(src, window, boundary, FocalOperation::Mean)
354}
355
356pub fn focal_median(
368 src: &RasterBuffer,
369 window: &WindowShape,
370 boundary: &BoundaryMode,
371) -> Result<RasterBuffer> {
372 focal_operation(src, window, boundary, FocalOperation::Median)
373}
374
375pub fn focal_range(
387 src: &RasterBuffer,
388 window: &WindowShape,
389 boundary: &BoundaryMode,
390) -> Result<RasterBuffer> {
391 focal_operation(src, window, boundary, FocalOperation::Range)
392}
393
394pub fn focal_variety(
406 src: &RasterBuffer,
407 window: &WindowShape,
408 boundary: &BoundaryMode,
409) -> Result<RasterBuffer> {
410 focal_operation(src, window, boundary, FocalOperation::Variety)
411}
412
413pub fn focal_min(
425 src: &RasterBuffer,
426 window: &WindowShape,
427 boundary: &BoundaryMode,
428) -> Result<RasterBuffer> {
429 focal_operation(src, window, boundary, FocalOperation::Min)
430}
431
432pub fn focal_max(
444 src: &RasterBuffer,
445 window: &WindowShape,
446 boundary: &BoundaryMode,
447) -> Result<RasterBuffer> {
448 focal_operation(src, window, boundary, FocalOperation::Max)
449}
450
451pub fn focal_sum(
463 src: &RasterBuffer,
464 window: &WindowShape,
465 boundary: &BoundaryMode,
466) -> Result<RasterBuffer> {
467 focal_operation(src, window, boundary, FocalOperation::Sum)
468}
469
470pub fn focal_stddev(
482 src: &RasterBuffer,
483 window: &WindowShape,
484 boundary: &BoundaryMode,
485) -> Result<RasterBuffer> {
486 focal_operation(src, window, boundary, FocalOperation::StdDev)
487}
488
489pub fn focal_majority(
501 src: &RasterBuffer,
502 window: &WindowShape,
503 boundary: &BoundaryMode,
504) -> Result<RasterBuffer> {
505 focal_operation(src, window, boundary, FocalOperation::Majority)
506}
507
508fn focal_operation(
510 src: &RasterBuffer,
511 window: &WindowShape,
512 boundary: &BoundaryMode,
513 operation: FocalOperation,
514) -> Result<RasterBuffer> {
515 let width = src.width();
516 let height = src.height();
517 let mut dst = RasterBuffer::nodata_filled(width, height, src.data_type(), src.nodata());
521
522 let offsets = window.offsets();
523
524 #[cfg(feature = "parallel")]
525 {
526 let results: Result<Vec<_>> = (0..height)
527 .into_par_iter()
528 .map(|y| {
529 let mut row_data = Vec::with_capacity(width as usize);
530 for x in 0..width {
531 let value = compute_focal_value(src, x, y, &offsets, boundary, operation)?;
532 row_data.push(value);
533 }
534 Ok((y, row_data))
535 })
536 .collect();
537
538 for (y, row_data) in results? {
539 for (x, value) in row_data.into_iter().enumerate() {
540 dst.set_pixel(x as u64, y, value)
541 .map_err(AlgorithmError::Core)?;
542 }
543 }
544 }
545
546 #[cfg(not(feature = "parallel"))]
547 {
548 for y in 0..height {
549 for x in 0..width {
550 let value = compute_focal_value(src, x, y, &offsets, boundary, operation)?;
551 dst.set_pixel(x, y, value).map_err(AlgorithmError::Core)?;
552 }
553 }
554 }
555
556 Ok(dst)
557}
558
559fn compute_focal_value(
561 src: &RasterBuffer,
562 x: u64,
563 y: u64,
564 offsets: &[(i64, i64)],
565 boundary: &BoundaryMode,
566 operation: FocalOperation,
567) -> Result<f64> {
568 let width = src.width() as i64;
569 let height = src.height() as i64;
570 let x_i64 = x as i64;
571 let y_i64 = y as i64;
572
573 let mut values = Vec::with_capacity(offsets.len());
574
575 for &(dx, dy) in offsets {
576 let nx = x_i64 + dx;
577 let ny = y_i64 + dy;
578
579 let value = if nx >= 0 && nx < width && ny >= 0 && ny < height {
580 src.get_pixel(nx as u64, ny as u64)
581 .map_err(AlgorithmError::Core)?
582 } else {
583 match boundary {
584 BoundaryMode::Ignore => continue,
585 BoundaryMode::Constant(c) => *c as f64,
586 BoundaryMode::Reflect => {
587 let rx = if nx < 0 {
588 -nx - 1
589 } else if nx >= width {
590 2 * width - nx - 1
591 } else {
592 nx
593 };
594 let ry = if ny < 0 {
595 -ny - 1
596 } else if ny >= height {
597 2 * height - ny - 1
598 } else {
599 ny
600 };
601 src.get_pixel(rx as u64, ry as u64)
602 .map_err(AlgorithmError::Core)?
603 }
604 BoundaryMode::Wrap => {
605 let wx = ((nx % width) + width) % width;
606 let wy = ((ny % height) + height) % height;
607 src.get_pixel(wx as u64, wy as u64)
608 .map_err(AlgorithmError::Core)?
609 }
610 BoundaryMode::Edge => {
611 let ex = nx.clamp(0, width - 1);
612 let ey = ny.clamp(0, height - 1);
613 src.get_pixel(ex as u64, ey as u64)
614 .map_err(AlgorithmError::Core)?
615 }
616 }
617 };
618
619 if src.is_nodata(value) || value.is_nan() {
622 continue;
623 }
624
625 values.push(value);
626 }
627
628 if values.is_empty() {
629 return Ok(src.nodata().as_f64().unwrap_or(0.0));
634 }
635
636 let result = match operation {
637 FocalOperation::Mean => {
638 let sum: f64 = values.iter().sum();
639 sum / values.len() as f64
640 }
641 FocalOperation::Median => {
642 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
643 let mid = values.len() / 2;
644 if values.len() % 2 == 0 {
645 (values[mid - 1] + values[mid]) / 2.0
646 } else {
647 values[mid]
648 }
649 }
650 FocalOperation::Range => {
651 let (min, max) = values.iter().copied().fold(
654 (f64::INFINITY, f64::NEG_INFINITY),
655 |(min_val, max_val), val| {
656 let new_min = if val < min_val { val } else { min_val };
657 let new_max = if val > max_val { val } else { max_val };
658 (new_min, new_max)
659 },
660 );
661 max - min
662 }
663 FocalOperation::Variety => {
664 let mut unique = values.clone();
665 unique.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
666 unique.dedup_by(|a, b| (*a - *b).abs() < 1e-10);
667 unique.len() as f64
668 }
669 FocalOperation::Min => {
670 values
673 .iter()
674 .copied()
675 .fold(f64::INFINITY, |acc, val| if val < acc { val } else { acc })
676 }
677 FocalOperation::Max => {
678 values.iter().copied().fold(
681 f64::NEG_INFINITY,
682 |acc, val| if val > acc { val } else { acc },
683 )
684 }
685 FocalOperation::Sum => values.iter().sum(),
686 FocalOperation::StdDev => {
687 let mean = values.iter().sum::<f64>() / values.len() as f64;
688 let variance = values
689 .iter()
690 .map(|v| {
691 let diff = v - mean;
692 diff * diff
693 })
694 .sum::<f64>()
695 / values.len() as f64;
696 variance.sqrt()
697 }
698 FocalOperation::Majority => {
699 let mut counts: Vec<(f64, usize)> = Vec::new();
701 for &val in &values {
702 if let Some(entry) = counts.iter_mut().find(|(v, _)| (v - val).abs() < 1e-10) {
703 entry.1 += 1;
704 } else {
705 counts.push((val, 1));
706 }
707 }
708 let (majority_val, _) =
711 counts
712 .into_iter()
713 .fold((0.0, 0), |(acc_val, acc_count), (val, count)| {
714 if count > acc_count {
715 (val, count)
716 } else {
717 (acc_val, acc_count)
718 }
719 });
720 majority_val
721 }
722 };
723
724 Ok(result)
725}
726
727pub fn focal_mean_separable(
741 src: &RasterBuffer,
742 width: usize,
743 height: usize,
744) -> Result<RasterBuffer> {
745 if width.is_multiple_of(2) || height.is_multiple_of(2) {
746 return Err(AlgorithmError::InvalidParameter {
747 parameter: "window_size",
748 message: "Window dimensions must be odd".to_string(),
749 });
750 }
751
752 let img_width = src.width();
753 let img_height = src.height();
754
755 let mut temp = RasterBuffer::zeros(img_width, img_height, RasterDataType::Float64);
757 let hw = (width / 2) as i64;
758
759 for y in 0..img_height {
760 for x in 0..img_width {
761 let mut sum = 0.0;
762 let mut count = 0;
763
764 for dx in -hw..=hw {
765 let nx = (x as i64 + dx).clamp(0, img_width as i64 - 1);
766 sum += src.get_pixel(nx as u64, y).map_err(AlgorithmError::Core)?;
767 count += 1;
768 }
769
770 temp.set_pixel(x, y, sum / count as f64)
771 .map_err(AlgorithmError::Core)?;
772 }
773 }
774
775 let mut dst = RasterBuffer::zeros(img_width, img_height, src.data_type());
777 let hh = (height / 2) as i64;
778
779 for y in 0..img_height {
780 for x in 0..img_width {
781 let mut sum = 0.0;
782 let mut count = 0;
783
784 for dy in -hh..=hh {
785 let ny = (y as i64 + dy).clamp(0, img_height as i64 - 1);
786 sum += temp.get_pixel(x, ny as u64).map_err(AlgorithmError::Core)?;
787 count += 1;
788 }
789
790 dst.set_pixel(x, y, sum / count as f64)
791 .map_err(AlgorithmError::Core)?;
792 }
793 }
794
795 Ok(dst)
796}
797
798pub fn focal_convolve(
812 src: &RasterBuffer,
813 kernel: &[f64],
814 width: usize,
815 height: usize,
816 normalize: bool,
817) -> Result<RasterBuffer> {
818 if kernel.len() != width * height {
819 return Err(AlgorithmError::InvalidParameter {
820 parameter: "kernel",
821 message: "Kernel size must match width * height".to_string(),
822 });
823 }
824
825 let img_width = src.width();
826 let img_height = src.height();
827 let mut dst = RasterBuffer::zeros(img_width, img_height, src.data_type());
828
829 let hw = (width / 2) as i64;
830 let hh = (height / 2) as i64;
831
832 let kernel_sum: f64 = if normalize {
833 let sum: f64 = kernel.iter().sum();
834 if sum.abs() < 1e-10 { 1.0 } else { sum }
835 } else {
836 1.0
837 };
838
839 for y in 0..img_height {
840 for x in 0..img_width {
841 let mut sum = 0.0;
842
843 for ky in 0..height {
844 for kx in 0..width {
845 let dx = kx as i64 - hw;
846 let dy = ky as i64 - hh;
847 let nx = (x as i64 + dx).clamp(0, img_width as i64 - 1);
848 let ny = (y as i64 + dy).clamp(0, img_height as i64 - 1);
849
850 let pixel_val = src
851 .get_pixel(nx as u64, ny as u64)
852 .map_err(AlgorithmError::Core)?;
853 sum += pixel_val * kernel[ky * width + kx];
854 }
855 }
856
857 dst.set_pixel(x, y, sum / kernel_sum)
858 .map_err(AlgorithmError::Core)?;
859 }
860 }
861
862 Ok(dst)
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use approx::assert_abs_diff_eq;
869
870 #[test]
871 fn test_window_shape_rectangular() {
872 let window = WindowShape::rectangular(3, 3)
873 .expect("3x3 rectangular window creation should succeed in test");
874 assert_eq!(window.dimensions(), (3, 3));
875 assert_eq!(window.cell_count(), 9);
876 assert!(window.includes(0, 0));
877 assert!(window.includes(1, 1));
878 assert!(!window.includes(2, 2));
879 }
880
881 #[test]
882 fn test_window_shape_circular() {
883 let window = WindowShape::circular(1.5)
884 .expect("circular window with radius 1.5 should succeed in test");
885 assert!(window.includes(0, 0));
886 assert!(window.includes(1, 0));
887 assert!(window.includes(0, 1));
888 assert!(!window.includes(2, 2));
889 }
890
891 #[test]
892 fn test_window_shape_custom() {
893 let mask = vec![false, true, false, true, true, true, false, true, false];
894 let window = WindowShape::custom(mask, 3, 3)
895 .expect("custom 3x3 window creation should succeed in test");
896 assert_eq!(window.cell_count(), 5);
897 assert!(window.includes(0, 0));
898 assert!(!window.includes(1, 1));
899 }
900
901 #[test]
902 fn test_focal_mean() {
903 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
904 src.set_pixel(2, 2, 5.0)
906 .expect("setting pixel (2,2) should succeed in test");
907
908 let window = WindowShape::rectangular(3, 3)
909 .expect("3x3 rectangular window creation should succeed in test");
910 let result = focal_mean(&src, &window, &BoundaryMode::Edge)
911 .expect("focal_mean operation should succeed in test");
912
913 let center = result
915 .get_pixel(2, 2)
916 .expect("getting pixel (2,2) from result should succeed in test");
917 assert_abs_diff_eq!(center, 5.0 / 9.0, epsilon = 0.01);
918 }
919
920 #[test]
921 fn test_focal_median() {
922 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
923 src.set_pixel(2, 2, 9.0)
924 .expect("setting pixel (2,2) should succeed in test");
925
926 let window = WindowShape::rectangular(3, 3)
927 .expect("3x3 rectangular window creation should succeed in test");
928 let result = focal_median(&src, &window, &BoundaryMode::Edge)
929 .expect("focal_median operation should succeed in test");
930
931 let center = result
933 .get_pixel(2, 2)
934 .expect("getting pixel (2,2) from result should succeed in test");
935 assert_abs_diff_eq!(center, 0.0, epsilon = 0.01);
936 }
937
938 #[test]
939 fn test_focal_range() {
940 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
941 src.set_pixel(2, 2, 10.0)
942 .expect("setting pixel (2,2) should succeed in test");
943
944 let window = WindowShape::rectangular(3, 3)
945 .expect("3x3 rectangular window creation should succeed in test");
946 let result = focal_range(&src, &window, &BoundaryMode::Edge)
947 .expect("focal_range operation should succeed in test");
948
949 let center = result
951 .get_pixel(2, 2)
952 .expect("getting pixel (2,2) from result should succeed in test");
953 assert_abs_diff_eq!(center, 10.0, epsilon = 0.01);
954 }
955
956 #[test]
957 fn test_focal_variety() {
958 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
959 src.set_pixel(2, 2, 1.0)
960 .expect("setting pixel (2,2) should succeed in test");
961 src.set_pixel(2, 3, 2.0)
962 .expect("setting pixel (2,3) should succeed in test");
963
964 let window = WindowShape::rectangular(3, 3)
965 .expect("3x3 rectangular window creation should succeed in test");
966 let result = focal_variety(&src, &window, &BoundaryMode::Edge)
967 .expect("focal_variety operation should succeed in test");
968
969 let center = result
971 .get_pixel(2, 2)
972 .expect("getting pixel (2,2) from result should succeed in test");
973 assert_abs_diff_eq!(center, 3.0, epsilon = 0.01);
974 }
975
976 #[test]
977 fn test_focal_stddev() {
978 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
979 for y in 0..5 {
980 for x in 0..5 {
981 src.set_pixel(x, y, (x + y) as f64)
982 .expect("setting pixel should succeed in test");
983 }
984 }
985
986 let window = WindowShape::rectangular(3, 3)
987 .expect("3x3 rectangular window creation should succeed in test");
988 let result = focal_stddev(&src, &window, &BoundaryMode::Edge)
989 .expect("focal_stddev operation should succeed in test");
990
991 let center = result
993 .get_pixel(2, 2)
994 .expect("getting pixel (2,2) from result should succeed in test");
995 assert!(center > 0.0);
996 }
997
998 use proptest::prelude::*;
999
1000 proptest! {
1001 #[test]
1005 fn prop_focal_min_mean_max_ordering(
1006 values in prop::collection::vec(-1000.0f64..1000.0, 25),
1007 ) {
1008 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float64);
1009 for (i, &v) in values.iter().enumerate() {
1010 let (x, y) = (i as u64 % 5, i as u64 / 5);
1011 src.set_pixel(x, y, v).expect("set pixel");
1012 }
1013 let window = WindowShape::rectangular(3, 3).expect("window");
1014 let fmin = focal_min(&src, &window, &BoundaryMode::Edge).expect("min");
1015 let fmax = focal_max(&src, &window, &BoundaryMode::Edge).expect("max");
1016 let fmean = focal_mean(&src, &window, &BoundaryMode::Edge).expect("mean");
1017 let frange = focal_range(&src, &window, &BoundaryMode::Edge).expect("range");
1018
1019 for y in 0..5 {
1020 for x in 0..5 {
1021 let mn = fmin.get_pixel(x, y).expect("mn");
1022 let mx = fmax.get_pixel(x, y).expect("mx");
1023 let me = fmean.get_pixel(x, y).expect("me");
1024 let rg = frange.get_pixel(x, y).expect("rg");
1025 prop_assert!(mn <= me + 1e-9);
1026 prop_assert!(me <= mx + 1e-9);
1027 prop_assert!((rg - (mx - mn)).abs() < 1e-6);
1028 }
1029 }
1030 }
1031 }
1032
1033 #[test]
1034 fn test_focal_mean_excludes_nodata() {
1035 use oxigeo_core::types::NoDataValue;
1036 let mut src =
1038 RasterBuffer::nodata_filled(5, 5, RasterDataType::Float32, NoDataValue::Float(-9999.0));
1039 for y in 0..5 {
1042 for x in 0..5 {
1043 src.set_pixel(x, y, 4.0)
1044 .expect("setting pixel should succeed in test");
1045 }
1046 }
1047 src.set_pixel(1, 2, -9999.0)
1048 .expect("setting NoData pixel should succeed in test");
1049
1050 let window = WindowShape::rectangular(3, 3)
1051 .expect("3x3 rectangular window creation should succeed in test");
1052 let result = focal_mean(&src, &window, &BoundaryMode::Edge)
1053 .expect("focal_mean operation should succeed in test");
1054
1055 let center = result
1059 .get_pixel(2, 2)
1060 .expect("getting pixel (2,2) from result should succeed in test");
1061 assert_abs_diff_eq!(center, 4.0, epsilon = 1e-6);
1062 }
1063
1064 #[test]
1065 fn test_focal_all_nodata_propagates_nodata() {
1066 use oxigeo_core::types::NoDataValue;
1067 let src =
1069 RasterBuffer::nodata_filled(5, 5, RasterDataType::Float32, NoDataValue::Float(-9999.0));
1070 let window = WindowShape::rectangular(3, 3)
1071 .expect("3x3 rectangular window creation should succeed in test");
1072 let result = focal_mean(&src, &window, &BoundaryMode::Edge)
1073 .expect("focal_mean operation should succeed in test");
1074
1075 let center = result
1077 .get_pixel(2, 2)
1078 .expect("getting pixel (2,2) from result should succeed in test");
1079 assert!(
1080 result.is_nodata(center),
1081 "all-NoData window must propagate NoData, got {center}"
1082 );
1083 }
1084
1085 #[test]
1086 fn test_focal_sum_excludes_nan_nodata() {
1087 use oxigeo_core::types::NoDataValue;
1088 let mut src = RasterBuffer::nodata_filled(
1089 5,
1090 5,
1091 RasterDataType::Float64,
1092 NoDataValue::Float(f64::NAN),
1093 );
1094 for y in 0..5 {
1095 for x in 0..5 {
1096 src.set_pixel(x, y, 2.0)
1097 .expect("setting pixel should succeed in test");
1098 }
1099 }
1100 src.set_pixel(2, 1, f64::NAN)
1101 .expect("setting NaN NoData pixel should succeed in test");
1102
1103 let window = WindowShape::rectangular(3, 3)
1104 .expect("3x3 rectangular window creation should succeed in test");
1105 let result = focal_sum(&src, &window, &BoundaryMode::Edge)
1106 .expect("focal_sum operation should succeed in test");
1107
1108 let center = result
1110 .get_pixel(2, 2)
1111 .expect("getting pixel (2,2) from result should succeed in test");
1112 assert!(center.is_finite(), "sum must not be NaN-poisoned");
1113 assert_abs_diff_eq!(center, 16.0, epsilon = 1e-6);
1114 }
1115
1116 #[test]
1117 fn test_focal_mean_separable() {
1118 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1119 for y in 0..10 {
1120 for x in 0..10 {
1121 src.set_pixel(x, y, (x + y) as f64)
1122 .expect("setting pixel should succeed in test");
1123 }
1124 }
1125
1126 let result = focal_mean_separable(&src, 3, 3)
1127 .expect("focal_mean_separable operation should succeed in test");
1128
1129 let window = WindowShape::rectangular(3, 3)
1131 .expect("3x3 rectangular window creation should succeed in test");
1132 let expected = focal_mean(&src, &window, &BoundaryMode::Edge)
1133 .expect("focal_mean operation should succeed in test");
1134
1135 for y in 1..9 {
1136 for x in 1..9 {
1137 let val = result
1138 .get_pixel(x, y)
1139 .expect("getting pixel from result should succeed in test");
1140 let exp = expected
1141 .get_pixel(x, y)
1142 .expect("getting pixel from expected should succeed in test");
1143 assert_abs_diff_eq!(val, exp, epsilon = 0.1);
1144 }
1145 }
1146 }
1147
1148 #[test]
1149 fn test_boundary_modes() {
1150 let src = RasterBuffer::zeros(3, 3, RasterDataType::Float32);
1151 let window = WindowShape::rectangular(3, 3)
1152 .expect("3x3 rectangular window creation should succeed in test");
1153
1154 let _ = focal_mean(&src, &window, &BoundaryMode::Ignore);
1156 let _ = focal_mean(&src, &window, &BoundaryMode::Constant(0));
1157 let _ = focal_mean(&src, &window, &BoundaryMode::Reflect);
1158 let _ = focal_mean(&src, &window, &BoundaryMode::Wrap);
1159 let _ = focal_mean(&src, &window, &BoundaryMode::Edge);
1160 }
1161}