资讯动态

YOLOV4后处理解析:从NMS到DIOU-NMS的原理与Python实现

发布时间:2026/10/13 20:34:29 来源:尧图企业网站定制
YOLOV4学习系列写到第三篇前面咱们拆过CSPDarknet53骨干结构聊过SPP和PANet的Neck设计今天终于轮到推理阶段的最后一道工序——非极大值抑制。NMS这个词很多同学早就不陌生了检测模型调试、比赛刷分、论文复现都绕不开它。但你去问YOLOV4为什么把传统NMS换成了DIOU-NMS能说明白的人就不多了。这篇文章我就结合我读源码以及亲手复现的实现把DIOU-NMS掰开揉碎讲一遍它到底救了什么问题、代码里每一步在算什么、阈值该怎么调以及我实际踩过的坑。内容以Python代码为主线不绑定任何具体框架做目标检测的工程师或学习者都能直接参考。1. 为什么YOLOV4要把NMS换掉传统非极大值抑制的软肋1.1 传统NMS在拥挤场景下的“误杀”传统NMS的执行流程其实非常简单先对某一类别的所有候选框按置信度降序排列然后选最高分的框作为基准跟剩下的框逐一计算IoU超过设定阈值就把它抑制掉接着在保留的框里重复这个过程直到处理完所有候选框。这个流程在目标稀疏的场景下没什么问题但一旦画面里出现遮挡和拥挤毛病就暴露了。我举一个很常见的例子两个人都挤在地铁车厢门口检测器分别给出了两个高置信度的框由于身体贴得近两个框的重叠面积很大IoU可能直接超过0.5。传统NMS此时就会认为这是同一个目标的重复检测把得分相对较低的那个框直接删掉。框一删漏检就来了。根因不在算法流程上而在IoU这个度量本身。IoU只描述两个框交集和并集的面积比例它完全不关心两个框的中心点在图像空间里的相对位置。换句话说两个大面积重叠的框既可能是同一个目标的冗余框也可能是两个目标挨得太近导致的重叠框IoU没法把这两种情况分开。传统NMS等于默认了一套隐式假设框重叠面积大就一定是同一个目标这显然在拥挤场景下站不住脚。YOLOV4作者很早就注意到这个问题。他们提出DIOU-NMS的思路也很直接既然只看面积不可靠那就把中心点距离也加进来作为判断依据。这也是这一系列文章里我反复强调的一点好的后处理不应该只是暴力过滤它要对检测结果的几何结构有更细致的理解。1.2 DIOU-NMS到底改了什么一句话概括DIOU-NMS把“是否抑制某个框”的依据从单纯的IoU变成了IoU减去一个中心点距离惩罚项。先看传统NMS的条件式IoU大于阈值就抑制。换成DIOU-NMS之后条件变成IoU减去中心点距离归一化值仍大于阈值才抑制。这里的语义差异非常关键。中心点距离惩罚项恒为非负值所以对于一个指定的候选框对同样的IoU情况下DIOU-NMS的抑制条件比传统NMS更苛刻。反之一个在传统NMS下本来会被抑制的框对只要它的中心点明显分开DIOU-NMS就倾向于放它一马。直观地理解就是两个框重叠面积很大但如果它们的中心点在图像空间里有明显偏移那就值得怀疑它们是两个不同目标不应该草率删除。这个改动没有增加任何可学习的参数也不改变模型推理的耗时结构纯粹后处理的公式替换工程上改造成本极低。这正是它的价值所在。2. DIOU-NMS原理拆解从IoU到Distance-IoU2.1 DIOU的核心思想重叠度加中心距离一个公式说清楚DIOU这个名词最早出现在边界框回归损失函数的设计里YOLOV4论文中也大量使用了CIOU Loss用于训练。DIOU的定义很简洁在IoU的基础上加一个中心点距离惩罚项L_DIoU 1 - IoU ρ²(b, b^gt) / c²其中ρ代表两个框中心点之间的欧氏距离c代表能够同时框住这两个框的最小外接矩形的对角线长度。分母的c²起到归一化作用让惩罚项不受图像绝对尺寸的影响取值落在0到1之间。这一点非常重要因为不同分辨率图像上的中心偏移绝对值差别很大不归一化的话根本无法统一设定阈值。举一个具体例子帮助你建立量感。假设两个框都是100乘100的正方形中心偏移20像素那么ρ²是400。两个框外接矩形的对角线大约170像素平方是28900惩罚项约等于0.014。中心偏移70像素时ρ²是4900惩罚项约等于0.17。可以看到中心偏移从20到70像素惩罚项从几乎不影响判定变成可以显著改变结果这个变化趋势正是DIOU-NMS要利用的。把这个公式搬到NMS里就是计算每对候选框的DIOU用DIOU替代IoU去跟阈值比较。YOLOV4的作者等于把一个原本用于训练回归的几何度量推广到了推理后处理阶段这个迁移背后的逻辑是通的训练时如果这个度量能更准确地反映框之间的真实距离那么推理时用它做去重同样会比单纯看交叉面积更合理。2.2 传统NMS与DIOU-NMS判定式的等价关系我把两个判定逻辑放在一起对比就能看得很清楚。传统NMS抑制条件IoU TDIOU-NMS抑制条件IoU - ρ² / c² T把这个不等式变形一下DIOU-NMS要求的实际是IoU T ρ² / c²也就是说中心点距离越远的两个框需要达到更高的IoU才会被抑制。这不等价于“松了”而是在IoU这个维度上动态地调整门槛。传统NMS里T是固定标量DIOU-NMS里阈值条件下额外叠加了一个跟框对几何关系相关的变量。这个变化的实际效果是让后处理在“删除冗余框”和“保留邻近目标框”之间找到更合理的平衡。同一个目标产生的冗余框中心点通常都落在目标中心附近彼此距离非常近惩罚项很小仍然会被正常抑制两个不同目标形成的重叠框中心点往往错开比较远惩罚项足够大就有机会保留下来。值得强调的一点是在很多常规检测场景下DIOU-NMS和传统NMS的差异其实并不大。因为IoU本身的定义就隐含了对中心偏移的约束两个高IoU的框很难在中心距离上差得非常远。差异真正放大的场景是细长目标、密集遮挡、目标尺寸差异大的情况。这也是为什么有些人在自己的数据集上试验DIOU-NMS后感觉“没什么变化”这跟算法实现没关系而是数据分布本身决定的。3. 结合代码逐行实现DIOU-NMS3.1 完整可运行的Python实现下面这份代码是我在实际项目里使用的版本去掉了跟具体框架绑定的部分只依赖NumPy。你把它单独保存成一个脚本输入一组坐标框和置信度分数就能直接得到NMS后保留的框索引。import numpy as np def diou_nms(boxes, scores, threshold0.5): if len(boxes) 0: return np.array([]) boxes boxes.astype(np.float64) order scores.argsort()[::-1] keep [] while order.size 0: idx order[0] keep.append(idx) if order.size 1: break rest order[1:] cur boxes[idx] rest_boxes boxes[rest] # 1. 计算相互之间的IoU xx1 np.maximum(cur[0], rest_boxes[:, 0]) yy1 np.maximum(cur[1], rest_boxes[:, 1]) xx2 np.minimum(cur[2], rest_boxes[:, 2]) yy2 np.minimum(cur[3], rest_boxes[:, 3]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) inter w * h area_cur (cur[2] - cur[0]) * (cur[3] - cur[1]) area_rest (rest_boxes[:, 2] - rest_boxes[:, 0]) * (rest_boxes[:, 3] - rest_boxes[:, 1]) union area_cur area_rest - inter iou inter / np.maximum(union, 1e-10) # 2. 计算中心点距离的平方 center_cur np.array([(cur[0] cur[2]) * 0.5, (cur[1] cur[3]) * 0.5]) center_rest (rest_boxes[:, :2] rest_boxes[:, 2:]) * 0.5 dist2 np.sum((center_rest - center_cur) ** 2, axis1) # 3. 计算最小外接矩形对角线长度的平方 enclose_x1 np.minimum(cur[0], rest_boxes[:, 0]) enclose_y1 np.minimum(cur[1], rest_boxes[:, 1]) enclose_x2 np.maximum(cur[2], rest_boxes[:, 2]) enclose_y2 np.maximum(cur[3], rest_boxes[:, 3]) diag2 (enclose_x2 - enclose_x1) ** 2 (enclose_y2 - enclose_y1) ** 2 # 4. 计算DIOU并决定保留还是抑制 diou iou - dist2 / np.maximum(diag2, 1e-10) keep_mask diou threshold order rest[keep_mask] return np.array(keep)读这份代码时重点是理解每一步在算什么指标而不是背代码。我把整个函数按计算顺序拆成了四个阶段下面逐一说明。3.2 关键步骤拆解每个数组运算在算什么第一阶段计算IoU。当前基准框cur会跟rest里所有候选框做一次矢量化交集计算。np.maximum和np.minimum负责约束相交区域当两个框在某个方向没有重叠时宽或高会变成0交集面积就是0。这里用np.maximum(0.0, ...)而不是直接截断是为了防止负值被带进后续面积计算。第二阶段算中心距离平方。center_cur是基准框的中心点center_rest是其余所有框的中心点。两者做差后取平方再按axis1求和得到的是每个剩余框跟基准框中心点欧氏距离的平方对应公式里的ρ²。这里全部用平方计算一个原因是距离平方在数学形式上跟DIOU定义一致另一个原因是能少一次开方运算推理阶段能省一点时间。第三阶段算外接矩形对角线长度平方。这步不能漏漏了就没有归一化不同尺寸的图中中心距离的绝对数值会完全不可比。enclose_变量取的是当前基准框和某个剩余框的并集边界然后按勾股定理算对角线平方对应公式里的c²。第四阶段算最终判定指标。diou iou - dist2 / diag2然后把diou小于阈值的框保留下来。注意保留条件是“小于”因为DIOU值越小说明惩罚越多说明更可能是两个独立目标。注意代码中使用np.maximum(union, 1e-10)以及np.maximum(diag2, 1e-10)是必要的防守措施。极端情况下两个框完全重合或尺寸为零时可能出现除零错误。工程实现里宁可多写一个max也不要在线上推理时暴露异常。3.3 阈值语义的变化与适配传统NMS里阈值0.5的语义非常直观两个框的面积重叠超过50%就被认为重复。DIOU-NMS里同样的0.5语义变成了“面积重叠减去中心距离归一化惩罚后仍然超过50%才抑制”门槛实际上变高了。这意味着你从传统NMS切到DIOU-NMS时不能继续沿用原来的阈值不做思考。我见过不少人在复现YOLOV4时只是把后处理函数换掉阈值还是0.5然后发现结果不涨反跌这其实不是DIOU-NMS的问题而是阈值没有重新适配。实际调参时可以参考这个规律如果原来传统NMS用的0.5总体效果是好的那么切到DIOU-NMS后在0.55到0.7这个区间里做一次小范围搜索往往能找到更优值。这个做法的道理在于DIOU-NMS多了一个非负惩罚项你需要把阈值稍微调高一点让整体抑制强度回到跟原来接近的水平同时把惩罚项带来的额外容差保留下来。4. 效果对比与阈值调优实测4.1 三种典型框关系的判定结果对比只看公式毕竟抽象我把一组模拟数据放进上面的代码里跑了一遍用三个典型场景说明传统NMS和DIOU-NMS的判定差异。假设阈值都为0.5。第一个场景是“重叠充分且中心接近”。两个框IoU是0.6惩罚项只有0.02最终DIOU约0.58仍然大于0.5传统NMS和DIOU-NMS都会抑制掉低分框。这是最常见的情况冗余框会被正确去掉。第二个场景是“重叠充分但中心明显偏移”。两个框IoU同为0.6但因为目标呈细长状中心偏移让惩罚项达到0.18最终DIOU约0.42小于0.5。传统NMS会直接删除低分框DIOU-NMS则选择保留因为它判断这更可能是两个邻近的独立目标。第三个场景是“重叠一般且中心接近”。IoU只有0.45惩罚项0.01DIOU约0.44两种情况都保留。这对应的是本来就不该被抑制的两个目标。我把这三个场景整理成一张表方便你对照理解。场景IoU惩罚项ρ²/c²DIOU传统NMS结果DIOU-NMS结果重叠充分中心接近0.60.020.58抑制抑制重叠充分中心偏移0.60.180.42抑制保留重叠一般中心接近0.450.010.44保留保留第二行就是DIOU-NMS真正发挥作用的地方也是你在遮挡密集场景下最希望看到的行为。第一行和第三行说明在多数常规场景下两种NMS并不会产生剧烈分歧DIOU-NMS不会颠覆原有后处理逻辑。4.2 阈值怎么调先看分布再定参数基于我的实际经验闭着眼睛调阈值效率太低。我建议你在验证集上做一次离线统计把那些“传统NMS下被抑制的框对”全部收集起来计算每对框的IoU和ρ²/c²然后把惩罚项的分布拉出来看。如果大多数被抑制框对的惩罚项集中在0.05以下说明你的数据里几乎没有需要靠中心距离来区分的遮挡案例DIOU-NMS的改动空间非常小这时候调阈值收益不高。如果数据里大量存在目标互相遮挡、密集排列的情况惩罚项分布会明显往0.1以上延展这时DIOU-NMS就有了发挥空间阈值也可以放心往上调。我实践里的参考做法是把验证集检测结果导出后分别跑传统NMS和DIOU-NMS对结果不一致的那部分框逐一可视化。这个方法比单看mAP涨跌更能说明问题你会发现被DIOU-NMS额外保留的框里有一些确实框住了被漏检的目标。看到这种画面你才能确认算法改动真正产生了价值。5. 常见问题与避坑实录5.1 为什么开了DIOU-NMS效果反而变差我刚开始在某个项目里把传统NMS换成DIOU-NMSmAP非但没涨还掉了0.3左右。当时第一反应是代码写错了于是把中间结果打印出来跟传统NMS逐框对比发现大部分框对判定结果一模一样只有少数框对被保留了下来。问题恰恰出在那些多保留的框上。我的数据是普通的道路车辆检测目标之间很少出现严重遮挡传统NMS的激进恰好帮我压制了一批低质量重复框。DIOU-NMS把其中一部分框放进来之后输出中低置信度预测变多了在置信度阈值设置较低的情况下假阳性也随之上升最终拖低了mAP。这个现象说明一个朴素但重要的道理后处理算法的价值取决于你的数据分布。如果数据本身没有大量的邻近目标遮挡问题DIOU-NMS带来的改动就不是“优化”而是“扰动”。在那种情况下坚持用传统NMS毫无问题不要因为论文里写了YOLOV4用DIOU-NMS就强行套到自己项目里。5.2 DIOU-NMS和Soft-NMS怎么选很多初学者会把DIOU-NMS和Soft-NMS搞混其实它们解决的问题不一样。Soft-NMS的核心是软抑制当某个框跟高置信度框重叠度较高时不直接删除它而是给它的置信度乘以一个衰减系数让它在后续过程中靠低分数自然失去竞争力。它保留信息的方式是改分数不是改几何判定。DIOU-NMS的核心是改“哪些框算重复”的判定标准它只在两个框是否相似这个维度上做更精细的区分。换句话说传统NMS和DIOU-NMS的结果都是保留还是删除二选一而Soft-NMS的结果是给每个框一个被衰减后的分数后续处理链路的输入是完全不同的。实际选型时我的经验是如果你的业务场景有大量密集目标且对漏检更敏感优先尝试DIOU-NMS因为它不改变输出分数分布部署时置信度阈值还能沿用原有逻辑。如果你对误检容忍度更低同时又有遮挡情况可以考虑Soft-NMS或者把两者结合成Soft-DIOU-NMS但组合后保留框会明显变多必须配合更高的置信度阈值否则后处理输出会很脏。5.3 工程实现里的几个细节坑第一个坑是精度问题。坐标值比较大的时候直接用float32计算中心点坐标精度损失会导致惩罚项出现微小偏差。我在实现里把boxes先转成float64虽然推理阶段稍微多了点内存开销但避免了一类非常难排查的数值抖动问题。第二个坑是坐标裁剪。有些检测头会输出超出图像边界的框如果不先裁剪到图像范围内两个框的最小外接矩形对角线会被异常拉大归一化惩罚项偏小导致本来应该被保留的框被抑制掉。所以进NMS之前一定要确保坐标都做完clip操作。第三个坑是类别混叠。YOLO的NMS是按类别独立执行的同样DIOU-NMS也必须是按类别独立执行。如果工程实现时把所有类别的候选框合并成一个数组跑NMS就会出现跨类别的互相抑制。举个极端例子旁边一辆自行车和一个行人检测框如果重叠度较高跨类别NMS会把其中一整个类别直接抹掉这是部署阶段非常容易犯的错误。第四个坑是对小目标要谨慎。小目标本身的框面积小中心点位置估算对边界偏移非常敏感一个像素的偏差就会让惩罚项产生显著波动。如果你的模型主要检测小目标DIOU-NMS可能带来不必要的扰动建议先在离线统计里验证惩罚项分布再决定是否启用必要的时候把惩罚项的权重系数调低比如乘一个0.5的缩放因子而不是直接用论文里的默认1。6. 一点个人体会与调试技巧我在复现YOLOV4指标的过程中最大的心得是后处理里没有一个改动是“银弹”。DIOU-NMS确实在遮挡场景下有提升但对数据分布不敏感的项目它的收益可能小到难以感知甚至会造成轻微倒退。判断你的项目适不适合DIOU-NMS最有效的动作不是反复调阈值而是先可视化和统计那些“被不同NMS版本区别对待”的框对看到具体案例之后再做决定。最后分享两个小技巧。如果你用的检测框架封装了NMS不方便直接改底层函数可以先跑一轮推理把网络输出的原始框和分数保存成npy文件然后用我上面的代码离线跑传统NMS和DIOU-NMS对比差异这个方法能快速定位到NMS本身的问题过滤掉训练和预处理环节的干扰。另一个技巧是调阈值时不要只盯mAP额外看一眼每个类别的召回曲线DIOU-NMS带来的提升经常体现在某个特定类别上整体指标平滑掉了不拆开看很难发现它到底有没有在起作用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