AI实现换脸

介绍

人脸互换是计算机视觉领域比较热门的一个应用,它可以应用于视频合成,提供隐私服务,肖像更换等各个领域。本实验将从自编码器,上采样,数据增强等知识点出发,对深度学习下的人脸互换进行讲解。并且利用 TensorFlow 2 实现川普和道格拉斯·凯奇的人脸互换。完成后的模型可以在不修改原图表情的情况下,完成人脸替换。

知识点

数据的可视化
数据增强
自编码器
上采样与下采样
子像素卷积

传统人脸互换

在深度学习出来之前,人脸互换主要是通过对比两张脸的相似信息来进行互换。我们可以通过特征点(下图的红色点)来提取一张脸的眉毛、眼睛等特征信息,然后匹配到另外一张人脸上。如下图所示,这种实现方法不需要训练时间,每次只需要遍历所有的像素点即可。但是,这样实现的效果比较差,无法修改人脸的表情。

而深度学习却可以在不修改人脸表情的情况下,做到人脸特征替换的效果。由于视频中的人脸互换所需要的资源过多,并且视频就是由一张张图片组成的,因此本次实验只考虑图片中的人脸替换。我们会借用自编码器的核心思想,然后对 DeepFake 的源码进行解析,最后实现川普和尼古拉斯 · 凯奇的人脸互换。

数据的可视化

在实验开始之前,让我们先来安装一下实验所需的工具包,代码如下:在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
数据集主要由两个文件夹构成,一个文件名为 trump ,一个为 cage 。接下来,我们利用 Python 遍历这两个文件夹,并获得所有文件的路径。
在这里插入图片描述
在这里插入图片描述
接下来,我们利用 Python 中的 OpenCV 库,对图片进行批量加载。在这里插入图片描述
在这里插入图片描述
这里我们分别加载了两个人物的前三张图片。从上面的运行结果可以看出,每张图片大小为 256×256 。那么怎样才能一次性,将这些图片同时展示出来呢?核心思想便是将这 6 张图片拼在一起,形成一个 512×768 的图片(一行三张,一共两行)。整体的思路如下图所示:在这里插入图片描述
首先让我们来实现 stack_images 函数。为了方便以后使用,我们将 stack_images 写成一个可以将图片集合转变成一张图片的函数。在这里插入图片描述
终于,我们可以将 A_images ,B_images 两个图片集合进行展示了。由于 OpenCV 无法在 Notebook 上进行图片的展示。因此我们只能利用 OpenCV 读取图片,再利用 Matplotlib 进行展示。在这里插入图片描述

自编码器

编码器与解码器
自编码器是一种用于非监督学习过程的人工神经网络。自动编码器通常由两部分构成:编码器和解码器。下面,我们通过图示来对其进行解释。在这里插入图片描述
Encoder :编码器,由各种下采样的方法构成。将输入图片压缩成空间特征 (上图的 Code ),也就是对原图片进行特征提取。
Decoder :解码器,由各种上采样的方法构成。重构编码器输出的空间特征,并对其进行解码,输出新的图片。
自编码器的全过程:编码器对输入的图片进行特征提取,然后解码器对提取的特征进行解析,最后输出新的图片。
从上图可以看出,手写的数字 4 通过自编码器后,会生成一张看起来像手写字符 4 的新图片。 那么这样做有什么意义呢?其实,在对图片进行去噪的时候,我们经常会采用这种技术。
在这里插入图片描述
如上图所示,我们可以将加噪点后的手写字符放入自编码器中,然后以加噪点前的手写字符为目标进行训练。最终就能得到一个专门处理噪点的神经网络模型。当以后出现新的具有噪点的图片时,只需放入训练好的自编码器就可以直接进行去噪了。

根据上面的知识,我们可以发现自编码器最重要的就是编码器结构和解码器结构。实现编码器的下采样的方法有很多,比如我们熟知的池化、卷积等。但是实现解码器的上采样方法又有哪些呢?怎样才能将缩小的图像放大成原图呢?这里我们将会学习到一种叫做子像素卷积的上采样方法。
子像素卷积( Sub-pixel Convolution )
子像素卷积是一种巧妙的图像及特征图的 upscale 方法,又叫做 Pixel Shuffle(像素洗牌)。这种方法于 2016 年被 Wenzhe Shi 等人 提出。较之前的上采样算法,子像素卷积在速度和质量上都有明显的提升。
子像素卷积的结构(主要观察后面的彩色部分)如下所示:
在这里插入图片描述
第一个彩色部分是通道数为 𝑟2 ,大小为 𝑛×𝑛 的特性图,即为 Sub-pixel Convolution 前的图像。
第二个彩色部分是通道数为 1 ,大小为 𝑛𝑟×𝑛𝑟 的特征图,即为 Sub-pixel Convolution 后的图像。
上图很直观得表达了子像素卷积的做法,前面就是一个普通的 CNN 网络,到后面彩色部分就是子像素卷积的操作了。
简单的说,就是将每一个像素点的所有通道合并在了一起。例如通道数为 9,那么我就可以把第一个像素点的所有通道拿出来,排成一个 3×3 的“像素点”,如下图所示:
在这里插入图片描述
对每个像素点都进行上述操作,最后得到了大小为 𝑛𝑟×𝑛𝑟 的特征图,进而提高了原图的分辨率。这种提高分辨率的过程就叫做子像素卷积。

子像素卷积层,用于上采样

PixelShuffler layer for Keras

from keras.utils import conv_utils
from keras.engine.topology import Layer
import keras.backend as K

class PixelShuffler(Layer):
# 初始化 子像素卷积层,并在输入数据时,对数据进行标准化处理。
def init(self, size=(2, 2), data_format=None, **kwargs):
super(PixelShuffler, self).init(**kwargs)
self.data_format = K.normalize_data_format(data_format)
self.size = conv_utils.normalize_tuple(size, 2, ‘size’)
def call(self, inputs):
# 根据得到输入层图层 batch_size,h ,w,c 的大小
input_shape = K.int_shape(inputs)
batch_size, h, w, c = input_shape
if batch_size is None:
batch_size = -1
rh, rw = self.size

    # 计算转换后的图层大小与通道数oh, ow = h * rh, w * rwoc = c // (rh * rw)# 先将图层分开,并且将每一层装换到自己应该到维度# 最后再利用一次 reshape 函数(计算机会从外到里的一个个的将数据排下来),这就可以转成指定大小的图层了out = K.reshape(inputs, (batch_size, h, w, rh, rw, oc))out = K.permute_dimensions(out, (0, 1, 3, 2, 4, 5))out = K.reshape(out, (batch_size, oh, ow, oc))return out
# compute_output_shape()函数用来输出这一层输出尺寸的大小
# 尺寸是根据input_shape以及我们定义的output_shape计算的。
def compute_output_shape(self, input_shape):height = input_shape[1] * self.size[0] if input_shape[1] is not None else Nonewidth = input_shape[2] * self.size[1]  if input_shape[2] is not None else Nonechannels = input_shape[3] // self.size[0] // self.size[1]return (input_shape[0],height,width,channels)
# 设置配置文件
def get_config(self):config = {'size': self.size,'data_format': self.data_format}base_config = super(PixelShuffler, self).get_config()return dict(list(base_config.items()) + list(config.items()))下采样层与上采样层的编写

下采样和上采样就是构成编码器和解码器的具体部件。下采样层主要用于缩小图层大小,扩大图层通道数(即编码器)。上采样主要用于扩大图层大小,缩小图层通道数(即解码器)。

