* Correct confusion matrix calculation-function evaluate_detection_batch * Correct confusion matrix computation * Add 3 more tests for empty Detections-GTs * Minor changes in too-long lines corrected * Replace deprecated mock_detections with _create_detections * Fix indentation in test assertions and add missing `self` parameter in `test_confusion_matrix`. * Update metric computations to improve numerical stability and replace deprecated NumPy functions * Add IoU+class matching tests and synthetic dataset fixtures for detection metrics * Use `Optional` for type hinting `classes` parameter in `_yolo_dataset_factory`. Import `Optional` from `typing`. * Refactor detection metric tests to simplify confusion matrix assertions and enhance IoU+class matching validation. * Apply suggestions from code review * fix(pre_commit): 🎨 auto format pre-commit hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
225 lines
5.2 KiB
Python
225 lines
5.2 KiB
Python
from typing import Optional
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from supervision.detection.core import Detections
|
|
|
|
|
|
@pytest.fixture
|
|
def detections_50_50():
|
|
return Detections(
|
|
xyxy=np.array([[10, 10, 50, 50]], dtype=np.float32),
|
|
confidence=np.array([0.9]),
|
|
class_id=np.array([0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def targets_50_50():
|
|
return Detections(
|
|
xyxy=np.array([[10, 10, 50, 50]], dtype=np.float32),
|
|
class_id=np.array([0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def dummy_prediction():
|
|
return Detections(
|
|
xyxy=np.array([[10, 10, 20, 20]], dtype=np.float32),
|
|
confidence=np.array([0.8]),
|
|
class_id=np.array([0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def predictions_no_overlap():
|
|
return Detections(
|
|
xyxy=np.array([[10, 10, 20, 20]], dtype=np.float32),
|
|
confidence=np.array([0.9]),
|
|
class_id=np.array([0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def targets_no_overlap():
|
|
return Detections(
|
|
xyxy=np.array([[100, 100, 110, 110]], dtype=np.float32),
|
|
class_id=np.array([0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def targets_two_objects_class_0():
|
|
return Detections(
|
|
xyxy=np.array(
|
|
[
|
|
[10, 10, 50, 50],
|
|
[100, 100, 110, 110],
|
|
],
|
|
dtype=np.float32,
|
|
),
|
|
class_id=np.array([0, 0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def predictions_multiple_classes():
|
|
return Detections(
|
|
xyxy=np.array(
|
|
[
|
|
[10, 10, 50, 50], # class 0, matches target
|
|
[60, 60, 100, 100], # class 1, matches target
|
|
[120, 120, 130, 130], # class 1, false positive
|
|
],
|
|
dtype=np.float32,
|
|
),
|
|
confidence=np.array([0.9, 0.8, 0.7]),
|
|
class_id=np.array([0, 1, 1]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def targets_multiple_classes():
|
|
return Detections(
|
|
xyxy=np.array(
|
|
[
|
|
[10, 10, 50, 50], # class 0
|
|
[60, 60, 100, 100], # class 1
|
|
],
|
|
dtype=np.float32,
|
|
),
|
|
class_id=np.array([0, 1]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def predictions_iou_064():
|
|
return Detections(
|
|
xyxy=np.array([[15, 15, 55, 55]], dtype=np.float32),
|
|
confidence=np.array([0.9]),
|
|
class_id=np.array([0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def targets_iou_064():
|
|
return Detections(
|
|
xyxy=np.array([[10, 10, 60, 60]], dtype=np.float32),
|
|
class_id=np.array([0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def predictions_confidence_ranking():
|
|
return Detections(
|
|
xyxy=np.array(
|
|
[
|
|
[10, 10, 50, 50],
|
|
[11, 11, 49, 49],
|
|
],
|
|
dtype=np.float32,
|
|
),
|
|
confidence=np.array([0.6, 0.9]),
|
|
class_id=np.array([0, 0]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def prediction_class_1():
|
|
return Detections(
|
|
xyxy=np.array([[60, 60, 100, 100]], dtype=np.float32),
|
|
confidence=np.array([0.8]),
|
|
class_id=np.array([1]),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def target_class_1():
|
|
return Detections(
|
|
xyxy=np.array([[60, 60, 100, 100]], dtype=np.float32),
|
|
class_id=np.array([1]),
|
|
)
|
|
|
|
|
|
def _yolo_dataset_factory(
|
|
tmp_path,
|
|
num_images: int = 20,
|
|
classes: Optional[list[str]] = None,
|
|
objects_per_image_range: tuple[int, int] = (1, 3),
|
|
):
|
|
"""
|
|
Factory function to create synthetic YOLO-format datasets with custom parameters.
|
|
|
|
Args:
|
|
tmp_path: Pytest tmp_path fixture
|
|
num_images: Number of images to generate
|
|
classes: List of class names
|
|
objects_per_image_range: Range of objects per image as (min, max)
|
|
|
|
Returns:
|
|
dict with dataset paths and metadata
|
|
"""
|
|
from tests.helpers import create_yolo_dataset
|
|
|
|
if classes is None:
|
|
classes = ["dog", "cat", "person"]
|
|
|
|
return create_yolo_dataset(
|
|
dataset_dir=str(tmp_path / "yolo_dataset"),
|
|
num_images=num_images,
|
|
image_size=(640, 640, 3),
|
|
classes=classes,
|
|
objects_per_image_range=objects_per_image_range,
|
|
seed=42,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def yolo_dataset_structure(tmp_path):
|
|
"""
|
|
Synthetic YOLO-format dataset for testing confusion matrix and detection metrics.
|
|
|
|
Configuration:
|
|
- 20 images
|
|
- 640x640 resolution
|
|
- 3 classes: ["dog", "cat", "person"]
|
|
- 1-3 objects per image
|
|
|
|
Use this for tests that need multi-class scenarios (3+ classes).
|
|
|
|
Returns:
|
|
dict with dataset paths and metadata
|
|
"""
|
|
return _yolo_dataset_factory(
|
|
tmp_path,
|
|
num_images=20,
|
|
classes=["dog", "cat", "person"],
|
|
objects_per_image_range=(1, 3),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def yolo_dataset_two_classes(tmp_path):
|
|
"""
|
|
Synthetic YOLO-format dataset for testing mAR and binary classification metrics.
|
|
|
|
Configuration:
|
|
- 15 images
|
|
- 640x640 resolution
|
|
- 2 classes: ["class_0", "class_1"]
|
|
- 2-4 objects per image
|
|
|
|
Use this for tests that specifically need 2-class scenarios or depend on
|
|
specific class distributions (e.g., mAR @ K per-image limiting tests).
|
|
|
|
Returns:
|
|
dict with dataset paths and metadata
|
|
"""
|
|
return _yolo_dataset_factory(
|
|
tmp_path,
|
|
num_images=15,
|
|
classes=["class_0", "class_1"],
|
|
objects_per_image_range=(2, 4),
|
|
)
|