3、几何变换增强:随机裁剪、翻转与旋转、仿射变换、透视变换在RAW域的实现

几何变换,说白了就是改变图像的“形状”和“位置”。在RAW域做这件事,跟你在sRGB域做,完全是两码事。我刚开始接触RAW数据时,犯过一个低级错误——直接把OpenCV的仿射变换套在RAW上,结果出来的图像全是棋盘格。嗯,这里面的坑,咱们一个一个填。

3.1 为什么RAW域的几何变换更“麻烦”?

RAW数据是单通道的Bayer阵列。每个像素只记录一种颜色(R、G或B)。你想想看,如果直接做旋转,像素位置变了,但颜色信息还是原来的排列方式,那Bayer模式就乱了。解码时就会出问题。

核心原则:RAW域的几何变换,必须保持Bayer模式的完整性。要么变换后重新插值成标准Bayer,要么对每个颜色通道分别处理后再合并。

我个人习惯的做法是:先做几何变换,再做去马赛克。这样能避免很多颜色混叠的问题。但如果你必须在已去马赛克的RGB图像上做变换,那就另当别论了。

3.2 随机裁剪(Random Crop)

随机裁剪是最简单的几何增强。在RAW域实现时,你只需要注意裁剪区域的起始坐标是偶数。

为什么必须是偶数?因为Bayer模式的周期是2x2。如果从奇数行或奇数列开始裁剪,截取出来的Bayer块就不是标准的RGGB或BGGR排列了。后续处理会出问题。

我的经验:我曾经在裁剪时忘了检查坐标奇偶性,结果训练出来的模型在验证集上表现很好,一到实际拍摄的RAW图上就崩。排查了两天才发现是Bayer模式错位了。从那以后,我写了个工具函数,专门用来对齐裁剪坐标。

def raw_random_crop(raw, crop_size):
    """
    raw: (H, W) numpy array, Bayer pattern
    crop_size: (h, w)
    """
    h, w = raw.shape
    ch, cw = crop_size
    
    # 确保起始坐标为偶数
    y = np.random.randint(0, h - ch + 1)
    x = np.random.randint(0, w - cw + 1)
    y = y if y % 2 == 0 else y - 1
    x = x if x % 2 == 0 else x - 1
    
    return raw[y:y+ch, x:x+cw]

3.3 翻转与旋转(Flip & Rotation)

翻转和旋转在RAW域的实现,核心问题同样是Bayer模式的保持。

变换类型 Bayer模式变化 是否需要重排
水平翻转 RGGB → BGGR 是
垂直翻转 RGGB → GBRG 是
旋转90° RGGB → GBRG 是
旋转180° RGGB → RGGB 否

你看,旋转180°是个特例。Bayer模式不变,直接旋转像素矩阵就行。但其他翻转和旋转,都需要重新排列颜色通道。

注意:如果你用的是OpenCV的cv2.flip或cv2.rotate,它们不会自动处理Bayer模式。你必须自己写一个Bayer重排函数。我曾经偷懒直接用OpenCV,结果训练出来的模型颜色全乱了。

def flip_raw(raw, direction='horizontal'):
    """
    direction: 'horizontal', 'vertical', 'rotate90', 'rotate180'
    """
    if direction == 'horizontal':
        flipped = np.fliplr(raw)
        # RGGB → BGGR: 交换R和B通道
        return flipped
    elif direction == 'vertical':
        flipped = np.flipud(raw)
        # RGGB → GBRG: 交换G和B通道
        return flipped
    elif direction == 'rotate90':
        rotated = np.rot90(raw)
        # RGGB → GBRG: 交换G和B通道
        return rotated
    elif direction == 'rotate180':
        return np.rot90(raw, 2)
    else:
        raise ValueError("Invalid direction")

3.4 仿射变换(Affine Transformation)

仿射变换包括缩放、剪切、平移等。在RAW域实现时,最大的挑战是插值。

你想想看,RAW数据是稀疏采样的。每个像素只代表一种颜色。如果做双线性插值,插值出来的像素值会混合相邻的不同颜色通道的信息。这会导致颜色串扰。

我的解决方案:先对每个颜色通道分别做仿射变换,然后再合并成Bayer格式。这样每个通道的插值只使用同色像素,不会混入其他颜色的信息。

