前言
想着整理一下换脸相关的技术方法,免得以后忘记了,最近脑袋越来越不好使了。应该会包含三个系列: 仅换眼口鼻;换整个面部;3D换脸
先看看2D换脸吧,网上已经有现成的教程了,这里拿过来整理一下,做个记录。
国际惯例,参考博客:
Switching Eds: Face swapping with Python, dlib, and OpenCV
opencv
人脸关键点检测模型
这里面一般涉及到脸与脸之间的映射变换矩阵,这里记录一个opencv
中的函数findHomography
,用于找到多个二维点之间的最优变换矩阵。
流程
整个换脸包含四步:
- 检测人脸关键点
- 旋转、缩放、平移第二张图片,使其与第一张图片的人脸对齐
- 调整第二张图片的色彩平衡,使其与第一张图片对应
- 将两张图片融合
先加载一些必要的库
import cv2
import numpy as np
import matplotlib.pyplot as plt
检测人脸关键点
使用dlib
库检测人脸关键点,不过由于dlib的安装貌似必须有cmake和c++编译器,个人电脑暂时没有,所以使用opencv
的人脸关键点检测算法
先去这里下载人脸框检测模型haarcascade_frontalface_alt2.xml,这里下载人脸关键点检测模型LBF.model,然后预加载模型:
cas = cv2.CascadeClassifier('./model/haarcascade_frontalface_alt2.xml')
obj = cv2.face.createFacemarkLBF()
obj.loadModel('./model/lbfmodel.yaml')
检测人脸关键点:
def detect_facepoint(img):img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)faces = cas.detectMultiScale(img_gray,2,3,0,(30,30))landmarks = obj.fit(img_gray,faces)assert landmarks[0],'no face detected'if(len(landmarks[1])>1):print('multi face detected,use the first')return faces[0],np.squeeze(landmarks[1][0])
画人脸关键点
def draw_kps(img,face_box,kps):img_show = img.copy()cv2.rectangle(img_show,(face_box[0],face_box[1]),(face_box[0]+face_box[2],face_box[1]+face_box[3]),(0,255,0),3)for i in range(kps.shape[0]):cv2.circle(img_show,(kps[i,0],kps[i,1]),2,(0,255,0),-1)img_show = cv2.cvtColor(img_show,cv2.COLOR_BGR2RGB)return img_show
测试一波
人脸对齐
有关键点以后,需要将两个关键点的大小形状变换地尽可能接近才能进行换脸,主要涉及到旋转、平移、缩放。假设定义如下变量:旋转R
、平移T
、缩放S
,第一个人脸关键点是qi(i=1⋯68)q_i(i=1\cdots 68)qi(i=1⋯68),第二个人脸关键点是pi(i=1⋯68)p_i(i=1\cdots 68)pi(i=1⋯68),那么我们需要最小化
∑i=168∥sRpiT+T−qiT∥2\sum_{i=1}^{68} \parallel sRp_i^T+T-q_i^T \parallel ^2 i=1∑68∥sRpiT+T−qiT∥2
也就是关键点ppp经过旋转、缩放、平移以后,要和关键点qqq相近
以下对齐顺序是将第二个图像与第一个图像对齐
方法1:SVD分解+仿射变换(推荐)
这里原文提到了一个Ordinary Procrustes Analysis的算法,专门用于提取这个旋转矩阵的,用的SVD
分解方法:
def transformation_from_points(points1, points2):points1 = points1.copy()points2 = points2.copy()points1 = points1.astype(np.float64)points2 = points2.astype(np.float64)c1 = np.mean(points1, axis=0)c2 = np.mean(points2, axis=0)points1 -= c1points2 -= c2s1 = np.std(points1)s2 = np.std(points2)points1 /= s1points2 /= s2U, S, Vt = np.linalg.svd(np.dot(points1.T , points2))R = (np.dot(U , Vt)).T return np.vstack([np.hstack(((s2 / s1) * R,np.array([c2.T - np.dot((s2 / s1) * R , c1.T)]).T )),np.array([0., 0., 1.])])
得到变换矩阵以后,就可以使用opencv
的warpAffine
进行仿射变换
def wrap_im(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 align_img1(img1,img2,landmarks1,landmarks2):trans_mat = transformation_from_points(landmarks1, landmarks2)img2_align = wrap_im(img2,trans_mat,img1.shape)return img2_align
额外加一个比较好的SVD
理论证明
方法2:透视变换(不推荐)
这篇文章提到的一个方法,直接用opencv
的findHomography
找到变换矩阵,再用warpPerspective
做透视变换。函数如下:
def align_img2(img1,img2,landmarks1,landmarks2):trans_mat,mask = cv2.findHomography(landmarks2, landmarks1, cv2.RANSAC,5.0)img2_align = cv2.warpPerspective(img2.copy(),trans_mat,(img1.shape[1],img1.shape[0]))return img2_align
对比
分别调用对比看看
img2_align = align_img1(img1,img2,face_kps1,face_kps2)
plt.figure(figsize=[8,8])
plt.subplot(131)
plt.imshow(cv2.cvtColor(img1.copy(),cv2.COLOR_BGR2RGB))
plt.subplot(132)
plt.imshow(cv2.cvtColor(img2_align.copy(),cv2.COLOR_BGR2RGB))
img2_align2 = align_img2(img1,img2,face_kps1,face_kps2)
plt.subplot(133)
plt.imshow(cv2.cvtColor(img2_align2.copy(),cv2.COLOR_BGR2RGB))
这么一看,咱们还是用方法1的仿射变换吧,咳咳。透视变换功能过于强大。
获取被替换部分的掩膜
按照参考博客和本博客的目的,只替换眼、鼻、口
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 = 11def draw_convex_hull(im, points, color):points = cv2.convexHull(points)cv2.fillConvexPoly(im, points, color=color)def get_face_mask(im, landmarks):im = np.zeros(im.shape[:2], dtype=np.float64)#双眼的外接多边形、鼻和嘴的多边形,作为掩膜for group in OVERLAY_POINTS:draw_convex_hull(im,landmarks[group],color=1)im = np.array([im, im, im]).transpose((1, 2, 0))im = (cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0) > 0) * 1.0im = cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0)return im
提取完mask,当然需要将mask也按照仿射变换的方法进行校正
mask = get_face_mask(img2, face_kps2)
warped_mask = align_img1(img1, mask,face_kps1,face_kps2)
combined_mask = np.max([get_face_mask(img1, face_kps1), warped_mask],axis=0)
可视化
plt.imshow(np.multiply(img2_align,combined_mask).astype('uint8')[...,::-1])
颜色校正
直接贴图,肯定会由于颖宝和老干部的肤色不搭,会有非常明显的贴图边缘痕迹。
output_im = img1 * (1.0 - combined_mask) + img2_align * combined_mask
很明显,mask的边缘部分,有非常明显的颜色差
所以需要矫正一波颜色,在后续博客中,会提供其它颜色校正方法。
COLOUR_CORRECT_BLUR_FRAC = 0.6
LEFT_EYE_POINTS = list(range(42, 48))
RIGHT_EYE_POINTS = list(range(36, 42))def correct_colours(im1, im2, landmarks1):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 += 1im1_blur = cv2.GaussianBlur(im1, (blur_amount, blur_amount), 0)im2_blur = cv2.GaussianBlur(im2, (blur_amount, blur_amount), 0)# Avoid divide-by-zero errors.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))
矫正完毕以后:
img2_correct = correct_colours(img1,img2_align,face_kps1)
plt.imshow(cv2.cvtColor(img2_correct.copy().astype('uint8'),cv2.COLOR_BGR2RGB))
plt.axis('off')
再来瞅瞅结果:
output_im = img1 * (1.0 - combined_mask) + img2_correct * combined_mask
好了,边缘基本没有贴图痕迹了,但是整体效果。。。。。。。。
打扰了~~
后面用其它算法优化
后记
简单的记录一下只替换五官的情况下,整个换脸算法的流程。
博客代码:
链接:https://pan.baidu.com/s/1a-TNuIVslJiOTWgKse58Ag
提取码:kabv
本文已经同步到微信公众号中,公众号与本博客将持续同步更新运动捕捉、机器学习、深度学习、计算机视觉算法,敬请关注