3. 传统NMS算法流程:算法步骤、伪代码、优缺点分析
聊完了NMS到底在解决什么问题,咱们得把它的底裤扒干净——看看传统NMS到底是怎么一步步把那些冗余框干掉的。说实话,我刚入行那会儿,觉得NMS不就是选个得分最高的框,然后把重叠的都删掉嘛,有啥好学的?结果第一次在YOLOv3上做密集小目标检测,直接翻车了。嗯,从那以后我才老老实实把NMS的每一步都啃透。
3.1 核心思想:一句话说清楚
传统NMS(Non-Maximum Suppression,非极大值抑制)说白了就是:在局部区域内,只保留得分最高的那个检测框,把其他跟它重叠度太高的框全部干掉。
你想想看,一个目标上可能产生十几个框,每个框都带着一个置信度分数。我们当然要留下那个最有信心的,对吧?但问题来了——怎么定义“局部区域”?怎么衡量“重叠度太高”?这就是IoU(交并比)登场的地方。
一句话记忆法:NMS = 找老大 + 清小弟。老大是得分最高的框,小弟是跟老大IoU超过阈值的框。
3.2 算法步骤:五步走
传统NMS的流程其实非常直观,我习惯把它拆成5个步骤。每次写代码前,我都会在心里过一遍这个流程,避免漏掉边界情况。
- 输入准备:拿到所有检测框的集合 B = {b₁, b₂, ..., bₙ},每个框附带一个置信度分数 S = {s₁, s₂, ..., sₙ},以及一个预设的IoU阈值 Nt(通常取0.5或0.45)。
- 按分排序:把所有框按照置信度分数从高到低排序。得分最高的框,就是我们当前要保下来的“种子框”。
- 挑选种子:从排序后的列表中取出得分最高的框,把它加入最终结果集 D,同时从原列表 B 中移除。
- 计算IoU并淘汰:遍历 B 中剩下的所有框,计算它们与当前种子框的IoU。如果 IoU > Nt,就把这个框从 B 中删除(因为它跟种子框重叠太多了,大概率是在检测同一个目标)。
- 重复迭代:回到第2步,继续从剩余的 B 中选得分最高的框,重复步骤3和4,直到 B 为空。
我在项目中遇到过一个问题:当两个目标靠得非常近时,IoU阈值设得太高(比如0.7),会把其中一个目标的框误删掉。后来我学乖了,密集场景下会把阈值降到0.4左右,宁可多留几个假阳性,也别漏掉真目标。
3.3 伪代码:手写版
下面是我自己习惯用的伪代码风格,跟教科书上的略有不同——我加了一些实际工程中会考虑的边界检查。你直接照着这个逻辑写Python,基本不会出大问题。
function NMS(boxes, scores, iou_threshold):
// boxes: [[x1,y1,x2,y2], ...], scores: [s1, s2, ...]
// 返回保留框的索引列表
// 1. 按得分降序排序
indices = argsort(scores, descending=True)
keep = []
while len(indices) > 0:
// 2. 取当前得分最高的框
current = indices[0]
keep.append(current)
// 3. 如果只剩一个框,直接结束
if len(indices) == 1:
break
// 4. 计算当前框与剩余所有框的IoU
rest = indices[1:]
ious = compute_iou(boxes[current], boxes[rest])
// 5. 保留IoU小于阈值的框
indices = rest[ious < iou_threshold]
return keep
我的小习惯:实际写代码时,我会把 compute_iou 单独抽成一个函数,方便后面换成GIoU或DIoU。另外,记得处理空框(宽或高为0)的情况,否则除零错误会让你怀疑人生。
3.4 优缺点分析:别被它骗了
传统NMS看起来简单粗暴,但它在YOLO时代能活这么久,自然有它的道理。不过,它的缺点也相当致命。我整理了一个表格,方便你对比着看。
| 维度 | 优点 | 缺点 |
|---|---|---|
| 计算效率 | 算法复杂度O(n²),但实际中n通常很小(每张图几十到几百个框),CPU上也能跑得飞快 | 当候选框数量极大(比如5000+)时,纯Python实现会卡成PPT,需要C++或GPU加速 |
| 实现难度 | 代码量不到20行,逻辑清晰,调试方便 | 太简单反而容易让人忽略边界情况(比如空输入、全零框) |
| 检测效果 | 对稀疏目标、大目标效果很好,几乎不会误删 | 密集场景下,两个紧挨着的目标会被当成一个,导致漏检 |
| 阈值敏感度 | 只有一个超参数(IoU阈值),调参简单 | 阈值太严(如0.3)容易漏检,太松(如0.7)容易保留大量冗余框 |
| 适用场景 | 通用目标检测、行人检测、车辆检测等标准场景 | 密集小目标、遮挡严重、不规则形状目标(如细长杆子)表现差 |
我曾经踩过的坑:有一次做无人机视角下的车辆检测,车停得密密麻麻的。我用默认的0.5阈值,结果NMS把相邻车道的车框全删了,只剩下一辆车。后来我把阈值降到0.3,又发现很多冗余框没清干净。最后没办法,只能上Soft-NMS。所以记住:传统NMS不是万能的,密集场景下一定要考虑变种方案。
3.5 核心流程图:NMS的一次完整迭代
下面这张图展示了一次NMS迭代中发生了什么。我特意把“种子框”和“被淘汰框”用不同颜色标出来,方便你理解。
3.6 什么时候该用传统NMS?
说实话,虽然现在各种NMS变种层出不穷,但传统NMS在以下场景中依然是首选:
- 目标稀疏的场景:比如监控画面中只有一两个人,或者空旷道路上的车辆检测。这种情况下,框与框之间几乎不会重叠,传统NMS完全够用。
- 对延迟要求极高的场景:比如实时视频流处理,每帧只有几毫秒的推理时间。传统NMS的O(n²)复杂度在框数少时几乎无感,而Soft-NMS或DIoU-NMS会引入额外计算。
- 作为baseline对比:我在做算法改进时,一定会先用传统NMS跑一遍结果,作为后续改进的基准线。如果改进后的算法连传统NMS都打不过,那说明方向有问题。
我的建议:不要一上来就上各种花哨的NMS变种。先用传统NMS跑通整个pipeline,确认模型本身没问题之后,再根据具体场景的痛点(密集、遮挡、小目标)去选择对应的改进方案。这样定位问题会快很多。