3D人臉表情驅動——基於eos庫

前言

之前出過三篇換臉的博文,遇到一個問題是表情那一塊不好處理,可行方法是直接基於2D人臉關鍵點做網格變形,強行將表情矯正到目標人臉,還有就是使用PRNet的思想,使用目標人臉的頂點模型配合源人臉的紋理,可以讓表情遷移過來,但是這個表情是很僵硬的。比如笑臉的3D頂點模型,結合不笑人臉的紋理圖,生成的笑臉是非常奇怪的。有興趣可以翻csdn前面的文章,或者關注公衆號檢索人臉相關文章。

這裏針對表情,採用另一種方案——blendshape。這個理論在表情動畫中經常使用到,目的就是驅動人臉表情,無論是動畫人臉還是真人的人臉,只要你這個人臉具有對應的頂點模型、紋理,還有很多標準的blendshape模型,分別對應不同的表情。

我們這裏採用eos庫實現表情變換,一來是很多blendshape數據集獲取難度比較大,二來是這個庫還是蠻好用的,有C++/python/matlab的接口,而且與之前研究的PRNet有很多相似的的。

國際慣例,參考博客:

基於PRNet的3D人臉重建與替換

eos官方文檔

eos源碼

eos作者提供的model的可視化工具,包括blendshape控制

算法流程

分爲四步:

  • 人臉關鍵點提取
  • 3D人臉擬合
  • 表情驅動
  • 渲染

注意,一般來說,人臉重建是基於人臉關鍵點,不斷去調整3D標準人臉的,使其變換到目標人臉的關鍵點形狀,這一點可以去知乎上看看3DMM人臉重建相關文章,擬合過程一般涉及到兩類參數:形狀、表情

預備

先安裝一些必備的環境,直接用pip安裝eos-py、opencv-python、opencv-contrib-python

導入必要的庫:

import eos
import numpy as np
import cv2
from matplotlib import pyplot as plt

然後把eos的源碼下載保存在一個文件夾中,我們寫的代碼都在eos源碼文件夾並列的代碼中寫,不要進到eos文件夾裏面寫代碼,面得污染了環境。

人臉關鍵點提取

之前人臉替換系列的博客都用的opencv人臉關鍵點檢測方法,這裏也就不再說了,直接貼代碼:

#初始化檢測器
cas = cv2.CascadeClassifier('./facemodel/haarcascade_frontalface_alt2.xml')
obj = cv2.face.createFacemarkLBF()
obj.loadModel('./facemodel/lbfmodel.yaml')

# 檢測人臉關鍵點
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)

# 轉換爲eos庫所需要的關鍵點輸入格式
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
    
#可視化關鍵點
img_show = img.copy()
for kps in landmarks:
    face_kps = kps.coordinates
    cv2.circle(img_show,(face_kps[0],face_kps[1]),5,(0,255,0),-1)
vis_img(img_show)
plt.axis('off')

在這裏插入圖片描述

使用eos 重建人臉

因爲是要用標準人臉和blendshape去擬合圖片人臉關鍵點,所以需要先初始化一堆內容,這個是固定套路:

# 初始化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')

這個操作不用管,只要使用這個eos庫重建人臉,只需把這一串代碼複製下來用就行了,把模型路徑改改就行;這些路徑對應文件都在官方源碼上有。

稍微解釋一下作者爲什麼不把這一串操作封到一個對象裏面,我們使用的時候直接一句話初始一個對象就行了?原因在於這個庫是可以支持三種人臉模型(Surrey Face Model (SFM), 4D Face Model (4DFM), Basel Face Model (BFM)),而且對應的blendshape表情數也可以增加,還有很多其他的映射關係表也根據不同的模型而變化,所以還不如全部暴露出來,用戶自行設置修改。

【注】上述初始化使用的人臉模型是SFM,作者提供的這個模型對應的blendshapes只有六種表情anger, disgust, fear, happiness, sadness, surprise,所以重建或者驅動效果其實不是特別理想,但是能看出來有驅動。如果讀者其它兩種模型,比如4DFM的人臉模型精細度(網格數目)就比較高,而且多達36種表情,就建議使用高精度模型嘗試一波

初始化完畢,就可以針對關鍵點進行擬合:

