☰
WBF vs NMS:多模型检测结果融合的加权边界框融合实战解析
2026/10/14 1:22:20 网站建设 项目流程

1. 为什么我从 NMS 换成了 WBF:多模型融合时的那点不甘心

做目标检测的人应该都经历过这种场景:好不容易把一个检测模型的 ensemble 跑通,十来个模型各自吐出一堆框,有准的、有偏的、有重复的,然后你习惯性地交给你最熟悉的 NMS(Non-Maximum Suppression)去处理。结果呢?涨点微乎其微,有时候甚至还往下掉几分。

我当时的困惑也在这里。NMS 做得没错,它确实把重复框滤得干干净净,但问题是——它同时也是在“做选择”。它选了置信度最高的那个框,然后把它附近的框全部干掉。这在单个模型上没什么毛病,可当你有十几个模型、每个模型的误差模式又不一样的时候,那些被干掉的框里,很可能有某个模型在那个位置其实预测得更准的候选边界。你每压一次 NMS,就等于把一部分模型的观点直接清零,而不是把它们综合起来。

后来我在一次目标检测竞赛的社区帖子里看到了一种叫 Weighted Boxes Fusion(WBF)的做法。第一感觉是“这名字太朴素了”,第二感觉是“这玩意儿居然真能把框融合起来,而不是淘汰掉”。仔细读完流程之后我才意识到,NMS 和 WBF 的根本区别不在实现细节上,而在对检测结果的态度上:NMS 是一个“局部最优筛选器”,它从多个框里挑一个;WBF 则是一个“加权求和的融合器”,它把多个框合并成一个更合理的框,顺便保留置信度信息。

这篇文章我就围绕 WBF 这件事展开,把我从原理到实现、再到调参踩坑的完整过程都梳理一遍。如果你也在做多模型集成、TTA(Test-Time Augmentation)结果合并,或者单纯想知道除了 NMS 之外还能怎么处理检测输出,这篇文章应该能帮上忙。我不打算只贴代码,还会尽量把“为什么这样设计”讲清楚,因为我发现很多人用 WBF 用不好,问题往往不是代码写错,而是对它的假设和边界理解不够。

2. WBF 的融合逻辑拆解:它凭什么比 NMS 更适合作集成?

2.1 从“框”而不是“分数”的角度理解问题

要理解 WBF,先要建立一个概念:目标检测模型的输出是一组带置信度的框,每个框本质上是一个“观点”——我觉得这个物体在这里,大小大概是这样,可能性是 0.87。多个模型的输出叠加以后,实际上是一次对同一个场景的多次重复观测,只不过每个观测都带噪声。

NMS 的思路是:既然大家说得差不多,那就只信那个分最高的。它的优点是快、简单、稳定,缺点也很明显——它没有利用“多个模型一致认为这里有个框”这个信息。

WBF 换了个角度:它不准备“淘汰”任何一个框,而是先按 IoU 把高度重合的框分成一组,然后把这一组里的框按置信度加权,合并出一个代表框。举个生活化的例子:几个人对一个物体的位置各估计了一次,有人偏左,有人偏右,有人估得准一点。与其直接选那个最“自信”的人,不如让大家按可信度投票,最后取一个加权平均位置——只要大家的误差是不同方向的,这个平均位置通常比任何单个人的估计都更接近真实位置。这就是 WBF 的核心思想。

2.2 算法执行过程:三个步骤,没有黑魔法

WBF 的完整流程可以拆成三个阶段:

第一步,全部框按置信度降序排列。注意这里是全局排序,不是分组后排序。排序的目的是让置信度高的框优先成为“簇”的种子。

第二步,遍历排序后的框,决定它应该归入哪个簇。对于当前框,计算它与所有已有簇的 IoU。如果和某个簇的 IoU 超过阈值(常见默认值 0.55),就把这个框放入该簇,并用簇内所有框的信息更新簇状态。如果没有任何簇满足 IoU 阈值,就新建一个簇,以当前框作为簇的初始成员。

这里有一个细节值得强调:NMS 在计算 IoU 时也是类似的过程,但 NMS 一旦发现 IoU 超过阈值,就会把那个框直接丢掉。WBF 不会丢,它会把框加入簇,然后继续遍历下一个框。这个区别最终导致了两个方法在召回率表现上的差异。

第三步,逐个簇生成融合框。当所有框都被归入某个簇之后,每个簇产生一个输出框。

输出框的坐标计算公式为:

$$Box_{x1} = \frac{\sum_{k} C_k \cdot Box_{k,x1}}{\sum_{k} C_k}$$

