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图——长曝光也有噪声,多帧平均也不完美。所以工业界的主流做法是合成噪声。

我的做法是这样的:

  1. 准备干净RAW图:用低ISO、长曝光、多帧平均的方式拍一组“准干净”图。或者直接用渲染引擎生成无噪声的RAW数据。
  2. 标定传感器噪声参数:对每个ISO档位,拍灰卡,统计均值和方差,拟合出泊松-高斯模型的参数 a 和 σ。
  3. 合成噪声:对每张干净图,按目标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域去噪的核心流程

下面这张图把整个流程串起来了——从噪声模型到数据合成,再到网络训练和部署:

RAW域深度学习去噪核心流程 噪声模型 泊松-高斯模型 参数标定 灰卡+ISO拟合 合成数据生成 干净RAW+噪声叠加 网络选择 CNN / UNet 训练策略 L1损失+残差学习 部署优化 量化+NPU加速 核心要点 ① 噪声模型必须标定真实传感器参数,不能瞎猜 ② 合成数据要覆盖目标ISO范围,暗部亮部都要有 实际部署建议 • 模型输入输出都用4通道Bayer packed格式 • 残差学习比直接预测干净图收敛更快 • 量化到INT8时注意噪声残差的动态范围

3.6 实战经验:训练与部署的坑

最后分享几个我在实际项目中踩过的坑:

  • 数据增强要小心:RAW域不能随便做颜色增强,比如改变色温、色调,因为噪声模型跟信号强度相关。我一般只做翻转和旋转,不做颜色抖动。
  • Batch size别太大:RAW图分辨率高,显存吃紧。我习惯用patch训练,比如256×256的patch,batch size设4-8就够了。
  • 推理时要注意黑电平:训练时如果没模拟黑电平,推理时真实RAW图的黑电平偏移会导致去噪结果偏色。嗯,这个坑我栽过一次,后来在数据合成时加了黑电平模拟才解决。

一句话总结:RAW域去噪的核心不是网络结构有多花哨,而是噪声模型有多准、合成数据有多真。模型选UNet基本不会错,但数据质量决定了上限。