数据预处理管线:从RAW到训练样本

各位同学,欢迎来到第四章。前面我们聊了RAW域的基础知识,也看了噪声模型长什么样。但说实话,光有这些还不够——你没法直接把一张RAW图扔给网络去训练。为什么?因为深度学习需要的是大量、多样、格式统一的样本对。今天我就带你走一遍我自己的预处理管线,从一张RAW图到最终能喂给网络的训练样本,每一步我都会讲清楚“为什么这么做”以及“我踩过哪些坑”。

核心思路:预处理不是简单的“改格式”,而是为了提升模型的泛化能力和训练效率。每一步都有它的物理意义或数学依据。

第一步:Packing(打包)——把拜耳阵列拆成四通道

RAW图是什么?是拜耳阵列。每个像素只记录R、G、B中的一个颜色分量。你不能直接把它当三通道图用,因为空间位置是错位的。我早期犯过一个错误:直接把RAW图resize成三通道送进网络,结果模型学出来的全是伪影。

正确的做法是Packing。说白了,就是把一个H×W的RAW图,按照拜耳模式拆成4个通道:R、G1、G2、B。每个通道的尺寸是H/2 × W/2。这样既保留了完整的空间信息,又把颜色分开了。

def pack_raw(raw, bayer_pattern='RGGB'):
    """
    raw: numpy array, shape (H, W)
    bayer_pattern: 拜耳模式,默认RGGB
    返回: packed, shape (H//2, W//2, 4)
    """
    H, W = raw.shape
    # 按2x2块提取
    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]
    
    # 根据拜耳模式调整顺序
    if bayer_pattern == 'RGGB':
        packed = np.stack([R, G1, G2, B], axis=-1)
    elif bayer_pattern == 'BGGR':
        packed = np.stack([B, G2, G1, R], axis=-1)
    # 其他模式类似处理
    
    return packed

嗯,这里要注意:Packing之后,你得到的是4通道数据。但网络输入通常是3通道怎么办?我个人的习惯是保留4通道,修改网络的第一层卷积。因为G1和G2虽然都是绿色,但它们在传感器上的物理位置不同,响应可能有细微差异。保留它们能让网络学到更精细的信息。

小技巧:如果你的框架不支持4通道输入,可以把G1和G2平均成一个G通道。但我在项目中对比过,保留双绿通道在PSNR上能提升0.1-0.2dB。

第二步:翻转与旋转增强——别让模型记住方向

数据增强是防止过拟合的利器。对于RAW域数据,翻转和旋转是最简单也最有效的增强手段。但有个坑:你不能随便翻转。

为什么?因为拜耳阵列是有方向性的。你想想看,一张RGGB模式的RAW图,如果直接水平翻转,拜耳模式就变成了GBRG。如果你不做相应的通道重排,模型就学乱了。

我自己的做法是:每次做几何变换时,同步调整通道顺序。比如水平翻转后,R和G1交换位置,G2和B交换位置。这样拜耳模式保持不变。

def flip_raw(packed, mode='horizontal'):
    """
    packed: shape (H//2, W//2, 4)
    mode: 'horizontal' 或 'vertical'
    """
    if mode == 'horizontal':
        # 水平翻转:R↔G1, G2↔B
        flipped = np.fliplr(packed)
        flipped = flipped[:, :, [1, 0, 3, 2]]  # 重排通道
    elif mode == 'vertical':
        # 垂直翻转:R↔G2, G1↔B
        flipped = np.flipud(packed)
        flipped = flipped[:, :, [2, 3, 0, 1]]
    return flipped

旋转也是同理。90度旋转后,拜耳模式会变,你需要重新映射。我曾经因为没处理好这个,模型在测试集上表现很好,但一换数据集就崩——说白了就是过拟合了方向信息。

避坑指南:不要对RAW图做随机裁剪后再做翻转。先翻转再裁剪,否则边界处的拜耳模式会乱掉。我吃过这个亏,训练了三天才发现。

第三步:随机裁剪——既要尺寸统一,又要内容丰富

网络输入需要固定尺寸,但RAW图大小不一。随机裁剪是最常用的方法。但裁剪不是随便切一块就行。

