ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

目标检测里的 NMS:为什么同一物体只留一个框

2026/9/14 0:55:10 拓冰建站 浏览量
目标检测里的 NMS:为什么同一物体只留一个框 目标检测里的 NMS为什么同一物体只留一个框同一只猫检测器却吐出十几个重叠的框——从一堆框里优雅地留下唯一答案靠的就是 NMS。今天用几十行 numpy 把它的原理、公式和工程坑一次讲透。一、背景与痛点你跑过一次目标检测模型就会发现一个怪现象明明图里只有一只狗输出却是一串几乎重合的狗框。这不是模型坏了而是工作机制决定的。检测器在特征图的每个位置预先撒下成百上千个锚框每个锚框都要回答两个问题这里有没有物体框要往哪调于是同一物体周围往往有几十个锚框都差不多能框住它且都被判成了前景。若改用滑动窗口逐像素扫描一张 800×600 的图仅 32 种尺度就要生成上百万个窗口累计以小时计更糟的是窗口稍偏几个像素IoU 就从 0.7 掉到 0.4分类器对着半只猫根本无法稳定工作。所以工程上必须做两件事先用少量高质量候选替代暴力扫描再把重叠框收敛成一个干净结果。后者的核心算法就是 NMS非极大值抑制——几乎所有现代检测器的最后一道工序。二、核心原理1. 锚框把候选变成可学习的先验给定尺度集合与长宽比集合一个尺度sss与长宽比rrr组合出的锚框宽高为ws⋅r,hsrw s \cdot \sqrt{r}, \qquad h \frac{s}{\sqrt{r}}ws⋅r​,hr​s​这样设计保证不同形状的锚框面积都是w×hs2w \times h s^2w×hs2只是被拉长或压扁。把特征图第(i,j)(i,j)(i,j)个格点映射回原图中心((j0.5)⋅stride,(i0.5)⋅stride)((j0.5)\cdot \text{stride}, (i0.5)\cdot \text{stride})((j0.5)⋅stride,(i0.5)⋅stride)在每个中心放置∣S∣×∣R∣|S|\times|R|∣S∣×∣R∣个锚框。一张 64×64 图、stride8、3 尺度 × 3 长宽比只产生 576 个锚框比上百万滑窗压缩了几个数量级且锚框长在卷积特征上与分类共享计算。2. 正负样本匹配训练时要告诉网络哪个锚框该负责哪个物体。标准做法先计算所有锚框与所有真值框的 IoU 矩阵对每个真值框强制把 IoU 最高的锚框标为正样本保证每个物体至少有一个锚框负责其余锚框按阈值划分——与任一真值 IoU ≥ 0.7 的标为正样本与所有真值 IoU 0.3 的标为负样本中间 0.3~0.7 的灰色地带直接忽略、不参与损失。因为负样本可能占到 99%训练时还要随机采样到 1:1~1:3 的固定比例。3. NMS贪心地收敛重叠框分类头会给每个锚框一个分数同一个物体周围于是堆着几十个高分框。NMS 的思路极其朴素维护候选集CCC与保留集KKK每轮取出分数最高的框b∗arg⁡max⁡b∈Cscore(b)b^* \arg\max_{b \in C}\text{score}(b)b∗argmaxb∈C​score(b)移入KKK再把所有与它 IoU 超过阈值τ\tauτ通常 0.4~0.5的框从CCC中删掉C←C∖{b:IoU(b,b∗)τ}C \leftarrow C \setminus \{b : \text{IoU}(b, b^*) \tau\}C←C∖{b:IoU(b,b∗)τ}重复到候选框耗尽。它是贪心算法结果依赖处理顺序复杂度O(n2)O(n^2)O(n2)不保证全局最优但实践中效果稳定。最后再叠加边界框回归把框修准Gx′AxΔx⋅Aw,Gw′Aw⋅eΔwG_x A_x \Delta x\cdot A_w, \qquad G_w A_w\cdot e^{\Delta w}Gx′​Ax​Δx⋅Aw​,Gw′​Aw​⋅eΔw把检测精度从锚框级提升到像素级这也是高 IoU 阈值下还能拿分的关键。三、代码实战下面用 numpy 实现 IoU 与 NMS输入 6 个互相重叠的框看看算法如何把同一个目标的多个框收敛成一个importnumpyasnpdefiou_xyxy(a,b):ix1,iy1max(a[0],b[0]),max(a[1],b[1])ix2,iy2min(a[2],b[2]),min(a[3],b[3])iw,ihmax(0.0,ix2-ix1),max(0.0,iy2-iy1)interiw*ih area_a(a[2]-a[0])*(a[3]-a[1])area_b(b[2]-b[0])*(b[3]-b[1])returninter/(area_aarea_b-inter1e-12)defnms(boxes,scores,iou_thr0.5):boxesnp.asarray(boxes,dtypefloat)scoresnp.asarray(scores,dtypefloat)ordernp.argsort(-scores)# 按分数降序keep[]whilelen(order)0:iorder[0]keep.append(i)# 保留当前最高分iflen(order)1:breakiousnp.array([iou_xyxy(boxes[i],boxes[j])forjinorder[1:]])orderorder[1:][iousiou_thr]# 抑制 IoU 超阈值的框returnnp.array(keep)boxesnp.array([[30,30,70,70],[32,32,68,68],[35,35,75,75],[80,30,110,70],[20,80,60,110],[25,85,55,105]])scoresnp.array([0.95,0.88,0.72,0.80,0.60,0.45])keepnms(boxes,scores,0.5)print(保留索引:,keep.tolist())print(保留分数:,scores[keep].tolist())运行输出保留索引: [0, 3, 4, 5] 保留分数: [0.95, 0.8, 0.6, 0.45]前三个框[30,30,70,70]、[32,32,68,68]、[35,35,75,75]互相高度重叠其实是同一个目标NMS 只保留了分数最高的 0 号把 1、2 号抑制掉后面的区域各自独立各留一个。6 个框 → 4 个正是同一物体只留一个的效果被抑制的恰好是与高分框高度重叠的低分跟风者。四、关键经验/避坑锚框尺度要用数据聚类得出别拍脑袋选用 k-means 对训练集真值框的宽高聚类用聚类中心当尺度召回率通常明显提升。每个真值至少一个正锚框不能省否则小物体或极端长宽比物体可能一个正样本都分不到网络永远学不会它们。正负样本必须采样平衡负锚框动辄数千全量参与损失会让模型退化成永远说没有物体。灰色地带要忽略而非当负样本强行把 0.3~0.7 的框当负样本等于教网络讨厌那些接近真值的框反而伤害回归精度。NMS 阈值按场景调人群、车流这类密集目标要用更小的阈值如 0.3否则相邻物体会被误吞稀疏场景可用 0.5 保留更多候选。多类别要在每个类别内分别做 NMS。回归输出要裁剪宽高用指数形式保证为正但仍要限制范围防爆炸坐标超出图像边界时要 clamp 到图像内。五、完整系列推荐 本文选自《计算机视觉详解》100 期系统教程第 062 期锚框、候选区域与 NMS每期配可运行 Python 代码。完整系列100 期正文 3 篇番外每期文章代码已在 ima 知识号【Kruptos】持续更新 69 技术知识库计算机视觉、强化学习、推荐系统、大模型微调、操作系统、AI Agent……几乎覆盖全部软硬件技术栈 8 款 AI 技能系列生产、知识库管理、CMMI 受管开发、自进化 Agent 等已在 ima 技能广场上架即装即用✅ 全部免费订阅后续更新自动推送 订阅方式打开 ima腾讯智能工作台→ 搜索「Kruptos」→ 一键订阅或在 ima 内直接搜索《计算机视觉详解》。作者Kruptos西电毕业13 年无线通信/DSP/嵌入式科研现深耕 AI 与云原生原创内容转载注明出处。