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.coordinatescv2.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, 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) # 人脸网格对应的顶点索引# 人脸顶点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中获取,同时本博文也同步更新到微信公众号中:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/246574.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3D姿态估计——ThreeDPose项目简单易用的模型解析

前言 之前写过tensorflow官方的posenet模型解析,用起来比较简单,但是缺点是只有2D关键点,本着易用性的原则,当然要再来个简单易用的3D姿态估计。偶然看见了ThreeDPose的项目,感觉很强大的,所以把模型扒下来…

简易的素描图片转换流程与实现

前言 之前经常在网上看到用PS实现真实图片到素描图片的转换,但是流程都大同小异,身为一只程序猿,必须来个一键转化额。 国际惯例,参考博客: Photoshop基础教程:混合模式原理篇 颜色减淡的原理讲解以及应…

一个简单好用的磨皮祛斑算法理论和python实现

前言 最近看了一个磨皮算法祛斑感觉效果不错,效果图看文末就行,个人觉得效果非常不错滴。 国际惯例,参考博客: 磨皮算法的源码:YUCIHighPassSkinSmoothing How To Smooth And Soften Skin With Photoshop 图像算法…

OpenVINO——配置与道路分割案例

前言 最近看到了一个深度学习库OpenVINO,专门用于Intel硬件上部署深度学习模型,其内置了非常非常多使用的预训练模型,比如道路分割、人脸提取、3D姿态估计等等。但是配置和调用有点小恶心,这里以道路分割为例,展示如何…

图像颜色迁移《color transfer between images》

前言 前段时间,在深度学习领域不是有个比较火的方向叫风格迁移的嘛,对于我这种不喜欢深度学习那种不稳定结果的人来说,还是想看看传统图像处理领域有什么类似的技术,发现了一个颜色迁移的算法,很久前的论文了。 国际…

ColorSpace颜色空间简介

前言 如果看过之前的介绍的图像颜色迁移《color transfer between images》和颜色协调模型Color Harmoniztion就会发现,大部分图像处理算法虽然输入输出是RGB像素值,但是中间进行算法处理时很少直接更改RGB值,而是转换到其它空间&#xff0c…

Ogre共享骨骼与两种骨骼驱动方法

前言 最近业务中用到Ogre做基于3D关键点虚拟角色骨骼驱动,但是遇到两个问题: 身体、头、眼睛、衣服等mesh的骨骼是分开的,但是骨骼结构都是一样的,需要设置共享骨骼驱动的时候可以直接修改骨骼旋转量,或者将旋转量存…

仿射变换和透视变换

前言 在前面做换脸的博客中提到了使用仿射变换和透视变换将两张不同的人脸基于关键点进行对齐,保证一张人脸贴到另一张人脸时,大小完全一致;所以有必要理解一下这两个概念的区别,由于以实用性为目的,所以所有的图像算…

obj格式解析

前言 最近处理一些网格渲染的时候,需要解析Obj文件,从Free3D上随便找了个免费的人体obj模型解析测试一波 国际惯例,参考博客: 本文所使用的从Free3D下载的模型 .obj文件格式与.mtl文件格式 详解3D中的obj文件格式 3D中OBJ文…

Flask服务部署与简单内网穿透

前言 最近学习部署的时候,想到深度学习里面通常用的部署方法是flask做服务端,然后使用nginx做负载均衡,貌似也能做内网穿透。不过我不太懂负载均衡,只想利用本地电脑搭建一个简单的服务器,实现外部调用API服务的功能。…

OpenCV学习——轮廓检测

前言 轮廓检测是传统视觉中非常常用的功能,这里简单记录一下opencv中的轮廓检测算法使用方法,至于理论,后续有机会再去细品。 国际惯例: OpenCV官方的轮廓检测教程python版 OpenCV中的二值化方法教程 OpenCV轮廓层级官方文档…

RBF神经网络理论与实现

前言 最近发现有挺多人喜欢径向基函数(Radial Basis Function,RBF)神经网络,其实它就是将RBF作为神经网络层间的一种连接方式而已。这里做一个简单的描述和找了个代码解读。 之前也写过一篇,不过排版不好看,可以戳这里跳转 国际惯例&#x…

基于python和unity交互的卡通角色肢体和表情驱动(深度学习)

前言 最近看到了好多卡通角色的肢体驱动的东东,感觉是时候发挥一下读研时候学的东西了,而且虽然现在不炼丹了,但是还是得保持吃丹的技能。这个项目找了很多很多代码进行测试,最终集成了一个3D姿态估计和人脸关键点提取的代码。 …

OpenCV学习——形态学

前言 继续学习图像里面的形态学知识——结构元、腐蚀、膨胀、开运算、闭运算、击中/不击中变换。以及部分基本形态学算法,包括边界提取、空洞填充、连通分量的提取、凸壳、细化、粗化、骨架、裁剪、形态学重建。 其实就是对冈萨雷斯的《数字图像处理》中第9章节《…

径向基函数RBF三维网格变形

前言 之前写过径向基函数(RBF)神经网络做分类或者拟合。然后挖了个坑说在《Phase-Functioned Neural Networks for Character Control》里面提到了用于做地形编辑,所以这篇博客就是解析一下如何用RBF做网格编辑系统。 参考博客: Noe’s tutorial on d…

OBJ可视化——UV还原(修正)

前言 前面写过一篇obj格式解析的博客,但是这篇文章中可视化的工作是参考PRNet的源码进行的,后来细细思考了一下,有点问题,具体看下面。 问题来源 在PRNet源码的render.py中有个函数render_texture,是作者用于将uv展…

Unity中BVH骨骼动画驱动的可视化理论与实现

前言 找了很久使用BVH到unity中驱动骨骼动画的代码,但是都不是特别好用,自己以前写过,原理很简单,这里记录一下。 理论 初始姿态 在BVH或者其它骨骼动画中,一般涉及到三种姿势:A-pose,T-pos…

卡通驱动项目ThreeDPoseTracker——模型驱动解析

前言 之前解析过ThreeDPoseTracker这个项目中的深度学习模型,公众号有兄弟私信一些问题,我刚好对这个项目实现有兴趣,就分析一波源码,顺便把问题解答一下。 这个源码其实包括很多内容:3D姿态估计,坐标平滑…

卡通驱动项目ThreeDPoseTracker——关键点平滑方案解析

前言 之前对ThreeDPoseTracker的深度学习模型和unity中的驱动方法进行过解析,还有一个比较重要的就是从深度学习模型出来的3D关键点数据会有抖动,在ThreeDPoseTracker源码中有做两次平滑,一部分是卡尔曼滤波,还有一部分是低通滤波…

卡通角色表情驱动系列一

前言 分析完ThreeDPoseTracker来做卡通角色的身体驱动,接下来在卡通驱动领域还有一个是表情驱动。对这个真的是一窍不通啊,只能慢慢看论文了。 国际惯例,参考博客/论文: 《Landmark-guided deformation transfer of template f…