kuwahara-filter 0.1.0

Fast Kuwahara filter implementation for artistic image effects
Documentation
use image::{ImageBuffer, RgbaImage};
use rayon::prelude::*;

/// Kuwahara filter that processes RGBA data directly
/// 
/// Performance optimizations:
/// - Uses raw pixel data access instead of get_pixel() for better cache locality
/// - Pre-computes boundary constraints to minimize runtime checks
/// - Processes output buffer in parallel using Rayon
/// - Minimizes memory allocations by working directly with Vec<u8>
pub fn kuwahara_filter(
    img: RgbaImage,
    radius: i32,
) -> RgbaImage {
    let (width, height) = img.dimensions();
    let width_usize = width as usize;
    let height_i32 = height as i32;
    let width_i32 = width as i32;
    
    // Get raw pixel data for faster access
    let pixels = img.as_raw();
    
    // Create output buffer
    let mut output = vec![0u8; pixels.len()];
    
    // Process rows in parallel
    output
        .par_chunks_mut(width_usize * 4)
        .enumerate()
        .for_each(|(y, row_chunk)| {
            let y_i32 = y as i32;
            
            for x in 0..width_usize {
                let x_i32 = x as i32;
                let mut best_var = f64::INFINITY;
                let mut best_pixel = [0u8; 4];
                
                // Check 4 quadrants around the pixel
                for &(dx_min, dx_max, dy_min, dy_max) in &[
                    (-radius, 0, -radius, 0),
                    (0, radius, -radius, 0),
                    (-radius, 0, 0, radius),
                    (0, radius, 0, radius),
                ] {
                    let x0 = (x_i32 + dx_min).max(0);
                    let x1 = (x_i32 + dx_max).min(width_i32 - 1);
                    let y0 = (y_i32 + dy_min).max(0);
                    let y1 = (y_i32 + dy_max).min(height_i32 - 1);
                    
                    let mut sum_r = 0u64;
                    let mut sum_g = 0u64;
                    let mut sum_b = 0u64;
                    let mut sum_r2 = 0u64;
                    let mut sum_g2 = 0u64;
                    let mut sum_b2 = 0u64;
                    let mut count = 0u64;
                    
                    // Direct pixel access using raw data
                    for py in y0..=y1 {
                        let row_start = (py as usize) * width_usize * 4;
                        for px in x0..=x1 {
                            let idx = row_start + (px as usize) * 4;
                            
                            let r = pixels[idx] as u64;
                            let g = pixels[idx + 1] as u64;
                            let b = pixels[idx + 2] as u64;
                            
                            sum_r += r;
                            sum_g += g;
                            sum_b += b;
                            sum_r2 += r * r;
                            sum_g2 += g * g;
                            sum_b2 += b * b;
                            count += 1;
                        }
                    }
                    
                    if count == 0 {
                        continue;
                    }
                    
                    let count_f = count as f64;
                    let mean_r = sum_r as f64 / count_f;
                    let mean_g = sum_g as f64 / count_f;
                    let mean_b = sum_b as f64 / count_f;
                    
                    let var_r = (sum_r2 as f64 / count_f) - mean_r * mean_r;
                    let var_g = (sum_g2 as f64 / count_f) - mean_g * mean_g;
                    let var_b = (sum_b2 as f64 / count_f) - mean_b * mean_b;
                    
                    let total_var = var_r + var_g + var_b;
                    
                    if total_var < best_var {
                        best_var = total_var;
                        best_pixel[0] = mean_r.clamp(0.0, 255.0).round() as u8;
                        best_pixel[1] = mean_g.clamp(0.0, 255.0).round() as u8;
                        best_pixel[2] = mean_b.clamp(0.0, 255.0).round() as u8;
                        best_pixel[3] = pixels[y * width_usize * 4 + x * 4 + 3]; // preserve alpha
                    }
                }
                
                // Set pixel in output
                let pixel_idx = x * 4;
                row_chunk[pixel_idx] = best_pixel[0];
                row_chunk[pixel_idx + 1] = best_pixel[1];
                row_chunk[pixel_idx + 2] = best_pixel[2];
                row_chunk[pixel_idx + 3] = best_pixel[3];
            }
        });
    
    ImageBuffer::from_raw(width, height, output).unwrap()
}

#[cfg(test)]
mod tests {
    use super::*;
    use image::Rgba;

    #[test]
    fn test_kuwahara_filter() {
        let mut img = ImageBuffer::new(3, 3);
        
        // Fill with solid color
        for pixel in img.pixels_mut() {
            *pixel = Rgba([100, 150, 200, 255]);
        }
        
        let result = kuwahara_filter(img, 1);
        
        // Should preserve solid color
        for pixel in result.pixels() {
            assert_eq!(pixel[0], 100);
            assert_eq!(pixel[1], 150);
            assert_eq!(pixel[2], 200);
            assert_eq!(pixel[3], 255);
        }
    }
}