前言:
网上关于使用pytorch复现SRCNN的文章和代码已经多如牛毛,为什么我还要写这篇文章呢?
这是因为在我一开始学习超分辨率网络时,发现网上的代码并没有严格按照论文中的表述进行复现,对数据的处理和评价指标PSNR的计算也没有与论文达到一致。
这些问题导致网络的输出结果与论文无法在相同标准下比较,带来了很大的麻烦。例如,因为python和matlab的插值算法等与论文不同,导致PSNR的数值与论文的数值不在一个baseline。这些问题导致复现的结果无法直接与论文结果比较,从而无法确认我们是否真的正确复现了论文,这对学习超分辨率网络是有害的。如果一开始的路就歪了,那之后想要纠正就会付出更大代价。
以上种种,促成了这篇文章(可能之后会有一个系列)的诞生。本文 不会详细介绍网络的原理 ,文章的目的主要是记录SRCNN的复现过程,同时将复现时一些容易出现错误的细节问题进行总结,做到 尽量正确还原论文结果 。如有错误,欢迎指正!
论文:SRCNN官方网站(含论文和caffe代码)
我的代码:Pytorch复现SRCNN
转载请附加原文链接。
一、网络的结构
SRCNN的网络结构很简单,只有3层卷积网络。前两层使用ReLU激活函数。
一些表示的说明:
因此论文中的SRCNN网络可表示为: Conv(1,64,9)-ReLU-Conv(64,32,1)-ReLU-Conv(32,1,5)
Pytorch中的实现方法:
class
SRCNN
(
nn
.
Module
):
def
__init__
(
self
,
padding
=
False
,
num_channels
=
1
):
super
(
SRCNN
,
self
)
.
__init__
()
self
.
conv1
=
nn
.
Sequential
(
nn
.
Conv2d
(
num_channels
,
64
,
kernel_size
=
9
,
padding
=
4
*
int
(
padding
),
padding_mode
=
'replicate'
),
nn
.
ReLU
(
inplace
=
True
))
self
.
conv2
=
nn
.
Sequential
(
nn
.
Conv2d
(
64
,
32
,
kernel_size
=
1
,
padding
=
0
),
# n1 * 1 * 1 * n2
nn
.
ReLU
(
inplace
=
True
))
self
.
conv3
=
nn
.
Conv2d
(
32
,
num_channels
,
kernel_size
=
5
,
padding
=
2
*
int
(
padding
),
padding_mode
=
'replicate'
)
def
forward
(
self
,
x
):
x
=
self
.
conv1
(
x
)
x
=
self
.
conv2
(
x
)
x
=
self
.
conv3
(
x
)
return
x
def
init_weights
(
self
):
for
L
in
self
.
conv1
:
if
isinstance
(
L
,
nn
.
Conv2d
):
L
.
weight
.
data
.
normal_
(
mean
=
0.0
,
std
=
0.001
)
L
.
bias
.
data
.
zero_
()
for
L
in
self
.
conv2
:
if
isinstance
(
L
,
nn
.
Conv2d
):
L
.
weight
.
data
.
normal_
(
mean
=
0.0
,
std
=
0.001
)
L
.
bias
.
data
.
zero_
()
self
.
conv3
.
weight
.
data
.
normal_
(
mean
=
0.0
,
std
=
0.001
)
self
.
conv3
.
bias
.
data
.
zero_
()
细节一: 论文中,网络在训练时卷积层不进行padding,在测试时进行padding,padding的像素数量为每边填充 \lfloor f/2 \rfloor ,padding方式为 ‘replicate’ ,如果采用零填充会导致输出的图片四周有边框(boarder effect)。
细节二: 所有卷积层的权重weight使用均值为零,标准差为0.001的正态分布初始化,偏置bias为零。
二、数据集的准备
训练集: SRCNN的训练集使用91-images。
1、数据增广(augment):为了提高模型的泛化能力,对数据集进行了增广。方法是:将原始图片先进行 0°、90°、180°、270° 旋转,然后缩放 0.6、0.7、0.8、0.9、1.0 ,得到20倍于91-images的数据集,共1820张HR图片。
2、准备LR-HR数据对:
在经过上述处理后最后得到276864对数据对,即每个epoch处理276864对。
细节一: 由于python的PIL库中的‘bicubic’插值与matlab的‘bicubic’不同,因此需要在python中实现matlab的‘bicubic’插值。本人在网上找到了一个python实现matlab中imresize函数的库,经过验证与matlab得到的结果一致。
细节二: 同样的,matlab中rgb2ycbcr的函数也略有不同,主要是转换矩阵的值和取整上的差别。以下为python实现的相同效果的转换函数:
# https://en.wikipedia.org/wiki/YCbCr
m
=
np
.
array
([[
65.481
,
128.553
,
24.966
],
[
-
37.797
,
-
74.203
,
112.0
],
[
112
,
-
93.786
,
-
18.214
]])
def
rgb2ycbcr
(
rgb
):
shape
=
rgb
.
shape
if
len
(
shape
)
==
3
:
rgb
=
rgb
.
reshape
((
shape
[
0
]
*
shape
[
1
],
3
))
ycbcr
=
np
.
dot
(
rgb
,
m
.
transpose
()
/
255.
)
ycbcr
[:,
0
]
+=
16.
ycbcr
[:,
1
:]
+=
128.
ycbcr
=
np
.
round
(
ycbcr
)
return
ycbcr
.
reshape
(
shape
)
def
ycbcr2rgb
(
ycbcr
):
shape
=
ycbcr
.
shape
if
len
(
shape
)
==
3
:
ycbcr
=
ycbcr
.
reshape
((
shape
[
0
]
*
shape
[
1
],
3
))
rgb
=
copy
.
deepcopy
(
ycbcr
)
rgb
[:,
0
]
-=
16.
rgb
[:,
1
:]
-=
128.
rgb
=
np
.
dot
(
rgb
,
np
.
linalg
.
inv
(
m
.
transpose
())
*
255.
)
rgb
=
np
.
round
(
rgb
)
return
rgb
.
clip
(
0
,
255
)
.
reshape
(
shape
)
细节三: 转换为YCbCr时需要注意,如果图片本身就是灰度图,只有一个通道,就不需要再转化,直接将该通道作为Y通道输入即可。
验证集: 验证集使用Set5。
与训练集的处理相同,但是可以不用裁剪sub-images,直接将完整的图片作为输入,这样验证的结果更接近实际的测试结果。
测试集: 测试集使用Set5、Set14。测试集的处理将在第四节介绍。
三、模型训练设置
训练时一些详细的参数论文中并没有提及,因此为了能够加速训练结果收敛,一些设置是我自己的经验,文中使用括号标注。
训练的设置如下:
四、测试PSNR
PSNR的计算公式如下:
PSNR=10*log_{10}(\frac{MAX^2}{MSE})=20*log_{10}(\frac{MAX}{\sqrt{MSE}})
其中的 MAX 为像素的取值范围的最大值,如果图像的像素为 [0,1] ,则MAX=1 ;如果为 [0,255] ,则MAX=255 。
在测试时计算PSNR,按照「江湖规矩」,需要将原始的高清图片 hr 的四个边缘各裁剪scale个像素。
由于在测试时会进行padding,因此输出的 sr 与 hr 同尺寸,所以为了计算PSNR,也要对 sr 进行同样的处理。
由于输入网络的是Y通道,因此若要得到完整的图片,需要将输出的Y通道与输入前的CbCr组合成完整的YCbCr图像,然后转换为RGB。若为灰度图则无需处理。
五、最终结果
以下结果均为上述设置下,训练 6000 个 epoch 得到的结果。
在Set5、Set14上测试PSNR:
图片结果: