File size: 5,198 Bytes
11246bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | """
Core evaluation metrics for face detection.
Implements:
- IoU computation (pairwise and matrix)
- Average Precision (AP) with VOC-style 11-point interpolation
- Recall at various IoU thresholds
- WiderFace evaluation protocol helpers
"""
import numpy as np
from typing import List, Tuple, Optional
def compute_iou_matrix(boxes1: np.ndarray, boxes2: np.ndarray) -> np.ndarray:
"""
Compute pairwise IoU between two sets of boxes.
Args:
boxes1: [N, 4] (x1, y1, x2, y2)
boxes2: [M, 4] (x1, y1, x2, y2)
Returns:
[N, M] IoU matrix
"""
x1 = np.maximum(boxes1[:, 0:1], boxes2[:, 0:1].T)
y1 = np.maximum(boxes1[:, 1:2], boxes2[:, 1:2].T)
x2 = np.minimum(boxes1[:, 2:3], boxes2[:, 2:3].T)
y2 = np.minimum(boxes1[:, 3:4], boxes2[:, 3:4].T)
inter = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
union = area1[:, None] + area2[None, :] - inter
return inter / (union + 1e-6)
def compute_ap(recall: np.ndarray, precision: np.ndarray,
use_11_point: bool = True) -> float:
"""
Compute Average Precision from recall-precision curve.
WiderFace uses 11-point interpolation (VOC2007 style).
Args:
recall: [N] sorted recall values
precision: [N] corresponding precision values
use_11_point: Use 11-point interpolation (default: True)
Returns:
AP value
"""
if use_11_point:
# 11-point interpolation
ap = 0.0
for t in np.arange(0, 1.1, 0.1):
if np.sum(recall >= t) == 0:
p = 0
else:
p = np.max(precision[recall >= t])
ap += p / 11
return ap
else:
# All-point interpolation (VOC2010+ style)
mrec = np.concatenate(([0.0], recall, [1.0]))
mpre = np.concatenate(([0.0], precision, [0.0]))
# Make precision monotonically decreasing
for i in range(len(mpre) - 1, 0, -1):
mpre[i - 1] = max(mpre[i - 1], mpre[i])
# Compute area under curve
idx = np.where(mrec[1:] != mrec[:-1])[0]
ap = np.sum((mrec[idx + 1] - mrec[idx]) * mpre[idx + 1])
return ap
def compute_recall_at_iou(pred_boxes: np.ndarray, pred_scores: np.ndarray,
gt_boxes: np.ndarray, iou_threshold: float = 0.5
) -> Tuple[float, np.ndarray, np.ndarray]:
"""
Compute recall and precision at a given IoU threshold.
Args:
pred_boxes: [N, 4] predicted boxes sorted by score (descending)
pred_scores: [N] prediction scores
gt_boxes: [M, 4] ground truth boxes
iou_threshold: IoU threshold for matching
Returns:
(ap, recall_curve, precision_curve)
"""
num_gt = gt_boxes.shape[0]
if num_gt == 0:
return 0.0, np.array([]), np.array([])
# Sort by score
order = np.argsort(-pred_scores)
pred_boxes = pred_boxes[order]
iou_matrix = compute_iou_matrix(pred_boxes, gt_boxes)
# Greedy matching
gt_matched = np.zeros(num_gt, dtype=bool)
tp = np.zeros(len(pred_boxes))
fp = np.zeros(len(pred_boxes))
for i in range(len(pred_boxes)):
if iou_matrix.shape[1] > 0:
best_gt = iou_matrix[i].argmax()
if iou_matrix[i, best_gt] >= iou_threshold and not gt_matched[best_gt]:
tp[i] = 1
gt_matched[best_gt] = True
else:
fp[i] = 1
else:
fp[i] = 1
tp_cumsum = np.cumsum(tp)
fp_cumsum = np.cumsum(fp)
recall = tp_cumsum / num_gt
precision = tp_cumsum / (tp_cumsum + fp_cumsum)
ap = compute_ap(recall, precision)
return ap, recall, precision
def match_detections_to_gt(pred_boxes: np.ndarray, gt_boxes: np.ndarray,
iou_threshold: float = 0.5
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Match predictions to ground truth for detailed analysis.
Returns:
(tp_mask, fp_mask, fn_indices)
tp_mask: [N] boolean, True for true positives
fp_mask: [N] boolean, True for false positives
fn_indices: indices of unmatched GT boxes (false negatives)
"""
if len(pred_boxes) == 0:
return (np.array([], dtype=bool),
np.array([], dtype=bool),
np.arange(len(gt_boxes)))
if len(gt_boxes) == 0:
return (np.zeros(len(pred_boxes), dtype=bool),
np.ones(len(pred_boxes), dtype=bool),
np.array([], dtype=int))
iou_matrix = compute_iou_matrix(pred_boxes, gt_boxes)
gt_matched = np.zeros(len(gt_boxes), dtype=bool)
tp_mask = np.zeros(len(pred_boxes), dtype=bool)
for i in range(len(pred_boxes)):
best_gt = iou_matrix[i].argmax()
if iou_matrix[i, best_gt] >= iou_threshold and not gt_matched[best_gt]:
tp_mask[i] = True
gt_matched[best_gt] = True
fp_mask = ~tp_mask
fn_indices = np.where(~gt_matched)[0]
return tp_mask, fp_mask, fn_indices
|