总序

“换脸”Python实现共分为四个部分,第一部分是原理讲解,第二部分是Python实现,第三部分是效果改进,前三部分都是单张图片,第四部分是应用到视频。

网上的很多教程没有原理讲解,所以也许可以照猫画虎,但没有总体的理解,就很难有自己的改进方法,希望本四次的分享对你有帮助。


第二部分 Python3实现

在第一部分中,我们提到,换脸主要包括以下步骤:

1. 确定人脸位置
2. 把图乙的人脸大小和方向调节得和图甲中的差不多
3. 挖掉图甲中原来的人脸,换成图乙中的人脸

下面我们依次来实现。

准备

  • Python包准备

​ 我使用的是Ubuntu系统,Windows类似,用到的Python3 的主要有 $dlib$, $opencv$ 和$numpy$,请自行安装。 导入包代码如下:

1
2
3
4
import sys, os
import numpy as np
import dlib
import cv2
  • 文件夹

    本项目的文件夹如下,face 是根目录,下面有个 changeFace.py ,写代码,与 changeFace.py 同级建两个文件夹,一个 model/ ,用来放模型,一个 faces/ ,用来放图片。

    • face/
      • changeFace.py
      • model/
      • faces/
  • dlib 训练结果下载

对于确定人脸的关键点,已经有训练好的模型,官网下载 shape_predictor_68_face_landmarks.dat 和 dlib_face_recognition_resnet_model_v1.dat 文件。放在上一步说的 model/ 文件夹下。

  • 图片准备

随便找两张只有一个人的正脸的图片,图片大小可以不一样,最好一男一女,这样换脸后也比较明显。分别命名成 boy.jpeg 和 girl.jpeg。我们的目标给男孩子换上女孩的脸^-^。

使用dlib预测器

下面使用dlib预测器,做一些初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import sys, os, glob
import numpy as np
import dlib
import cv2

# 预测器和模型的路径
predictor_path = r'./model/shape_predictor_68_face_landmarks.dat'
face_rec_model_path = r'./model/dlib_face_recognition_resnet_model_v1.dat'
faces_folder_path = r'./faces/'

# 声明一个人脸关键点预测器 predictor,全局变量
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(predictor_path)


# 68个点,分别代表眼,口,鼻等关键点的位置,现在分开一下
LEFT_EYE_POINTS = list(range(42, 48))
RIGHT_EYE_POINTS = list(range(36, 42))
LEFT_BROW_POINTS = list(range(22, 27))
RIGHT_BROW_POINTS = list(range(17, 22))
NOSE_POINTS = list(range(27, 35))
MOUTH_POINTS = list(range(48, 61))

# 我们要用的确定人脸的关键点列表
OVERLAY_POINTS = [
LEFT_EYE_POINTS + RIGHT_EYE_POINTS +
LEFT_BROW_POINTS + RIGHT_BROW_POINTS +
NOSE_POINTS + MOUTH_POINTS,
]

# 特征数
FEATHER_AMOUNT = 11

获取脸部关键点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 获取脸部关键点
def get_landmark(image):
face_rect = detector(image, 1)
if len(face_rect) != 1:
print('No one face in one picture')
else:
return np.matrix([[p.x, p.y] for p in predictor(image, face_rect[0]).parts()])

# 测试 get_landmark(img) 是否正常, 可不写
def test_get_landmark():
for img_path in glob.glob(os.path.join(faces_folder_path, "*.jpeg")):
img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
landmarks = get_landmark(img)
print(landmarks)

运行 test_get_landmark(), 可以看见输出一些两列的矩阵,每行代表一个点。

人脸对齐

两张图片上的脸的大小和方向很可能是不同的,要换脸,那把脸变得差不多大是必须的。这个过程使用 Procrustes analysis(普氏分析),具体细节可以参考这里 。其实主要也是通过平移、旋转等仿射变化实现。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 使用普氏分析调整脸部
# p1,p2分别是两张图的关键点landmarks列表
# 返回结果是从 p2 到 p1 的仿射变换矩阵
def transformation_from_points(p1, p2):
p1 = p1.astype(np.float64)
p2 = p2.astype(np.float64)

c1 = np.mean(p1, axis=0)
c2 = np.mean(p2, axis=0)
p1 -= c1
p2 -= c2

s1 = np.std(p1)
s2 = np.std(p2)

p1 /= s1
p2 /= s2

U, S, Vt = np.linalg.svd(p1.T * p2)
R = (U * Vt).T

trans_mat = np.vstack([np.hstack(((s2 / s1)*R, c2.T-(s2/s1)*R*c1.T)),
np.matrix([0., 0., 1.])])
return trans_mat