在本次实验中,每个下采样层包括了一个卷积层和一个 LeakyReLU 激活函数层。而上采样包含了一个卷积层,一个 LeakyReLU 激活函数层和一个像素洗牌层。下采样中的卷积层用于缩小图层大小提取图层特征,上采样中的卷积层用于扩大图层通道数,保证在像素洗牌后的图层的通道数和所需通道数相同。

假设在上采样时,我们输入的图层大小为 32×32 ,而我们需要输出的图层大小为 64×64,通道数为 256 。那么我们就需要在子像素卷积之前先进行一次卷积,使得图层的通道数变为 256×4 (即得到 32×32×1024 的图层)。然后再通过子像素卷积,就能够输出 64×64×256 的新图层了。

接下来我们利用子像素卷积函数以及 Keras 提供的卷积函数对自编码器中的上采样层和下采样层进行编写。在这里插入图片描述
接下来,我们传入一张图片对上面自定义的两个网络层进行测试:
在这里插入图片描述
在这里插入图片描述
从结果可以看出,上采样层可以将图层的大小减小为原来的 1/2 ,下采样层可以将图层大小扩大为原来的 2 倍。

人脸互换的基本架构

其实人脸互换的基本结构就是两个自编码器,更准确的说应该是 1 个编码器 + 2 个解码器。接下来,我会从训练过程和运用过程分别对 AI 换脸的概念进行阐述。

训练过程

如上图,我们利用同一套方法(编码器)对两种图片进行特征提取。将提取出来的特征放到各自对应的解码器中,生成各自所对应的图像。然后利用生成的图像与原来的图像计算损失,再反向传播并对模型参数进行调整,如此循环,直到损失最小。

当损失最小时,我们把川普的图片放入训练好的(Encoder,Decoder_A) 中就能够得到一张和川普神似的图片。同理,若把凯奇的图片放入训练好的(Encode,Decode_B)中,也能得到和凯奇神似的图片。也就是说,在模型训练过程中,原始图片既是训练集合也是目标集合。

运用过程
现在让我们理一下思路,A,B 两类图片通过同一种方法进行特征提取,然后把得到的特征放入各自的解码器中得到了属于自己的图片。

也就是说 Decoder_A 和 Decoder_B 都能够识别 Encoder 所提取的特征。因此,从同一个 Encoder 中出来的特征既可以放入 Decoder_A 中,也可以放到 Deconder_B中,这就是人脸互换的关键,也是 Encoder 只有一个的原因。

因此,在上图的模型训练好后,我们只需进行下图的操作即可实现人脸互换:
如上图所示,将一张川普的图片放入训练好的 EnCoder 中,得到一组特征。将这组特征放入 Decoder_A 中,就能得到一张神似川普的新图片。若将这组特征放入 Decoder_B 中,就会输出与川普表情一样但是和凯奇神似的图片。
神经网络结构
说完人脸互换的原理后,让我们来谈谈本次实验中用到的 Encoder 和 Decoder 的具体网络结构,如下图所示:
在这里插入图片描述
中间那层为编码器的神经网络结构。它由 4 个下采样的卷积层,2 个全连接层,1 个上采样层构成。其中下采样卷积层用于对图片特征进行提取。全连接层用于打乱特征的空间结构,使模型能够学习到更加有用的东西。上采样层用于增加图层大小。

上下两层为两个解码器。他们的网络结构相同,但是参数不同。他们都是由三个上采样层和一个下采样卷积层构成。其中上采样层的作用是为了扩大图层大小,使最后能够输出和原图片一样大小的新图片。最后的卷积层是为了缩小图层通道数,使最后输出的是一个三通道的图片。

接下来利用 Keras 对 Encoder 和 Decoder 进行编写:
在这里插入图片描述
根据人脸互换所需要的自编码器结构,创建 (Encoder,Decoder_A)和(Encoder,Decoder_B)结构,并且选择绝对平方损失作为模型的损失函数。在这里插入图片描述
在这里插入图片描述
数据预处理
为了能够训练出较好的模型,在模型训练之前,我们必须先对数据进行相关处理。接下来,我们将对数据集做如下处理:在这里插入图片描述
数据增强
数据增强是深度学习中很重要的一步。这种方法可以在不消耗任何成本的情况下,获得更多的数据,进而训练出更好的模型。通过旋转、平移、缩放、剪切等操作,将原来的一张图片拓展成多张图片是数据增强的一种方法。

