数据预处理管线:从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图,模型学到的只是“去马赛克”或“色彩校正”,而不是真正的去噪。所以我们需要在训练时注入噪声。
噪声注入有两种策略:
- 合成噪声:用我们第三章讲的噪声模型,生成高斯+泊松噪声,加到干净RAW图上。这种方法可控性强,但可能和真实噪声有差距。
- 真实噪声对:用短曝光+长曝光的方式采集真实噪声对。这种方法最真实,但采集成本高。
我个人在项目中两种都用。初期用合成噪声快速迭代模型,后期用真实噪声对做微调。这里有个细节:注入噪声的强度要随机变化。我一般让噪声水平在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图到训练样本,我们走了四步:Packing、翻转旋转、随机裁剪、噪声注入。每一步都有它的道理,也都有容易踩的坑。我个人觉得,预处理管线花的时间,至少要和模型设计一样多。因为数据质量直接决定了模型的上限。