下面具体利用上面的仿射变化函数 trans_mat 及图片大小,进行变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 把 image 变成 dshape 大小,并用仿射矩阵 M 进行变化,这里的M就是上面的 trans_mat
def warp_image(image, M, dshape):
output_image = np.zeros(dshape, dtype=image.dtype)
cv2.warpAffine(image, M[:2], (dshape[1], dshape[0]),
dst=output_image, flags=cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_TRANSPARENT)
return output_image


# 测试函数,可不写
def test_wrap_image():
boy = cv2.imread(r'./faces/boy.jpeg')
girl = cv2.imread(r'./faces/girl.jpeg')
cv2.imshow("boy", boy)
cv2.imshow("girl_2", girl)

boy_landmarks = get_landmark(boy)
girl_landmarks = get_landmark(girl)

trans_mat = transformation_from_points(
boy_landmarks, girl_landmarks)
output_image = wrap_image(girl, trans_mat, dshape=boy.shape)

cv2.imshow("result", output_image)

cv2.waitKey(0)
cv2.destroyAllWindows()

运行 test_wrap_image(), 效果如下:

多余的背景是我的桌面,上面是截图。最右面一张是变化后的效果,可以看其脸型大小和图片尺寸都和 boy 大小差不多了。

获取人脸掩模

先用 boy 的脸来做实验,其实脸主要看眼睛、鼻子和嘴巴的位置,我就要这几个关键部位。那先找出脸的形状,这个用 opencv 的凸包函数 convexHull() 来实现。

右图就是左图脸的掩模。代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 在img上绘制points点列表的凸包,Python参数默认引用,所以此处没用返回值
def draw_convex_hull(img, points, color):
points = cv2.convexHull(points)
cv2.fillConvexPoly(img, points, color)

# 获取人脸掩模
def get_face_mask(img, landmarks):
# 用一张灰度的图片来绘制
img = np.zeros(img.shape[:2], dtype=np.float64)
for group in OVERLAY_POINTS:
draw_convex_hull(img, landmarks[group], color=1)
# 之前的 img 是单通道的灰度图,所以下面有三个 img
img = np.array([img, img, img]).transpose((1, 2, 0))
return img

# 测试函数, 可不写。
def test_get_face_mask():
boy = cv2.imread(r'./faces/boy.jpeg')
boy_landmarks = get_landmark(boy)
boy_mask = get_face_mask(boy, boy_landmarks)
cv2.imshow("boy", boy)
cv2.imshow("boy_mask", boy_mask)
cv2.waitKey(0)
cv2.destroyAllWindows()

运行 test_get_face_mask(), 效果如上。

换脸

到上面,基本工作完成了,那就来看看怎么让 boy 有 girl 的脸。下面依次实现

  • 把 girl 的脸变成和 boy 一样大小;
  • 拿到 girl 的脸和 boy 去掉脸的剩下部分,这步用掩模辅助实现。其实这里只有一个掩模,因为两张脸认为一样大,这样两张脸可以无缝连接;
  • 用矩阵加法和对应元素相乘的方法实现换脸,因为图片颜色为 0255,这里要变成01。

下面是代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def change_face():
boy = cv2.imread('faces/boy.jpeg')
girl = cv2.imread('faces/girl.jpeg')
boy_landmarks = get_landmark(boy)
girl_landmarks = get_landmark(girl)

# 获取变化矩阵, OVERLAY_POINTS 是上面定义的关键点下标
trans_mat = transformation_from_points(
boy_landmarks[OVERLAY_POINTS],
girl_landmarks[OVERLAY_POINTS]
)

# 掩模就用 boy_mask就好
boy_mask = get_face_mask(boy, boy_landmarks)
# warped_girl 是一张大小和 boy 一样大,并且脸对应的图片
warped_girl = warp_image(girl, trans_mat, boy.shape)

boy = boy.astype(np.float64)
warped_girl = warped_girl.astype(np.float64)

# 图片相加, boy, girl 的像素点取值为 0~255,boy_mask像素点取值为 0或1
renyao = boy * (1 - boy_mask) + warped_girl * boy_mask

# 为了正常显示,像素值应转换成整型
boy = boy.astype(np.uint8)
warped_girl = warped_girl.astype(np.uint8)
renyao = renyao.astype(np.uint8)

cv2.imshow('boy', boy)
cv2.imshow('warped_girl', warped_girl)
cv2.imshow('renyao', renyao)
cv2.waitKey(0)

运行 change_face(), 即可看见拭目以待的效果!如下,好看不?

从左到右依次为原始boy,调整脸和图片大小后的 girl,换脸后的boy(人妖)。

总结

