图像超分辨率模型记录(二)
文章目录
1. SRResNet / SRGAN
论文出处:Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network
1.1 模型介绍
SRGAN本质上是提供了一种新的Loss function — perceptual loss(感知损失),回忆一下之前的SR都是由MSE损失函数来教会网络如何实现LR → \rightarrow → HR,因此在本文中作者就会指出MSE的不足之处:其会对图像的细节进行平滑,使得重建的图像虽然有很高的PSNR,但是失去了人肉眼感知的高分辨率感,即论文中的Photo-Realistic。
该论文重要的提出PSNR高不代表逼真。在以往的图像超分辨率工作中,都是以最小化MSE和最大化峰值信噪比(PSNR)作为常用度量。然而MSE和PSNR捕捉感知相关差异的能力(如高纹理细节)非常有限,因为它们是基于像素级图像差异定义的。SRGAN定义了一种新的感知损失,不再以MSE为唯一最小化目标,因此作者提出了一种新的图像质量评价指标来反映测试对象和真实高分辨率图像的相似程度,它就是mean-opinion-score(MOS)。
1.2 模型代码实现
SRGAN由生成器网络和鉴别器网络组成。生成器的目标是学会足够真实地对图像进行超分辨率,使得经过训练可以识别此类人造来源的迹象的鉴别器无法再可靠地分辨出差异。并且两个网络是同时进行训练的。
生成器 — SRResNet:


此处单独展示生成器的结构,可以将SRResNet单独训练,方便做实验测试。
残差块结构:Conv — BN — PReLU — Conv — BN — 跳层链接(+)
SRResNet一共有16个这样的残差块。SRResNet的整个网络结构为:
Input(LR) — Conv — PReLU — 16个B — Cov — BN — 两个“Conv — PixelShuffler — PReLU”(x4,如果x2、x3就是一个) — Conv — Output(SR)
图中k是卷积核、n是通道数、s是步长。k9n64s1就是out channels=64,kernel size=9, stride=1
模型代码实现 — SRResNet:
class _Residual_Block(nn.Module):
"""残差块(Residual Block)定义
包含两个卷积层,每个卷积层后接实例归一化和LeakyReLU激活
最后通过跳跃连接(skip connection)将输入与输出相加
"""
def __init__(self):
super(_Residual_Block, self).__init__()
# 第一个卷积层:3x3卷积,保持空间维度不变(padding=1)
self.conv1 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1, bias=False)
# 实例归一化层(Instance Normalization),可学习参数
self.in1 = nn.InstanceNorm2d(64, affine=True)
# LeakyReLU激活函数,负斜率为0.2
self.relu = nn.LeakyReLU(0.2, inplace=True)
# 第二个卷积层
self.conv2 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1, bias=False)
self.in2 = nn.InstanceNorm2d(64, affine=True)
def forward(self, x):
# 保存输入作为跳跃连接
identity_data = x
# 第一层卷积 + 归一化 + 激活
output = self.relu(self.in1(self.conv1(x)))
# 第二层卷积 + 归一化(无激活)
output = self.in2(self.conv2(output))
# 跳跃连接:输入与输出相加
output = torch.add(output, identity_data)
return output
class _NetG(nn.Module):
"""生成器网络定义(用于超分辨率任务)
整体架构:
1. 初始卷积层
2. 16个残差块
3. 中间卷积层
4. 上采样模块(4倍放大)
5. 输出卷积层
"""
def __init__(self):
super(_NetG, self).__init__()
# 初始卷积层:将3通道输入转换为64通道特征图
self.conv_input = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=9, stride=1, padding=4, bias=False)
self.relu = nn.LeakyReLU(0.2, inplace=True)
# 由16个残差块组成的层
self.residual = self.make_layer(_Residual_Block, 16)
# 中间卷积层:保持通道数不变
self.conv_mid = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1, bias=False)
self.bn_mid = nn.InstanceNorm2d(64, affine=True)
# 上采样模块:通过两次PixelShuffle实现4倍上采样
self.upscale4x = nn.Sequential(
# 第一次上采样:放大2倍
nn.Conv2d(in_channels=64, out_channels=256, kernel_size=3, stride=1, padding=1, bias=False),
nn.PixelShuffle(2), # 将256通道重组为64通道,空间尺寸×2
nn.LeakyReLU(0.2, inplace=True),
# 第二次上采样:再放大2倍(总共4倍)
nn.Conv2d(in_channels=64, out_channels=256, kernel_size=3, stride=1, padding=1, bias=False),
nn.PixelShuffle(2),
nn.LeakyReLU(0.2, inplace=True),
)
# 输出卷积层:将64通道转换为3通道输出
self.conv_output = nn.Conv2d(in_channels=64, out_channels=3, kernel_size=9, stride=1, padding=4, bias=False)
# 权重初始化:使用He初始化
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
def make_layer(self, block, num_of_layer):
"""创建由多个残差块组成的层"""
layers = []
for _ in range(num_of_layer):
layers.append(block())
return nn.Sequential(*layers)
def forward(self, x):
# 初始卷积 + 激活
out = self.relu(self.conv_input(x))
# 保存残差连接
residual = out
# 通过残差块
out = self.residual(out)
# 中间卷积 + 归一化
out = self.bn_mid(self.conv_mid(out))
# 残差连接:输入与输出相加
out = torch.add(out, residual)
# 上采样4倍
out = self.upscale4x(out)
# 最终输出卷积
out = self.conv_output(out)
return out
SRGAN(整体结构):

