好久不见, 最近搞了一会与超分辨率相关的机器学习的东西, 所以这次是这几天简单用Pytorch复现的超分辨率论文和一点笔记. 这里我复现的几篇文章顺序都是按照知乎帖子 [从SRCNN到EDSR,总结深度学习端到端超分辨率方法发展历程]https://zhuanlan.zhihu.com/p/31664818 整理而来(文末点击原文可以跳转), 由于最近研究的就是这方面的东西, 因此接下来还会继续复现一些新的超分辨率论文, 攒够一波就发出来. 图形学的文章背地里我也有在写, 但是还没准备好因此还没发出来, 见谅.
才疏学浅, 错漏在所难免, 如果我的复现中有对论文的理解问题希望大家在留言处指出. 这些复现的代码风格各自有些不同, 这是因为我本身也是借此机会在学习Pytorch, 因此故意用不同的写法编写, 见谅.
全文3.5k字, 篇幅不长, 不难的. 本文同步存于我的Github仓库(https://github.com/ZFhuang/Study-Notes/tree/main/Content/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/%E8%B6%85%E5%88%86%E8%BE%A8%E7%8E%87%E5%AE%9E%E8%B7%B5)
Learning a Deep Convolutional Network for Image Super-Resolution
SRCNN
作为最早的超分辨率神经网络, 结构很简单, 就是三个卷积层, 两个激活层的组合, 效果自然也不敢恭维
class SRCNN(nn.Module):
def __init__(self):
super(SRCNN, self).__init__()
# 输出大小计算: O=(I-K+2P)/S+1
# 三层的大小都是不变的, 通道数在改变
# 原文没有使用padding因此图片会变小, 这里使用了padding
self.conv1=nn.Conv2d(1,64,9, padding=4)
self.conv2=nn.Conv2d(64,32,1, padding=0)
self.conv3=nn.Conv2d(32,1,5, padding=2)
def forward(self, img):
# 三层的学习率不同
# 两个激活层
img=torch.relu(self.conv1(img))
img=torch.relu(self.conv2(img))
# 注意最后一层不要激活
return self.conv3(img)
Accelerating the Super-Resolution Convolutional Neural Network
FSRCNN
从上面论文中的对比图可以发现其与SRCNN最大的区别就是结尾使用的反卷积层, 反卷积让我们可以直接用没有插值的低分辨率图片进行超分辨率学习, 从而减少超分辨途中的参数数量, 加快网络效率. 并且使用了PReLU作为激活层, 使得激活层本身也可以被学习来提高网络效果
class FSRCNN(nn.Module):
def __init__(self,d,s,m,ratio=2):
super(FSRCNN, self).__init__()
feature_extraction=nn.Conv2d(1,d,5, padding=2)
shrinking=nn.Conv2d(d,s,1)
seq=[]
for i in range(m):
seq.append(nn.Conv2d(s,s,3,padding=1))
non_linear=nn.Sequential(*seq)
expanding=nn.Conv2d(s,d,1,padding=0)
# 反卷积尺寸计算 O=(I-1)×s+k-2P
deconvolution=nn.ConvTranspose2d(d,1,9,stride=ratio,padding=4)
self.body=nn.Sequential(
feature_extraction,
nn.PReLU(),
shrinking,
nn.PReLU(),
non_linear,
nn.PReLU(),
expanding,
nn.PReLU(),
deconvolution
)
def forward(self, img):
return self.body(img)
Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network
ESPCN
核心的优化在于最后一层的亚像素卷积过程, 其思想就是将卷积得到的多通道低分辨率图的像素按照周期排列得到高分辨率的图片, 这样训练出能够共同作用来增强分辨率的多个滤波器. 借用 [一边Upsample一边Convolve:Efficient Sub-pixel-convolutional-layers详解] https://oldpan.me/archives/upsample-convolve-efficient-sub-pixel-convolutional-layers 的示意图可以更好理解亚像素卷积的过程.
亚像素卷积
class ESPCN(nn.Module):
def __init__(self, ratio=2):
super(ESPCN, self).__init__()
self.add_module('n1 conv', nn.Conv2d(1,64,5,padding=2))
self.add_module('tanh 1',nn.Tanh())
self.add_module('n2 conv', nn.Conv2d(64,32,3,padding=1))
self.add_module('tanh 2',nn.Tanh())
self.add_module('n3 conv', nn.Conv2d(32,1*ratio*ratio,3,padding=1))
# 亚像素卷积层
self.add_module('pixel shuf',nn.PixelShuffle(ratio))
def forward(self, img):
for module in self._modules.values():
img = module(img)
return img
Accurate Image Super-Resolution Using Very Deep Convolutional Networks
VDSR
使用大量3*3的 卷积-激活块 进行串联, 用padding保证输入输出的尺寸, 整体用一个残差来优化训练. 网络的目标是得到的残差尽可能接近HR-LR, 用MSE作为loss训练.
class VDSR(nn.Module):
def __init__(self):
super(VDSR, self).__init__()
self.body=VDSR_Block()
def forward(self, img):
img=self.body(img)
for i in range(img.shape[0]):
# 由于Relu的存在, 得到的残差需要移动平均来应用
img[i,0,:,:]-=torch.mean(img[i,0,:,:])
# 由于网络只有一个残差块, 所以把残差的相加写到了loss计算中
return img
class VDSR_Block(nn.Module):
def __init__(self):
super(VDSR_Block, self).__init__()
self.inp=nn.Conv2d(1,64,3,bias=False,padding=1)
seq=[]
# 20层卷积
for j in range(20):
seq.append(nn.Conv2d(64,64,3,padding=1))
seq.append(nn.ReLU(True))
self.conv=nn.Sequential(*seq)
self.out=nn.Conv2d(64,1,3,padding=1)
def forward(self, img):
img=torch.relu(self.inp(img))
img=self.conv(img)
img=self.out(img)
return img
Deeply-Recursive Convolutional Network for Image Super-Resolution
DRCN
DRCN的亮点就在于中间的递归结构, 其使得每层都按照相同的参数进行了一次处理, 得到的残差通过跳接层相加得到一份结果, 然后所有级数的结果加权合在一起得到最终图像. 由于中间的递归结构每层使用的滤波都是相同的参数, 因此网络的训练难度低了很多, 训练比较高效而且效果也很不错.
class DRCN(nn.Module):
def __init__(self, recur_time=16):
super(DRCN, self).__init__()
self.recur_time = recur_time
self.Embedding = nn.Sequential(
nn.Conv2d(1, 256, 3, padding=1),
nn.ReLU(True)
)
self.Inference = nn.Sequential(
nn.Conv2d(256, 256, 3, padding=1),
nn.ReLU(True)
)
self.Reconstruction = nn.Sequential(
nn.Conv2d(256, 1, 3, padding=1),
nn.ReLU(True)
)
self.WeightSum = nn.Conv2d(recur_time, 1, 1)
def forward(self, img):
skip = img
img = self.Embedding(img)
output = torch.empty(
(img.shape[0], self.recur_time, img.shape[2], img.shape[3]),device='cuda')
# 残差连接, 权值共享
for i in range(self.recur_time):
img = self.Inference(img)
output[:, i, :, :] = (skip+self.Reconstruction(img)).squeeze(1)
# 加权合并
output = self.WeightSum(output)
return output
Image Restoration Using Convolutional Auto-encoders with Symmetric Skip Connections
RED
可以看作FSRCNN和VDSR的结合体, 网络自身是卷积和反卷积组合成的对称结构, 每step层就进行一次残差连接, 通过这样反复的特征提取以期望得到质量更高的低分辨率图, 最后用一个反卷积恢复大小. 对称的特征提取组合有学习意义, 可惜最后的反卷积层过于粗暴使得效果不佳
class RED(nn.Module):
def __init__(self,ratio=2, num_feature=32, num_con_decon_mod=5, filter_size=3, skip_step=2):
super(RED, self).__init__()
self.num_con_decon_mod = num_con_decon_mod
self.skip_step = skip_step
self.input_conv = nn.Sequential(
nn.Conv2d(1, num_feature, 3, padding=1),
nn.ReLU(True)
)
# 提取特征, 要保持大小不变
conv_seq = []
for i in range(0, num_con_decon_mod):
conv_seq.append(nn.Sequential(
nn.Conv2d(num_feature, num_feature,
filter_size, padding=filter_size//2),
nn.ReLU(True)
))
self.convs= nn.Sequential(*conv_seq)
# 反卷积返还特征, 要保持大小不变
deconv_seq = []
for i in range(0, num_con_decon_mod):
deconv_seq.append(nn.Sequential(
nn.ConvTranspose2d(num_feature, num_feature, filter_size,padding=filter_size//2),
nn.ReLU(True)
))
self.deconvs=nn.Sequential(*deconv_seq)
# 真正的放大步骤
self.output_conv = nn.ConvTranspose2d(num_feature, 1, 3,stride=ratio,padding=filter_size//2)
def forward(self, img):
img = self.input_conv(img)
skips = []
# 对称残差连接
for i in range(0, self.num_con_decon_mod):
if i%self.skip_step==0:
skips.append(img)
img = self.convs[i](img)
for i in range(0, self.num_con_decon_mod):
img = self.deconvs[i](img)
if i%self.skip_step==0:
img=img+skips.pop()
# 测试中这里不激活效果更好
# img=torch.relu(img+skips.pop())
img=self.output_conv(img)
return img