以上就是换脸Python实现的全部内容了,可以看出换脸后及其不自然,下面一节来完善一下。汇总一下看到最终效果的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import sys, os, glob
import numpy as np
import dlib
import cv2

# 预测器和模型的路径
predictor_path = r'./model/shape_predictor_68_face_landmarks.dat'
face_rec_model_path = r'./model/dlib_face_recognition_resnet_model_v1.dat'
faces_folder_path = r'./faces/'

# 声明一个人脸关键点预测器 predictor,全局变量
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(predictor_path)


# 68个点,分别代表眼,口,鼻等关键点的位置,现在分开一下
LEFT_EYE_POINTS = list(range(42, 48))
RIGHT_EYE_POINTS = list(range(36, 42))
LEFT_BROW_POINTS = list(range(22, 27))
RIGHT_BROW_POINTS = list(range(17, 22))
NOSE_POINTS = list(range(27, 35))
MOUTH_POINTS = list(range(48, 61))

# 我们要用的确定人脸的关键点列表
OVERLAY_POINTS = [
LEFT_EYE_POINTS + RIGHT_EYE_POINTS +
LEFT_BROW_POINTS + RIGHT_BROW_POINTS +
NOSE_POINTS + MOUTH_POINTS,
]

# 特征数
FEATHER_AMOUNT = 11

# 获取脸部关键点
def get_landmark(image):
face_rect = detector(image, 1)
if len(face_rect) != 1:
print('No one face in one picture')
else:
return np.matrix([[p.x, p.y] for p in predictor(image, face_rect[0]).parts()])

# 使用普氏分析调整脸部
# p1,p2分别是两张图的关键点landmarks列表
# 返回结果是从 p2 到 p1 的仿射变换矩阵
def transformation_from_points(p1, p2):
p1 = p1.astype(np.float64)
p2 = p2.astype(np.float64)

c1 = np.mean(p1, axis=0)
c2 = np.mean(p2, axis=0)
p1 -= c1
p2 -= c2

s1 = np.std(p1)
s2 = np.std(p2)

p1 /= s1
p2 /= s2

U, S, Vt = np.linalg.svd(p1.T * p2)
R = (U * Vt).T

trans_mat = np.vstack([np.hstack(((s2 / s1)*R, c2.T-(s2/s1)*R*c1.T)),
np.matrix([0., 0., 1.])])
return trans_mat


# 把 image 变成 dshape 大小,并用仿射矩阵 M 进行变化,这里的M就是上面的 trans_mat
def warp_image(image, M, dshape):
output_image = np.zeros(dshape, dtype=image.dtype)
cv2.warpAffine(image, M[:2], (dshape[1], dshape[0]),
dst=output_image, flags=cv2.WARP_INVERSE_MAP,
borderMode=cv2.BORDER_TRANSPARENT)
return output_image


# 在img上绘制points点列表的凸包,Python参数默认引用,所以此处没用返回值
def draw_convex_hull(img, points, color):
points = cv2.convexHull(points)
cv2.fillConvexPoly(img, points, color)

# 获取人脸掩模
def get_face_mask(img, landmarks):
# 用一张灰度的图片来绘制
img = np.zeros(img.shape[:2], dtype=np.float64)
for group in OVERLAY_POINTS:
draw_convex_hull(img, landmarks[group], color=1)
# 之前的 img 是单通道的灰度图,所以下面有三个 img
img = np.array([img, img, img]).transpose((1, 2, 0))
return img

def change_face():
boy = cv2.imread('faces/boy.jpeg')
girl = cv2.imread('faces/girl.jpeg')
boy_landmarks = get_landmark(boy)
girl_landmarks = get_landmark(girl)

# 获取变化矩阵, OVERLAY_POINTS 是上面定义的关键点下标
trans_mat = transformation_from_points(
boy_landmarks[OVERLAY_POINTS],
girl_landmarks[OVERLAY_POINTS]
)

# 掩模就用 boy_mask就好
boy_mask = get_face_mask(boy, boy_landmarks)
# warped_girl 是一张大小和 boy 一样大,并且脸对应的图片
warped_girl = warp_image(girl, trans_mat, boy.shape)

boy = boy.astype(np.float64)
warped_girl = warped_girl.astype(np.float64)

# 图片相加, boy, girl 的像素点取值为 0~255,boy_mask像素点取值为 0或1
renyao = boy * (1 - boy_mask) + warped_girl * boy_mask

# 为了正常显示,像素值应转换成整型
boy = boy.astype(np.uint8)
warped_girl = warped_girl.astype(np.uint8)
renyao = renyao.astype(np.uint8)

cv2.imshow('boy', boy)
cv2.imshow('warped_girl', warped_girl)
cv2.imshow('renyao', renyao)
cv2.waitKey(0)

change_face()