非极大值抑制(NMS)详解:原理、变体与工程优化

发布时间:2026/10/5 15:36:56
非极大值抑制(NMS)详解:原理、变体与工程优化 目标检测跑完模型一张图出来几千个候选框密密麻麻叠在一起直接可视化根本没法看画出来的图跟涂鸦一样。这时候就该非极大值抑制NMS上场了。这篇博文会把NMS从原理到工程实现的细节完整拆一遍包括标准NMS的数学逻辑、Python/C实现方式、Soft-NMS和DIoU-NMS等变体的改进思路以及多类别检测时跨类抑制的争议和工程落地中的性能优化手段。适合刚接触目标检测的初学者也适合已经在用NMS但想搞清楚里面门道、想在项目里做优化的开发者。我最早接触NMS时以为它就是个简单的挑最大框、删重叠框的去重工具。后来在项目里被各种边界情况折腾过之后才明白NMS的每一步设计都有讲究阈值怎么定、排序按什么排、类别之间要不要互相抑制每个决策都会直接影响mAP指标和实际部署效果。本文会把这些问题逐一讲透。1. 为什么目标检测的后处理里必须有NMS这一环1.1 检测模型天然会输出大量冗余框先想一个问题为什么模型输出的框不是干干净净每个目标一个这要从目标检测的推理机制说起。无论Faster R-CNN这种两阶段模型还是YOLO、SSD这类单阶段模型本质都在做两件事判断哪里有目标以及目标是什么。以Anchor-based方法为例模型会在特征图的每个位置预设多个不同尺寸、不同长宽比的anchor然后预测每个anchor内是否包含目标、目标的类别、以及相对anchor的偏移量。一张800x600的输入图经过下采样后特征图可能是100x75假设每个位置有9个anchor那一层就有67500个候选框。再加上多尺度特征层候选框数量轻松超过20万。推理时这些框同时输出一个目标往往被周围多个anchor同时命中。尤其是靠近目标中心的anchor预测出的框都差不多IoU经常在0.7以上。如果把这些框全部保留检测结果就是一团浆糊。NMS的作用就是在这些高度重叠的框里挑出置信度最高、位置最准的那个把剩下的抑制掉。1.2 NMS在检测流水线中的位置NMS处于模型推理的最后一环属于纯后处理不涉及任何网络参数更新。完整链路是输入图像 - 模型前向传播 - 解码得到候选框和类别概率 - 置信度过滤 - NMS - 输出最终检测结果这里有个容易忽略的点NMS之前通常会先做一次置信度阈值过滤。比如把score低于0.05的框全部扔掉这样参与NMS的框数量大幅减少速度会快很多。顺序不能反先NMS再过滤相当于在几十万个框上做O(n²)的运算完全没必要。1.3 NMS的数学本质NMS解决的问题可以形式化描述为给定一组带类别和大小的候选框集合B {b₁, b₂, ..., bₙ}每个框对应的置信度S {s₁, s₂, ..., sₙ}从中选出一个子集D使得D中任意两个框的IoU小于预设阈值NtD中每个框都是其邻域内置信度最高的说白了就是局部取最大、重叠则抑制。这也是非极大值抑制这个名字的由来——在重叠的极大值区域里保留最大的那个抑制其他局部极大值。2. 从数学定义到Python实现把NMS的每一步算清楚2.1 IoU的准确计算方式IoUIntersection over Union是NMS的核心度量没有它NMS就无从谈起。公式非常简单IoU (A ∩ B) / (A ∪ B)也就是两个框的交集面积除以并集面积。IoU等于1说明两个框完全重合等于0说明完全不相交。NMS的决策完全依赖这个数值。具体计算时给定两个框的坐标(x1, y1, x2, y2)左上角和右下角交集区域的宽高是inter_w max(0, min(xA2, xB2) - max(xA1, xB1)) inter_h max(0, min(yA2, yB2) - max(yA1, yB1)) inter_area inter_w * inter_h注意这里必须用max(0, ...)否则两个框不相交时inter_w或inter_h可能算出负值导致交集面积是负数结果就错了。这是我见过新手最容易踩的坑。并集面积可以直接用两个框的面积之和减去交集面积不需要真的画图算union_area areaA areaB - inter_area2.2 标准NMS的五步流程标准NMS的流程非常简洁总共五步输入所有候选框B和对应置信度S按置信度从高到低排序取出当前置信度最高的框b_max加入最终结果集D并从B中删除计算b_max与B中剩余所有框的IoU删掉IoU大于阈值Nt的框回到第3步直到B为空这个流程看起来简单但每一步都有值得琢磨的细节。比如排序的稳定性、IoU阈值的选择、以及当存在多个类别时流程如何处理后面的章节会展开。2.3 一步一步写Python代码直接用NumPy写一个标准NMS实现代码量很短但每一行都有讲究import numpy as np def nms(boxes, scores, iou_threshold): boxes: (N, 4) array, 每行是 [x1, y1, x2, y2] scores: (N,) array, 每个框的置信度 iou_threshold: IoU阈值, 超过该值的框被抑制 x1 boxes[:, 0] y1 boxes[:, 1] x2 boxes[:, 2] y2 boxes[:, 3] 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:]]) # 计算交集面积, 注意宽度和高度要限制在0以上 w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) inter w * h # IoU 交集 / 并集 iou inter / (areas[i] areas[order[1:]] - inter) # 保留IoU小于阈值的框, 大于等于阈值的框被抑制 inds np.where(iou iou_threshold)[0] order order[inds 1] return keep这个实现里有几个容易写错的细节order[inds 1]这行的1非常关键。因为iou数组的长度是order.size - 1索引0对应的是order[1]所以inds是相对于iou数组的索引要映射回order数组需要加1。漏掉1会导致索引错位结果完全错误。计算IoU时每轮循环只需要计算当前最高分框与剩余框的IoU不需要算剩余框两两之间的IoU。因为当前最高分框必然加入结果集后续被它抑制的框直接删掉即可不需要再参与后续计算。2.4 时间复杂度与优化空间标准NMS的时间复杂度是O(n²)其中n是候选框数量。每一轮循环都要遍历所有剩余框计算IoU而循环次数等于最终保留框的数量加上被抑制框的数量总体就是O(n²)。如果候选框数量只有几百个这个复杂度完全不是问题。但在实际工程中模型输出的候选框往往有几万个O(n²)就会成为性能瓶颈。常见的优化方向有两个一是NMS之前的置信度过滤。把score低于0.3的框直接抛弃参与NMS的框数量可能从几万降到几千速度提升立竿见影。这也是为什么推理代码里几乎都会在NMS之前先做一次过滤。二是用GPU并行化。NMS的每轮循环中计算当前框与所有剩余框的IoU这一步是完全可并行的可以一次性对所有候选框做向量化运算这也是上面的NumPy实现能做到的关键。如果用纯Python循环写同样的逻辑性能可能要慢几十倍。3. 经典NMS的四大软肋与改进变体对比3.1 软肋一密集场景下漏检严重标准NMS有个先天缺陷如果一个目标紧挨着另一个目标两个框的IoU很高其中置信度较低的那个会被直接删掉即使它对应的是另一个独立目标。典型的场景是人群检测、货架上的密集商品检测、显微镜下的细胞检测。两个相邻目标可能重叠面积很大但其实是两个独立的检测目标。标准NMS一刀切的做法会把其中一个直接抑制掉造成漏检。我在一个人群计数项目里就遇到过这个问题两个行人靠得比较近互相遮挡模型给两个人各输出一个置信度0.9和0.85的框IoU大约是0.65。如果用0.5的NMS阈值置信度0.85那个框直接被删掉检测结果就从正确变成了漏检。这种问题在标准NMS里没有完美解法只能依靠改进变体来缓解。3.2 软肋二置信度不够高不一定代表框不准NMS有一个隐含假设置信度越高的框位置越准。但这个假设在实际情况中经常不成立。有些框虽然置信度稍低但定位精确有些框置信度最高却偏离目标中心。出现这种情况的原因是模型对目标的定位能力和分类置信度并不完全耦合。尤其在目标被遮挡或者边缘模糊的时候模型可能给一个位置有偏差的框打了更高分。标准NMS对置信度和位置一视同仁只按分数取舍就会丢掉一些定位更好的框。3.3 软肋三硬抑制导致检测结果数量不可控标准NMS是硬抑制IoU超过阈值就彻底删除没有中间状态。这在目标密集的场景下会带来连锁反应某个框被删除后它原本可能抑制的其他框也没了最终的检测结果数量可能明显少于实际目标数量。而且NMS没有任何全局视角它只会按顺序从高到低判断不会回头看已经保留的框是否需要重新调整。整个过程是贪心的每一步的局部最优不一定能带来全局最优。3.4 软肋四IoU阈值敏感需要频繁调参NMS对IoU阈值非常敏感。阈值太高保留的框冗余多、误检多阈值太低相邻目标容易被误删。这个值在原生的目标检测数据集上一般设为0.5左右COCO的评测标准里甚至会做0.5到0.95的多重IoU评估。但换到实际场景比如密集的仓储场景或者无人机俯拍的小目标场景0.5未必适用需要重新调参。3.5 改进变体Soft-NMS的思路与实现Soft-NMS的出发点是与其把IoU超过阈值的框直接删掉不如按IoU大小降低它们的置信度让高IoU的框降得多、低IoU的框降得少。这样即使两个目标确实靠得近置信度被降低后如果还高于最终阈值依然可能被保留。Soft-NMS的权重更新有两种形式线性加权score score * (1 - iou) # iou Nt时高斯加权score score * exp(-iou² / sigma)高斯加权的效果更平滑也不用写if判断实际实现中更常用。Soft-NMS的代价是需要对同一个框可能被多次更新置信度而且在已经排好序的序列里降低某个框的置信度会影响它后续参与排序的优先级实现逻辑比标准NMS复杂一些。3.6 改进变体DIoU-NMS怎么利用几何信息DIoU-NMS的出发点更直接两个框重叠面积大不一定代表它们对应同一个目标。如果两个框的中心点距离比较远哪怕IoU很高也更可能是两个邻近的不同目标。DIoU-NMS把标准NMS里的IoU替换成DIoUDistance-IoU公式是DIoU IoU - (d² / c²)其中d是两个框中心点的欧氏距离c是两个框最小外接矩形的对角线长度。中心点距离越远DIoU越小被抑制的可能性就越低。这样在人群、车辆等目标有重叠但中心点不重合的场景下可以显著减少漏检。我在密集车辆检测项目里对比过用DIoU-NMS替代标准NMSmAP能提升约1到2个点尤其是在车辆互相遮挡的区域效果非常明显。3.7 变体对比表格方法核心思路计算量适用场景优点缺点标准NMSIoU超过阈值直接删除O(n²)通用简单、快速、易实现密集场景漏检严重Soft-NMSIoU越大置信度衰减越多O(n²)目标密集、遮挡多不搞一刀切漏检少可能保留更多冗余框DIoU-NMS中心点距离联合IoU判断O(n²)重叠但中心点不同的目标密集场景mAP提升明显需要额外计算距离类别感知NMS只抑制同类框O(n²)多类别检测符合直觉实现容易异性重叠时结果不佳4. 多类别检测时的NMS该怎么做跨类抑制为何争议不断4.1 按类别分别做NMS的常规做法大多数目标检测框架处理多类别检测时NMS的方式是逐类别独立执行。也就是说类别A的所有候选框放一起做一次NMS类别B的候选框放一起再做一次NMS类别之间互不干扰。这样做的好处是逻辑简单各个类别的框不会互相误抑制。比如一个行人和一辆自行车几乎重叠它们的IoU很高但本质是两个不同类别不应该互相抑制。按类别独立做NMS两个框都能保留。4.2 跨类别抑制问题的真实存在性按类别独立做NMS虽然实现简单却有一个争议点不同类别的检测框是否需要互相抑制。举个例子一张图里有个骑自行车的人检测模型可能同时输出一个人的框和一个自行车的框这两个框高度重叠。按类别独立NMS两个框都保留。从检测任务的角度看这可能是正确的因为人和自行车确实都是可见目标。但从实际应用角度看如果做的是智能交通系统用户可能只关心骑车的人输出两个重叠框反而增加了处理复杂度甚至可能被下游模块误判为人车分离。另一个典型场景是检测领带和人领带的框完全落在人的框内部IoU极大。如果跨类别抑制领带被删掉如果按类别独立NMS领带保留。后者对细粒度识别任务是正确的但确实增加了输出冗余。4.3 跨类NMS的适用边界根据我的实际项目经验判断是否需要跨类NMS不能只看IoU还要看任务目标和类别之间的关系类别之间语义互斥如猫和狗在同一位置不该互相抑制但通常模型也不会在同一位置同时输出这两种高置信度的框类别之间有包含关系如人和上衣按类别独立NMS即可因为上衣框的检测目标就是上衣本身类别语义重叠度高如行人和骑车人可以考虑用跨类NMS或直接合并类别标签跨类NMS的常见实现是先把所有类别的框合并计算IoU时只抑制高IoU且类别不同的框但需要设置一个比同类NMS更高的阈值比如0.7甚至更高避免不同类别的合理检测结果被误删。这个阈值需要根据实际数据分布调整没有通用答案。4.4 大规模多类别检测时的实现细节如果类别非常多比如Open Images数据集有500个类别逐类独立做NMS意味着要把候选框按类别切分成500组分别处理。实现时要注意两点一是按类别索引分组必须用高效的字典或数组索引不要用循环遍历所有类别去匹配框否则会产生类别数乘以框数的额外开销。更优的做法是先按类别的最大可能数量建好空列表遍历框时直接放入对应的组。二是有序性处理。为了后续操作方便把分数排序放在分组之前比分组之后更好。先全局排序再分组组内天然保持有序不需要重复排序。这样可以省去多次排序的额外耗时在候选框数量很大的时候能省下可观的推理时间。5. 工程落地中最容易被忽视的阈值与性能细节5.1 IoU阈值到底怎么选IoU阈值的选取直接决定NMS的严格程度。阈值设得越低比如0.3抑制越激进保留的框越少同时误删的概率越高阈值设得越高比如0.7抑制越保守保留的框越多冗余框也会更多。实际项目里怎么定这个值我的经验是第一看评测指标的需求。如果对标的基准是COCO mAP0.5那么NMS的IoU阈值设为0.5通常够用再低就可能把正确检测也删掉。如果是mAP[0.5:0.95]建议NMS阈值稍高一些比如0.6或0.7因为评测要求在更严格的IoU下计算准确率NMS太激进会让定位稍有偏差的框被过早删除。第二看实际场景对精确率和召回率的偏好。安防告警系统更看重误报率低可以把阈值调低一些自动驾驶感知更看重不丢目标阈值应该调高。5.2 置信度过滤阈值与NMS的联动很多初学者只关注NMS自身的阈值忽略了它和置信度过滤阈值之间的联动关系。置信度过滤阈值设在0.5NMS阈值设在0.5和置信度过滤阈值设在0.3、NMS阈值设在0.6即使最终输出的框数量相同检测结果也可能差异很大。原因是更低的置信度阈值会引入更多低置信度的框参与NMS这些框可能在局部区域形成一个高密度簇把中间一个中等置信度但定位很好的框给抑制掉。这个联动效应在调试时经常被忽略。优化检测效果的时候不要只单独调NMS阈值或者单独调置信度阈值建议画一条性能曲线在置信度阈值和NMS阈值这两个维度上做网格搜索选出一组最优组合。5.3 用C实现时要注意的性能瓶颈在C工程里实现NMS最常见的性能瓶颈是候选框的排序和IoU计算。这两个部分分别有不同的优化策略。排序方面标准库的sort已经很好用关键是自定义比较器要写得高效。不要用lambda捕获大量外部数据然后比较尽量直接基于原始分数数组排序索引。性能实测中顺序访问比分跳跃快得多。IoU计算方面把坐标数组拆成x1、y1、x2、y2四个独立数组比保存成结构体数组更有利于内存连续访问。计算当前框与所有候选框的IoU时用四个指针并行遍历编译器可以更好做自动向量化。还有一个小技巧NMS处理前先快速剔除完全不可能重叠的框。比如当前框在坐标x上的范围是[100, 200]那么x2小于50或者x1大于250的框可以跳过IoU计算直接判为不重叠。这种空间过滤可以把计算量减少一大半对实时推理系统帮助很大。5.4 实测数据优化前后性能对比我在一个工业质检项目里做过一次NMS性能优化候选框大约两万多个。优化前代码是直接对候选框做全量排序然后循环计算IoU单帧NMS耗时约12毫秒。优化之后分成三步用置信度0.3做预过滤约2万个框降到6000个按x坐标做一次桶排序只对可能重叠的候选框计算IoU用float32替代double减小区间判断的额外分支优化后单帧NMS耗时约2毫秒速度提升了6倍而且检测效果几乎没有变化。这说明NMS的优化空间相当大瓶颈并不在算法复杂度本身而在于很多工程实现里都是直接拿候选框全量硬算浪费了大量时间。6. 从NMS到端到端后处理是不是迟早要消失6.1 NMS成为检测流程不够优雅的那一环目标检测领域这些年一直在往端到端的方向演进。NMS作为一个非参数、启发式的后处理模块存在几个天然不优雅的点一是不可导没法把NMS集成到训练流程里做端到端优化。虽然有一些工作尝试用Soft-NMS或者可导近似替代但本质上NMS是离散的删除操作梯度无法穿过它。二是超参数多IoU阈值、置信度阈值都要人工设定并且高度依赖数据集。不同的数据集、场景、类别分布都可能导致最优阈值变化自动调优成本高。三是设计逻辑与模型能力割裂。模型训练时用的是匹配策略和损失函数推理时用的是NMS两者之间的语义鸿沟存在已久。模型不知道推理阶段会用多少阈值做NMS自然也无法针对性地优化输出分布。6.2 DETR和端到端检测如何绕开NMSDETR系列模型通过二分图匹配的方式做匈牙利匹配把每个目标分配给唯一一个预测框从机制上消除了大量重叠框。加上Transformer的全局建模能力模型输出的框经过匹配后天然是稀疏的后处理阶段只需要做简单的置信度过滤即可。不过DETR这类端到端方案也不是完全不需要NMS的思想。它的二分图匹配本质上就是一种更聪明的NMS只不过把抑制决策放到训练阶段去学而不是推理阶段用硬规则删。训练时学习到要么匹配要么不匹配推理时不需要额外删除。6.3 现实工程里NMS短时间内不会消失尽管端到端检测是趋势但现实情况是NMS仍然活跃在几乎所有主流检测框架里。YOLO系列、Faster R-CNN、SSD、DETR的某些变体推理代码里都保留着NMS或NMS类似的后处理逻辑。原因很现实NMS计算简单、行为可预期、调参成本低而且对于大多数应用场景它的效果完全够用。端到端方案确实优雅但训练更复杂对数据量的要求更高在中小规模项目里反而没有模型NMS的方案实用。6.4 使用NMS时的最后一个建议根据我的经验无论你是准备在项目里继续使用NMS还是考虑迁移到端到端检测方案都建议先把你当前场景下的NMS调优到位。跑一组实验画出置信度阈值和NMS阈值的二维性能热力图看看当前模型到底在哪个参数组合下表现最好。很多时候模型性能不够不是模型本身的问题而是后处理参数根本没有调对。这个热力图分析花不了多少时间但对检测效果的影响可能让你意外。另外如果你正在训练一个多类别检测模型建议在训练时就考虑推理阶段NMS的影响。比如在损失函数设计时对靠近目标边界、容易被NMS抑制的预测框给予更多关注虽然训练代码改起来麻烦一些但推理效果会扎实很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询