尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

NMS与softNMS/softerNMS/IoU-Net:物体检测后处理全解析

NMS与softNMS/softerNMS/IoU-Net:物体检测后处理全解析 做物体检测的同行应该都有过这种时刻模型跑完前向一张图上密密麻麻全是框。同一个行人身上叠着五六个框彼此就差几个像素评分还都挺高。你盯着输出心里清楚直接拿去评测肯定没法看于是开始写后处理——这里登场的就是非极大值抑制也就是 NMS。NMS 这四个字母在检测领域被提了十几年但真正把它讲透的文章并不多。很多人用过 Faster R-CNN、YOLO、DETR也调过nms_iou_threshold这个参数但对这套过滤逻辑背后的设计动机、以及为什么后来会冒出 softNMS、softerNMS、IoU-Net 这一堆变体往往只停留在见过名字的程度。这篇文章我从工程实践的角度把这四个方法从头到尾捋一遍各自解决什么问题、核心公式怎么算、代码怎么写、训练和部署时有哪些坑。顺便聊一个最近的观察——越来越多团队在用 Qwen3-VL 这类多模态大模型微调物体检测任务模型是换了但后处理环节 NMS 这套逻辑依然绕不开甚至因为大模型输出更不可控后处理变得更关键。这篇文章适合两类人刚入检测、只会调参还说不清原理的初学者以及已经用过 NMS、想系统了解各类变体、准备在真实项目里做选型的老手。我尽量把公式和实现细节都摊开讲保证你看完能直接照着写。1. NMS 原理与工程实现先把地基打牢1.1 为什么 NMS 是检测管线里逃不掉的一环要理解 NMS先得明白检测头到底输出什么。不管是早期的两阶段检测器Faster R-CNN 这类还是后来的单阶段模型YOLO、FCOS推理时每个候选位置都会输出一组置信度 坐标训练阶段有正负样本分配网络只对分配到的目标负责但推理时没有这一堆框属于同一个目标的先验于是同一个物体上经常出现十几个高度重叠的候选框。我见过不少刚入行的同学问能不能直接让模型少出几个框理论上可以靠更严格的置信度阈值去卡但那样会把低置信度的真目标一起卡掉。阈值调到 0.9漏检率立刻上来了。所以 NMS 并不是可选优化而是检测模型输出的必要收尾——它的本质是贪心去重在重叠的候选框里保留一个最好的把其余的清理掉。1.2 完整计算流程与阈值语义NMS 的计算流程朴素得不能再朴素四步走把所有候选框按置信度分数降序排序。取分数最高的框加入保留列表。计算该框与其余所有框的交并比IoU删除 IoU 超过阈值的框。对剩余框重复第 2、3 步直到候选框为空。这里最容易忽略的是阈值语义。IoU 阈值设 0.5意味着两个框只要有超过一半的区域重叠就认为它们指向同一目标设 0.7 则更严格允许更多相似框共存。PASCAL VOC 的评测标准是 AP50把 IoU 阈值定在 0.5NMS 阈值也常用 0.5COCO 的评测是 AP[0.5:0.95] 的均值NMS 阈值一般取 0.5 到 0.7 之间具体要看你的业务对框得准不准有多敏感。工程上一个常见误区是把分类置信度阈值和 NMS 阈值搞混。前者是这个框里有没有物体的底线后者是两个框是不是同一个物体的分界线两个参数互相影响调参时必须一起看。1.3 一份能直接用的 Numpy 实现NMS 的原理简单实现起来却很能体现基本功。我贴一份标准实现每一行都有讲究import numpy as np def nms(dets, thresh0.5): x1 dets[:, 0] y1 dets[:, 1] x2 dets[:, 2] y2 dets[:, 3] scores dets[:, 4] areas (x2 - x1 1) * (y2 - y1 1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) w np.maximum(0.0, xx2 - xx1 1) h np.maximum(0.0, yy2 - yy1 1) inter w * h iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou thresh)[0] order order[inds 1] return keep核心思路就是每次取order[0]然后利用np.maximum和np.minimum做向量化的 IoU 计算。inds 1这行尤其关键iou数组是相对order[1:]计算的索引 0 对应原order的第 1 个位置所以筛选后要整体偏移一位否则会把保留框自己又塞回去出现死循环。1.4 实现细节里最容易翻车的三个地方第一坐标必须 clamp。真实场景中模型可能输出负坐标或超出图片边界的坐标直接在原图上算面积会导致负宽高IoU 出现负值或大于 1 的离谱结果。计算前最好先做np.clip把它限制在图片范围内。第二1这个系数要看坐标体系。传统检测框坐标是离散像素索引宽高要x2 - x1 1但很多新模型的输出是连续归一化坐标加不加 1 对结果影响极小反而会让代码难理解。我建议统一在连续坐标体系下写去掉1这个差异在开源代码里能吵好几屏。第三性能问题。Python 里while循环处理单张图尚可批量推理时就会成为瓶颈。实测某检测模型在 GPU 上前向只要 20 毫秒CPU 上的 Numpy NMS 却要 40 毫秒后处理比推理还慢。这个场景直接用torchvision.ops.nms或者 CUDA 版的 NMS别自己造轮子。提示自己写 NMS 时一定要用单元测试验证输出框数量。最简单的套路是一个目标只出一个框、两个目标高度重叠时保留分数高的那个。连这种基准测试都过不了后面的 softNMS 更没法排查。2. softNMS把删除改成降权之后发生了什么2.1 传统 NMS 在重叠场景里的尴尬经典 NMS 的问题一句话概括一刀切太暴力。两个目标如果重叠超过阈值分数较低的框会直接被删掉哪怕它对应的是另一个真目标。这在人群、密集车辆、显微镜细胞这类场景下尤其致命——你去看实际项目里的失败案例很大一部分不是因为模型没检出而是后处理把其中一个真目标的框干掉了。更隐蔽的问题是最高分框不一定是最准的框。NMS 的每一步都假设分数最高的框质量最好但分类分数反映的是这个框里是不是有这个物体完全不反映框有没有框准。如果最高分框偏了半个身位它会把旁边一个框得更准但分数略低的框直接吞掉而人类的需求恰恰是后者。2.2 线性衰减和高斯衰减两条路线softNMSCVPR 2017Bodla 等人的思路非常直接与其把重叠框直接抹掉不如给它降权让它在分数衰减后和别的框竞争。核心是打分函数替换两种权重形式线性加权当 IoU ≥ Nt 时score * (1 - IoU)。高斯加权score * exp(-(IoU^2) / sigma)sigma 通常取 0.5。线性形式在 IoU 接近 1 时会把分数压到接近 0本质还是删高斯形式则更平滑即使 IoU 很大也保留一点残余分数。原论文的实验结论是线性版本在 PASCAL VOC 上略好但我自己的复现结果是两者差距很小真正拉开差距的是你选的阈值和数据集密度。代码改动只有几行。以高斯版本为例def soft_nms_gaussian(dets, sigma0.5, score_thresh0.001): x1, y1, x2, y2, scores dets.T areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) iou (w * h) / (areas[i] areas[order[1:]] - w * h) scores[order[1:]] * np.exp(-(iou ** 2) / sigma) keep_mask scores[order[1:]] score_thresh order order[1:][keep_mask] return keep注意排序需要实时更新每次抑制后分数变化了严格来说要重新排序。工程上很多实现省略了这一步用原始分数顺序凑合效果会打折扣。正确做法是每次衰减后把剩余框按最新分数重新排一下虽然慢一点但更忠实于论文。2.3 不加训练、只换后处理能涨多少点softNMS 最大的卖点不是效果好到离谱而是性价比极高完全不需要重训模型推理时换一个函数就行。论文报告在 PASCAL VOC 上 mAP 提升约 1.7%在 MS COCO 上约 1.3%。我自己的经验是在密集场景的数据集上收益更明显能到 2 到 3 个点在稀疏场景上几乎没变化。为什么有效关键在于它保留了被抑制框的残余信息。传统 NMS 是完全割舍softNMS 则让低分框仍然有机会参与后续竞争这相当于给后处理增加了一点容错。另一个因素是 softNMS 天然缓解了两个真目标高度重叠时误删的问题——当第二个目标也存在时它的分数只是被压低如果本身够高依然能活下来。2.4 softNMS 的适用边界别把 softNMS 当万能药。它有三个明显的限制第一它依然在选框最终还是只保留一个框不会对框的位置做任何修正第二在 AP75、AP90 这种高 IoU 阈值的评测下提升非常有限因为问题已经不在选谁而在框本身准不准第三多类别场景必须按类别分别执行 softNMS否则两个不同类别物体的重叠框会互相压分。工程上接入 softNMS 的成本几乎为零但它不是终点。当你发现该保留谁已经处理得差不多但框的位置始终差那么几个像素时就该往 softerNMS 和 IoU-Net 的方向看了。3. softerNMS让网络学会承认自己没框准3.1 从该删谁到该信谁的坐标softNMS 解决的是该删谁但它隐含了一个假设所有候选框里有一个是足够好的选出来就行。如果这一整簇框全都偏离真实位置呢你可能选了一个相对最好的但它在 IoU 0.8 的水平上到头了无法再进一步。softerNMSCVPR 2019He 等人换了个思路不执着于挑选单个框而是把聚类内多个框的信息融合起来。多个从不同角度、不同位置回归出的框加权平均之后往往比任何一个单独的框都更接近真实目标。就像射击打靶几个弹着点都在靶心周围散布你取它们的中心点往往比任何一个单发都靠近靶心。但加权平均有个前提你得知道每个框的可信度。这就是 softerNMS 的核心贡献——让网络在回归坐标的同时输出一个位置不确定性。3.2 预测方差一种更明确的质量信号具体做法是改动回归头原来输出 4 个值x1, y1, x2, y2现在输出 8 个值多出来的 4 个是对应坐标的方差。训练时用 KL 散度作为损失函数目标是最小化预测分布与真实位置的差异L_reg (x_g - x_e)^2 / (2 * sigma^2) 0.5 * log(sigma^2)这个公式值得停下来品一品。第一项的意思是如果模型认为某个坐标很准方差小那它就必须把坐标回归得足够接近真值否则惩罚很大第二项则防止模型无脑把所有方差都设得巨大来逃避惩罚。换句话说模型必须诚实——它只有在真正有把握的位置才能报出小方差没把握的位置方差自然变大。对没有匹配到 GT 的 proposal论文的做法是让该位置的密度值尽量小。工程实现上常见做法是对这部分样本直接停止梯度回传或者用一个不产生无效梯度的高斯互补损失否则很容易学歪。这一点我在 3.4 还会细说。3.3 基于方差的加权合并训练完成后推理阶段的 NMS 也不一样。在保留最高分框的同时不再直接把重叠框删掉而是对聚类内所有框做加权平均权重取方差倒数再乘分类分数x_final sum(s_i / sigma_i^2 * x_i) / sum(s_i / sigma_i^2)直觉很清晰方差小的框说明模型对它的定位有信心给更大权重分类分数高的框也给更大权重。这个融合策略在高 IoU 阈值评测下效果非常显著因为它的本质是直接提升了框的定位精度而不是在选框环节做文章。我在自己的数据集上复现过模型从 Faster R-CNN 换到带有方差头的版本AP75 提升了 1.5 到 2 个点AP50 几乎不动。这个现象验证了它的定位softerNMS 管的是准不是有没有。3.4 KL Loss 训练时的注意点softerNMS 的实现坑比 softNMS 多不少我踩过几个列出来供参考。第一方差必须保证非负。回归头输出的原始数值可正可负需要接一个 softplus 或 exp 激活再参与损失计算。直接对原始输出取平方也行但梯度特性不如 softplus 稳。第二方差要设下限。训练后期有的框方差会被压到接近 0导致损失函数第一项爆炸。我的做法是对方差做clamp(min1e-4)简单粗暴但有效。第三也是最重要的未匹配样本的梯度处理。如果对背景 proposal 也施加同样的 KL 损失模型会学会把所有背景框的方差都报得巨大以规避惩罚导致推理时背景框和前景框的方差分布完全混在一起加权融合就失效了。实现上对未匹配样本用detach()截断或单独走一个不反传的分支。注意softerNMS 这个名字容易让人误以为它是 softNMS 的改良版。实际上两者解决的问题完全不同softNMS 改的是抑制策略softerNMS 改的是框本身的质量。如果需要两者可以叠加使用先做方差加权融合再做分数衰减。4. IoU-Net把交并比本身变成网络的一路输出4.1 分类分数高不等于框得准到这里NMS 家族的另一个核心矛盾已经藏不住了整个 NMS 流程都在按分类分数排序但分类分数从来不是定位质量的指标。一个框分类分数 0.99它的 IoU 可能是 0.7另一个框分类分数 0.8IoU 却是 0.9。NMS 每次都先保前者结果就是系统性地偏向自信但不准的框。Softmax 分类分数本身就存在校准问题这在多类别不平衡的数据上更严重——常见的类别分数天然偏高稀有类别分数偏低但框的定位质量完全没有体现在分数里。这个问题在 AP50 评测下还能忍因为 IoU 0.5 本来就很宽松但到了 AP75 甚至 AP90排序错误的代价就大了。IoU-NetCVPR 2019Jiang 等人痛点的解法听起来有点循环论证既然 NMS 是按 IoU 排序最合理那就直接训练一个网络来预测 IoU。这个 IoU 预测值就是定位置信度。4.2 IoU 预测分支的设计与训练IoU-Net 的架构改动不大在检测头的 ROIPooling 特征后面除了原有的分类分支和回归分支再加一个并行分支输出一个标量——预测该 proposal 与对应真实目标的 IoU。训练这个分支时回归目标就是 proposal 与它匹配到的 GT 框之间的真实 IoU。损失函数常用 Smooth L1也可以视为回归问题用 BCE。关键设计点是分类头关注是什么IoU 头关注框得准不准两者共享特征但不共享语义。如果直接让分类头去预测 IoU效果会差很多因为分类特征被类别语义主导了。当时我做实验时最深刻的感受是IoU 预测分支并不难训难的是怎么在 R-CNN 这类两阶段框架里处理好 proposal 的匹配策略。匹配的 GT 不同IoU 标签就完全不同。一般沿用训练时的 IoU 匹配策略保证训练和推理分布一致。4.3 IoU-guided NMS 和框细化机制有了 IoU 预测之后后处理就顺理成章地改了。传统 NMS 按分类分数排序IoU-guided NMS 按预测 IoU 排序分数最高的框换成预测定位最准的框重叠抑制的阈值判断不变。这一步直接解决了 4.1 里说的排序错位问题。IoU-Net 还有一个额外机制框细化Box Refinement。思路是把 IoU 预测器当成一个可微分的函数通过梯度上升优化框的坐标让预测 IoU 增大相当于在推理阶段做局部搜索。我在实际项目里用过这个功能效果是有的但计算开销不小且对初始框位置敏感不是所有场景都值得开。这套组合下来在 COCO 的高 IoU 阈值评测上AP75 和 AP90 的提升比 — 更准确地说比低阈值下的提升 — 明显得多。原因很简单高 IoU 阈值下排序质量和框精度直接决定分数而这两件事正好是 IoU-Net 着力解决的。4.4 IoU-Net 的思路在后续模型里的影子IoU-Net 的影响远不止 NMS 本身。它提出的预测定位置信度这个方向在后来的检测器里遍地开花。FCOS 用 center-ness 分支预测框中心质量ATSS 的统计量选择和 IoU 密切相关Double-Head R-CNN 在 ROI 阶段把分类和回归拆成两个 head还有很多近期工作直接用预测 IoU 做训练样本的加权。即便最近用 Qwen3-VL 这类大模型微调检测也有人在做类似尝试让模型额外输出一个框置信度再用它筛选输出。所以理解 IoU-Net不应该只停留在换个排序键的层次。它的核心价值是提出了一种可学习的定位质量度量方式这比任何手工设计的后处理规则都更鲁棒。5. 四代 NMS 横向对比与选型建议5.1 一张表看清四种方法的区别方法核心思想是否需要重训排序依据典型收益代价NMS贪心删除高 IoU 重框否分类分数基线无softNMS对重叠框降权而非删除否衰减后的分类分数VOC mAP 1.7COCO 1.3 左右几乎为零softerNMS预测坐标方差加权融合框位置是需重训回归头分类分数 方差加权融合高 IoU 阈值 AP 提升明显回归头加 4 个输出训练复杂IoU-Net预测 IoU按定位质量排序和细化是需重训预测 IoUAP75/AP90 提升明显多一个 head推理略慢从这张表能看出一条清晰的演进脉络NMS 关心删不删softNMS 关心怎么删softerNMS 关心留下的框准不准IoU-Net 关心用什么标准来判断准不准。5.2 不同场景下怎么选我做选型时的判断逻辑一般是按这四条路走第一纯工程迁移、不想动训练管线直接用 softNMS。替换成本最低密集场景下白捡一两个点上线压力小。第二业务对定位精度有硬指标比如自动驾驶标注、医疗影像测量优先上 softerNMS。它能在不大改网络结构的前提下把框的定位上限抬高但前提是你有能力重训模型。第三做评测刷分或者研究性质的项目IoU-Net 值得投入。它训练稳定之后在高 IoU 阈值下的收益是最可观的硬伤是推理阶段如果开框细化速度会掉下来。第四检测器本身是 DETR 这类基于集合预测 二分匹配的通常不需要 NMS——它天然避免了冗余框的产生。这种情况就别硬套 NMS 变体了后处理重点变成置信度过滤和去重逻辑。5.3 计算开销与部署友好度后处理算法的落地除了精度还要考虑开销。softNMS 只是多一个指数运算在 GPU 上基本可以忽略softerNMS 需要增加 4 个回归输出通道推理时多一次加权求和开销仍然可控IoU-Net 多一个 head 的前向计算开框细化后开销最大需要评估算力。端侧或服务端部署时还要考虑算子支持。Numpy 版本的 NMS 在 ONNX 导出后往往需要重写为自定义算子但 softNMS 和 softerNMS 里的操作都是基础张量运算很多推理框架可以直接支持。需要说明的是写后处理时尽量用统一的张量形式避免 Python 控制流否则转 ONNX、TensorRT 的时候会非常痛苦。6. 实战踩坑记录与高频问题排查6.1 高频问题速查表现象可能原因解决方式两个不同类别的重叠物体被误删对全部类别执行了一个 NMS按类别分别执行 NMS小目标全被过滤掉IoU 阈值太低或置信度阈值过高降低 IoU 阈值或改用 softNMS 高斯形式同一目标残留双框NMS 阈值太松或 score 阈值太低收紧 IoU 阈值并检查低分框的过滤条件推理后处理比前向还慢Numpy 循环在 CPU 上跑换 torchvision.ops.nms 或 CUDA 实现softerNMS 训练发散方差接近 0 导致损失爆炸对方差做 softplus 激活并 clamp 下限softerNMS 背景框权重异常未匹配样本的梯度没有截断对未匹配 proposal 使用 detach 或单独损失IoU 预测值整体偏高训练数据匹配策略与推理不一致统一训练和推理时的 proposal-GT 匹配方式6.2 调参顺序和评估方法论后处理调参最忌讳一上来就换算法。我自己的固定套路是先把置信度阈值拉到 0.05NMS 阈值从 0.3 到 0.7 扫一遍同时看 AP50、AP75 和可视化结果。只有在这个基础上确认该保留的框都有、该删的没删干净再去考虑 softNMS 还是 softerNMS。评估时一定不要只看 mAP。mAP 是 AP50 到 AP95 的平均容易被 AP50 掩盖问题。定位精度优化的效果必须盯着 AP75、AP90 看这才是 NMS 变体真正发力的区间。还有一个经验调参要有对照组。同一批验证集固定随机种子先跑原始 NMS 得到基线再换 softNMS保证两次的输入候选框完全一致。否则你很难分辨涨点到底来自算法改进还是来自候选框质量波动。6.3 结合 Qwen3-VL 微调检测模型的一点观察最近用 Qwen3-VL 这类多模态模型微调检测任务的人越来越多。这类模型输出框的格式是归一化坐标而且因为语言模型解码的随机性同一个目标经常产生多个相近但坐标略有差异的框。很多人以为大模型智能到不需要后处理实际测试下来不做 NMS 直接拿输出去评测mAP 掉得厉害。我的建议是无论底层模型是什么后处理这一层都要保留。对于大模型微调的检测输出softNMS 尤其适用——因为这类输出的分数分布往往不够锐利直接硬删容易误伤降权策略更稳。更激进的思路是参考 IoU-Net 的精神在微调时让模型额外输出一个框质量估计只是这个方向目前还没有统一做法需要自己摸索。踩过几次坑之后我个人最深的体会是NMS 这类后处理不是模型训完之后的附属品而是整个检测系统的一部分。训练数据、损失函数、后处理策略三者必须放在一起考虑。很多项目检测效果上不去问题不在模型结构而在后处理环节一直在用默认参数。先把 NMS 吃透再谈更复杂的后处理这是每个检测工程师都值得花时间做的基本功。
返回列表