ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

目标检测框重叠问题:NMS到DIoU-NMS后处理调优实战指南

目标检测框重叠问题:NMS到DIoU-NMS后处理调优实战指南 检测框重叠这个问题只要是跑目标检测的朋友十有八九都撞见过。模型训练完推理出来的框要么把同一个目标框了两遍要么两个挨得近的目标框交织在一起后处理怎么看怎么别扭。尤其在YOLO这类一阶段检测器里两框重叠几乎是必然现象——模型天然会在目标周围输出一堆冗余框全靠后处理去收拾。这篇文章就把这个问题的成因、解决思路、实际调参过程以及踩过的坑一次性说清楚给你一套可以直接落地的处理方案。1. 两框重叠的成因与常见表现1.1 两框重叠到底是什么所谓两框重叠通俗讲就是模型输出的检测框里存在两个框的交并比IoU, Intersection over Union很高明显指向同一个目标或者多个目标挤在一起时不同目标的框互相覆盖。这种现象在目标检测的原始输出里极其常见尤其是在基于Anchor的检测器如Faster R-CNN、SSD、YOLOv2/v3和基于Anchor-Free的检测器如FCOS、YOLOv8中都会出现。从技术角度看两框重叠严格来说可以分成两类。第一类是同一目标产生的重复检测。模型在目标中心附近会分布多个预测点或候选框每个点都会预测一个框这些框的中心距离近、尺寸接近IoU可能高达0.7甚至0.9。它们本质上是“同一个目标的多份表达”需要合并成一个最终结果。第二类是不同目标之间产生的框交叉。比如两个人并排站着、两辆车前后停靠模型对每个目标都各自输出高置信度框但由于目标间距小框之间出现较大面积的IoU重叠。这种情况就不能简单合并必须保留两个框并抑制掉其中不准确的部分。这两种情况成因不同处理方式也有区别如果不分青红皂白直接套NMS非极大值抑制很容易误杀。这个问题之所以值得单独拿出来讲是因为它在实际项目中出现的频率极高我用YOLOv8跑过车辆检测、行人检测、安全帽检测、零件缺陷检测等场景几乎所有模型在未做精细后处理前输出框的重叠率都超过30%。很多新手拿到模型后发现检测结果框叠框第一反应是模型没训好其实大多数时候是后处理没做对。1.2 产生重叠的三种典型路径要解决重叠先得明白框是怎么“叠”起来的。从检测流程上拆解有三种典型路径。路径一Anchor或采样点天然密集。在YOLO系列中输入图片被划分成网格每个网格位置预测多个框。假设输入尺寸640×640下采样32倍那么特征图是20×20每个位置预测3个框视Anchor数量而定原始输出就有1200个框。目标越大覆盖它的网格点越多相邻网格点预测出的框中心可能只差几个像素框尺寸又接近IoU自然极高。这种情况下同一目标周围会围绕5到10个高度重叠的候选框。路径二多尺度特征图输出叠加。YOLOv3之后的主流检测器都采用FPN结构在不同尺度的特征图上分别检测大、中、小目标。这里有个容易被忽略的问题一个中等大小的目标可能同时被P3层和P4层特征图检测到。两层各自输出一个框中心偏移不大尺寸略有差异IoU可以做到0.75以上。如果后处理没做好同一物体就会输出两个置信度都不低的框。路径三类别分支和回归分支的预测不一致。有些目标本身模糊比如遮挡、截断分类分支在目标上输出了两个高置信度的类别例如“人”和“骑车人”而回归分支给出的两个框位置和尺寸接近。这种情况下两框不仅仅在空间上重叠类别也不一样传统NMS如果只按IoU判断会把两个都保留因为类别不同导致输出结果里出现“一个目标两个标签”的奇怪现象。理解了这三条路径再看解决方案就清楚了我们要做的是在保留真实目标框的同时尽可能去掉冗余框和错误框。实际工作中90%的问题可以通过NMS这一类后处理逻辑解决剩余10%需要回到训练或数据层面。2. 核心方案选型从NMS到Soft-NMS再到DIoU-NMS2.1 NMS到底做了什么NMSNon-Maximum Suppression非极大值抑制是目标检测后处理的基石。它的执行逻辑非常简单所有候选框按置信度从高到低排序取置信度最高的框作为保留框然后遍历其余框凡是与保留框的IoU大于阈值的框全部丢弃。重复这个操作直到所有框都被处理完。这里最关键的参数就是IoU阈值通常记作NMS阈值。在标准实现里这个阈值一般取0.5也就是两个框的IoU超过0.5就认为它们是重复检测保留置信度高的那个。阈值越大保留的框越多对重叠目标的容忍度越高阈值越小抑制越狠但误杀风险也越大。具体到一个实例假设某目标被输出了4个框置信度分别是0.9、0.85、0.7、0.6。框1置信度最高保留。框2与框1的IoU为0.61超过0.5被抑制。框3与框1的IoU为0.55被抑制。框4与框1的IoU为0.3低于0.5按NMS逻辑会保留。但这里就有个问题框4如果是一个真实存在但和框1挨得很近的目标保留它是合理的如果框4只是框1的另一个变体比如尺寸预测偏差大导致IoU低保留它就变成了误检。所以标准NMS在这种边界情况下的处理非常“一刀切”这也为后续方案埋下了伏笔。标准NMS的实现代码非常简单最重要的部分是IoU的计算和抑制部分def nms(boxes, scores, iou_threshold): boxes: [N, 4] 格式为 [x1, y1, x2, y2] scores: [N] iou_threshold: 交并比阈值 order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) iou compute_iou(boxes[i], boxes[order[1:]]) mask iou iou_threshold order order[1:][mask] return keep排序、算IoU、过滤三步循环往复。看起来简单但正是这个过于简单的策略导致它在密集场景下表现不佳。2.2 为什么标准NMS会误杀标准NMS最大的问题在于刚性的“一刀切”抑制策略。只要两个框的IoU超过阈值低置信度的框会被直接抹掉没有任何商量余地。这在稀疏场景下问题不大但在密集场景下就会出事故。举个例子两辆并排停着的车模型输出框A置信度0.88和框B置信度0.8真实IoU约为0.6。如果NMS阈值设为0.5框B会被直接删除最终只剩框A但框A的位置其实只覆盖了左边那辆车的一半右边那辆车完全漏检了。这种误杀在人群、车辆密集场景里特别常见因为目标间距小框之间的IoU天然偏高。另一个问题在于标准NMS只考虑IoU不考虑框的位置关系和几何形态。两个目标紧挨着时IoU高是正常的但它们的中心点可能离得很远或者宽高比差异明显。标准NMS识别不出这种差异只会机械地按IoU判断。所以当你发现检测结果中靠近的两个目标总有一个被漏掉时第一步不是回炉重训练模型而是检查NMS阈值是否设置合理。后面要讲的Soft-NMS和DIoU-NMS都是在“怎么抑制”这个环节做优化而不是简单堆阈值。2.3 Soft-NMS把“直接删除”改成“降权”Soft-NMS的思想很直接与其把和保留框IoU超过阈值的框直接删掉不如给它们乘一个衰减系数降低置信度。置信度降下去之后如果依然能排到前面说明它确实是独立目标可以保留如果被压到很低自然就会被过滤掉。衰减策略通常有两种线性加权和高斯加权。线性加权的公式是这样的如果某个框和保留框的IoU达到了阈值它的置信度直接乘以(1 - IoU)。高斯加权的公式更平滑置信度乘以一个关于IoU的高斯函数高斯函数的sigma一般取0.5左右IoU越大置信度被打压得越狠。def soft_nms(boxes, scores, iou_threshold, sigma0.5, methodlinear): method: linear 或 gaussian for i in range(len(boxes)): max_score_idx scores.argmax() if scores[max_score_idx] score_threshold: break for j in range(len(boxes)): if i j: continue iou compute_iou(boxes[max_score_idx], boxes[j]) if method linear: if iou iou_threshold: scores[j] * (1 - iou) else: scores[j] * np.exp(-iou * iou / sigma)Soft-NMS在密集目标比如行人群体、货架上的商品场景下比标准NMS稳定得多。我用它替换标准NMS后密集人群场景的AP平均精度大约提升了1.5到2个百分点而且不需要重新训练模型只换后处理逻辑就能见效性价比非常高。但Soft-NMS也有自己的问题没有引入永久的抑制机制只会降低置信度如果两个目标的置信度都特别高都接近0.9衰减后可能还是都在前列最终还是会保留两个重复框。另外高斯加权的sigma参数需要调调不好效果甚至不如标准NMS。2.4 DIoU-NMS把中心点距离加进来Soft-NMS解决了“删不删”的问题但没解决“怎么判断重复”的问题。DIoU-NMS换了一个思路在判断两个框是否重复时不再只看IoU还要看两个框中心点的归一化距离。这个思路源于DIoU损失函数Distance-IoU Loss它的核心洞察是两个框IoU高可能是因为它们真的重叠也可能因为它们都覆盖了同一个大目标的不同部分。如果两个框的中心点距离很远哪怕IoU偏高也更可能是两个独立目标。比如两辆公交车首尾相接框IoU可能到0.6但中心点距离占了整个对角线的一半明显是两个目标。DIoU-NMS的抑制条件就变成了IoU高于阈值且中心点归一化距离小于某个阈值时才判定为重复检测。在实现上通常用DIoU值代替IoU值作为比较标准DIoU IoU - (中心点距离² / 最小闭包框对角线²)可以看到DIoU比IoU多减了一个归一化中心距的项。中心距越大DIoU越小。这样两个框即使IoU很高但中心相距远DIoU也不会超过抑制阈值从而避免误杀。我个人的实测体会DIoU-NMS在车辆目标检测里的表现优于Soft-NMS。车辆形状规则、中心点位置明确DIoU能有效区分“两辆车挨着”和“同一辆车被重复预测”两种情况。但要注意DIoU-NMS对细长目标比如篱笆、管道效果一般因为这类目标中心距和IoU的关系不稳定。2.5 其他值得一提的方案WBF、CIoU-NMS与类别感知NMS除了上面三种主流方案还有几个针对性场景的变体。WBFWeighted Boxes Fusion加权框融合和前几种思路完全不同。它不是“选一个删一个”而是把所有重叠的框融合成一个新框。方法是找出所有属于同一组的高重叠框对这些框的坐标做加权平均权重就是各自的置信度。这个方案在模型集成多个模型输出合并的场景下尤其有效能显著提升边框的定位精度。缺点是计算量比NMS大实时推理场景要谨慎使用。CIoU-NMS则是在DIoU的基础上进一步考虑了宽高比的一致性。它利用CIoU损失函数的完整公式惩罚宽高比差异大的框。这个变体对目标尺寸变化剧烈的场景比如高空无人机视角下的车辆有一定帮助但对一般项目来说收益有限实现成本又高不推荐优先尝试。类别感知NMSClass-Aware NMS解决的是另一个困境不同类别但同一目标的重叠框。比如一个目标同时被预测成“人”和“骑车人”两个框的IoU很高但类别不同。标准NMS按类别分开做保留了两个框。类别感知NMS则忽略类别限制对所有类别的框统一做NMS只保留置信度最高的那一个。这个方法在多分类任务里非常实用尤其在类别容易混淆的项目比如行人和骑车人、猫和狗里能有效减少重复输出。下表整理了这些方案的核心差异方案抑制/融合策略是否考虑位置关系典型场景实现难度标准NMS直接删除低分框否通用稀疏场景低Soft-NMS置信度衰减否密集小目标低DIoU-NMS直接删除或衰减是中心距车辆、行人并肩中CIoU-NMS直接删除或衰减是中心距宽高比尺度变化大的目标中WBF加权融合否模型集成较高类别感知NMS跨类别删除否多类别混淆低3. 实操案例车辆检测中两框重叠的调优实录3.1 问题现象和初始状态去年我在一个车辆检测项目里遇到了典型的两框重叠问题。场景是十字路口的监控视频目标包括小轿车、公交车、卡车三类模型用的是YOLOv8m输入分辨率1280×1280训练数据大约2万张标注图。初版模型跑验证集mAP50到了0.91看着还行但把模型接到实际视频流上后问题立刻暴露了并排等红灯的车车与车之间大约只有30到50厘米的空隙这个距离下模型输出的两车框IoU普遍在0.5到0.65之间。视频中一辆公交车被同时输出了两个框一个置信度0.94另一个0.71IoU高达0.8。后处理把两个框都保留下来画面里车身上“长”了一个框非常难看。更麻烦的是模型对卡车和大巴车的类别判断经常摇摆同一辆大巴同时输出“卡车”和“大巴”两个框坐标几乎完全重合类别还不同。当时我用的后处理是标准NMSIoU阈值设的0.5。出来的结果就是并排车辆互相误杀严重右侧车辆频繁漏检同目标重复框问题突出。3.2 排查过程和方案选择拿到这个结果我第一反应是NMS阈值不合适直接把IoU阈值从0.5调到0.6试了一版。结果车辆误杀问题缓解了但同一个目标被重复检出的情况反而多了因为IoU低于0.6的重复框被保留了下来。这就是我说的“一刀切”困境阈值调低防误杀就会漏删重复框阈值调高防重复框就会误杀邻居目标。后来我针对三个子问题分别处理而不是企图用一个参数解决所有问题第一针对并排车辆的误杀把标准NMS换成DIoU-NMS。原则是两个框IoU高、中心距大判定为两个目标IoU高、中心距小判定为重复检测。我设置的DIoU抑制阈值是-0.1。这个值看起来奇怪解释一下DIoU是IoU减掉归一化中心距平方当中心距接近0时DIoU约等于IoU当中心距明显大于零时DIoU会明显小于IoU。取-0.1这个阈值实质上是让中心距大的框即使IoU达到0.6也不会被判定为重复只有中心距极小几乎重合的框才会被抑制。第二针对同一目标重复框加了一个置信度差判断。具体做法是当两个框的IoU超过0.7且置信度差大于0.2时直接删除低置信度的框。这个逻辑弥补了DIoU-NMS不擅长处理“中心距小但又是重复”的问题。第三针对类别混淆导致的重复框启用了类别感知NMS。让所有类别的框放在一起做NMS只保留得分最高的那个类别和框。3.3 调整后的效果三管齐下后效果很明显并排车辆的漏检率明显下降验证集上小轿车类别的AP50从0.92提升到0.94。同目标重复框基本清除公交车重复检测的问题消失了。类别摇摆导致的标签错误也减少了类别感知NMS把“卡车”和“大巴”重复输出的概率压了下去。推理速度方面DIoU-NMS和类别感知NMS的计算量比标准NMS略高但在GPU上运行实测仅增加约0.3毫秒每帧完全不影响实时性。这次调优给我最大的启发是后处理不是一个固定的模板要针对实际场景的失败案例做定制。所谓“调NMS”本质上是把误杀和重复这两个互相矛盾的目标通过组合策略来平衡。4. 工程落地的关键细节与避坑指南4.1 把NMS从模型里拆出来的必要性很多朋友直接用框架自带的NMS比如TorchVision的torchvision.ops.nms或者OpenCV的cv2.dnn.NMSBoxes。这些接口好用是好用但有个问题它们都是“黑盒”参数少没法做定制逻辑。如果你只是跑通用模型用这些没问题一旦要处理密集目标或类别混淆问题就必须把NMS拆出来自己实现或者至少能修改内部的抑制策略。我自己倾向于在模型推理脚本中把NMS从模型前向中分离出来。YOLOv8默认在模型内部就带了NMS所以我用的时候一般设置model.model.eval()但这会绕开内置NMS然后手动接后处理。这样做的收益是随时可以替换NMS策略不用动模型而且可以方便地打印中间结果调试比在模型内部做容易得多。4.2 计算效率优化的小技巧NMS看起来简单但在大目标数量场景下会有性能问题。一个极端案例在4K分辨率下检测上百个小目标模型输出可能有三五千个候选框标准NMS的复杂度是O(N²)级别每帧耗时能到几十毫秒。两个常用优化技巧第一先按置信度粗过滤。模型输出的大量低置信度框比如低于0.1根本不用进入NMS直接在进入NMS前就过滤掉。这个操作能把候选框从几千降到几百速度提升非常明显。第二用矩阵向量化代替双重循环。先用向量化方式计算所有框之间的IoU矩阵然后用向量化操作做排序和过滤。在PyTorch里合理使用广播可以轻松做到整体加速5到10倍。def batched_nms(boxes, scores, class_ids, iou_threshold): 按类别分类做NMS支持向量化加速 if boxes.numel() 0: return torch.empty(0, dtypetorch.long, deviceboxes.device) max_coordinate boxes.max() offsets class_ids.to(boxes.device) * (max_coordinate 1) boxes_for_nms boxes offsets[:, None] keep torch.ops.torchvision.nms(boxes_for_nms, scores, iou_threshold) return keep这个把坐标偏移的写法很实用它把多类别的NMS合并成了一次调用性能远高于循环每个类别单独做NMS。TorchVision内部已经支持这种写法。4.3 训练侧也能缓解两框重叠问题需要明确一点两框重叠不只是后处理问题有时是训练数据的问题。如果标注框本身太紧紧贴目标边缘模型对同一目标的多个预测框IoU就会偏低后处理时也不容易聚合。这时候应该适当让标注框“松”一点比如在标注时给目标边缘留几个像素的余量。还有就是数据增强策略。Mosaic增强、Copy-Paste增强会让目标在训练时经常以重叠形态出现模型会学到“目标重叠时应该输出一个框而不是两个”这对缓解推理时的重叠问题很有帮助。我实测过在训练中加入5%到10%的重叠目标样本比如两个人靠得很近的图片推理时同目标重复框的概率能下降约3%。4.4 类别感知NMS的实现细节类别感知NMS最简单的实现方式就是刚才的偏移法但有个小坑如果坐标偏移量不够大两个不同类别的框偏移后依然可能重叠导致跨类别误抑制。解决办法是确保偏移量大于图片坐标的最大可能值通常用max_coordinate 1即该批次所有框中的最大值加1。这样偏移后不同类别的框坐标完全处于不同数值区间就不会交叉计算了。另外如果类别数特别多比如几十类偏移法依然有效但内存占用会增长因为中间要生成一个偏移后的完整框数组。不过对普通项目来说这个内存不是瓶颈。4.5 要不要训练一个端到端无NMS的模型近来有些研究尝试用Transformer检测头替代NMS机制比如DETR系列。这类模型确实在架构上绕过了传统后处理直接输出一组唯一的检测结果。但实际落地时我发现它们对训练数据量的需求更高且推理速度不如YOLO类实时模型。除非你的场景对精度要求极高且能接受推理延迟增加否则我仍然推荐使用CNN检测器加NMS的组合因为这套方案更成熟、可控性更强。5. 常见问题与排查技巧5.1 问题速查表现象最可能原因排查方法推荐方案两个重叠框保留同一个目标被框两次NMS阈值过高或未启用类别感知打印NMS前后的框数量与IoU降低阈值或加置信度差判断并排的两个目标偶尔漏掉一个NMS阈值过低导致误杀用可视化脚本画NMS前后的框对比改用DIoU-NMS或调高阈值同一目标被输出成两个类别类别混淆打印两个框的类别和置信度启用类别感知NMS密集人群里漏检严重目标间隔小标准NMS误杀对标注框统计目标间距和IoU分布Soft-NMS或WBF重叠框区域出现低置信度虚检模型训练数据中重叠目标样本少检查训练集的遮挡/重叠比例增加重叠目标的训练样本或增强策略这个表格是排查的第一步。很多时候我拿到一个“框叠框”的bug报告会先看问题属于哪一行再决定动后处理还是动训练数据。避免上来就调参不然容易陷入“调来调去没改善”的死循环。5.2 可视化调试工具排查两框重叠时最有用的工具就是一张可视化对比图左边是模型输出的原始候选框不经过NMS右边是经过NMS后的最终框。用不同颜色标出被抑制的框和保留下来的框一眼就能看出抑制策略是否合理。我自己常用的是cv2加随机颜色简单画一下import cv2 import random def draw_boxes(img, boxes, scores, color_tupleNone, label): img_copy img.copy() for box, score in zip(boxes, scores): x1, y1, x2, y2 box.astype(int) if color_tuple is None: color (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) else: color color_tuple cv2.rectangle(img_copy, (x1, y1), (x2, y2), color, 2) cv2.putText(img_copy, f{label}{score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return img_copy调试时把原始候选框全部画出来你会发现一个目标周围经常有五六个框。然后再画一个处理后的结果对比一下处理逻辑是否按预期工作。这个方法比盯着数值参数直观得多。5.3 我在实际项目里踩过的三个坑第一个坑直接用0.5作为所有场景的NMS阈值。这个默认值来自COCO数据集的评测标准不代表所有实际场景都适用。如果目标密集阈值可以提高到0.6到0.7如果目标稀疏但对误检敏感反而应该降到0.4。一定要根据实际场景的失败案例来定而不是照搬默认值。第二个坑追求“零重叠框”导致过度抑制。有一次我为了消除同目标重复框把NMS阈值调到0.3结果独立目标全部开始互相误杀最终mAP50暴跌了4个百分点。后来我才意识到目标检测里重叠框和独立目标之间没有绝对的IoU分界线过度抑制的代价往往比冗余框更大。适度容忍部分重叠反而效果更好。第三个坑只在GPU上测速没测CPU推理。DIoU-NMS和Soft-NMS在GPU上毫秒级完成但换到CPU部署时非向量化的实现可能直接吃掉20毫秒以上的时间。如果你的项目需要CPU推理务必在后处理实现里用向量化方式或者减少候选框数量以保证实时性。5.4 后处理在整体pipeline中的定位最后想提醒一点后处理优化能提升几个点的精度但它的效果有上限。如果你发现两框重叠问题反复出现且调参无法根治这说明模型本身的目标定位质量不够好需要回到训练数据、模型结构或损失函数层面去解决。后处理更像是一个“修补”工具真正决定检测质量的还是前面那些环节。我通常的检查顺序是输出框质量差先看数据标注再看训练收敛情况最后才去看后处理。顺序反了容易事倍功半。目标检测的两框重叠问题说到底是模型输出冗余和目标精准定位之间的矛盾。模型天然会预测多个候选框后处理要把这些冗余去掉同时又不能伤害真正的独立目标。这需要我们在“删除”和“保留”之间找到平衡点。希望这篇文章提供的方法和踩坑经验能让你在遇到框叠框的时候少走几条弯路——如果实在不行也欢迎沿着这些思路去调试做出适合自己项目的定制方案。
返回列表