目录
1 文件下载
代码: https://github.com/KupynOrest/DeblurGAN
数据集:
- blurred_sharp(需翻墙): https://drive.google.com/uc?id=1CPMBmRj-jBDO2ax4CxkBs9iczIFrs8VA&export=download
- blurred_sharp: https://pan.baidu.com/s/1-Pg8p71zz1Ilu4PQ_IbgMw
提取码:m66c - GOPRO_Large: https://pan.baidu.com/s/13dR1wnsqeoAb5cjTaMhD2w
提取码:3r8c - K o ¨ \ddot{o} o¨hler(根据需要自行下载或测试): http://webdav.is.mpg.de/pixel/benchmark4camerashake/
2 论文解读
2.1 主要贡献
- 提出DeblurGAN, 相比之前的去模糊算法在SSIM,视觉效果和时间上有了明显提高(作者认为PSNR指标可能有些缺陷,所以他们的PSNR不那么突出也是合理的)。
在GoPro数据集上峰值信噪比、结构相似度以及运行时间与之前一些方法的对比(表源于原文):
在GoPro数据集上视觉效果与之前一些方法的对比(图源于原文):
(左: 模糊图片, 中:Nah et al.1, 右:DeblurGAN)
- 提出一种从清晰图片生成合成运动模糊图片的方法,可以用来扩充一些真实运动模糊图片数据集。
2.2 网络结构
2.2.1 总体结构:(图源于原文)
作者用cGAN来设计整个网络(PS:作者说是用cGAN,但又在文中说“we do not need to penalize mismatch between the input and output.”,这不是相当于用的就是一个普通的GAN吗?而且在代码中也一点没读出来cGAN的意思,WGAN还差不多)。
Generator接受模糊图像 (blurred image)输入,产生重构的清晰图像(restored images);
Discriminator(文中称“critic network”)接受Generator产生的restored images和原始的清晰图像(sharp images),产生的是特定尺寸的特征图,并非是一个标量(如果我没搞错的话,特征图尺寸:(c=1, h=35, w=35))。
2.2.2 Generator:(图源于原文)
生成器的结构信息很详细地展现在图中了,大致是【3个卷积块+9个残差块+2个转置卷积块+1个卷积块】。关于生成器,有以下几点需要注意:
- 正则化方式全部采用的是instance normalization,没有用最常用的batch normalization,作者并未在文中说明为什么选这个;
- 在每一个残差块的第一个卷积层后面都以0.5的概率加了一个Dropout层,这个笔者也没搞懂为啥,Dropout层通常是应用在全连接层后面,用来防止过拟合的,参数量不大的卷积层好像并不适用,Srivastava/Hinton虽然在dropout文章中写过,在低层卷积层增加dropout可以获得额外的性能增益,因为它为较高的全连接层提供了噪声输入,但DeblurGAN中:①没有用到全连接层;②添加dropout的卷积层数不低;
- 除了残差块中的skip connection之外,作者引入了一个global skip connection,也就是图中的那条大黑线,作者说这样可以让训练更快并且得到的结果更好,因为文章中没有消融实验,也没办法证明这个的有效性。
2.2.3 Discriminator:
文中并未有Discriminator的结构图,所以我把Discriminator用黑框框打印了出来,见下图:
Discriminator正则化方式也全部采用的是instance normalization,设计地较中规中矩,没有什么可说的。
2.3 Loss设计
-
Adversarial loss
L G A N = ∑ n = 1 N − D θ D ( G θ G ( I B ) ) \mathcal{L}_{GAN}= \sum_{n=1}^N-D_{\theta_D}(G_{\theta_G}(I^B)) LGAN=n=1∑N−DθD(GθG(IB))
式中: I B I^B IB代表模糊图像, D θ D D_{\theta_D} DθD代表Discriminator,G_{\theta_D}代表Generator。
至于对抗损失为啥是这样子,笔者没咋明白。作者提供的代码中,写了三种GAN的实现:vanilla,lsgan,wgan-gp,wgan-gp的代码实现也确实是按照原来的wgan-gp来设计的,看不懂这个式子没关系,看得懂代码就行了。 -
Content loss
L X = 1 W i , j H i , j ∑ x = 1 W i , j ∑ y = 1 H i , j ( ϕ i , j ( I S ) x , y − ϕ i , j ( G θ G ( I B ) ) x , y ) 2 \mathcal{L}_{X}= \frac{1}{W_{i,j}H_{i,j}}\sum_{x=1}^{W_{i,j}}\sum_{y=1}^{H_{i,j}}(\phi_{i,j}(I^S)_{x,y}-\phi_{i,j}(G_{\theta_G}(I^B))_{x,y})^2 LX=Wi,jHi,j1x=1∑Wi,jy=1∑Hi,j(ϕi,j(IS)x,y−ϕi,j(GθG(IB))x,y)2
式中: ϕ i , j \phi_{i,j} ϕi,j代表预训练的VGG19的i块j层输出的特征图, I S I^S IS代表sharp image, I B I^B IB代表blurred image, G θ G G_{\theta_G} GθG代表Generator。
式子看起来特别长,有点唬人,其实和下面的式子是等价的:
L X = M S E ( V G G i , j ( I S ) − V G G i , j ( G θ G ( I B ) ) ) \mathcal{L}_{X}= MSE(VGG_{i,j}(I^S)-VGG_{i,j}(G_{\theta_G}(I^B))) LX=MSE(VGGi,j(IS)−VGGi,j(GθG(IB)))
式中:MSE就是均方误差, V G G i , j VGG_{i,j} VGGi,j代表预训练的VGG19的i块j层输出的特征图。
是不是一目了然?就是将sharp image和restored images传入预训练的VGG19,分别取i块j层的特征图,然后计算一下MSE。 -
total loss
L = L G A N + λ ⋅ L X \mathcal{L}=\mathcal{L}_{GAN}+\lambda·\mathcal{L}_{X} L=LGAN+λ⋅LX
式中: λ \lambda λ就是一个权重,文中取100。
一共就一个对抗损失和一个内容损失(感知损失),loss的设计还是挺简单的。
2.4 生成合成运动模糊图片
(PS:此小节基本上是文中完全独立的一个部分,笔者也并未仔细看。)
成对的模糊数据集是难以获取的,即便有现有的成对模糊数据集,想要扩充也并非易事。在本文之前,Sun et al.2 ,Xu et al.3 都已经使用过线性运动模糊核来对清晰图像进行卷积,从而得到合成的模糊图像。即:
b
l
u
r
r
e
d
_
i
m
a
g
e
=
s
h
a
r
p
_
i
m
a
g
e
∗
k
e
r
n
e
l
+
n
o
i
s
e
blurred\_image = sharp\_image * kernel + noise
blurred_image=sharp_image∗kernel+noise
式中: “
∗
*
∗” 代表卷积
举个简单的产生运动模糊栗子(这里不带噪音):
图片来源: https://blog.csdn.net/xueruhongchen/article/details/52783119?locationNum=4.
文中作者认为他们的方法可以模拟更真实、更复杂的模糊核,模糊核轨迹的生成遵循马尔可夫过程。下图是文中模拟模糊核具体的算法:
下图是用作者的随机化方法生成的模糊核(底行)与Fergus et al.4 用真实图片估计出的模糊核(顶行)的一个对比:(图源于原文)
有了模糊核之后,便可以由一张清晰图片产生与之对应的模糊图片,也就相当于给训练提供了标签。作者在三种数据集上做了训练:
①:直接由GoPro训练集中取了1000张成对图片;
②:从coco数据集中取了一些图片,然后用别人的方法产生模糊图片;
③:以2:1的比例分别取了用作者提出方法合成的图片与GoPro中的图片作为组合。
结果显示第三种情况得到的性能更好。
3 代码调试
3.1 数据集组织形式
因为代码是固定的,所以如果不改代码的话,数据集必须按照作者代码中写的来组织,不然代码跑不通。数据集的组织形式如下(以GoPro为例):
其中图中的“train”和“test”不可更改为其他名称,其他的可自行更改名称,只要和自己的命令对应上就可以。
3.2 获取图片对
作者在gitbub上说如果想训练自己的数据集,必须先创建图片对,作为训练的数据,即将一张模糊图片和一张清晰图片在宽度维度拼接得到拼接后的图片,就像下图这样:
下面的代码是笔者组合模糊和清晰图片对应于3.1数据集组织形式的命令,GoPro/train/combine是组合后的图片的存储路径,combine文件夹也需要放在…/train/路径下面,组合成功后combine会包含n个子文件夹,每个子文件夹又会包含m张图片,和blur,sharp文件夹一一对应。
python combine_A_and_B.py --fold_A GoPro/train/blur --fold_B GoPro/train/sharp --fold_AB GoPro/train/combine
如果sharp下图片的名称为XXX_A.xxx,以及blur下图片的名称为XXX_B.xxx的话,并且你只想训练这些带A和B的图片,便可以使use_AB=True,笔者sharp和blur中的图片名称中都不带有A和B,就让use_AB为false,即在combine_A_and_B.py中添加args.use_AB = False。不管怎样,只要sharp和blur下的文件夹以及图片一一对应,加上args.use_AB = False肯定不会报错,不加可能会出错。
3.3 Train
在训练之前,作者提供的代码中还有一些需要改动和做的地方,如下:
- aligned_dataset.py 中:(combine根据自己定义的文件夹名称更改。)
将 self.dir_AB = os.path.join(opt.dataroot, opt.phase)
改为 self.dir_AB = os.path.join(opt.dataroot, opt.phase, 'combine')
- train.py 中:
删掉 opt.dataroot = 'D:\Photos\TrainingData\BlurredSharp\combined'
修改 opt.gan_type = "wgan-gp",原代码中用的是gan,但作者论文中花了大量篇幅讲wgan-gp,最后用个普通的gan算怎么回事,笔者就改成了wgan-gp。
添加 opt.nThreads = 6,按照自己的CPU核心数改。
- 开启visdom服务,训练过程需要一直开启,否则控制台会一直提示连接失败,终端输入以下命令开启:
python -m visdom.server
做完以上就可以进行训练了:
python train.py --dataroot GoPro --learn_residual --resize_or_crop crop --fineSize 256
这里只传入根目录就OK啦!笔者用的是1个Nvidia RTX2080 Ti,6个CPU核心。笔者在训练过程中没有看到可视化的训练过程,因为笔者用的服务器有些限制,访问不了visdom的默认地址:http://127.0.0.1:8097,事实上应该是可以可视化训练过程的。
3.4 Test
在测试之前,也有些要做的工作:
- test.py 中:
将 from ssim import SSIM
改为 util.metrics import SSIM
- metrics.py 中:如果想观察PSNR和SSIM定量指标,需要把test.py中关于PSNR和SSIM的注释去掉,但作者的SSIM好像有些问题,笔者给改了下,下面是笔者的PSNR和SSIM代码:
import torch
import torch.nn.functional as F
from torch.autograd import Variable
import cv2
import numpy as np
from math import exp
import math
# PSNR
def PSNR(img1, img2): # img1和img2是等shape的数组类型。
mse = np.mean( (img1 - img2) ** 2 )
if mse == 0:
return 100
PIXEL_MAX = 255.0
return 20 * math.log10(PIXEL_MAX / math.sqrt(mse))
# SSIM
def gaussian(window_size, sigma):
gauss = torch.Tensor([exp(-(x - window_size//2)**2/float(2*sigma**2)) for x in range(window_size)])
return gauss/gauss.sum()
def create_window(window_size, channel):
_1D_window = gaussian(window_size, 1.5).unsqueeze(1)
_2D_window = _1D_window.mm(_1D_window.t()).float().unsqueeze(0).unsqueeze(0)
window = Variable(_2D_window.expand(channel, 1, window_size, window_size))
return window
def SSIM(img1, img2): # img1和img2是等shape的数组类型。
img1 = torch.from_numpy(np.rollaxis(img1, 2)).float().unsqueeze(0)/255.0
img2 = torch.from_numpy(np.rollaxis(img2, 2)).float().unsqueeze(0)/255.0
img1 = Variable( img1, requires_grad=False)
img2 = Variable( img2, requires_grad = False)
(_, channel, _, _) = img1.size()
window_size = 11
window = create_window(window_size, channel)
if torch.cuda.is_available(): #如果测试数据太多,不使用GPU将会非常慢!!!
img1 = img1.cuda()
img2 = img2.cuda()
window = window.cuda()
mu1 = F.conv2d(img1, window, padding = window_size//2, groups = channel)
mu2 = F.conv2d(img2, window, padding = window_size//2, groups = channel)
mu1_sq = mu1.pow(2)
mu2_sq = mu2.pow(2)
mu1_mu2 = mu1*mu2
sigma1_sq = F.conv2d(img1*img1, window, padding = window_size//2, groups = channel) - mu1_sq
sigma2_sq = F.conv2d(img2*img2, window, padding = window_size//2, groups = channel) - mu2_sq
sigma12 = F.conv2d(img1*img2, window, padding = window_size//2, groups = channel) - mu1_mu2
C1 = 0.01**2
C2 = 0.03**2
ssim_map = ((2*mu1_mu2 + C1)*(2*sigma12 + C2))/((mu1_sq + mu2_sq + C1)*(sigma1_sq + sigma2_sq + C2))
return ssim_map.mean()
除此以外,以作者在代码中的命名来看,SSIM应该是fake_B(restored_image)与real_B(sharp_image)比较,如以下代码所示(代码在test.py中):
avgSSIM += SSIM(visuals['fake_B'],visuals['real_B'])
但这句话会报错:
Traceback (most recent call last):
File "test.py", line 39, in <module>
avgPSNR += PSNR(visuals['fake_B'],visuals['real_B'])
KeyError: 'real_B'
这是因为作者在测试中并没有记录real_B,而是记录了real_A,visuals对象里只有real_A(blurred_image)和fake_B两个关键字,导致没办法进行PSNR和SSIM定量分析,这也是笔者在文中第n个不懂的地方,也不知道作者文中的数据怎么来的。最后,笔者改了一下代码,在single_dataset里加入了sharp_image,即real_B,使得可以正常进行定量分析。
即:需要修改single_dataset.py和test_model.py两个文件。由于占得篇幅太长,笔者把修改过的这两个文件放到网盘里:
链接:https://pan.baidu.com/s/1_fMpKhR5MSFv7gG0RRs6mg
提取码:5s55
笔者在训练结束后又对代码进行了一些操作,默认的latest可能不是训练好的model,但用epoch300的肯定不会错。测试命令如下:
python test.py --dataroot /zhaosuiyi/datasets/GoPro --learn_residual --model test --which_epoch 300
3.5 结果
笔者使用wgan-gp(gantype=wgan-gp)和vanilla gan(gantype=gan)分别训练了300个epoch,每个训练大概花费了5天,之后在GoPro的2200张测试图片上测试,得到如下结果:
-
wgan-gp
定性分析,如下图:
定量分析:
avgPSNR = 33.053706
avgSSIM = 0.805461 -
vanilla gan
定性分析,如下图:
定量分析:
avgPSNR = 32.358385
avgSSIM = 0.810351
可以看到模型可以正常地去模糊了,wgan-gp和vanilla gan视觉效果看不出啥差别,PSNR和SSIM有些差别,但都和作者文中出入很大,也不知道原因!因为github上的预训练模型失效了,没有作者训练好的模型用来测试,笔者向作者发了邮件,但很遗憾未收到回复。以下是笔者训练好的模型:
链接:https://pan.baidu.com/s/17PvJh7C-JMI8SA7lMHnTBg
提取码:3z51
4 写在最后
小白才疏学浅,如有不当之处,欢迎大家批评指正!
创作不易,转载请注明出处,尊重别人的劳动成果就是尊重自己!
S. Nah, T. Hyun, K. Kyoung, and M. Lee. Deep Multi-scale Convolutional Neural Network for Dynamic Scene Deblurring. In IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2017. ↩︎
J. Sun, W. Cao, Z. Xu, and J. Ponce. Learning a Convolutional Neural Network for Non-uniform Motion Blur Removal. 2015. ↩︎
L. Xu, J. S. J. Ren, C. Liu, and J. Jia. Deep convolutional neural network for image deconvolution. In Proceedings of the 27th International Conference on Neural Information Processing Systems -Volume 1, NIPS’14, pages 1790–1798, Cambridge, MA, USA, 2014. MIT Press. ↩︎
R. Fergus, B. Singh, A. Hertzmann, S. T. Roweis, and W. T. Freeman. Removing camera shake from a single photograph. ACM Trans. Graph., 25(3):787–794, July 2006. ↩︎