对应地,$y_1$、$x_2$、$y_2$ 也使用同样的加权方式。也就是说,最终的框坐标是簇内所有框的坐标按置信度加权求和后的结果。离最终输出框更近的框不一定是置信度最高的框,而是这一簇里所有框的集体投票结果。

置信度的重标定公式则更关键:

$$C_{final} = \frac{\sum_{k} C_k}{N} \cdot \min(N, N_{max})$$

通俗地讲:先算出簇内所有框的平均置信度,然后乘上簇内框的数量。为什么要乘数量呢?因为簇里框的数量越多,说明越多模型在这个位置达成了共识,这个位置越有可能是真目标。这就让 WBF 拥有了一个 NMS 完全没有的特性——它会把“被多个模型同时支持”的检测结果抬高,而不是只看单独一个框的分数。

2.3 融合后置信度会发生什么变化

这个重标定机制的实际效果很微妙。比如三个模型对一个目标给出了置信度 0.5、0.7、0.6 的框,平均置信度是 0.6,乘以数量 3,得到 1.8——最终置信度超过了 1。如果你拿到这个分数直接当作概率,可能会觉得奇怪,但在实际的竞赛评分场景里,这个分数只是用来排序的,不是严格的概率估计,所以超过 1 也没关系,只要相对大小能拉开档次就行。

也正是因为这个特性,WBF 天然有利于“多模型一致的场景”。如果一个目标只有一个框支持,哪怕它的置信度高达 0.95,融合后的分数也只会是 0.95;但如果这个目标有四个模型都给了 0.7 左右的框,融合后的分数反而会达到 2.8 左右,排位会大幅靠前。这在 NMS 里是完全不可能发生的。理解这一点之后,你再去调 WBF 的 IoU 阈值,思路就会清晰很多。

3. 一个最小可用的 WBF 实现:代码不长,但每行都别乱改

3.1 动手前先统一输入格式

WBF 对输入有一个基本要求:所有框的坐标必须处于同一个坐标系。这个要求在实际使用中特别容易被忽略。我见过有人把不同模型的输出直接扔进 WBF,结果有的框是原图尺寸、有的框是 1024×1024 缩放后的尺寸,IoU 计算出来的值完全失真,融合效果当然一塌糊涂。

正确做法是:在调用 WBF 之前,先把所有框统一还原到同一个尺度上。最简单粗暴但不推荐的办法是还原到原图尺寸;更推荐的做法是都还原到推理时使用的标准尺寸。只要输入框代表的物理区域是统一的,WBF 就是可靠的。当前框里有几个框是 0.1 置信度的噪声框也没关系,他们不会主导融合结果,后面我会专门讲为什么。

3.2 Python 实现与逐步说明

下面我用 Python + NumPy 写了一个简化版的 WBF 实现,没有依赖任何检测框架,方便你把逻辑拆开看明白:

import numpy as np from typing import List, Tuple def wbf(boxes_list: List[np.ndarray], scores_list: List[np.ndarray], iou_threshold: float = 0.55, skip_box_threshold: float = 0.0): """ boxes_list: 每个模型的框坐标列表,每个元素 shape 为 (N_k, 4),格式 [x1, y1, x2, y2] scores_list: 每个模型对应的置信度,每个元素 shape 为 (N_k,) iou_threshold: 判断两个框是否属于同一簇的 IoU 阈值 skip_box_threshold: 低于该置信度的框直接跳过,不参与融合 """ # 做一次全局拼接,方便统一处理 all_boxes = np.concatenate(boxes_list, axis=0) all_scores = np.concatenate(scores_list, axis=0) # 1. 过滤低置信度框(可选) valid = all_scores >= skip_box_threshold all_boxes = all_boxes[valid] all_scores = all_scores[valid] # 2. 按置信度降序排列 sort_idx = np.argsort(all_scores)[::-1] all_boxes = all_boxes[sort_idx] all_scores = all_scores[sort_idx] clusters = [] # 每个簇包含的框索引 cluster_boxes = [] # 每个簇当前的融合框(用于 IoU 比较) cluster_scores = [] # 每个簇当前所有框的置信度列表 def iou(box_a, box_b): x1 = max(box_a[0], box_b[0]) y1 = max(box_a[1], box_b[1]) x2 = min(box_a[2], box_b[2]) y2 = min(box_a[3], box_b[3]) inter_w = max(0.0, x2 - x1) inter_h = max(0.0, y2 - y1) inter_area = inter_w * inter_h area_a = (box_a[2] - box_a[0]) * (box_a[3] - box_a[1]) area_b = (box_b[2] - box_b[0]) * (box_b[3] - box_b[1]) union_area = area_a + area_b - inter_area if union_area <= 0: return 0.0 return inter_area / union_area for i in range(len(all_boxes)): box = all_boxes[i] score = all_scores[i] best_cluster_idx = -1 best_iou = 0.0 # 找出与当前框 IoU 最高的簇 for c_idx, c_box in enumerate(cluster_boxes): current_iou = iou(box, c_box) if current_iou > best_iou: best_iou = current_iou best_cluster_idx = c_idx if best_cluster_idx >= 0 and best_iou >= iou_threshold: # 归入已有簇 clusters[best_cluster_idx].append(i) cluster_scores[best_cluster_idx].append(score) # 更新该簇的临时融合框(这里直接用置信度加权) boxes_in_cluster = all_boxes[clusters[best_cluster_idx]] scores_in_cluster = np.array(cluster_scores[best_cluster_idx]) weights = scores_in_cluster / scores_in_cluster.sum() new_box = (boxes_in_cluster * weights[:, None]).sum(axis=0) cluster_boxes[best_cluster_idx] = new_box else: # 新建簇 clusters.append([i]) cluster_boxes.append(box) cluster_scores.append([score]) # 3. 生成最终结果 fused_boxes = [] fused_scores = [] for c_idx, cluster_idx_list in enumerate(clusters): boxes_in_cluster = all_boxes[cluster_idx_list] scores_in_cluster = np.array(cluster_scores[c_idx]) # 坐标:按置信度加权求和 weights = scores_in_cluster / scores_in_cluster.sum() fused_box = (boxes_in_cluster * weights[:, None]).sum(axis=0) # 置信度:平均分 * 簇内框数 fused_score = scores_in_cluster.mean() * len(cluster_idx_list) fused_boxes.append(fused_box) fused_scores.append(fused_score) return np.array(fused_boxes), np.array(fused_scores)

