简介在这篇文章中,我将介绍如何编写一个简短的(200行)Python脚本来自动将一张图片的人脸替换为另一张图片的人脸。这个过程有四个步骤:检测面部标记。旋转、缩放、平移和第二张图片以匹配第一步。调整第二张图像的色彩平衡以匹配第一张图像。将第二张图像的特征混合到第一张图像中。1.使用dlib提取面部标记此脚本使用dlib的Python绑定来提取面部标记:Dlib实现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数组,返回一个68×2的元素矩阵。输入图像的每个特征点对应于每一行的一个x,y坐标。特征提取器(预测器)需要一个粗边界框作为算法的输入,由传统的人脸检测器(检测器)提供,该检测器返回一个矩形列表,每个矩形对应于图像中的一张脸。2.用Procrustes分析调整人脸现在我们有两个标记矩阵,每一行都有一组坐标对应一个特定的面部特征(比如第30行的坐标对应鼻子)。我们现在必须弄清楚如何旋转、平移和缩放第一个向量,以便它们尽可能接近第二个向量的点。一个想法是用相同的变换将第二个图像叠加在第一个图像之上。将此问题数学化,求出T、s和R,使得如下表达式:结果最小,其中R为2×2正交矩阵,s为标量,T为二维向量,pi和qi为上面标记的矩阵OK。事实证明,这种问题可以通过“常规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-=cs1=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问题的详细信息,请参见维基百科。使用仿射变换矩阵返回完整的变换。结果可以插入到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)returnoutput_im对齐结果如下:3.修正第二张图片的颜色如果我们尝试直接叠加面部特征,很快就会看到问题:问题是两张图像之间不同的肤色和光照导致叠加区域的边缘不连续。我们尝试纠正: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))结果如下:这个函数试图改变im2的颜色以适应im1。它的工作原理是将im2除以im2的高斯模糊值,然后乘以im1的高斯模糊值。这里的想法是用RGB来缩放色彩校正,但不是对所有图像使用一个整体恒定的比例因子,而是每个像素都有自己的局部比例因子。两幅图像之间的光照差异只能通过这种方式在一定程度上得到校正。例如,如果图像1从一侧被照亮,但图像2被均匀照亮,则经过颜色校正后图像2在未照亮的一侧也会显得更暗。也就是说,这是一个比较粗略的解法,解决问题的关键是合适的高斯核函数大小。如果它太小,第一张图像中的面部特征将显示在第二张图像中。如果太大,核外的像素点会被覆盖而变色。这里的内核使用0.6*的瞳孔距离。4.将第二张图片的特征混合到第一张图片中,使用mask来选择image2和image1的哪些部分应该是最终显示的图片:其中值为1(显示为白色)的区域就是image2中的区域应该显示的,值为0(显示为黑色)的区域就是image1应该显示的区域。0到1之间的一个值是image1和image2的混合区域。我们来分解一下上面的过程:get_face_mask()的定义是为一张图片生成mask和一个marker矩阵,里面画了两个白色的凸多边形:一个是眼睛周围的区域,一个是鼻子和嘴巴周围的区域.然后,它向蒙版边缘外侧羽化11个像素,这有助于隐藏任何不连续性。使用与步骤2中相同的变换,同时为两个图像生成这样的掩码,将图像2的掩码转换为图像1的坐标空间。之后,两个掩码通过一个元素合并为一个-明智的最大。结合两个蒙版确保图像1被蒙版,同时显示图像2的属性。最后,使用蒙版获得最终图像:output_im=im1*(1.0-combined_mask)+warped_corrected_im2*combined_mask
