3. 深度学习去噪:CNN/UNet在RAW域去噪中的应用、噪声模型与合成数据生成
图像去噪这件事,传统方法折腾了十几年,从空域滤波到变换域阈值,再到BM3D这种非局部自相似性王者,基本把能挖的潜力都挖干净了。但到了RAW域,情况就不太一样了——传感器原始数据里的噪声模型其实比sRGB域干净得多,也更有规律可循。深度学习一来,RAW域去噪就成了一个“数据+模型”的游戏。
我个人习惯把RAW域去噪看作一个信号恢复问题,而不是单纯的“磨皮”。因为RAW数据是线性的,噪声分布也相对简单,模型学起来反而比在sRGB域容易。今天我们就来聊聊CNN和UNet怎么在RAW域干活,以及怎么造出靠谱的合成数据。
3.1 RAW域的噪声模型
先搞清楚噪声从哪来。传感器上的噪声主要分两部分:
- 光子散粒噪声(Shot Noise):光子到达传感器是随机的,服从泊松分布。信号越强,噪声方差越大。
- 读出噪声(Read Noise):电路本身的热噪声、量化噪声等,近似高斯分布,与信号强度无关。
所以一个经典的RAW域噪声模型长这样:
I_noisy = I_clean + n_shot(I_clean) + n_read
其中 n_shot 是泊松噪声,n_read 是高斯噪声。实际工程中,我们通常用一个泊松-高斯模型来近似:
I_noisy = a * Poisson(I_clean / a) + Gaussian(0, σ²)
这里的 a 是增益因子,跟ISO有关。ISO越高,a 越小,散粒噪声越明显。
关键点:RAW域的噪声是信号相关的。亮区噪声大,暗区噪声小。这个特性在合成数据时必须模拟准确,否则模型学出来就是错的。
我在项目中遇到过一个问题:直接用高斯噪声训练模型,结果在真实低光场景下完全失效。后来才发现,散粒噪声的“信号依赖”特性才是去噪模型泛化的关键。
3.2 合成数据生成:怎么造出靠谱的噪声对
深度学习去噪需要成对的干净/噪声图像。但真实场景下你很难拍到“干净”的RAW图——长曝光也有噪声,多帧平均也不完美。所以工业界的主流做法是合成噪声。
我的做法是这样的:
- 准备干净RAW图:用低ISO、长曝光、多帧平均的方式拍一组“准干净”图。或者直接用渲染引擎生成无噪声的RAW数据。
- 标定传感器噪声参数:对每个ISO档位,拍灰卡,统计均值和方差,拟合出泊松-高斯模型的参数
a和σ。 - 合成噪声:对每张干净图,按目标ISO参数,逐像素生成泊松噪声和高斯噪声,叠加得到噪声图。
下面是一个简化的合成流程:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 读取干净RAW | 确保是线性数据,未做黑电平校正 |
| 2 | 计算信号相关噪声 | 对每个像素,按泊松分布采样 |
| 3 | 叠加读出噪声 | 加一个固定方差的高斯噪声 |
| 4 | 模拟黑电平与量化 | 加偏移、截断、量化到整数 |
| 5 | 输出噪声RAW | 与干净图配对,用于训练 |
避坑指南:我曾经直接用Python的numpy.random.poisson生成噪声,结果发现训练出来的模型在暗部有偏色。后来排查发现,泊松分布的采样值必须跟像素值在同一量级,而且要注意数据类型——uint16的溢出截断会破坏噪声分布。
3.3 CNN在RAW域去噪中的应用
CNN做去噪,说白了就是学一个映射:从噪声图到干净图。但RAW域跟sRGB域有几个关键区别:
- 通道数不同:RAW通常是Bayer模式,4个通道(R、G1、G2、B),或者用packing变成4通道。
- 数据范围不同:RAW是12-14bit线性数据,范围0-16383,不是0-255。
- 噪声分布不同:信号相关,不是均匀的。
所以CNN的输入层要适配4通道,输出也是4通道。我常用的一个轻量CNN结构是这样的:
Input: 4×H×W (Bayer packed)
↓
Conv3×3, ReLU → 32 channels
↓
Conv3×3, ReLU → 32 channels
↓
Conv3×3, ReLU → 32 channels
↓
Conv3×3 → 4 channels (残差输出)
↓
Output: 4×H×W (去噪后RAW)
这里用的是残差学习,网络预测的是噪声残差,而不是直接输出干净图。这样做收敛更快,因为残差更稀疏。
注意:RAW域去噪的损失函数最好用L1或Charbonnier loss,别用L2。L2对异常值太敏感,容易导致去噪结果偏平滑,丢失纹理。我在一个夜景项目里试过L2,结果星星全被抹掉了——教训深刻。
3.4 UNet:为什么它更适合RAW域去噪
CNN堆几层卷积,感受野有限,对大块噪声区域效果不好。UNet的优势在于多尺度特征融合——下采样捕捉全局结构,上采样恢复细节,跳连接保留边缘信息。
RAW域用UNet,有几个设计要点:
- 输入输出都是4通道:Bayer packed格式,不要拆成单通道处理,否则会破坏颜色相关性。
- 下采样用stride卷积:别用pooling,stride卷积能保留更多位置信息。
- 跳连接用加法而不是拼接:减少参数量,训练更快。我试过拼接,效果差不多但模型大了30%。
下面是一个典型的UNet结构示意:
Input (4×H×W)
↓
Conv3×3, ReLU → 64ch
↓
Conv3×3, ReLU → 64ch
↓
Downsample (stride2) → 128ch
↓
Conv3×3, ReLU → 128ch
↓
Downsample (stride2) → 256ch
↓
Conv3×3, ReLU → 256ch
↓
Upsample (转置卷积) → 128ch
↓
跳连接 + Conv3×3 → 128ch
↓
Upsample → 64ch
↓
跳连接 + Conv3×3 → 64ch
↓
Conv3×3 → 4ch (残差输出)
嗯,这里要注意的是,UNet的深度不要超过4层。RAW图分辨率通常很高(比如4000×3000),太深了显存放不下,而且感受野已经够用了。
3.5 知识结构图:RAW域去噪的核心流程
下面这张图把整个流程串起来了——从噪声模型到数据合成,再到网络训练和部署:
3.6 实战经验:训练与部署的坑
最后分享几个我在实际项目中踩过的坑:
- 数据增强要小心:RAW域不能随便做颜色增强,比如改变色温、色调,因为噪声模型跟信号强度相关。我一般只做翻转和旋转,不做颜色抖动。
- Batch size别太大:RAW图分辨率高,显存吃紧。我习惯用patch训练,比如256×256的patch,batch size设4-8就够了。
- 推理时要注意黑电平:训练时如果没模拟黑电平,推理时真实RAW图的黑电平偏移会导致去噪结果偏色。嗯,这个坑我栽过一次,后来在数据合成时加了黑电平模拟才解决。
一句话总结:RAW域去噪的核心不是网络结构有多花哨,而是噪声模型有多准、合成数据有多真。模型选UNet基本不会错,但数据质量决定了上限。