[LAB 09 · 检测几何:框、匹配、NMS 与 AP](/course/notebooks/90d213b4e1e04c7e889ee6fc042b2358.html) # 4 · AP 必须说明积分协议 · 实现代码 [独立阅读](/course/notebook-blocks/86f64f86923e48fcaaea1efc4926457c.html) 并入相关学习路径 固定框匹配、PR/AP、NMS误删相邻对象反例 展开完整实现代码 ``` def all_points_ap(recall, precision): """All-points interpolated AP = sum of recall increments * envelope. This is not 11-point AP, the mean of raw precisions, or COCO 101-point AP. A sentinel at recall 1 with precision 0 gives unreached recall zero credit. """ recall, precision = np.asarray(recall, float), np.asarray(precision, float) if recall.shape != precision.shape or np.any(np.diff(recall) < -1e-12): raise ValueError("Aligned arrays and nondecreasing recall required") if np.any((recall < 0) | (recall > 1) | (precision < 0) | (precision > 1)): raise ValueError("Recall and precision must be in [0,1]") mrec = np.r_[0.0, recall, 1.0] mpre = np.r_[0.0, precision, 0.0] envelope = np.maximum.accumulate(mpre[::-1])[::-1] changed = np.where(mrec[1:] > mrec[:-1])[0] ap = np.sum((mrec[changed + 1] - mrec[changed]) * envelope[changed + 1]) return float(ap), mrec, envelope def evaluate_single_class(pred_boxes, scores, gt_boxes, iou_threshold=0.50): """One image / one class: choose the best-IoU GT, then check if already used. VOC-style greedy matching, aligned with LAB10: a used best GT makes the prediction a duplicate FP; do not fall back to a lower-IoU unused GT. Score ties and IoU ties preserve input order. Threshold comparison is >=. No crowd, ignore regions, maxDets, area bins, or dataset pooling. """ if not 0 <= iou_threshold <= 1: raise ValueError("iou_threshold must be in [0,1]") pred_boxes, gt_boxes = boxes_xyxy(pred_boxes), boxes_xyxy(gt_boxes) scores = np.asarray(scores, dtype=float) if scores.shape != (len(pred_boxes),) or not np.isfinite(scores).all(): raise ValueError("Require one finite score per prediction") if len(gt_boxes) == 0: raise ValueError("AP is undefined for this no-GT single-class example") order = np.argsort(-scores, kind="stable") used = np.zeros(len(gt_boxes), dtype=bool) overlaps = box_iou(pred_boxes, gt_boxes) tp, rows = [], [] for rank, pred_i in enumerate(order, 1): gt_i = int(np.argmax(overlaps[pred_i])) best_iou = float(overlaps[pred_i, gt_i]) meets_threshold = best_iou >= iou_threshold is_tp = bool(meets_threshold and not used[gt_i]) if is_tp: used[gt_i] = True reason = "new GT matched" elif meets_threshold: reason = "duplicate: maximum-IoU GT already used; no fallback" else: reason = "IoU below evaluation threshold" tp.append(int(is_tp)) rows.append({"rank": rank, "prediction": int(pred_i + 1), "score": float(scores[pred_i]), "status": "TP" if is_tp else "FP", "gt": gt_i + 1 if is_tp else None, "best_gt": gt_i + 1, "best_iou_any_gt": best_iou, "reason": reason}) tp = np.asarray(tp, dtype=int) cumulative_tp, cumulative_fp = np.cumsum(tp), np.cumsum(1 - tp) recall = cumulative_tp / len(gt_boxes) precision = np.divide(cumulative_tp, np.arange(1, len(tp) + 1), dtype=float) ap, mrec, envelope = all_points_ap(recall, precision) for i, row in enumerate(rows): row.update(cumulative_tp=int(cumulative_tp[i]), cumulative_fp=int(cumulative_fp[i]), recall=float(recall[i]), precision=float(precision[i])) return {"iou_threshold": iou_threshold, "rows": rows, "ap": ap, "recall": recall.tolist(), "precision": precision.tolist(), "envelope_recall": mrec.tolist(), "precision_envelope": envelope.tolist(), "final_recall": float(recall[-1]) if len(recall) else 0.0} for threshold in EVAL_THRESHOLDS: evaluation = evaluate_single_class(PRED, SCORES, GT, threshold) print("IoU threshold:", threshold, "all-points AP:", evaluation["ap"]) for row in evaluation["rows"]: print(row) ```