use crate::result::{Error, Result};
use crate::SweepProcessor;
use nexrad_model::data::{GateStatus, SweepField};
pub struct MedianFilter {
pub azimuth_kernel: usize,
pub range_kernel: usize,
}
impl SweepProcessor for MedianFilter {
fn name(&self) -> &str {
"MedianFilter"
}
fn process(&self, input: &SweepField) -> Result<SweepField> {
if self.azimuth_kernel % 2 == 0 || self.range_kernel % 2 == 0 {
return Err(Error::InvalidParameter(
"kernel sizes must be odd".to_string(),
));
}
if self.azimuth_kernel == 0 || self.range_kernel == 0 {
return Err(Error::InvalidParameter(
"kernel sizes must be >= 1".to_string(),
));
}
if self.azimuth_kernel == 1 && self.range_kernel == 1 {
return Ok(input.clone());
}
let az_count = input.azimuth_count();
let gate_count = input.gate_count();
let az_half = self.azimuth_kernel / 2;
let range_half = self.range_kernel / 2;
let mut output = input.clone();
let mut neighborhood = Vec::with_capacity(self.azimuth_kernel * self.range_kernel);
for az_idx in 0..az_count {
for gate_idx in 0..gate_count {
let (_, status) = input.get(az_idx, gate_idx);
if status != GateStatus::Valid {
continue;
}
neighborhood.clear();
for daz in 0..self.azimuth_kernel {
let az_offset = daz as isize - az_half as isize;
let neighbor_az =
((az_idx as isize + az_offset).rem_euclid(az_count as isize)) as usize;
for dr in 0..self.range_kernel {
let range_offset = dr as isize - range_half as isize;
let neighbor_gate = gate_idx as isize + range_offset;
if neighbor_gate < 0 || neighbor_gate >= gate_count as isize {
continue;
}
let (val, st) = input.get(neighbor_az, neighbor_gate as usize);
if st == GateStatus::Valid {
neighborhood.push(val);
}
}
}
if !neighborhood.is_empty() {
neighborhood
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = neighborhood[neighborhood.len() / 2];
output.set(az_idx, gate_idx, median, GateStatus::Valid);
}
}
}
Ok(output)
}
}
pub struct GaussianSmooth {
pub sigma_azimuth: f32,
pub sigma_range: f32,
}
impl SweepProcessor for GaussianSmooth {
fn name(&self) -> &str {
"GaussianSmooth"
}
fn process(&self, input: &SweepField) -> Result<SweepField> {
if self.sigma_azimuth <= 0.0 || self.sigma_range <= 0.0 {
return Err(Error::InvalidParameter(
"sigma values must be positive".to_string(),
));
}
let az_count = input.azimuth_count();
let gate_count = input.gate_count();
let az_radius = (self.sigma_azimuth * 3.0).ceil() as usize;
let range_radius = (self.sigma_range * 3.0).ceil() as usize;
let mut output = input.clone();
for az_idx in 0..az_count {
for gate_idx in 0..gate_count {
let (_, status) = input.get(az_idx, gate_idx);
if status != GateStatus::Valid {
continue;
}
let mut weighted_sum = 0.0f64;
let mut weight_sum = 0.0f64;
for daz in 0..=(2 * az_radius) {
let az_offset = daz as isize - az_radius as isize;
let neighbor_az =
((az_idx as isize + az_offset).rem_euclid(az_count as isize)) as usize;
for dr in 0..=(2 * range_radius) {
let range_offset = dr as isize - range_radius as isize;
let neighbor_gate = gate_idx as isize + range_offset;
if neighbor_gate < 0 || neighbor_gate >= gate_count as isize {
continue;
}
let (val, st) = input.get(neighbor_az, neighbor_gate as usize);
if st != GateStatus::Valid {
continue;
}
let az_dist = az_offset as f32;
let r_dist = range_offset as f32;
let weight = (-0.5
* ((az_dist / self.sigma_azimuth).powi(2)
+ (r_dist / self.sigma_range).powi(2)))
.exp() as f64;
weighted_sum += val as f64 * weight;
weight_sum += weight;
}
}
if weight_sum > 0.0 {
output.set(
az_idx,
gate_idx,
(weighted_sum / weight_sum) as f32,
GateStatus::Valid,
);
}
}
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_field() -> SweepField {
let mut field = SweepField::new_empty(
"Test",
"dBZ",
0.5,
vec![0.0, 1.0, 2.0, 3.0, 4.0],
1.0,
2.0,
0.25,
5,
);
for az in 0..5 {
for gate in 0..5 {
field.set(az, gate, 30.0, GateStatus::Valid);
}
}
field
}
#[test]
fn test_median_filter_uniform() {
let field = make_test_field();
let filter = MedianFilter {
azimuth_kernel: 3,
range_kernel: 3,
};
let result = filter.process(&field).unwrap();
for az in 0..5 {
for gate in 0..5 {
let (val, status) = result.get(az, gate);
assert_eq!(val, 30.0);
assert_eq!(status, GateStatus::Valid);
}
}
}
#[test]
fn test_median_filter_removes_spike() {
let mut field = make_test_field();
field.set(2, 2, 100.0, GateStatus::Valid);
let filter = MedianFilter {
azimuth_kernel: 3,
range_kernel: 3,
};
let result = filter.process(&field).unwrap();
let (val, _) = result.get(2, 2);
assert_eq!(val, 30.0);
}
#[test]
fn test_median_filter_even_kernel_error() {
let field = make_test_field();
let filter = MedianFilter {
azimuth_kernel: 2,
range_kernel: 3,
};
assert!(filter.process(&field).is_err());
}
#[test]
fn test_median_filter_1x1_noop() {
let mut field = make_test_field();
field.set(2, 2, 99.0, GateStatus::Valid);
let filter = MedianFilter {
azimuth_kernel: 1,
range_kernel: 1,
};
let result = filter.process(&field).unwrap();
let (val, _) = result.get(2, 2);
assert_eq!(val, 99.0);
}
#[test]
fn test_median_filter_preserves_nodata() {
let mut field = make_test_field();
field.set(2, 2, 0.0, GateStatus::NoData);
let filter = MedianFilter {
azimuth_kernel: 3,
range_kernel: 3,
};
let result = filter.process(&field).unwrap();
let (_, status) = result.get(2, 2);
assert_eq!(status, GateStatus::NoData);
}
#[test]
fn test_gaussian_smooth_uniform() {
let field = make_test_field();
let smoother = GaussianSmooth {
sigma_azimuth: 1.0,
sigma_range: 1.0,
};
let result = smoother.process(&field).unwrap();
for az in 0..5 {
for gate in 0..5 {
let (val, _) = result.get(az, gate);
assert!((val - 30.0).abs() < 0.01, "Expected ~30.0, got {}", val);
}
}
}
#[test]
fn test_gaussian_smooth_reduces_spike() {
let mut field = make_test_field();
field.set(2, 2, 100.0, GateStatus::Valid);
let smoother = GaussianSmooth {
sigma_azimuth: 1.0,
sigma_range: 1.0,
};
let result = smoother.process(&field).unwrap();
let (val, _) = result.get(2, 2);
assert!(val < 100.0, "Expected smoothed value < 100, got {}", val);
assert!(val > 30.0, "Expected smoothed value > 30, got {}", val);
}
#[test]
fn test_gaussian_smooth_invalid_sigma() {
let field = make_test_field();
let smoother = GaussianSmooth {
sigma_azimuth: 0.0,
sigma_range: 1.0,
};
assert!(smoother.process(&field).is_err());
}
}