4、经典数据增强方法(上):几何变换与颜色变换

数据增强,说白了就是「无中生有」。但这不是瞎编,而是用合理的变换让模型看到更多样的数据。我做了这么多年缺陷检测,可以负责任地告诉你:一个靠谱的增强策略,往往比调模型结构更能提升泛化能力。

这一章我们先聊最经典的两大类:几何变换和颜色变换。它们简单、高效,是每个实战项目里几乎必用的手段。

核心观点:几何变换改变空间位置,颜色变换改变像素分布。两者互补,缺一不可。
经典数据增强方法(上)知识体系 数据增强 几何变换 颜色变换 旋转 翻转 裁剪 亮度 对比度 饱和度 注意:几何变换不改变像素值,颜色变换不改变空间位置 两者组合使用效果更佳,但要注意顺序和参数范围

4.1 几何变换:让模型学会「换个角度看缺陷」

几何变换的核心思想很简单:缺陷出现在图像的不同位置、不同角度,本质上还是同一个缺陷。模型如果学不会这个,就会把「左上角的划痕」和「右下角的划痕」当成两种东西。

4.1.1 旋转(Rotation)

旋转是最直观的几何变换。我习惯把旋转分为两类:

  • 小角度旋转(±10°~±30°):模拟拍摄时轻微的倾斜,适合大多数工业场景
  • 大角度旋转(90°、180°、270°):适合圆形对称工件,或者方向无关的缺陷

这里有个坑——旋转后的黑边。默认的旋转操作会在图像角落填充黑色像素,这些黑边会被模型当成「特征」学进去。我曾经在一个项目中,模型莫名其妙对图像四角特别敏感,排查了半天才发现是旋转增强留下的黑边在作祟。

我的建议:使用 OpenCV 的 cv2.warpAffine 时,设置 borderMode=cv2.BORDER_REFLECT 或 cv2.BORDER_WRAP,避免黑边污染。
import cv2
import numpy as np

