news 2026/10/13 20:34:28

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOV4后处理解析:从NMS到DIOU-NMS的原理与Python实现

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-IoU

2.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 > T

DIOU-NMS抑制条件:

IoU - ρ² / c² > T

把这个不等式变形一下,DIOU-NMS要求的实际是:

IoU > T + ρ² / c²

也就是说,中心点距离越远的两个框,需要达到更高的IoU才会被抑制。这不等价于“松了”,而是在IoU这个维度上动态地调整门槛。传统NMS里T是固定标量,DIOU-NMS里阈值条件下额外叠加了一个跟框对几何关系相关的变量。

这个变化的实际效果是让后处理在“删除冗余框”和“保留邻近目标框”之间找到更合理的平衡。同一个目标产生的冗余框,中心点通常都落在目标中心附近,彼此距离非常近,惩罚项很小,仍然会被正常抑制;两个不同目标形成的重叠框,中心点往往错开比较远,惩罚项足够大,就有机会保留下来。

值得强调的一点是,在很多常规检测场景下,DIOU-NMS和传统NMS的差异其实并不大。因为IoU本身的定义就隐含了对中心偏移的约束,两个高IoU的框很难在中心距离上差得非常远。差异真正放大的场景是细长目标、密集遮挡、目标尺寸差异大的情况。这也是为什么有些人在自己的数据集上试验DIOU-NMS后感觉“没什么变化”,这跟算法实现没关系,而是数据分布本身决定的。

3. 结合代码逐行实现DIOU-NMS

3.1 完整可运行的Python实现

下面这份代码是我在实际项目里使用的版本,去掉了跟具体框架绑定的部分,只依赖NumPy。你把它单独保存成一个脚本,输入一组坐标框和置信度分数,就能直接得到NMS后保留的框索引。

import numpy as np def diou_nms(boxes, scores, threshold=0.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, axis=1) # 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是其余所有框的中心点。两者做差后取平方,再按axis=1求和,得到的是每个剩余框跟基准框中心点欧氏距离的平方,对应公式里的ρ²。这里全部用平方计算,一个原因是距离平方在数学形式上跟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.01,DIOU约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-NMS,mAP非但没涨,还掉了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带来的提升经常体现在某个特定类别上,整体指标平滑掉了,不拆开看很难发现它到底有没有在起作用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 20:28:48

C++与MFC实现一元高次方程求解桌面程序:数值算法与工程实践

简介&#xff1a;这份资源是面向C与MFC初学者及Windows桌面开发者的解一元高次方程完整项目&#xff0c;包含可编译源码与已编译程序&#xff0c;帮助读者理解如何将数学求解算法落地为图形界面应用。压缩包共43个文件&#xff0c;约58.77MB&#xff0c;涵盖cpp与h源文件、vcxp…

作者头像 李华
网站建设 2026/10/13 20:26:58

保险银行项目软件测试Bug汇总:状态、金额、并发与接口排查

最近总有人问我&#xff0c;做软件测试到底什么项目最难测。我的回答一直没变&#xff1a;保险和银行。这两类系统业务规则密集、金额敏感、状态流转复杂&#xff0c;外部接口又多&#xff0c;很多bug在测试环境一测就过&#xff0c;一上线就翻车。我把自己这些年攒下的软件测试…

作者头像 李华
网站建设 2026/10/13 20:24:26

实测RustFS:S3兼容性承诺到底有几分真?

上半年做存储选型&#xff0c;我几乎把所有号称支持对象存储的方案都过了一遍。最让我警惕的&#xff0c;就是项目介绍里那几个轻飘飘的字&#xff1a;S3 兼容。很多项目把“S3 Compatible”当作一块万能招牌&#xff0c;实际上你拿标准客户端一测&#xff0c;连最基础的分片上…

作者头像 李华
网站建设 2026/10/13 20:23:23

数据搬运卡顿真凶:Host-to-Host拷贝次数深度解析与优化

先说结论&#xff1a;很多从标称带宽上算出来“应该没问题”的数据搬运场景&#xff0c;实际跑起来却卡得让人摸不着头脑。我最近在处理一个跨主机数据搬运的调优问题时&#xff0c;就遇到了这种情况。链路侧 PCIe 的有效带宽并不差&#xff0c;内存侧 DDR 带宽看起来更充裕&am…

作者头像 李华
网站建设 2026/10/13 20:21:14

RHINO基础教程:NURBS建模与曲面连续性全流程解析

简介&#xff1a;面向Rhino零基础读者的入门级教程文档&#xff0c;按章节系统讲解软件安装激活、工作流程与基本概念、界面与导航操作、二维图形绘制、基础曲面创建与编辑等核心内容&#xff0c;帮助初学者从零掌握NURBS建模思路。资源为单个doc文档&#xff0c;压缩包大小847…

作者头像 李华
网站建设 2026/10/13 20:21:01

AI辅助数学研究的方法论:从验证成本到人机协作的工程实践

1. 一场访谈引发的思考&#xff1a;AI 做数学到底靠不靠谱前阵子数学圈有个访谈在技术社区传得挺开&#xff0c;一位拿过顶级数学奖项的学者聊了聊自己用 AI 辅助数学研究的真实体验。我第一时间把相关内容翻了几遍&#xff0c;越看越觉得有意思——不是因为里面有什么惊天动地…

作者头像 李华