v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
202
core/capture/README.md
Normal file
202
core/capture/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Module de Capture d'Écran
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le module `screen_capturer` fournit une interface unifiée pour capturer des screenshots avec fallback automatique entre différentes bibliothèques.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- ✅ Capture d'écran rapide avec `mss` (méthode préférée)
|
||||
- ✅ Fallback automatique vers `pyautogui` si mss n'est pas disponible
|
||||
- ✅ Détection de la fenêtre active avec `pygetwindow`
|
||||
- ✅ Conversion automatique au format RGB numpy
|
||||
- ✅ Validation des images capturées
|
||||
- ✅ Gestion propre des ressources
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Installer les dépendances
|
||||
cd rpa_vision_v3
|
||||
./install_capture_deps.sh
|
||||
|
||||
# Ou manuellement
|
||||
pip install mss>=9.0.0 pygetwindow>=0.0.9
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Capture Simple
|
||||
|
||||
```python
|
||||
from core.capture.screen_capturer import ScreenCapturer
|
||||
|
||||
# Initialiser le capturer
|
||||
capturer = ScreenCapturer()
|
||||
|
||||
# Capturer l'écran
|
||||
img = capturer.capture() # numpy array (H, W, 3) RGB
|
||||
|
||||
# Vérifier la capture
|
||||
if img is not None:
|
||||
print(f"Image capturée: {img.shape}")
|
||||
```
|
||||
|
||||
### Détection de Fenêtre Active
|
||||
|
||||
```python
|
||||
# Obtenir les infos de la fenêtre active
|
||||
window = capturer.get_active_window()
|
||||
|
||||
if window:
|
||||
print(f"Fenêtre: {window['title']}")
|
||||
print(f"Position: ({window['x']}, {window['y']})")
|
||||
print(f"Taille: {window['width']}x{window['height']}")
|
||||
```
|
||||
|
||||
### Intégration avec PIL
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
|
||||
# Capturer et convertir en PIL Image
|
||||
img_array = capturer.capture()
|
||||
img_pil = Image.fromarray(img_array)
|
||||
|
||||
# Sauvegarder
|
||||
img_pil.save("screenshot.png")
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ScreenCapturer
|
||||
├── __init__() # Initialise avec mss ou pyautogui
|
||||
├── capture() # Capture l'écran complet
|
||||
├── get_active_window() # Détecte la fenêtre active
|
||||
├── _capture_mss() # Capture avec mss (rapide)
|
||||
└── _capture_pyautogui()# Capture avec pyautogui (fallback)
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
| Méthode | Temps moyen | Mémoire |
|
||||
|---------|-------------|---------|
|
||||
| mss | ~10-20ms | Faible |
|
||||
| pyautogui | ~50-100ms | Moyenne |
|
||||
|
||||
**Recommandation**: Utiliser `mss` pour les captures fréquentes.
|
||||
|
||||
## Format de Sortie
|
||||
|
||||
- **Type**: `numpy.ndarray`
|
||||
- **Shape**: `(hauteur, largeur, 3)`
|
||||
- **Dtype**: `uint8`
|
||||
- **Ordre des canaux**: RGB (pas BGR)
|
||||
- **Valeurs**: 0-255
|
||||
|
||||
## Gestion d'Erreurs
|
||||
|
||||
```python
|
||||
try:
|
||||
img = capturer.capture()
|
||||
if img is None:
|
||||
print("Capture a échoué")
|
||||
except Exception as e:
|
||||
print(f"Erreur: {e}")
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
# Tester le module
|
||||
python examples/test_screen_capturer.py
|
||||
|
||||
# Résultat attendu:
|
||||
# ✓ Méthode utilisée: mss
|
||||
# ✓ Image capturée: (1080, 1920, 3)
|
||||
# ✓ Format RGB valide
|
||||
# ✓ Fenêtre active détectée
|
||||
```
|
||||
|
||||
## Dépendances
|
||||
|
||||
### Obligatoires
|
||||
- `numpy>=1.24.0`
|
||||
|
||||
### Optionnelles (au moins une requise)
|
||||
- `mss>=9.0.0` (recommandé)
|
||||
- `pyautogui>=0.9.54` (fallback)
|
||||
|
||||
### Pour détection de fenêtre
|
||||
- `pygetwindow>=0.0.9`
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **Multi-écrans**: Capture actuellement le moniteur principal uniquement
|
||||
2. **Fenêtre active**: Peut ne pas fonctionner sur tous les gestionnaires de fenêtres Linux
|
||||
3. **Permissions**: Peut nécessiter des permissions spéciales sur certains systèmes
|
||||
|
||||
## Compatibilité
|
||||
|
||||
- ✅ Linux (X11)
|
||||
- ✅ Linux (Wayland) - avec limitations
|
||||
- ✅ Windows
|
||||
- ✅ macOS
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Erreur: "Neither mss nor pyautogui available"
|
||||
|
||||
```bash
|
||||
pip install mss pyautogui
|
||||
```
|
||||
|
||||
### Erreur: "Captured image has invalid dimensions"
|
||||
|
||||
Vérifier que l'écran est bien détecté:
|
||||
```python
|
||||
import mss
|
||||
with mss.mss() as sct:
|
||||
print(sct.monitors)
|
||||
```
|
||||
|
||||
### Fenêtre active non détectée
|
||||
|
||||
Sur certains systèmes Linux, installer:
|
||||
```bash
|
||||
sudo apt-get install python3-xlib
|
||||
```
|
||||
|
||||
## Exemples Avancés
|
||||
|
||||
### Capture d'une région spécifique
|
||||
|
||||
```python
|
||||
# TODO: À implémenter
|
||||
# capturer.capture_region(x, y, width, height)
|
||||
```
|
||||
|
||||
### Capture avec timestamp
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
|
||||
img = capturer.capture()
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"screenshot_{timestamp}.png"
|
||||
|
||||
Image.fromarray(img).save(filename)
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Support de capture de région spécifique
|
||||
- [ ] Support multi-écrans avec sélection
|
||||
- [ ] Cache de captures pour optimisation
|
||||
- [ ] Compression automatique des images
|
||||
- [ ] Support de formats de sortie alternatifs (JPEG, WebP)
|
||||
|
||||
## Contribution
|
||||
|
||||
Pour améliorer ce module, voir `rpa_vision_v3/docs/specs/tasks.md`.
|
||||
4
core/capture/__init__.py
Normal file
4
core/capture/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Screen capture module"""
|
||||
from .screen_capturer import ScreenCapturer
|
||||
|
||||
__all__ = ['ScreenCapturer']
|
||||
485
core/capture/screen_capturer.py
Normal file
485
core/capture/screen_capturer.py
Normal file
@@ -0,0 +1,485 @@
|
||||
"""
|
||||
Screen Capture Module - Capture d'écran continue pour RPA Vision V3
|
||||
|
||||
Fonctionnalités:
|
||||
- Capture unique ou continue
|
||||
- Buffer circulaire pour historique
|
||||
- Détection de changement d'écran
|
||||
- Support multi-moniteur
|
||||
- Optimisation mémoire
|
||||
"""
|
||||
import numpy as np
|
||||
from typing import Optional, Dict, List, Callable, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import hashlib
|
||||
from PIL import Image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaptureFrame:
|
||||
"""Un frame capturé avec métadonnées"""
|
||||
image: np.ndarray
|
||||
timestamp: datetime
|
||||
frame_id: int
|
||||
hash: str
|
||||
window_info: Optional[Dict] = None
|
||||
changed_from_previous: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaptureStats:
|
||||
"""Statistiques de capture"""
|
||||
total_captures: int = 0
|
||||
captures_per_second: float = 0.0
|
||||
unchanged_frames_skipped: int = 0
|
||||
average_capture_time_ms: float = 0.0
|
||||
buffer_size: int = 0
|
||||
memory_usage_mb: float = 0.0
|
||||
|
||||
|
||||
class ScreenCapturer:
|
||||
"""
|
||||
Capturer d'écran avancé avec mode continu.
|
||||
|
||||
Modes:
|
||||
- Single: Capture unique à la demande
|
||||
- Continuous: Capture en boucle avec callback
|
||||
- Buffered: Maintient un historique des N derniers frames
|
||||
|
||||
Example:
|
||||
>>> capturer = ScreenCapturer(buffer_size=10)
|
||||
>>> # Capture unique
|
||||
>>> frame = capturer.capture()
|
||||
>>> # Mode continu
|
||||
>>> capturer.start_continuous(callback=on_frame, interval_ms=500)
|
||||
>>> # ... plus tard ...
|
||||
>>> capturer.stop_continuous()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
buffer_size: int = 10,
|
||||
detect_changes: bool = True,
|
||||
change_threshold: float = 0.02,
|
||||
monitor_index: int = 1
|
||||
):
|
||||
"""
|
||||
Initialiser le capturer.
|
||||
|
||||
Args:
|
||||
buffer_size: Nombre de frames à garder en mémoire
|
||||
detect_changes: Détecter si l'écran a changé
|
||||
change_threshold: Seuil de changement (0-1)
|
||||
monitor_index: Index du moniteur (1=principal)
|
||||
"""
|
||||
self.buffer_size = buffer_size
|
||||
self.detect_changes = detect_changes
|
||||
self.change_threshold = change_threshold
|
||||
self.monitor_index = monitor_index
|
||||
|
||||
# Buffer circulaire
|
||||
self._buffer: List[CaptureFrame] = []
|
||||
self._frame_counter = 0
|
||||
self._last_hash: Optional[str] = None
|
||||
|
||||
# Mode continu
|
||||
self._continuous_running = False
|
||||
self._continuous_thread: Optional[threading.Thread] = None
|
||||
self._continuous_callback: Optional[Callable[[CaptureFrame], None]] = None
|
||||
self._continuous_interval_ms = 500
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# Stats
|
||||
self._stats = CaptureStats()
|
||||
self._capture_times: List[float] = []
|
||||
|
||||
# Initialiser le backend de capture
|
||||
self._init_capture_backend()
|
||||
|
||||
logger.info(f"ScreenCapturer initialized (buffer={buffer_size}, changes={detect_changes})")
|
||||
|
||||
def _init_capture_backend(self) -> None:
|
||||
"""Initialiser le backend de capture (mss ou pyautogui)."""
|
||||
# Ne plus garder self.sct - créer MSS à chaque capture (Option A - ultra stable)
|
||||
self.sct = None
|
||||
self.pyautogui = None
|
||||
self.method = None
|
||||
|
||||
try:
|
||||
import mss
|
||||
# Test que mss fonctionne sans garder l'instance
|
||||
with mss.mss() as test_sct:
|
||||
pass
|
||||
self.method = "mss"
|
||||
logger.info("Using mss for screen capture (thread-safe mode)")
|
||||
except ImportError:
|
||||
try:
|
||||
import pyautogui
|
||||
self.pyautogui = pyautogui
|
||||
self.method = "pyautogui"
|
||||
logger.info("Using pyautogui for screen capture")
|
||||
except ImportError:
|
||||
raise ImportError("Neither mss nor pyautogui available for screen capture")
|
||||
|
||||
# =========================================================================
|
||||
# Capture unique
|
||||
# =========================================================================
|
||||
|
||||
def capture(self) -> Optional[np.ndarray]:
|
||||
"""
|
||||
Capture unique de l'écran.
|
||||
|
||||
Returns:
|
||||
Screenshot as numpy array (H, W, 3) RGB ou None si erreur
|
||||
"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
if self.method == "mss":
|
||||
img = self._capture_mss()
|
||||
else:
|
||||
img = self._capture_pyautogui()
|
||||
|
||||
# Stats
|
||||
capture_time = (time.time() - start_time) * 1000
|
||||
self._capture_times.append(capture_time)
|
||||
if len(self._capture_times) > 100:
|
||||
self._capture_times.pop(0)
|
||||
self._stats.total_captures += 1
|
||||
self._stats.average_capture_time_ms = sum(self._capture_times) / len(self._capture_times)
|
||||
|
||||
return img
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Capture failed: {e}")
|
||||
return None
|
||||
|
||||
def capture_frame(self) -> Optional[CaptureFrame]:
|
||||
"""
|
||||
Capture avec métadonnées complètes.
|
||||
|
||||
Returns:
|
||||
CaptureFrame avec image, timestamp, hash, etc.
|
||||
"""
|
||||
img = self.capture()
|
||||
return self._create_frame(img)
|
||||
|
||||
def _capture_frame_threaded(self, thread_sct) -> Optional[CaptureFrame]:
|
||||
"""
|
||||
Capture avec instance mss thread-local.
|
||||
|
||||
Args:
|
||||
thread_sct: Instance mss créée dans le thread
|
||||
|
||||
Returns:
|
||||
CaptureFrame ou None
|
||||
"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
if self.method == "mss" and thread_sct:
|
||||
monitor_idx = self.monitor_index if len(thread_sct.monitors) > self.monitor_index else 0
|
||||
monitor = thread_sct.monitors[monitor_idx]
|
||||
sct_img = thread_sct.grab(monitor)
|
||||
img = np.array(sct_img)
|
||||
img = img[:, :, :3][:, :, ::-1] # BGRA to RGB
|
||||
else:
|
||||
img = self._capture_pyautogui()
|
||||
|
||||
# Stats
|
||||
capture_time = (time.time() - start_time) * 1000
|
||||
self._capture_times.append(capture_time)
|
||||
if len(self._capture_times) > 100:
|
||||
self._capture_times.pop(0)
|
||||
self._stats.total_captures += 1
|
||||
self._stats.average_capture_time_ms = sum(self._capture_times) / len(self._capture_times)
|
||||
|
||||
return self._create_frame(img)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Threaded capture failed: {e}")
|
||||
return None
|
||||
|
||||
def _create_frame(self, img: Optional[np.ndarray]) -> Optional[CaptureFrame]:
|
||||
"""Créer un CaptureFrame à partir d'une image."""
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
# Calculer le hash pour détecter les changements
|
||||
img_hash = self._compute_hash(img)
|
||||
changed = True
|
||||
|
||||
if self.detect_changes and self._last_hash:
|
||||
changed = img_hash != self._last_hash
|
||||
if not changed:
|
||||
self._stats.unchanged_frames_skipped += 1
|
||||
|
||||
self._last_hash = img_hash
|
||||
self._frame_counter += 1
|
||||
|
||||
frame = CaptureFrame(
|
||||
image=img,
|
||||
timestamp=datetime.now(),
|
||||
frame_id=self._frame_counter,
|
||||
hash=img_hash,
|
||||
window_info=self.get_active_window(),
|
||||
changed_from_previous=changed
|
||||
)
|
||||
|
||||
# Ajouter au buffer
|
||||
self._add_to_buffer(frame)
|
||||
|
||||
return frame
|
||||
|
||||
def capture_screen(self) -> Optional[Image.Image]:
|
||||
"""
|
||||
Capture et retourne une PIL Image (compatibilité avec ExecutionLoop).
|
||||
|
||||
Returns:
|
||||
PIL Image ou None
|
||||
"""
|
||||
img = self.capture()
|
||||
if img is None:
|
||||
return None
|
||||
return Image.fromarray(img)
|
||||
|
||||
def _capture_mss(self) -> np.ndarray:
|
||||
"""Capture using mss - Option A: créer MSS à chaque capture (ultra stable)."""
|
||||
import mss
|
||||
# Créer une nouvelle instance MSS à chaque capture - zéro surprise, marche dans n'importe quel thread
|
||||
with mss.mss() as sct:
|
||||
monitor_idx = 1 if len(sct.monitors) > 1 else 0
|
||||
monitor = sct.monitors[monitor_idx]
|
||||
sct_img = sct.grab(monitor)
|
||||
|
||||
img = np.array(sct_img)
|
||||
# Convert BGRA to RGB
|
||||
img = img[:, :, :3][:, :, ::-1]
|
||||
|
||||
if img.size == 0 or img.shape[0] == 0 or img.shape[1] == 0:
|
||||
raise ValueError("Captured image has invalid dimensions")
|
||||
|
||||
return img
|
||||
|
||||
def _capture_pyautogui(self) -> np.ndarray:
|
||||
"""Capture using pyautogui."""
|
||||
screenshot = self.pyautogui.screenshot()
|
||||
img = np.array(screenshot)
|
||||
|
||||
if img.size == 0 or img.shape[0] == 0 or img.shape[1] == 0:
|
||||
raise ValueError("Captured image has invalid dimensions")
|
||||
|
||||
return img
|
||||
|
||||
# =========================================================================
|
||||
# Mode continu
|
||||
# =========================================================================
|
||||
|
||||
def start_continuous(
|
||||
self,
|
||||
callback: Callable[[CaptureFrame], None],
|
||||
interval_ms: int = 500,
|
||||
skip_unchanged: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Démarrer la capture continue.
|
||||
|
||||
Args:
|
||||
callback: Fonction appelée pour chaque frame
|
||||
interval_ms: Intervalle entre captures (ms)
|
||||
skip_unchanged: Ne pas appeler callback si écran inchangé
|
||||
|
||||
Returns:
|
||||
True si démarré avec succès
|
||||
"""
|
||||
with self._lock:
|
||||
if self._continuous_running:
|
||||
logger.warning("Continuous capture already running")
|
||||
return False
|
||||
|
||||
self._continuous_callback = callback
|
||||
self._continuous_interval_ms = interval_ms
|
||||
self._skip_unchanged = skip_unchanged
|
||||
self._continuous_running = True
|
||||
|
||||
self._continuous_thread = threading.Thread(
|
||||
target=self._continuous_loop,
|
||||
daemon=True
|
||||
)
|
||||
self._continuous_thread.start()
|
||||
|
||||
logger.info(f"Started continuous capture (interval={interval_ms}ms)")
|
||||
return True
|
||||
|
||||
def stop_continuous(self) -> None:
|
||||
"""Arrêter la capture continue."""
|
||||
with self._lock:
|
||||
self._continuous_running = False
|
||||
|
||||
if self._continuous_thread:
|
||||
self._continuous_thread.join(timeout=2.0)
|
||||
self._continuous_thread = None
|
||||
|
||||
logger.info("Stopped continuous capture")
|
||||
|
||||
def is_continuous_running(self) -> bool:
|
||||
"""Vérifier si la capture continue est active."""
|
||||
return self._continuous_running
|
||||
|
||||
def _continuous_loop(self) -> None:
|
||||
"""Boucle de capture continue (thread)."""
|
||||
last_capture_time = 0
|
||||
captures_in_second = 0
|
||||
second_start = time.time()
|
||||
|
||||
# Créer une nouvelle instance mss pour ce thread (requis pour X11)
|
||||
thread_sct = None
|
||||
if self.method == "mss":
|
||||
import mss
|
||||
thread_sct = mss.mss()
|
||||
|
||||
while self._continuous_running:
|
||||
try:
|
||||
# Capturer avec l'instance thread-local
|
||||
frame = self._capture_frame_threaded(thread_sct)
|
||||
|
||||
if frame:
|
||||
# Calculer FPS
|
||||
captures_in_second += 1
|
||||
if time.time() - second_start >= 1.0:
|
||||
self._stats.captures_per_second = captures_in_second
|
||||
captures_in_second = 0
|
||||
second_start = time.time()
|
||||
|
||||
# Appeler callback si changement ou si on ne skip pas
|
||||
if self._continuous_callback:
|
||||
if frame.changed_from_previous or not self._skip_unchanged:
|
||||
try:
|
||||
self._continuous_callback(frame)
|
||||
except Exception as e:
|
||||
logger.error(f"Callback error: {e}")
|
||||
|
||||
# Attendre l'intervalle
|
||||
elapsed = (time.time() - last_capture_time) * 1000
|
||||
sleep_time = max(0, self._continuous_interval_ms - elapsed) / 1000.0
|
||||
if sleep_time > 0:
|
||||
time.sleep(sleep_time)
|
||||
last_capture_time = time.time()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Continuous capture error: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
# Cleanup thread-local mss
|
||||
if thread_sct:
|
||||
try:
|
||||
thread_sct.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# =========================================================================
|
||||
# Buffer et historique
|
||||
# =========================================================================
|
||||
|
||||
def _add_to_buffer(self, frame: CaptureFrame) -> None:
|
||||
"""Ajouter un frame au buffer circulaire."""
|
||||
with self._lock:
|
||||
self._buffer.append(frame)
|
||||
if len(self._buffer) > self.buffer_size:
|
||||
self._buffer.pop(0)
|
||||
self._stats.buffer_size = len(self._buffer)
|
||||
|
||||
# Calculer utilisation mémoire
|
||||
if self._buffer:
|
||||
frame_size = self._buffer[0].image.nbytes / (1024 * 1024)
|
||||
self._stats.memory_usage_mb = frame_size * len(self._buffer)
|
||||
|
||||
def get_buffer(self) -> List[CaptureFrame]:
|
||||
"""Obtenir une copie du buffer."""
|
||||
with self._lock:
|
||||
return list(self._buffer)
|
||||
|
||||
def get_last_frame(self) -> Optional[CaptureFrame]:
|
||||
"""Obtenir le dernier frame capturé."""
|
||||
with self._lock:
|
||||
return self._buffer[-1] if self._buffer else None
|
||||
|
||||
def get_frame_by_id(self, frame_id: int) -> Optional[CaptureFrame]:
|
||||
"""Obtenir un frame par son ID."""
|
||||
with self._lock:
|
||||
for frame in self._buffer:
|
||||
if frame.frame_id == frame_id:
|
||||
return frame
|
||||
return None
|
||||
|
||||
def clear_buffer(self) -> None:
|
||||
"""Vider le buffer."""
|
||||
with self._lock:
|
||||
self._buffer.clear()
|
||||
self._stats.buffer_size = 0
|
||||
|
||||
# =========================================================================
|
||||
# Utilitaires
|
||||
# =========================================================================
|
||||
|
||||
def _compute_hash(self, img: np.ndarray) -> str:
|
||||
"""Calculer un hash rapide de l'image pour détecter les changements."""
|
||||
# Sous-échantillonner pour un hash rapide
|
||||
small = img[::20, ::20, :].tobytes()
|
||||
return hashlib.md5(small).hexdigest()
|
||||
|
||||
def get_active_window(self) -> Optional[Dict]:
|
||||
"""Obtenir les infos de la fenêtre active."""
|
||||
try:
|
||||
import pygetwindow as gw
|
||||
active = gw.getActiveWindow()
|
||||
if active:
|
||||
return {
|
||||
'title': active.title,
|
||||
'x': active.left,
|
||||
'y': active.top,
|
||||
'width': active.width,
|
||||
'height': active.height,
|
||||
'app': getattr(active, '_app', 'unknown')
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not get active window: {e}")
|
||||
return None
|
||||
|
||||
def get_screen_resolution(self) -> Tuple[int, int]:
|
||||
"""Obtenir la résolution de l'écran."""
|
||||
if self.method == "mss":
|
||||
import mss
|
||||
# Créer une instance temporaire pour obtenir la résolution
|
||||
with mss.mss() as sct:
|
||||
monitor = sct.monitors[1] if len(sct.monitors) > 1 else sct.monitors[0]
|
||||
return (monitor['width'], monitor['height'])
|
||||
else:
|
||||
size = self.pyautogui.size()
|
||||
return (size.width, size.height)
|
||||
|
||||
def get_stats(self) -> CaptureStats:
|
||||
"""Obtenir les statistiques de capture."""
|
||||
return self._stats
|
||||
|
||||
def save_frame(self, frame: CaptureFrame, path: str) -> bool:
|
||||
"""Sauvegarder un frame sur disque."""
|
||||
try:
|
||||
img = Image.fromarray(frame.image)
|
||||
img.save(path)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save frame: {e}")
|
||||
return False
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup."""
|
||||
self.stop_continuous()
|
||||
# Plus besoin de fermer self.sct car nous créons MSS à chaque capture
|
||||
Reference in New Issue
Block a user