MidasMap / tests /test_evaluate.py
AnikS22's picture
Sync all source code, docs, and configs
1fc7794 verified
Raw
History Blame Contribute Delete
3.98 kB
"""Unit tests for evaluation matching and metrics."""
import numpy as np
import pytest
from src.evaluate import match_detections_to_gt, compute_f1, compute_average_precision
class TestComputeF1:
def test_perfect_score(self):
f1, p, r = compute_f1(10, 0, 0)
assert f1 == pytest.approx(1.0, abs=0.001)
assert p == pytest.approx(1.0, abs=0.001)
assert r == pytest.approx(1.0, abs=0.001)
def test_zero_detections(self):
f1, p, r = compute_f1(0, 0, 10)
assert f1 == pytest.approx(0.0, abs=0.01)
assert r == pytest.approx(0.0, abs=0.01)
def test_all_false_positives(self):
f1, p, r = compute_f1(0, 10, 0)
assert p == pytest.approx(0.0, abs=0.01)
class TestMatchDetections:
def test_perfect_matching(self):
"""Detections at exact GT locations should all match."""
gt_6nm = np.array([[100.0, 100.0], [200.0, 200.0]])
gt_12nm = np.array([[300.0, 300.0]])
dets = [
{"x": 100.0, "y": 100.0, "class": "6nm", "conf": 0.9},
{"x": 200.0, "y": 200.0, "class": "6nm", "conf": 0.8},
{"x": 300.0, "y": 300.0, "class": "12nm", "conf": 0.7},
]
results = match_detections_to_gt(dets, gt_6nm, gt_12nm)
assert results["6nm"]["tp"] == 2
assert results["6nm"]["fp"] == 0
assert results["6nm"]["fn"] == 0
assert results["12nm"]["tp"] == 1
def test_wrong_class_no_match(self):
"""Detection near GT but wrong class should not match."""
gt_6nm = np.array([[100.0, 100.0]])
gt_12nm = np.empty((0, 2))
dets = [
{"x": 100.0, "y": 100.0, "class": "12nm", "conf": 0.9},
]
results = match_detections_to_gt(dets, gt_6nm, gt_12nm)
assert results["6nm"]["fn"] == 1 # missed
assert results["12nm"]["fp"] == 1 # false positive
def test_beyond_radius_no_match(self):
"""Detection beyond match radius should not match."""
gt_6nm = np.array([[100.0, 100.0]])
gt_12nm = np.empty((0, 2))
dets = [
{"x": 120.0, "y": 100.0, "class": "6nm", "conf": 0.9}, # 20px away > 9px radius
]
results = match_detections_to_gt(
dets, gt_6nm, gt_12nm, match_radii={"6nm": 9.0, "12nm": 15.0}
)
assert results["6nm"]["tp"] == 0
assert results["6nm"]["fp"] == 1
assert results["6nm"]["fn"] == 1
def test_within_radius_matches(self):
"""Detection within match radius should match."""
gt_6nm = np.array([[100.0, 100.0]])
gt_12nm = np.empty((0, 2))
dets = [
{"x": 105.0, "y": 100.0, "class": "6nm", "conf": 0.9}, # 5px away < 9px
]
results = match_detections_to_gt(
dets, gt_6nm, gt_12nm, match_radii={"6nm": 9.0, "12nm": 15.0}
)
assert results["6nm"]["tp"] == 1
def test_no_detections(self):
"""No detections: all GT are false negatives."""
gt_6nm = np.array([[100.0, 100.0], [200.0, 200.0]])
results = match_detections_to_gt([], gt_6nm, np.empty((0, 2)))
assert results["6nm"]["fn"] == 2
assert results["6nm"]["f1"] == pytest.approx(0.0, abs=0.01)
def test_no_ground_truth(self):
"""No GT: all detections are false positives."""
dets = [{"x": 100.0, "y": 100.0, "class": "6nm", "conf": 0.9}]
results = match_detections_to_gt(dets, np.empty((0, 2)), np.empty((0, 2)))
assert results["6nm"]["fp"] == 1
class TestAveragePrecision:
def test_perfect_ap(self):
"""All detections match in rank order → AP = 1.0."""
gt = np.array([[100.0, 100.0], [200.0, 200.0]])
dets = [
{"x": 100.0, "y": 100.0, "class": "6nm", "conf": 0.9},
{"x": 200.0, "y": 200.0, "class": "6nm", "conf": 0.8},
]
ap = compute_average_precision(dets, gt, match_radius=9.0)
assert ap == pytest.approx(1.0, abs=0.01)