一、从产线瑕疵检测说起
上周在客户现场调试,产线摄像头拍到的PCB板图像里,那些微小的焊点缺陷和刻痕,YOLOv11一个都没抓出来。不是模型精度不够,而是这些目标在640×640的输入尺度下,往往只有4×4甚至更小的像素区域------在经历了多次下采样之后,特征图上可能就剩个寂寞了。
小目标检测一直是工业落地中的硬骨头。今天我们就拆解几套实战中验证过的改进策略,不搞理论堆砌,直接上能写进代码的干货。
二、数据层面:别急着改模型
遇到小目标漏检,第一反应不应该是改网络结构。先看看数据怎么喂的。
保持原始分辨率
YOLO默认的640输入,对于小目标就是灾难。我一般先尝试跳到1024甚至1280:
python
# 训练时直接改输入尺寸
model.train(data='pcb.yaml', imgsz=1280, batch=8) # batch得调小,显存警告!
# 推理时也要保持一致
results = model('defect.jpg', imgsz=1280) # 这里踩过坑:训练推理尺寸不一致,效果直接崩
但注意,大尺寸训练会显著拖慢速度,且容易过拟合小目标------其他目标可能反而变差。
切图大法好
更稳的一招是大图切块训练+滑动窗口推理。把高分辨率原图切成重叠的小块,每块里的小目标就变成了"中目标":
python
# 简易切图示例
def slide_crop(img, crop_size=640, overlap=0.2):
stride = int(crop_size * (1 - overlap))
patches = []
# 别这样写死循环,大图会炸,建议用yield省内存
for y in range(0, img.shape[0], stride):
for x in range(0, img.shape[1], stride):
patch = img[y:y+crop_size, x:x+crop_size]
if patch.shape[0] == crop_size and patch.shape[1] == crop_size:
patches.append(patch)
return patches
训练时对每个切块单独标注,推理时切块检测再拼回去。代价是计算量翻倍,但召回率提升明显。
人工放大标注区域
对于特别关键的小缺陷,可以在标注时稍微把框画大一圈,给模型一点学习余地。这招有点"作弊",但产线急上线时很管用。
三、网络结构:三个必改点
1. 减少下采样次数
YOLO默认下采样32倍,小目标到最后一层特征图早就没了。把最后一个stride=2的卷积或池化去掉,改成16倍下采样:
yaml
# 修改model.yaml的backbone部分
# 把某个P5层的stride从2改成1,记得调整后续通道数匹配
# 这块改完一定要算清楚特征图尺寸,不然head会报shape不对
2. 增加高分辨率检测头
YOLOv11默认三个检测头(P3/P4/P5)。对于小目标,我在P2(更大特征图)上加了一个检测头:
python
# 在head部分新增一个P2输出
- from: ['backbone.某个浅层输出']
number: 1
args: [[128, 256, 'NeckBlock', ...]] # 这里参数根据实际通道数调整
# 注意这个头只检测小目标,anchor要重新聚类
然后针对P2头,只用小尺寸的anchor(比如8×8、16×16),避免大anchor干扰。
3. 注意力机制不是银弹
很多人喜欢加SE、CBAM等注意力模块。对于小目标,空间注意力比通道注意力更重要。我习惯在浅层特征后加一个简单的空间注意力:
python
class SimpleSpatialAttention(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(2, 1, kernel_size=7, padding=3, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# 沿着通道维度做均值池化和最大池化
avg_out = torch.mean(x, dim=1, keepdim=True)
max_out, _ = torch.max(x, dim=1, keepdim=True)
concat = torch.cat([avg_out, max_out], dim=1)
attention = self.sigmoid(self.conv(concat))
return x * attention # 增强重要区域
但注意,注意力模块会拖慢推理速度,部署前要评估性价比。
四、损失函数:让模型更"关注"小目标
1. 修改anchor匹配策略
YOLO默认用IoU匹配anchor,小目标因为位置敏感,可以改用NWD(Normalized Wasserstein Distance) 作为匹配度量。NWD对微小框的位置偏差更敏感:
python
# NWD计算函数
def wasserstein_loss(pred, target):
# 把框转为高斯分布,算Wasserstein距离
# 具体实现略,篇幅有限,可搜NWD for YOLO
return distance
2. 损失权重动态调整
在loss计算时,给小目标更高的权重:
python
# 在compute_loss函数里
for i, pi in enumerate(pred):
# pi是第i个检测头的输出
# 根据target的尺寸动态赋权
box_loss_scale = 2.0 - target[:, 3] * target[:, 4] # 框越小,权重越高
# 乘到bbox_loss上
3. 分类损失用QFL
Quality Focal Loss不仅解决正负样本不平衡,还对难例小目标有更好的梯度响应。把普通的Focal Loss换成QFL,小目标召回能提2-3个点。
五、后处理与部署陷阱
1. NMS的改进
小目标经常密集出现,传统NMS容易误杀。试试Soft-NMS 或DIoU-NMS:
python
# 直接用torchvision里的soft_nms
from torchvision.ops import nms, soft_nms
# soft_nms返回的是新分数,需要阈值过滤
更简单的办法是调高NMS的iou_thres,比如从0.45调到0.6,让重叠的小目标更容易保留。
2. 部署时的分辨率对齐
训练时用了大尺寸或切图,部署时也要保持一致。嵌入式端显存不够怎么办?动态分辨率 或者ROI区域检测:
cpp
// 嵌入式C++伪代码
// 第一遍用低分辨率检测大目标
// 裁出疑似区域,第二遍高分辨率细查
// 这样整体耗时可控
3. 量化带来的精度损失
小目标对量化更敏感,8bit量化后可能消失。建议:
- 对小目标检测头单独用更高精度(如16bit)
- 用感知量化训练(QAT)而不是后训练量化(PTQ)
- 量化前先做通道蒸馏,让网络对小目标更鲁棒
六、个人经验包
-
小目标检测没有通用解法,先分析你的目标到底多小、多密、多模糊。拿张典型图,用画图工具数像素,再定策略。
-
数据永远比模型重要。花两天时间精细化标注,比调一个月模型提升更大。特别是边界模糊的小目标,标注一致性是关键。
-
不要盲目堆模块。先试输入分辨率→再试数据增强(如mosaic+小目标复制)→最后改网络。我见过有人先改了一堆结构,最后发现是训练时忘了开mosaic。
-
部署时留余量。训练时小目标召回95%,部署后可能只剩80%。预留一些阈值调整空间,比如检测分数阈值做成可配置参数。
-
终极方案:上高像素相机。算法工程师的尊严是能用算法解决,但实际项目里,换个好相机可能立竿见影------硬件升级有时候是最经济的方案。