""" Utilitaires pour la capture d'écran et le traitement d'images Fournit des fonctions pour capturer l'écran, extraire des ROI et dessiner des bounding boxes """ import numpy as np import cv2 from typing import Tuple, Optional import platform import subprocess def capture_screen() -> np.ndarray: """ Capture l'écran complet et retourne l'image en format numpy array Returns: Image de l'écran en format BGR (OpenCV standard) Raises: RuntimeError: Si la capture d'écran échoue """ try: # Utiliser différentes méthodes selon le système d'exploitation system = platform.system() if system == "Linux": # Sur Linux, utiliser scrot ou gnome-screenshot return _capture_screen_linux() elif system == "Windows": # Sur Windows, utiliser mss ou pyautogui return _capture_screen_windows() elif system == "Darwin": # macOS # Sur macOS, utiliser screencapture return _capture_screen_macos() else: raise RuntimeError(f"Système d'exploitation non supporté: {system}") except Exception as e: raise RuntimeError(f"Échec de la capture d'écran: {str(e)}") def _capture_screen_linux() -> np.ndarray: """ Capture d'écran spécifique à Linux Utilise mss pour une capture rapide """ try: import mss import mss.tools with mss.mss() as sct: # Capturer le moniteur principal monitor = sct.monitors[1] screenshot = sct.grab(monitor) # Convertir en numpy array img = np.array(screenshot) # Convertir BGRA vers BGR img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) return img except ImportError: # Fallback: utiliser PIL/Pillow return _capture_screen_pil() def _capture_screen_windows() -> np.ndarray: """ Capture d'écran spécifique à Windows """ try: import mss import mss.tools with mss.mss() as sct: monitor = sct.monitors[1] screenshot = sct.grab(monitor) img = np.array(screenshot) img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) return img except ImportError: return _capture_screen_pil() def _capture_screen_macos() -> np.ndarray: """ Capture d'écran spécifique à macOS """ try: import mss import mss.tools with mss.mss() as sct: monitor = sct.monitors[1] screenshot = sct.grab(monitor) img = np.array(screenshot) img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) return img except ImportError: return _capture_screen_pil() def _capture_screen_pil() -> np.ndarray: """ Capture d'écran en utilisant PIL/Pillow (fallback) """ try: from PIL import ImageGrab screenshot = ImageGrab.grab() img = np.array(screenshot) # Convertir RGB vers BGR (format OpenCV) img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) return img except ImportError: raise RuntimeError("Aucune bibliothèque de capture d'écran disponible. " "Installez 'mss' ou 'Pillow'.") def get_active_window() -> str: """ Obtient le titre de la fenêtre active Returns: Titre de la fenêtre active, ou chaîne vide si impossible à déterminer """ try: system = platform.system() if system == "Linux": return _get_active_window_linux() elif system == "Windows": return _get_active_window_windows() elif system == "Darwin": # macOS return _get_active_window_macos() else: return "" except Exception as e: print(f"Erreur lors de la récupération de la fenêtre active: {e}") return "" def _get_active_window_linux() -> str: """ Obtient la fenêtre active sur Linux avec plusieurs méthodes de fallback """ # Méthode 1: xdotool (le plus fiable) try: result = subprocess.run( ["xdotool", "getactivewindow", "getwindowname"], capture_output=True, text=True, timeout=1, check=False ) if result.returncode == 0 and result.stdout.strip(): return result.stdout.strip() except (subprocess.TimeoutExpired, FileNotFoundError): pass # Méthode 2: xprop avec _NET_ACTIVE_WINDOW try: # Obtenir l'ID de la fenêtre active result = subprocess.run( ["xprop", "-root", "_NET_ACTIVE_WINDOW"], capture_output=True, text=True, timeout=1, check=False ) if result.returncode == 0: # Extraire l'ID de fenêtre (format: "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x...") window_id = result.stdout.strip().split()[-1] # Obtenir le nom de la fenêtre result2 = subprocess.run( ["xprop", "-id", window_id, "WM_NAME"], capture_output=True, text=True, timeout=1, check=False ) if result2.returncode == 0: # Format: WM_NAME(STRING) = "Titre de la fenêtre" name = result2.stdout.strip() if '=' in name: title = name.split('=', 1)[1].strip().strip('"') if title: return title except (subprocess.TimeoutExpired, FileNotFoundError): pass # Méthode 3: wmctrl try: result = subprocess.run( ["wmctrl", "-l", "-p"], capture_output=True, text=True, timeout=1, check=False ) if result.returncode == 0: lines = result.stdout.strip().split('\n') # Essayer de trouver la fenêtre active (première ligne comme approximation) if lines and lines[0]: parts = lines[0].split(None, 4) if len(parts) >= 5: return parts[4] except (subprocess.TimeoutExpired, FileNotFoundError): pass # Méthode 4: Essayer avec Python Xlib (si disponible) try: from Xlib import X, display from Xlib.error import XError d = display.Display() root = d.screen().root # Obtenir la fenêtre active window_id = root.get_full_property( d.intern_atom('_NET_ACTIVE_WINDOW'), X.AnyPropertyType ) if window_id and window_id.value: active_window = d.create_resource_object('window', window_id.value[0]) window_name = active_window.get_wm_name() if window_name: return window_name except (ImportError, XError, Exception): pass return "Unknown Window" def _get_active_window_windows() -> str: """ Obtient la fenêtre active sur Windows """ try: import win32gui hwnd = win32gui.GetForegroundWindow() return win32gui.GetWindowText(hwnd) except ImportError: # Fallback sans pywin32 try: import ctypes hwnd = ctypes.windll.user32.GetForegroundWindow() length = ctypes.windll.user32.GetWindowTextLengthW(hwnd) buff = ctypes.create_unicode_buffer(length + 1) ctypes.windll.user32.GetWindowTextW(hwnd, buff, length + 1) return buff.value except Exception: return "" def _get_active_window_macos() -> str: """ Obtient la fenêtre active sur macOS """ try: script = ''' tell application "System Events" set frontApp to name of first application process whose frontmost is true set frontWindow to name of front window of application process frontApp return frontApp & " - " & frontWindow end tell ''' result = subprocess.run( ["osascript", "-e", script], capture_output=True, text=True, timeout=1 ) if result.returncode == 0: return result.stdout.strip() except (subprocess.TimeoutExpired, FileNotFoundError): pass return "" def extract_roi(frame: np.ndarray, bbox: Tuple[int, int, int, int]) -> np.ndarray: """ Extrait une région d'intérêt (ROI) d'une image Args: frame: Image source en format numpy array bbox: Bounding box (x, y, width, height) en pixels Returns: Image de la région d'intérêt Raises: ValueError: Si la bounding box est invalide """ x, y, w, h = bbox # Valider les dimensions if w <= 0 or h <= 0: raise ValueError(f"Dimensions de bounding box invalides: width={w}, height={h}") # Obtenir les dimensions de l'image img_height, img_width = frame.shape[:2] # Limiter les coordonnées aux dimensions de l'image x = max(0, min(x, img_width - 1)) y = max(0, min(y, img_height - 1)) x2 = max(0, min(x + w, img_width)) y2 = max(0, min(y + h, img_height)) # Extraire la ROI roi = frame[y:y2, x:x2] # Vérifier que la ROI n'est pas vide if roi.size == 0: raise ValueError(f"ROI vide avec bbox={bbox}, image_size=({img_width}, {img_height})") return roi def draw_bbox(frame: np.ndarray, bbox: Tuple[int, int, int, int], label: str = "", color: Tuple[int, int, int] = (0, 255, 0), thickness: int = 2) -> np.ndarray: """ Dessine une bounding box sur une image avec un label optionnel Args: frame: Image sur laquelle dessiner bbox: Bounding box (x, y, width, height) en pixels label: Label à afficher au-dessus de la box (optionnel) color: Couleur BGR de la box (par défaut: vert) thickness: Épaisseur de la ligne en pixels Returns: Image avec la bounding box dessinée (copie de l'original) """ # Créer une copie pour ne pas modifier l'original img = frame.copy() x, y, w, h = bbox # Dessiner le rectangle cv2.rectangle(img, (x, y), (x + w, y + h), color, thickness) # Dessiner le label si fourni if label: # Calculer la taille du texte font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.6 font_thickness = 2 (text_width, text_height), baseline = cv2.getTextSize( label, font, font_scale, font_thickness ) # Dessiner un rectangle de fond pour le texte label_y = y - 10 if y - 10 > text_height else y + h + text_height + 10 cv2.rectangle( img, (x, label_y - text_height - baseline), (x + text_width, label_y + baseline), color, -1 # Remplir ) # Dessiner le texte cv2.putText( img, label, (x, label_y), font, font_scale, (255, 255, 255), # Blanc font_thickness ) return img def resize_image(image: np.ndarray, max_width: int = 1920, max_height: int = 1080) -> np.ndarray: """ Redimensionne une image en conservant le ratio d'aspect Args: image: Image à redimensionner max_width: Largeur maximale max_height: Hauteur maximale Returns: Image redimensionnée """ height, width = image.shape[:2] # Calculer le ratio de redimensionnement ratio = min(max_width / width, max_height / height) # Si l'image est déjà plus petite, ne pas la redimensionner if ratio >= 1.0: return image # Calculer les nouvelles dimensions new_width = int(width * ratio) new_height = int(height * ratio) # Redimensionner resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) return resized def save_image(image: np.ndarray, filepath: str) -> bool: """ Sauvegarde une image sur le disque Args: image: Image à sauvegarder filepath: Chemin du fichier de destination Returns: True si la sauvegarde a réussi, False sinon """ try: cv2.imwrite(filepath, image) return True except Exception as e: print(f"Erreur lors de la sauvegarde de l'image: {e}") return False def load_image(filepath: str) -> Optional[np.ndarray]: """ Charge une image depuis le disque Args: filepath: Chemin du fichier image Returns: Image en format numpy array, ou None si le chargement échoue """ try: image = cv2.imread(filepath) if image is None: print(f"Impossible de charger l'image: {filepath}") return image except Exception as e: print(f"Erreur lors du chargement de l'image: {e}") return None if __name__ == "__main__": # Tests basiques des utilitaires d'image print("Test des utilitaires d'image RPA Vision V2") print("=" * 50) # Test 1: Capture d'écran print("\n1. Test capture_screen():") try: screen = capture_screen() print(f" ✓ Capture réussie: {screen.shape} (H x W x C)") print(f" Type: {screen.dtype}") except Exception as e: print(f" ✗ Échec: {e}") # Test 2: Fenêtre active print("\n2. Test get_active_window():") window_title = get_active_window() if window_title: print(f" ✓ Fenêtre active: '{window_title}'") else: print(f" ⚠ Impossible de déterminer la fenêtre active") # Test 3: Extraction ROI print("\n3. Test extract_roi():") try: # Créer une image de test test_img = np.zeros((480, 640, 3), dtype=np.uint8) test_img[100:200, 150:300] = [0, 255, 0] # Rectangle vert # Extraire une ROI roi = extract_roi(test_img, (150, 100, 150, 100)) print(f" ✓ ROI extraite: {roi.shape}") # Test avec bbox invalide (devrait être limité) roi2 = extract_roi(test_img, (600, 400, 100, 100)) print(f" ✓ ROI avec bbox hors limites: {roi2.shape}") except Exception as e: print(f" ✗ Échec: {e}") # Test 4: Dessin de bounding box print("\n4. Test draw_bbox():") try: test_img = np.zeros((480, 640, 3), dtype=np.uint8) # Dessiner plusieurs bounding boxes img_with_bbox = draw_bbox(test_img, (100, 100, 200, 150), "Bouton 1", (0, 255, 0)) img_with_bbox = draw_bbox(img_with_bbox, (350, 200, 150, 100), "Bouton 2", (255, 0, 0)) print(f" ✓ Bounding boxes dessinées: {img_with_bbox.shape}") except Exception as e: print(f" ✗ Échec: {e}") # Test 5: Redimensionnement print("\n5. Test resize_image():") try: large_img = np.zeros((2160, 3840, 3), dtype=np.uint8) resized = resize_image(large_img, max_width=1920, max_height=1080) print(f" ✓ Image redimensionnée: {large_img.shape} -> {resized.shape}") except Exception as e: print(f" ✗ Échec: {e}") print("\n✓ Tests terminés!")