总序

“换脸”Python实现共分为四个部分,第一部分是原理讲解,第二部分是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
# 根据瞳距进行颜色校正,这是一个经验方法
COLOUR_CORRECT_BLUR_FRAC = 0.6
def color_correct(im1, im2, landmarks1):
# 根据左右眼之间的距离,乘以0.6,为高斯核的大小
blur_amount = COLOUR_CORRECT_BLUR_FRAC * np.linalg.norm(
np.mean(landmarks1[LEFT_EYE_POINTS], axis=0) -
np.mean(landmarks1[RIGHT_EYE_POINTS], axis=0))
blur_amount = int(blur_amount)
if blur_amount % 2 == 0:
blur_amount += 1
im1_blur = cv2.GaussianBlur(im1, (blur_amount, blur_amount), 0)
im2_blur = cv2.GaussianBlur(im2, (blur_amount, blur_amount), 0)

# 避免后面除以0,设一个 im2_blur_2
im2_blur[im2_blur < 1] = 1

im2 = im2.astype(np.float64)
im1_blur = im2.astype(np.float64)
im2_blur = im2_blur.astype(np.float64)
# 类似 A * B / A
im2_color_correct = im2 * im1_blur / im2_blur
im2_color_correct[im2_color_correct < 0] = 0
im2_color_correct[im2_color_correct > 255] = 255
return im2_color_correct.astype(np.uint8)

上面的这个函数可以把图丙的脸的亮度变成我们需要的亮度,效果如下,最右面一张是亮度校准后的图片,希望的是和左面的boy的脸亮度一致,小姑娘的脸果然变黑了:

以上就换脸差不多了,看一下效果:

感觉还是有一丝丝的不和谐,头发被截下来,这个处理不了,不过脸的边缘可以再和谐一下。这可以通过把掩模用高斯核处理一下来完成,学了这么多,这个原因就不解释了,还不懂就说明没认真思考。

最终 change_face() 函数的代码如下:

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
def change_face(img1, img2):
boy = cv2.imread(img1)
girl = cv2.imread(img2)
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 和 warped_girl 的掩模,再取两个掩模的白色部分的并集
# 这样得到的掩模 combined_mask 就包含了两张脸所在的范围
boy_mask = get_face_mask(boy, boy_landmarks)
girl_mask = get_face_mask(girl, girl_landmarks)
warped_girl_mask = warp_image(girl_mask, trans_mat, boy.shape)
combined_mask = np.max([boy_mask, warped_girl_mask], axis=0)

# 为了让整容的脸衔接更好,把掩模边缘进行高斯模糊一下,核大小可以自己试着取
combined_mask = cv2.GaussianBlur(combined_mask, (19, 19), 0)
combined_mask = cv2.GaussianBlur(combined_mask, (13, 13), 0)
combined_mask = cv2.GaussianBlur(combined_mask, (7, 7), 0)

# warped_girl 是一张大小和 boy 一样大,并且脸对应的图片
warped_girl = warp_image(girl, trans_mat, boy.shape)
warped_girl_color_correct = color_correct(boy, warped_girl, boy_landmarks)
boy = boy.astype(np.float64)
warped_girl = warped_girl.astype(np.float64)

# 图片相加, boy, girl 的像素点取值为 0~255,boy_mask像素点取值为 0或1
renyao = boy * (1 - combined_mask) + warped_girl_color_correct * combined_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('faces/boy.jpeg', 'faces/girl.jpeg')

效果如下,因为有头发原因,上不不是很好,但脸部基本可以:

1565590765647

下面还有一个猴哥和八戒的变换:

把大师兄试着换成自己头像吧,enjoy it!

完整代码如下,除了增加 color_correct() 函数已经改动一下 change_face()外,其他函数和第(2)部分的一样:

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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

# Amount of blur to use during colour correction, as a fraction of the
# pupillary distance. (pupillary:瞳距)
# 根据瞳距进行颜色校正,这是一个经验方法
COLOUR_CORRECT_BLUR_FRAC = 0.6
def color_correct(im1, im2, landmarks1):
# 根据左右眼之间的距离,乘以0.6,为高斯核的大小
blur_amount = COLOUR_CORRECT_BLUR_FRAC * np.linalg.norm(
np.mean(landmarks1[LEFT_EYE_POINTS], axis=0) -
np.mean(landmarks1[RIGHT_EYE_POINTS], axis=0))
blur_amount = int(blur_amount)
if blur_amount % 2 == 0:
blur_amount += 1
im1_blur = cv2.GaussianBlur(im1, (blur_amount, blur_amount), 0)
im2_blur = cv2.GaussianBlur(im2, (blur_amount, blur_amount), 0)

# 避免后面除以0,设一个 im2_blur_2
im2_blur[im2_blur < 1] = 1

im2 = im2.astype(np.float64)
im1_blur = im1_blur.astype(np.float64)
im2_blur = im2_blur.astype(np.float64)
# 类似 A * B / A
im2_color_correct = im2 * im1_blur / im2_blur
im2_color_correct[im2_color_correct < 0] = 0
im2_color_correct[im2_color_correct > 255] = 255
return im2_color_correct.astype(np.uint8)

def change_face(img1, img2):
boy = cv2.imread(img1)
girl = cv2.imread(img2)
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 和 warped_girl 的掩模,再取两个掩模的白色部分的并集
# 这样得到的掩模 combined_mask 就包含了两张脸所在的范围
boy_mask = get_face_mask(boy, boy_landmarks)
girl_mask = get_face_mask(girl, girl_landmarks)
warped_girl_mask = warp_image(girl_mask, trans_mat, boy.shape)
combined_mask = np.max([boy_mask, warped_girl_mask], axis=0)

# 为了让整容的脸衔接更好,把掩模边缘进行高斯模糊一下,核大小可以自己试着取
combined_mask = cv2.GaussianBlur(combined_mask, (19, 19), 0)
combined_mask = cv2.GaussianBlur(combined_mask, (13, 13), 0)
combined_mask = cv2.GaussianBlur(combined_mask, (7, 7), 0)

# warped_girl 是一张大小和 boy 一样大,并且脸对应的图片
warped_girl = warp_image(girl, trans_mat, boy.shape)
warped_girl_color_correct = color_correct(boy, warped_girl, boy_landmarks)
boy = boy.astype(np.float64)
warped_girl = warped_girl.astype(np.float64)

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

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

cv2.imshow('Wukong Sun', boy)
cv2.imshow('Bajie Zhu', warped_girl)
cv2.imshow('Who are you?', renyao)
cv2.waitKey(0)

change_face('faces/swk.jpeg', 'faces/zbj_2.jpeg')