[LAB 09 · 检测几何:框、匹配、NMS 与 AP](/course/notebooks/90d213b4e1e04c7e889ee6fc042b2358.html) # 可视化与实际输出 · 实现代码 [独立阅读](/course/notebook-blocks/8d1df38317244d5097b2ef92388bf925.html) 并入相关学习路径 固定框匹配、PR/AP、NMS误删相邻对象反例 展开完整实现代码 ``` def svg_document(width, height, fragments): return (f'' '' + "".join(fragments) + "") def svg_text(x, y, value, cls="label", extra=""): return f'{escape(str(value))}' geometry = [svg_text(32, 36, "Two GT boxes / four scored predictions", "title"), svg_text(32, 62, "Continuous xyxy coordinates; dashed = GT; solid = prediction", "label")] for value in range(9): x, y = 80 + value * 55, 100 + value * 55 geometry.append(f'') geometry += [svg_text(x - 4, 566, value, "small"), svg_text(55, y + 5, value, "small")] geometry += [svg_text(293, 592, "x coordinate"), svg_text(20, 327, "y", "label")] for i, b in enumerate(GT): x1, y1, x2, y2 = b geometry.append(f'') geometry.append(svg_text(83 + x1 * 55, 88, f"GT{i+1}")) colors = ["#156a9c", "#d16a20", "#188164", "#845ec2"] for i, b in enumerate(PRED): x1, y1, x2, y2 = b geometry.append(f'') geometry.append(f'') geometry.append(svg_text(627, 134 + i * 50, f"P{i+1} | score {SCORES[i]:.2f}")) geometry.append(svg_text(627, 153 + i * 50, f"IoU(GT1,GT2) = {iou_matrix[i,0]:.3f}, {iou_matrix[i,1]:.3f}", "small")) geometry += [svg_text(585, 372, "AP50: TP, FP, TP, FP"), svg_text(585, 399, "P2 overlaps GT1, already used."), svg_text(585, 444, "AP75: TP, FP, FP, FP"), svg_text(585, 471, "P3 IoU = 2/3 < 0.75."), svg_text(585, 527, "The axes include the distant FP.", "small")] (OUT / "geometry.svg").write_text(svg_document(955, 615, geometry), encoding="utf-8") pr = [svg_text(28, 36, "All-points AP integrates the precision envelope", "title"), svg_text(28, 62, "Original predictions, before NMS | solid: envelope; dots: raw ranked precision", "label")] for panel, key in enumerate(["AP50", "AP75"]): result = baseline[key] left, top, width, height = 80 + panel * 460, 108, 340, 290 for tick in [0, .25, .5, .75, 1]: x, y = left + tick * width, top + (1 - tick) * height pr += [f'', svg_text(x - 8, top + height + 26, f"{tick:g}", "small"), svg_text(left - 35, y + 4, f"{tick:g}", "small")] mrec, env = np.array(result["envelope_recall"]), np.array(result["precision_envelope"]) intervals = np.flatnonzero(np.diff(mrec) > 0) for interval_i, j in enumerate(intervals): x0, x1 = left + mrec[j] * width, left + mrec[j + 1] * width y = top + (1 - env[j + 1]) * height pr.append(f'') pr.append(f'') if interval_i + 1 < len(intervals): next_y = top + (1 - env[intervals[interval_i + 1] + 1]) * height pr.append(f'') for i, (r, p) in enumerate(zip(result["recall"], result["precision"])): x, y = left + r * width, top + (1 - p) * height pr += [f'', svg_text(x + (10 if x < left + width else -26), y + (18 if i in (0, 3) else -9), f"P{i+1}", "small")] pr += [svg_text(left + 108, 94, f"{key} = {result['ap']:.6f}"), svg_text(left + 130, 451, "Recall"), svg_text(left - 38, 85, "Precision", "small")] pr += [svg_text(80, 489, "AP50 = (1/2)*1 + (1/2)*(2/3) = 5/6"), svg_text(540, 489, "AP75 = (1/2)*1 + (1/2)*0 = 1/2"), svg_text(80, 529, "Repeated recall adds no area; the interval from 0.5 to 1 uses precision 2/3.", "small")] (OUT / "precision_recall.svg").write_text(svg_document(970, 556, pr), encoding="utf-8") nms = [svg_text(28, 36, "The same NMS rule can remove a duplicate or a real object", "title"), svg_text(28, 63, "Class-aware NMS: suppress same-class boxes only when IoU > 0.50", "label")] for panel, (title, boxes, mutual, before, after) in enumerate([ ("A. Duplicate predictions for one object", PRED[:2], 9/11, 5/6, 1.0), ("B. Two adjacent real objects", NEIGHBOR_GT, .6, 1.0, .5)]): left = 40 + panel * 485 nms.append(svg_text(left, 110, title)) for i, b in enumerate(boxes): x1, y1, x2, y2 = b nms.append(f'') nms += [svg_text(left + 25, 345, f"Mutual IoU = {mutual:.3f} > 0.50"), svg_text(left + 25, 373, "Blue P1 retained; dashed orange P2 removed."), svg_text(left + 25, 410, f"AP50: {before:.6f} -> {after:.6f}")] if panel == 0: nms += [svg_text(left + 25, 442, "P2 was a duplicate FP; full-scene AP improves.", "small"), svg_text(left + 25, 465, "P3 and P4 remain in the original scene.", "small")] else: nms += [svg_text(left + 25, 442, "P2 was a TP; recall falls from 1.0 to 0.5.", "small"), svg_text(left + 25, 465, "Changing only NMS threshold to 0.70 keeps both.", "small")] (OUT / "nms_tradeoff.svg").write_text(svg_document(1000, 505, nms), encoding="utf-8") # Rich display is optional; SVG files are created above without IPython. try: from IPython.display import SVG, display for figure in ["geometry.svg", "precision_recall.svg", "nms_tradeoff.svg"]: display(SVG(filename=str(OUT / figure))) except ImportError: print("SVG files saved; open them in a browser for viewing.") ```