use crate::signal::traits::wiener::WienerAlgorithms;
use numr::error::{Error, Result};
use numr::runtime::cpu::{CpuClient, CpuRuntime};
use numr::tensor::Tensor;
impl WienerAlgorithms<CpuRuntime> for CpuClient {
fn wiener(
&self,
x: &Tensor<CpuRuntime>,
kernel_size: Option<usize>,
noise: Option<f64>,
) -> Result<Tensor<CpuRuntime>> {
wiener_cpu(x, kernel_size, noise)
}
fn wiener2d(
&self,
x: &Tensor<CpuRuntime>,
kernel_size: Option<(usize, usize)>,
noise: Option<f64>,
) -> Result<Tensor<CpuRuntime>> {
wiener2d_cpu(x, kernel_size, noise)
}
}
fn wiener_cpu(
x: &Tensor<CpuRuntime>,
kernel_size: Option<usize>,
noise: Option<f64>,
) -> Result<Tensor<CpuRuntime>> {
if x.ndim() != 1 {
return Err(Error::InvalidArgument {
arg: "x",
reason: "Input must be 1D".to_string(),
});
}
let n = x.shape()[0];
let device = x.device();
if n == 0 {
return Err(Error::InvalidArgument {
arg: "x",
reason: "Input signal cannot be empty".to_string(),
});
}
let ksize = kernel_size.unwrap_or(3);
if ksize == 0 {
return Err(Error::InvalidArgument {
arg: "kernel_size",
reason: "Kernel size must be positive".to_string(),
});
}
if ksize.is_multiple_of(2) {
return Err(Error::InvalidArgument {
arg: "kernel_size",
reason: "Kernel size must be odd".to_string(),
});
}
let data: Vec<f64> = x.to_vec();
let half = ksize / 2;
let mut local_means = Vec::with_capacity(n);
let mut local_vars = Vec::with_capacity(n);
for i in 0..n {
let start = i.saturating_sub(half);
let end = (i + half + 1).min(n);
let window_size = end - start;
let sum: f64 = data[start..end].iter().sum();
let mean = sum / window_size as f64;
local_means.push(mean);
let var_sum: f64 = data[start..end].iter().map(|&v| (v - mean).powi(2)).sum();
let var = var_sum / window_size as f64;
local_vars.push(var);
}
let noise_var = noise.unwrap_or_else(|| {
local_vars
.iter()
.cloned()
.fold(f64::INFINITY, f64::min)
.max(1e-10)
});
let mut result = Vec::with_capacity(n);
for i in 0..n {
let local_mean = local_means[i];
let local_var = local_vars[i];
let filter_coeff = if local_var > noise_var {
(local_var - noise_var) / local_var
} else {
0.0
};
let filtered = local_mean + filter_coeff * (data[i] - local_mean);
result.push(filtered);
}
Ok(Tensor::from_slice(&result, &[n], device))
}
fn wiener2d_cpu(
x: &Tensor<CpuRuntime>,
kernel_size: Option<(usize, usize)>,
noise: Option<f64>,
) -> Result<Tensor<CpuRuntime>> {
if x.ndim() != 2 {
return Err(Error::InvalidArgument {
arg: "x",
reason: "Input must be 2D".to_string(),
});
}
let shape = x.shape();
let height = shape[0];
let width = shape[1];
let device = x.device();
if height == 0 || width == 0 {
return Err(Error::InvalidArgument {
arg: "x",
reason: "Input image cannot be empty".to_string(),
});
}
let (kh, kw) = kernel_size.unwrap_or((3, 3));
if kh == 0 || kw == 0 {
return Err(Error::InvalidArgument {
arg: "kernel_size",
reason: "Kernel sizes must be positive".to_string(),
});
}
if kh % 2 == 0 || kw % 2 == 0 {
return Err(Error::InvalidArgument {
arg: "kernel_size",
reason: "Kernel sizes must be odd".to_string(),
});
}
let data: Vec<f64> = x.to_vec();
let half_h = kh / 2;
let half_w = kw / 2;
let total_pixels = height * width;
let mut local_means = vec![0.0; total_pixels];
let mut local_vars = vec![0.0; total_pixels];
for i in 0..height {
for j in 0..width {
let row_start = i.saturating_sub(half_h);
let row_end = (i + half_h + 1).min(height);
let col_start = j.saturating_sub(half_w);
let col_end = (j + half_w + 1).min(width);
let window_size = (row_end - row_start) * (col_end - col_start);
let mut sum = 0.0;
for row in row_start..row_end {
for col in col_start..col_end {
sum += data[row * width + col];
}
}
let mean = sum / window_size as f64;
local_means[i * width + j] = mean;
let mut var_sum = 0.0;
for row in row_start..row_end {
for col in col_start..col_end {
let diff = data[row * width + col] - mean;
var_sum += diff * diff;
}
}
let var = var_sum / window_size as f64;
local_vars[i * width + j] = var;
}
}
let noise_var = noise.unwrap_or_else(|| {
local_vars
.iter()
.cloned()
.fold(f64::INFINITY, f64::min)
.max(1e-10)
});
let mut result = vec![0.0; total_pixels];
for i in 0..total_pixels {
let local_mean = local_means[i];
let local_var = local_vars[i];
let filter_coeff = if local_var > noise_var {
(local_var - noise_var) / local_var
} else {
0.0
};
result[i] = local_mean + filter_coeff * (data[i] - local_mean);
}
Ok(Tensor::from_slice(&result, &[height, width], device))
}
#[cfg(test)]
mod tests {
use super::*;
use numr::runtime::cpu::CpuDevice;
fn setup() -> (CpuClient, CpuDevice) {
let device = CpuDevice::new();
let client = CpuClient::new(device.clone());
(client, device)
}
#[test]
fn test_wiener_reduces_noise() {
let (client, device) = setup();
let signal = vec![4.5, 5.5, 4.8, 5.2, 5.0, 4.9, 5.1, 4.7, 5.3, 5.0];
let x = Tensor::from_slice(&signal, &[signal.len()], &device);
let result = client.wiener(&x, Some(3), Some(0.05)).unwrap();
let result_data: Vec<f64> = result.to_vec();
let orig_var: f64 = signal.iter().map(|&v| (v - 5.0_f64).powi(2)).sum::<f64>() / 10.0;
let filt_var: f64 = result_data
.iter()
.map(|&v| (v - 5.0_f64).powi(2))
.sum::<f64>()
/ 10.0;
assert!(
filt_var <= orig_var + 0.01,
"Wiener should reduce noise variance"
);
}
#[test]
fn test_wiener_preserves_edges() {
let (client, device) = setup();
let signal = vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
let x = Tensor::from_slice(&signal, &[signal.len()], &device);
let result = client.wiener(&x, Some(3), None).unwrap();
let result_data: Vec<f64> = result.to_vec();
assert!(result_data[0] < 0.3, "Should preserve low region");
assert!(result_data[1] < 0.3, "Should preserve low region");
assert!(result_data[6] > 0.7, "Should preserve high region");
assert!(result_data[7] > 0.7, "Should preserve high region");
}
#[test]
fn test_wiener_with_high_noise() {
let (client, device) = setup();
let signal = vec![1.0, 10.0, 2.0, 9.0, 3.0, 8.0, 4.0, 7.0];
let x = Tensor::from_slice(&signal, &[signal.len()], &device);
let result = client.wiener(&x, Some(3), Some(10.0)).unwrap();
let result_data: Vec<f64> = result.to_vec();
let mean: f64 = signal.iter().sum::<f64>() / signal.len() as f64;
for val in result_data.iter() {
assert!(
(*val - mean).abs() < 5.0,
"High noise should smooth to near-constant"
);
}
}
#[test]
fn test_wiener2d_simple() {
let (client, device) = setup();
let image = vec![
1.0, 1.0, 1.0, 1.0, 5.0, 1.0, 1.0, 1.0, 1.0,
];
let x = Tensor::from_slice(&image, &[3, 3], &device);
let result = client.wiener2d(&x, Some((3, 3)), Some(0.1)).unwrap();
let result_data: Vec<f64> = result.to_vec();
assert!(
result_data[4] < 5.0,
"Center outlier should be reduced by Wiener filter"
);
}
#[test]
fn test_wiener2d_preserves_structure() {
let (client, device) = setup();
let image = vec![
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
];
let x = Tensor::from_slice(&image, &[5, 5], &device);
let result = client.wiener2d(&x, Some((3, 3)), Some(0.01)).unwrap();
let result_data: Vec<f64> = result.to_vec();
assert!(
result_data[12] > 0.8,
"Center of pattern should be preserved"
);
assert!(result_data[0] < 0.2, "Corners should remain dark");
assert!(result_data[4] < 0.2, "Corners should remain dark");
}
#[test]
fn test_wiener_auto_noise_estimation() {
let (client, device) = setup();
let signal = vec![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0];
let x = Tensor::from_slice(&signal, &[signal.len()], &device);
let result = client.wiener(&x, Some(3), None).unwrap();
let result_data: Vec<f64> = result.to_vec();
assert_eq!(result_data.len(), signal.len());
for val in result_data.iter() {
assert!(val.is_finite(), "Output should be finite");
}
}
#[test]
fn test_wiener_odd_kernel_required() {
let (client, device) = setup();
let signal = vec![1.0, 2.0, 3.0];
let x = Tensor::from_slice(&signal, &[signal.len()], &device);
let result = client.wiener(&x, Some(4), None);
assert!(result.is_err());
}
}