总序

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

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


第四部分 视频

视频不过是一张一张的图片,在上一篇的基础上,用 opencv 实现读取视频,即可完成换脸。但之前需要做一件视频,就是判断当前帧里面是否只有一个人,代码如下:

1
2
3
4
def find_one_face(image):
# dectetor 是人脸检测器
face_rect = detector(image, 1)
return len(face_rect) == 1

此外,我们新建 change_face_video() 函数来实现显示视频:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def change_face_video(target_face_file, camera=0):
cap = cv2.VideoCapture(camera)
target_face = cv2.imread(target_face_file)
# 为了保存处理后的视频
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('output.avi',fourcc, 20.0, (640,480))

while (cap.isOpened()):
hasFrame, frame = cap.read()
# print(frame.shape, target_face.shape)
new_face = frame
if hasFrame and find_one_face(frame):
new_face = change_face(frame, target_face)
cv2.imshow('new_face', new_face)
out.write(frame) # 把帧写入视频
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.waitKey(0)
cap.release()#释放摄像头
cv2.destroyAllWindows()#删除全部窗口

里面需要调用 change_face() 函数,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
45
def change_face(img1, img2):
boy = img1
girl = 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)

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

完整代码如下,命名成 changeface.py,运行 python3 changeface.py 即可打开摄像头,显示变脸后的图片,可以试试把自己变成二师兄^-^ !

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 变脸完整程序

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()])

def find_one_face(image):
face_rect = detector(image, 1)
return len(face_rect) == 1

# 使用普氏分析调整脸部
# 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

# 根据瞳距进行颜色校正,这是一个经验方法
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 = img1
girl = 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)

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

def change_face_video(target_face_file, camera=0):
cap = cv2.VideoCapture(camera)
target_face = cv2.imread(target_face_file)
# 为了保存处理后的视频
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('output.avi',fourcc, 20.0, (640,480))

while (cap.isOpened()):
hasFrame, frame = cap.read()
# print(frame.shape, target_face.shape)
new_face = frame
if hasFrame and find_one_face(frame):
new_face = change_face(frame, target_face)
cv2.imshow('new_face', new_face)
out.write(frame) # 把帧写入视频
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.waitKey(0)
cap.release()#释放摄像头
cv2.destroyAllWindows()#删除全部窗口

# 把 faces/girl.jpeg 换成你想要变到的脸的图片
change_face_video('faces/girl.jpeg')