//! Floutage des zones sensibles dans les captures d'ecran. //! //! Detecte les champs de saisie (zones claires rectangulaires) et applique //! un flou gaussien pour proteger les donnees sensibles (mots de passe, etc.). //! Equivalent de agent_v1/vision/blur_sensitive.py. //! //! Algorithme : //! 1. Conversion en niveaux de gris //! 2. Seuillage binaire (detecter les zones claires = champs de saisie) //! 3. Detection de contours rectangulaires > 50px de large //! 4. Application d'un flou gaussien sur les zones detectees //! //! Utilise le crate image pour le traitement et imageproc pour le flou. use image::{DynamicImage, GrayImage, Rgba, RgbaImage}; /// Seuil de luminosite pour detecter les champs de saisie (0-255). /// Les zones plus claires que ce seuil sont considerees comme des champs. const BRIGHTNESS_THRESHOLD: u8 = 220; /// Largeur minimale d'un champ de saisie detecte (en pixels). const MIN_FIELD_WIDTH: u32 = 50; /// Hauteur minimale d'un champ de saisie detecte (en pixels). const MIN_FIELD_HEIGHT: u32 = 15; /// Hauteur maximale d'un champ de saisie (evite de flouter l'ecran entier). const MAX_FIELD_HEIGHT: u32 = 80; /// Largeur maximale d'un champ (evite les faux positifs sur grandes zones blanches). const MAX_FIELD_WIDTH: u32 = 800; /// Intensite du flou gaussien (sigma). const BLUR_SIGMA: f32 = 10.0; /// Rectangle representant une zone a flouter. #[derive(Debug, Clone)] pub struct BlurRegion { pub x: u32, pub y: u32, pub width: u32, pub height: u32, } /// Detecte les champs de saisie dans une image et les floute. /// /// Retourne l'image modifiee avec les zones sensibles floutees. /// Si aucun champ n'est detecte, retourne l'image inchangee. pub fn blur_sensitive_fields(img: &DynamicImage) -> DynamicImage { let regions = detect_input_fields(img); if regions.is_empty() { return img.clone(); } println!( "[BLUR] {} zone(s) sensible(s) detectee(s) — floutage...", regions.len() ); let mut result = img.to_rgba8(); for region in ®ions { blur_region(&mut result, region); } DynamicImage::ImageRgba8(result) } /// Detecte les champs de saisie (zones claires rectangulaires). /// /// Algorithme simplifie : /// 1. Convertir en niveaux de gris /// 2. Seuillage binaire /// 3. Scanner les lignes horizontales pour trouver les series de pixels clairs /// 4. Regrouper les series adjacentes en rectangles pub fn detect_input_fields(img: &DynamicImage) -> Vec { let gray = img.to_luma8(); let (width, height) = gray.dimensions(); let mut regions = Vec::new(); // Creer une image binaire (seuillage) let binary = threshold_image(&gray, BRIGHTNESS_THRESHOLD); // Scanner par bandes horizontales pour detecter les champs // On cherche des sequences continues de pixels blancs sur plusieurs lignes let mut y = 0; while y < height { // Pour chaque ligne, trouver les segments horizontaux blancs let segments = find_white_segments(&binary, y, width); for (seg_start, seg_end) in &segments { let seg_width = seg_end - seg_start; if seg_width < MIN_FIELD_WIDTH || seg_width > MAX_FIELD_WIDTH { continue; } // Verifier combien de lignes consecutives partagent ce segment let field_height = count_vertical_extent( &binary, *seg_start, *seg_end, y, height, ); if field_height >= MIN_FIELD_HEIGHT && field_height <= MAX_FIELD_HEIGHT { // Verifier que cette region ne chevauche pas une region existante let new_region = BlurRegion { x: *seg_start, y, width: seg_width, height: field_height, }; if !overlaps_existing(®ions, &new_region) { regions.push(new_region); } } } // Avancer de la hauteur du dernier champ detecte, ou de 1 ligne y += 1; } // Deduplication : fusionner les regions tres proches merge_close_regions(&mut regions); regions } /// Applique un seuillage binaire simple. fn threshold_image(gray: &GrayImage, threshold: u8) -> GrayImage { let (width, height) = gray.dimensions(); let mut binary = GrayImage::new(width, height); for y in 0..height { for x in 0..width { let pixel = gray.get_pixel(x, y).0[0]; if pixel >= threshold { binary.put_pixel(x, y, image::Luma([255])); } else { binary.put_pixel(x, y, image::Luma([0])); } } } binary } /// Trouve les segments horizontaux de pixels blancs sur une ligne. fn find_white_segments(binary: &GrayImage, y: u32, width: u32) -> Vec<(u32, u32)> { let mut segments = Vec::new(); let mut in_segment = false; let mut seg_start = 0u32; for x in 0..width { let is_white = binary.get_pixel(x, y).0[0] > 128; if is_white && !in_segment { seg_start = x; in_segment = true; } else if !is_white && in_segment { segments.push((seg_start, x)); in_segment = false; } } if in_segment { segments.push((seg_start, width)); } segments } /// Compte le nombre de lignes consecutives ou le segment est blanc. fn count_vertical_extent( binary: &GrayImage, seg_start: u32, seg_end: u32, start_y: u32, max_y: u32, ) -> u32 { let mut count = 0u32; let check_width = seg_end - seg_start; let threshold = (check_width as f64 * 0.7) as u32; // 70% doivent etre blancs for y in start_y..max_y.min(start_y + MAX_FIELD_HEIGHT + 5) { let mut white_count = 0u32; for x in seg_start..seg_end { if binary.get_pixel(x, y).0[0] > 128 { white_count += 1; } } if white_count >= threshold { count += 1; } else { break; } } count } /// Verifie si une region chevauche une region existante. fn overlaps_existing(regions: &[BlurRegion], new_region: &BlurRegion) -> bool { for region in regions { let x_overlap = new_region.x < region.x + region.width && new_region.x + new_region.width > region.x; let y_overlap = new_region.y < region.y + region.height && new_region.y + new_region.height > region.y; if x_overlap && y_overlap { return true; } } false } /// Fusionne les regions tres proches (< 10px de distance). fn merge_close_regions(regions: &mut Vec) { if regions.len() < 2 { return; } // Tri par position (y, puis x) regions.sort_by(|a, b| a.y.cmp(&b.y).then(a.x.cmp(&b.x))); let mut merged = Vec::new(); let mut current = regions[0].clone(); for region in regions.iter().skip(1) { let x_close = (current.x + current.width + 10 >= region.x) && (region.x + region.width + 10 >= current.x); let y_close = (current.y + current.height + 5 >= region.y) && (region.y + region.height + 5 >= current.y); if x_close && y_close { // Fusionner let min_x = current.x.min(region.x); let min_y = current.y.min(region.y); let max_x = (current.x + current.width).max(region.x + region.width); let max_y = (current.y + current.height).max(region.y + region.height); current = BlurRegion { x: min_x, y: min_y, width: max_x - min_x, height: max_y - min_y, }; } else { merged.push(current); current = region.clone(); } } merged.push(current); *regions = merged; } /// Applique un flou gaussien sur une region de l'image. /// /// Implementation simplifiee : box blur avec plusieurs passes /// (approximation du gaussien, plus rapide que le vrai gaussien). fn blur_region(img: &mut RgbaImage, region: &BlurRegion) { let (img_w, img_h) = img.dimensions(); // Borner la region aux dimensions de l'image let x_start = region.x.min(img_w); let y_start = region.y.min(img_h); let x_end = (region.x + region.width).min(img_w); let y_end = (region.y + region.height).min(img_h); if x_start >= x_end || y_start >= y_end { return; } let radius = BLUR_SIGMA as u32; let kernel_size = (radius * 2 + 1) as i32; let kernel_area = (kernel_size * kernel_size) as u32; // Box blur : moyenne des pixels dans un carre de rayon `radius` // On fait 3 passes pour approximer un flou gaussien for _pass in 0..3 { // Copier les pixels de la region dans un buffer temporaire let reg_w = (x_end - x_start) as usize; let reg_h = (y_end - y_start) as usize; let mut buffer: Vec<[u8; 4]> = Vec::with_capacity(reg_w * reg_h); for y in y_start..y_end { for x in x_start..x_end { buffer.push(img.get_pixel(x, y).0); } } // Appliquer le box blur for y in y_start..y_end { for x in x_start..x_end { let mut sum_r = 0u32; let mut sum_g = 0u32; let mut sum_b = 0u32; let mut count = 0u32; for ky in -(radius as i32)..=(radius as i32) { for kx in -(radius as i32)..=(radius as i32) { let sx = x as i32 + kx; let sy = y as i32 + ky; if sx >= x_start as i32 && sx < x_end as i32 && sy >= y_start as i32 && sy < y_end as i32 { let bx = (sx - x_start as i32) as usize; let by = (sy - y_start as i32) as usize; let pixel = buffer[by * reg_w + bx]; sum_r += pixel[0] as u32; sum_g += pixel[1] as u32; sum_b += pixel[2] as u32; count += 1; } } } if count > 0 { let pixel = Rgba([ (sum_r / count) as u8, (sum_g / count) as u8, (sum_b / count) as u8, 255, ]); img.put_pixel(x, y, pixel); } } } } let _ = kernel_area; // suppress unused warning }