我们通过对旋转角度,平移距离,缩放比例等随机取值,来对原始图片进行随机转换。在这里插入图片描述
让我们传入一张图片,进行测试,并观察图片的变化情况:在这里插入图片描述

仔细比较结果中的两个川普(从他们的下巴与下边界的距离,目光方向等方面进行比较),你会发现图片已经发生了变化。当然,如果你并没有发现太大变化,可以多次运行上述代码,如果幸运的话,你可以看到图片出现了翻转(设置的图片翻转概率为 40% )。

输入数据集和目标数据集

由于图片已经把川普的整个大头包含了进去,而我们需要训练只是川普的脸部特征。因此为了提高模型的训练效率,我们需要将川普的小脸从他的大头中切割出来,即将一张 256×256 的大图变为 64×64 的小图。将切割下来的小图放入模型中,既可以提高模型的训练速度,又可以提高准确性。

如果仅仅是裁剪,我们可以直接进行随机剪切。但是为了提高模型的泛化性,在剪切的时候,我们还需要做一次数据增强,也就是将图片进行了扭曲编写。

那么如何做到图片的扭曲呢?首先这里我们用到了 OpenCV 中的简单映射的方法 :将一张图片的一个像素点的值,放到另一张图片上的某个像素点上。在这里插入图片描述
如上图所示,为了达到卷曲的效果,可以在决定映射位置时,添加一个小的波动,即某个点可能会映射到他原来位置的相邻位置。比如,原图的 4 位置本来应该映射到另外一张图的 4 位置,但是我们可以加上一个比较小的随机值,是它的映射位置出现细微偏移进而达到卷曲的效果。代码如下在这里插入图片描述
从上面代码中可以看出,我们并没有直接把做好的输入数据集当做目标数据集,而是对输入数据集中的图片又进行了一次转换。这次转换采用的是点云匹配算法,其本质还是一种映射算法。(碍于篇幅本文不作详解,有兴趣的同学可以查看 该篇论文)。

Markdown Code
当然这个算法的代码不用我们自己编写,可以直接从官方库中下载,我们只需要运行一下即可。代码如下:

License (Modified BSD)

umeyama function from scikit-image/skimage/transform/_geometric.py

def umeyama(src, dst, estimate_scale):
“”“Estimate N-D similarity transformation with or without scaling.
Parameters
———-
src : (M, N) array
Source coordinates.
dst : (M, N) array
Destination coordinates.
estimate_scale : bool
Whether to estimate scaling factor.
Returns
——-
T : (N + 1, N + 1)
The homogeneous similarity transformation matrix. The matrix contains
NaN values only if the problem is not well-conditioned.
References
———-
… [1] “Least-squares estimation of transformation parameters between two
point patterns”, Shinji Umeyama, PAMI 1991, DOI: 10.1109/34.88573
“””

