Lecture 03 学习讲义
正文
工具
按问题查找
Lecture 036 · 反例:相邻真实对象被 NMS 合并 · 实现代码

6 · 反例:相邻真实对象被 NMS 合并 · 实现代码

独立阅读

并入相关学习路径

固定框匹配、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)