上文已经单独描述了生成器的结构,这边不再赘述。此处将实现SRGAN的整体结构。
鉴别器的结构:鉴别网络的结构为:Conv — LeakyReLU — (Conv — BN — LeakyReLU)x7 — AdaptiveAvgPool — Conv — LeakyReLU— Conv — Sigmoid
模型代码实现:
class Generator(nn.Module):
"""SRGAN的生成器网络(基于SRResNet)
用于将低分辨率图像转换为高分辨率图像
参数:
scale_factor: 上采样比例(2, 4或8)
"""
def __init__(self, scale_factor):
# 计算需要的上采样模块数量(log2(scale_factor))
upsample_block_num = int(math.log(scale_factor, 2)) # scale=2→1, scale=4→2, scale=8→3
super(Generator, self).__init__()
# 初始特征提取层
self.block1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=9, padding=4), # 9x9卷积,保持空间维度
nn.PReLU() # 参数化ReLU激活函数
)
# 残差块部分(TODO: 原文使用16个,这里简化为6个)
self.block2 = ResidualBlock(64)
self.block3 = ResidualBlock(64)
self.block4 = ResidualBlock(64)
self.block5 = ResidualBlock(64)
self.block6 = ResidualBlock(64)
# 上采样前的处理层
self.block7 = nn.Sequential(
nn.Conv2d(64, 64, kernel_size=3, padding=1), # 3x3卷积
nn.BatchNorm2d(64) # 批归一化
)
# 上采样模块序列
# 每个UpsampleBLock执行2倍上采样,最后接一个卷积层输出3通道图像
block8 = [UpsampleBLock(64, 2) for _ in range(upsample_block_num)]
block8.append(nn.Conv2d(64, 3, kernel_size=9, padding=4)) # 输出卷积层
self.block8 = nn.Sequential(*block8)
def forward(self, x):
# 前向传播过程
block1 = self.block1(x) # 初始特征提取
block2 = self.block2(block1) # 残差块1
block3 = self.block3(block2) # 残差块2
block4 = self.block4(block3) # 残差块3
block5 = self.block5(block4) # 残差块4
block6 = self.block6(block5) # 残差块5
block7 = self.block7(block6) # 上采样前处理
# 跳跃连接:将初始特征与上采样前特征相加
block8 = self.block8(block1 + block7)
# 输出处理:tanh激活并将范围从[-1,1]映射到[0,1]
# (TODO: 论文中未明确说明使用tanh,但实践中常用)
return (torch.tanh(block8) + 1) / 2
class Discriminator(nn.Module):
"""SRGAN的鉴别器网络
用于判断输入图像是真实高分辨率图像还是生成器生成的图像
采用类似VGG的网络结构
"""
def __init__(self):
super(Discriminator, self).__init__()
# 鉴别器网络结构(序列形式)
self.net = nn.Sequential(
# 第一层:普通卷积+LeakyReLU
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.LeakyReLU(0.2),
# 第二层:下采样卷积+BN+LeakyReLU
nn.Conv2d(64, 64, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.LeakyReLU(0.2),
# 第三层:卷积+BN+LeakyReLU
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2),
# 第四层:下采样卷积+BN+LeakyReLU
nn.Conv2d(128, 128, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2),
# 第五层:卷积+BN+LeakyReLU
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2),
# 第六层:下采样卷积+BN+LeakyReLU
nn.Conv2d(256, 256, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2),
# 第七层:卷积+BN+LeakyReLU
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2),
# 第八层:下采样卷积+BN+LeakyReLU
nn.Conv2d(512, 512, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2),
# 全局平均池化+全连接层
nn.AdaptiveAvgPool2d(1), # 自适应平均池化到1x1
nn.Conv2d(512, 1024, kernel_size=1), # 1x1卷积相当于全连接
nn.LeakyReLU(0.2),
nn.Conv2d(1024, 1, kernel_size=1) # 输出二分类结果
)
def forward(self, x):
batch_size = x.size(0)
# 通过网络并应用sigmoid激活函数输出概率
return torch.sigmoid(self.net(x).view(batch_size)) # 压缩到[0,1]范围
class ResidualBlock(nn.Module):
"""残差块(Residual Block)
包含两个卷积层,每个卷积层后接批归一化和PReLU激活
"""
def __init__(self, channels):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.prelu = nn.PReLU()
self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
def forward(self, x):
# 残差连接:输入与输出相加
residual = self.conv1(x)
residual = self.bn1(residual)
residual = self.prelu(residual)
residual = self.conv2(residual)
residual = self.bn2(residual)
return x + residual
class UpsampleBLock(nn.Module):
"""上采样模块
使用PixelShuffle实现2倍上采样
"""
def __init__(self, in_channels, up_scale):
super(UpsampleBLock, self).__init__()
# 卷积层:输出通道数为in_channels * up_scale^2
self.conv = nn.Conv2d(in_channels, in_channels * up_scale ** 2,
kernel_size=3, padding=1)
self.pixel_shuffle = nn.PixelShuffle(up_scale) # PixelShuffle上采样
self.prelu = nn.PReLU() # 激活函数
def forward(self, x):
x = self.conv(x) # 卷积
x = self.pixel_shuffle(x) # PixelShuffle上采样
x = self.prelu(x) # 激活
return x
1.3 感知损失函数
接下来介绍生成网络的Loss function — 感知损失函数。在之前我们的Loss一般都是MSE,但是正如之前所说的MSE无法很好的恢复图像的细节,故我们改采用感知损失:

