4、AI去噪(一):基于CNN的空间域降噪原理

各位好,欢迎来到AI去噪的第一讲。

暗光场景下,噪声是ISP的头号敌人。传统去噪方法,比如BM3D、NLM,效果不错,但计算量太大,而且容易把纹理细节也抹掉。我个人习惯把这类方法叫做“手工特征”去噪——你告诉算法“噪声长这样”,它就去滤除。但问题是,噪声的形态千变万化,手工特征很难覆盖全。

CNN(卷积神经网络)的出现,改变了这个局面。它不靠人定义特征,而是从大量数据中自己“学”出噪声的分布。说白了,就是让网络看几万张带噪图和对应的干净图,然后它自己总结出一套去噪规则。

4.1 为什么是CNN?空间域的优势

图像去噪可以在两个域做:空间域(像素域)和变换域(频域)。传统方法很多在变换域做,比如小波去噪。但CNN天然工作在空间域,它直接在像素上做卷积操作。

你想想看,噪声在空间上是什么表现?是像素值的随机跳变。CNN的卷积核,本质上就是一个局部滤波器。3x3的核,看周围8个邻居;5x5的核,看周围24个邻居。这种局部感受野,非常适合捕捉噪声的局部统计特性。

我在项目中遇到过一个问题:用频域方法去噪,去完噪图像边缘总有振铃效应。换成CNN空间域方法,振铃基本消失了。为什么?因为CNN在空间域做的是“像素到像素”的映射,它直接学习每个像素应该变成什么值,而不是先变换再反变换,避免了频域截断带来的伪影。

核心观点: CNN空间域去噪的本质,是学习一个从“带噪像素块”到“干净中心像素”的非线性映射函数。

4.2 经典架构:DnCNN

说到CNN去噪,绕不开DnCNN。这是2016年张凯等人提出的网络,结构简单但效果惊艳。直到今天,很多工业级ISP方案里还能看到它的影子。

DnCNN的核心思路是:不直接预测干净图像,而是预测残差——也就是噪声本身。网络输入是带噪图,输出是噪声图,然后用带噪图减去噪声图,得到干净图。这个技巧叫“残差学习”。

为什么残差学习有效?因为噪声的分布相对简单,均值接近0,方差有限。网络学一个接近0的残差,比直接学一个复杂的干净图像容易得多。我刚开始做去噪时,直接让网络预测干净图,训练了三天都不收敛。改成残差学习,半天就出效果了。

DnCNN的网络结构如下:

  • 输入层: 带噪图像,单通道或三通道
  • 中间层: 若干卷积层(通常15-20层),每层64个3x3卷积核,配合BN(批归一化)和ReLU激活
  • 输出层: 单通道残差图,无激活函数
  • 损失函数: MSE(均方误差)
# DnCNN核心结构示意(PyTorch风格)
class DnCNN(nn.Module):
    def __init__(self, depth=17, n_channels=64):
        super().__init__()
        layers = []
        # 第一层:Conv + ReLU
        layers.append(nn.Conv2d(3, n_channels, 3, padding=1))
        layers.append(nn.ReLU(inplace=True))
        # 中间层:Conv + BN + ReLU
        for _ in range(depth - 2):
            layers.append(nn.Conv2d(n_channels, n_channels, 3, padding=1))
            layers.append(nn.BatchNorm2d(n_channels))
            layers.append(nn.ReLU(inplace=True))
        # 最后一层:Conv(输出残差)
        layers.append(nn.Conv2d(n_channels, 3, 3, padding=1))
        self.net = nn.Sequential(*layers)

    def forward(self, x):
        residual = self.net(x)
        return x - residual  # 干净图 = 带噪图 - 残差
经验之谈: 网络深度不是越深越好。对于去噪任务,15-17层通常就够了。太深反而容易过拟合,而且推理速度慢。我在手机ISP上部署时,为了控制延迟,把深度压缩到9层,效果只下降了不到0.3dB。

4.3 感受野与去噪性能的关系

CNN去噪有一个关键参数:感受野。它决定了网络能看到多大范围的上下文信息。

对于3x3卷积核,堆叠d层,感受野大小是 2d+1。也就是说,17层的DnCNN,感受野是35x35像素。这意味着每个输出像素,参考了周围35x35范围内的信息。

为什么感受野重要?因为噪声不是完全独立的。真实传感器噪声有相关性,比如固定模式噪声(FPN)在空间上有重复模式。感受野越大,网络越能捕捉到这些长程相关性。

