3D人脸表情驱动——基于eos库
前言
之前出過三篇換臉的博文,遇到一個(gè)問題是表情那一塊不好處理,可行方法是直接基于2D人臉關(guān)鍵點(diǎn)做網(wǎng)格變形,強(qiáng)行將表情矯正到目標(biāo)人臉,還有就是使用PRNet的思想,使用目標(biāo)人臉的頂點(diǎn)模型配合源人臉的紋理,可以讓表情遷移過來,但是這個(gè)表情是很僵硬的。比如笑臉的3D頂點(diǎn)模型,結(jié)合不笑人臉的紋理圖,生成的笑臉是非常奇怪的。有興趣可以翻csdn前面的文章,或者關(guān)注公眾號(hào)檢索人臉相關(guān)文章。
這里針對(duì)表情,采用另一種方案——blendshape。這個(gè)理論在表情動(dòng)畫中經(jīng)常使用到,目的就是驅(qū)動(dòng)人臉表情,無論是動(dòng)畫人臉還是真人的人臉,只要你這個(gè)人臉具有對(duì)應(yīng)的頂點(diǎn)模型、紋理,還有很多標(biāo)準(zhǔn)的blendshape模型,分別對(duì)應(yīng)不同的表情。
我們這里采用eos庫實(shí)現(xiàn)表情變換,一來是很多blendshape數(shù)據(jù)集獲取難度比較大,二來是這個(gè)庫還是蠻好用的,有C++/python/matlab的接口,而且與之前研究的PRNet有很多相似的的。
國際慣例,參考博客:
基于PRNet的3D人臉重建與替換
eos官方文檔
eos源碼
eos作者提供的model的可視化工具,包括blendshape控制
算法流程
分為四步:
- 人臉關(guān)鍵點(diǎn)提取
- 3D人臉擬合
- 表情驅(qū)動(dòng)
- 渲染
注意,一般來說,人臉重建是基于人臉關(guān)鍵點(diǎn),不斷去調(diào)整3D標(biāo)準(zhǔn)人臉的,使其變換到目標(biāo)人臉的關(guān)鍵點(diǎn)形狀,這一點(diǎn)可以去知乎上看看3DMM人臉重建相關(guān)文章,擬合過程一般涉及到兩類參數(shù):形狀、表情
預(yù)備
先安裝一些必備的環(huán)境,直接用pip安裝eos-py、opencv-python、opencv-contrib-python
導(dǎo)入必要的庫:
import eos import numpy as np import cv2 from matplotlib import pyplot as plt然后把eos的源碼下載保存在一個(gè)文件夾中,我們寫的代碼都在eos源碼文件夾并列的代碼中寫,不要進(jìn)到eos文件夾里面寫代碼,面得污染了環(huán)境。
人臉關(guān)鍵點(diǎn)提取
之前人臉替換系列的博客都用的opencv人臉關(guān)鍵點(diǎn)檢測方法,這里也就不再說了,直接貼代碼:
#初始化檢測器 cas = cv2.CascadeClassifier('./facemodel/haarcascade_frontalface_alt2.xml') obj = cv2.face.createFacemarkLBF() obj.loadModel('./facemodel/lbfmodel.yaml')# 檢測人臉關(guān)鍵點(diǎn) 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 vis_img(img):plt.imshow(cv2.cvtColor(img.copy(),cv2.COLOR_BGR2RGB))測試看看
img_file = "./images/zly.jpg" img = cv2.imread(img_file) face_box,coords = detect_facepoint(img)# 轉(zhuǎn)換為eos庫所需要的關(guān)鍵點(diǎn)輸入格式 landmarks = [] ibug_index = 1 # count from 1 to 68 for all ibug landmarks for l in range(coords.shape[0]):landmarks.append(eos.core.Landmark(str(ibug_index), [float(coords[ibug_index-1][0]), float(coords[ibug_index-1][1])]))ibug_index = ibug_index + 1#可視化關(guān)鍵點(diǎn) img_show = img.copy() for kps in landmarks:face_kps = kps.coordinatescv2.circle(img_show,(face_kps[0],face_kps[1]),5,(0,255,0),-1) vis_img(img_show) plt.axis('off')使用eos 重建人臉
因?yàn)槭且脴?biāo)準(zhǔn)人臉和blendshape去擬合圖片人臉關(guān)鍵點(diǎn),所以需要先初始化一堆內(nèi)容,這個(gè)是固定套路:
# 初始化eos model = eos.morphablemodel.load_model("./eos/share/sfm_shape_3448.bin") blendshapes = eos.morphablemodel.load_blendshapes("./eos/share/expression_blendshapes_3448.bin") # Create a MorphableModel with expressions from the loaded neutral model and blendshapes: morphablemodel_with_expressions = eos.morphablemodel.MorphableModel(model.get_shape_model(), blendshapes,color_model=eos.morphablemodel.PcaModel(), vertex_definitions=None, texture_coordinates=model.get_texture_coordinates()) landmark_mapper = eos.core.LandmarkMapper('./eos/share/ibug_to_sfm.txt') edge_topology = eos.morphablemodel.load_edge_topology('./eos/share/sfm_3448_edge_topology.json') contour_landmarks = eos.fitting.ContourLandmarks.load('./eos/share/ibug_to_sfm.txt') model_contour = eos.fitting.ModelContour.load('./eos/share/sfm_model_contours.json')這個(gè)操作不用管,只要使用這個(gè)eos庫重建人臉,只需把這一串代碼復(fù)制下來用就行了,把模型路徑改改就行;這些路徑對(duì)應(yīng)文件都在官方源碼上有。
稍微解釋一下作者為什么不把這一串操作封到一個(gè)對(duì)象里面,我們使用的時(shí)候直接一句話初始一個(gè)對(duì)象就行了?原因在于這個(gè)庫是可以支持三種人臉模型(Surrey Face Model (SFM), 4D Face Model (4DFM), Basel Face Model (BFM)),而且對(duì)應(yīng)的blendshape表情數(shù)也可以增加,還有很多其他的映射關(guān)系表也根據(jù)不同的模型而變化,所以還不如全部暴露出來,用戶自行設(shè)置修改。
【注】上述初始化使用的人臉模型是SFM,作者提供的這個(gè)模型對(duì)應(yīng)的blendshapes只有六種表情anger, disgust, fear, happiness, sadness, surprise,所以重建或者驅(qū)動(dòng)效果其實(shí)不是特別理想,但是能看出來有驅(qū)動(dòng)。如果讀者其它兩種模型,比如4DFM的人臉模型精細(xì)度(網(wǎng)格數(shù)目)就比較高,而且多達(dá)36種表情,就建議使用高精度模型嘗試一波
初始化完畢,就可以針對(duì)關(guān)鍵點(diǎn)進(jìn)行擬合:
# 重建人臉 (mesh, pose, shape_coeffs, blendshape_coeffs) = eos.fitting.fit_shape_and_pose(morphablemodel_with_expressions,landmarks, landmark_mapper, image_width, image_height, edge_topology, contour_landmarks, model_contour)注意上面的返回值,shape_coeffs和blenshape_coeffs就是3DMM人臉重建中經(jīng)常說的形狀系數(shù)和表情系數(shù)了,前者擬合臉型,后者擬合表情。待會(huì)表情驅(qū)動(dòng)就是利用表情系數(shù)來做的。
如果還記得之前寫的PRNet人臉重建文章,里面有幾個(gè)信息比較重要:3D頂點(diǎn)、人臉紋理圖、網(wǎng)格頂點(diǎn)索引,在eos庫中,可以直接通過下面這句話獲取紋理信息
# 提取紋理 isomap = eos.render.extract_texture(mesh,pose,img).swapaxes(0,1)因?yàn)楹竺媸褂胢eshlab打開重建的人臉需要這個(gè)紋理文件,所以提前保存一下,順便可視化一波:
cv2.imwrite("result.isomap.png",isomap) vis_img(isomap)接下來就是需要根據(jù)得到的形狀參數(shù)和表情系數(shù),將標(biāo)準(zhǔn)人臉變換成咱趙麗穎的人臉,這里需要注意在issuee 35有人提到過C++中使用這句話
auto merged_shape = morphable_model.get_shape_model().draw_sample(fitted_coeffs) + to_matrix(blendshapes) * Mat(blendshape_coefficients);但是在python中并未提供表情系數(shù)乘法對(duì)應(yīng)的函數(shù),那么直接寫一個(gè)
def blendshape_add(bss,bc):bs_array = []for bs in bss:bs_array.append(bs.deformation)bs_array = np.array(bs_array).transpose()bc = np.array(bc)return np.dot(bs_array,bc)再仿照C++代碼寫重建方法:
merge_shape = morphablemodel_with_expressions.get_shape_model().draw_sample(shape_coeffs) + blendshape_add(blendshapes,blendshape_coeffs);表情驅(qū)動(dòng)
如果要驅(qū)動(dòng)表情,那么僅僅改改表情系數(shù)就可以了,比如
# 改變表情 anger, disgust, fear, happiness, sadness, surprise blendshape_coeffs = [0,1,0,0,0,0]這只是獲取了形狀,我們最終需要渲染的是mesh,所以還需要做一個(gè)轉(zhuǎn)換,記錄一下顏色信息,頂點(diǎn)信息什么的
merged_mesh = eos.morphablemodel.sample_to_mesh(merge_shape,morphablemodel_with_expressions.get_color_model().get_mean(),morphablemodel_with_expressions.get_shape_model().get_triangle_list(),morphablemodel_with_expressions.get_color_model().get_triangle_list(),morphablemodel_with_expressions.get_texture_coordinates());渲染
接下來介紹兩種可視化方法
-
使用meshlab可視化,因?yàn)樯厦嫖覀儽4孢^紋理文件,所以這里只需要把mesh保存一下
outputfile = "result.obj" eos.core.write_textured_obj(merged_mesh,outputfile);這時(shí)候我們就有了result.obj、result.mtl、result.isomap.png三個(gè)文件,直接雙擊obj用meshlab打開
-
使用代碼可視化,按照之前學(xué)習(xí)PRNet中得到的知識(shí),需要分別獲取到人臉模型的3D頂點(diǎn)坐標(biāo),每個(gè)人臉網(wǎng)格頂點(diǎn)索引,每個(gè)頂點(diǎn)的顏色信息,這些在eos求取的mesh中都有,分別取出來
triangles = np.array(merged_mesh.tvi) # 人臉網(wǎng)格對(duì)應(yīng)的頂點(diǎn)索引 # 人臉頂點(diǎn) vertices = [] for v in merged_mesh.vertices:vertices.append(np.array([v[0],-v[1],v[2]])) vertices = np.array(vertices) vertices = vertices-np.min(vertices) # 紋理坐標(biāo) texcoords = [] for tc in merged_mesh.texcoords:texcoords.append(tc) texcoords = np.array(texcoords) # 根據(jù)紋理坐標(biāo)獲取每個(gè)頂點(diǎn)的顏色 colors = [] for i in range(texcoords.shape[0]):colors.append(isomap[int(texcoords[i][1]*(isomap.shape[0]-1)),int(texcoords[i][0]*(isomap.shape[1]-1)),0:3]) colors = np.array(colors,np.float32)然后利用頂點(diǎn)的顏色,求平均得到網(wǎng)格的顏色
#獲取三角形每個(gè)頂點(diǎn)的color,平均值作為三角形顏色 tri_tex = (colors[triangles[:,0] ,:] + colors[triangles[:,1],:] + colors[triangles[:,2],:])/3.對(duì)每個(gè)網(wǎng)格上色
img_3D = np.zeros_like(img,dtype=np.uint8) for i in range(triangles.shape[0]):cnt = np.array([(vertices[triangles[i,0],0],vertices[triangles[i,0],1]),(vertices[triangles[i,1],0],vertices[triangles[i,1],1]),(vertices[triangles[i,2],0],vertices[triangles[i,2],1])],dtype=np.int32)img_3D = cv2.drawContours(img_3D,[cnt],0,(int(tri_tex[i][0]), int(tri_tex[i][1]), int(tri_tex[i][2])),-1)可視化
plt.figure(figsize=(8,8)) vis_img(img_3D)有人奇怪,我上傳的圖片,講道理沒張嘴啊,為什么張嘴了,因?yàn)槲覀凃?qū)動(dòng)了表情
# 改變表情 anger, disgust, fear, happiness, sadness, surprise blendshape_coeffs = [0,1,0,0,0,0]所以現(xiàn)在看起來是disgust這個(gè)表情,為啥看起來不自然,當(dāng)然是因?yàn)辂惙f的照片本來就在笑,導(dǎo)致默認(rèn)紋理在笑,然后再加上blendshape比較粗糙,看起來就有點(diǎn)怪怪的。
生成表情驅(qū)動(dòng)的gif
通過上面的一系列操作,我們可以基于eos自帶的6種表情blendshape改變大穎妹子的面部表情,那么來搞個(gè)gif玩玩,思路就是對(duì)blendshape_coeffs做一個(gè)線性過渡即可
buff = [] frame_num = 20 for i in range(frame_num): #一個(gè)gif 10幀# 改變表情 anger, disgust, fear, happiness, sadness, surpriseblendshape_coeffs = [1.0 - i/frame_num,0,0,0,0,i/frame_num]merge_shape = morphablemodel_with_expressions.get_shape_model().draw_sample(shape_coeffs) + blendshape_add(blendshapes,blendshape_coeffs);merged_mesh = eos.morphablemodel.sample_to_mesh(merge_shape,morphablemodel_with_expressions.get_color_model().get_mean(),morphablemodel_with_expressions.get_shape_model().get_triangle_list(),morphablemodel_with_expressions.get_color_model().get_triangle_list(),morphablemodel_with_expressions.get_texture_coordinates());triangles = np.array(merged_mesh.tvi) # 人臉網(wǎng)格對(duì)應(yīng)的頂點(diǎn)索引# 人臉頂點(diǎn)vertices = []for v in merged_mesh.vertices:vertices.append(np.array([v[0],-v[1],v[2]]))vertices = np.array(vertices)vertices = vertices-np.min(vertices)# 紋理坐標(biāo)texcoords = []for tc in merged_mesh.texcoords:texcoords.append(tc)texcoords = np.array(texcoords)# 根據(jù)紋理坐標(biāo)獲取每個(gè)頂點(diǎn)的顏色colors = []for i in range(texcoords.shape[0]):colors.append(isomap[int(texcoords[i][1]*(isomap.shape[0]-1)),int(texcoords[i][0]*(isomap.shape[1]-1)),0:3])colors = np.array(colors,np.float32)#獲取三角形每個(gè)頂點(diǎn)的color,平均值作為三角形顏色tri_tex = (colors[triangles[:,0] ,:] + colors[triangles[:,1],:] + colors[triangles[:,2],:])/3.img_3D = np.zeros_like(img,dtype=np.uint8)for i in range(triangles.shape[0]):cnt = np.array([(vertices[triangles[i,0],0],vertices[triangles[i,0],1]),(vertices[triangles[i,1],0],vertices[triangles[i,1],1]),(vertices[triangles[i,2],0],vertices[triangles[i,2],1])],dtype=np.int32)img_3D = cv2.drawContours(img_3D,[cnt],0,(int(tri_tex[i][0]), int(tri_tex[i][1]), int(tri_tex[i][2])),-1)buff.append(cv2.cvtColor(img_3D,cv2.COLOR_BGR2RGB)) gif=imageio.mimsave('expression.gif',buff,'GIF',duration=0.1)從憤怒到驚訝的效果圖
后記
當(dāng)前只是針對(duì)之前人臉替換的表情問題,按照blendshape驅(qū)動(dòng)的方法,做了一個(gè)實(shí)驗(yàn),至于還有其它問題,比如為啥這個(gè)人臉上有黑洞洞啊、怎么把人臉拼接到原圖上,這個(gè)后續(xù)有機(jī)會(huì)再去折騰了,這個(gè)eos庫的python接口文檔不是特別詳細(xì),而且沒C++那么完善。建議真有興趣的老鐵多看看issues,里面有很多有趣的問題。
代碼上面都放出來了,如果想要我實(shí)驗(yàn)的代碼,直接關(guān)注微信公眾號(hào),在公眾號(hào)簡介中的github中獲取,同時(shí)本博文也同步更新到微信公眾號(hào)中:
總結(jié)
以上是生活随笔為你收集整理的3D人脸表情驱动——基于eos库的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux卸载并更新显卡驱动
- 下一篇: 检测到目标服务器启用了trace方法_深