# agent_v1/vision/blur_sensitive.py """ Floutage automatique des zones de texte sensible dans les screenshots. Conformité AI Act : les screenshots utilisés pour l'apprentissage ne doivent pas contenir de données patient lisibles, mots de passe, etc. Stratégie : - Détecte les champs de saisie (rectangles clairs avec du texte) - Floute leur CONTENU tout en gardant la structure UI visible - Rapide (<200ms) : uniquement des opérations OpenCV simples, pas de deep learning Usage : from .blur_sensitive import blur_sensitive_regions blur_sensitive_regions(img) # modifie l'image PIL en place """ import logging import time logger = logging.getLogger(__name__) # Seuils configurables pour la détection des champs de saisie _INPUT_FIELD_MIN_WIDTH = 50 # Largeur minimale en pixels _INPUT_FIELD_MIN_HEIGHT = 15 # Hauteur minimale _INPUT_FIELD_MAX_HEIGHT = 80 # Hauteur maximale (exclut les grandes zones) _INPUT_FIELD_MIN_ASPECT_RATIO = 2.0 # Ratio largeur/hauteur minimum _INPUT_FIELD_MIN_AREA = 1000 # Surface minimale en pixels² _INPUT_FIELD_BRIGHTNESS_THRESHOLD = 200 # Luminosité moyenne minimum (fond clair) # Pour les zones de texte multi-lignes (textarea) _TEXTAREA_MIN_WIDTH = 100 _TEXTAREA_MIN_HEIGHT = 60 _TEXTAREA_MAX_HEIGHT = 500 _TEXTAREA_MIN_AREA = 8000 _TEXTAREA_MIN_ASPECT_RATIO = 1.2 # Paramètres du flou gaussien _BLUR_KERNEL_SIZE = (23, 23) _BLUR_SIGMA = 12 _BLUR_MARGIN = 3 # Marge en pixels pour garder le bord du champ visible def blur_sensitive_regions(pil_image): """Floute les zones de texte sensible dans une image PIL. Modifie l'image en place et la retourne. Rapide : ~50-150ms selon la résolution. Args: pil_image: Image PIL (mode RGB) Returns: L'image PIL modifiée (même objet, modifié en place) """ try: import cv2 import numpy as np except ImportError: logger.warning("OpenCV non disponible — floutage désactivé") return pil_image t0 = time.perf_counter() # Conversion PIL → OpenCV (sans copie disque) img_array = np.array(pil_image) # PIL est RGB, OpenCV attend BGR img_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR) gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) blurred_count = 0 # --- Passe 1 : Champs de saisie classiques (input text) --- blurred_count += _blur_input_fields(img_bgr, gray) # --- Passe 2 : Zones de texte multi-lignes (textarea, éditeurs) --- blurred_count += _blur_textareas(img_bgr, gray) if blurred_count > 0: # Reconversion OpenCV → PIL en place from PIL import Image as _PILImage img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) pil_image.paste(_PILImage.fromarray(img_rgb)) elapsed_ms = (time.perf_counter() - t0) * 1000 if blurred_count > 0: logger.debug(f"Floutage : {blurred_count} zones en {elapsed_ms:.0f}ms") return pil_image def _blur_input_fields(img_bgr, gray): """Détecte et floute les champs de saisie simples (input text). Les champs de saisie sont typiquement des rectangles à fond clair (blanc ou gris très clair) avec du texte sombre dedans. """ import cv2 import numpy as np count = 0 # Seuillage : zones quasi-blanches (fond des champs de saisie) _, white_mask = cv2.threshold(gray, 230, 255, cv2.THRESH_BINARY) # Nettoyage morphologique : fermer les petits trous dans les champs kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5)) white_mask = cv2.morphologyEx(white_mask, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(white_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: x, y, w, h = cv2.boundingRect(contour) aspect_ratio = w / max(h, 1) area = w * h # Filtrage : forme typique d'un champ de saisie if (w < _INPUT_FIELD_MIN_WIDTH or h < _INPUT_FIELD_MIN_HEIGHT or h > _INPUT_FIELD_MAX_HEIGHT or aspect_ratio < _INPUT_FIELD_MIN_ASPECT_RATIO or area < _INPUT_FIELD_MIN_AREA): continue # Vérifier la luminosité moyenne (les boutons ont souvent un fond coloré) roi = gray[y:y+h, x:x+w] mean_val = np.mean(roi) if mean_val < _INPUT_FIELD_BRIGHTNESS_THRESHOLD: continue # Pas assez clair, probablement pas un champ de saisie # Vérifier qu'il y a du contenu (variation de luminosité = texte présent) std_val = np.std(roi) if std_val < 5: continue # Zone uniformément blanche, pas de texte à flouter # Appliquer le flou gaussien sur le contenu (garder le bord visible) m = _BLUR_MARGIN y1, y2 = y + m, y + h - m x1, x2 = x + m, x + w - m if y2 > y1 and x2 > x1: roi_color = img_bgr[y1:y2, x1:x2] if roi_color.size > 0: blurred = cv2.GaussianBlur(roi_color, _BLUR_KERNEL_SIZE, _BLUR_SIGMA) img_bgr[y1:y2, x1:x2] = blurred count += 1 return count def _blur_textareas(img_bgr, gray): """Détecte et floute les zones de texte multi-lignes (textarea, éditeurs). Ces zones sont plus grandes que les champs simples, avec un fond clair et beaucoup de texte. """ import cv2 import numpy as np count = 0 # Seuillage un peu plus tolérant pour les textareas (parfois gris clair) _, light_mask = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY) # Nettoyage morphologique plus agressif pour les grandes zones kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 10)) light_mask = cv2.morphologyEx(light_mask, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(light_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: x, y, w, h = cv2.boundingRect(contour) aspect_ratio = w / max(h, 1) area = w * h # Filtrage : forme typique d'une textarea if (w < _TEXTAREA_MIN_WIDTH or h < _TEXTAREA_MIN_HEIGHT or h > _TEXTAREA_MAX_HEIGHT or aspect_ratio < _TEXTAREA_MIN_ASPECT_RATIO or area < _TEXTAREA_MIN_AREA): continue # Vérifier la luminosité et la présence de texte roi = gray[y:y+h, x:x+w] mean_val = np.mean(roi) std_val = np.std(roi) if mean_val < 190 or std_val < 8: continue # Pas un textarea avec du contenu # Flou sur le contenu m = _BLUR_MARGIN + 2 # Marge un peu plus grande pour les textarea y1, y2 = y + m, y + h - m x1, x2 = x + m, x + w - m if y2 > y1 and x2 > x1: roi_color = img_bgr[y1:y2, x1:x2] if roi_color.size > 0: blurred = cv2.GaussianBlur(roi_color, _BLUR_KERNEL_SIZE, _BLUR_SIGMA) img_bgr[y1:y2, x1:x2] = blurred count += 1 return count