但感受野也不是越大越好。我做过一个实验:把网络加深到30层,感受野到61x61,去噪PSNR反而下降了0.1dB。为什么?因为过大的感受野引入了太多无关的纹理信息,网络开始“模糊”细节。

网络深度 感受野 PSNR (dB) 推理时间 (ms)
5层 11x11 28.3 1.2
10层 21x21 29.1 2.5
17层 35x35 29.6 4.3
25层 51x51 29.5 6.8

嗯,这里要注意:上表数据来自我在实验室用Kodak24数据集测的结果。实际部署时,不同传感器噪声特性不同,最优深度需要重新调。

4.4 空间域去噪的局限性

CNN空间域去噪虽然强,但不是万能的。我总结了几条“坑”,都是曾经踩过的:

  • 纹理过平滑: 网络分不清“噪声”和“细纹理”。草地的纹理、布料的编织纹,经常被当成噪声滤掉。我曾经在夜景人像上翻过车——毛衣的纹理全没了,看起来像塑料。
  • 边缘模糊: 强边缘处,噪声和边缘混合在一起,网络容易把边缘也平滑掉。结果就是去完噪,图像像蒙了一层雾。
  • 噪声水平泛化差: 用σ=25的噪声训练的网络,拿到σ=50的噪声上,效果断崖式下跌。真实传感器的噪声水平是变化的,单一模型很难覆盖全范围。
避坑指南: 我曾经在量产项目上直接用公开数据集训练的DnCNN,结果在真实传感器图像上效果很差。后来发现,公开数据集的噪声是高斯噪声,而真实传感器噪声是泊松-高斯混合噪声。两者分布差异很大。解决方案:用传感器标定数据生成训练集,或者用噪声估计网络先做噪声水平预测。

4.5 空间域去噪的SVG结构图

下面这张图展示了CNN空间域去噪的完整流程,从输入到输出,以及残差学习的核心逻辑。

CNN空间域去噪流程(残差学习) 带噪图像 I_noisy CNN网络 Conv+BN+ReLU×N层 残差学习 预测噪声 N_pred 减法操作 I_clean = I_noisy - N_pred 跳跃连接(残差路径) 干净图像 I_clean 💡 残差学习优势: • 网络学习目标从复杂图像变为简单噪声(均值接近0),收敛更快 • 跳跃连接让梯度直接回传到浅层,缓解梯度消失问题

4.6 训练数据与数据增强

CNN去噪的效果,很大程度上取决于训练数据。我见过不少团队花大量时间调网络结构,但训练数据随便用ImageNet加高斯噪声。结果呢?效果就是上不去。

真实场景的噪声,远比高斯噪声复杂。以CMOS传感器为例,噪声来源包括:

  • 光子散粒噪声: 服从泊松分布,与信号强度相关。亮区噪声大,暗区噪声小。
  • 读出噪声: 近似高斯分布,与信号无关。
  • 固定模式噪声: 空间上固定的偏移,每颗传感器都不一样。
  • 量化噪声: ADC量化带来的误差。

我建议的做法是:用实际传感器在不同ISO、不同曝光时间下采集的raw图,标定出噪声模型,然后用这个模型生成训练数据。如果条件不允许,至少要用泊松-高斯混合模型,而不是纯高斯。

数据增强方面,除了常规的旋转、翻转、裁剪,我特别推荐加一个操作:亮度抖动。因为暗光场景的亮度变化范围很大,网络需要适应不同亮度水平下的噪声强度。

小技巧: 训练时,把干净图和带噪图都做相同的亮度缩放(比如乘以0.8到1.2之间的随机系数)。这样网络学到的去噪能力,对亮度变化更鲁棒。我在一个夜景项目上用这个技巧,PSNR提升了0.4dB。

4.7 空间域去噪的评估指标

评估去噪效果,不能只看PSNR。PSNR高不代表视觉效果好。我见过PSNR 32dB的图像,看起来比30dB的还差——因为PSNR对纹理细节不敏感。

我常用的评估组合:

指标 关注点 说明
PSNR 像素级误差 数值越高越好,但与人眼感知不完全一致
SSIM 结构相似性 更关注亮度、对比度、结构,比PSNR更贴近人眼
LPIPS 感知相似性 用预训练网络提取特征,计算特征距离,最接近人眼判断
NIQE 无参考质量 不需要干净图,适合评估真实场景去噪效果

我个人习惯:实验室阶段看PSNR+SSIM,产品化阶段看LPIPS+NIQE。因为最终用户不关心像素误差,他们只关心“看起来干不干净”。