我们采用感知损失来训练生成网络,它由内容损失(文中取MSE或者VGG损失)以及一定比率的对抗损失(GAN网络本身就有的损失函数)组成;SRResNet是只由MSE损失函数训练的,而不是感知损失,但是网络还是用的上面的网络;而SRGAN的训练中,生成网络部分才使用感知损失训练的。
2. ESRGAN
论文出处:Enhanced Super-Resolution Generative Adversarial Networks(ESRGAN)
模型改进:
ESRGAN对SRGAN进行了四点改进:
- 生成器网络结构改进:对于生成器G,删除BN层和替换残差块
首先删除了所有的BN层。论文给出了解释:在面向PSNR的任务中,BN层表现很差,因为训练集和测试集的分布不一致,而且凭经验看,在关于GAN的深层网络中,BN会带来一些伪影。去除之后,可以提升性能和降低计算复杂度。

然后把残差块替换为Residual in Residual Dense Block (RRDB),这是残差套残差的一个结构,如图所示。而且使用了一个权重β来缩小dense block,还使用一个较小的初始方差。 - 借鉴了Relativistic GAN的思想进行改进,传统的GAN计算loss时,传统GAN的判别器计算真实数据与1之间的loss,计算生成数据与0之间的loss。这里改为:直接计算真实数据与生成数据之间的loss。

鉴别器损失函数:

生成器损失函数:

- 感性损失
类似于SRGAN里面使用VGG求特征的损失。与之不同的是,本文提出,使用的特征应在VGG的激活层之前。而且又加了一个content loss。所以,总的loss应该分为三个方面:

- 网络插值
先训练一个基于PSNR的网络,再在该网络的基础上finetune一个GAN网络,最后把这两个网络插值。
3. SRDenseNet
论文出处:Image Super-Resolution Using Dense Skip Connections(SRDenseNet)
3.1 模型介绍
论文提出的模型整体结构如下:

论文提出的新模型被称之为SRDenseNet,其利用Dense块作为基本结构,使用skip connection来结合低层特征信息和高层特征信息,然后通过反卷积网络进行图像重建,从而实现 LR → \rightarrow → HR 的转变,并且也说明了不同深度层的特征之间包含的信息是互补的。
DenseNet blocks:

如上图所示,是一个Dense块的内部结构,Dense块各个层之间的skip connection是通过concat在一起的,而Resnet块是通过求和加在一起的。这样的好处在于可以缓解梯度消失的问题以及加强了信息在各个layer之间的流动。从上图中可以看出,为了可以达到concat,每一张feature map都是同样的size。
可以将整个网络拆分来看:

从上图中我们可以看出,后一层的网络是由前一层网络concat过来,再和前一层经过处理的结果合并而来的,因此对于每一层来说只有通道数为16的那一个单元是重新学习的,其余的都是之前层学习得到的,因此我们可以说这个新的单元和旧的单元所学的东西互为补充。
3.2 模型流程
- 首先,输入的低分辨率图像经过第一层CNN提取处于低层的特征信息。
- 然后经过8个Dense块提取高层特征信息。
- 通过skip connection的方式将各个level的特征信息直接和反卷积子网络端相连。
- 中间加了一层Bottleneck layer是为了降低前面特征图concat导致图像张数(通道数)太多而带来的高计算复杂度问题,通过1 × \times × 1卷积层进行缩减。
- 接下来就是反卷积子网络,用于学习出一个上采样滤波器。
- 最后就是网络重建端,就是一个CNN网络,使用3 × \times × 3卷积核以及输出通道为1。
3.3 模型代码实现
SRDenseNet的网络结构为:
Input(LR) — 低级特征层Conv(1,128) — 高级特征层8个DenseBlock(128,… ,1024) —
降维Bottleneck(1152,256) — 2个反卷积层Deconv(256,256) — Reconstruction(256,1) — Output(SR)
模型代码实现:
def get_upsample_filter(size):
"""生成一个2D双线性上采样滤波器核
Args:
size (int): 滤波器的大小(size x size)
Returns:
torch.Tensor: 可用于转置卷积的双线性滤波器核
"""
factor = (size + 1) // 2 # 计算中心位置因子
if size % 2 == 1:
center = factor - 1 # 奇数尺寸的中心位置
else:
center = factor - 0.5 # 偶数尺寸的中心位置
# 生成网格坐标
og = np.ogrid[:size, :size]
# 创建双线性插值核
# 公式:(1 - abs(x - center)/factor) * (1 - abs(y - center)/factor)
filter = (1 - abs(og[0] - center) / factor) * \
(1 - abs(og[1] - center) / factor)
# 转换为PyTorch张量并转为float类型
return torch.from_numpy(filter).float()
class _Dense_Block(nn.Module):
"""密集连接块,包含多个卷积层,每层的输入是前面所有层的输出拼接"""
def __init__(self, channel_in):
super(_Dense_Block, self).__init__()
# 使用PReLU激活函数
self.relu = nn.PReLU()
# 定义8个卷积层,每个卷积层的输出通道数为16
# 每个后续卷积层的输入通道数是前面所有层输出通道数的总和
self.conv1 = nn.Conv2d(in_channels=channel_in, out_channels=16, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
self.conv3 = nn.Conv2d(in_channels=32, out_channels=16, kernel_size=3, stride=1, padding=1) # 32=16(前2层)+16
self.conv4 = nn.Conv2d(in_channels=48, out_channels=16, kernel_size=3, stride=1, padding=1) # 48=前3层+16
self.conv5 = nn.Conv2d(in_channels=64, out_channels=16, kernel_size=3, stride=1, padding=1) # 64=前4层+16
self.conv6 = nn.Conv2d(in_channels=80, out_channels=16, kernel_size=3, stride=1, padding=1) # 80=前5层+16
self.conv7 = nn.Conv2d(in_channels=96, out_channels=16, kernel_size=3, stride=1, padding=1) # 96=前6层+16
self.conv8 = nn.Conv2d(in_channels=112, out_channels=16, kernel_size=3, stride=1, padding=1) # 112=前7层+16
def forward(self, x):
"""前向传播"""
# 第一层卷积
conv1 = self.relu(self.conv1(x))
# 第二层卷积,并将前两层输出拼接
conv2 = self.relu(self.conv2(conv1))
cout2_dense = self.relu(torch.cat([conv1, conv2], 1)) # 沿通道维度拼接
# 第三层卷积,并将前三层输出拼接
conv3 = self.relu(self.conv3(cout2_dense))
cout3_dense = self.relu(torch.cat([conv1, conv2, conv3], 1))
# 第四层卷积,并将前四层输出拼接
conv4 = self.relu(self.conv4(cout3_dense))
cout4_dense = self.relu(torch.cat([conv1, conv2, conv3, conv4], 1))
# 第五层卷积,并将前五层输出拼接
conv5 = self.relu(self.conv5(cout4_dense))
cout5_dense = self.relu(torch.cat([conv1, conv2, conv3, conv4, conv5], 1))
# 第六层卷积,并将前六层输出拼接
conv6 = self.relu(self.conv6(cout5_dense))
cout6_dense = self.relu(torch.cat([conv1, conv2, conv3, conv4, conv5, conv6], 1))
# 第七层卷积,并将前七层输出拼接
conv7 = self.relu(self.conv7(cout6_dense))
cout7_dense = self.relu(torch.cat([conv1, conv2, conv3, conv4, conv5, conv6, conv7], 1))
# 第八层卷积,并将所有八层输出拼接
conv8 = self.relu(self.conv8(cout7_dense))
cout8_dense = self.relu(torch.cat([conv1, conv2, conv3, conv4, conv5, conv6, conv7, conv8], 1))
return cout8_dense
class Net(nn.Module):
"""主网络结构,包含多个密集块和上采样层"""
def __init__(self):
super(Net, self).__init__()
# 使用PReLU激活函数
self.relu = nn.PReLU()
# 低级特征提取层
self.lowlevel = nn.Conv2d(in_channels=1, out_channels=128, kernel_size=3, stride=1, padding=1)
# 瓶颈层,用于减少通道数
self.bottleneck = nn.Conv2d(in_channels=1152, out_channels=256, kernel_size=1, stride=1, padding=0, bias=False)
# 重建层,输出最终图像
self.reconstruction = nn.Conv2d(in_channels=256, out_channels=1, kernel_size=3, stride=1, padding=1, bias=False)
# 8个密集块,每个块的输入通道数逐渐增加
self.denseblock1 = self.make_layer(_Dense_Block, 128)
self.denseblock2 = self.make_layer(_Dense_Block, 256)
self.denseblock3 = self.make_layer(_Dense_Block, 384)
self.denseblock4 = self.make_layer(_Dense_Block, 512)
self.denseblock5 = self.make_layer(_Dense_Block, 640)
self.denseblock6 = self.make_layer(_Dense_Block, 768)
self.denseblock7 = self.make_layer(_Dense_Block, 896)
self.denseblock8 = self.make_layer(_Dense_Block, 1024)
# 上采样部分,包含两个转置卷积层
self.deconv = nn.Sequential(
nn.ConvTranspose2d(in_channels=256, out_channels=256, kernel_size=2, stride=2, padding=0, bias=False),
nn.PReLU(),
nn.ConvTranspose2d(in_channels=256, out_channels=256, kernel_size=2, stride=2, padding=0, bias=False),
nn.PReLU()
)
# 初始化权重
for m in self.modules():
if isinstance(m, nn.Conv2d):
# 使用Kaiming初始化
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
if isinstance(m, nn.ConvTranspose2d):
# 为转置卷积设置双线性上采样滤波器
c1, c2, h, w = m.weight.data.size()
weight = get_upsample_filter(h)
m.weight.data = weight.view(1, 1, h, w).repeat(c1, c2, 1, 1)
if m.bias is not None:
m.bias.data.zero_()
def make_layer(self, block, channel_in):
"""创建由单个密集块组成的层
Args:
block: 密集块类
channel_in: 输入通道数
Returns:
nn.Sequential: 包含单个密集块的序列
"""
layers = []
layers.append(block(channel_in))
return nn.Sequential(*layers)
def forward(self, x):
"""前向传播"""
# 提取低级特征
residual = self.relu(self.lowlevel(x))
# 通过第一个密集块并拼接特征
out = self.denseblock1(residual)
concat = torch.cat([residual, out], 1)
# 通过第二个密集块并拼接特征
out = self.denseblock2(concat)
concat = torch.cat([concat, out], 1)
# 通过第三个密集块并拼接特征
out = self.denseblock3(concat)
concat = torch.cat([concat, out], 1)
# 通过第四个密集块并拼接特征
out = self.denseblock4(concat)
concat = torch.cat([concat, out], 1)
# 通过第五个密集块并拼接特征
out = self.denseblock5(concat)
concat = torch.cat([concat, out], 1)
# 通过第六个密集块并拼接特征
out = self.denseblock6(concat)
concat = torch.cat([concat, out], 1)
# 通过第七个密集块并拼接特征
out = self.denseblock7(concat)
concat = torch.cat([concat, out], 1)
# 通过第八个密集块并拼接所有特征
out = self.denseblock8(concat)
out = torch.cat([concat, out], 1)
# 通过瓶颈层减少通道数
out = self.bottleneck(out)
# 上采样
out = self.deconv(out)
# 重建输出图像
out = self.reconstruction(out)
return out
4. LapSRN
论文出处:Deep Laplacian Pyramid Networks for Fast and Accurate Super-Resolution(LapSRN)
4.1 模型介绍

以拉普拉斯金字塔框架构建的网络。整个模型以LR图像作为输入(而不是LR图像的放大版本),并逐步预测残差图像。上图中红色箭头表示卷积层、蓝色箭头表示转置卷积(上采样)、绿色箭头表示逐元素加法运算符、橙色箭头表示循环层。模型有两个分支:特征提取和图像重建。
特征提取:特征提取分支由d个卷积层和1个转置卷积层组成,每个转置卷积层的输出连接到两个不同的层:用于重构残差图像的卷积层和用于更细的提取特征的卷积层。注意,模型以粗分辨率进行特征提取,只用一个转置卷积层以更细的分辨率生成特征映射与在精细分辨率下执行所有特征提取和重建的现有网络相比,网络设计显著降低了计算复杂度。
图像重建:输入图像使用转置卷积(上采样)层以2的比例上采样。模型使用双线性核初始化这一层,并允许它与所有其他层联合优化。然后将上采样的图像与来自特征提取分支的预测残差图像(使用逐元素求和)相结合,生成高分辨率输出图像。然后将输出级别的HR图像馈送到图像重建分支。
LapSRN的另一个创新是使用了新的损失函数,即Charbonnier。
4.2 模型代码实现
def get_upsample_filter(size):
"""Make a 2D bilinear kernel suitable for upsampling"""
# 计算上采样滤波器的中心位置
factor = (size + 1) // 2
if size % 2 == 1:
center = factor - 1
else:
center = factor - 0.5
# 创建网格坐标
og = np.ogrid[:size, :size]
# 生成双线性插值核
# 公式:(1 - abs(x - center)/factor) * (1 - abs(y - center)/factor)
filter = (1 - abs(og[0] - center) / factor) * \
(1 - abs(og[1] - center) / factor)
# 转换为PyTorch张量并返回
return torch.from_numpy(filter).float()
class _Conv_Block(nn.Module):
def __init__(self):
super(_Conv_Block, self).__init__()
# 定义一个包含多个卷积层和LeakyReLU激活函数的序列
self.cov_block = nn.Sequential(
# 10个连续的卷积层,每个后面跟着LeakyReLU激活
nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# ... (重复8次,共10个卷积层)
nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# 最后是一个转置卷积层,用于上采样
nn.ConvTranspose2d(in_channels=64, out_channels=64, kernel_size=4, stride=2, padding=1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
)
def forward(self, x):
# 前向传播,直接通过序列块
output = self.cov_block(x)
return output
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 输入卷积层,将1通道输入转换为64通道
self.conv_input = nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1, bias=False)
self.relu = nn.LeakyReLU(0.2, inplace=True)
# 第一组上采样组件
self.convt_I1 = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=4, stride=2, padding=1, bias=False) # 直接上采样
self.convt_R1 = nn.Conv2d(in_channels=64, out_channels=1, kernel_size=3, stride=1, padding=1, bias=False) # 从特征图预测残差
self.convt_F1 = self.make_layer(_Conv_Block) # 特征提取块
# 第二组上采样组件
self.convt_I2 = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=4, stride=2, padding=1, bias=False) # 直接上采样
self.convt_R2 = nn.Conv2d(in_channels=64, out_channels=1, kernel_size=3, stride=1, padding=1, bias=False) # 从特征图预测残差
self.convt_F2 = self.make_layer(_Conv_Block) # 特征提取块
# 初始化权重
for m in self.modules():
if isinstance(m, nn.Conv2d):
# 常规卷积层的权重初始化
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
if isinstance(m, nn.ConvTranspose2d):
# 转置卷积层的权重初始化为双线性上采样核
c1, c2, h, w = m.weight.data.size()
weight = get_upsample_filter(h)
m.weight.data = weight.view(1, 1, h, w).repeat(c1, c2, 1, 1)
if m.bias is not None:
m.bias.data.zero_()
def make_layer(self, block):
# 创建由指定块组成的序列层
layers = []
layers.append(block())
return nn.Sequential(*layers)
def forward(self, x):
# 初始处理
out = self.relu(self.conv_input(x))
# 第一级上采样
convt_F1 = self.convt_F1(out) # 特征提取
convt_I1 = self.convt_I1(x) # 直接上采样输入
convt_R1 = self.convt_R1(convt_F1) # 从特征预测残差
HR_2x = convt_I1 + convt_R1 # 结合直接上采样和残差得到2倍超分辨率结果
# 第二级上采样
convt_F2 = self.convt_F2(convt_F1) # 进一步特征提取
convt_I2 = self.convt_I2(HR_2x) # 上采样2倍结果
convt_R2 = self.convt_R2(convt_F2) # 从特征预测残差
HR_4x = convt_I2 + convt_R2 # 结合得到4倍超分辨率结果
return HR_2x, HR_4x # 返回2倍和4倍超分辨率结果
5. RDN
论文出处:Residual Dense Network for Image Super-Resolution(RDN)
部分内容转载于博主 Ton10 博文:超分之RDN
5.1 模型介绍


