简介:面向YOLOv5等目标检测与分割任务的数据增强工具,针对训练样本数量不足、手工标注成本高的问题,提供带标签图片的自动扩增方案。工具支持LabelImg与LabelMe两种常用标注格式,内置随机翻转、剪切、仿射变换、高斯模糊、平移、自适应高斯噪声、亮度调整等策略,并可将多种策略随机组合后作用于训练样本。资源包共23个文件,以jpg示例图片和4个Python脚本为主,附带xml、json标注样例及md说明文档,压缩包仅1.64MB,目录结构清晰,便于按demo和md说明快速上手。使用时将待增强图片和已有标注文件放入对应文件夹,执行脚本即可批量生成新样本,有效扩充数据集、提升模型泛化能力,显著减少重复标注工作量;部署前需安装OpenCV等依赖。目前已有876人学习下载,尤其适合需要快速扩增检测或分割数据集的研究者和开发者。
1. 数据集太小,YOLO 训练总是过拟合?带标签扩增才是正解
做目标检测的应该都有这种感觉:YOLO 模型结构再先进,扔给它一个只有两三千张图的训练集,训练曲线漂亮得像过拟合教科书——验证集 mAP 上不去,loss 降到底了但检测框乱飘。很多人第一反应是加数据,但标注成本摆在那,一张图人工画框平均要几十秒,一万张图就是几十个小时的纯体力活。这时候数据增强是唯一能在不增加标注成本的前提下把训练集撑大的手段。
但这里有个关键区别:分类任务的数据增强可以直接对图片做翻转、裁剪、调色,标签跟着图走就行了;YOLO 检测任务不一样,每张图带的是一组归一化坐标的边界框,图一旦变形、裁剪、拼接,框的坐标也得同步换算。带标签扩增指的就是这个——增强过程中图片和标注同步变换,扩增出来的每一张图都自带有效标签,可以直接丢进训练 pipeline,不需要人工二次标注。本文就说清楚这套东西怎么落地:原理、常用方法、参数怎么调、工程上怎么组织,以及我踩过的那些坑。
2. 带标签扩增的原理:为什么增强必须动坐标
2.1 几何变换下标签的数学映射
增强操作分两大类:几何变换和颜色变换。颜色变换(亮度、对比度、色相、饱和度)不改变像素位置,标签完全不用动,直接对整图做就行。几何变换则不同——翻转、旋转、缩放、裁剪、平移,每一个操作都在改变像素坐标,标签框必须跟着做同样的矩阵变换。
YOLO 的标签格式是归一化的中心点坐标加宽高,即class_id, x_center, y_center, width, height,所有值都除以了图片宽高,取值在 0 到 1 之间。这意味着任何几何变换完成后,要把新坐标重新归一化。
以水平翻转为例。翻转前某个框的中心点是 (x_center, y_center),图宽为 W。水平翻转后,新的中心点 x 坐标变成 W - x_center * W,归一化后就是 1 - x_center。y 坐标完全不变,宽高也不变。就这么简单的一个公式,但很多人写增强脚本时,把图翻转了却忘了改标签,或者标签改了但宽高没同步,训练出来的模型必然出问题。
2.2 坐标变换的常见坑
旋转比翻转麻烦得多。旋转 90 度时,标签框的宽高要交换,中心点坐标要经过旋转变换矩阵计算。旋转任意角度(比如 30 度)更复杂——原框是轴对齐矩形,旋转后可能变成倾斜框,YOLO 格式不支持倾斜框,只能重新计算旋转后覆盖该目标的最小外接轴对齐矩形,这会导致框变大,把背景也包进去。
裁剪和缩放同理。随机裁剪时,如果目标被部分裁掉,要么丢弃这个目标,要么保留剩余部分并重新计算框。实践中常见做法是设一个 IoU 阈值,裁剪后目标剩余面积与原框面积的比值低于某个值(比如 0.3)就丢弃该标签,否则保留并更新坐标。
提示:无论用哪套增强库,最终验证标签正确性的方法只有一个——增强后把新标注画回新图上,肉眼检查一批。坐标算错在代码里很难看出来,但画出来一眼就露馅。
2.3 增强空间的选择逻辑
不是所有增强方法都适合所有场景。做工业质检的,目标永远是正着摆放的零件,随机旋转 90 度增强出来的数据反而会让模型学会检测倒着的零件,推理时误检率飙升。做航拍检测的,目标朝向随机,旋转增强就非常有用。
选择增强方法要看你的推理场景:训练时的增强分布要尽量覆盖部署时可能遇到的变化,但不要覆盖不可能出现的变化。这一步没有标准答案,需要对业务场景有判断。下面一节我按方法逐个说适用场景和参数设置。
3. 常用增强方法与参数设置:从 Mosaic 到 Copy-Paste
3.1 Mosaic 增强:YOLOv4 之后的事实标准
Mosaic 是 YOLOv4 提出的增强方法,思路很简单:把 4 张训练图随机缩放后拼成一张大图,同时更新所有标签坐标。它的好处有三个:一是图片内容多样性大增,一个 batch 里能看到 4 张图的上下文;二是批量归一化的统计量更稳定;三是小目标检测效果明显改善——拼接后每张子图变小,相当于模型看到了更多小尺寸目标。
实现要点是拼接边界要随机,具体做法:先在 0.5 到 1.5 之间随机采样一个缩放因子,对每张子图做缩放,然后按随机位置拼到四象限,切掉超出边界的目标。代码层面 OpenCV 的warpAffine配合标签矩阵同步变换即可实现。
参数设置上,Ultralytics YOLO 系列默认开启 Mosaic,在训练最后 10 个 epoch 会自动关闭——因为最后阶段需要看到接近真实分布的数据来收敛。我一般保持默认,只在检测目标特别小或特别大的极端场景下手动调整拼接比例。
3.2 HSV 扰动:零成本提升鲁棒性
HSV 色域扰动是性价比最高的一类增强,因为它完全不改坐标,出问题的概率最低。对 H(色相)、S(饱和度)、V(明度)三个通道分别做偏移和缩放,模拟不同光照条件。
参数上常见的取值范围:H 偏移 ±0.015,S 缩放 0.7 到 1.3,V 缩放 0.7 到 1.3。这些值不是拍脑袋定的,而是对采集场景光照变化范围的经验估计。室内固定光源可以收窄到 ±0.01 和 0.9 到 1.1,室外自然光可以放宽到 ±0.02 和 0.5 到 1.5。
注意一点:颜色扰动不要做得太狠,否则目标颜色特征被破坏,模型学到的颜色特征失真。比如检测红色刹车卡钳的项目,H 通道偏移过大可能让红色变成蓝色,学出来的模型在真实红色卡钳上表现极差。
3.3 随机翻转与旋转:几何增强的基础盘
水平翻转是几乎所有目标检测训练默认开启的增强,因为绝大多数场景不存在左右不对称的硬约束(除了车牌、文字等方向敏感目标)。垂直翻转使用频率低很多,因为真实场景中目标倒置出现的概率很小。
随机旋转要注意边界处理。旋转后图像四角会出现黑边或空白,常见处理方式有三种:一是用边缘像素填充;二是直接裁剪掉空白区域(这等同于随机裁剪,改变标签);三是保持原图尺寸不变,旋转后缩放(这会让目标变小)。具体做法取决于旋转角度范围,小角度旋转(±10 度以内)用边缘填充问题不大,大角度旋转最好走裁剪路线。
旋转角度范围的设置原则:超过部署场景中可能出现的角度变化就是负优化。正常道路场景的车牌检测,±5 度足够;工业零件随意摆放的场景,0 到 360 度全转都不为过。
3.4 随机裁剪与缩放:等价于目标尺度扰动
随机裁剪模拟的是目标部分被遮挡或出画的情况,同时也在改变目标在画面中的尺度。YOLO 训练时有个常见问题——大目标和小目标的损失权重是相同的,但小目标天然更难学,如果训练集中小目标数量偏少,模型对小目标的召回率就很低。这时可以通过随机裁剪让同一目标以不同尺度多次出现。
具体做法有两种:一是对整图做随机裁剪,裁剪区域大小在 0.4 到 1.0 倍原图面积之间采样,裁完 resize 回训练尺寸;二是对某个目标做局部放大——找到该目标框,以它为中心向四周扩张 1.2 到 1.5 倍作为裁剪区域,然后 resize 回训练尺寸。第二种做法本质是让模型多看到该目标的细节。
裁剪后标签要过滤掉面积过小或出画比例过高的目标,这个在后面避坑章节细说。
3.5 Copy-Paste:实例级增强的思路
Copy-Paste 增强的思路是把一张图中的目标实例剪切出来,粘贴到另一张图上,两张图的标签要合并。这个增强对提升检测器在复杂背景下的鲁棒性有明显帮助,但当目标和背景差异过大时,粘贴痕迹太明显,模型会学到「贴上去的目标」这种虚假特征。
要降低粘贴痕迹,常见做法是粘贴时做颜色抖动和边缘羽化。另一个办法是只粘贴到语义相似的背景上——比如把路上的车贴到另一条路上,而不是贴到草地上。但语义相似性判断本身需要额外模型,工程复杂度高。如果训练集规模本身已经过万,Copy-Paste 的边际收益不大,优先把 Mosaic、翻转、HSV 这三板斧用好更现实。
4. 工程落地:用 Python 脚本实现带标签扩增的完整流程
4.1 先搭一个最小的代码骨架
带标签扩增的工程实现,最稳妥的做法是写一个统一的数据读取和增强pipeline,而不是每次手工调用 OpenCV 拼凑。下面这个脚本是我常用的最小实现,逻辑一次说清。
import cv2 import numpy as np import random import os def read_yolo_label(label_path): """读取 YOLO 格式的标签文件""" boxes = [] with open(label_path, 'r') as f: for line in f.readlines(): line = line.strip().split() if len(line) != 5: continue class_id = int(line[0]) x_center, y_center, w, h = map(float, line[1:]) boxes.append([class_id, x_center, y_center, w, h]) return np.array(boxes, dtype=float) def horizontal_flip(image, boxes): """水平翻转:x_center 变为 1 - x_center,其余不变""" h, w = image.shape[:2] image = cv2.flip(image, 1) if len(boxes) > 0: boxes[:, 1] = 1.0 - boxes[:, 1] # 只改 x_center return image, boxes def random_rotate(image, boxes, angle_range=(-10, 10)): """随机旋转,并重新计算旋转后的外接框""" h, w = image.shape[:2] angle = random.uniform(*angle_range) matrix = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) image = cv2.warpAffine(image, matrix, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101) if len(boxes) > 0: # 归一化坐标转像素坐标 cx = boxes[:, 1] * w cy = boxes[:, 2] * h bw = boxes[:, 3] * w bh = boxes[:, 4] * h # 框的四个角点 corners = np.array([ [cx - bw / 2, cy - bh / 2], [cx + bw / 2, cy - bh / 2], [cx - bw / 2, cy + bh / 2], [cx + bw / 2, cy + bh / 2] ]) # 每个框 4 个角点 # 用旋转矩阵对角点做变换 pts = np.hstack([corners.reshape(-1, 2), np.ones((len(corners), 1))]) transformed = pts @ matrix.T new_corners = transformed.reshape(-1, 4, 2) # 取变换后角点的外接矩形 x_min = new_corners[:, :, 0].min(axis=1) x_max = new_corners[:, :, 0].max(axis=1) y_min = new_corners[:, :, 1].min(axis=1) y_max = new_corners[:, :, 1].max(axis=1) # 转回归一化坐标 boxes[:, 1] = ((x_min + x_max) / 2) / w boxes[:, 2] = ((y_min + y_max) / 2) / h boxes[:, 3] = (x_max - x_min) / w boxes[:, 4] = (y_max - y_min) / h return image, boxes这段代码有两点值得细说。horizontal_flip里归一化坐标的优势体现得很明显——只改一个值就行,不用关心图片实际分辨率。random_rotate中旋转后外接框的计算是常见翻车点:如果要严格精确应该用cv2.transform或对每个角点做矩阵乘法,但工程上用矩阵运算合并批量处理足够。边缘填充用了BORDER_REFLECT_101,它比BORDER_CONSTANT(黑色填充)效果好很多,因为填充内容来自图像边缘像素,不会引入突兀的黑边。
4.2 随机裁剪的实现与过滤规则
def random_crop(image, boxes, crop_scale=(0.4, 1.0), min_remaining=0.3): """随机裁剪,返回裁剪后图片和无出界目标""" h, w = image.shape[:2] crop_w = int(w * random.uniform(*crop_scale)) crop_h = int(h * random.uniform(*crop_scale)) x_start = random.randint(0, w - crop_w) y_start = random.randint(0, h - crop_h) x_end = x_start + crop_w y_end = y_start + crop_h image = image[y_start:y_end, x_start:x_end] new_h, new_w = image.shape[:2] new_boxes = [] for box in boxes: cls, xc, yc, bw, bh = box # 归一化转像素 x1 = (xc - bw / 2) * w y1 = (yc - bh / 2) * h x2 = (xc + bw / 2) * w y2 = (yc + bh / 2) * h # 与裁剪区域的交集 ix1 = max(x1, x_start) iy1 = max(y1, y_start) ix2 = min(x2, x_end) iy2 = min(y2, y_end) if ix2 <= ix1 or iy2 <= iy1: continue # 完全出界,丢弃 inter_area = (ix2 - ix1) * (iy2 - iy1) orig_area = (x2 - x1) * (y2 - y1) if inter_area / orig_area < min_remaining: continue # 剩余面积过小,丢弃 # 换算到裁剪后的图上 nx1, ny1 = ix1 - x_start, iy1 - y_start nx2, ny2 = ix2 - x_start, iy2 - y_start nxc = ((nx1 + nx2) / 2) / new_w nyc = ((ny1 + ny2) / 2) / new_h nbw = (nx2 - nx1) / new_w nbh = (ny2 - ny1) / new_h new_boxes.append([cls, nxc, nyc, nbw, nbh]) return image, np.array(new_boxes)这个函数的参数是血泪经验换来的。min_remaining=0.3表示目标剩余面积低于原来的三成就丢,因为剩余太少的目标特征已经残破,模型学到的要么是被遮挡目标的特征(有用),要么是残缺背景(有害)。实际使用中我会观察保留率——每跑一次增强打印一下丢了多少目标,如果某个类的目标被大量丢弃,说明该类目标相对图片面积偏大,裁剪策略对它不友好。
另一个细节:裁剪后的new_boxes可能为空数组,代码里要处理这种情况。常见做法是空标签图直接丢弃,不做训练样本,因为 YOLO 对全背景图会强制学「这里没有目标」,干扰训练。
4.3 离线扩增的目录组织
离线预扩增时,目录组织建议和原始数据集保持一致,让 YOLO 训练脚本不用改任何配置就能直接吃数据:
# 原始数据集 dataset/ ├── images/train/ ├── images/val/ ├── labels/train/ └── labels/val/ # 扩增后新增的数据 dataset_aug/ ├── images/train/ ├── labels/train/ ├── images/val/ # 不要扩增验证集! └── labels/val/这里有个重要的工程决策:验证集不要做增强。很多人图省事,把增强后的所有数据都塞进训练集,验证集也顺手做了增强,结果训练时验证集的分布和训练集高度一致,模型评估分数虚高,换到真实场景就露馅。验证集的意义在于模拟真实分布,增强过的数据偏离真实分布。
扩增数量规划上,我一般按 3 到 5 倍扩增,而不是贪婪地扩到 10 倍以上。原因是相同分布的增强数据超过一定量后边际收益递减,还徒增训练时间。比如原始训练集 2000 张,扩到 8000 到 10000 张是一个性价比不错的区间。
5. 踩坑与排查:带标签扩增最容易翻车的 5 个细节
5.1 标签越界导致训练 loss 变成 nan
现象:训练到一半 loss 突然变成 nan,或 loss 曲线出现断崖式下跌后不再恢复。
原因:增强后标签坐标超出了 [0, 1] 范围。比如旋转 45 度时,图像角上的目标框外接矩形可能超出图像边界,归一化后坐标大于 1 或小于 0。YOLO 训练脚本内部通常会对坐标做越界处理,但有些库不处理,直接导致损失急剧膨胀。
解决:增强 pipeline 输出前做一次坐标裁剪和过滤。对 x_center、y_center、w、h 做np.clip把值限制在 [0, 1],同时过滤掉 width 或 height 小于 0.0001 的框。也可以检查增强前后框的数量一致性——凡是丢掉超过 20% 目标的情况就要检查是不是裁剪范围或旋转角度设置得过于激进。
5.2 验证集被增强,模型评估分数虚高
现象:训练状态显示 mAP 高达 0.98,但部署到线上实测检测率只有 60%。
原因:最常见的就是验证集或测试集也走了一遍增强 pipeline。验证集本意是模拟真实场景,经过增强的验证集分布和训练集趋同,模型评估自然只会有好成绩——相当于开卷考试。
解决:代码里把验证集路径单独管理,绝不走增强函数。如果实验需要对比不同增强策略的效果,应该保持验证集固定不变,只动训练集。另外,增强后的模型要和未增强的基线模型在同一验证集上做对比,否则无法判断增强到底带来了多少增益。
5.3 目标类别不均衡在增强中被放大
现象:训练后某个类别(尤其小目标、罕见类)的 AP 反而低于增强前。
原因:如果原始数据集中 A 类有 5000 个目标,B 类只有 200 个,Mosaic 和随机裁剪按图片均匀采样,A 类被增强的次数远多于 B 类。B 类在增强后的数据中占比进一步缩小,模型更难学好它。
解决:按目标类别统计样本数量,对样本少的类别做过采样——把含 B 类的图片复制几份,在 pipeline 中对这些副本做更大的增强参数,或者给 B 类目标设定更高的保留权重。我一般会在增强 pipeline 里加一行日志,输出每类目标增强前后的数量对比,用数字确认不均衡现象有没有被放大。说到底这一步是最容易被忽略的。
5.4 小目标被旋转增强直接抹掉
现象:增强后的训练数据里小目标数量明显变少,模型的 mAP 中小目标 AP 一直垫底。
原因:图片旋转后,原本 10×10 像素的小目标经过缩放插值后可能只剩 5×5 像素,模糊到难以辨认,甚至缩放后面积低于过滤阈值被直接丢弃。这在离线扩增中是静默的,整个流程中没有任何报错。
解决:如果检测场景小目标居多,大角度旋转慎用。改用小角度旋转(±5 度)结合平移增强,目标尺度变化交给随机缩放去完成。增强前统计一下小目标的数量,增强后再统计一次,差太多就要调参数。
5.5 标签画框时目标本身被遮挡
现象:增强前手动标注时,目标被遮挡一半,框把遮挡物也包进去了。增强后遮挡物变成局部特征,模型反而学到「有遮挡物就认为是目标」。
原因:说白了这类问题根源不在增强,但增强会把它放大。原始标注框的误差偏移被多次增强后,模型学到的框中心点分布方差变大,NMS 后容易出现框偏移半个身位的情况。
解决:训练前对数据集做一次标签清洗,用置信度较高的模型(比如 COCO 预训练模型)跑一遍检测,输出每个框的尺寸、面积、位置分布,人工抽查异常样本。工程上我见过比较靠谱的做法是——把标签面积异常的框筛选出来,单独批量人工复核。这一步很耗时,但性价比高,因为标注质量直接决定了增强数据的天花板。
6. 训练时的增强配置与验证方法:把扩增数据用出最好的效果
扩增好的数据集要真正发挥价值,训练配置和验证方法得配套。先说训练配置:如果走离线扩增,数据集文件里的train路径直接指向扩增后的数据目录;如果用在线增强,Ultralytics YOLO 的hyp.yaml里有大量增强参数可调,包括 hsv_h、hsv_s、hsv_v、degrees、translate、scale、fliplr、mosaic,按我第三节说的参数范围直接改就行。
有个被反复验证有效的习惯:在线增强和离线扩增结合使用。离线扩增给数据带来多样性上限,在线增强在每次 epoch 中进一步引入随机的颜色扰动和几何扰动,两者叠加比只用其中一种效果好。但注意在线增强参数不要和离线扩增参数重复过大,否则等于套了两层厚滤镜,目标特征被磨没了。
验证方法讲一个简单但可靠的方案:用同样的超参分别训练基线模型和增强模型,在同一个固定验证集上对比 mAP。对比时至少要看出三点:整体 mAP 是否提升、每个类别 AP 的分项变化、小目标 AP 是否改善。只看整体 mAP 会漏掉细节——有时候整体没变,但小目标 AP 提升明显,这种增强策略也值得保留。
最后说一个我自己踩过的坑:刚开始做扩增时喜欢把所有方法全开,把 5 倍扩增的数据一股脑丢进去训练,结果模型训了 300 个 epoch 还没收敛。后来才意识到增强力度过猛,模型需要更多迭代次数才能见过所有变体,如果算力有限,别贪心。现在我的做法是先用默认增强跑一版基线,再逐步加增强力度,每个版本在验证集上对比——角标打印出来,心里有数再上车。数据增强调参这几年下来,最大的心得就是慢慢试,一次只动一个变量。希望帮到你。
本文还有配套的精品资源,点击获取