[LAB 09 · 检测几何:框、匹配、NMS 与 AP](/course/notebooks/90d213b4e1e04c7e889ee6fc042b2358.html)
# 可视化与实际输出 · 实现代码
[独立阅读](/course/notebook-blocks/8d1df38317244d5097b2ef92388bf925.html)
并入相关学习路径
固定框匹配、PR/AP、NMS误删相邻对象反例
展开完整实现代码
```
def svg_document(width, height, fragments):
return (f'")
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.")
```