简介:YOLO数据增强工具包面向目标检测开发者和算法学习者,专门解决已标注数据集规模不足、模型易过拟合的问题。工具针对.txt格式的YOLO标注文件实现同步增强,覆盖旋转、平移、翻转、裁剪、调整亮度与增加噪声6种方式,生成图片与对应标签自动保持同名关联,无需手动调整;内置随机参数控制,可灵活设置每种增强的强度、概率与边界约束,批量扩充训练样本。压缩包仅14KB,共6个文件,以Python源码为主,包含增强处理引擎、TXT与XML格式转换工具、说明文档及附赠内容,脚本结构清晰,可直接运行或二次修改,适合集成进已有数据处理流程。目前已有145人学习下载,尤其适合刚接触YOLO数据准备工作的初级开发者参考使用。资源为网络分享,仅限学习交流,使用时请注意数据来源与合法合规。
1. 给已标注的 YOLO 数据集做增强:这活儿看着简单,坑比想象多
训练 YOLO 时最常遇到的尴尬不是模型不收敛,而是标注数据不够。自己框了几千张图,训练出来 mAP 就是上不去,加数据又得重新标注,人先疯了。YOLO 数据增强的常规做法是直接对图片和 .txt 标注文件做同步变换,旋转、平移、翻转、裁剪、调亮度、加噪声这六种操作,能在不碰标注工具的前提下把数据集扩出好几倍。但这个方向的坑在于:不是所有增强方式都能无脑套在检测任务上,尤其是 .txt 里存的归一化坐标,旋转 90 度后宽高互换、裁剪后目标出界、亮度增强后标签完全不用动,这些细节半点不能马虎。这篇就把六种增强的完整思路、坐标变换原理和落地代码讲清楚,适合正在自己扩数据集、不想依赖在线增强的读者。
2. 先搞懂 .txt 标注的数学:归一化坐标和图像变换的关系
2.1 YOLO 标签格式里容易忽略的两个前提
YOLO 的 .txt 标注每一行是class center_x center_y width height,前四个值全部是相对于图片宽高的归一化比例。意味着只要图片尺寸不变,这四个值在绝大多数增强操作里都不会变。但一旦涉及旋转、裁剪这类改变像素布局的操作,坐标必须跟着图像矩阵同步算。很多人在这一步翻车,是因为直接把图像旋转了,标签却原封不动写回去,训练时目标框全错位。
另一个前提是坐标原点和方向。OpenCV 的坐标系原点在左上角,x 轴向右,y 轴向下;而数学里常见的旋转公式是基于原点在左下角、y 轴向上的笛卡尔坐标系。做图像旋转时如果直接套x' = x*cos - y*sin这套公式,y 方向会反,框的位置全部镜像错乱。正确的做法是先按图像中心做平移,再在图像坐标系下套 OpenCV 版本的旋转公式,或者干脆用 OpenCV 的cv2.getRotationMatrix2D拿到变换矩阵,再用矩阵去乘坐标点,这样最稳。
2.2 变换矩阵是统一所有增强的钥匙
旋转、平移、翻转、裁剪这四种几何操作,本质上都可以用一个 2x3 的仿射变换矩阵表达。对图片调用cv2.warpAffine时传入这个矩阵,对坐标点做变换时也同样乘这个矩阵,就能保证图和标注永远同步。增强库常见的做法是先把目标框的四个角点取出来,用矩阵变换角点,再重新计算变换后角点的最小外接矩形作为新框。
用角点而不是直接变换中心点加宽高,是因为旋转 90 度这类操作会让框的宽高方向对调,直接变换中心点必须额外判断宽高互换,容易漏。角点法最通用:无论怎么旋转、裁剪,四个角点变换后取min_x, min_y, max_x, max_y,就能得到新的轴对齐框。
2.3 归一化坐标和像素坐标的来回转换
写增强脚本时,我会统一先把 .txt 里的归一化坐标转成像素坐标,做完几何变换,再归一化写回去。原因是 OpenCV 的 warpAffine 操作的是像素矩阵,坐标点计算用像素值更直观,避免归一化值乘来乘去丢掉精度。转换公式很简单:
x_pixel = x_norm * img_width y_pixel = y_norm * img_height w_pixel = w_norm * img_width h_pixel = h_norm * img_height对应地写回时再除以新图片的宽高。注意旋转后图片尺寸可能变化,比如正方形图旋转 45 度后,OpenCV 默认会裁掉超出边界的部分;若要保留全部内容,需要扩大画布,此时新图的宽高就不是原图宽高了,归一化分母要换成新宽高。
3. 六种增强的完整实现:从单张图片到批量扩数据集
3.1 旋转:角度选择和边界填充是两大选择点
旋转是六种增强里最容易引入无效黑边的操作。实现时先读原图和 .txt,构造旋转矩阵,然后对图和标注框的角点做变换。角度我一般从[90, 180, 270]里随机选,或者在小角度范围[-15, 15]内随机,前者能保持图片尺寸不变(宽高互换后重新缩放),后者需要填充。两者各有用处,大角度适合让模型学到目标的朝向不变性,小角度适合矫正轻微倾斜。
import cv2 import numpy as np import random def rotate_image_and_labels(img, labels, angle): h, w = img.shape[:2] center = (w / 2, h / 2) M = cv2.getRotationMatrix2D(center, angle, 1.0) # 计算旋转后画布尺寸,避免内容被裁 cos = abs(M[0, 0]) sin = abs(M[0, 1]) new_w = int(h * sin + w * cos) new_h = int(h * cos + w * sin) M[0, 2] += new_w / 2 - center[0] M[1, 2] += new_h / 2 - center[1] rotated_img = cv2.warpAffine(img, M, (new_w, new_h), borderValue=(114, 114, 114)) new_labels = [] for cls, cx, cy, bw, bh in labels: # 归一化转像素 x1 = (cx - bw / 2) * w y1 = (cy - bh / 2) * h x2 = (cx + bw / 2) * w y2 = (cy + bh / 2) * h corners = np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtype=np.float32) # 角点变换 ones = np.ones(shape=(len(corners), 1)) points_ones = np.hstack([corners, ones]) transformed = M.dot(points_ones.T).T min_x = np.min(transformed[:, 0]) max_x = np.max(transformed[:, 0]) min_y = np.min(transformed[:, 1]) max_y = np.max(transformed[:, 1]) # 过滤完全出界的框 if max_x <= 0 or max_y <= 0 or min_x >= new_w or min_y >= new_h: continue # 裁剪到边界内 min_x = max(0, min_x) min_y = max(0, min_y) max_x = min(new_w, max_x) max_y = min(new_h, max_y) new_cx = (min_x + max_x) / 2 / new_w new_cy = (min_y + max_y) / 2 / new_h new_bw = (max_x - min_x) / new_w new_bh = (max_y - min_y) / new_h new_labels.append([cls, new_cx, new_cy, new_bw, new_bh]) return rotated_img, new_labels这段代码的核心是先用getRotationMatrix2D得到旋转矩阵,然后调整平移量让旋转后的图完整落在画布内,最后对每个标注框的四个角点做矩阵乘法。注意new_w和new_h的计算是旋转后外接矩形的尺寸,必须用这个尺寸作为 warpAffine 的输出尺寸,否则旋转超过 90 度时边缘会被裁掉。borderValue设为 114 是 YOLO 训练时常用的填充值,和 mosaic 增强的填充一致,避免黑边对归一化产生不利影响。
3.2 平移:最容易写对,但要防止目标大面积出界
平移就是对整个图像做偏移,坐标变换只需要给中心点加偏移量。难点在于平移比例选太大,目标框会大量移出画面,有效样本反而变少。我一般把平移比例限制在图片宽高的 10% 到 20% 之间,超过这个范围,小目标很容易完全出界被过滤掉,数据集的有效信息密度下降。
平移的矩阵比旋转简单:
def translate_image_and_labels(img, labels, dx_ratio, dy_ratio): h, w = img.shape[:2] dx = dx_ratio * w dy = dy_ratio * h M = np.float32([[1, 0, dx], [0, 1, dy]]) shifted_img = cv2.warpAffine(img, M, (w, h), borderValue=(114, 114, 114)) new_labels = [] for cls, cx, cy, bw, bh in labels: x1 = (cx - bw / 2) * w + dx y1 = (cy - bh / 2) * h + dy x2 = (cx + bw / 2) * w + dx y2 = (cy + bh / 2) * h + dy if x2 <= 0 or y2 <= 0 or x1 >= w or y1 >= h: continue x1 = max(0, x1); y1 = max(0, y1) x2 = min(w, x2); y2 = min(h, y2) new_labels.append([cls, (x1 + x2) / 2 / w, (y1 + y2) / 2 / h, (x2 - x1) / w, (y2 - y1) / h]) return shifted_img, new_labelsdx_ratio和dy_ratio是相对宽高的比例,比如0.1表示向右平移 10% 图片宽度。平移后图片尺寸不变,所以归一化分母仍是原宽高。这里有个取舍:平移产生的填充区域全是 114 的灰边,如果目标本身靠近边缘,平移后灰边占比会很大,等于引入了大量无信息区域。遇到这种情况,可以把 borderValue 改成用原图边缘像素填充,或者直接用cv2.copyMakeBorder配合裁剪模拟平移,效果更自然,但代码量会多不少。
3.3 翻转:水平和垂直翻转必须同步镜像标注
翻转是所有增强里风险最低、收益最高的操作。水平翻转后,目标的中心点 x 坐标变成1 - cx,宽高不变;垂直翻转则 y 坐标变成1 - cy。但要注意:翻转后图片的宽高顺序没变,所以 .txt 里的宽高值不需要交换。很多人做旋转 90 度时把翻转的公式也用进去,导致宽高搞反。
def flip_image_and_labels(img, labels, flip_code): # flip_code: 1 水平翻转, 0 垂直翻转, -1 水平+垂直 flipped_img = cv2.flip(img, flip_code) h, w = img.shape[:2] new_labels = [] for cls, cx, cy, bw, bh in labels: if flip_code == 1: new_cx = 1 - cx new_cy = cy elif flip_code == 0: new_cx = cx new_cy = 1 - cy else: new_cx = 1 - cx new_cy = 1 - cy new_labels.append([cls, new_cx, new_cy, bw, bh]) return flipped_img, new_labels这段逻辑虽然简单,但有一个盲点:某些特殊目标(比如车牌、文字)水平翻转后语义会变,模型可能学到错误特征。如果数据集里包含这类目标,建议只做垂直翻转,或者干脆不翻转。另外,YOLO 训练时的flipud和fliplr是在线增强,和这里做的离线增强叠加时要注意重复度,否则模型会见过太多翻转样本,反而对正常方向的样本泛化变差。
3.4 裁剪:边界框截断、过滤和坐标重新计算
裁剪增强不是简单地把图片切一块,而是要保证裁剪区域内有足够的目标。做法是随机选一个裁剪区域,然后计算哪些框落在区域内,再把这些框的坐标换算到新图里。最关键的参数是裁剪区域和目标框的 IoU 阈值:低于阈值的框应该被丢弃,而不是硬拽回来。
def crop_image_and_labels(img, labels, crop_ratio=0.8, iou_threshold=0.3): h, w = img.shape[:2] crop_w = int(w * random.uniform(crop_ratio, 1.0)) crop_h = int(h * random.uniform(crop_ratio, 1.0)) x_start = random.randint(0, w - crop_w) y_start = random.randint(0, h - crop_h) cropped_img = img[y_start:y_start + crop_h, x_start:x_start + crop_w] def compute_iou(box1, box2): x1 = max(box1[0], box2[0]); y1 = max(box1[1], box2[1]) x2 = min(box1[2], box2[2]); y2 = min(box1[3], box2[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]) return inter / (area1 + area2 - inter + 1e-6) new_labels = [] for cls, cx, cy, bw, bh in labels: x1 = (cx - bw / 2) * w y1 = (cy - bh / 2) * h x2 = (cx + bw / 2) * w y2 = (cy + bh / 2) * h crop_box = [x_start, y_start, x_start + crop_w, y_start + crop_h] if compute_iou([x1, y1, x2, y2], crop_box) < iou_threshold: continue # 裁剪框坐标换算到新图 nx1 = max(0, x1 - x_start) ny1 = max(0, y1 - y_start) nx2 = min(crop_w, x2 - x_start) ny2 = min(crop_h, y2 - y_start) if nx2 <= nx1 or ny2 <= ny1: continue new_labels.append([ cls, (nx1 + nx2) / 2 / crop_w, (ny1 + ny2) / 2 / crop_h, (nx2 - nx1) / crop_w, (ny2 - ny1) / crop_h ]) return cropped_img, new_labels这里iou_threshold设 0.3 是比较保守的值,目标被切掉一半以上就丢。如果想要保留更多样本,可以降到 0.2,但代价是标签框可能只框住目标的一小部分,引入噪声。另一个参数是crop_ratio,0.8 表示裁剪区域至少占原图的 80%,这样目标不会因为被切得太碎而失去语义。裁剪增强特别适合大尺寸原图,因为 YOLO 训练时通常会把图缩放到 640x640,直接裁剪相当于隐式地做了尺度增强。
3.5 亮度调整:改像素不改标注,唯一零风险操作
亮度增强是六种里唯一不需要动坐标的,直接对整张图做 HSV 空间的 V 通道缩放就行。常见做法是把 RGB 转 HSV,V 通道乘一个随机系数,再转回 RGB。这个操作的风险在于系数选太大,图像过暗或过亮导致目标纹理看不清,模型学到的是“亮度极端的样本”,而不是真正的光照不变性。
def adjust_brightness(img, labels, factor=None): if factor is None: factor = random.uniform(0.5, 1.5) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[:, :, 2] = np.clip(hsv[:, :, 2] * factor, 0, 255) adjusted_img = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) return adjusted_img, labelsfactor在 0.5 到 1.5 之间浮动,低于 0.5 会出现大范围死黑,高于 1.5 会过曝。注意这张代码里labels原样返回,因为亮度操作不改变像素位置。实际工程里,我会把亮度增强和色彩抖动合并,在 HSV 空间同时调整饱和度 S 和色相 H,这样一张图能变出多种光照风格,且完全不碰标注。
3.6 噪声增强:高斯噪声和椒盐噪声的强度边界
加噪声的目的是让模型对传感器噪点更鲁棒,但 YOLO 这类基于卷积的模型对高频噪声本身就有一抵御能力,加太强反而会让小目标特征被淹没。我用得最多的是高斯噪声,均值 0,方差 10 到 25,超过 25 之后 mAP 会明显下降。椒盐噪声则适合模拟摄像头坏点,比例控制在 0.5% 以内。
def add_gaussian_noise(img, labels, std=15): noise = np.random.normal(0, std, img.shape).astype(np.float32) noisy_img = np.clip(img.astype(np.float32) + noise, 0, 255).astype(np.uint8) return noisy_img, labels def add_salt_pepper_noise(img, labels, prob=0.003): noisy_img = img.copy() num_salt = int(prob * img.size) coords = [np.random.randint(0, i - 1, num_salt) for i in img.shape[:2]] noisy_img[coords[0], coords[1]] = 255 coords = [np.random.randint(0, i - 1, num_salt) for i in img.shape[:2]] noisy_img[coords[0], coords[1]] = 0 return noisy_img, labels高斯噪声的std参数决定了强度,对 0-255 的像素值来说 15 已经是肉眼可见的颗粒感了。如果训练时 YOLO 的输入尺寸是 640,可以适当把std降到 10,因为下采样本身会平滑掉一部分噪声,实际注入模型的噪声强度比原图看起来低。椒盐噪声的prob是像素级概率,0.003 意味着一百万像素里约三千个噪点,已经比较明显了。
4. 整合六种增强的批量脚本与输出策略
4.1 随机组合增强时要注意的顺序问题
六种增强不是随便排列的。几何变换(旋转、平移、翻转、裁剪)会影响坐标,而光度变换(亮度、噪声)不影响坐标。我的顺序是这样:先做裁剪(因为它会改变图像尺寸和内容范围),再做旋转和平移,然后做翻转,最后做亮度和噪声。如果先加噪声再裁剪,裁剪区域边缘的噪声分布会不自然;如果先旋转再裁剪,旋转产生的填充区域会被裁剪掉一部分,等于白填充。
实际落地时,我不会每次都把六种全用上,而是每次随机选 1 到 3 种组合。全部叠加会让图片畸变严重,比如旋转 15 度、再裁剪 80%、再调暗 0.5 倍,目标可能只剩半个且看不清。每个样本的组合方式要保证“人类还能认出目标”,这是增强的底线。
4.2 批处理脚本:读目录、增强、写回同名 .txt
把上面的函数拼接成一个批处理脚本,输入一个图片和标注文件夹,输出增强后的新图片新标注。命名策略我用原始文件名加后缀,例如img1.jpg增强后叫img1_rot90.jpg、img1_brt.jpg,对应标注文件img1_rot90.txt。这样一眼能看出每个样本做了哪种增强,排除问题时方便。
import os import random def load_labels(txt_path): labels = [] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) == 5: labels.append([int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])]) return labels def save_labels(txt_path, labels): with open(txt_path, 'w') as f: for label in labels: f.write(f"{int(label[0])} {label[1]:.6f} {label[2]:.6f} {label[3]:.6f} {label[4]:.6f}\n") def augment_one_image(img_path, txt_path, out_img_path, out_txt_path, aug_options): img = cv2.imread(img_path) labels = load_labels(txt_path) # 每种操作按概率独立触发 if 'rotate' in aug_options and random.random() < 0.6: angle = random.choice([90, 180, 270]) if random.random() < 0.5 else random.uniform(-15, 15) img, labels = rotate_image_and_labels(img, labels, angle) if 'translate' in aug_options and random.random() < 0.4: img, labels = translate_image_and_labels(img, labels, random.uniform(-0.15, 0.15), random.uniform(-0.15, 0.15)) if 'flip' in aug_options and random.random() < 0.5: img, labels = flip_image_and_labels(img, labels, random.choice([1, 0])) if 'crop' in aug_options and random.random() < 0.5: img, labels = crop_image_and_labels(img, labels, crop_ratio=random.uniform(0.7, 0.9)) if 'brightness' in aug_options and random.random() < 0.6: img, labels = adjust_brightness(img, labels) if 'noise' in aug_options and random.random() < 0.3: img, labels = add_gaussian_noise(img, labels, std=random.randint(10, 20)) cv2.imwrite(out_img_path, img) save_labels(out_txt_path, labels)每个操作前的random.random()是独立触发概率,保证同一张图多次增强结果不同。crop_ratio的范围和translate的比例需要根据目标大小调整:如果你的数据集里小目标多,裁剪比例就不能低于 0.8,平移比例小于 0.1,否则大部分小目标都会被过滤。这种概率式组合的好处是生成的数据分布更接近自然变化,而不是每张图都被强推一遍所有增强。
4.3 扩增倍数的选择和训练集、验证集划分的雷区
增强后的数据不能一股脑全塞进训练集。验证集和测试集必须保持原图,只能从原始数据里划分,否则你用增强后的图做验证,模型看到的验证样本和训练样本高度相似,指标会虚高,部署时立刻现原形。我的做法是先把原始数据集按比例分成 train 和 val,只对 train 部分做增强,val 保持纯净。
扩增倍数方面,我一般控制在原图数量的 3 到 5 倍。超过 5 倍后,模型反复见到同一张图的不同变换,边际收益递减,而且训练时间成倍增加。以一个 2000 张的原始训练集为例,每张随机做 3 次增强,得到 6000 张增强图,加上原始共 8000 张,这个量级对 YOLOv8 的小模型来说已经足够跑出明显效果。如果原始数据特别少(比如只有几百张),可以放大到 8 倍,但需要用早停和验证集指标密切监控过拟合。
5. YOLO 数据增强避坑指南:五条容易让人崩溃的踩坑记录
5.1 旋转 90 度后目标框变成竖条,损失异常飙升
现象:对图片做了 90 度旋转增强,训练时 loss 震荡剧烈,验证集 mAP 不升反降。
原因:旋转 90 度后,原图的宽(w)变成了新图的高,原图的高(h)变成了新图的宽。如果只用四个角点计算 min/max 得到新框,这个新框是轴对齐的,但旋转后的目标本身也是轴对齐的,理论上没问题。问题出在旋转 90 度时我用的是小角度旋转逻辑,没有额外处理宽高互换,导致标注框的宽高比例和实际目标完全不符,目标框里包含了大量背景。
解决:对 90、180、270 度这类直角旋转,不要走通用矩阵旋转流程。直接先np.rot90(img, k=1)旋转图片,然后按规律交换坐标分量:90 度时new_cx = 1 - cy、new_cy = cx,并且宽高互换;180 度时new_cx = 1 - cx、new_cy = 1 - cy,宽高不变;270 度时new_cx = cy、new_cy = 1 - cx,宽高互换。这样最不容易出错。
5.2 裁剪增强后某些标注框宽高变成负数,代码直接崩
现象:跑批处理脚本时,某几张图报ValueError: max() arg is an empty sequence,检查发现裁剪后的标注列表为空,或者新框坐标算出负值。
原因:裁剪区域恰好避开了所有目标框,导致new_labels为空;或者目标框只有一小部分落在裁剪区域内,nx2 - nx1计算出负数。很多新手只过滤了完全出界的框,却没有过滤被裁剪截断后剩余区域为零的情况。
解决:在计算nx1, ny1, nx2, ny2之后,增加一条if nx2 <= nx1 or ny2 <= ny1: continue的判断。同时,如果一张图裁剪后没有任何有效标注,那么这张增强图应该直接丢弃,不要硬写空的 .txt 进数据集,否则 YOLO 训练时会当成背景样本,干扰正负样本平衡。
5.3 亮度增强后目标框位置没变,但训练结果反而变差
现象:加了亮度增强,训练集 loss 正常下降,但验证集的 mAP 比不加增强时还低两个点。
原因:亮度因子范围太激进,比如设了 0.2 到 2.0,大量样本过暗或过曝,目标本身的纹理和边缘信息丢失,模型被迫学习“黑暗中找物体”这种不存在的规律。另一个原因是亮度和原有的色彩增强叠加,导致颜色分布偏移过大。
解决:把亮度因子范围收窄到 0.75 到 1.25,并且只对一部分样本(比如 50%)做亮度增强。如果数据集本身是室内均匀光照,0.75 到 1.25 足够模拟灯光明暗变化;如果是户外场景,可以放宽到 0.5 到 1.5,但要先做一轮小规模实验对比 mAP。
5.4 增强后的图片和标注文件名对不上,训练时报 no labels 错
现象:YOLO 训练时报Image ... has no labels,排查发现增强脚本里图片写入成功,但 .txt 文件名或路径没配对。
原因:我一开始把增强后的图片放在aug_images/,标注放在aug_labels/,但 YOLO 训练要求同一张图和它的 .txt 文件名完全相同,且默认从图片路径推导标注路径。如果两个文件夹组织方式不同,或者后缀名处理不一致(比如图片是.jpg,标注是.txt且文件名没去掉扩展名),就会找不到标签。
解决:严格保持同名文件一一对应,并在脚本里用同一个基础文件名生成图片和标注。比如base_name = file.stem,图片写f"{base_name}_aug.jpg",标注写f"{base_name}_aug.txt"。写完批量脚本后,先抽三张图手动检查标注框是否正确覆盖目标,再开训练。
5.5 增强后数据集里出现大量 114 灰边,模型学到奇怪的边框特征
现象:训练收敛后,把模型跑在真实场景上,对靠近图像边缘的目标检测不准,而对训练集里常见的灰边位置有响应。
原因:旋转和平移都用了 114 灰边填充,导致增强图里大面积是纯色。模型可能隐式学到了“目标周围有灰边时大概率是增强样本”这种伪特征,而不是真正学会目标本身。
解决:填充值不要用固定 114,改用cv2.replicate边缘复制模式,或者把填充区域挖掉(改成随机裁剪)。另外,增强后的图片在训练前会再次被 YOLO 的 resize 和 mosaic 处理,灰边会被进一步扭曲。最稳妥的方式是旋转时把角度限制在 90/180/270,这样没有灰色填充;平移时用边缘复制填充,避免纯色块。
6. 验证增强效果的最小实验:一张大图和三次对照训练
不要一口气把六种增强全部打开训一版就下结论。我验证增强方案是否有效的标准做法是跑三组对照:原图训练作为基线,只加几何增强(旋转、平移、翻转、裁剪),以及几何加光度增强(亮度、噪声)的完整方案。每组用相同的训练轮数、batch size 和模型结构,只改数据集。
先做一个快速冒烟实验:从原始训练集里随机取 200 张图,每张生成 5 个增强版本,得到 1000 张图。用这个小子集训练 30 个 epoch,观察验证集 mAP。如果增强集的 mAP 比原图集低,说明增强参数过于激进;如果高,说明方向可行,再扩展到全量数据。
验证时还要关注类别层面的 AP,而不是只看整体 mAP。有些增强方式对特定类别有害,比如旋转对“人”这种垂直朝向目标影响不大,但对“车”这种水平目标,旋转 90 度后宽高彻底颠倒,如果训练集里车的长宽比很统一,模型会被混淆。打印出每个类的 AP 对比,能帮你快速定位是哪类目标在哪个增强组合下变差了。
最后,把增强后模型在真实场景的测试集上跑一遍,看看误检是否增多。很多时候 mAP 微涨,但误检大量增加,比如把背景里的树影当成人。这种情况说明增强样本让模型对纹理的依赖变弱了,需要降低旋转角度范围或减少裁剪比例。我在实际项目里遇到过不止一次“mAP 涨了 2 个点但现场误报翻倍”的情况,所以最终判断标准永远是部署场景的实测,不是训练指标。
把增强脚本固化下来之后,你会发现自己调数据集的时间从几天压缩到几分钟。这个方案的长期价值不只是扩数据,更是让模型对拍摄角度、光照和成像噪声都更钝感。希望这篇能帮你把增强流程跑通,省下重复标注的力气。
本文还有配套的精品资源,点击获取