[LAB 09 · 检测几何:框、匹配、NMS 与 AP](/course/notebooks/90d213b4e1e04c7e889ee6fc042b2358.html) # 6 · 反例:相邻真实对象被 NMS 合并 · 实现代码 [独立阅读](/course/notebook-blocks/e4240ba681bd49f3871529d076ed4b97.html) 并入相关学习路径 固定框匹配、PR/AP、NMS误删相邻对象反例 展开完整实现代码 ``` GT = np.array([[0, 0, 2, 2], [3, 0, 5, 2]], dtype=float) PRED = np.array([[0, 0, 2, 2], [.2, 0, 2.2, 2], [3.4, 0, 5.4, 2], [7, 7, 8, 8]], float) SCORES = np.array([.9, .8, .7, .4]) CLASSES = np.zeros(4, dtype=int) iou_matrix = box_iou(PRED, GT) training_assignment, forced_anchors = assign_training_anchors(PRED, GT, positive_iou=.5) baseline = {f"AP{round(t * 100)}": evaluate_single_class(PRED, SCORES, GT, t) for t in EVAL_THRESHOLDS} kept, suppressed = class_aware_nms(PRED, SCORES, CLASSES, NMS_THRESHOLD) after_nms = {f"AP{round(t * 100)}": evaluate_single_class(PRED[kept], SCORES[kept], GT, t) for t in EVAL_THRESHOLDS} for result in after_nms.values(): for row in result["rows"]: row["prediction"] = int(kept[row["prediction"] - 1] + 1) # Two real, same-class objects overlap by IoU=0.6. Both predictions are exact. NEIGHBOR_GT = np.array([[0, 0, 2, 2], [.5, 0, 2.5, 2]], float) NEIGHBOR_SCORES = np.array([.95, .90]) neighbor_kept, neighbor_suppressed = class_aware_nms( NEIGHBOR_GT, NEIGHBOR_SCORES, [0, 0], NMS_THRESHOLD) neighbor_before = evaluate_single_class(NEIGHBOR_GT, NEIGHBOR_SCORES, NEIGHBOR_GT, .5) neighbor_after = evaluate_single_class(NEIGHBOR_GT[neighbor_kept], NEIGHBOR_SCORES[neighbor_kept], NEIGHBOR_GT, .5) neighbor_kept_070, _ = class_aware_nms(NEIGHBOR_GT, NEIGHBOR_SCORES, [0, 0], .70) # A forced training positive can fall below the positive IoU threshold. LOW_OVERLAP_ANCHORS = np.array([[0, 0, .7, .7], [.8, 0, .9, .1], [3, 0, 3.6, .6]]) low_assignment, low_forced = assign_training_anchors(LOW_OVERLAP_ANCHORS, GT, .5) # Protocol counterexample: two exact copies of GT1, while GT2 overlaps it by 0.6. # The production evaluator above must NOT reassign a duplicate to second-best GT2. DUPLICATE_NEIGHBOR_PRED = np.repeat(NEIGHBOR_GT[:1], 2, axis=0) protocol_standard = evaluate_single_class(DUPLICATE_NEIGHBOR_PRED, NEIGHBOR_SCORES, NEIGHBOR_GT, .5) # The following trace deliberately implements the OTHER protocol for comparison. alternative_used = np.zeros(len(NEIGHBOR_GT), dtype=bool) alternative_tp = [] alternative_ious = box_iou(DUPLICATE_NEIGHBOR_PRED, NEIGHBOR_GT) for pred_i in np.argsort(-NEIGHBOR_SCORES, kind="stable"): eligible = np.flatnonzero((~alternative_used) & (alternative_ious[pred_i] >= .5)) if len(eligible): gt_i = int(eligible[np.argmax(alternative_ious[pred_i, eligible])]) alternative_used[gt_i] = True alternative_tp.append(int(len(eligible) > 0)) alternative_recall = np.cumsum(alternative_tp) / len(NEIGHBOR_GT) alternative_precision = np.cumsum(alternative_tp) / np.arange(1, len(alternative_tp) + 1) alternative_ap = all_points_ap(alternative_recall, alternative_precision)[0] # Meaningful invariants and protocol boundaries, not a network benchmark. assert iou_matrix.shape == (4, 2) np.testing.assert_allclose(iou_matrix, [[1, 0], [9 / 11, 0], [0, 2 / 3], [0, 0]]) assert training_assignment.tolist() == [0, 0, 1, -1] assert [r["status"] for r in baseline["AP50"]["rows"]] == ["TP", "FP", "TP", "FP"] assert [r["status"] for r in baseline["AP75"]["rows"]] == ["TP", "FP", "FP", "FP"] np.testing.assert_allclose([baseline["AP50"]["ap"], baseline["AP75"]["ap"]], [5 / 6, .5]) assert kept.tolist() == [0, 2, 3] np.testing.assert_allclose([after_nms["AP50"]["ap"], after_nms["AP75"]["ap"]], [1, .5]) np.testing.assert_allclose([neighbor_before["ap"], neighbor_after["ap"]], [1, .5]) assert neighbor_kept_070.tolist() == [0, 1] assert low_assignment.tolist() == [0, -1, 1] and low_forced.tolist() == [True, False, True] assert class_aware_nms([[0, 0, 2, 2]] * 2, [.9, .8], [0, 1], .5)[0].tolist() == [0, 1] assert class_aware_nms([[0, 0, 2, 2]] * 2, [.9, .8], [0, 0], 1.0)[0].tolist() == [0, 1] assert box_iou([[0, 0, 0, 0]], [[0, 0, 0, 0]]).item() == 0 assert evaluate_single_class([], [], GT, .5)["ap"] == 0 assert [r["status"] for r in protocol_standard["rows"]] == ["TP", "FP"] assert alternative_tp == [1, 1] np.testing.assert_allclose([protocol_standard["ap"], alternative_ap], [.5, 1.0]) results = { "data_identity": "deterministic teaching example; no model trained or inferred", "coordinates": "continuous xyxy, no +1", "ap_protocol": "all-points precision envelope; single image / one class; not COCO mAP", "matching_protocol": "VOC-style: maximum-IoU same-image/class GT, then used check; no fallback; IoU >= threshold", "nms_protocol": {"class_aware": True, "suppress_if": "IoU > threshold", "threshold": NMS_THRESHOLD}, "input_shapes": {"gt": list(GT.shape), "pred": list(PRED.shape), "scores": list(SCORES.shape)}, "gt": GT.tolist(), "pred": PRED.tolist(), "scores": SCORES.tolist(), "iou_matrix_pred_by_gt": iou_matrix.tolist(), "training_assignment_gt_1based": [int(x + 1) if x >= 0 else None for x in training_assignment], "training_forced": forced_anchors.tolist(), "baseline": baseline, "nms_kept_predictions": (kept + 1).tolist(), "nms_suppressed": suppressed, "after_nms": after_nms, "neighbor_case": {"gt": NEIGHBOR_GT.tolist(), "mutual_iou": box_iou(NEIGHBOR_GT[:1], NEIGHBOR_GT[1:]).item(), "before_nms": neighbor_before, "after_nms": neighbor_after, "suppressed": neighbor_suppressed, "kept_at_nms_070": (neighbor_kept_070 + 1).tolist()}, "low_iou_forced_training_case": {"anchors": LOW_OVERLAP_ANCHORS.tolist(), "ious": box_iou(LOW_OVERLAP_ANCHORS, GT).tolist(), "assignment_gt_1based": [int(x + 1) if x >= 0 else None for x in low_assignment], "forced": low_forced.tolist()}, "matching_protocol_counterexample": {"gt": NEIGHBOR_GT.tolist(), "pred": DUPLICATE_NEIGHBOR_PRED.tolist(), "iou_matrix": alternative_ious.tolist(), "best_gt_then_used": protocol_standard, "best_unused_alternative": {"tp": alternative_tp, "recall": alternative_recall.tolist(), "precision": alternative_precision.tolist(), "ap": alternative_ap}}, "checks": "all assertions passed", } (OUT / "results.json").write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8") print("IoU matrix (prediction x GT):\n", np.round(iou_matrix, 6)) print("Training anchor targets (GT1, GT2; None=background):", results["training_assignment_gt_1based"]) for label, experiment in [("Before NMS", baseline), ("After NMS", after_nms)]: for metric, result in experiment.items(): print(label, metric, "=", round(result["ap"], 9), "sequence:", [r["status"] for r in result["rows"]], "recall:", result["final_recall"]) print("NMS kept original predictions:", (kept + 1).tolist()) print("Neighbor objects AP50:", neighbor_before["ap"], "->", neighbor_after["ap"], "; recall:", neighbor_before["final_recall"], "->", neighbor_after["final_recall"]) print("Low-IoU forced anchors:", results["low_iou_forced_training_case"]) print("Protocol counterexample (two copies of GT1): maximum-IoU GT then used ->", [r["status"] for r in protocol_standard["rows"]], "AP50", protocol_standard["ap"], "; best-unused alternative ->", alternative_tp, "AP50", alternative_ap) ```