3. AI ISP 基础:深度学习在图像处理中的应用
各位同学,欢迎来到第三章。从这一章开始,我们正式进入 AI ISP 的核心地带。说实话,很多做传统 ISP 的工程师,一开始对深度学习是有些抵触的。我当年也一样,觉得这东西像个黑盒子,不稳定。但后来发现,传统算法在低光、去噪、超分这些场景下,天花板实在太低了。
深度学习,说白了就是给 ISP 装上了一双能「学习」的眼睛。它不再依赖人工设计的固定规则,而是从海量数据中自己总结出最优的映射关系。这一章,我会带你打好 CNN、ResNet、U-Net 和注意力机制这几个地基。
3.1 卷积神经网络(CNN)基础
CNN 是几乎所有图像处理深度学习模型的基石。它的核心操作就是「卷积」——用一个小的窗口(卷积核)在图像上滑动,提取局部特征。
我刚开始学的时候,总觉得卷积和图像处理里的滤波是一回事。其实本质上确实很像,但 CNN 的卷积核参数是 学习出来 的,而不是我们手动设定的。
3.1.1 卷积层的核心参数
| 参数 | 含义 | 我的经验 |
|---|---|---|
| 卷积核大小 (Kernel Size) | 感受野的大小,常用 3x3 | 3x3 是黄金尺寸,计算量和感受野平衡得最好 |
| 步长 (Stride) | 卷积核每次移动的像素数 | 步长=2 可以替代下采样,减少计算量 |
| 填充 (Padding) | 在输入边缘补零,控制输出尺寸 | Same 填充保证输入输出尺寸一致,方便网络设计 |
| 输入/输出通道 (Channels) | 特征图的深度 | 通道数越多,网络表达能力越强,但参数量也越大 |
3.1.2 激活函数与池化
卷积是线性操作,激活函数负责引入非线性。没有它,再深的网络也只是线性变换的堆叠,毫无意义。
- ReLU: 最常用,f(x)=max(0,x)。计算快,能缓解梯度消失。但要注意「神经元死亡」问题——一旦输入为负,梯度就永远为0。
- Leaky ReLU: 给负数部分一个很小的斜率(比如0.01),解决了神经元死亡问题。我在低光图像增强中比较喜欢用这个。
- 池化(Pooling): 最大池化保留最显著特征,平均池化保留整体信息。现在很多网络用步长卷积代替池化,效果更好。
# 一个简单的 CNN 特征提取块(PyTorch 风格)
import torch.nn as nn
class ConvBlock(nn.Module):
def __init__(self, in_ch, out_ch):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
nn.Conv2d(out_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.conv(x)
3.2 残差网络(ResNet)
你想想看,网络越深,效果应该越好对吧?但实践发现,网络深到一定程度,训练误差反而上升了。这就是著名的「退化问题」。不是过拟合,而是网络太难优化了。
ResNet 的解决方案简单而优雅:让网络去学习残差,而不是直接学习原始映射。它引入了一个「跳跃连接」(Skip Connection),把输入直接加到输出上。
公式很简单:输出 = F(x) + x。如果 F(x) 学不到什么东西,至少还有 x 能传过去。这保证了深层网络至少不会比浅层网络差。
3.2.1 残差块的结构
我习惯把残差块分为两种:
- BasicBlock: 两个 3x3 卷积堆叠,适合 ResNet-18/34 等较浅网络。
- Bottleneck: 1x1 -> 3x3 -> 1x1 的结构,先降维再升维,适合 ResNet-50/101/152 等深层网络。1x1 卷积在这里起到「瓶颈」的作用,大幅减少参数量。
# BasicBlock 实现
class BasicBlock(nn.Module):
def __init__(self, in_ch, out_ch, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_ch)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1)
self.bn2 = nn.BatchNorm2d(out_ch)
# 如果维度不匹配,用 1x1 卷积调整
self.shortcut = nn.Sequential()
if stride != 1 or in_ch != out_ch:
self.shortcut = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 1, stride=stride),
nn.BatchNorm2d(out_ch)
)
def forward(self, x):
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x) # 跳跃连接
out = self.relu(out)
return out
3.3 U-Net 架构
U-Net 最初是为医学图像分割设计的,但它在图像恢复任务中表现极其出色。为什么?因为它完美解决了「下采样丢失细节」的问题。
U-Net 的结构像一个 U 形:左边是编码器(下采样),右边是解码器(上采样),中间通过「跳跃连接」把编码器的特征图直接拼到解码器上。这样一来,解码器既能利用高层的语义信息,又能保留底层的细节纹理。
3.3.1 U-Net 在 ISP 中的应用
我在做 RAW 域去噪时,U-Net 几乎是首选。原因有三:
- 多尺度特征: 下采样让网络看到更大的感受野,理解图像全局结构。
- 细节保留: 跳跃连接把浅层的纹理细节直接送到深层,避免细节丢失。
- 端到端训练: 输入噪声 RAW 图,输出干净 RAW 图,一气呵成。
# U-Net 的核心——跳跃连接
class UNet(nn.Module):
def __init__(self):
super().__init__()
# 编码器(下采样路径)
self.enc1 = ConvBlock(3, 64)
self.enc2 = ConvBlock(64, 128)
self.enc3 = ConvBlock(128, 256)
self.enc4 = ConvBlock(256, 512)
# 解码器(上采样路径)
self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2)
self.dec3 = ConvBlock(512, 256) # 注意:输入是上采样+跳跃连接拼接
self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2)
self.dec2 = ConvBlock(256, 128)
self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
self.dec1 = ConvBlock(128, 64)
self.final = nn.Conv2d(64, 3, 1)
def forward(self, x):
# 编码
e1 = self.enc1(x)
e2 = self.enc2(self.pool(e1))
e3 = self.enc3(self.pool(e2))
e4 = self.enc4(self.pool(e3))
# 解码 + 跳跃连接
d3 = self.up3(e4)
d3 = torch.cat([d3, e3], dim=1) # 拼接
d3 = self.dec3(d3)
d2 = self.up2(d3)
d2 = torch.cat([d2, e2], dim=1)
d2 = self.dec2(d2)
d1 = self.up1(d2)
d1 = torch.cat([d1, e1], dim=1)
d1 = self.dec1(d1)
return self.final(d1)
3.4 注意力机制(Attention Mechanism)
注意力机制,说白了就是让网络学会「关注哪里」和「关注什么」。在图像处理中,不是所有像素都同等重要——边缘、纹理区域比平坦区域更需要精细处理。
我最早接触注意力机制是在 2017 年的 SENet 上。它通过一个轻量级的模块,学习每个通道的重要性权重,然后对特征图进行重新标定。效果出奇的好,而且计算量几乎可以忽略。
3.4.1 通道注意力(SE Block)
SENet 的核心是 Squeeze-and-Excitation:先通过全局平均池化把每个通道压缩成一个标量(Squeeze),再通过两个全连接层学习通道间的依赖关系,输出每个通道的权重(Excitation),最后用这个权重去乘原始特征图。
class SEBlock(nn.Module):
def __init__(self, channel, reduction=16):
super().__init__()
self.squeeze = nn.AdaptiveAvgPool2d(1)
self.excitation = nn.Sequential(
nn.Linear(channel, channel // reduction),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.squeeze(x).view(b, c)
y = self.excitation(y).view(b, c, 1, 1)
return x * y # 通道加权
3.4.2 空间注意力
通道注意力关注「是什么」,空间注意力关注「在哪里」。常用的空间注意力机制包括:
- CBAM: 先通道注意力,再空间注意力,串联使用。空间注意力部分用最大池化和平均池化生成两个特征图,拼接后通过卷积得到空间权重。
- Non-local: 计算所有像素之间的相关性,捕捉长距离依赖。计算量较大,但在去噪任务中效果显著。
3.5 本章知识体系总览
下面这张图,我把本章的核心知识点串在了一起。你可以把它当作一张「地图」,随时回来查阅。
3.6 本章小结
这一章的内容,说实话信息量不小。但这些都是后续章节的「砖瓦」,必须打牢。
- CNN 是特征提取器,理解卷积、池化、感受野是基本功。
- ResNet 解决了深度网络的训练难题,跳跃连接的思想贯穿整个深度学习史。
- U-Net 是图像恢复任务的利器,编码器-解码器+跳跃连接的结构至今仍是主流。
- 注意力机制 让网络学会「聚焦」,通道注意力和空间注意力各有侧重。