From 6e3001194dfff7ef7cabe9968bfbd9afd7743bf4 Mon Sep 17 00:00:00 2001 From: Balthasar Date: Fri, 18 Jul 2025 17:19:10 +0200 Subject: [PATCH] improve tests --- .../test_mean_average_precision_area.py | 233 ++++++------------ 1 file changed, 70 insertions(+), 163 deletions(-) diff --git a/test/metrics/test_mean_average_precision_area.py b/test/metrics/test_mean_average_precision_area.py index 17257701..23e9f940 100644 --- a/test/metrics/test_mean_average_precision_area.py +++ b/test/metrics/test_mean_average_precision_area.py @@ -1,5 +1,6 @@ from __future__ import annotations +import pytest import numpy as np from supervision.detection.core import Detections @@ -9,191 +10,97 @@ from supervision.metrics.mean_average_precision import MeanAveragePrecision class TestMeanAveragePrecisionArea: """Test area calculation in MeanAveragePrecision.""" - def test_area_calculated_from_bbox_when_data_empty(self): - """Test that area is calculated from bbox when data is empty (normal case).""" - # Create detections with empty data (normal case) - gt = Detections( - xyxy=np.array( - [ - [10, 10, 40, 40], # Small: 30x30 = 900 - [100, 100, 200, 150], # Medium: 100x50 = 5000 - [300, 300, 500, 400], # Large: 200x100 = 20000 - ], - dtype=np.float32, + @pytest.mark.parametrize( + "xyxy, expected_areas, expected_size_maps", + [ + ( + np.array([ + [10, 10, 40, 40], # Small: 900 + [100, 100, 200, 150], # Medium: 5000 + [300, 300, 500, 400], # Large: 20000 + ], dtype=np.float32), + [900.0, 5000.0, 20000.0], + {"small": True, "medium": True, "large": True} ), - class_id=np.array([0, 0, 0]), - confidence=np.array([1.0, 1.0, 1.0]), + ( + np.array([[0, 0, 10, 10]], dtype=np.float32), # Small: 100 + [100.0], + {"small": True, "medium": False, "large": False} + ), + ( + np.array([[0, 0, 50, 50]], dtype=np.float32), # Medium: 2500 + [2500.0], + {"small": False, "medium": True, "large": False} + ), + ( + np.array([[0, 0, 100, 100]], dtype=np.float32), # Large: 10000 + [10000.0], + {"small": False, "medium": False, "large": True} + ), + ] + ) + def test_area_calculation_and_size_specific_map(self, xyxy, expected_areas, expected_size_maps): + """Test area calculation and size-specific mAP functionality.""" + gt = Detections( + xyxy=xyxy, + class_id=np.arange(len(xyxy)), ) - pred = Detections( xyxy=gt.xyxy.copy(), class_id=gt.class_id.copy(), - confidence=np.array([0.9, 0.9, 0.9]), + confidence=np.full(len(xyxy), 0.9), ) - # Verify data is empty (normal case) - assert gt.data == {} - assert pred.data == {} - - # Create mAP metric and test area calculation map_metric = MeanAveragePrecision() map_metric.update([pred], [gt]) - # Check that areas were calculated correctly from bbox - prepared_targets = map_metric._prepare_targets(map_metric._targets_list) - - areas = [ann["area"] for ann in prepared_targets["annotations"]] - expected_areas = [900.0, 5000.0, 20000.0] # width * height for each bbox - - assert np.allclose(areas, expected_areas, rtol=1e-05, atol=1e-08), ( - f"Expected {expected_areas}, got {areas}" - ) - - # Verify mAP works correctly (no -1.0 for medium/large objects) - result = map_metric.compute() - assert result.medium_objects.map50 >= 0.0, ( - "Medium objects should have valid mAP" - ) - assert result.large_objects.map50 >= 0.0, "Large objects should have valid mAP" - - def test_area_preserved_when_provided_in_data(self): - """Test that area from data is preserved when provided (COCO case).""" - # Create detections with area in data (COCO style) - gt = Detections( - xyxy=np.array([[100, 100, 200, 150]], dtype=np.float32), # Would be 5000 - class_id=np.array([0]), - confidence=np.array([1.0]), - ) - - # Add custom area to data (different from calculated) - gt.data = {"area": np.array([3000.0])} - - pred = Detections( - xyxy=gt.xyxy.copy(), class_id=gt.class_id.copy(), confidence=np.array([0.9]) - ) - pred.data = {"area": np.array([3000.0])} - # Test area calculation - map_metric = MeanAveragePrecision() - map_metric.update([pred], [gt]) - - # Check that provided area is used (not calculated) - prepared_targets = map_metric._prepare_targets(map_metric._targets_list) - used_area = prepared_targets["annotations"][0]["area"] - - assert np.allclose(used_area, 3000.0, rtol=1e-05, atol=1e-08), ( - f"Should use provided area 3000.0, got {used_area}" - ) - - # Verify it's different from what would be calculated - calculated_area = (200 - 100) * (150 - 100) # 100 * 50 = 5000 - assert not np.allclose(used_area, calculated_area, rtol=1e-05, atol=1e-08), ( - "Should use provided area, not calculated" - ) - - def test_mixed_area_sources(self): - """Test mix of detections with and without area in data.""" - # Create detections where some have area in data, others don't - gt1 = Detections( - xyxy=np.array([[10, 10, 40, 40]], dtype=np.float32), # 900 - class_id=np.array([0]), - ) - # No area in data - should be calculated - - gt2 = Detections( - xyxy=np.array([[100, 100, 200, 150]], dtype=np.float32), # 5000 - class_id=np.array([1]), - ) - # Add area in data - should be preserved - gt2.data = {"area": np.array([3000.0])} - - pred1 = Detections( - xyxy=gt1.xyxy.copy(), - class_id=gt1.class_id.copy(), - confidence=np.array([0.9]), - ) - - pred2 = Detections( - xyxy=gt2.xyxy.copy(), - class_id=gt2.class_id.copy(), - confidence=np.array([0.8]), - ) - pred2.data = {"area": np.array([3000.0])} - - # Test area calculation for mixed sources - map_metric = MeanAveragePrecision() - map_metric.update([pred1, pred2], [gt1, gt2]) - prepared_targets = map_metric._prepare_targets(map_metric._targets_list) areas = [ann["area"] for ann in prepared_targets["annotations"]] + assert np.allclose(areas, expected_areas), f"Expected {expected_areas}, got {areas}" - expected_areas = [900.0, 3000.0] # calculated, then provided - assert np.allclose(areas, expected_areas, rtol=1e-05, atol=1e-08), ( - f"Expected {expected_areas}, got {areas}" - ) + # Test size-specific mAP + result = map_metric.compute() - def test_size_specific_map_works_correctly(self): - """Test that size-specific mAP works correctly with area fix.""" - # Create detections with one object of each size + if expected_size_maps["small"]: + assert result.small_objects.map50 > 0.9, "Small objects should have high mAP" + else: + assert result.small_objects.map50 == -1.0, "Small objects should have no data" + + if expected_size_maps["medium"]: + assert result.medium_objects.map50 > 0.9, "Medium objects should have high mAP" + else: + assert result.medium_objects.map50 == -1.0, "Medium objects should have no data" + + if expected_size_maps["large"]: + assert result.large_objects.map50 > 0.9, "Large objects should have high mAP" + else: + assert result.large_objects.map50 == -1.0, "Large objects should have no data" + + def test_area_preserved_from_data(self): + """Test that area from data field is preserved (COCO case).""" gt = Detections( - xyxy=np.array( - [ - [10, 10, 40, 40], # Small: 30x30 = 900 < 1024 - [100, 100, 200, 150], # Medium: 100x50 = 5000 (1024 <= x < 9216) - [300, 300, 500, 400], # Large: 200x100 = 20000 >= 9216 - ], - dtype=np.float32, - ), - class_id=np.array([0, 0, 0]), + xyxy=np.array([[100, 100, 200, 150]], dtype=np.float32), # Would calculate to 5000 + class_id=np.array([0]), ) - - # Perfect predictions + # Override with custom area + gt.data = {"area": np.array([3000.0])} + pred = Detections( xyxy=gt.xyxy.copy(), class_id=gt.class_id.copy(), - confidence=np.array([0.9, 0.9, 0.9]), + confidence=np.array([0.9]), ) - - # Test mAP calculation + pred.data = {"area": np.array([3000.0])} + map_metric = MeanAveragePrecision() map_metric.update([pred], [gt]) - result = map_metric.compute() - - # All size categories should have valid results (not -1.0) - assert result.small_objects.map50 >= 0.0, "Small objects should have valid mAP" - assert result.medium_objects.map50 >= 0.0, ( - "Medium objects should have valid mAP" - ) - assert result.large_objects.map50 >= 0.0, "Large objects should have valid mAP" - - # Perfect matches should yield high mAP for medium and large - assert result.medium_objects.map50 > 0.9, ( - "Perfect medium matches should have high mAP" - ) - assert result.large_objects.map50 > 0.9, ( - "Perfect large matches should have high mAP" - ) - - def test_area_uses_detections_property(self): - """Test that area calculation uses Detections.area property correctly.""" - # Create detection - gt = Detections( - xyxy=np.array([[100, 100, 200, 150]], dtype=np.float32), - class_id=np.array([0]), - ) - - pred = Detections( - xyxy=gt.xyxy.copy(), class_id=gt.class_id.copy(), confidence=np.array([0.9]) - ) - - # Test that internal calculation matches Detections.area property - map_metric = MeanAveragePrecision() - map_metric.update([pred], [gt]) - + prepared_targets = map_metric._prepare_targets(map_metric._targets_list) used_area = prepared_targets["annotations"][0]["area"] - expected_area = gt.area[0] - - assert np.allclose(used_area, expected_area, rtol=1e-05, atol=1e-08), ( - f"Should use Detections.area property {expected_area}, got {used_area}" - ) + + assert np.allclose(used_area, 3000.0), f"Should use provided area 3000.0, got {used_area}" + + # Verify it's different from what would be calculated + calculated_area = (200 - 100) * (150 - 100) # 100 * 50 = 5000 + assert not np.allclose(used_area, calculated_area), "Should use provided area, not calculated"