简介
基于dlib进行人脸互换操作。
实践
1. 环境准备
# pip install boost | |
# pip install cmake | |
# pip install dlib |
如果仍然安装失败,请参考 https://blog.csdn.net/qq_45070929/article/details/118612849,进行c++相关组件安装教程。
2. 代码如下
# -*- coding: utf-8 -*- | |
""" | |
Created on Fri Aug 4 15:10:26 2017 | |
@author: Quantum Liu | |
reference:https://github.com/matthewearl/faceswap | |
""" | |
# pip install boost | |
# pip install cmake | |
# pip install dlib | |
import sys, os, traceback | |
import cv2 | |
import dlib | |
import numpy as np | |
class TooManyFaces(Exception): | |
''' | |
定位到太多脸 | |
''' | |
pass | |
class NoFace(Exception): | |
''' | |
没脸 | |
''' | |
pass | |
class Faceswapper(): | |
''' | |
人脸交换器类 | |
实例化时载入多个头照片资源 | |
''' | |
def __init__(self, heads_list=[], predictor_path="H:/FaceSwapper/shape_predictor_68_face_landmarks.dat"): | |
''' | |
head_list:./data/shape_predictor_68_face_landmarks.dat | |
头(背景和发型)来源图片的路径的字符串列表,根据此列表在实例化时载入多个头像资源, | |
并获得面部识别点坐标,以字典形式存储,键名为文件名 | |
predictor_path: | |
dlib资源的路径 | |
''' | |
# 五官等标记点 | |
self.PREDICTOR_PATH = predictor_path | |
self.FACE_POINTS = list(range(17, 68)) | |
self.MOUTH_POINTS = list(range(48, 61)) | |
self.RIGHT_BROW_POINTS = list(range(17, 22)) | |
self.LEFT_BROW_POINTS = list(range(22, 27)) | |
self.RIGHT_EYE_POINTS = list(range(36, 42)) | |
self.LEFT_EYE_POINTS = list(range(42, 48)) | |
self.NOSE_POINTS = list(range(27, 35)) | |
self.JAW_POINTS = list(range(0, 17)) | |
# 人脸的完整标记点 | |
self.ALIGN_POINTS = (self.LEFT_BROW_POINTS + self.RIGHT_EYE_POINTS + self.LEFT_EYE_POINTS + | |
self.RIGHT_BROW_POINTS + self.NOSE_POINTS + self.MOUTH_POINTS) | |
# 来自第二张图(脸)的标记点,眼、眉、鼻子、嘴,这一部分标记点将覆盖第一张图的对应标记点 | |
self.OVERLAY_POINTS = [ | |
self.LEFT_EYE_POINTS + self.RIGHT_EYE_POINTS + self.LEFT_BROW_POINTS + self.RIGHT_BROW_POINTS, | |
self.NOSE_POINTS + self.MOUTH_POINTS] | |
# 颜色校正参数 | |
self.COLOUR_CORRECT_BLUR_FRAC = 0.6 | |
# 人脸定位、特征提取器,来自dlib | |
self.detector = dlib.get_frontal_face_detector() | |
self.predictor = dlib.shape_predictor(self.PREDICTOR_PATH) | |
# 头像资源 | |
self.heads = {} | |
if heads_list: | |
self.load_heads(heads_list) | |
def load_heads(self, heads_list): | |
''' | |
根据head_list添加更多头像资源 | |
''' | |
self.heads.update({os.path.split(name)[-1]: (self.read_and_mark(name)) for name in heads_list}) | |
def get_landmarks(self, im, fname, n=1): | |
''' | |
人脸定位和特征提取,定位到两张及以上脸或者没有人脸将抛出异常 | |
im: | |
照片的numpy数组 | |
fname: | |
照片名字的字符串 | |
返回值: | |
人脸特征(x,y)坐标的矩阵 | |
''' | |
rects = self.detector(im, 1) | |
if len(rects) > n: | |
raise TooManyFaces('No face in ' + fname) | |
if len(rects) < 0: | |
raise NoFace('Too many faces in ' + fname) | |
return np.matrix([[p.x, p.y] for p in self.predictor(im, rects[0]).parts()]) | |
def read_im(self, fname, scale=1): | |
''' | |
读取图片 | |
''' | |
# ============================================================================= | |
# im = cv2.imread(fname, cv2.IMREAD_COLOR) | |
# ============================================================================= | |
im = cv2.imdecode(np.fromfile(fname, dtype=np.uint8), -1) | |
if type(im) == type(None): | |
print(fname) | |
raise ValueError('Opencv read image {} error, got None'.format(fname)) | |
return im | |
def read_and_mark(self, fname): | |
im = self.read_im(fname) | |
return im, self.get_landmarks(im, fname) | |
def resize(self, im_head, landmarks_head, im_face, landmarks_face): | |
''' | |
根据头照片和脸照片的大小(分辨率)调整图片大小,增强融合效果 | |
''' | |
scale = np.sqrt((im_head.shape[0] * im_head.shape[1]) / (im_face.shape[0] * im_face.shape[1])) | |
if scale > 1: | |
im_head = cv2.resize(im_head, (int(im_head.shape[1] / scale), int(im_head.shape[0] / scale))) | |
landmarks_head = (landmarks_head / scale).astype(landmarks_head.dtype) | |
else: | |
im_face = cv2.resize(im_face, (int(im_face.shape[1] * scale), int(im_face.shape[0] * scale))) | |
landmarks_face = (landmarks_face * scale).astype(landmarks_face.dtype) | |
return im_head, landmarks_head, im_face, landmarks_face | |
def draw_convex_hull(self, im, points, color): | |
''' | |
勾画多凸边形 | |
''' | |
points = cv2.convexHull(points) | |
cv2.fillConvexPoly(im, points, color=color) | |
def get_face_mask(self, im, landmarks, ksize=(11, 11)): | |
''' | |
获得面部遮罩 | |
''' | |
mask = np.zeros(im.shape[:2], dtype=np.float64) | |
for group in self.OVERLAY_POINTS: | |
self.draw_convex_hull(mask, | |
landmarks[group], | |
color=1) | |
mask = np.array([mask, mask, mask]).transpose((1, 2, 0)) | |
mask = (cv2.GaussianBlur(mask, ksize, 0) > 0) * 1.0 | |
mask = cv2.GaussianBlur(mask, ksize, 0) | |
return mask | |
def transformation_from_points(self, points1, points2): | |
""" | |
Return an affine transformation [s * R | T] such that: | |
sum ||s*R*p1,i + T - p2,i||^2 | |
is minimized. | |
计算仿射矩阵 | |
参考:https://github.com/matthewearl/faceswap/blob/master/faceswap.py | |
""" | |
# Solve the procrustes problem by subtracting centroids, scaling by the | |
# standard deviation, and then using the SVD to calculate the rotation. See | |
# the following for more details: | |
# https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem | |
points1 = points1.astype(np.float64) | |
points2 = points2.astype(np.float64) | |
c1 = np.mean(points1, axis=0) | |
c2 = np.mean(points2, axis=0) | |
points1 -= c1 | |
points2 -= c2 | |
s1 = np.std(points1) | |
s2 = np.std(points2) | |
points1 /= s1 | |
points2 /= s2 | |
U, S, Vt = np.linalg.svd(points1.T * points2) | |
# The R we seek is in fact the transpose of the one given by U * Vt. This | |
# is because the above formulation assumes the matrix goes on the right | |
# (with row vectors) where as our solution requires the matrix to be on the | |
# left (with column vectors). | |
R = (U * Vt).T | |
return np.vstack([np.hstack(((s2 / s1) * R, | |
c2.T - (s2 / s1) * R * c1.T)), | |
np.matrix([0., 0., 1.])]) | |
def warp_im(self, im, M, dshape): | |
''' | |
人脸位置仿射变换 | |
''' | |
output_im = np.zeros(dshape, dtype=im.dtype) | |
cv2.warpAffine(im, | |
M[:2], | |
(dshape[1], dshape[0]), | |
dst=output_im, | |
borderMode=cv2.BORDER_TRANSPARENT, | |
flags=cv2.WARP_INVERSE_MAP) | |
return output_im | |
def correct_colours(self, im1, im2, landmarks_head): | |
''' | |
颜色校正 | |
''' | |
blur_amount = int(self.COLOUR_CORRECT_BLUR_FRAC * np.linalg.norm( | |
np.mean(landmarks_head[self.LEFT_EYE_POINTS], axis=0) - | |
np.mean(landmarks_head[self.RIGHT_EYE_POINTS], axis=0))) | |
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) | |
im2_blur += (128 * (im2_blur <= 1.0)).astype(im2_blur.dtype) | |
return im2.astype(np.float64) * im1_blur.astype(np.float64) / im2_blur.astype(np.float64) | |
def swap(self, head_name, face_path): | |
''' | |
主函数 人脸交换 | |
head_name: | |
头资源的键名字符串 | |
face_path: | |
脸来源的图像路径名 | |
''' | |
im_head, landmarks_head, im_face, landmarks_face = self.resize(*self.heads[head_name], | |
*self.read_and_mark(face_path)) | |
M = self.transformation_from_points(landmarks_head[self.ALIGN_POINTS], | |
landmarks_face[self.ALIGN_POINTS]) | |
face_mask = self.get_face_mask(im_face, landmarks_face) | |
warped_mask = self.warp_im(face_mask, M, im_head.shape) | |
combined_mask = np.max([self.get_face_mask(im_head, landmarks_head), warped_mask], | |
axis=0) | |
warped_face = self.warp_im(im_face, M, im_head.shape) | |
warped_corrected_im2 = self.correct_colours(im_head, warped_face, landmarks_head) | |
out = im_head * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask | |
return out | |
def save(self, output_path, output_im): | |
''' | |
保存图片 | |
''' | |
cv2.imencode('.jpg', output_im)[1].tofile(output_path) | |
# ============================================================================= | |
# cv2.imwrite(os.path.abspath(output_path.encode('utf-8').decode('gbk')), output_im) | |
# ============================================================================= | |
if __name__ == '__main__': | |
''' | |
命令行运行: | |
python faceswapper.py <头路径> <脸路径> <输出图片路径>(可选,默认./output.jpg) | |
''' | |
head, face_path, out = sys.argv[1], sys.argv[2], (sys.argv[3] if len(sys.argv) >= 4 else 'output.jpg') | |
swapper = Faceswapper([head]) | |
output_im = swapper.swap(os.path.split(head)[-1], face_path) # 返回的numpy数组 | |
swapper.save(out, output_im) | |
output_im[output_im > 254.9] = 254.9 | |
# cv2.imshow('',output_im.astype('uint8')) | |
# cv2.waitKey() |
小结
参考: https://github.com/matthewearl/faceswap https://gitee.com/wllsxz/mask-changing/blob/master/faceswapper.py
安装教程:https://blog.csdn.net/qq_45070929/article/details/118612849