前言
PS:本来打算一篇文章写完的,发现自己做事太罗嗦了,就分成了两篇
最近学习到了看到一篇face restoration的论文,基于styleGAN来做的,由于之前没做过low-level的视觉知识,由于第一次做SR的任务,看到里面的代码觉得很头大,就写篇文章记录一下,顺便帮助同样烦恼的小伙伴们少走弯路,话不多说,先看效果
复原效果还是挺不错的,下面简要介绍一下论文的原理和代码,后面会将框架和原理拆解开来看
论文地址:Towards Real-World Blind Face Restoration with Generative Facial Prior
代码地址:github
算法框架
按照图中序号的顺序分别介绍框架和代码:
1.Input处理,这块介绍一下原文对输入图像的处理
2.Degradation Removal,生成latent code作为GAN的输入
3.GAN,这里作者对styleGAN的输出又拼接了一点东西
4.Loss,网络的loss设计部分
Input处理
首先看下Dataset怎么写的
def __getitem__(self, index):
......
#直接看return内容
if self.crop_components:
return_dict = {
'lq': img_lq,
'gt': img_gt,
'gt_path': gt_path,
'loc_left_eye': loc_left_eye,
'loc_right_eye': loc_right_eye,
'loc_mouth': loc_mouth
}
return return_dict
else:
return {'lq': img_lq, 'gt': img_gt, 'gt_path': gt_path}
这里返回的主要有三项:img_lq
,img_gt
, gt_path
,分别是LR输入、真值,和gt的路径。loc_left_eye
、loc_right_eye
、loc_mouth
是使用眼睛和嘴巴位置信息时,作为可选输出。
其中img_lq
是由img_gt
变换得到的LR图像,img_gt
是高分辨图像,再来看一下img_lq
的生成过程
def __getitem__(self, index):
...#数据读入部分
# ------------------------ generate lq image ------------------------ #
# blur
kernel = degradations.random_mixed_kernels(...)
img_lq = cv2.filter2D(img_gt, -1, kernel)
# downsample
scale = np.random.uniform(self.downsample_range[0], self.downsample_range[1])
img_lq = cv2.resize(img_lq, (int(w // scale), int(h // scale)), interpolation=cv2.INTER_LINEAR)
# noise
if self.noise_range is not None:
img_lq = degradations.random_add_gaussian_noise(img_lq, self.noise_range)
# jpeg compression
if self.jpeg_range is not None:
img_lq = degradations.random_add_jpg_compression(img_lq, self.jpeg_range)
# resize to original size
img_lq = cv2.resize(img_lq, (w, h), interpolation=cv2.INTER_LINEAR)
# random color jitter (only for lq)
if self.color_jitter_prob is not None and (np.random.uniform() < self.color_jitter_prob):
img_lq = self.color_jitter(img_lq, self.color_jitter_shift)
# random to gray (only for lq)
if self.gray_prob and np.random.uniform() < self.gray_prob:
img_lq = cv2.cvtColor(img_lq, cv2.COLOR_BGR2GRAY)
img_lq = np.tile(img_lq[:, :, None], [1, 1, 3])
if self.opt.get('gt_gray'): # whether convert GT to gray images
img_gt = cv2.cvtColor(img_gt, cv2.COLOR_BGR2GRAY)
img_gt = np.tile(img_gt[:, :, None], [1, 1, 3]) # repeat the color channels
# BGR to RGB, HWC to CHW, numpy to tensor
img_gt, img_lq = img2tensor([img_gt, img_lq], bgr2rgb=True, float32=True)
# random color jitter (pytorch version) (only for lq)
if self.color_jitter_pt_prob is not None and (np.random.uniform() < self.color_jitter_pt_prob):
brightness = self.opt.get('brightness', (0.5, 1.5))
contrast = self.opt.get('contrast', (0.5, 1.5))
saturation = self.opt.get('saturation', (0, 1.5))
hue = self.opt.get('hue', (-0.1, 0.1))
img_lq = self.color_jitter_pt(img_lq, brightness, contrast, saturation, hue)
...#像素值范围映射unit8,正则化
...#return
主要就是模糊、加噪、随机像素值抖动、随机灰度化、随机色彩变化等过程生成LR图像
网络结构
网络结构这部分还是结合代码来看吧,其他栏主已经介绍了很多关于结构理论的内容,首先我们找到.yml
文件看到里面的配置,如下代码,总共定义了6个部件名称:network_d_left_eye
,network_d_right_eye
,network_d_mouth
是同一个结构,其实看四个结构就行。
# network structures
network_g:
type: GFPGANv1
network_d:
type: StyleGAN2Discriminator
network_d_left_eye:
type: FacialComponentDiscriminator
network_d_right_eye:
type: FacialComponentDiscriminator
network_d_mouth:
type: FacialComponentDiscriminator
network_identity:
type: ResNetArcFace
GFPGANv1
这块算是整个结构中内容最多的一部分了,首先是框图中的②和③,该部分是对输入图像的通道进行降维,产生latent code,通过MLP送到生成器中,
首先经过一个3通道到512通道的变换,然后再做通道和空间都进行下采样的6个相同操作,这部分是BasicSR里面的ResBlock函数完成的,可以自行查一下相关实现,最终输出latent code和需要进行空间特征约束的condition。
其中一部分作用是将img_lr 变成 latent code
def __init__(self, ...)
first_out_size = 2**(int(math.log(out_size, 2)))
#ConvLayer
self.conv_body_first = ConvLayer(3, channels[f'{first_out_size}'], 1, bias=True, activate=True)
# downsample
in_channels = channels[f'{first_out_size}']
self.conv_body_down = nn.ModuleList()
for i in range(self.log_size, 2, -1):
out_channels = channels[f'{2**(i - 1)}']
self.conv_body_down.append(ResBlock(in_channels, out_channels, resample_kernel))
in_channels = out_channels
self.final_linear = EqualLinear(channels['4'] * 4 * 4, linear_out_channel, bias=True, bias_init_val=0, lr_mul=1, activation=None)
...
def forward(self, ...)
# encoder
feat = self.conv_body_first(x)
for i in range(self.log_size - 2):
feat = self.conv_body_down[i](feat)
unet_skips.insert(0, feat)
feat = self.final_conv(feat)
style_code = self.final_linear(feat.view(feat.size(0), -1))
...
另外一部分产生Fspatial再通过卷积操作得到两个系数作为condition约束,送到后面的
def __init__():
# for SFT modulations (scale and shift)
self.condition_scale = nn.ModuleList()
self.condition_shift = nn.ModuleList()
for i in range(3, self.log_size + 1):
out_channels = channels[f'{2**i}']
if sft_half:
sft_out_channels = out_channels
else:
sft_out_channels = out_channels * 2
self.condition_scale.append(
nn.Sequential(
EqualConv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=True, bias_init_val=0),
ScaledLeakyReLU(0.2),
EqualConv2d(out_channels, sft_out_channels, 3, stride=1, padding=1, bias=True, bias_init_val=1)))
self.condition_shift.append(
nn.Sequential(
EqualConv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=True, bias_init_val=0),
ScaledLeakyReLU(0.2),
EqualConv2d(out_channels, sft_out_channels, 3, stride=1, padding=1, bias=True, bias_init_val=0)))
然后将上面的两部分送到StyleGAN2Generator中处理,基本和沿用了StyleGAN2的代码,所不同的是,在逐层进行卷积运算时,将前面产生的conditions约束也加入运算,下面贴一张styleGAN2的结构,可以对照着结构看代码。
class StyleGAN2GeneratorSFT(StyleGAN2Generator):
"""StyleGAN2 Generator with SFT modulation (Spatial Feature Transform).
Args:
out_size (int): The spatial size of outputs.
num_style_feat (int): Channel number of style features. Default: 512.
num_mlp (int): Layer number of MLP style layers. Default: 8.
channel_multiplier (int): Channel multiplier for large networks of StyleGAN2. Default: 2.
resample_kernel (list[int]): A list indicating the 1D resample kernel magnitude. A cross production will be
applied to extent 1D resample kernel to 2D resample kernel. Default: (1, 3, 3, 1).
lr_mlp (float): Learning rate multiplier for mlp layers. Default: 0.01.
narrow (float): The narrow ratio for channels. Default: 1.
sft_half (bool): Whether to apply SFT on half of the input channels. Default: False.
"""
def __init__(self,
out_size,
num_style_feat=512,
num_mlp=8,
channel_multiplier=2,
resample_kernel=(1, 3, 3, 1),
lr_mlp=0.01,
narrow=1,
sft_half=False):
super(StyleGAN2GeneratorSFT, self).__init__(
out_size,
num_style_feat=num_style_feat,
num_mlp=num_mlp,
channel_multiplier=channel_multiplier,
resample_kernel=resample_kernel,
lr_mlp=lr_mlp,
narrow=narrow)
self.sft_half = sft_half
def forward(self,
styles,
conditions,
input_is_latent=False,
noise=None,
randomize_noise=True,
truncation=1,
truncation_latent=None,
inject_index=None,
return_latents=False):
"""Forward function for StyleGAN2GeneratorSFT.
Args:
styles (list[Tensor]): Sample codes of styles.
conditions (list[Tensor]): SFT conditions to generators.
input_is_latent (bool): Whether input is latent style. Default: False.
noise (Tensor | None): Input noise or None. Default: None.
randomize_noise (bool): Randomize noise, used when 'noise' is False. Default: True.
truncation (float): The truncation ratio. Default: 1.
truncation_latent (Tensor | None): The truncation latent tensor. Default: None.
inject_index (int | None): The injection index for mixing noise. Default: None.
return_latents (bool): Whether to return style latents. Default: False.
"""
# style codes -> latents with Style MLP layer
if not input_is_latent:
styles = [self.style_mlp(s) for s in styles]
# noises
if noise is None:
if randomize_noise:
noise = [None] * self.num_layers # for each style conv layer
else: # use the stored noise
noise = [getattr(self.noises, f'noise{i}') for i in range(self.num_layers)]
# style truncation
if truncation < 1:
style_truncation = []
for style in styles:
style_truncation.append(truncation_latent + truncation * (style - truncation_latent))
styles = style_truncation
# get style latents with injection
if len(styles) == 1:
inject_index = self.num_latent
if styles[0].ndim < 3:
# repeat latent code for all the layers
latent = styles[0].unsqueeze(1).repeat(1, inject_index, 1)
else: # used for encoder with different latent code for each layer
latent = styles[0]
elif len(styles) == 2: # mixing noises
if inject_index is None:
inject_index = random.randint(1, self.num_latent - 1)
latent1 = styles[0].unsqueeze(1).repeat(1, inject_index, 1)
latent2 = styles[1].unsqueeze(1).repeat(1, self.num_latent - inject_index, 1)
latent = torch.cat([latent1, latent2], 1)
# main generation
out = self.constant_input(latent.shape[0])
out = self.style_conv1(out, latent[:, 0], noise=noise[0])
skip = self.to_rgb1(out, latent[:, 1])
i = 1
for conv1, conv2, noise1, noise2, to_rgb in zip(self.style_convs[::2], self.style_convs[1::2], noise[1::2],
noise[2::2], self.to_rgbs):
out = conv1(out, latent[:, i], noise=noise1)
# the conditions may have fewer levels
if i < len(conditions):
# SFT part to combine the conditions
#只有这里是跟styleGan有所区别的地方
if self.sft_half: # only apply SFT to half of the channels
out_same, out_sft = torch.split(out, int(out.size(1) // 2), dim=1)
out_sft = out_sft * conditions[i - 1] + conditions[i]
out = torch.cat([out_same, out_sft], dim=1)
else: # apply SFT to all the channels
out = out * conditions[i - 1] + conditions[i]
out = conv2(out, latent[:, i + 1], noise=noise2)
skip = to_rgb(out, latent[:, i + 2], skip) # feature back to the rgb space
i += 2
image = skip
if return_latents:
return image, latent
else:
return image, None
StyleGAN2Discriminator
网络的Discriminator部分沿用的StyleGAN的部分,完全没做改动,可以看到这里就是一些卷积下采样和线性层的叠加,最终输出一个类似二分类的判定结果。
class StyleGAN2Discriminator(nn.Module):
"""StyleGAN2 Discriminator.
Args:
out_size (int): The spatial size of outputs.
channel_multiplier (int): Channel multiplier for large networks of
StyleGAN2. Default: 2.
resample_kernel (list[int]): A list indicating the 1D resample kernel
magnitude. A cross production will be applied to extent 1D resample
kernel to 2D resample kernel. Default: (1, 3, 3, 1).
stddev_group (int): For group stddev statistics. Default: 4.
narrow (float): Narrow ratio for channels. Default: 1.0.
"""
def __init__(self, out_size, channel_multiplier=2, resample_kernel=(1, 3, 3, 1), stddev_group=4, narrow=1):
super(StyleGAN2Discriminator, self).__init__()
channels = {
'4': int(512 * narrow),
'8': int(512 * narrow),
'16': int(512 * narrow),
'32': int(512 * narrow),
'64': int(256 * channel_multiplier * narrow),
'128': int(128 * channel_multiplier * narrow),
'256': int(64 * channel_multiplier * narrow),
'512': int(32 * channel_multiplier * narrow),
'1024': int(16 * channel_multiplier * narrow)
}
log_size = int(math.log(out_size, 2))
conv_body = [ConvLayer(3, channels[f'{out_size}'], 1, bias=True, activate=True)]
in_channels = channels[f'{out_size}']
for i in range(log_size, 2, -1):
out_channels = channels[f'{2**(i - 1)}']
conv_body.append(ResBlock(in_channels, out_channels, resample_kernel))
in_channels = out_channels
self.conv_body = nn.Sequential(*conv_body)
self.final_conv = ConvLayer(in_channels + 1, channels['4'], 3, bias=True, activate=True)
self.final_linear = nn.Sequential(
EqualLinear(
channels['4'] * 4 * 4, channels['4'], bias=True, bias_init_val=0, lr_mul=1, activation='fused_lrelu'),
EqualLinear(channels['4'], 1, bias=True, bias_init_val=0, lr_mul=1, activation=None),
)
self.stddev_group = stddev_group # default=4
self.stddev_feat = 1
def forward(self, x):
out = self.conv_body(x)
b, c, h, w = out.shape
# concatenate a group stddev statistics to out
group = min(b, self.stddev_group) # Minibatch must be divisible by (or smaller than) group_size
stddev = out.view(group, -1, self.stddev_feat, c // self.stddev_feat, h, w)
stddev = torch.sqrt(stddev.var(0, unbiased=False) + 1e-8)
stddev = stddev.mean([2, 3, 4], keepdims=True).squeeze(2)
stddev = stddev.repeat(group, 1, h, w)
out = torch.cat([out, stddev], 1)
out = self.final_conv(out)
out = out.view(b, -1)
out = self.final_linear(out)
return out
后续
由于篇幅限制,后面的内容放到下一次吧