YOLO数据集增强实战:旋转、平移、裁剪等6种方式标签同步与避坑指南 简介面向YOLO目标检测训练场景的数据增强工具包专为已标注的.txt格式数据集设计解决小样本训练中样本不足、模型泛化能力弱的问题。包内共4个文件以3个Python脚本和1份说明文档为主压缩包约11KB轻量易部署。脚本围绕标注文件与图像同步变换展开涵盖旋转、平移、翻转、裁剪、调整亮度和增加噪声六种增强方式可在扩充图像的同时自动更新对应的YOLO标注框避免人工重新标注。说明文档交代了使用方式与目录组织便于快速接入现有训练流程。已有2451人学习下载适合需要提升数据多样性、改善模型鲁棒性的算法工程师与深度学习学习者参考使用。1. 已标注的 .txt 数据集为什么一增强就“对不上号”手里有一批 YOLO 数据集图片和同名 .txt 标签一一对应训练出来 mAP 也还行但样本太少、场景太单一模型一换光照、一换角度就崩。这时候大多数人第一反应是“做数据增强”于是打开某个在线工具或者随手写个脚本把图片旋转 90 度、翻转一下结果训练直接报错或者 mAP 掉得比不增强还惨——问题几乎都出在标签没跟着一起变。YOLO 的 .txt 标签是归一化的class x_center y_center w h坐标是相对整张图的比例值不是像素。图片一旦旋转、平移、裁剪这四个数必须同步做几何变换否则框就飘到背景上去了。更麻烦的是旋转和裁剪会让部分目标跑出画布这些越界框如果不处理训练时就是纯噪声。这篇笔记就围绕“已标注 .txt 数据集增强”这件事把旋转、平移、翻转、裁剪、调整亮度、增加噪声这 6 种方式拆开讲清楚每种变换标签怎么算、参数怎么设、哪些组合是坑。适合已经跑通 YOLO 训练、想靠增强把数据集做厚的同学也适合被“增强后标签错位”折磨过的老手对照排查。2. 六种增强的标签同步逻辑与最小实现2.1 先统一坐标系归一化值 ↔ 像素值所有几何增强的第一步都是把归一化标签还原成像素坐标做完变换再归一化回去。这一步不做后面全是错的。假设图片宽W、高H标签(cx, cy, w, h)都是 0~1# 归一化 - 像素左上角 x1y1右下角 x2y2 def xywhn2xyxy(cx, cy, w, h, W, H): x1 (cx - w / 2) * W y1 (cy - h / 2) * H x2 (cx w / 2) * W y2 (cy h / 2) * H return x1, y1, x2, y2 # 像素 - 归一化 def xyxy2xywhn(x1, y1, x2, y2, W, H): cx (x1 x2) / 2 / W cy (y1 y2) / 2 / H w (x2 - x1) / W h (y2 - y1) / H return cx, cy, w, h逻辑说明YOLO 用的是中心点加宽高而几何变换旋转矩阵、平移向量天然作用在角点上所以中间统一转成xyxy更省事。参数上唯一要注意的是W、H必须用变换后的画布尺寸裁剪和旋转补边都会改变画布大小用原图尺寸归一化就会整体偏移。2.2 旋转绕图心转标签跟着转旋转是最容易翻车的一种。常见做法是绕图片中心旋转任意角度旋转后画布要扩到能装下整张图否则四角被切掉标签也跟着丢。import cv2 import numpy as np def rotate_img_and_boxes(img, boxes, angle): H, W img.shape[:2] cx, cy W / 2, H / 2 M cv2.getRotationMatrix2D((cx, cy), angle, 1.0) # 计算旋转后画布尺寸 cos, sin abs(M[0, 0]), abs(M[0, 1]) nW int(H * sin W * cos) nH int(H * cos W * sin) M[0, 2] nW / 2 - cx M[1, 2] nH / 2 - cy rotated cv2.warpAffine(img, M, (nW, nH), borderValue(114, 114, 114)) new_boxes [] for (x1, y1, x2, y2, cls) in boxes: pts np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtypenp.float32) pts np.hstack([pts, np.ones((4, 1))]) pts pts M.T # 四个角点一起变换 nx1, ny1 pts[:, 0].min(), pts[:, 1].min() nx2, ny2 pts[:, 0].max(), pts[:, 1].max() new_boxes.append((nx1, ny1, nx2, ny2, cls)) return rotated, new_boxes, nW, nH逻辑说明cv2.getRotationMatrix2D给的是 2x3 仿射矩阵直接乘角点即可。参数上angle建议取[-15, 15]的小角度或者[90, 180, 270]的直角小角度贴近真实拍摄抖动直角适合航拍、俯视图这类方向不敏感的场景。borderValue用 114 灰是 YOLO 官方 letterbox 的填充色比纯黑更不容易让模型把黑边当目标。旋转后框会变大外接矩形这是正常的不要试图“缩回去”。2.3 平移与翻转最便宜也最稳的两种平移就是把框和像素一起挪翻转分水平、垂直。这两种不改变框的形状只改位置标签处理最简单也最不容易出错建议作为增强的主力。def translate(img, boxes, tx, ty): 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 [(x1 tx, y1 ty, x2 tx, y2 ty, c) for x1, y1, x2, y2, c in boxes] return out, new_boxes def flip_h(img, boxes): W img.shape[1] out cv2.flip(img, 1) new_boxes [(W - x2, y1, W - x1, y2, c) for x1, y1, x2, y2, c in boxes] return out, new_boxes逻辑说明平移的tx、ty建议控制在图像宽高的 ±10% 以内太大就会把目标推出画布。水平翻转的标签变换是x1 W - x2、x2 W - x1y不变垂直翻转同理换y。参数上翻转概率可以给到 0.5平移给 0.3 左右。注意如果数据集里有文字、方向性强的目标比如交通标志上的箭头水平翻转要慎用翻完语义就反了。2.4 裁剪越界框必须做取舍裁剪随机裁剪一块区域再缩放回原尺寸能显著提升模型对小目标和遮挡的鲁棒性但它是六种里标签处理最麻烦的目标可能被裁掉一半也可能完全出界。def random_crop(img, boxes, scale(0.6, 1.0)): H, W img.shape[:2] s np.random.uniform(*scale) cw, ch int(W * s), int(H * s) x0 np.random.randint(0, W - cw 1) y0 np.random.randint(0, H - ch 1) crop img[y0:y0 ch, x0:x0 cw] new_boxes [] for (x1, y1, x2, y2, c) in boxes: nx1, ny1 max(x1 - x0, 0), max(y1 - y0, 0) nx2, ny2 min(x2 - x0, cw), min(y2 - y0, ch) if nx2 - nx1 4 or ny2 - ny1 4: # 剩余面积太小直接丢 continue new_boxes.append((nx1, ny1, nx2, ny2, c)) return crop, new_boxes, cw, ch逻辑说明scale控制裁剪区域占原图的比例(0.6, 1.0)表示最多裁掉 40%。关键在越界处理先和裁剪框求交集交集宽或高小于 4 像素就丢弃这个框否则保留。参数上这个 4 像素阈值可以按数据集调整小目标多的场景要调小但太小会留下大量“半个框”反而害模型。裁剪后记得把标签按新画布cw、ch重新归一化。2.5 亮度与噪声只动像素不动标签这两种是纯像素级增强标签完全不用改实现成本最低但对泛化帮助很实在。def change_brightness(img, delta30): hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[..., 2] np.clip(hsv[..., 2] delta, 0, 255) return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) def add_noise(img, sigma10): noise np.random.normal(0, sigma, img.shape).astype(np.float32) return np.clip(img.astype(np.float32) noise, 0, 255).astype(np.uint8)逻辑说明亮度调整走 HSV 的 V 通道比直接对 BGR 加常数更符合人眼感知delta取[-40, 40]比较自然。噪声用高斯噪声sigma取 5~15太大图像会糊成一片模型学不到东西。这两种增强可以叠加使用但亮度变化幅度和噪声强度要一起控制别把图搞成“又暗又花”。2.6 六种方式怎么组合一份可抄的增强策略单种增强效果有限实际训练里都是随机组合。下面这份策略是我常用的默认配置可以直接套增强方式触发概率关键参数标签是否变化水平翻转0.5无是平移0.3±10% 宽高是旋转0.3±15° 或 90° 倍数是随机裁剪0.3scale 0.6~1.0是亮度调整0.5delta ±40否高斯噪声0.3sigma 5~15否组合原则几何增强前四种每次最多叠加两种像素增强后两种可以自由叠加。所有几何增强做完后统一做一次越界框过滤和归一化再写回 .txt。这样一套下来一个 1000 张图的数据集能扩到 5000 张以上且标签始终对得上。3. 把增强脚本接进 YOLO 训练流程3.1 离线增强生成新图新标签最直观离线增强就是把增强后的图片和标签实实在在写到磁盘训练时当普通数据用。优点是可视化检查方便缺点是占空间。import os from pathlib import Path def offline_augment(img_dir, lbl_dir, out_img, out_lbl, repeat3): os.makedirs(out_img, exist_okTrue) os.makedirs(out_lbl, exist_okTrue) for img_path in Path(img_dir).glob(*.jpg): lbl_path Path(lbl_dir) / (img_path.stem .txt) if not lbl_path.exists(): continue img cv2.imread(str(img_path)) H, W img.shape[:2] boxes [] for line in lbl_path.read_text().strip().splitlines(): c, cx, cy, w, h line.split() x1, y1, x2, y2 xywhn2xyxy(float(cx), float(cy), float(w), float(h), W, H) boxes.append((x1, y1, x2, y2, int(c))) for i in range(repeat): aug_img, aug_boxes, nW, nH random_crop(img, boxes) aug_img change_brightness(aug_img, np.random.randint(-40, 40)) name f{img_path.stem}_aug{i} cv2.imwrite(f{out_img}/{name}.jpg, aug_img) with open(f{out_lbl}/{name}.txt, w) as f: for x1, y1, x2, y2, c in aug_boxes: cx, cy, w, h xyxy2xywhn(x1, y1, x2, y2, nW, nH) f.write(f{c} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)逻辑说明repeat控制每张原图生成几张增强图一般 2~4 张就够太多会导致增强样本主导训练。写标签时保留 6 位小数避免精度损失。注意增强后的文件名要唯一否则会互相覆盖。3.2 在线增强训练时实时做省空间如果不想占磁盘可以在 Dataset 的__getitem__里实时增强。YOLO 官方训练流程本身带 mosaic、mixup 等增强但如果你要加自定义的旋转、裁剪就得自己改 dataloader。class YoloAugDataset(Dataset): def __init__(self, img_dir, lbl_dir, imgsz640, augmentTrue): self.img_dir, self.lbl_dir img_dir, lbl_dir self.imgsz, self.augment imgsz, augment self.files [p for p in Path(img_dir).glob(*.jpg)] def __getitem__(self, idx): img_path self.files[idx] img cv2.imread(str(img_path)) H, W img.shape[:2] boxes load_yolo_labels(self.lbl_dir, img_path.stem, W, H) if self.augment and np.random.rand() 0.5: img, boxes, W, H random_crop(img, boxes) img cv2.resize(img, (self.imgsz, self.imgsz)) # 后续 letterbox、归一化、转 tensor 按官方流程走 return img, boxes逻辑说明在线增强的随机性每轮都不同相当于无限数据但调试困难标签错了不容易发现。参数上augment开关要留好验证集必须关掉增强否则 mAP 没法比。imgsz要和训练配置一致缩放后标签也要同步缩放。3.3 增强后必须做的两步校验不管离线还是在线增强完一定要校验否则训练半天才发现标签全错。第一步可视化抽查。随机抽 20 张增强图把标签框画上去肉眼看框是否还套在目标上。这一步能抓出 90% 的坐标变换错误。def draw_check(img, boxes): for x1, y1, x2, y2, c in boxes: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.imwrite(check.jpg, img)第二步统计越界框比例。增强后如果超过 10% 的框越界或面积过小说明参数太激进要收紧。def check_invalid(boxes, W, H): bad 0 for x1, y1, x2, y2, _ in boxes: if x1 0 or y1 0 or x2 W or y2 H or (x2 - x1) 4 or (y2 - y1) 4: bad 1 return bad / max(len(boxes), 1)4. 增强踩坑排查这 5 个错误我几乎每次都遇到4.1 现象训练 loss 正常但 mAP 极低框全飘原因标签归一化用错了画布尺寸。旋转或裁剪后画布变了代码里还在用原图W、H归一化导致所有框整体偏移。解决把nW、nH作为参数一路传到写标签的函数别用全局的原图尺寸。写完抽一张图把框画出来一眼就能看出来。4.2 现象增强后某些类别样本突然变少原因裁剪时把大量小目标框裁没了而小目标恰好是某个稀有类。if nx2 - nx1 4这个阈值对小目标太狠。解决按类别统计增强前后的框数量稀有类单独降低裁剪概率或者把面积阈值改成相对值比如小于原框面积的 30% 才丢。4.3 现象旋转 90 度后框变成竖长条明显不对原因旋转矩阵只作用了中心点没作用宽高或者角点变换后没重新求外接矩形。解决必须对四个角点做矩阵乘法再取 min/max 得到新框。只转中心点是典型错误框的方向和大小都会错。4.4 现象亮度增强后图片出现色块断层原因直接在 BGR 上加减常数并 clip容易在暗部或亮部产生硬截断。解决走 HSV 的 V 通道或者用 gamma 变换代替线性加减。delta别超过 ±40超了就会出现明显断层。4.5 现象在线增强训练速度慢一半原因每轮都在 CPU 上做旋转、裁剪dataloader 成了瓶颈。解决几何增强用cv2而不是 PILcv2快很多或者把增强放到 GPU 上做再不行就退回离线增强用空间换时间。5. 让增强真正涨点的两个进阶技巧第一个技巧是按类别和尺度分层增强。别对所有图用同一套概率。小目标多的图裁剪概率调低、缩放类增强调高大目标多的图裁剪和旋转可以放开。实现上就是在 Dataset 里先统计每张图的框数量和平均面积再动态选增强组合。我一般会维护一个class_scale_stats字典训练前跑一遍数据集生成增强时按图查表。这样比“一刀切”策略通常能多涨 1~2 个点 mAP尤其是类别不均衡的数据集。第二个技巧是增强强度随训练轮次衰减。训练前期模型欠拟合强增强能提升泛化训练后期模型已经收敛强增强反而让 loss 震荡。可以在前 70% 轮次用完整增强后 30% 把旋转角度、裁剪比例、噪声强度都减半。实现上给增强函数传一个epoch_ratio参数内部按比例缩放各强度即可。def get_aug_strength(epoch, total_epoch): r epoch / total_epoch if r 0.7: return 1.0 return max(0.3, 1.0 - (r - 0.7) / 0.3 * 0.7)验证这套有没有用别只看最终 mAP要对比“增强前 vs 增强后”在验证集上的表现而且验证集绝对不能做增强。我习惯固定一个 200 张的干净验证集每轮都跑曲线一对比就知道增强是帮忙还是添乱。血泪经验是增强不是越多越好标签同步错了增强越猛掉得越狠。先把一种增强的标签变换写对、可视化确认再往上叠组合这个顺序别省。希望帮到你。本文还有配套的精品资源点击获取