094、YOLOv8改进实战:Copy-Paste增强在目标检测中的应用与YOLOv8代码实现
上个月调一个密集小目标场景的模型,mAP卡在0.52死活上不去。试了Mosaic、MixUp、CutMix,效果都一般。后来翻到一篇2020年的论文,Copy-Paste增强,抱着试试看的心态改了一版,mAP直接跳到0.61。今天把这个坑填上,聊聊Copy-Paste在YOLOv8里的落地实现。
为什么Mosaic搞不定密集小目标
Mosaic把四张图拼在一起,确实增加了小目标数量,但有个致命问题——小目标在拼接后变得更小了。原本32x32的物体,缩放到原图1/4区域后变成16x16,特征图上的响应几乎消失。Copy-Paste的思路完全不同:直接把目标对象复制粘贴到其他图片上,目标尺寸不变,只是换个背景。
这个思路在工业场景特别实用。比如检测生产线上的螺丝,正常样本里螺丝分布稀疏,Copy-Paste可以制造出密集场景,模型被迫学会区分重叠目标。
Copy-Paste的核心逻辑
简单说就是三步:从源图抠出目标,随机选个位置贴到目标图,处理遮挡关系。但落地时有两个关键点容易翻车。
第一个坑是标签处理。粘贴后目标框坐标变了,需要重新计算。如果粘贴位置和原有目标重叠,要不要保留?我的做法是保留所有目标,让模型自己去学遮挡情况。实验证明,保留遮挡目标比删除效果好3-4个点。
第二个坑是粘贴比例。贴太多会破坏背景分布,模型学会"有目标的地方背景就奇怪"。我控制在每张图最多贴3个目标,超过这个数效果反而下降。
YOLOv8代码实现
先看数据加载部分。YOLOv8的Dataset类在ultralytics/data/dataset.py里,我们需要在load_mosaic方法后面加一个分支。
defload_copy_paste(self,index):# 这里踩过坑:直接修改原图会导致后续epoch数据重复# 正确做法是每次随机选择源图和目标图ifrandom.random()>self.copy_paste_prob:# 控制概率,我设0.5returnself.load_image(index)# 随机选一张源图,别选同一张src_idx=random.choice([iforiinrange(len(self.im_files))ifi!=index])src_img,src_hyp=self.load_image(src_idx)# 获取源图的目标框和类别src_labels=self.labels[src_idx].copy()iflen(src_labels)==0:returnself.load_image(index)# 源图没目标就跳过# 随机选一个目标来粘贴obj_idx=random.randint(0,len(src_labels)-1)obj_box=src_labels[obj_idx][1:5]# xywh格式obj_cls=src_labels[obj_idx][0]# 抠出目标区域,这里别用cv2.resize,会改变目标尺寸x1,y1,x2,y2=self.xywh2xyxy(obj_box)obj_patch=src_img[int(y1):int(y2),int(x1):int(x2)].copy()# 随机生成粘贴位置,保证目标完全在图像内h,w=src_img.shape[:2]paste_x=random.randint(0,w-(x2-x1))paste_y=random.randint(0,h-(y2-y1))# 粘贴操作,这里用alpha blending效果更好# 简单做法直接覆盖,但边缘会有锯齿target_img=self.load_image(index)[0]target_img[paste_y:paste_y+obj_patch.shape[0],paste_x:paste_x+obj_patch.shape[1]]=obj_patch# 更新标签:添加新目标框new_box=[obj_cls,(paste_x+paste_x+obj_patch.shape[1])/2/w,# cx(paste_y+paste_y+obj_patch.shape[0])/2/h,# cyobj_patch.shape[1]/w,# wobj_patch.shape[0]/h]# hself.labels[index]=np.vstack([self.labels[index],new_box])returntarget_img这个实现有个问题——每次都要加载两张图,训练速度会下降。我的优化方案是预加载一批图到缓存,随机选源图时从缓存里取。
训练配置
在YOLOv8的配置文件中添加参数:
# copy_paste增强参数copy_paste:0.5# 应用概率copy_paste_max_objects:3# 每张图最多贴几个copy_paste_scale_range:[0.5,1.5]# 目标缩放范围,别太大注意copy_paste_scale_range这个参数。我一开始没加缩放,模型对固定尺寸目标过拟合。加上0.5到1.5的随机缩放后,泛化能力明显提升。
踩过的坑
第一个坑是类别不平衡。如果源图里某个类别特别多,粘贴后这个类别的样本会爆炸。我加了类别采样权重,让每个类别被选中的概率均衡。
第二个坑是背景污染。粘贴的目标边缘有原背景的像素,导致模型学到"目标周围有特定颜色"。解决方案是用高斯模糊处理边缘,或者用泊松融合。
第三个坑是标签格式。YOLOv8的标签是归一化的,粘贴后要重新计算归一化坐标。我在这里debug了半小时,因为忘记把像素坐标转成归一化坐标。
效果对比
在VisDrone数据集上,不加Copy-Paste的mAP是0.52,加了之后0.61。提升主要来自小目标类别,比如"行人"从0.38涨到0.49,"自行车"从0.41涨到0.53。
但注意,这个增强对中大目标效果不明显,甚至可能下降。因为中大目标本身特征丰富,粘贴后反而引入噪声。我建议只在密集小目标场景使用。
个人经验
Copy-Paste不是万能药。如果你的数据集目标分布均匀,Mosaic+MixUp的组合就够了。但遇到以下情况,Copy-Paste值得一试:目标尺寸小于32x32、同类目标经常重叠、背景单一但目标密集。
另外,训练轮数要适当增加。Copy-Paste增加了数据复杂度,我通常把epoch从300调到400,让模型充分学习粘贴后的分布。
最后提醒一点:验证集不要用Copy-Paste。我见过有人把增强用在验证集上,结果mAP虚高,上线后直接翻车。