Files
rpa_vision_v3/tests/unit/test_roi_optimizer.py
Dom a27b74cf22 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>
2026-01-29 11:23:51 +01:00

275 lines
9.0 KiB
Python

"""
Tests pour ROI Optimizer
Valide:
- Redimensionnement intelligent des screenshots
- Détection rapide des ROIs
- Cache des résultats
- Fusion des ROIs qui se chevauchent
"""
import pytest
import numpy as np
import cv2
from pathlib import Path
import tempfile
import shutil
from core.detection.roi_optimizer import (
ROIOptimizer,
ROICache,
ROI,
OptimizedFrame
)
class TestROIOptimizer:
"""Tests pour l'optimiseur ROI"""
def setup_method(self):
"""Setup avant chaque test"""
self.temp_dir = Path(tempfile.mkdtemp())
self.optimizer = ROIOptimizer(
max_width=1920,
max_height=1080,
enable_cache=True,
cache_size=10
)
def teardown_method(self):
"""Cleanup après chaque test"""
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
def _create_test_image(self, width: int, height: int) -> str:
"""Créer une image de test"""
# Créer une image avec quelques formes
image = np.ones((height, width, 3), dtype=np.uint8) * 255
# Ajouter des rectangles (simulent des boutons)
cv2.rectangle(image, (50, 50), (150, 100), (0, 0, 255), -1)
cv2.rectangle(image, (200, 50), (300, 100), (0, 255, 0), -1)
cv2.rectangle(image, (50, 150), (150, 200), (255, 0, 0), -1)
# Ajouter du texte
cv2.putText(image, "Button 1", (60, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
cv2.putText(image, "Button 2", (210, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
# Sauvegarder
path = self.temp_dir / f"test_{width}x{height}.png"
cv2.imwrite(str(path), image)
return str(path)
def test_resize_small_image(self):
"""Test redimensionnement d'une petite image (pas de resize)"""
# Créer une petite image
image_path = self._create_test_image(800, 600)
# Optimiser
optimized = self.optimizer.optimize_frame(image_path)
# Vérifier qu'il n'y a pas eu de redimensionnement
assert optimized.scale_factor == 1.0
assert optimized.resized_size == (800, 600)
assert optimized.original_size == (800, 600)
def test_resize_large_image(self):
"""Test redimensionnement d'une grande image"""
# Créer une grande image
image_path = self._create_test_image(2560, 1440)
# Optimiser
optimized = self.optimizer.optimize_frame(image_path)
# Vérifier qu'il y a eu redimensionnement
assert optimized.scale_factor < 1.0
assert optimized.resized_size[0] <= 1920
assert optimized.resized_size[1] <= 1080
assert optimized.original_size == (2560, 1440)
def test_roi_detection(self):
"""Test détection des ROIs"""
# Créer une image avec des éléments
image_path = self._create_test_image(800, 600)
# Optimiser
optimized = self.optimizer.optimize_frame(image_path)
# Vérifier qu'on a détecté des ROIs
assert len(optimized.rois) > 0
# Vérifier que les ROIs sont valides
for roi in optimized.rois:
assert roi.x >= 0
assert roi.y >= 0
assert roi.w > 0
assert roi.h > 0
assert 0.0 <= roi.confidence <= 1.0
assert roi.roi_type in ["contour", "text", "merged", "full_frame"]
def test_cache_hit(self):
"""Test cache hit sur même image"""
# Créer une image
image_path = self._create_test_image(800, 600)
# Premier appel (cache miss)
optimized1 = self.optimizer.optimize_frame(image_path)
cache_stats1 = self.optimizer.cache.get_stats()
assert cache_stats1["misses"] == 1
assert cache_stats1["hits"] == 0
# Deuxième appel (cache hit)
optimized2 = self.optimizer.optimize_frame(image_path)
cache_stats2 = self.optimizer.cache.get_stats()
assert cache_stats2["hits"] == 1
assert cache_stats2["misses"] == 1
# Vérifier que les ROIs sont les mêmes
assert len(optimized1.rois) == len(optimized2.rois)
def test_cache_miss_different_images(self):
"""Test cache miss sur images différentes"""
# Créer deux images différentes
image_path1 = self._create_test_image(800, 600)
image_path2 = self._create_test_image(1024, 768)
# Premier appel
self.optimizer.optimize_frame(image_path1)
# Deuxième appel avec image différente
self.optimizer.optimize_frame(image_path2)
# Vérifier les stats
cache_stats = self.optimizer.cache.get_stats()
assert cache_stats["misses"] == 2
assert cache_stats["hits"] == 0
def test_scale_coordinates(self):
"""Test conversion de coordonnées"""
# Test avec scale_factor = 0.5
x_orig, y_orig = self.optimizer.scale_coordinates(100, 200, 0.5)
assert x_orig == 200
assert y_orig == 400
# Test avec scale_factor = 1.0 (pas de scaling)
x_orig, y_orig = self.optimizer.scale_coordinates(100, 200, 1.0)
assert x_orig == 100
assert y_orig == 200
def test_roi_merge(self):
"""Test fusion de ROIs qui se chevauchent"""
# Créer des ROIs qui se chevauchent fortement
roi1 = ROI(x=10, y=10, w=50, h=50, confidence=0.9, roi_type="contour")
roi2 = ROI(x=20, y=20, w=50, h=50, confidence=0.8, roi_type="contour") # Chevauchement plus important
roi3 = ROI(x=200, y=200, w=50, h=50, confidence=0.9, roi_type="contour")
rois = [roi1, roi2, roi3]
# Fusionner avec seuil bas
merged = self.optimizer._merge_overlapping_rois(rois, iou_threshold=0.2)
# Devrait avoir fusionné roi1 et roi2, mais pas roi3
assert len(merged) == 2
# Vérifier qu'un des ROIs fusionnés contient les deux originaux
merged_areas = [r.w * r.h for r in merged]
# Le ROI fusionné devrait être plus grand que les originaux
assert any(area > 2500 for area in merged_areas) # 50*50 = 2500
def test_stats(self):
"""Test statistiques de l'optimiseur"""
# Créer et traiter quelques images
for i in range(3):
image_path = self._create_test_image(800 + i * 100, 600)
self.optimizer.optimize_frame(image_path)
# Obtenir les stats
stats = self.optimizer.get_stats()
# Vérifier
assert stats["total_frames_processed"] == 3
assert "avg_processing_time_ms" in stats
assert "cache" in stats
assert stats["cache"]["size"] == 3
class TestROICache:
"""Tests pour le cache ROI"""
def test_cache_put_get(self):
"""Test ajout et récupération"""
cache = ROICache(max_size=5)
# Créer une image de test
image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
# Créer des ROIs
rois = [
ROI(x=10, y=10, w=50, h=50, confidence=0.9, roi_type="test")
]
# Ajouter au cache
cache.put(image, rois, processing_time=0.1)
# Récupérer
cached_rois = cache.get(image)
# Vérifier
assert cached_rois is not None
assert len(cached_rois) == 1
assert cached_rois[0].x == 10
def test_cache_miss(self):
"""Test cache miss"""
cache = ROICache(max_size=5)
# Créer une image
image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
# Essayer de récupérer (devrait être None)
cached_rois = cache.get(image)
assert cached_rois is None
def test_cache_eviction(self):
"""Test éviction LRU"""
cache = ROICache(max_size=3)
# Ajouter 4 images (devrait évincer la première)
for i in range(4):
image = np.ones((100, 100, 3), dtype=np.uint8) * i
rois = [ROI(x=i, y=i, w=10, h=10, confidence=0.9, roi_type="test")]
cache.put(image, rois)
# Vérifier la taille
assert len(cache.cache) == 3
def test_cache_stats(self):
"""Test statistiques du cache"""
cache = ROICache(max_size=5)
# Créer une image
image = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
rois = [ROI(x=10, y=10, w=50, h=50, confidence=0.9, roi_type="test")]
# Miss
cache.get(image)
# Put
cache.put(image, rois, processing_time=0.1)
# Hit
cache.get(image)
# Vérifier stats
stats = cache.get_stats()
assert stats["hits"] == 1
assert stats["misses"] == 1
assert stats["hit_rate"] == 0.5
assert stats["total_time_saved_ms"] == 100.0 # 0.1s = 100ms
if __name__ == "__main__":
pytest.main([__file__, "-v"])