简介:这是一份面向YOLO目标检测训练场景的数据增强工具包,主要解决已标注数据集样本不足、场景单一的问题,适合正在使用YOLO系列模型、需要扩充训练集的研究者与工程人员。资源以Python脚本为核心,围绕.txt格式标注文件实现旋转、平移、翻转、裁剪、调整亮度和增加噪声六种增强方式,可在不破坏标注对应关系的前提下批量生成新样本。压缩包共4个文件,包含3个py脚本与1份md说明文档,整体约11KB,体积轻量、便于直接集成到现有训练流程中。其中脚本分别承担标注格式转换与增强逻辑执行等职责,说明文档则帮助快速理解调用方式与参数含义。目前已有2450人学习下载,适合希望低成本提升模型泛化能力、快速搭建数据增强环节的读者参考使用。
1. 为什么 .txt 标注数据集不能直接拿去训 YOLO
很多人第一次训 YOLO 时,数据集只有几十张图,标注文件是 LabelImg 导出的.txt,每行class x_center y_center width height,坐标全是 0~1 的归一化值。直接开训,mAP 卡在 0.3 上不去,验证集 loss 震荡,模型对光照和角度几乎没鲁棒性。问题往往不在网络结构,而在数据本身太"干净"——同一个场景、同一个角度、同一种亮度,模型记住的是背景而不是目标。
数据增强要解决的就是这件事:在不改变标注语义的前提下,把有限的已标注样本扩展成数量级更大的训练集。旋转、平移、翻转、裁剪、调整亮度、增加噪声这六种方式,覆盖了几何变换和像素变换两大类,是 YOLO 训练里性价比最高的一组增强手段。关键在于,增强必须同步更新.txt里的坐标,否则图和标签对不上,训练直接崩。
这篇面向已经用 LabelImg 或类似工具打好 YOLO 格式标签、准备扩充数据集的人。下面会从.txt坐标的数学含义讲起,给出六种增强的可复现代码,再落到批量处理、参数设置和排错上。
2. YOLO .txt 标注格式与增强坐标变换原理
2.1 归一化坐标的数学含义与还原方式
YOLO 的.txt每行五个字段:class_id x_center y_center width height。后四个都是相对整张图宽高的归一化值,范围 0~1。要理解增强,先得把它还原成像素坐标:
x_center_px = x_center * img_w y_center_px = y_center * img_h w_px = width * img_w h_px = height * img_h x1 = x_center_px - w_px / 2 y1 = y_center_px - h_px / 2 x2 = x_center_px + w_px / 2 y2 = y_center_px + h_px / 2所有几何增强的本质,就是对(x1, y1, x2, y2)这四个角点做变换,再反算回归一化值。像素级增强(亮度、噪声)不动坐标,只改图像矩阵。这个区分很重要:几何变换必须同步改标签,像素变换只改图。
2.2 六种增强方式对标注框的影响差异
| 增强方式 | 类型 | 是否改坐标 | 主要作用 |
|---|---|---|---|
| 旋转 | 几何 | 是 | 提升角度鲁棒性 |
| 平移 | 几何 | 是 | 缓解目标位置偏置 |
| 翻转 | 几何 | 是 | 扩充镜像样本 |
| 裁剪 | 几何 | 是 | 模拟遮挡与尺度变化 |
| 亮度 | 像素 | 否 | 适应光照变化 |
| 噪声 | 像素 | 否 | 提升抗干扰能力 |
旋转和平移是热搜里出现频率最高的两个词,也是坑最多的两个。旋转后框会变成斜矩形,而 YOLO 只支持水平矩形框,所以必须用旋转后的外接矩形(axis-aligned bounding box)来近似,这会引入少量背景,但工程上可接受。平移如果移出边界,框会被裁掉一部分,需要判断剩余面积占比,太小就丢弃该框。
2.3 增强后坐标越界与框失效的处理原则
变换后要过三道检查:坐标是否落在[0, 1]内、宽高是否大于 0、框面积占原图比例是否过小。常见做法是裁剪到边界,然后计算剩余面积,低于原面积 30% 的框直接删掉。如果一张图所有框都被删了,这张增强图也一并丢弃,否则会引入纯背景负样本,干扰训练。
注意:坐标裁剪后一定要重新归一化,且宽高要用裁剪后的实际值重算,不能沿用旧值。
3. 用 Python 实现六种增强并同步改写 .txt
3.1 环境准备与目录结构约定
依赖只有 OpenCV、NumPy 和标准库,不需要额外框架:
pip install opencv-python numpy目录按 YOLO 惯例组织,增强脚本读images/和labels/,输出到aug_images/和aug_labels/,文件名保持一致(只换扩展名):
dataset/ images/ xxx.jpg labels/ xxx.txt augmented/ images/ xxx_rot.jpg labels/ xxx_rot.txt3.2 旋转、平移、翻转的坐标同步代码
import cv2 import numpy as np import os def read_label(txt_path, img_w, img_h): boxes = [] if not os.path.exists(txt_path): return boxes with open(txt_path) as f: for line in f: c, x, y, w, h = line.strip().split() c, x, y, w, h = int(c), float(x), float(y), float(w), float(h) # 归一化 -> 像素角点 x1 = (x - w / 2) * img_w y1 = (y - h / 2) * img_h x2 = (x + w / 2) * img_w y2 = (y + h / 2) * img_h boxes.append([c, x1, y1, x2, y2]) return boxes def write_label(boxes, txt_path, img_w, img_h): lines = [] for c, x1, y1, x2, y2 in boxes: # 裁剪到图像边界 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(img_w, x2), min(img_h, y2) if x2 - x1 < 1 or y2 - y1 < 1: continue xc = (x1 + x2) / 2 / img_w yc = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{c} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(txt_path, "w") as f: f.write("\n".join(lines))read_label把归一化值还原成像素角点,write_label做边界裁剪、面积过滤再写回归一化。这两个函数是所有几何增强的公共底座,后面每种增强都复用它们。
3.3 裁剪、亮度、噪声的像素级实现
def aug_brightness(img, factor=1.3): # 亮度调整:factor>1 变亮,<1 变暗 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[..., 2] = np.clip(hsv[..., 2] * factor, 0, 255) return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) def aug_noise(img, sigma=15): # 高斯噪声 noise = np.random.normal(0, sigma, img.shape).astype(np.float32) out = np.clip(img.astype(np.float32) + noise, 0, 255) return out.astype(np.uint8)亮度在 HSV 的 V 通道上乘系数,避免直接改 BGR 导致偏色。噪声用高斯分布叠加,sigma控制强度,15 左右比较温和,超过 30 会明显糊掉目标边缘。这两种增强不动标签,直接复制原.txt即可。
3.4 旋转与平移的仿射矩阵写法
def aug_rotate(img, boxes, angle=15): h, w = img.shape[:2] M = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) out = cv2.warpAffine(img, M, (w, h), borderValue=(114, 114, 114)) new_boxes = [] for c, x1, y1, x2, y2 in boxes: pts = np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtype=np.float32) pts = pts @ M[:, :2].T + M[:, 2] nx1, ny1 = pts[:, 0].min(), pts[:, 1].min() nx2, ny2 = pts[:, 0].max(), pts[:, 1].max() new_boxes.append([c, nx1, ny1, nx2, ny2]) return out, new_boxes def aug_translate(img, boxes, tx=30, ty=20): h, w = img.shape[:2] M = np.float32([[1, 0, tx], [0, 1, ty]]) out = cv2.warpAffine(img, M, (w, h), borderValue=(114, 114, 114)) new_boxes = [[c, x1 + tx, y1 + ty, x2 + tx, y2 + ty] for c, x1, y1, x2, y2 in boxes] return out, new_boxes旋转用getRotationMatrix2D生成仿射矩阵,四个角点乘矩阵后取外接矩形。borderValue用 114 是 YOLO 官方 letterbox 的填充灰,保持一致能减少分布偏移。平移就是给角点加偏移量,逻辑更直接。翻转只需把x1和x2对调再镜像:nx1 = w - x2, nx2 = w - x1。
4. 批量增强脚本与参数调优实战
4.1 批量遍历与增强倍数控制
单张增强没意义,要批量跑并控制每张图生成几张。常见做法是每张原图随机抽 2~3 种增强组合,而不是六种全上——全上会让数据集爆炸且冗余。
import random def batch_augment(img_dir, lbl_dir, out_img, out_lbl, repeat=3): os.makedirs(out_img, exist_ok=True) os.makedirs(out_lbl, exist_ok=True) for name in os.listdir(img_dir): if not name.lower().endswith((".jpg", ".png", ".jpeg")): continue stem = os.path.splitext(name)[0] img = cv2.imread(os.path.join(img_dir, name)) h, w = img.shape[:2] boxes = read_label(os.path.join(lbl_dir, stem + ".txt"), w, h) for i in range(repeat): aug_img, aug_boxes = img.copy(), [b[:] for b in boxes] ops = random.sample(["rot", "trans", "flip", "crop", "bright", "noise"], 2) for op in ops: if op == "rot": aug_img, aug_boxes = aug_rotate(aug_img, aug_boxes, random.uniform(-20, 20)) elif op == "trans": aug_img, aug_boxes = aug_translate(aug_img, aug_boxes, random.randint(-40, 40), random.randint(-40, 40)) elif op == "bright": aug_img = aug_brightness(aug_img, random.uniform(0.7, 1.4)) elif op == "noise": aug_img = aug_noise(aug_img, random.randint(5, 20)) out_name = f"{stem}_aug{i}" cv2.imwrite(os.path.join(out_img, out_name + ".jpg"), aug_img) write_label(aug_boxes, os.path.join(out_lbl, out_name + ".txt"), w, h)repeat控制每张图生成几张,random.sample保证同一次不重复选同种增强。翻转和裁剪可以按同样模式接进去。跑完检查输出目录,图数应该是原图的repeat倍。
4.2 关键参数取值范围与经验值
| 参数 | 建议范围 | 说明 |
|---|---|---|
| 旋转角度 | -20° ~ 20° | 超过 30° 外接框背景过多 |
| 平移比例 | ±15% 图宽 | 太大导致目标频繁出界 |
| 亮度系数 | 0.7 ~ 1.4 | 再暗会丢纹理 |
| 噪声 sigma | 5 ~ 20 | 超过 30 目标边缘模糊 |
| 裁剪面积比 | 保留 ≥ 0.3 | 低于此值框失效 |
这些值是通用起点,具体任务要调。比如烟草病虫害数据集里叶片占满画面,旋转可以放宽到 ±30°;而小目标检测(如投篮场景的球)平移要收紧,否则球容易移出画面。
4.3 增强后数据集校验与可视化检查
增强完必须抽查,最直接的办法是把框画回图上:
def visualize(img_path, txt_path, out_path): img = cv2.imread(img_path) h, w = img.shape[:2] for c, x1, y1, x2, y2 in read_label(txt_path, w, h): cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.imwrite(out_path, img)随机抽 20 张看框是否贴合目标。如果框整体偏移,多半是旋转矩阵的角点计算写错了;如果框消失,检查面积过滤阈值是不是太严。这一步能挡掉 90% 的坐标同步 bug。
提示:增强后的数据集建议单独放一个目录,不要和原图混在一起,训练时用两个 data yaml 分别验证原图和增强图的效果差异。
5. 增强数据接入 YOLO 训练与效果验证技巧
增强数据准备好后,写 data yaml 指向增强目录,train和val分开。一个容易忽略的点是:验证集不要用增强图,否则评估的是增强分布而不是真实分布,mAP 会虚高。常见做法是原图按 8:2 切训练和验证,只对训练集做增强。
训练时观察 loss 曲线,如果增强后 loss 下降更平滑、验证 mAP 提升 3~8 个点,说明增强有效。如果反而下降,先排查标签是否错位,再考虑增强强度是否过大——旋转 45° 加平移 30% 这种组合,框里几乎全是背景,模型学不到东西。
一个实用技巧是分阶段增强:前 50 个 epoch 用弱增强(小角度旋转、轻微亮度),后 50 个 epoch 加大强度。这样模型先学到目标基本特征,再适应各种扰动,收敛更稳。另外,增强倍数不是越多越好,原图 500 张扩到 2000 张通常够用,扩到 10000 张边际收益很低,还拖慢训练。
最后验证增强是否真的同步了标签,可以拿增强图单独跑一次推理,看预测框和标注框是否重合。重合度高,说明整条增强链路是通的。
本文还有配套的精品资源,点击获取