use crate::error::{CvError, CvResult};
pub trait ImageFilter {
fn apply(&self, src: &[u8], width: u32, height: u32) -> CvResult<Vec<u8>>;
fn kernel_size(&self) -> usize;
}
#[derive(Debug, Clone)]
pub struct GaussianBlur {
sigma: f64,
size: usize,
kernel: Vec<f64>,
}
impl GaussianBlur {
#[must_use]
pub fn new(sigma: f64, size: usize) -> Self {
let size = if size % 2 == 0 { size + 1 } else { size };
let size = size.max(3);
let kernel = create_gaussian_kernel_1d(sigma, size);
Self {
sigma,
size,
kernel,
}
}
#[must_use]
pub const fn sigma(&self) -> f64 {
self.sigma
}
#[must_use]
pub fn kernel(&self) -> &[f64] {
&self.kernel
}
}
impl ImageFilter for GaussianBlur {
fn apply(&self, src: &[u8], width: u32, height: u32) -> CvResult<Vec<u8>> {
validate_input(src, width, height)?;
let temp = convolve_horizontal(src, width, height, &self.kernel);
let result = convolve_vertical(&temp, width, height, &self.kernel);
Ok(result)
}
fn kernel_size(&self) -> usize {
self.size
}
}
#[derive(Debug, Clone, Copy)]
pub struct BoxBlur {
size: usize,
}
impl BoxBlur {
#[must_use]
pub fn new(size: usize) -> Self {
let size = if size % 2 == 0 { size + 1 } else { size };
Self { size: size.max(3) }
}
}
impl ImageFilter for BoxBlur {
fn apply(&self, src: &[u8], width: u32, height: u32) -> CvResult<Vec<u8>> {
validate_input(src, width, height)?;
let integral = compute_integral_image(src, width, height);
let result = box_filter_integral(&integral, width, height, self.size);
Ok(result)
}
fn kernel_size(&self) -> usize {
self.size
}
}
#[derive(Debug, Clone, Copy)]
pub struct MedianFilter {
size: usize,
}
impl MedianFilter {
#[must_use]
pub fn new(size: usize) -> Self {
let size = if size % 2 == 0 { size + 1 } else { size };
Self { size: size.max(3) }
}
}
impl ImageFilter for MedianFilter {
fn apply(&self, src: &[u8], width: u32, height: u32) -> CvResult<Vec<u8>> {
validate_input(src, width, height)?;
let half = self.size / 2;
let expected_size = width as usize * height as usize;
let mut dst = vec![0u8; expected_size];
for y in 0..height as usize {
for x in 0..width as usize {
let mut values: Vec<u8> = Vec::with_capacity(self.size * self.size);
for ky in 0..self.size {
let sy =
(y as i32 + ky as i32 - half as i32).clamp(0, height as i32 - 1) as usize;
for kx in 0..self.size {
let sx = (x as i32 + kx as i32 - half as i32).clamp(0, width as i32 - 1)
as usize;
values.push(src[sy * width as usize + sx]);
}
}
values.sort_unstable();
dst[y * width as usize + x] = values[values.len() / 2];
}
}
Ok(dst)
}
fn kernel_size(&self) -> usize {
self.size
}
}
#[derive(Debug, Clone)]
pub struct BilateralFilter {
sigma_space: f64,
sigma_color: f64,
size: usize,
spatial_weights: Vec<f64>,
}
impl BilateralFilter {
#[must_use]
pub fn new(sigma_space: f64, sigma_color: f64, size: usize) -> Self {
let size = if size % 2 == 0 { size + 1 } else { size };
let size = size.max(3);
let half = size / 2;
let mut spatial_weights = Vec::with_capacity(size * size);
let two_sigma_sq = 2.0 * sigma_space * sigma_space;
for y in 0..size {
for x in 0..size {
let dx = x as f64 - half as f64;
let dy = y as f64 - half as f64;
let dist_sq = dx * dx + dy * dy;
spatial_weights.push((-dist_sq / two_sigma_sq).exp());
}
}
Self {
sigma_space,
sigma_color,
size,
spatial_weights,
}
}
#[must_use]
pub const fn sigma_space(&self) -> f64 {
self.sigma_space
}
#[must_use]
pub const fn sigma_color(&self) -> f64 {
self.sigma_color
}
}
impl ImageFilter for BilateralFilter {
fn apply(&self, src: &[u8], width: u32, height: u32) -> CvResult<Vec<u8>> {
validate_input(src, width, height)?;
let half = self.size / 2;
let expected_size = width as usize * height as usize;
let mut dst = vec![0u8; expected_size];
let two_sigma_color_sq = 2.0 * self.sigma_color * self.sigma_color;
for y in 0..height as usize {
for x in 0..width as usize {
let center_val = src[y * width as usize + x] as f64;
let mut weighted_sum = 0.0;
let mut weight_sum = 0.0;
for ky in 0..self.size {
let sy =
(y as i32 + ky as i32 - half as i32).clamp(0, height as i32 - 1) as usize;
for kx in 0..self.size {
let sx = (x as i32 + kx as i32 - half as i32).clamp(0, width as i32 - 1)
as usize;
let neighbor_val = src[sy * width as usize + sx] as f64;
let color_diff = neighbor_val - center_val;
let range_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let spatial_idx = ky * self.size + kx;
let weight = self.spatial_weights[spatial_idx] * range_weight;
weighted_sum += neighbor_val * weight;
weight_sum += weight;
}
}
if weight_sum.abs() > f64::EPSILON {
dst[y * width as usize + x] =
(weighted_sum / weight_sum).round().clamp(0.0, 255.0) as u8;
} else {
dst[y * width as usize + x] = center_val as u8;
}
}
}
Ok(dst)
}
fn kernel_size(&self) -> usize {
self.size
}
}
#[derive(Debug, Clone)]
pub struct ConvolutionKernel {
data: Vec<f64>,
width: usize,
height: usize,
}
impl ConvolutionKernel {
#[must_use]
pub fn new(data: Vec<f64>, width: usize, height: usize) -> Self {
assert_eq!(data.len(), width * height);
Self {
data,
width,
height,
}
}
#[must_use]
pub const fn width(&self) -> usize {
self.width
}
#[must_use]
pub const fn height(&self) -> usize {
self.height
}
#[must_use]
pub fn data(&self) -> &[f64] {
&self.data
}
#[must_use]
pub fn sharpen() -> Self {
Self::new(vec![0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0], 3, 3)
}
#[must_use]
pub fn emboss() -> Self {
Self::new(vec![-2.0, -1.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0, 2.0], 3, 3)
}
pub fn apply(&self, src: &[u8], width: u32, height: u32) -> CvResult<Vec<u8>> {
validate_input(src, width, height)?;
let half_w = self.width / 2;
let half_h = self.height / 2;
let expected_size = width as usize * height as usize;
let mut dst = vec![0u8; expected_size];
for y in 0..height as usize {
for x in 0..width as usize {
let mut sum = 0.0;
for ky in 0..self.height {
let sy =
(y as i32 + ky as i32 - half_h as i32).clamp(0, height as i32 - 1) as usize;
for kx in 0..self.width {
let sx = (x as i32 + kx as i32 - half_w as i32).clamp(0, width as i32 - 1)
as usize;
let kernel_val = self.data[ky * self.width + kx];
sum += src[sy * width as usize + sx] as f64 * kernel_val;
}
}
dst[y * width as usize + x] = sum.round().clamp(0.0, 255.0) as u8;
}
}
Ok(dst)
}
}
fn create_gaussian_kernel_1d(sigma: f64, size: usize) -> Vec<f64> {
let half = size / 2;
let mut kernel = Vec::with_capacity(size);
let mut sum = 0.0;
let two_sigma_sq = 2.0 * sigma * sigma;
for i in 0..size {
let x = i as f64 - half as f64;
let value = (-x * x / two_sigma_sq).exp();
kernel.push(value);
sum += value;
}
for v in &mut kernel {
*v /= sum;
}
kernel
}
fn validate_input(data: &[u8], width: u32, height: u32) -> CvResult<()> {
if width == 0 || height == 0 {
return Err(CvError::invalid_dimensions(width, height));
}
let expected_size = width as usize * height as usize;
if data.len() < expected_size {
return Err(CvError::insufficient_data(expected_size, data.len()));
}
Ok(())
}
fn convolve_horizontal(src: &[u8], width: u32, height: u32, kernel: &[f64]) -> Vec<f64> {
let half = kernel.len() / 2;
let expected_size = width as usize * height as usize;
let mut dst = vec![0.0; expected_size];
for y in 0..height as usize {
for x in 0..width as usize {
let mut sum = 0.0;
for (ki, &kv) in kernel.iter().enumerate() {
let sx = (x as i32 + ki as i32 - half as i32).clamp(0, width as i32 - 1) as usize;
sum += src[y * width as usize + sx] as f64 * kv;
}
dst[y * width as usize + x] = sum;
}
}
dst
}
fn convolve_vertical(src: &[f64], width: u32, height: u32, kernel: &[f64]) -> Vec<u8> {
let half = kernel.len() / 2;
let expected_size = width as usize * height as usize;
let mut dst = vec![0u8; expected_size];
for y in 0..height as usize {
for x in 0..width as usize {
let mut sum = 0.0;
for (ki, &kv) in kernel.iter().enumerate() {
let sy = (y as i32 + ki as i32 - half as i32).clamp(0, height as i32 - 1) as usize;
sum += src[sy * width as usize + x] * kv;
}
dst[y * width as usize + x] = sum.round().clamp(0.0, 255.0) as u8;
}
}
dst
}
fn compute_integral_image(src: &[u8], width: u32, height: u32) -> Vec<u64> {
let w = width as usize;
let h = height as usize;
let mut integral = vec![0u64; (w + 1) * (h + 1)];
for y in 0..h {
for x in 0..w {
let idx = (y + 1) * (w + 1) + (x + 1);
integral[idx] = src[y * w + x] as u64
+ integral[y * (w + 1) + (x + 1)]
+ integral[(y + 1) * (w + 1) + x]
- integral[y * (w + 1) + x];
}
}
integral
}
fn box_filter_integral(integral: &[u64], width: u32, height: u32, size: usize) -> Vec<u8> {
let w = width as usize;
let h = height as usize;
let half = size / 2;
let mut dst = vec![0u8; w * h];
let iw = w + 1;
for y in 0..h {
for x in 0..w {
let x0 = x.saturating_sub(half);
let y0 = y.saturating_sub(half);
let x1 = (x + half + 1).min(w);
let y1 = (y + half + 1).min(h);
let sum = integral[y1 * iw + x1] + integral[y0 * iw + x0]
- integral[y0 * iw + x1]
- integral[y1 * iw + x0];
let count = (x1 - x0) * (y1 - y0);
dst[y * w + x] = (sum / count as u64) as u8;
}
}
dst
}
pub fn convolve_3x3_simd(src: &[u8], width: u32, height: u32, kernel: &[f32; 9]) -> Vec<u8> {
assert!(
src.len() >= width as usize * height as usize,
"src buffer is too small for the given dimensions"
);
convolve_3x3_scalar(src, width, height, kernel)
}
fn convolve_3x3_scalar(src: &[u8], width: u32, height: u32, kernel: &[f32; 9]) -> Vec<u8> {
let w = width as usize;
let h = height as usize;
let mut dst = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let mut acc = 0.0f32;
for ky in 0..3usize {
let sy = (y as i32 + ky as i32 - 1).clamp(0, h as i32 - 1) as usize;
for kx in 0..3usize {
let sx = (x as i32 + kx as i32 - 1).clamp(0, w as i32 - 1) as usize;
acc += src[sy * w + sx] as f32 * kernel[ky * 3 + kx];
}
}
dst[y * w + x] = acc.round().clamp(0.0, 255.0) as u8;
}
}
dst
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gaussian_blur_creation() {
let blur = GaussianBlur::new(1.0, 5);
assert_eq!(blur.kernel_size(), 5);
assert!((blur.sigma() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_gaussian_blur_even_size() {
let blur = GaussianBlur::new(1.0, 4);
assert_eq!(blur.kernel_size(), 5); }
#[test]
fn test_gaussian_blur_apply() {
let src = vec![100u8; 25];
let blur = GaussianBlur::new(1.0, 3);
let result = blur.apply(&src, 5, 5).expect("apply should succeed");
assert_eq!(result.len(), 25);
for &v in &result {
assert!((v as i32 - 100).abs() < 5);
}
}
#[test]
fn test_box_blur() {
let src = vec![100u8; 25];
let blur = BoxBlur::new(3);
let result = blur.apply(&src, 5, 5).expect("apply should succeed");
assert_eq!(result.len(), 25);
for &v in &result {
assert_eq!(v, 100);
}
}
#[test]
fn test_median_filter() {
let mut src = vec![100u8; 25];
src[12] = 255;
let filter = MedianFilter::new(3);
let result = filter.apply(&src, 5, 5).expect("apply should succeed");
assert!(result[12] < 200);
}
#[test]
fn test_bilateral_filter() {
let src = vec![100u8; 25];
let filter = BilateralFilter::new(10.0, 30.0, 5);
let result = filter.apply(&src, 5, 5).expect("apply should succeed");
assert_eq!(result.len(), 25);
for &v in &result {
assert!((v as i32 - 100).abs() < 5);
}
}
#[test]
fn test_convolution_kernel_sharpen() {
let kernel = ConvolutionKernel::sharpen();
assert_eq!(kernel.width(), 3);
assert_eq!(kernel.height(), 3);
}
#[test]
fn test_convolution_kernel_apply() {
let src = vec![100u8; 25];
let kernel = ConvolutionKernel::sharpen();
let result = kernel.apply(&src, 5, 5).expect("apply should succeed");
assert_eq!(result.len(), 25);
}
#[test]
fn test_integral_image() {
let src = vec![1u8; 9];
let integral = compute_integral_image(&src, 3, 3);
assert_eq!(integral[4 * 4 - 1], 9);
}
#[test]
fn test_invalid_dimensions() {
let src = vec![0u8; 25];
let blur = GaussianBlur::new(1.0, 3);
assert!(blur.apply(&src, 0, 5).is_err());
assert!(blur.apply(&src, 5, 0).is_err());
}
#[test]
fn test_insufficient_data() {
let src = vec![0u8; 10];
let blur = GaussianBlur::new(1.0, 3);
assert!(blur.apply(&src, 5, 5).is_err()); }
#[test]
fn test_3x3_convolution_identity() {
#[rustfmt::skip]
let identity: [f32; 9] = [
0.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 0.0,
];
let src: Vec<u8> = (0u8..=24).collect();
let result = convolve_3x3_simd(&src, 5, 5, &identity);
assert_eq!(result.len(), 25);
assert_eq!(result, src, "Identity kernel must preserve pixel values");
}
#[test]
fn test_3x3_convolution_blur() {
#[rustfmt::skip]
let box_blur: [f32; 9] = [
1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0,
1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0,
1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0,
];
let src = vec![120u8; 25];
let result = convolve_3x3_simd(&src, 5, 5, &box_blur);
assert_eq!(result.len(), 25);
for &v in &result {
assert!(
(v as i32 - 120).abs() <= 1,
"Expected value close to 120, got {v}"
);
}
}
#[test]
fn test_3x3_convolution_non_uniform() {
#[rustfmt::skip]
let kernel: [f32; 9] = [
0.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 0.0,
];
let src: Vec<u8> = (0u8..100).collect();
let result = convolve_3x3_simd(&src, 10, 10, &kernel);
assert_eq!(
result, src,
"Identity kernel should preserve gradient image"
);
}
}