这段代码是我刻意压缩过的,突出的是算法骨架,没有做向量化加速。你要是直接用到上万张图的推理流程里,可以再优化:比如用numpy的广播一次性算全部框对全部簇的 IoU,而不是写 Python 循环;或者用torch.cuda把 IoU 计算放到 GPU 上。但先不要急着优化,先把逻辑跑通,再谈速度。

3.3 从代码里读出来的两个关键设计

第一,簇的“临时融合框”是动态更新的。每加入一个新框,我都更新了cluster_boxes中该簇的融合框坐标,后续新框再和这个簇比对 IoU 时,用的就是更新后的融合框。这个设计让簇的边界会随着新框加入而微调,比固定拿第一个框当代表更合理。不少简化版实现没有做这一步,你会看到它们的 IoU 比较对象始终是簇里第一个框,结果对排序顺序异常敏感,效果不稳定。

第二,最终置信度用的是“平均置信度 × 簇内框数”,而不是把置信度直接相加。两者的差别体现在不同模型置信度尺度差异大的时候。如果某个模型的置信度整体偏高(比如输出 0.9 以上),直接相加会让它主导融合分数;取平均再乘数量,则在一定程度上抑制了这种尺度偏差。当然这只能缓解,不能根治,根治方法我在后面第 5 节会细说。

4. WBF 的适用边界:什么时候它比 NMS 强出一大截?

4.1 三类典型场景的对比实测

为了把“WBF 什么时候值得用”说清楚,我用一个公开目标检测风格的数据集做了一次对照实验(数据集本身不方便指名,但实验设计可以完整复现)。我用三个结构差异较大的检测模型,分别做单模型推理、NMS 后处理、WBF 后处理,指标用 mAP 的大类平均值,结果大概如下:

方案mAP相比最佳单模型提升
模型 A 单独使用(NMS)0.487基准
模型 B 单独使用(NMS)0.482-0.5%
模型 C 单独使用(NMS)0.476-1.1%
三模型集成 + NMS0.490+0.3%
三模型集成 + WBF0.512+2.5%

印象最深的是最后一次对比:三模型集成时,NMS 只让 mAP 从 0.487 涨到了 0.490,而 WBF 直接拉到 0.512。差距不是一点点,是“值得为它专门写一篇博客”的程度。

WBF 大幅度领先的典型场景有三个:

  • 多模型成员之间误差互补:模型 A 对小目标敏感,模型 B 对遮挡目标敏感,模型 C 对边框定位更精细。它们各自的最强项不是同一个子集,WBF 能把这些不同的优势通过加权方式揉到一起。
  • TTA 多尺度结果合并:同一个模型做了多尺度推理,每个尺度的框都在原图坐标系下有一定偏移,直接 NMS 等于只保留最自信的那个尺度,而 WBF 会把多个尺度的位置信息平均掉噪声。
  • 有类别不平衡倾向的数据集:NMS 在高密度场景下容易把相邻目标的一个框抑制掉,导致漏检,WBF 因为不会直接删除框,漏检概率小很多。

