use crate::core::image::RgbImage;
pub fn apply_bilateral_filter(
image: &mut RgbImage,
spatial_sigma: f32,
range_sigma: f32,
radius: u32,
) {
let width = image.width() as usize;
let height = image.height() as usize;
if width == 0 || height == 0 {
return;
}
let r = radius as usize;
let ksize = 2 * r + 1;
let two_ss_sq = 2.0_f32 * spatial_sigma * spatial_sigma;
let two_rs_sq = 2.0_f32 * range_sigma * range_sigma;
let mut spatial_lut = vec![0.0_f32; ksize * ksize];
for dy in 0..ksize {
for dx in 0..ksize {
let fx = dx as f32 - r as f32;
let fy = dy as f32 - r as f32;
let d2 = fx * fx + fy * fy;
spatial_lut[dy * ksize + dx] = (-d2 / two_ss_sq).exp();
}
}
let input = image.data.clone();
for y in 0..height {
for x in 0..width {
let center_idx = (y * width + x) * 3;
for c in 0..3usize {
let center_val = input[center_idx + c] as f32;
let mut numerator = 0.0_f32;
let mut denominator = 0.0_f32;
let y_min = y.saturating_sub(r);
let y_max = (y + r).min(height - 1);
let x_min = x.saturating_sub(r);
let x_max = (x + r).min(width - 1);
for ny in y_min..=y_max {
for nx in x_min..=x_max {
let lut_dy = (ny as isize - y as isize + r as isize) as usize;
let lut_dx = (nx as isize - x as isize + r as isize) as usize;
let spatial_w = spatial_lut[lut_dy * ksize + lut_dx];
let neighbor_val = input[(ny * width + nx) * 3 + c] as f32;
let diff = center_val - neighbor_val;
let range_w = (-diff * diff / two_rs_sq).exp();
let w = spatial_w * range_w;
numerator += neighbor_val * w;
denominator += w;
}
}
let result = if denominator > 0.0 {
(numerator / denominator).round() as u16
} else {
input[center_idx + c]
};
image.data[center_idx + c] = result;
}
}
}
}
pub fn apply_gaussian_blur(image: &mut RgbImage, sigma: f32, radius: u32) {
let width = image.width() as usize;
let height = image.height() as usize;
if width == 0 || height == 0 {
return;
}
let r = radius as usize;
let ksize = 2 * r + 1;
let two_sq = 2.0_f32 * sigma * sigma;
let mut kernel = vec![0.0_f32; ksize];
for (i, v) in kernel.iter_mut().enumerate() {
let x = i as f32 - r as f32;
*v = (-x * x / two_sq).exp();
}
let k_sum: f32 = kernel.iter().sum();
for v in kernel.iter_mut() {
*v /= k_sum;
}
let mut tmp = image.data.clone();
for y in 0..height {
for x in 0..width {
for c in 0..3usize {
let mut acc = 0.0_f32;
let mut wsum = 0.0_f32;
let x_min = x.saturating_sub(r);
let x_max = (x + r).min(width - 1);
for nx in x_min..=x_max {
let ki = (nx as isize - x as isize + r as isize) as usize;
let w = kernel[ki];
acc += image.data[(y * width + nx) * 3 + c] as f32 * w;
wsum += w;
}
tmp[(y * width + x) * 3 + c] = if wsum > 0.0 {
(acc / wsum).round() as u16
} else {
image.data[(y * width + x) * 3 + c]
};
}
}
}
for y in 0..height {
for x in 0..width {
for c in 0..3usize {
let mut acc = 0.0_f32;
let mut wsum = 0.0_f32;
let y_min = y.saturating_sub(r);
let y_max = (y + r).min(height - 1);
for ny in y_min..=y_max {
let ki = (ny as isize - y as isize + r as isize) as usize;
let w = kernel[ki];
acc += tmp[(ny * width + x) * 3 + c] as f32 * w;
wsum += w;
}
image.data[(y * width + x) * 3 + c] = if wsum > 0.0 {
(acc / wsum).round() as u16
} else {
tmp[(y * width + x) * 3 + c]
};
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_uniform(width: u32, height: u32, value: u16) -> RgbImage {
let n = (width as usize) * (height as usize) * 3;
RgbImage::new(width, height, vec![value; n])
}
fn channel_variance(image: &RgbImage, channel: usize) -> f64 {
let vals: Vec<f64> = image
.data
.chunks_exact(3)
.map(|px| px[channel] as f64)
.collect();
let mean = vals.iter().sum::<f64>() / vals.len() as f64;
vals.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64
}
#[test]
fn test_bilateral_uniform_image_unchanged() {
let mut img = make_uniform(8, 8, 1000);
apply_bilateral_filter(&mut img, 2.0, 2000.0, 2);
assert!(img.data.iter().all(|&v| v == 1000));
}
#[test]
fn test_bilateral_reduces_noise() {
let w = 16u32;
let h = 16u32;
let n = (w as usize) * (h as usize);
let mut data = Vec::with_capacity(n * 3);
for i in 0..n {
let v = if i % 2 == 0 { 1000u16 } else { 2000u16 };
data.extend_from_slice(&[v, v, v]);
}
let mut img = RgbImage::new(w, h, data.clone());
let var_before = channel_variance(&img, 0);
apply_bilateral_filter(&mut img, 3.0, 5000.0, 3);
let var_after = channel_variance(&img, 0);
assert!(
var_after < var_before,
"variance should decrease: before={var_before}, after={var_after}"
);
}
#[test]
fn test_bilateral_preserves_edges() {
let w = 16u32;
let h = 8u32;
let n = (w as usize) * (h as usize);
let mut data = Vec::with_capacity(n * 3);
for _y in 0..h {
for x in 0..w {
let v = if x < w / 2 { 0u16 } else { 60000u16 };
data.extend_from_slice(&[v, v, v]);
}
}
let mut img = RgbImage::new(w, h, data);
apply_bilateral_filter(&mut img, 2.0, 1000.0, 2);
let left_px = img.data[((4 * w as usize) + 2) * 3];
let right_px = img.data[((4 * w as usize) + 13) * 3];
assert!(left_px < 10000, "left edge should stay dark, got {left_px}");
assert!(
right_px > 50000,
"right edge should stay bright, got {right_px}"
);
}
#[test]
fn test_gaussian_blur_uniform_unchanged() {
let mut img = make_uniform(8, 8, 5000);
apply_gaussian_blur(&mut img, 1.5, 2);
assert!(img.data.iter().all(|&v| v == 5000));
}
#[test]
fn test_filter_small_image() {
let mut img1 = make_uniform(1, 1, 100);
apply_bilateral_filter(&mut img1, 2.0, 1000.0, 2);
let mut img2 = make_uniform(2, 2, 200);
apply_gaussian_blur(&mut img2, 1.0, 2);
}
#[test]
fn test_gaussian_blur_reduces_noise() {
let w = 16u32;
let h = 16u32;
let n = (w as usize) * (h as usize);
let mut data = Vec::with_capacity(n * 3);
for i in 0..n {
let v: u16 = if i % 2 == 0 { 1000 } else { 3000 };
data.extend_from_slice(&[v, v, v]);
}
let mut img = RgbImage::new(w, h, data);
let var_before = channel_variance(&img, 0);
apply_gaussian_blur(&mut img, 2.0, 3);
let var_after = channel_variance(&img, 0);
assert!(
var_after < var_before,
"Gaussian blur should reduce variance: before={var_before}, after={var_after}"
);
}
}