我个人习惯用128×128或256×256的patch。为什么是这个尺寸?太小了(比如64×64)噪声统计特性不稳定,太大了(比如512×512)GPU显存扛不住。128×128是个不错的平衡点。

裁剪时要注意两点:

  • 对齐2的倍数:因为Packing后尺寸是H/2×W/2,裁剪时确保起始坐标是偶数,这样不会破坏拜耳结构。
  • 内容多样性:我一般会设置一个“纹理阈值”,如果裁剪区域的方差太小(比如全是天空或墙壁),就重新裁。否则模型会学到太多平滑区域,对纹理的泛化能力变差。
def random_crop(packed, crop_size=128):
    """
    packed: shape (H, W, 4)
    crop_size: 裁剪尺寸
    """
    H, W, _ = packed.shape
    # 确保起始坐标是偶数
    x = np.random.randint(0, W - crop_size) // 2 * 2
    y = np.random.randint(0, H - crop_size) // 2 * 2
    crop = packed[y:y+crop_size, x:x+crop_size, :]
    
    # 检查纹理丰富度
    if np.var(crop) < 0.01:  # 阈值根据数据调整
        return random_crop(packed, crop_size)  # 重新裁
    return crop

经验之谈:我习惯在裁剪前先做归一化,这样方差阈值更容易设置。归一化到[0,1]区间后,阈值设在0.005-0.01之间比较合理。

第四步:噪声注入策略——让模型学会“抗噪”

这是整个预处理管线里最核心的一步。你想想看,如果训练数据都是干净的RAW图,模型学到的只是“去马赛克”或“色彩校正”,而不是真正的去噪。所以我们需要在训练时注入噪声。

噪声注入有两种策略:

  1. 合成噪声:用我们第三章讲的噪声模型,生成高斯+泊松噪声,加到干净RAW图上。这种方法可控性强,但可能和真实噪声有差距。
  2. 真实噪声对:用短曝光+长曝光的方式采集真实噪声对。这种方法最真实,但采集成本高。

我个人在项目中两种都用。初期用合成噪声快速迭代模型,后期用真实噪声对做微调。这里有个细节:注入噪声的强度要随机变化。我一般让噪声水平在0.5倍到2倍之间随机采样,这样模型能适应不同光照条件。

def inject_noise(clean, noise_level=1.0):
    """
    clean: 干净RAW图,shape (H, W, 4)
    noise_level: 噪声强度缩放因子
    """
    # 生成泊松噪声(信号相关)
    poisson = np.random.poisson(clean * 100) / 100.0 - clean
    # 生成高斯噪声(信号无关)
    gaussian = np.random.randn(*clean.shape) * 0.01
    
    # 随机调整噪声强度
    scale = np.random.uniform(0.5, 2.0) * noise_level
    noisy = clean + scale * (poisson + gaussian)
    
    return noisy

注意:噪声注入要在Packing之后做,不要在RAW图上直接加噪声。因为Packing会改变噪声的空间分布特性。我刚开始没注意这点,结果模型在真实数据上完全失效。

完整的预处理管线流程图

下面这张图总结了我上面讲的整个流程。你可以把它当作一个检查清单,每次做数据预处理时对照着走一遍。

RAW域数据预处理管线 输入:RAW图 (H×W) 第一步:Packing(打包) 拜耳阵列 → 4通道 (H/2×W/2×4) 第二步:翻转与旋转增强 同步调整通道顺序,保持拜耳模式 第三步:随机裁剪 固定尺寸 (128×128),对齐偶数坐标 第四步:噪声注入 合成噪声 / 真实噪声对,随机强度 输出:训练样本对 注意:每一步都要保持拜耳结构完整性,否则模型学到的特征会出错

总结一下

好了,这一章的内容就是这些。从RAW图到训练样本,我们走了四步:Packing、翻转旋转、随机裁剪、噪声注入。每一步都有它的道理,也都有容易踩的坑。我个人觉得,预处理管线花的时间,至少要和模型设计一样多。因为数据质量直接决定了模型的上限。