前言
最近读到几则关于换脸的新闻,一则说一个英国小伙进入大英博物馆和雕塑合影后换脸爆笑,二则说reddit网站封禁用明星视频给动作电影换脸的社区,正好近期正在研究人脸识别技术的应用,忽然受到启发,大假闲暇,莫名就来了兴致,忍不住就要尝试一番。
本博文仅就静态图片的处理展开讨论,其实静态图片的处理是基础,一通百通,微积聚散,动态视频以及人脸高低俯仰,喜怒哀乐都可以非常容易的实现,如果可以用到人工智能的深度学习,当然更加事半功倍,这些更深的技术我们在后续博文中再行研究。
技术无错,有错的是掌握技术的人,把人工智能技术应用于岛国动作片,确实是走入了歧途。哪一个男人心中没有仗剑天涯的英雄情,哪一个少女心中没有白马彩虹的公主梦,如果可以将那些英雄童话视频里的主人公换成自己,岂不也是美事一桩。
特别声明
所有图片源自公开网络,如觉不适,通知后立即删除。
本文是原创技术讨论帖,非经允许,切勿转载。
新闻图片
我们先来看一下那二则新闻的图片和效果:
对大英博物馆的换脸效果,出于搞笑还是极具创意的,但换脸的效果太差,不能忍。动态换脸的技术我们后面再来讨论。
环境
win10 32位笔记本
VistualStudio2017
python3.6
opencv3.4
dlib19.9
换脸技术
我们在百度上搜索“python换脸”会出现一大堆博文,但天下文章一大抄,真正原创的可能就那么几篇,其实现的技术途径大同小异,感兴趣的同学可以自行探究。但原有的文章大都基于旧有的软件版本,一些变换和融合算法还需自己设定,另外采用类似“T”型的蒙版处理显然差强人意,在我的研究中,全部采用最新的软件,算法都是已经封装好的,蒙版也作了一些变化,效果应该比以前的要好。
在“200行python代码实现换脸”中,给出的效果图如下:采用的蒙版
我给出的换脸技术步骤:
- 选取图片,进行预处理;
- 识别人脸,获得人脸的精准特征点坐标;
- 绘制人脸蒙版,确定仿射变换或者透视变换的对应点(自适应人脸大小和角度);
- 进行人脸变换;
人脸融合
代码
# -*- coding: utf-8 -*-
#VS2017+python3.6+opencv3.4+Dlib19.9
#2018.02.04
#作者:艾克思
import sys
import cv2
import dlib
import numpy as np
import math
detector = dlib.get_frontal_face_detector() #构建探测器分类器
landmark_predictor = dlib.shape_predictor('../shape_predictor_68_face_landmarks.dat') #构建特征点探测器
file1='../s1.jpg'
file2='../menglu.jpg'
#提取68个特征点的函数,输入图片,返回68个点坐标,用landmarks[0-67]表示
def marks68(img):
faces = detector(img,1)
if (len(faces) > 0):
for i,rect in enumerate(faces):
shape=landmark_predictor(img,rect)
#特征点全部保存在了shape里面,rect是dlib.rectangle(),人脸检测矩形的左上和右下坐标,shape.part(i)是第i个特征点
landmarks=[(0,0)]*68
for i in range(68):
landmarks[i]=(shape.part(i).x,shape.part(i).y)
#cv2.putText(img, '%s'%i,(shape.part(i).x,shape.part(i).y-5),cv2.FONT_HERSHEY_SIMPLEX, 0.2, (0, 0, 255), 1,cv2.LINE_8, 0)
#draw_face(img,landmarks)
#draw_rec(img,rect)
return landmarks
#画出68个点和轮廓的函数
def draw_face(img,points):
for i in range(16):cv2.line(img,points[i],points[i+1],(255,0,0),1) #脸型
for i in range(17,21): #左眉
cv2.line(img,points[i],points[i+1],(255,0,0),1)
cv2.line(img,points[17],points[21],(255,0,0),1)
for i in range(22,26): #右眉
cv2.line(img,points[i],points[i+1],(255,0,0),1)
cv2.line(img,points[22],points[26],(255,0,0),1)
for i in range(27,35): #鼻梁
cv2.line(img,points[i],points[i+1],(255,0,0),1)
cv2.line(img,points[30],points[35],(255,0,0),1)
for i in range(36,41):
cv2.line(img,points[i],points[i+1],(255,0,0),1) #左眼
cv2.line(img,points[36],points[41],(255,0,0),1)
for i in range(42,47): #右眼
cv2.line(img,points[i],points[i+1],(255,0,0),1)
cv2.line(img,points[42],points[47],(255,0,0),1)
for i in range(48,54):cv2.line(img,points[i],points[i+1],(255,0,0),1) #上唇上
for i in range(60,64): #上唇下
cv2.line(img,points[i],points[i+1],(255,0,0),1)
cv2.line(img,points[48],points[60],(255,0,0),1)
cv2.line(img,points[54],points[64],(255,0,0),1)
for i in range(64,67):cv2.line(img,points[i],points[i+1],(255,0,0),1) #下唇上
for i in range(54,59): #下唇下
cv2.line(img,points[i],points[i+1],(255,0,0),1)
cv2.line(img,points[48],points[67],(255,0,0),1)
cv2.line(img,points[48],points[59],(255,0,0),1)
cv2.line(img,points[54],points[64],(255,0,0),1)
def draw_rec(img,d):
x1=d.left()
y1=d.top()
x2=d.right()
y2=d.bottom()
delt=(x2-x1)//6
cv2.rectangle(img,(x1+delt//3,y1+delt//3),(x2-delt//3,y2-delt//3),(255,255,255),1)
cv2.line(img,(x1,y1),(x1+delt,y1),(255,255,255),2)
cv2.line(img,(x1,y1),(x1,y1+delt),(255,255,255),2)
cv2.line(img,(x2,y1),(x2-delt,y1),(255,255,255),2)
cv2.line(img,(x2,y1),(x2,y1+delt),(255,255,255),2)
cv2.line(img,(x1,y2),(x1+delt,y2),(255,255,255),2)
cv2.line(img,(x1,y2),(x1,y2-delt),(255,255,255),2)
cv2.line(img,(x2,y2),(x2-delt,y2),(255,255,255),2)
cv2.line(img,(x2,y2),(x2,y2-delt),(255,255,255),2)
def main():
img1=cv2.imread(file1)
img2=cv2.imread(file2)
h1,w1=img1.shape[:2]
h2,w2=img2.shape[:2]
landmarks1=marks68(img1)
landmarks2=marks68(img2)
face1,pts1,mask1=mask(img1,landmarks1)
face2,pts2,mask2=mask(img2,landmarks2)
#cv2.imshow('face1',face1)
#cv2.imshow('face2',face2)
#cv2.imshow('mask1',mask1)
#cv2.imshow('mask2',mask2)
#M = cv2.getAffineTransform(pnts1,pnts2)
#dst1 = cv2.warpAffine(face1,M,(w0,h0))
#mask0=cv2.warpAffine(mask1,M,(w0,h0))
M1 = cv2.getPerspectiveTransform(pts1,pts2)
dst11 = cv2.warpPerspective(face1,M1,(w2,h2))
mask1_2=cv2.warpPerspective(mask1,M1,(w2,h2))
cnts1, hierarchy1, rr1 = cv2.findContours(mask1_2.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for c1 in hierarchy1:
x11,y11,w11,h11=cv2.boundingRect(c1)
#cv2.rectangle(mask1_2,(x11,y11),(x11+w11,y11+h11),(255,255,255),2)
#cv2.line(mask1_2,(x11,y11),(x11+w11,y11+h11),(255,255,255),1)
#cv2.line(mask1_2,(x11+w11,y11),(x11,y11+h11),(255,255,255),1)
point1=(x11+w11//2,y11+h11//2)
#cv2.circle(mask1_2,point1,3,(0,0,255),-1)
#cv2.imshow('mask11',mask1_2)
#cv2.imshow('mask2',dst11)
mask_inv1_2 = cv2.bitwise_not(mask1_2)
dst12=cv2.bitwise_and(img2,img2,mask=mask_inv1_2)
dst1_2=cv2.add(dst11,dst12)
img1_2=cv2.seamlessClone(dst1_2,img2,mask1_2,point1,cv2.NORMAL_CLONE)
cv2.imshow('dst11',img1)
cv2.imshow('dst1',img1_2)
cv2.imshow('dst12',img2)
M2 = cv2.getPerspectiveTransform(pts2,pts1)
dst21 = cv2.warpPerspective(face2,M2,(w1,h1))
mask2_1=cv2.warpPerspective(mask2,M2,(w1,h1))
cnts2, hierarchy2, rr2 = cv2.findContours(mask2_1.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for c2 in hierarchy2:
x21,y21,w21,h21=cv2.boundingRect(c2)
cv2.rectangle(mask2_1,(x21,y21),(x21+w21,y21+h21),(255,255,255),2)
cv2.line(mask1_2,(x21,y21),(x21+w21,y21+h21),(255,255,255),1)
cv2.line(mask1_2,(x21+w21,y21),(x21,y21+h21),(255,255,255),1)
point2=(x21+w21//2,y21+h21//2)
cv2.circle(mask2_1,point2,3,(0,0,255),-1)
mask_inv2_1 = cv2.bitwise_not(mask2_1)
dst22=cv2.bitwise_and(img1,img1,mask=mask_inv2_1)
dst2_1=cv2.add(dst21,dst22)
img2_1=cv2.seamlessClone(dst2_1,img1,mask2_1,point2,cv2.NORMAL_CLONE)
#cv2.imshow('dst1',mask1_2)
#cv2.imshow('dst2',dst2)
#cv2.imshow('dst2',img2_1)
if cv2.waitKey(0)==ord('q'):cv2.destroyAllWindows()
def mask(img,pnt):
mask0=np.zeros(img.shape[:2],np.uint8)
pnts =np.float32([pnt[17],pnt[6],pnt[26],pnt[10]]) #最好是外边界,可以保证脸型一致
#cv2.circle(img,pnt[17],3,(0,0,255),-1)
#cv2.circle(img,pnt[6],3,(0,0,255),-1)
#cv2.circle(img,pnt[26],3,(0,0,255),-1)
#cv2.circle(img,pnt[10],3,(0,0,255),-1)
#cv2.line(img,pnt[6],pnt[10],(255,255,255),1)
#cv2.line(img,pnt[10],pnt[26],(255,255,255),1)
#cv2.line(img,pnt[26],pnt[17],(255,255,255),1)
#cv2.line(img,pnt[17],pnt[6],(255,255,255),1)
chain = np.array( pnt[0:17]+pnt[24:27][::-1]+pnt[17:20][::-1])
cv2.fillConvexPoly(mask0,chain,(255,255,255))
face0 = cv2.bitwise_and(img,img,mask = mask0)
#delt=10
#face=face0[(min(pnt[20][1],pnt[25][1])-delt): (pnt[9][1]+delt),(pnt[0][0]-delt):(pnt[16][0]+delt) ]
#cv2.imshow('mask',mask0)
return face0,pnts,mask0
if __name__=='__main__':
main()
代码基本上是一种调试状态,变量名和参数都还没有归拢,理解就好。
完整的处理过程
选取图片
本文名为“和历史有个约会”,受到大英博物馆恶搞小伙的启发,我选取我们的国家宝藏-兵马俑来作为源图片。每次看到兵马俑,我都恍惚觉得这样的作品实在是太过写实和逼真,他们就是一个个活生生的人,只不过正在沉沉睡去,也许忽然就会睁开眼睛,猛然醒来,正如时下的伟大中国。
对于目标图片,我选择一位外国的西服英俊男,和兵马俑相对,古今中外这四个字就齐了,当然这也增加了程式的难度。
对于图片的预处理是非常讲究的,本文在本质上是脸型的变换,还没有涉及到五官的变化,因此,在本例程中,应尽量选取正面或者角度一致的图片,这里,我只是把图片大小变为一致,便于行文。
人脸识别提取特征点
dlib库提取68个特征点,百度大脑提取72个特征点,各家的开源库功能也各不相同,对本文而言,dlib可以提取到基本完整的脸型坐标,已经够用了,后续如果要变换五官,dlib就会显得力不从心。本例程中采用dlib来实现特征点的提取,这方面的文章和例子非常多,这里就不再赘言。效果如下:
人脸蒙版
我们需要把人脸抠出来,利用上面得到的人脸特征点建立轮廓,构建起抠图蒙版,效果如下:
透视变换的特征点确定
以前的文章大多选择三点定位的仿射变换,其实质是平移、旋转和缩放,这会带来脸型匹配的问题。本例程中采用四点定位的透视变换,可以将源脸按照目标脸的形状进行变换,两者的效果是完全不同的,从我的实践看,后者的融合效果更好。我们选取左右眉尾和左右下巴的四个点作为两张脸型透视变换的基准点,这样可以兼顾上下左右,不至于出现剧烈变形的情况。
变换效果
我们需要对源脸和蒙版分别进行变换,在变换时,需要对蒙版中的色块进行分析,自动判断中心位置点。
源脸变换效果
蒙版变换效果
对目标图片抠图
用变换过的蒙版对目标图片进行抠图,去处目标图片中的人脸。
融合
将变换后的图片经进行融合:
平滑处理
比对效果
是不是比大英博物馆的恶搞图片来的更加精准更加自然,我们的兵马俑小伙穿越到现代,穿上西服打上领带还是非常帅气的,有一种难以名状的美。
生产作品
兵马俑附体吴京和黄晓明,帅小伙一枚。
我们的兵马俑替换自由像,更有古朴的感觉。
穿越到古罗马的雕塑身上,别有一番神情。
相生相杀,恩怨情仇,为了世界和平,我们合体好了。
不是谁都可以担的起这样的发型。
非典型性网红和梦露的合体。
后记
本历程中给出的换脸实质上是脸型的变换,两者当然具有很好的融合度。
既然可以实现脸型的变换,我们又可以拿到每一张脸的精准特征点坐标,将人脸细分为有限个局部区域,将其中的五官进行变换,构建喜怒哀乐的表情,把我们自己不满意的五官更换为理想中的明星的样子,是不是一件非常有趣的事,把精武剧中的陈真换成自己的模样,酣畅淋漓地横扫虹口道场,让倭贼吃纸的样子会不会太酷!
期待我的后续研究吧。