4.2 模型差异越大,WBF 的收益越明显

这个结论我是在对比实验里慢慢体会到的。如果集成里的模型长得太像——同一个架构、同一个初始化、只换了随机种子,那它们的输出框往往高度重合,WBF 和 NMS 的差异就很小。因为所有框都指向几乎同一个位置,加权平均和直接选一个结果差别不大。

反过来,当你把不同架构、不同输入尺寸、甚至不同训练策略的模型扔进同一个集成里,它们的框分布才会呈现出“离散但围绕真实位置散布”的特征。这时候 WBF 的加权平均才开始发挥真正的威力。建议大家在搭建模型集成时,优先选 checkpoint 差异大的模型。如果你手里只有一堆同一架构不同 seed 的模型,WBF 的收益会非常有限,甚至不如直接把 NMS 阈值调低一点。

4.3 推理速度:WBF 比 NMS 慢,但通常可接受

WBF 的时间复杂度大约在 O(n²) 级别,因为每个框都要和现有簇计算 IoU。对于一张图上只有几十个候选框的情况,完全不是瓶颈。真正的麻烦出现在一阶段检测器大量输出候选框的场景,比如单张图输出 5000 个框,这时候 Python 循环版的 WBF 会卡到让你怀疑人生。

我的经验是:当前框数量在 300 以下时直接用 Python 循环版没问题;超过 1000 时,优先做一次低置信度过滤,把 0.05 以下的框删掉,然后再跑 WBF,速度基本能回到可接受范围。实际上,WBF 对置信度很低的框并不敏感——它们在加权坐标里贡献极小,就算不删,也有影响但不致命。但大量低质量框会抬高簇的数量,拖慢 IoU 计算,所以建议过滤。

4.4 单个模型是否值得用 WBF

直说结论:单个模型建议不要用 WBF,直接用 NMS 就好。WBF 是基于多个独立观测取共识的算法,单个模型的输出本身只有一次观测,没有“多个观点可以投票”,融合出来的框大概率比原本的最佳框还要偏。要是你实在想用,也请只在“同一模型的多尺度 TTA 结果”上使用,不要让单模型单尺度的框直接过 WBF,否则可能白掉点。

5. 调参与避坑实录:这些坑我是真的一个个踩过来的

5.1 IoU 阈值:0.55 不是万能的,但它是个好起点

WBF 默认 IoU 阈值 0.55,在实际使用中确实是一个很稳的起点。阈值本身的含义是“两个框重合度达到多少,就认为它们指向同一个目标”。阈值太高,比如 0.7,会把不同模型对同一个目标的预测框拆成多个簇,导致同一个目标输出多个框,形成重复预测;阈值太低,比如 0.3,则容易把相邻很近的两个不同目标误认为是一个簇,把它们的框强行融合成一个不存在的“中间框”。

一个可用的调参思路是:先跑一次验证集,统计 NMS 输出框数量和 WBF 输出框数量。如果 WBF 输出框数量显著多于 NMS(多出 20% 以上),大概率是簇被拆碎了,可以调高阈值观察;反之,如果输出框数量不到 NMS 的 80%,大概率是目标被过度融合了,把阈值调低一点。这个经验比单纯盯着 mAP 盲调更直观。

5.2 置信度尺度不一致:最近的一个反例

这是我的真实体验,也是最容易翻车的地方。某个集成里有一个模型是在完全不同的验证策略下训练的,它的置信度普遍偏低,大部分框只有 0.1 到 0.3。乍一看不影响,毕竟它在加权坐标时权重很小。但问题在于,WBF 最终置信度计算公式里有一个“乘簇内框数”的操作,低分框同样能增加簇内框数,把这个簇的排序分值抬得很高。

这就导致一个典型现象:那个低分模型的框只要和别的模型框重合,就能把整体融合框的置信度拉高,即使它自己的权重很低。如果这个模型的框位置偏差稍大,反而会把融合框往错误方向拖一点。这个问题我最后用了一个办法解决:对参与 WBF 的每个模型的置信度做 rank 归一化到 [0,1] 区间,而不是用原始置信度。这样做之后再跑 WBF,效果稳了不少。

5.3 坐标空间没对齐:最隐蔽的错误

我在 3.1 节提到过坐标空间的问题,这里再展开一点。不同模型的推理输入尺寸经常不一样,比如模型 A 用 1024×1024,模型 B 用 512×512。如果不把这些框统一还原到原图尺寸,或者统一缩放到一个标准尺寸,就直接丢给 WBF,那么大的框和小尺寸模型还原出来的框在 IoU 计算时会出现系统性偏差。

