1use crate::result::{Error, Result};
2use crate::SweepProcessor;
3use nexrad_model::data::{GateStatus, SweepField};
4
5pub struct MedianFilter {
14 pub azimuth_kernel: usize,
16 pub range_kernel: usize,
18}
19
20impl SweepProcessor for MedianFilter {
21 fn name(&self) -> &str {
22 "MedianFilter"
23 }
24
25 fn process(&self, input: &SweepField) -> Result<SweepField> {
26 if self.azimuth_kernel % 2 == 0 || self.range_kernel % 2 == 0 {
27 return Err(Error::InvalidParameter(
28 "kernel sizes must be odd".to_string(),
29 ));
30 }
31 if self.azimuth_kernel == 0 || self.range_kernel == 0 {
32 return Err(Error::InvalidParameter(
33 "kernel sizes must be >= 1".to_string(),
34 ));
35 }
36
37 if self.azimuth_kernel == 1 && self.range_kernel == 1 {
39 return Ok(input.clone());
40 }
41
42 let az_count = input.azimuth_count();
43 let gate_count = input.gate_count();
44 let az_half = self.azimuth_kernel / 2;
45 let range_half = self.range_kernel / 2;
46
47 let mut output = input.clone();
48 let mut neighborhood = Vec::with_capacity(self.azimuth_kernel * self.range_kernel);
49
50 for az_idx in 0..az_count {
51 for gate_idx in 0..gate_count {
52 let (_, status) = input.get(az_idx, gate_idx);
53 if status != GateStatus::Valid {
54 continue;
55 }
56
57 neighborhood.clear();
58
59 for daz in 0..self.azimuth_kernel {
60 let az_offset = daz as isize - az_half as isize;
61 let neighbor_az =
62 ((az_idx as isize + az_offset).rem_euclid(az_count as isize)) as usize;
63
64 for dr in 0..self.range_kernel {
65 let range_offset = dr as isize - range_half as isize;
66 let neighbor_gate = gate_idx as isize + range_offset;
67
68 if neighbor_gate < 0 || neighbor_gate >= gate_count as isize {
69 continue;
70 }
71
72 let (val, st) = input.get(neighbor_az, neighbor_gate as usize);
73 if st == GateStatus::Valid {
74 neighborhood.push(val);
75 }
76 }
77 }
78
79 if !neighborhood.is_empty() {
80 neighborhood
81 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
82 let median = neighborhood[neighborhood.len() / 2];
83 output.set(az_idx, gate_idx, median, GateStatus::Valid);
84 }
85 }
86 }
87
88 Ok(output)
89 }
90}
91
92pub struct GaussianSmooth {
100 pub sigma_azimuth: f32,
102 pub sigma_range: f32,
104}
105
106impl SweepProcessor for GaussianSmooth {
107 fn name(&self) -> &str {
108 "GaussianSmooth"
109 }
110
111 fn process(&self, input: &SweepField) -> Result<SweepField> {
112 if self.sigma_azimuth <= 0.0 || self.sigma_range <= 0.0 {
113 return Err(Error::InvalidParameter(
114 "sigma values must be positive".to_string(),
115 ));
116 }
117
118 let az_count = input.azimuth_count();
119 let gate_count = input.gate_count();
120
121 let az_radius = (self.sigma_azimuth * 3.0).ceil() as usize;
123 let range_radius = (self.sigma_range * 3.0).ceil() as usize;
124
125 let mut output = input.clone();
126
127 for az_idx in 0..az_count {
128 for gate_idx in 0..gate_count {
129 let (_, status) = input.get(az_idx, gate_idx);
130 if status != GateStatus::Valid {
131 continue;
132 }
133
134 let mut weighted_sum = 0.0f64;
135 let mut weight_sum = 0.0f64;
136
137 for daz in 0..=(2 * az_radius) {
138 let az_offset = daz as isize - az_radius as isize;
139 let neighbor_az =
140 ((az_idx as isize + az_offset).rem_euclid(az_count as isize)) as usize;
141
142 for dr in 0..=(2 * range_radius) {
143 let range_offset = dr as isize - range_radius as isize;
144 let neighbor_gate = gate_idx as isize + range_offset;
145
146 if neighbor_gate < 0 || neighbor_gate >= gate_count as isize {
147 continue;
148 }
149
150 let (val, st) = input.get(neighbor_az, neighbor_gate as usize);
151 if st != GateStatus::Valid {
152 continue;
153 }
154
155 let az_dist = az_offset as f32;
156 let r_dist = range_offset as f32;
157 let weight = (-0.5
158 * ((az_dist / self.sigma_azimuth).powi(2)
159 + (r_dist / self.sigma_range).powi(2)))
160 .exp() as f64;
161
162 weighted_sum += val as f64 * weight;
163 weight_sum += weight;
164 }
165 }
166
167 if weight_sum > 0.0 {
168 output.set(
169 az_idx,
170 gate_idx,
171 (weighted_sum / weight_sum) as f32,
172 GateStatus::Valid,
173 );
174 }
175 }
176 }
177
178 Ok(output)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn make_test_field() -> SweepField {
187 let mut field = SweepField::new_empty(
188 "Test",
189 "dBZ",
190 0.5,
191 vec![0.0, 1.0, 2.0, 3.0, 4.0],
192 1.0,
193 2.0,
194 0.25,
195 5,
196 );
197
198 for az in 0..5 {
200 for gate in 0..5 {
201 field.set(az, gate, 30.0, GateStatus::Valid);
202 }
203 }
204
205 field
206 }
207
208 #[test]
209 fn test_median_filter_uniform() {
210 let field = make_test_field();
211 let filter = MedianFilter {
212 azimuth_kernel: 3,
213 range_kernel: 3,
214 };
215
216 let result = filter.process(&field).unwrap();
217
218 for az in 0..5 {
220 for gate in 0..5 {
221 let (val, status) = result.get(az, gate);
222 assert_eq!(val, 30.0);
223 assert_eq!(status, GateStatus::Valid);
224 }
225 }
226 }
227
228 #[test]
229 fn test_median_filter_removes_spike() {
230 let mut field = make_test_field();
231 field.set(2, 2, 100.0, GateStatus::Valid);
233
234 let filter = MedianFilter {
235 azimuth_kernel: 3,
236 range_kernel: 3,
237 };
238
239 let result = filter.process(&field).unwrap();
240
241 let (val, _) = result.get(2, 2);
243 assert_eq!(val, 30.0);
244 }
245
246 #[test]
247 fn test_median_filter_even_kernel_error() {
248 let field = make_test_field();
249 let filter = MedianFilter {
250 azimuth_kernel: 2,
251 range_kernel: 3,
252 };
253
254 assert!(filter.process(&field).is_err());
255 }
256
257 #[test]
258 fn test_median_filter_1x1_noop() {
259 let mut field = make_test_field();
260 field.set(2, 2, 99.0, GateStatus::Valid);
261
262 let filter = MedianFilter {
263 azimuth_kernel: 1,
264 range_kernel: 1,
265 };
266
267 let result = filter.process(&field).unwrap();
268 let (val, _) = result.get(2, 2);
269 assert_eq!(val, 99.0);
270 }
271
272 #[test]
273 fn test_median_filter_preserves_nodata() {
274 let mut field = make_test_field();
275 field.set(2, 2, 0.0, GateStatus::NoData);
276
277 let filter = MedianFilter {
278 azimuth_kernel: 3,
279 range_kernel: 3,
280 };
281
282 let result = filter.process(&field).unwrap();
283 let (_, status) = result.get(2, 2);
284 assert_eq!(status, GateStatus::NoData);
285 }
286
287 #[test]
288 fn test_gaussian_smooth_uniform() {
289 let field = make_test_field();
290 let smoother = GaussianSmooth {
291 sigma_azimuth: 1.0,
292 sigma_range: 1.0,
293 };
294
295 let result = smoother.process(&field).unwrap();
296
297 for az in 0..5 {
299 for gate in 0..5 {
300 let (val, _) = result.get(az, gate);
301 assert!((val - 30.0).abs() < 0.01, "Expected ~30.0, got {}", val);
302 }
303 }
304 }
305
306 #[test]
307 fn test_gaussian_smooth_reduces_spike() {
308 let mut field = make_test_field();
309 field.set(2, 2, 100.0, GateStatus::Valid);
310
311 let smoother = GaussianSmooth {
312 sigma_azimuth: 1.0,
313 sigma_range: 1.0,
314 };
315
316 let result = smoother.process(&field).unwrap();
317
318 let (val, _) = result.get(2, 2);
320 assert!(val < 100.0, "Expected smoothed value < 100, got {}", val);
321 assert!(val > 30.0, "Expected smoothed value > 30, got {}", val);
322 }
323
324 #[test]
325 fn test_gaussian_smooth_invalid_sigma() {
326 let field = make_test_field();
327 let smoother = GaussianSmooth {
328 sigma_azimuth: 0.0,
329 sigma_range: 1.0,
330 };
331
332 assert!(smoother.process(&field).is_err());
333 }
334}