# 重建人臉
(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_coeffsblenshape_coeffs就是3DMM人臉重建中經常說的形狀係數和表情係數了,前者擬合臉型,後者擬合表情。待會表情驅動就是利用表情係數來做的。

如果還記得之前寫的PRNet人臉重建文章,裏面有幾個信息比較重要:3D頂點、人臉紋理圖、網格頂點索引,在eos庫中,可以直接通過下面這句話獲取紋理信息

# 提取紋理
isomap = eos.render.extract_texture(mesh,pose,img).swapaxes(0,1)

因爲後面使用meshlab打開重建的人臉需要這個紋理文件,所以提前保存一下,順便可視化一波:

cv2.imwrite("result.isomap.png",isomap)
vis_img(isomap)

在這裏插入圖片描述

接下來就是需要根據得到的形狀參數和表情係數,將標準人臉變換成咱趙麗穎的人臉,這裏需要注意在issuee 35有人提到過C++中使用這句話

auto merged_shape = morphable_model.get_shape_model().draw_sample(fitted_coeffs) + to_matrix(blendshapes) * Mat(blendshape_coefficients);

但是在python中並未提供表情係數乘法對應的函數,那麼直接寫一個

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);

表情驅動

如果要驅動表情,那麼僅僅改改表情係數就可以了,比如

# 改變表情 anger, disgust, fear, happiness, sadness, surprise
blendshape_coeffs = [0,1,0,0,0,0]

這只是獲取了形狀,我們最終需要渲染的是mesh,所以還需要做一個轉換,記錄一下顏色信息,頂點信息什麼的

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可視化,因爲上面我們保存過紋理文件,所以這裏只需要把mesh保存一下

    outputfile = "result.obj"
    eos.core.write_textured_obj(merged_mesh,outputfile);
    

    這時候我們就有了result.objresult.mtlresult.isomap.png三個文件,直接雙擊objmeshlab打開

在這裏插入圖片描述

  • 使用代碼可視化,按照之前學習PRNet中得到的知識,需要分別獲取到人臉模型的3D頂點座標,每個人臉網格頂點索引,每個頂點的顏色信息,這些在eos求取的mesh中都有,分別取出來

    triangles = np.array(merged_mesh.tvi) # 人臉網格對應的頂點索引
    # 人臉頂點
    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)
    # 紋理座標
    texcoords = []
    for tc in merged_mesh.texcoords:
        texcoords.append(tc)
    texcoords = np.array(texcoords)
    # 根據紋理座標獲取每個頂點的顏色
    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)
    

    然後利用頂點的顏色,求平均得到網格的顏色

    #獲取三角形每個頂點的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)
    

    可視化

    plt.figure(figsize=(8,8))
    vis_img(img_3D)
    

    在這裏插入圖片描述

    有人奇怪,我上傳的圖片,講道理沒張嘴啊,爲什麼張嘴了,因爲我們驅動了表情

    # 改變表情 anger, disgust, fear, happiness, sadness, surprise
    blendshape_coeffs = [0,1,0,0,0,0]
    

    所以現在看起來是disgust這個表情,爲啥看起來不自然,當然是因爲麗穎的照片本來就在笑,導致默認紋理在笑,然後再加上blendshape比較粗糙,看起來就有點怪怪的。

生成表情驅動的gif

通過上面的一系列操作,我們可以基於eos自帶的6種表情blendshape改變大穎妹子的面部表情,那麼來搞個gif玩玩,思路就是對blendshape_coeffs做一個線性過渡即可

buff = []
frame_num = 20
for i in range(frame_num): #一個gif 10幀
    # 改變表情 anger, disgust, fear, happiness, sadness, surprise
    blendshape_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) # 人臉網格對應的頂點索引
    # 人臉頂點
    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)
    # 紋理座標
    texcoords = []
    for tc in merged_mesh.texcoords:
        texcoords.append(tc)
    texcoords = np.array(texcoords)
    # 根據紋理座標獲取每個頂點的顏色
    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)
    #獲取三角形每個頂點的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)

從憤怒到驚訝的效果圖

在這裏插入圖片描述

後記

當前只是針對之前人臉替換的表情問題,按照blendshape驅動的方法,做了一個實驗,至於還有其它問題,比如爲啥這個人臉上有黑洞洞啊、怎麼把人臉拼接到原圖上,這個後續有機會再去折騰了,這個eos庫的python接口文檔不是特別詳細,而且沒C++那麼完善。建議真有興趣的老鐵多看看issues,裏面有很多有趣的問題。

代碼上面都放出來了,如果想要我實驗的代碼,直接關注微信公衆號,在公衆號簡介中的github中獲取,同時本博文也同步更新到微信公衆號中:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章