feat: agent Rust complet — systray, chat, enregistrement, floutage (2.4 MB)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
340
agent_rust/src/blur.rs
Normal file
340
agent_rust/src/blur.rs
Normal file
@@ -0,0 +1,340 @@
|
||||
//! 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
|
||||
}
|
||||
Reference in New Issue
Block a user