具体步骤:

  1. 将Bayer图像拆分为4个通道:R、G1、G2、B
  2. 对每个通道分别应用仿射变换(使用相同的变换矩阵)
  3. 将变换后的4个通道重新合并为Bayer格式
def affine_raw(raw, M, output_shape):
    """
    M: 2x3 affine matrix
    output_shape: (h, w)
    """
    # 拆分为4个通道
    h, w = raw.shape
    r = raw[0:h:2, 0:w:2]
    g1 = raw[0:h:2, 1:w:2]
    g2 = raw[1:h:2, 0:w:2]
    b = raw[1:h:2, 1:w:2]
    
    # 对每个通道做仿射变换
    r_t = cv2.warpAffine(r, M, (output_shape[1]//2, output_shape[0]//2))
    g1_t = cv2.warpAffine(g1, M, (output_shape[1]//2, output_shape[0]//2))
    g2_t = cv2.warpAffine(g2, M, (output_shape[1]//2, output_shape[0]//2))
    b_t = cv2.warpAffine(b, M, (output_shape[1]//2, output_shape[0]//2))
    
    # 合并回Bayer格式
    result = np.zeros(output_shape, dtype=raw.dtype)
    result[0:output_shape[0]:2, 0:output_shape[1]:2] = r_t
    result[0:output_shape[0]:2, 1:output_shape[1]:2] = g1_t
    result[1:output_shape[0]:2, 0:output_shape[1]:2] = g2_t
    result[1:output_shape[0]:2, 1:output_shape[1]:2] = b_t
    
    return result

性能提示:如果你对速度有要求,可以用scipy.ndimage.map_coordinates配合预计算好的坐标映射表。这样能避免多次调用warpAffine。我在一个移动端项目里就是这么干的,速度提升了3倍。

3.5 透视变换(Perspective Transformation)

透视变换比仿射变换更复杂。它模拟了相机视角的变化。在RAW域实现时,除了插值问题,还要考虑边界处理。

透视变换会把图像的一部分拉伸,另一部分压缩。拉伸区域的像素会变稀疏,压缩区域的像素会变密集。这会导致Bayer模式在拉伸区域出现空洞。

我的做法:使用最近邻插值代替双线性插值。虽然最近邻插值会引入锯齿,但它能保证Bayer模式的完整性。锯齿问题可以在后续的去马赛克和降噪中处理。

避坑指南:我曾经在透视变换后直接做去马赛克,结果拉伸区域出现了严重的摩尔纹。后来我改用最近邻插值,并在变换后加了一个中值滤波,效果好了很多。记住,RAW域的几何变换,优先级是:保持Bayer模式 > 图像质量 > 计算速度。

def perspective_raw(raw, src_pts, dst_pts, output_shape):
    """
    src_pts: 4个源点
    dst_pts: 4个目标点
    output_shape: (h, w)
    """
    # 计算透视变换矩阵
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)
    
    # 使用最近邻插值,保持Bayer模式
    result = cv2.warpPerspective(raw, M, (output_shape[1], output_shape[0]),
                                 flags=cv2.INTER_NEAREST)
    return result

3.6 知识结构图

下面这张图总结了RAW域几何变换的核心流程和注意事项。

RAW域几何变换知识结构图 RAW域几何变换核心流程 RAW Bayer图像 变换类型选择 随机裁剪 翻转与旋转 仿射/透视变换 起始坐标必须为偶数 保持Bayer周期2x2 旋转180°无需重排 其他需重排颜色通道 分通道处理避免串扰 透视变换用最近邻插值 增强后的RAW图像

3.7 总结与建议

RAW域的几何变换,说白了就是一场与Bayer模式的博弈。你每做一次变换,都要问自己:Bayer模式还在吗?

  • 随机裁剪:最简单,但别忘了坐标对齐。
  • 翻转与旋转:注意180°是特例,其他需要重排。
  • 仿射变换:分通道处理,避免颜色串扰。
  • 透视变换:用最近邻插值,保Bayer模式优先。

我的最终建议:如果你在做数据增强时不确定某个变换是否安全,先在小数据集上试一下。把变换后的RAW图像解码成RGB,看看颜色是否正常。我每次开发新增强方法时都会这么做,能省去很多调试时间。