这篇文章提出了一种结合 ResNet 结构和 DenseNet 结构的深度超分网络 —— Residual Dense Network(后文简称RDN)。RDN基于Residual Dense Block(后文简称RDB)块以及全局残差连接来提取全局特征,而RDB块基于Dense结构和局部残差连接进一步提取局部特征。通过这种结构,作者最大化利用了LR不同层级的特征,在当时取得了SOTA的表现力。具体介绍参考Ton10博文:超分之RDN
5.2 模型代码实现

RDB块实现的代码如下:
# Dense层结构:Conv+ReLU
class DenseLayer(nn.Module):
def __init__(self, in_channels, out_channels):
"""
初始化一个密集连接层(DenseLayer)
参数:
in_channels: 输入特征图的通道数
out_channels: 输出特征图的通道数(即增长率)
"""
super(DenseLayer, self).__init__()
# 3x3卷积层,保持空间分辨率不变(通过padding=1)
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=3 // 2)
# ReLU激活函数,inplace=True表示直接修改输入而不创建新副本,节省内存
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
"""
前向传播
参数:
x: 输入特征图,形状为[batch, in_channels, height, width]
返回:
拼接后的特征图,形状为[batch, in_channels+out_channels, height, width]
"""
# 将原始输入x与卷积+激活后的结果在通道维度上拼接
return torch.cat([x, self.relu(self.conv(x))], 1) # 1表示通道维度(dim=1)
# RDB实现(Residual Dense Block),growth_rate是通道增长率
class RDB(nn.Module):
def __init__(self, in_channels, growth_rate, num_layers):
"""
初始化一个残差密集块(RDB)
参数:
in_channels: 输入特征图的通道数
growth_rate: 每个DenseLayer的输出通道数(增长率)
num_layers: 块内包含的DenseLayer数量
"""
super(RDB, self).__init__()
# 创建num_layers个DenseLayer,每个层的输入通道数会动态增长
# 第i个层的输入通道数为:in_channels + growth_rate * i
self.layers = nn.Sequential(
*[DenseLayer(in_channels + growth_rate * i, growth_rate)
for i in range(num_layers)]
)
# 局部特征融合(Local Feature Fusion)
# 1x1卷积,将所有DenseLayer的输出(in_channels + growth_rate*num_layers)
# 融合为growth_rate个通道
self.lff = nn.Conv2d(
in_channels + growth_rate * num_layers,
growth_rate,
kernel_size=1
)
def forward(self, x):
"""
前向传播
参数:
x: 输入特征图,形状为[batch, in_channels, height, width]
返回:
残差连接后的输出,形状为[batch, growth_rate, height, width]
"""
# 先通过所有DenseLayer处理
# 然后通过1x1卷积进行特征融合
# 最后与原始输入x相加(局部残差连接)
return x + self.lff(self.layers(x)) # local residual learning
RDN整体网络实现代码:
class RDN(nn.Module):
def __init__(self, scale_factor, num_channels, num_features, growth_rate, num_blocks, num_layers):
"""
初始化残差密集网络(RDN)
参数:
scale_factor: 上采样比例(2、3或4)
num_channels: 输入图像的通道数(如RGB图像为3)
num_features: 基础特征通道数(G0)
growth_rate: 每个RDB块的通道增长率(G)
num_blocks: RDB块的数量(D)
num_layers: 每个RDB块中的DenseLayer数量(C)
"""
super(RDN, self).__init__()
self.G0 = num_features # 基础特征通道数
self.G = growth_rate # 通道增长率
self.D = num_blocks # RDB块数量
self.C = num_layers # 每个RDB块中的层数
# 浅层特征提取部分
# 第一层卷积:从输入通道数提取到基础特征通道数
self.sfe1 = nn.Conv2d(num_channels, num_features, kernel_size=3, padding=3 // 2)
# 第二层卷积:保持特征通道数不变
self.sfe2 = nn.Conv2d(num_features, num_features, kernel_size=3, padding=3 // 2)
# 构建残差密集块(RDB)
# 第一个RDB块接收G0通道输入
self.rdbs = nn.ModuleList([RDB(self.G0, self.G, self.C)])
# 后续RDB块接收G通道输入(因为前一个RDB的输出是G通道)
for _ in range(self.D - 1):
self.rdbs.append(RDB(self.G, self.G, self.C))
# 全局特征融合(GFF)
# 首先用1x1卷积将所有RDB的输出(D*G通道)压缩到G0通道
# 然后用3x3卷积进一步处理特征
self.gff = nn.Sequential(
nn.Conv2d(self.G * self.D, self.G0, kernel_size=1),
nn.Conv2d(self.G0, self.G0, kernel_size=3, padding=3 // 2)
)
# 上采样部分
assert 2 <= scale_factor <= 4, "Scale factor must be 2, 3, or 4"
if scale_factor == 2 or scale_factor == 4:
# 对于2倍或4倍上采样,分多次进行
self.upscale = []
for _ in range(scale_factor // 2):
# 每次上采样先通过卷积增加通道数(上采样因子平方倍)
self.upscale.extend([
nn.Conv2d(self.G0, self.G0 * (2 ** 2), kernel_size=3, padding=3 // 2),
# 然后使用PixelShuffle进行2倍上采样
nn.PixelShuffle(2)
])
self.upscale = nn.Sequential(*self.upscale)
else:
# 对于3倍上采样,直接进行
self.upscale = nn.Sequential(
nn.Conv2d(self.G0, self.G0 * (scale_factor ** 2), kernel_size=3, padding=3 // 2),
nn.PixelShuffle(scale_factor)
)
# 输出层:将特征映射回原始通道数
self.output = nn.Conv2d(self.G0, num_channels, kernel_size=3, padding=3 // 2)
def forward(self, x):
"""
前向传播
参数:
x: 输入图像,形状为[batch, num_channels, height, width]
返回:
上采样后的图像,形状为[batch, num_channels, height*scale_factor, width*scale_factor]
"""
# 浅层特征提取
sfe1 = self.sfe1(x) # 第一层特征
sfe2 = self.sfe2(sfe1) # 第二层特征
# 通过所有RDB块
x = sfe2
local_features = []
for i in range(self.D):
x = self.rdbs[i](x) # 通过第i个RDB块
local_features.append(x) # 保存每个RDB的输出用于全局特征融合
# 全局特征融合
# 将所有RDB的输出在通道维度上拼接
# 然后通过全局特征融合模块处理
# 最后与浅层特征sfe1相加(全局残差连接)
x = self.gff(torch.cat(local_features, 1)) + sfe1
# 上采样
x = self.upscale(x)
# 输出层
x = self.output(x)
return x
更多推荐


所有评论(0)