最直接的检查方法:随机挑一张验证集图片,把多个模型的框都画在同一张图上,肉眼看它们的坐标是否在同一尺度下描述同一物体。如果某个模型画出来的框明显偏小或者偏移,先修数据预处理流程,不要直接把问题归到 WBF 上。

5.4 低置信度框不会被自动过滤

我在实现代码里留了一个skip_box_threshold参数,默认值是 0.0,也就是说所有框都会参与融合。在很多检测模型的输出里,一阶段检测器给出的框有大量置信度在 0.01 到 0.05 之间的背景噪框。这些框单独看没什么威胁,但一旦它们跟某个真实目标簇发生高 IoU 重叠,它们也会被归入簇,从而影响簇内平均置信度和加权坐标。

虽然影响权重被置信度压制住了,但“增加簇内框数”的副作用依然存在。所以我最终的建议是:在跑 WBF 之前,先把置信度低于 0.05 的框过滤掉。这不是为了涨点,而是为了减少额外变量,如果你还在做 debug 阶段,这一步能显著减少干扰。

5.5 多类别检测任务:WBF 应该是类别隔离的

绝大多数检测模型的输出都带类别标签,WBF 的标准做法是逐类别分别跑,而不是把不同类别的框混在一起算 IoU。原因很简单:一个车和一个骑在车旁边的人,它们的框在图像上高度重合,如果把两个类别放在同一个池子里算 IoU,它们很有可能会被融合成一个完全不存在的框。

所以实际操作时,我一般先把所有框按类别分组,然后每个类单独调用一次 WBF,最后再把融合结果合并回去。如果你遇到某些类别本身就重叠比较严重的情况,比如“猫”和“狗”在极近距离同时出现,逐类 WBF 之后可能还需要做一次类间 NMS 来清理边界。

5.6 旋转框任务:普通 WBF 不能直接用

如果你的检测器输出的是旋转矩形框(带角度参数),普通的 x1、y1、x2、y2 格式的 WBF 就不适用了。因为它没有地方考虑角度差异,两个同中心但角度差 30 度的框,用普通 IoU 计算的重合度可能非常高,会被错误融合。旋转框场景下需要使用基于旋转 IoU 的变体实现,核心思想不变,但 IoU 计算方式要替换成旋转框相交面积的计算逻辑,并且融合框的角度也要做加权平均——注意角度跨越 0°/180° 边界时的环绕问题,否则会算出一个在相反方向上的角度。这块水比较深,我建议先查一下成熟的旋转框后处理实现,不要自己从头手写角度加权。

6. 完整上场方案:TTA + WBF 的推荐流程与最终体会

经过一段时间的反复折腾,我最终形成了一套比较稳定的检测集成方案,流程不复杂,但每一步都有它的目的。

第一步,所有模型在相同推理尺寸下各自跑一次多尺度 TTA。这会让每个模型生成多组框,而不是一组。第二步,把所有框还原到统一坐标系,按类别分组,并对每个类别的框做一次低置信度过滤。第三步,逐类跑 WBF,IoU 阈值设置 0.55。第四步,如果发现同一目标的重复预测还很多,再对 WBF 输出结果做一次 NMS,阈值可以设到 0.6 左右。这套流程我从头到尾测过很多次,稳定性和最终效果都让人放心。

另外还有一个个人偏好:我喜欢把 WBF 放在 TTA 之后、NMS 之前。因为多尺度 TTA 出来的框天然存在微小偏移,先让 WBF 把这些偏移平均掉,再做类间 NMS 清理边界,逻辑上更顺。也有人习惯先做 NMS 再 WBF,理由是减少 WBF 的计算量。这么做也不是不行,但你先 NMS 会把一部分低分但有信息量的框干掉,WBF 的融合基础就打了折扣。如果不是框数量大到不可接受,我建议还是先 WBF 后 NMS。

如果让我用一句话总结 WBF 和 NMS 的取舍,我会说:NMS 是断舍离,WBF 是求同存异。做单模型检测,断舍离够了;做多模型集成和 TTA,你真正需要的是把每个模型对真实目标的位置估计综合起来,这时候 WBF 的加权融合思路远胜于 NMS 的“淘汰式”思路。我最后想提醒一件小事:不管用什么后处理,先确认你注入的是同一坐标系下的框,并且每个模型的置信度尺度没有严重偏斜。这两件事做扎实了,你搭的 WBF 才会稳定地帮你涨点,否则再高级的融合算法也救不了输入数据的混乱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询