Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions perceptionmetrics/utils/detection_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,20 @@ def compute_metrics(self) -> Dict[int, Dict[str, float]]:

ap, precision, recall = compute_ap(tps, fps, fn_count)

precision_val = precision[-1] if len(precision) > 0 else 0
recall_val = recall[-1] if len(recall) > 0 else 0

f1_score = (
2 * precision_val * recall_val / (precision_val + recall_val)
if (precision_val + recall_val) > 0
else 0.0
)

metrics[label] = {
"AP": ap,
"Precision": precision[-1] if len(precision) > 0 else 0,
"Recall": recall[-1] if len(recall) > 0 else 0,
"Precision": precision_val,
"Recall": recall_val,
"F1": f1_score,
"TP": sum(tps),
"FP": sum(fps),
"FN": fn_count,
Expand Down Expand Up @@ -301,7 +311,7 @@ def get_overall_precision_recall_curve(self) -> Dict[str, List[float]]:
return {"precision": [0.0], "recall": [0.0]}

fn_count = sum(1 for d in all_detections if d[1] == -1)

# Sort by score
all_detections = sorted(
[d for d in all_detections if d[0] is not None], key=lambda x: -x[0]
Expand Down Expand Up @@ -356,7 +366,7 @@ def get_metrics_dataframe(self, ontology: dict) -> pd.DataFrame:
metrics_dict = {}
class_names = list(ontology.keys())

for metric in ["AP", "Precision", "Recall", "TP", "FP", "FN"]:
for metric in ["AP", "Precision", "Recall", "F1", "TP", "FP", "FN"]:
metrics_dict[metric] = {}
for class_name, class_data in ontology.items():
idx = class_data["idx"]
Expand Down
44 changes: 38 additions & 6 deletions tests/test_detection_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ def test_match_predictions_logic(metrics_factory):
results = matches[1]
assert (0.95, 1) in results, "High overlap prediction should be a True Positive"
assert (0.6, 0) in results, "Zero overlap prediction should be a False Positive"
assert (None, -1) not in results, (
"GT was matched, so there should be no False Negative"
)
assert (
None,
-1,
) not in results, "GT was matched, so there should be no False Negative"


def test_compute_metrics(metrics_factory):
Expand Down Expand Up @@ -138,9 +139,9 @@ def test_compute_coco_map_perfect_match(metrics_factory):
metrics_factory.update(gt_boxes, [1], pred_boxes, [1], [0.9])
coco_map = metrics_factory.compute_coco_map()

assert coco_map == 1.0, (
"Perfect overlap must yield perfect mAP across all thresholds"
)
assert (
coco_map == 1.0
), "Perfect overlap must yield perfect mAP across all thresholds"


def test_compute_coco_map_complex_multi_class(metrics_factory):
Expand Down Expand Up @@ -232,3 +233,34 @@ def test_coco_map_empty_vs_non_empty_class(metrics_factory):

# (AP_Class1 + AP_Class2) / 2 => (1.0 + 0.0) / 2 = 0.5
assert np.isclose(mAP, 0.5), f"Expected mAP of 0.5, but got {mAP}"


def test_f1_score(metrics_factory):
"""
Verify F1 score calculation.
"""

gt_boxes = np.array([[10, 10, 50, 50]])
gt_labels = [1]

pred_boxes = np.array(
[
[12, 12, 48, 48], # TP
[11, 11, 49, 49], # FP (duplicate detection)
]
)

pred_labels = [1, 1]
pred_scores = [0.9, 0.4]

metrics_factory.update(
gt_boxes,
gt_labels,
pred_boxes,
pred_labels,
pred_scores,
)

metrics = metrics_factory.compute_metrics()

assert np.isclose(metrics[1]["F1"], 2 / 3)
Loading