介绍本文将介绍如何用仅200行的Python脚本来为两张人像“换脸”。这个过程可以分为四个步骤:检测面部标志。旋转、缩放和平移第二个图像以适合第一个图像。调整第二张图片的色彩平衡以匹配第一张。将第二张图像的特征混合到第一张图像中。完整的源代码可以从这里下载:https://github.com/matthewearl/faceswap/blob/master/faceswap.py1.使用dlib提取面部标记此脚本使用dlib的Python绑定来提取面部标记:Dlib实现了论文OneMillisecondFaceAlignmentwithanEnsembleofRegressionTrees中的算法(http://www.csc.kth.se/~vahidk/papers/KazemiCVPR14.pdf作者:VahidKazemi和JosephineSullivan)。算法本身很复杂,但是dlib接口使用起来很简单:PREDICTOR_PATH="/home/matt/dlib-18.16/shape_predictor_68_face_landmarks.dat"detector=dlib.get_frontal_face_detector()predictor=dlib.shape_predictor(PREDICTOR_PATH)defget_landmarks(im):rects=检测器(im,1)iflen(rects)>1:raiseTooManyFacesiflen(rects)==0:raiseNoFacesreturnnumpy.matrix([[p.x,p.y]forpinpredictor(im,rects[0]).parts()])get_landmarks()函数将图像转换为numpy数组并返回一个68x2的元素矩阵,输入图像的每个特征点对应每一行的一个x,y坐标。特征提取器(预测器)需要一个粗略的边界框作为算法的输入,由传统的人脸检测器(检测器)提供,该检测器返回一个矩形列表,每个矩形对应于图像中的一张脸。为了构建特征提取器,预训练模型是必不可少的,相关模型可以从dlibsourceforge库下载(http://sourceforge.net/projects/dclib/files/dlib/v18.10/shape_predictor_68_face_landmarks.dat.bz2)。2.用Procrustes分析调整人脸现在我们有两个标记矩阵,每一行都有一组坐标对应特定的面部特征(比如第30行给出的鼻子坐标)。我们现在必须弄清楚如何旋转、平移和缩放第一个向量,以便它们尽可能接近第二个向量的点。这个想法是第二张图片可以用相同的变换叠加在第一张图片上。更数学地讲,找到使以下表达式的结果最小化的T、s和R:R是2x2正交矩阵,s是标量,T是二维向量,pi和qi是矩阵中标记的行多于。事实证明,这类问题可以用普通的Procrustes分析来解决:deftransformation_from_points(points1,points2):points1=points1.astype(numpy.float64)points2=points2.astype(numpy.float64)c1=numpy.mean(points1,axis=0)c2=numpy.mean(points2,axis=0)points1-=c1points2-=c2s1=numpy.std(points1)s2=numpy.std(points2)points1/=s1points2/=s2U,S,Vt=numpy.linalg.svd(points1.T*points2)R=(U*Vt).Treturnnumpy.vstack([numpy.hstack(((s2/s1)*R,c2.T-(s2/s1)*R*c1.T)),numpy.matrix([0.,0.,1.])])代码分别实现了以下步骤:将输入矩阵转换为浮点数。这是后续步骤的必要条件。每个点集都从其质心中减去。一旦为这两个新的点集找到了最佳缩放和旋转方法,就可以使用两个质心c1和c2来找到完整的解决方案。同样,每个点集除以其标准偏差。这消除了有问题的组件缩放偏差。使用奇异值分解计算旋转部分。有关解决正交Procrustes问题的详细信息,请参见维基百科(https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem)。使用仿射变换矩阵返回完整变换(https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations)。之后,可以将结果插入到OpenCV的cv2.warpAffine函数中,将图像二映射到图像一:defwarp_im(im,M,dshape):output_im=numpy.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)返回output_im图像对齐结果如下:COLOUR_CORRECT_BLUR_FRAC=0.6LEFT_EYE_POINTS=list(range(42,48))RIGHT_EYE_POINTS=list(range(36,42))如果我们试图直接覆盖面部特征,我们很快就会发现一个问题:两幅图像之间不同的肤色和光照导致边缘不连续覆盖面积。我们尝试纠正:COLOUR_CORRECT_BLUR_FRAC=0.6LEFT_EYE_POINTS=list(range(42,48))RIGHT_EYE_POINTS=list(range(36,42))defcorrect_colours(im1,im2,landmarks1):blur_amount=COLOUR_CORRECT_BLUR_FRAC*numpy.linalg.norm(numpy.mean(landmarks1[LEFT_EYE_POINTS],axis=0)-numpy.mean(landmarks1[RIGHT_EYE_POINTS],axis=0))blur_amount=int(blur_amount)ifblur_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)#Avoiddivide-by-zeroerrors.im2_blur+=128*(im2_blur<=1.0)return(im2.astype(numpy.float64))*im1_blur.astype(numpy.float64)/im2_blur.astype(numpy.float64))结果是这样的:这个函数试图改变图像2的颜色以匹配图像1。它的工作原理是将im2除以im2的高斯模糊,然后乘以im1的高斯模糊。这里的想法是用RGB来缩放色彩校正,但不是对所有图像使用一个整体恒定的比例因子,而是每个像素都有自己的局部比例因子。通过这种方式只能在一定程度上校正两幅图像之间的光照差异。例如,如果图像1从一侧被照亮,但图像2被均匀照亮,则经过颜色校正后图像2在未照亮的一侧也会显得更暗。也就是说,这是一种相当粗略的方法,解决问题的关键是适当的高斯核大小。如果它太小,第一张图像中的面部特征将显示在第二张图像中。如果太大,核外的像素点会被覆盖而变色。这里的内核使用0.6*的瞳孔距离。4.将第二幅图像的特征混合到第一幅图像中使用遮罩选择图像2和图像1的哪些部分应该是最终显示的图像:值1(白色)图像2应该显示的区域值是0(黑色)是图像1应该显示的区域。0到1之间的一个值是image1和image2的混合区域。这是生成上面那个张图的代码: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=11defdraw_convex_point(im,convex_point)颜色):points=cv2.convexHull(points)cv2.fillConvexPoly(im,points,color=color)defget_face_mask(im,landmarks):im=numpy.zeros(im.shape[:2],dtype=numpy.float64)forgroupinOVERLAY_POINTS:draw_convex_hull(im,landmarks[group],color=1)im=numpy.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)returnimmask=get_face_mask(im2,landmarks2)warped_mask=warp_im(mask,M,im1.shape)combined_mask=numpy.max([get_face_mask(im1,landmarks1),warped_mask],axis=0)我们把上面描述的代码解析:get_face_mask()的定义为一图像和标记矩阵以生成绘制两个白色凸多边形的蒙版:一个用于眼睛周围的区域,一个用于鼻子和嘴巴周围的区域。羽化延伸,可以帮助隐藏任何不连续的区域。使用与步骤2中相同的变换,同时为两个图像生成这样的掩码,将图像2的掩码转换为图像1的坐标空间。之后,两个掩码通过一个元素合并为一个-明智的价值。组合两个蒙版确保图像1被蒙版,同时显示图像2的属性。***,应用蒙版得到最终图像:output_im=im1*(1.0-combined_mask)+warped_corrected_im2*combined_mask
