3. 数据增强与预处理:Mosaic增强、MixUp增强、HSV色域变换、归一化与Resize、Albumentations库实战
数据增强,说白了就是给模型“喂”更多样化的数据。你想想看,如果训练集里只有晴天拍的汽车,那模型遇到雨天、雾天或者被遮挡的车,大概率就懵了。
我个人习惯把数据增强分成两类:几何变换和色彩变换。YOLO系列之所以在工业界这么能打,除了网络结构本身,数据增强策略功不可没。尤其是Mosaic和MixUp,简直是YOLOv4之后的“灵魂组件”。
3.1 Mosaic增强:一张图里看四个世界
Mosaic是YOLOv4提出来的,后来成了YOLO系列的标配。它的做法很简单:把4张训练图片随机裁剪、缩放、拼接成一张新图。
为什么这么做?
- 丰富小目标样本: 4张图拼在一起,每张图里的目标相对变小了。模型被迫去学习检测小目标。
- 提升背景多样性: 一张图里同时出现马路、草地、室内、天空,模型不会“死记硬背”某个固定背景。
- 平衡类别分布: 如果某个类别样本少,Mosaic能把它和其他类别混在一起,变相增加出现频率。
我在项目中遇到过一个问题:Mosaic拼接时,如果4张图的亮度差异太大,拼出来的图会有一块亮一块暗。后来我加了一个自适应亮度均衡的预处理,效果好了很多。
# Mosaic增强核心逻辑(简化版)
def mosaic_augmentation(images, labels, img_size=640):
# 随机选择中心点,决定4张图的拼接位置
cx, cy = random.randint(img_size//4, 3*img_size//4), random.randint(img_size//4, 3*img_size//4)
mosaic_img = np.zeros((img_size, img_size, 3), dtype=np.uint8)
mosaic_labels = []
for i, (img, label) in enumerate(zip(images, labels)):
# 根据象限决定裁剪和放置位置
if i == 0: # 左上
x1a, y1a, x2a, y2a = 0, 0, cx, cy
elif i == 1: # 右上
x1a, y1a, x2a, y2a = cx, 0, img_size, cy
elif i == 2: # 左下
x1a, y1a, x2a, y2a = 0, cy, cx, img_size
else: # 右下
x1a, y1a, x2a, y2a = cx, cy, img_size, img_size
# 随机缩放原图
scale = random.uniform(0.5, 1.5)
h, w = img.shape[:2]
new_h, new_w = int(h * scale), int(w * scale)
img_resized = cv2.resize(img, (new_w, new_h))
# 随机裁剪并放置到拼接图上
# ...(裁剪和坐标映射逻辑)
mosaic_img[y1a:y2a, x1a:x2a] = img_cropped
# 调整标签坐标
# ...(标签坐标映射逻辑)
return mosaic_img, mosaic_labels
3.2 MixUp增强:两张图“叠”在一起
MixUp最早是图像分类里的trick,后来被YOLOv4引入目标检测。它的原理更直接:把两张图按一定比例混合,标签也按同样比例混合。
公式很简单:
new_img = lambda * img1 + (1 - lambda) * img2
new_label = lambda * label1 + (1 - lambda) * label2
这里的lambda是从Beta分布里采样出来的,通常在0.4~0.6之间。嗯,这里要注意:MixUp会让目标变得半透明,模型需要学会在“模糊”的情况下依然能识别目标。
我曾经踩过一个坑:MixUp之后,两个目标的边界框重叠太多,导致模型训练时梯度冲突。后来我加了一个IoU阈值过滤,如果两个框的重叠度超过0.7,就放弃这次MixUp。
3.3 HSV色域变换:让模型不怕“变色”
HSV变换是YOLO系列最基础的色彩增强。它把图像从RGB转到HSV空间,然后随机调整H(色调)、S(饱和度)、V(明度)三个通道。
参数范围(YOLOv5默认值):
| 通道 | 调整范围 | 作用 |
|---|---|---|
| H(色调) | ±0.015 | 模拟不同光照色温 |
| S(饱和度) | ±0.7 | 模拟褪色或鲜艳场景 |
| V(明度) | ±0.4 | 模拟过曝或欠曝 |
我个人习惯把V的调整范围再放大一点,到±0.5。因为实际项目中,光照变化是最常见的干扰因素。你想想看,同一个停车场,中午和傍晚的光照能差好几倍。
# HSV变换实现
def hsv_augment(img, h_gain=0.015, s_gain=0.7, v_gain=0.4):
# 转HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float32)
# 随机生成调整量
h = random.uniform(-h_gain, h_gain) * 180
s = random.uniform(-s_gain, s_gain) * 255
v = random.uniform(-v_gain, v_gain) * 255
# 分别调整三个通道
hsv[:, :, 0] = (hsv[:, :, 0] + h) % 180
hsv[:, :, 1] = np.clip(hsv[:, :, 1] + s, 0, 255)
hsv[:, :, 2] = np.clip(hsv[:, :, 2] + v, 0, 255)
# 转回BGR
return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
3.4 归一化与Resize:标准化的“最后一公里”
不管前面做了多少花哨的增强,最后两步一定是Resize和归一化。这是模型输入的标准格式要求。
Resize策略:
- Letterbox: YOLO系列的标准做法。保持宽高比,把图缩放到目标尺寸(如640×640),不足的部分用灰色填充。
- 直接拉伸: 简单粗暴,但会改变目标形状,影响检测精度。我不推荐。
- 随机裁剪+Resize: 相当于又做了一次数据增强。我偶尔会用,但要注意不要裁掉关键目标。
归一化:
YOLO的归一化很简单:把像素值从[0, 255]缩放到[0, 1],然后减去均值除以标准差。不过YOLOv5之后,官方默认只做除以255的简单归一化,不做标准化。为什么?因为标准化会丢失一些色彩信息,而YOLO本身已经通过HSV变换学到了色彩不变性。
# Letterbox Resize + 归一化
def letterbox(img, target_size=640, stride=32):
h, w = img.shape[:2]
# 计算缩放比例
scale = min(target_size / h, target_size / w)
new_h, new_w = int(h * scale), int(w * scale)
# 缩放到新尺寸
img_resized = cv2.resize(img, (new_w, new_h))
# 创建画布并填充灰色
canvas = np.full((target_size, target_size, 3), 114, dtype=np.uint8)
# 计算偏移量,居中放置
dw = (target_size - new_w) // 2
dh = (target_size - new_h) // 2
canvas[dh:dh+new_h, dw:dw+new_w] = img_resized
# 归一化到[0, 1]
canvas = canvas / 255.0
return canvas, (scale, dw, dh) # 返回缩放和偏移信息,用于标签映射
3.5 Albumentations库实战:增强界的“瑞士军刀”
Albumentations是我最常用的数据增强库,没有之一。它比OpenCV自带的增强快很多,而且支持目标检测的bbox同步变换。
为什么选Albumentations?
- 速度快: 底层用C++实现,比纯Python快3~5倍。
- API友好: 一行代码就能组合多种增强。
- bbox自动适配: 增强后,边界框会自动跟着变换,不用手动算。
我在项目中常用的Albumentations增强组合:
import albumentations as A
from albumentations.pytorch import ToTensorV2
# 训练集增强管道
train_transform = A.Compose([
# 几何变换
A.RandomResizedCrop(height=640, width=640, scale=(0.5, 1.0), p=0.5),
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2, rotate_limit=15, p=0.5),
# 色彩变换
A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1, p=0.8),
A.RandomGamma(gamma_limit=(80, 120), p=0.5),
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),
# 噪声与模糊
A.GaussNoise(var_limit=(10.0, 50.0), p=0.3),
A.GaussianBlur(blur_limit=(3, 7), p=0.3),
# 归一化
A.Normalize(mean=[0, 0, 0], std=[1, 1, 1]),
ToTensorV2(),
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
# 验证集只做归一化
val_transform = A.Compose([
A.Normalize(mean=[0, 0, 0], std=[1, 1, 1]),
ToTensorV2(),
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
3.6 本章知识体系总览
下面这张图总结了本章所有增强方法的关系和流程。你可以把它当作一个“增强流水线”来理解。
从这张图你可以看到,整个增强流程是串行+并行的。Mosaic、MixUp、HSV是三个独立的增强分支,它们的结果会汇聚到Albumentations里做进一步的组合增强,最后经过Resize和归一化变成模型能吃的张量。