341 lines
11 KiB
Rust
341 lines
11 KiB
Rust
//! 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<BlurRegion> {
|
|
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<BlurRegion>) {
|
|
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
|
|
}
|