num = src.shape[0]
dim = src.shape[1]# Compute mean of src and dst.
src_mean = src.mean(axis=0)
dst_mean = dst.mean(axis=0)# Subtract mean from src and dst.
src_demean = src - src_mean
dst_demean = dst - dst_mean# Eq. (38). 下面的Eq 都分别对应着论文中的公式
A = np.dot(dst_demean.T, src_demean) / num# Eq. (39).
d = np.ones((dim,), dtype=np.double)
if np.linalg.det(A) < 0:d[dim - 1] = -1T = np.eye(dim + 1, dtype=np.double)U, S, V = np.linalg.svd(A)# Eq. (40) and (43).
rank = np.linalg.matrix_rank(A)
if rank == 0:return np.nan * T
elif rank == dim - 1:if np.linalg.det(U) * np.linalg.det(V) > 0:T[:dim, :dim] = np.dot(U, V)else:s = d[dim - 1]d[dim - 1] = -1T[:dim, :dim] = np.dot(U, np.dot(np.diag(d), V))d[dim - 1] = s
else:T[:dim, :dim] = np.dot(U, np.dot(np.diag(d), V.T))if estimate_scale:# Eq. (41) and (42).scale = 1.0 / src_demean.var(axis=0).sum() * np.dot(S, d)
else:scale = 1.0T[:dim, dim] = dst_mean - scale * np.dot(T[:dim, :dim], src_mean.T)
T[:dim, :dim] *= scalereturn T
接下来,我们传入一张图片测试,观察图片的变化,可以发现图片被被裁剪成了  64×64×3 的新图片。

在这里插入图片描述

在这里插入图片描述

构造 Batch 数据集

终于到了数据预处理的最后一步,构造 Batch 数据集。这是深度学习中常见的一个步骤,其本质就是根据 batch_size 的大小将数据集进行分批。大小合适的 batch_size 可以使模型更加高效的收敛。代码如下:在这里插入图片描述
接下来我们对上面代码进行测试,从川普的图片集合中取出一个 batch_size 的数据集。在这里插入图片描述
在这里插入图片描述

模型训练

现在神经网络结构搭好了,数据也准备齐了,我们终于可以开始进行模型的训练了。在这里插入图片描述
在这里插入图片描述

模型运用

这里我们从实验楼的网站上下载模型,并解压。
在这里插入图片描述
整个换脸模型被保存成了三部分:编码器 encoder.h5、解码器 A decoder_A.h5 和解码器 B decoder_B.h5。

Markdown Code
测试的代码和训练代码雷同,只是删去了循环和训练的步骤。虽然下列代码没有训练的过程,但是由于加载模型需要消耗一些时间,预计运行 1~3 min ,请耐心等待。

直接加载模型

print(“开始加载模型,请耐心等待……”)
encoder .load_weights(“encoder.h5”)
decoder_A.load_weights(“decoder_A.h5”)
decoder_B.load_weights(“decoder_B.h5”)

下面代码和训练代码类似

获取图片,并对图片进行预处理

images_A = get_image_paths(“trump”)
images_B = get_image_paths(“cage”)

图片进行归一化处理

images_A = load_images(images_A) / 255.0
images_B = load_images(images_B) / 255.0

images_A += images_B.mean(axis=(0, 1, 2)) – images_A.mean(axis=(0, 1, 2))
batch_size = 64
warped_A, target_A = get_training_data(images_A, batch_size)
warped_B, target_B = get_training_data(images_B, batch_size)

分别取当下批次下的川普和凯奇的图片的前三张进行观察

test_A = target_A[0:3]
test_B = target_B[0:3]

print(“开始预测,请耐心等待……”)

进行拼接 原图 A – 解码器 A 生成的图 – 解码器 B 生成的图

figure_A = np.stack([
test_A,
autoencoder_A.predict(test_A),
autoencoder_B.predict(test_A),
], axis=1)

进行拼接 原图 B – 解码器 B 生成的图 – 解码器 A 生成的图

figure_B = np.stack([
test_B,
autoencoder_B.predict(test_B),
autoencoder_A.predict(test_B),
], axis=1)

print(“开始画图,请耐心等待……”)

将多幅图拼成一幅图 (已在数据可视化部分进行了详细讲解)

figure = np.concatenate([figure_A, figure_B], axis=0)
figure = figure.reshape((2, 3) + figure.shape[1:])
figure = stack_images(figure)

将图片进行反归一化

figure = np.clip(figure * 255, 0, 255).astype(‘uint8’)

显示图片

plt.imshow(cv2.cvtColor(figure, cv2.COLOR_BGR2RGB))
plt.show()

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注