029、后处理改进(二):WBF与多模型集成策略
从产线误检说起
上周产线反馈了个头疼的问题:同一批电路板,用我们部署的YOLO模型检测,相邻两帧居然对同一个芯片给出了位置偏差很大的边界框。质检员手动复检时发现,两个框其实都只框住了芯片的一部分——模型在“抖动”。这种问题在NMS后处理中很常见:当多个重叠框的IoU达不到阈值时,NMS会武断地只留一个,其余全删。但在实际场景中,这些框可能都是“部分正确”的。
NMS的局限性
传统NMS的逻辑很简单:按置信度排序,最高分的框留下,与其重叠度高的框删除。这个逻辑有个致命假设——得分最高的框一定是最准确的。但在复杂场景下,模型会有多种“犹豫”:光照变化导致边界模糊、遮挡造成特征不全、相似物体密集排列……这时候多个框可能互补,而NMS的“赢者通吃”策略反而丢弃了有用信息。
我们试过调高IoU阈值,结果漏检增多;调低阈值,又出现重复框。这种二选一的困境,正是我们需要WBF(加权框融合)的原因。
WBF的核心思想
WBF不做“选择”,而是做“融合”。它的思路很工程师:既然多个检测框都有道理,那就把它们当做一个委员会,每个框根据置信度投票,最终决策是加权平均的结果。
具体实现时,WBF先对所有框按置信度排序,但不是直接删除重叠框,而是将IoU超过阈值的框归为一组。每组内,根据每个框的置信度计算权重,对框的坐标进行加权平均。置信度高的框话语权大,但低置信度框也能贡献信息。
def weighted_box_fusion(boxes, scores, iou_thr=0.5):
"""
简易版WBF实现,实际部署要考虑很多边界条件
boxes: [[x1,y1,x2,y2], ...] # 注意这里用xyxy格式
scores: 对应置信度列表
"""
# 按置信度降序,这是老规矩了
order = scores.argsort()[::-1]
boxes = boxes[order]
scores = scores[order]
# 这里用列表存最终结果,动态添加
fused_boxes = []
fused_scores = []
while len(boxes) > 0:
# 取当前最高分框作为基准
current_box = boxes[0]
current_score = scores[0]
# 找和它重叠的“伙伴”
ious = calculate_iou(current_box, boxes)
similar_mask = ious > iou_thr
if np.sum(similar_mask) == 1:
# 没有重叠框,直接保留
fused_boxes.append(current_box)
fused_scores.append(current_score)
else:
# 关键步骤:加权融合
similar_boxes = boxes[similar_mask]
similar_scores = scores[similar_mask]
# 权重用置信度,这里可以尝试平方或其他加权方式
weights = similar_scores / np.sum(similar_scores)
# 坐标加权平均
fused_box = np.sum(similar_boxes * weights[:, None], axis=0)
fused_score = np.mean(similar_scores) # 融合后的置信度可以灵活处理
fused_boxes.append(fused_box)
fused_scores.append(fused_score)
# 移除已处理的框,继续下一轮
boxes = boxes[~similar_mask]
scores = scores[~similar_mask]
return np.array(fused_boxes), np.array(fused_scores)
注意上面这个简化实现有个坑:加权融合时,如果两个框置信度都很高但位置偏差大,融合结果可能反而“模糊”。实际工程中要加判断,比如融合前后框面积变化过大时,考虑不融合。
多模型集成策略
WBF更强大的地方在于它能融合不同模型的输出。我们在产线部署时,用了三个变体模型:一个专注小目标,一个对大目标更准,另一个在遮挡情况下表现稳定。三个模型独立推理,结果用WBF融合。
# 实际项目中的多模型WBF片段
def multi_model_fusion(model_outputs_list):
"""
model_outputs_list: 多个模型的输出列表
每个元素是(boxes, scores, labels)
"""
all_boxes = []
all_scores = []
all_labels = []
# 收集所有模型的检测结果
for boxes, scores, labels in model_outputs_list:
# 这里可以加模型权重,比如A模型整体置信度乘0.9
all_boxes.extend(boxes)
all_scores.extend(scores)
all_labels.extend(labels)
# 按类别分别处理
unique_labels = np.unique(all_labels)
final_results = []
for label in unique_labels:
mask = np.array(all_labels) == label
label_boxes = np.array(all_boxes)[mask]
label_scores = np.array(all_scores)[mask]
# 对同一类别的框做WBF
fused_boxes, fused_scores = weighted_box_fusion(label_boxes, label_scores)
for box, score in zip(fused_boxes, fused_scores):
final_results.append([*box, score, label])
return final_results
这种多模型融合在边缘设备上要谨慎。我们最初在Jetson上直接跑三个模型,帧率从25掉到8。后来改用TensorRT优化,模型量化到INT8,三个小模型并行跑,总算维持在15帧以上。
部署时的工程细节
WBF在服务器端跑很简单,但边缘端部署时要注意几点:
第一,IoU阈值别照搬论文。我们发现在密集小目标场景,阈值要降到0.3~0.4;大目标场景可以到0.6。这个参数和你的业务场景强相关。
第二,置信度加权方式可以调整。标准WBF用置信度直接加权,我们试过用置信度的平方,让高置信度框权重更大,效果在某些场景更好。
第三,内存管理。WBF需要保存所有候选框,内存消耗比NMS大。在嵌入式设备上,最好预设最大处理框数,避免内存溢出。
第四,实时性要求高的场景,可以分两阶段:先用轻量级NMS做初步筛选,再用WBF精细处理。这样能在精度和速度间折中。
个人经验谈
WBF不是银弹。我们在PCB缺陷检测上用它,mAP提升了3.2%;但在行人检测场景,提升不到1%。后来分析发现,行人检测的框通常比较规整,NMS已经够用。而PCB上的划痕、焊点缺陷,形状不规则,多个模型从不同角度“看”到的信息确实互补。
建议先分析你的检测失败案例:如果是模型根本“看不到”目标,WBF帮不上忙;如果是模型看到了但框不准,或者不同帧间结果不一致,WBF值得一试。
多模型集成时,别选三个同质化的模型。我们最初用三个不同训练轮次的YOLO变体,效果提升有限。后来换成了不同backbone(CSPDarknet、EfficientNet、MobileNet)的模型,差异大了,融合效果才明显。
最后提醒,WBF会增加推理延迟。在产线上,我们最终方案是:正常情况用NMS,当连续多帧检测结果波动大时,自动切换到WBF模式。这种动态策略,既保证了平均速度,又在关键时候提升了精度。
模型部署永远是权衡的艺术。WBF给了我们另一种权衡维度——不是“选哪个框”,而是“如何把框的信息合并起来”。这种思路,在其他工程问题上也值得借鉴。
更多推荐
所有评论(0)