""" 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"])