def rotate_image(image, angle, scale=1.0):
    h, w = image.shape[:2]
    center = (w // 2, h // 2)
    M = cv2.getRotationMatrix2D(center, angle, scale)
    # 关键:用 BORDER_REFLECT 代替默认的 BORDER_CONSTANT
    rotated = cv2.warpAffine(image, M, (w, h), 
                             borderMode=cv2.BORDER_REFLECT)
    return rotated

# 使用示例:随机旋转 -20° 到 20°
angle = np.random.uniform(-20, 20)
aug_img = rotate_image(original_img, angle)

4.1.2 翻转(Flip)

翻转分两种:水平翻转和垂直翻转。水平翻转最常见,相当于照镜子。垂直翻转在工业场景中要谨慎使用——你想想看,一个螺丝钉倒过来放,跟正着放,在物理世界中其实是两种状态。

我个人习惯这样判断:如果缺陷的方向性不重要,就用翻转;如果缺陷有明确的方向(比如划痕的方向),就别乱翻。

翻转类型 适用场景 慎用场景
水平翻转 对称工件、纹理缺陷、印刷品 有左右方向标识的工件
垂直翻转 圆形工件、无方向性缺陷 有重力方向依赖的缺陷(如流痕)

4.1.3 裁剪(Cropping)

裁剪有两种玩法:

  1. 随机裁剪:从原图中随机切出一块,再缩放到原图大小。这相当于让模型看到缺陷的局部特写。
  2. 中心裁剪:固定裁剪中心区域,适合去除边缘干扰。

嗯,这里要注意——裁剪比例别太狠。我见过有人把 640×640 的图裁到 224×224,结果缺陷特征完全丢失了。一般来说,裁剪比例控制在 0.7~0.9 之间比较安全。

避坑指南:裁剪后一定要检查缺陷标注的坐标是否还在图内。我曾经因为裁剪把标注框裁没了,模型训练时一直报「空标注」错误,排查了整整一天。

4.2 颜色变换:应对光照变化的「特效药」

颜色变换解决的是另一个问题:同一个缺陷,在不同光照下看起来不一样。车间里的光源会老化、会更换、会有阴影,模型如果没见过这些变化,上线就崩。

4.2.1 亮度(Brightness)

亮度调整说白了就是给每个像素加上或减去一个值。在 HSV 色彩空间里操作会更自然——只调 V 通道,不影响颜色本身。

def adjust_brightness(image, delta):
    """delta 范围:-50 ~ 50"""
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    h, s, v = cv2.split(hsv)
    v = cv2.add(v, delta)
    v = np.clip(v, 0, 255)
    hsv = cv2.merge([h, s, v])
    return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

# 随机调整亮度
delta = np.random.randint(-30, 30)
aug_img = adjust_brightness(original_img, delta)

我一般把亮度调整范围控制在 ±30 以内。太暗了细节全丢,太亮了过曝,反而引入噪声。

4.2.2 对比度(Contrast)

对比度调整会让「亮的更亮,暗的更暗」。这在缺陷检测中特别有用——有些缺陷在低对比度下几乎看不见,模型需要学会在各种对比度下都能识别。

对比度的数学本质是:new_pixel = alpha * (pixel - 128) + 128。alpha 大于 1 时增强对比度,小于 1 时降低。

实战经验:对比度增强配合直方图均衡化,对「浅划痕」「微裂纹」这类低对比度缺陷有奇效。我曾在手机屏幕缺陷检测项目中,用这个组合把召回率从 82% 提到了 91%。

4.2.3 饱和度(Saturation)

饱和度控制颜色的鲜艳程度。在工业场景中,饱和度调整主要用来模拟不同材质表面的颜色差异。比如同一批塑料件,不同批次可能有轻微色差,饱和度增强可以覆盖这种差异。

操作上,还是在 HSV 空间调 S 通道:

def adjust_saturation(image, factor):
    """factor 范围:0.5 ~ 1.5"""
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    h, s, v = cv2.split(hsv)
    s = cv2.multiply(s, factor)
    s = np.clip(s, 0, 255)
    hsv = cv2.merge([h, s, v])
    return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

4.3 组合策略:1+1 > 2

单独用某一种变换效果有限。我习惯把几何变换和颜色变换串起来用:

  1. 先做几何变换(旋转 + 翻转 + 裁剪)
  2. 再做颜色变换(亮度 + 对比度 + 饱和度)

顺序为什么这么定?因为几何变换会改变像素位置,如果先调颜色再旋转,旋转后的插值计算会轻微改变颜色值,造成不必要的误差。

小技巧:用 imgaug 或 albumentations 库可以轻松组合多个增强操作。我个人偏爱 albumentations,它的速度比 OpenCV 原生实现还快,而且自动处理标注框的变换。
import albumentations as A

transform = A.Compose([
    A.Rotate(limit=20, border_mode=cv2.BORDER_REFLECT, p=0.8),
    A.HorizontalFlip(p=0.5),
    A.RandomCrop(height=512, width=512, p=0.5),
    A.RandomBrightnessContrast(brightness_limit=0.2, 
                                contrast_limit=0.2, p=0.8),
    A.HueSaturationValue(hue_shift_limit=0,
                          sat_shift_limit=20,
                          val_shift_limit=0, p=0.5),
])

# 使用
augmented = transform(image=image, mask=mask)
aug_img = augmented['image']
aug_mask = augmented['mask']

4.4 参数选择的「黄金法则」

参数调太大,增强后的图像失真,模型学坏;参数调太小,等于没做。我总结了一个经验表格:

增强操作 推荐参数范围 过大的后果
旋转角度 ±15° ~ ±30° 缺陷变形严重,失去物理意义
亮度偏移 ±20 ~ ±40 过曝或过暗,细节丢失
对比度系数 0.7 ~ 1.3 图像出现「塑料感」,不真实
饱和度系数 0.6 ~ 1.4 颜色失真,引入伪缺陷
裁剪比例 0.7 ~ 0.9 缺陷被裁掉,标注失效
重要提醒:增强参数不是越大越好。我见过一个团队把旋转角度设到 ±90°,结果模型把「横着的划痕」和「竖着的划痕」当成两种缺陷,反而降低了准确率。适可而止,才是王道。