做物体检测的人大概率都有过这种经历模型结构改了一版又一版训练技巧拉到满可mAP就是差那么零点几个点最后排查来排查去问题居然出在后处理——NMS阈值调得太狠把本该留下的框连坐删掉了或者阈值太松重复框比检测结果还密。说实话NMS这玩意儿表面上就是几十行代码的“暴力去重”但它确实是检测器性能的一块隐形天花板。同样一套检测模型后处理写得糙和写得精细最终指标能差出1到2个点这在榜单上往往就是两三个名次的差距。这篇文章我就把NMS这条线完整捋一遍标准NMS、softNMS、softerNMS、IoU-Net这四代方案分别解决了什么问题、原理是什么、代码怎么改、实际落地效果如何。无论你是刚入坑的初学者还是已经在调检测模型的老手都应该能从里面找到点有用的东西。1. 先搞清楚一件事NMS到底在解决什么问题1.1 一个典型检测头的输出到底长什么样在讨论NMS之前得先回到检测器的输出端。无论是两阶段检测器Faster R-CNN、Cascade R-CNN还是单阶段检测器YOLO、SSD、FCOS推理时的原始输出绝对不是一张干净的“框好类别的图”而是一大堆候选框的集合。抛开不同检测器的细节差异最终输出通常包含四类信息目标的类别概率或者分类 logits用于判断这个框属于哪个类目标的边界框坐标通常表示为 x1, y1, x2, y2或者中心点加宽高目标置信度分数分类分数或者 objectness 分数用于衡量这个框有多可信一些辅助信息比如 Mask 分支的 mask或者姿态估计里的 keypoints关键在于一个目标物体在图像上会被多个锚点或多个滑动位置同时“看见”于是检测头会输出大量重叠的候选框。以 Faster R-CNN 为例RPN 在一张图上可能生成几千个 proposal经过分类和回归后仍然会存在大量针对同一目标的“重复框”。这些框彼此之间的 IoU 可能从 0.3 到 0.9 不等如果不做任何后处理画出来的图就是一团糊同一个行人被十几二十个框套着。1.2 没有NMS会怎样重复框的灾难现场可以做一个简单的思想实验假设一张图片里有一个行人检测器输出了 10 个框这 10 个框都大致落在行人身上IoU 两两之间都超过 0.5每个框的分类分数分别是 0.95, 0.93, 0.90, 0.88……一直到 0.76。如果没有 NMS这张图的最终输出就是这 10 个框全部报出来。评估时检测器会说“这里有 10 个目标”但实际上只有一个行人。mAP 的召回率没有提升但精确率被严重拉低。在真实的业务场景里更灾难比如工业质检同一个缺陷零件被重复框选后台统计就会误判成多倍数量的缺陷。NMS 的核心目标非常朴素在一堆重叠的候选框中只保留最有代表性的那一个把其他“重复表达同一个目标”的框全部干掉。这里的“最有代表性”最直观的定义就是分类分数最高的那个。所以NMS 的本质是一个去重算法它的输入是检测器的原始输出输出是一个精简后的检测结果集合。正是因为它处在整个检测流程的最后一环所以它对最终效果的影响极其直接粗暴——前面的卷积、特征金字塔、注意力机制再怎么卷最后这一刀要是切错了前面的努力全部白费。2. 标准NMS每一代检测器都绕不开的“暴力去重”2.1 NMS的核心流程与理解方式标准NMS算法其实几句话就能说清楚用“锦标赛淘汰制”来理解最直观第一步所有候选框按照分数从高到低排序最高分的框直接晋级因为它最可能是一个真实目标。第二步把晋级框和剩下的框逐一计算 IoU。如果一个剩余框和晋级框的 IoU 超过设定的阈值说明它大概率是在同一个目标上产生的冗余框直接淘汰。第三步重复这个过程每次都从剩余框里选分数最高的框晋级直到所有框都被处理完。用伪码表示就是这样输入候选框集合 B对应分数集合 S阈值 Nt 输出保留的框集合 D 初始化 D 为空 循环直到 B 为空 从 S 中取出最高分对应的框 M 从 B 中移除 M加入 D 对 B 中每一个框 b 计算 b 与 M 的 IoU 如果 IoU Nt 从 B 中移除 b从 S 中移除对应分数 返回 D这个算法的核心参数只有一个IoU 阈值 Nt。它表达了一个假设两个框的 IoU 超过阈值就认为它们“指代的是同一个目标”。这个假设看起来没问题但实际应用中坑非常多这部分到第三章详细说。2.2 一段可以直接抄的标准NMS实现用 Python 实现一个标准 NMS 非常直白。下面这段是经典写法网上流传的大部分版本都是它的变体import numpy as np def nms(dets, thresh): Pure Python NMS baseline. x1 dets[:, 0] y1 dets[:, 1] x2 dets[:, 2] y2 dets[:, 3] scores dets[:, 4] areas (x2 - x1 1) * (y2 - y1 1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) # 计算当前最高分框与其余所有框的IoU 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:]]) w np.maximum(0.0, xx2 - xx1 1) h np.maximum(0.0, yy2 - yy1 1) inter w * h ovr inter / (areas[i] areas[order[1:]] - inter) # 保留IoU小于阈值的框 inds np.where(ovr thresh)[0] order order[inds 1] return keep有两点需要注意第一计算面积时为什么要加1如果是像素坐标一个框从坐标 (x1, y1) 到 (x2, y2)它实际占据的像素宽度是 x2 - x1 1而不是 x2 - x1。加 1 是为了把离散坐标映射回连续的面积。虽然对于最终排序来说少加一个 1 影响不大但如果做严格的指标复现这个细节可能造成千分之几的差异。第二order[inds 1]这个下标的偏移。因为ovr是当前框和order[1:]之间的 IoU所以inds对应的索引序号是基于order[1:]的。需要加 1 才能映射回order的原始位置。这是新手最容易抄错的地方一旦写错结果会完全乱掉。2.3 阈值到底怎么调从0.3到0.7之间的权衡NMS 的 IoU 阈值是一个典型的天平参数。阈值设得低比如 0.3条件更严格两个框只要交叠稍微多一点就被干掉一个。这样输出的框数量少、重叠少但如果两个真实目标恰好挨得很近一个框就会被误删导致召回率下降。阈值设得高比如 0.7条件宽松只有高度重叠的框才会被抑制召回率有保障但输出框的重叠度也高可能会把同一个目标的多个框都报出来拉低精确率。在 COCO 数据集上标准的评价协议默认使用 0.5 到 0.95 的多个 IoU 阈值来算 mAP但后处理阶段的 NMS 阈值一般取 0.5 左右。实际业务里怎么调一定要结合场景行人检测、密集人群建议 0.4 到 0.5因为行人间相互遮挡严重阈值太高会导致大量重复框遥感图像中的密集小目标建议 0.3 到 0.5小目标之间的相对距离更近IoU 很容易超阈值通用目标检测0.5 是一个比较稳妥的起点COCO 上大多数检测器也是用 0.5需要特别注意的是NMS 阈值和数据集评估时的 IoU 阈值完全是两个概念。NMS 阈值是后处理去重时的判定标准而评估时的 IoU 阈值是判断“预测框是否和 GT 匹配”的标准。很多人一开始会把这两个搞混调试时一头雾水。3. 标准NMS的三个致命痛点阈值焦虑、遮挡失效、分数错位3.1 IoU阈值是“一锤子买卖”不同场景最优值完全不同标准 NMS 最大的问题在于那个阈值Nt是一个全局固定值。不管图像里是大目标还是小目标、密集场景还是稀疏场景、目标之间是相互独立还是高度重叠用的都是同一个阈值。这就导致了一个很尴尬的局面阈值调优变成了一场“拆东墙补西墙”的游戏。把阈值调低一点稀疏场景效果变好了但密集场景的召回率立刻崩把阈值调高一点密集场景保住了但输出里到处都是重复框精确率往下掉。我在实际项目里曾经调过一批安全帽检测的数据场景非常单一工地上的人基本都在画面里距离摄像头远近不同密集程度差异很大。远距离时目标小彼此不重叠NMS 阈值调到 0.3 就够用但到了进出闸机这种场景好几个工人挤在一起框与框的 IoU 经常到 0.6 以上阈值 0.3 会直接把旁边工人的框删掉。最后我只能折中取 0.45两边都牺牲一点但这个问题始终没有根治。3.2 密集遮挡场景抑制过头把该留的框也删了标准 NMS 的抑制逻辑是硬抑制一旦评判两个框是“同一个目标”另一个框的分数直接置 0完全从候选集合里消失。这种“零容忍”策略在密集遮挡场景下非常致命。举一个非常典型的例子三个人并排站着检测器在每个人身上都产生了多个候选框其中一个人身上的次级候选框分数 0.8和旁边一个人的主要候选框分数 0.85之间有 0.55 的 IoU。如果 NMS 阈值是 0.5那么旁边那个人的主要候选框就会被“误杀”因为它的分数比前一个人身上的次级框低一点。这种错误在标准 NMS 里是不可逆的——被置 0 的框直接出局没有任何“申诉”机制。在 COCO 的 mAP 评估中这种误杀会导致两个后果同时出现一个是本来正确的检测结果不见了召回率下降另一个是这个误删的框不可能再被匹配到 GT 上精确率也受影响。密集场景下NMS 引入的损失甚至可能超过网络本身引入的损失。3.3 分类分数≠定位质量NMS排序依据本身就是错的标准 NMS 用分类分数作为“框好坏”的衡量标准但这隐含了一个非常强的假设分数越高的框定位越准。但在实际训练中分类分支和回归分支是相对独立的它们的输出没有天然的强相关性。一个分类分数 0.95 的框其定位可能与 GT 差了一大截而旁边一个分类分数 0.88 的框却可能正好精准框住了目标。这就是所谓的 classification score 和 localization quality 之间的**错位misalignment**问题。这个错位对 NMS 的影响是致命的因为 NMS 每一步都从当前候选集合里挑“最高分”的框作为基准去抑制其他框。如果这个“最高分”框本身定位就很差那么它非但不能代表真实目标反而会把真正定位精准的框全部抑制掉。打个比方一个队长自己站错了位置还要求所有跟着他的人也站到错误的位置上队友但凡站得稍微准一点就被他“淘汰”了。最后整个队伍覆灭而真正站在正确位置上的队员一个都没留下来。这个痛点正是后面 softerNMS 和 IoU-Net 想要解决的核心问题。4. softNMS与其物理删除不如让分数“软着陆”4.1 核心思想IoU越大分数衰减越狠softNMS 发表于 ICCV 2017作者是 Navaneeth Bodla 等人。它的核心改进思路非常简洁标准 NMS 发现某个框 A 和当前最高分框 M 的 IoU 超过阈值时直接删掉 AsoftNMS 则是不删框只降分。根据 IoU 的大小衰减的程度不同。两个框重叠得越厉害说明它们是同一个目标的可能性越大分数就衰减得越狠重叠得没那么厉害分数衰减得就少一些。这样在密集场景中那些和最高分框有一定重叠、但可能属于另一个目标的框虽然分数下降了但没有被直接“杀死”只要最后分数仍高于输出阈值它就还有机会被保留下来。4.2 线性加权与高斯加权的实现细节softNMS 论文里给出了两种衰减函数线性加权score score * (1 - iou) 当 iou Nt 时 score score 当 iou Nt 时高斯加权score score * exp(-iou^2 / sigma)高斯版本对所有的重叠框都做衰减不需要设置阈值Nt只需要调一个超参数sigma。论文里的实验建议sigma取 0.5实测下来高斯版本通常比线性版本稳定一点但注意线性版本在密集场景中的可解释性更好因为它的衰减范围和标准 NMS 直接对应。代码上softNMS 对比标准 NMS 的改动非常小def soft_nms(dets, sigma0.5, Nt0.3, threshold0.001): x1 dets[:, 0]; y1 dets[:, 1] x2 dets[:, 2]; y2 dets[:, 3] scores dets[:, 4] areas (x2 - x1 1) * (y2 - y1 1) 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:]]) w np.maximum(0.0, xx2 - xx1 1) h np.maximum(0.0, yy2 - yy1 1) inter w * h ovr inter / (areas[i] areas[order[1:]] - inter) # 高斯衰减 scores[order[1:]] * np.exp(-ovr * ovr / sigma) # 重新排序 idx np.where(scores[order[1:]] threshold)[0] order order[1:][idx] order order[np.argsort(scores[order])[::-1]] return keep, scores注意到这里有一个操作变化在 softNMS 里每处理完当前最高分框后剩余框的分数会被更新所以需要重新按更新后的分数排序这比标准 NMS 多了不少排序开销。论文里提到如果想提速可以在原始分数排序的基础上做近似实现实际效果几乎不损失。4.3 实验效果与适用场景密集行人检测的救星softNMS 最典型的应用价值体现在密集行人检测这类场景。常规的行人检测数据集中一个人被另一个人大面积遮挡的情况非常常见。标准 NMS 在这种情况下很容易把被遮挡者的候选框直接删掉softNMS 通过降分而不是删框给了被遮挡目标一个“复活”的机会。在 COCO 数据集上做基准测试softNMS 在不改变任何网络结构的情况下单模型大概能带来 0.5 到 1.1 个点的 mAP 提升而且这个提升在“遮挡程度高”的子集上更明显。关键是它的代价几乎为零——不需要额外训练不需要修改网络结构推理多花的只是排序时间。我在实际项目里观察到一个现象很多模型在训练时已经做了数据增强比如随机裁剪、马赛克对遮挡有一定鲁棒性但是推理时的 NMS 仍然是标准版。这种情况下换上 softNMS提升可能不如论文里那么大但基本是正向的。不过要留意softNMS 会让输出框的置信度和标准 NMS 分布不一样如果你后续有基于置信度阈值的过滤逻辑可能要从头重新标定一遍阈值。5. softerNMS定位质量感知让“方差越小越可信”5.1 出发点定位精度高的框在NMS中更容易被保留softNMS 解决了“框被误删”的问题但没有解决 NMS 排序依据的问题——它仍然用分类分数来排序仍然会让那些“分数高但定位差”的框当老大去压制“分数低但定位准”的框。softerNMS 这篇工作CVPR 2019论文全称《Bounding Box Regression with Uncertainty for Accurate Object Detection》打的就是这个靶子。它的核心观点是我们应该在 NMS 流程中引入定位质量的信息让那些定位更准确的框在竞争中占优势。但这里有一个先有鸡还是先有蛋的问题在后处理阶段我们怎么知道哪个框定位更准常规的检测器只会输出四边形的坐标不会输出定位置信度。softerNMS 的做法是从检测器训练阶段就开始下手让检测头额外预测一个定位不确定性方差。5.2 如何让检测头输出定位不确定性方差在标准检测器里边界框回归分支输出的是四个值一般解释为目标框相对 anchor 或 proposal 的偏移量dx, dy, dw, dh训练时用 Smooth L1 损失约束。softerNMS 的思路是不直接回归四个偏移量而是假设每个偏移量服从一个高斯分布网络回归这个分布的均值和方差。均值就是原来的偏移量预测方差就是定位不确定性。方差越大说明网络对这个框的位置越“没底”。数学上训练时的损失函数用的是高斯分布的负对数似然NLL。对于一个回归目标 x_gGT偏移量如果网络预测的分布是 N(x_e, σ²)则损失为L log(σ²) / 2 (x_g - x_e)² / (2 * σ²)注意这里有个关键的细节分母中的 σ² 使得网络在梯度回传时会自动调节方差的权重。当预测的均值偏离 GT 很远时网络会倾向于增大方差来降低这个样本的损失权重当预测很准的时候方差会被压小这个样本对训练的贡献更大。这种机制天然地在训练中实现了“难例降权准例升权”。另外因为 σ 作为分母有可能导致训练不稳定论文里实际实现通常让网络预测log(σ²)而不是直接预测σ²这样能确保方差始终为正且数值更稳定。pytorch 里加一个回归头非常方便比如在原有的 bbox 回归分支旁边并一个全连接层输出 4 维的方差 log 值即可。5.3 基于方差的框合并策略vote机制实现训练完成后网络不仅输出每个框的坐标还输出每个框的定位方差。在 NMS 后处理阶段softerNMS 使用了一种**方差投票Variance Voting**的机制来替代“直接选出最高分框”的策略。流程如下第一步用标准 NMS或 softNMS选出一组候选框。注意这里的 NMS 选出的“最高分框”只是一个初始基准不再直接作为最终输出。第二步对于每个被保留下来的框 M寻找所有和它 IoU 大于某个阈值论文里通常用 0.5的邻域框。第三步对邻域框集合中所有框的坐标做加权平均权重与方差的倒数成正比x_final Σ(x_i / σ_xi²) / Σ(1 / σ_xi²) y_final Σ(y_i / σ_yi²) / Σ(1 / σ_yi²) w_final Σ(w_i / σ_wi²) / Σ(1 / σ_wi²) h_final Σ(h_i / σ_hi²) / Σ(1 / σ_hi²)这个公式的意义很直观方差小定位可信度高的框贡献大方差大定位不可信的框贡献小。相当于在多个“专家”的意见中根据每个专家的可靠程度做加权投票而不是让分数最高的“专家”一个人说了算。实现时上述加权平均是对 x、y、w、h 分别独立计算的而且注意 w 和 h 的方向。实际代码里因为网络回归的是偏移量而不是最终坐标投票时可以用最终坐标也可以用偏移量再解码两者在数学上等价但用最终坐标更方便调试。我在复现 softerNMS 时踩过一个坑方差投票只在一个簇内部进行不要跨簇投票。最开始实现的时候我图省事对所有 IoU 大于阈值的框做了全局投票结果把簇边的框坐标也拉歪了mAP 反而比标准 NMS 掉了 0.3。后来检查才发现聚类那一步没做干净。另外有一点要提醒softerNMS 论文的作者在实验部分也指出收益最大的是 KL Loss 训练本身——它让检测头的回归更准了方差投票对最终结果的提升相对有限。但对我们做应用的来说两者结合仍然是一个合理的选择。6. IoU-Net直接把“框的质量”当成一个回归目标来学6.1 核心洞察分类置信度和IoU之间的错位有多大前面提到标准 NMS 用分类分数排序的最大隐患是分类分数无法反映定位质量。IoU-NetECCV 2018论文全称《Acquisition of Localization Confidence for Accurate Object Detection》用一组数据把这个隐患量化了他们统计了 Faster R-CNN 在 COCO 验证集上的分类分数和最终框的 IoU 之间的关系发现在分类分数最高的那些检测框里有相当一部分的 IoU 只有 0.5 甚至更低而 IoU 高达 0.9 的框分类分数却可能排在几百名开外。也就是说在 NMS 这个“淘汰赛”里排在最前面的选手并不一定是真正的冠军而且这个排序错误是系统性的不是偶然的。IoU-Net 的解决方案非常直接你既然担心排序依据不靠谱那就训练一个新的分支直接预测每个框和 GT 的 IoU用这个预测出来的 IoU 替代分类分数作为 NMS 的排序依据。6.2 IoU预测头的结构与训练方式IoU-Net 在检测头中增加了一个并行的 IoU 预测分支。这个分支的输入通常是从 RoI 池化后的特征图或单阶段的特征图位置经过几层全连接输出一个标量IoU_pred。训练时IoU 预测头的目标函数非常简单L SmoothL1(IoU_pred, IoU_gt)其中IoU_gt是在训练时实时计算的对于当前的 proposal 或 anchor 经过回归后的框和它匹配的 GT 框做一次 IoU 计算得到 0 到 1 之间的实数直接作为回归目标。这里有几个实现细节值得注意第一IoU_gt 的范围是连续的 0 到 1不是二分类也不是离散分类。很多人在实现时会把 IoU 量化成 0.05 一个桶再分类但论文和后续实践都证明直接回归连续值的表现更好。第二IoU 预测头的训练数据来源一定要注意采样策略。RPN 或单阶段检测器的预选框在训练初期质量很差很多框的 IoU 是 0如果全部用这些数据训练 IoU 预测器预测值会整体偏低。比较稳妥的做法是在训练的前期阶段先用 RPN 的输出和 GT 做匹配采样 IoU 分布在 0.1 到 0.9 之间的框来训练避免极端不均衡。第三IoU 预测分支和回归分支是并行关系不共享坐标编码。IoU 预测头学习的是“这个框回归后的结果距离 GT 有多近”而不是学习“怎么回归”所以它的更新不会直接影响坐标回归。6.3 IoU-guided NMS用预测IoU排序用IoU作为定位精化准则IoU-Net 的推理阶段提出了两种用法。第一种是 IoU-guided NMS。候选框不再按分类分数排序而是按预测的 IoU 排序。也就是把标准 NMS 代码里scores dets[:, 4]这一行替换成 IoU 预测头的输出。这个改动非常小但效果立竿见影因为排序的“裁判”从分类分数换成了定位质量那些“位置精确但分类分数一般”的框终于能上位了。第二种是优化式的框精化Optimization-based refinement。这个思路更大胆既然 IoU 预测器可以打分那我能不能反过来用——固定 IoU 预测器的网络参数把框的坐标当成可学习的变量用梯度上升不断调整坐标让 IoU 预测值最大化。这个过程不需要任何标注信息完全是在推理时的一个自优化过程对 NMS 选出的框初始化其坐标把框坐标转成可微的形式通过 IoU 预测网络前向传播计算损失负的预测 IoU对坐标求梯度沿梯度方向更新坐标若干步论文里这个操作能显著提升框的定位精度在 COCO 上可以比基准提升 1 个多点。但代价是推理时间成倍增加——每个框都要跑多次前向和反向传播这对实时应用来说基本不可行。我的建议是离线场景或者对精度要求极致的竞赛中可以用线上低延迟服务就别考虑了。另外IoU-Net 还有一个非常实用的用法是替代 RPN 的 NMS。在小目标检测、实例分割等任务中先用 IoU 分数对 RPN 的输出做排序筛选比直接用 objectness 分数能筛出更多高质量 proposal。这个技巧在两阶段检测器里价值很高很多人不知道。7. 从NMS到IoU-Net的完整对比我该在什么场景用哪个7.1 四个方案的主干逻辑对比为了让你能一眼看明白四个方案的区别我把它们的主干逻辑整理成了表格方案核心问题处理方法排序依据抑制方式额外成本标准NMS重复框冗余直接删除IoU超阈值的框分类分数硬抑制置0无softNMS密集场景误删不删框只降分分类分数软抑制衰减几乎为零softerNMS排序依据不可靠预测定位方差投票合并坐标分类分数 方差投票软抑制 坐标精化额外回归头 训练改动IoU-Net排序依据不可靠直接预测IoU用IoU排序预测IoU硬抑制 可选坐标精化IoU预测头 训练改动从这个表格可以清晰看出四个方案的演进本质上是沿着两条线展开的一条线是抑制策略的软化标准 NMS 硬删softNMS 衰减分数而不是删除softerNMS 更进一步连坐标都做合并。另一条线是排序依据的升级标准 NMS 和 softNMS 都用分类分数排序softerNMS 用方差作为辅助信息IoU-Net 则直接预测 IoU 作为排序依据。7.2 实际落地经验哪些场景用软化策略就够了哪些需要IoU-Net结合我的项目经验可以给出下面几个比较靠谱的选型建议1. 项目刚起步后端时间紧张模型已经训好只想快速提升几个点。首选 softNMS因为它不需要重新训练模型推理代码改动不超过十行。在密集目标场景行人、车辆、货架商品下这个是性价比最高的方案。2. 目标尺寸差异大小目标和大目标混在同一个场景。这种情况建议换 softerNMS。小目标因为像素少回归天然不稳定方差预测可以帮助识别那些“虽然分数高但位置其实不可靠”的小目标框在投票合并时把大目标的稳定性和小目标的可信度做平衡。3. 对精度要求极高推理延迟不敏感比如离线分析、竞赛刷分。IoU-Net 的 IoU-guided NMS optimization refinement 是最优选择。特别是当你发现模型的分类分数和定位质量明显不对齐时用 IoU 排序几乎是立竿见影的。4. 实时视频流场景边缘设备部署。我的经验是标准 NMS 的在 CPU/GPU 上的实现经过高度优化比如 torchvision.ops.nms 的 CUDA 版本而 softNMS、softerNMS 的纯 Python 实现会有额外的排序开销。这种情况下可以把排除阈值前的“候选簇排布”分析做一次缓存或者直接在 C 层面实现 softNMS 的衰减逻辑性能损失会小很多。7.3 容易被忽视的工程细节多类别NMS、坐标夹紧、置信度联调最后聊几个工程上容易踩的坑。多类别怎么处理标准做法是逐类别做 NMS。事实上绝大多数检测器包括 YOLO、Faster R-CNN在推理时都是对每个类别分别调用 NMS类别之间互不影响。但这里有个隐患如果两个不同类别的框高度重叠比如“人”和“骑的人”这种逐类别 NMS 不会处理跨类别的重复框。更严格的做法是跨类别 NMS把所有类别的框合在一起做去重但这样容易误删“一个人骑车”这种本身就需要两个框同时存在的场景。具体业务里要按语义取舍。坐标夹紧。NMS 计算 IoU 之前推荐先把所有框的坐标夹紧到图像边界内x1 max(x1, 0) 等否则框超出图像边界的部分会把面积算得虚高导致 IoU 偏低。虽然大部分检测头的输出已经做了 clipping但保不齐某些自定义检测头的输出没做。置信度阈值和 NMS 阈值的联调。注意NMS 的输入一般是“所有分数超过某个低阈值的候选框”而不是“所有框”。这个低阈值通常取得很宽松比如 0.05保证召回然后 NMS 之后再用一个高置信度阈值比如 0.5过滤最终输出。如果你调了 softNMS 或 IoU-Net分数的分布会变化你原来定好的高置信度阈值必须重新标定否则最终输出框的数量和精度都会变。训练和推理要一致。很多检测框架在训练时也会加一个 NMS 模拟比如 ATSS 的 label assignment 过程涉及候选框筛选推理时标准 NMS 和训练时的一致性也很重要。如果模型是在带 softNMS 效果的数据增强下训练的推理时换回标准 NMS性能可能不升反降。前阵子我在试着做 qwen3-vl 微调物体检测相关的工作发现一个有意思的现象即便是基于视觉语言大模型的检测输出最后也常常需要一层类似 NMS 的去重逻辑因为模型在多个 token 位置可能输出对同一目标的多个框。很多做微调的人只看重模型本身忽略了后处理设计结果评测时被重复框坑了还不知道。说明 NMS 这条线无论检测范式怎么变都不会过时理解它的原理始终有价值。最后分享一个个人感受很多人觉得 NMS 这类“后处理”是雕虫小技不值得深入研究但实际上在检测器性能越来越卷的今天把排序依据从分类分数换成 IoU、把硬抑制换成软衰减往往比换一个 backbone 带来的收益更直接、更可控。不要小看这最后一步它可能是你 mAP 突破某个瓶颈的最后一块拼图。