简介在这篇文章中,我将介绍如何编写一个简短的(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为上面标记的矩阵确定。事实证明,这种问题可以通过“常规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.])])代码实现了这些步骤:1.将输入矩阵转换为浮点数。这是后续操作的基础。2.从每个点集中减去它的质心。一旦为点集找到了最佳缩放和旋转方法,就可以使用这两个质心c1和c2来找到完整的解决方案。3.同样,每个点集除以其标准差。这消除了组件缩放偏差的问题。4.使用奇异值分解计算旋转部分。有关求解正交Procrustes问题的详细信息,请参见维基百科。5.使用仿射变换矩阵返回完整的变换。结果可以插入到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(显示为黑色)的区域是图像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_im,x_hull():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()的定义是为图像和标记矩阵生成一个蒙版,该蒙版绘制两个白色凸多边形:一个用于眼睛周围的区域,一个用于鼻子和嘴巴周围的区域,然后它向蒙版边缘延伸11个像素。外部羽化延伸,可以帮助隐藏任何不连续的区域。使用与步骤2中相同的变换,同时为两个图像生成这样的掩码,将图像2的掩码转换为图像1的坐标空间。之后,两个掩码通过一个元素合并为一个-明智的价值。组合这两个蒙版可确保在显示图像2的属性的同时对图像1进行蒙版。***,使用使用遮罩得到最终:output_im=im1*(1.0combined_mask)+Warped_corrected_im2*compined_mask_masklinklink):importcv2importdlibimportsymportsymportsymportsys_path_=11FACE_POINTS=列表(范围(17,68))MOUTH_POINTS=列表(范围(48,61))RIGHT_BROW_POINTS=列表(范围(17,22))LEFT_BROW_POINTS=列表(范围(22,27))RIGHT_EYE_POINTS=列表(范围(36,42))LEFT_EYE_POINTS=list(range(42,48))NOSE_POINTS=list(range(27,35))JAW_POINTS=list(range(0,17))#Pointsusedtolineuptheimages.ALIGN_POINTS=(LEFT_BROW_POINTS+RIGHT_EYE_POINTS+LEFT_EYE_POINTS+RIGHT_BROW_POINTS+NOSE_POINTS+MOUTH_POINTS)#Pointsfromthesecondimagetooverlayonthefirst.Theconvexhullofeach#elementwillbeoverlaid.OVERLAY_POINTS=[LEFT_EYE_POINTS+RIGHT_EYE_POINTS+LEFT_BROW_POINTS+RIGHT_BROW_POINTS,NOSE_POINTS+MOUTH_POINTS,]#Amountofblurtouseduringcolourcorrection,asafractionofthe#pupillarydistance.COLOUR_CORRECT_BLUR_FRAC=0.6detector=dlib.get_frontal_face_detector()predictor=dlib.shape_predictor(PREDICTOR_PATH)classTooManyFaces(例外):passclassNoFaces(例外):passdefget_landmarks(im):rects=检测器(im,1)iflen(rects)>1:raiseTooManyFacesiflen(rects)==0:raiseNoFacesreturnnumpy.matrix([[p.x,p.y]forpinpredictor(im,rects[0]).parts()])defannotate_landmarks(im,landmarks):im=im.copy()foridx,pointinenumerate(landmarks):pos=(点[0,0],点[0,1])cv2.putText(im,str(idx),pos,fontFace=cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,fontScale=0.4,color=(0,0,255))cv2.circle(im,pos,3,color=(0,255,255))returnimdefdraw_convex_hull(im,points,color):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)returnimdeftransformation_from_points(points1,points2):"""返回仿射变换[s*R|T]suchthat:sum||s*R*p1,i+T-p2,i||^2isminimized."""#Solvetheprocrustesproblembbysubtractingcentroids,scalingbythe#standarddeviation,andthenusingtheSVDtocalculatetherotation.See#thefollowingformoredetails:#https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problempoints1=points1.astype(numpy.float64)points2=points2.astype(numpy.c1float64).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)#TheRweseekisinfactthetransposeoftheonegivenbyU*Vt.This#isbecausetheaboveformulationassumesthematrixgoesontheright#(withrowvectors)whereasoursolutionrequiresthematrixtobeonthe#left(withcolumnvectors).R=(U*Vt).Treturnnumpy.vstack([.hstack(((s2/s1)*R,c2.T-(s2/s1)*R*c1.T)),numpy.matrix([0.,0.,1.])])defread_im_and_landmarks(fname):im=cv2.imread(fname,cv2.IMREAD_COLOR)im=cv2.resize(im,(im.shape[1]*SCALE_FACTOR,im.shape[0]*SCALE_FACTOR))s=get_landmarks(im)returnim,sdefwarp_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_imdefcorrect_colours(im1,im2,landmarks1):模糊量=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))im1,landmarks1=read_im_and_landmarks(sys.argv[1])im2,landmarks2=read_im_and_landmarks(sys.argv[2])M=transformation_from_points(landmarks1[ALIGN_POINTS],landmarks2[ALIGN_POINTS])mask=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)warped_im2=warp_im(im2,M,im1.shape)warped_corrected_im2=correct_colours(im1,warped_im2,landmarks1)output_im=im1*(1.0-combined_mask)+warped_corrected_im2*combined_maskcv2.imwrite('output.jpg',output_im)
