前言

分析完ThreeDPoseTracker来做卡通角色的身体驱动,接下来在卡通驱动领域还有一个是表情驱动。对这个真的是一窍不通啊,只能慢慢看论文了。

国际惯例,参考博客/论文:

  • 《Landmark-guided deformation transfer of template facial expressions for automatic generation of avatar blendshapes》
  • 《FACSvatar: An Open Source Modular Framework for Real-Time FACS based Facial Animation》
  • 《Real-time Facial Animation for Untrained Users》
  • 《Modeling Facial Expressions in 3D Avatars from 2D Images》
  • 《Practice and Theory of Blendshape Facial Models》
  • 《Real-Time Facial Motion Capture System》
  • 《Semantic 3D Motion Retargeting for Facial Animation》
  • 《Learning Controls for Blend Shape Based Realistic Facial Animation》
  • 《Performance Driven Facial Animation using Blendshape Interpolation》
  • 《Expression Cloning Jun-yong》
  • 苹果的ARKit blendShapes
  • 如何在Maya流程下创建一整套面部绑定

简述

此块内容受知识限制,描述内容有限或者可能有误,所以大家有何见解可在评论区或者微信公众号私信我讨论。

从参考文献来看,表情驱动大致分为三种:

  • 网格形变/编辑(mesh deform)

    • 直接基于面部顶点进行网格形变,比如我之前的博客径向基函数RBF三维网格变形就是其中一种变形方法,但是在表情驱动中,不同论文也会提出各种不同的变形方法,使变形后的人脸表情更加自然,其核心就是变形算法,例如《Landmark-guided deformation transfer of template facial expressions for automatic generation of avatar blendshapes》、《Deformation transfer for triangle meshes》,可以将捕捉到的人脸网格重定向到一个数字人面部,同步他俩的动作。
    • 比如Blender 2.8 Facial motion capture tutorial利用关键点和blender自带的形变功能,此博客中要求人脸3D模型和人脸关键点保持一致(作者进行了人工绑定),所以无法用到卡通角色上;不过如果自己能找到人脸和其它卡通角色的面部捕捉点对应关系,同时也能建立一个真人面部关键点动作到虚拟角色对应关键点运动的映射关系(因为卡通角色和人脸角色的网格差距可能会很大),也可以做卡通角色的驱动。
  • 骨骼驱动。在面部创建骨骼,利用软件对面部肌肉和骨骼进行权重绑定,然后调整骨骼的时候,面部肌肉会根据绑定值自动计算面部顶点信息,此时不需要网格形变算法,直接基于权重重新计算人脸蒙皮。这个过程很接近人体蒙皮算法,后续应该会开一个博客解析如何将皮肤与骨骼绑定。
    • 比如B站的这个教程使用maya建立面部骨骼,然后做表情绑定的教程。
  • 使用blendshape融合变形;在美术领域,这个方法就是表情控制器的制作基础,每个表情控制器对应一套BS。关于详细可看苹果的ARKit blendShapes列出的BS类型,针对每个表情预先做好对应的面部模型,从无表情到有表情用一个0-1的系数即可控制每种表情的程度;人脸重建的基本方法3DMM就是基于BS的,核心在于如何获取当前人脸对应的BS系数。通常有两种方法
    • 一种是用最小二乘法求解,使得所有表情BS组合起来的人脸的关键点更加接近真实人脸提取的关键点。比如Realtime Facial Animation for Untrained User,StrongTrack
    • 用几何的方法直接算,比如Blender & OpenCV - Windows Guide - Facial Motion Capture,预先在做好了卡通角色的面部骨骼动画控制器,然后根据人脸关键点计算出五官变化,从而控制虚拟角色的表情。
    • 另一种是直接用深度学习去获取BS系数,但是非常受限于数据集,比如openface,具体应用为FACSvatar

还有直接用图像算法驱动表情的我就不说了,比如最近比较火的“蚂蚁雅黑”表情驱动就是基于first-order-model的,不想把它归到3D驱动中。

【注】上述方法不要区分太开,因为本质都是网格形变,只不过骨骼驱动是通过控制面部骨骼,利用预定义的(骨骼对面部影响)权重来自动计算对应表情的网格;BS也是用画刷结合权重,利用maya或者其它工具调整面部网格做出来的。所以要么是利用算法做网格变形实现驱动,要么预定义网格变形实现驱动。例如还是这个B站教程使用Maya工具节点绑定角色面部表情教程 - Rig a face using Maya’s Utility nodes,或者对应的图文解析如何在Maya流程下创建一整套面部绑定,就用骨骼驱动人脸,然后用blendshape实现微调,简单说就是“blendShapes的优势就是可以提供更精确的表情,而关节可以实现面部区域的拉伸,增加更多的表情灵活性”。因此每种方法都有自己优劣势,也可以结合使用,只不过常见代码中的做法基本都是基于BS来实现表情驱动的。

本文和接下来的系列博文,将先针对BS驱动,分析和抽取几个源码内容,来加深理解。

源码理论与实验分析

拿strongtrack的源码开刀,作者提供了效果视频,在油管上自己观看

【吐槽】本来想把视频转发到B站的,但是坑货B站把视频当做恐怖内容了,因为作者的视频只有头没有身体,就被当成恐怖镜头了,审核不通过。

准备工作

作者提供了一个人头BlendShape模型,关于Blender的模型可以去作者提供的谷歌网盘下载,也可以从我的百度云下载

链接:https://pan.baidu.com/s/15tlWmJ9grXw4eI6TiVC-Iw
提取码:871c

为了便于分析理论和BS的操作,我预先把所有的BS模型从Blender文件中导出来了,所有的BS可以在我的github上找到,作者做了50组BS,名字分别如下:

'eyeBlinkLeft', 'eyeBlinkRight', 'eyeSquintLeft', 'eyeSquintRight', 'eyeLookDownLeft', 'eyeLookDownRight', 'eyeLookInLeft',
'eyeLookInRight', 'eyeWideLeft', 'eyeWideRight', 'eyeLookOutLeft', 'eyeLookOutRight', 'eyeLookUpLeft', 'eyeLookUpRight', 'browDownLeft', 'browDownRight',
'browInnerUp', 'browOuterUpLeft', 'browOuterUpRight', 'jawOpen', 'mouthClose', 'jawLeft', 'jawRight', 'jawFwd',
'mouthUpperUpLeft', 'mouthUpperUpRight', 'mouthLowerDownLeft', 'mouthLowerDownRight', 'mouthRollUpper',
'mouthRollLower', 'mouthSmileLeft', 'mouthSmileRight', 'mouthDimpleLeft','mouthDimpleRight', 'mouthStretchLeft',
'mouthStretchRight', 'mouthFrownLeft', 'mouthFrownRight', 'mouthPressLeft', 'mouthPressRight', 'mouthPucker', 'mouthFunnel', 'mouthLeft','mouthRight',
'mouthShrugLower','mouthShrugUpper', 'noseSneerLeft', 'noseSneerRight', 'cheekPuff', 'cheekSquintLeft', 'cheekSquintRight'

至于每一组BS代表的表情,可以自行去Blender查看,或者用meshlab打开我导出的OBJ文件观看。

提前说一句,从代码里面可以发现,作者只用了其中10组表情基(BS),详细如下:

Basis, jawOpen, mouthSmile, mouthSmileLeft, mouthSmileRight, mouthFrown, mouthFunnel, mouthPucker, browInnerUp, browDown

源码流程

先大概介绍一下作者算法流程,在strongtrack.pyVideoThread函数中可以逐步分析

  • 先使用dlib获取人脸关键点,并且做了微调训练
  • 分别记录真人的10个关键姿态(与使用的BS对应),表情分别为:NeutralJaw Open, Closed Smile, Smile Left, Smile Right, Mouth Frown, Lip Funnel, Lip Pucker, Brows Up, Brows Down
  • 进入实时驱动阶段时候,核心在decomp_function.py文件中的findCoeffAll函数,其步骤为
    • 提取嘴部系数:因为与嘴巴相关的BS有八个,因此源码中先针对关键姿态和当前帧的嘴部关键点做中心对齐,然后使用稀疏编码求解系数
    • 提取眉毛系数:因为与眉毛相关的BS只有2个,而且一个向上,另一个向下,区分非常明显,所以可以直接计算,源码的方法是计算当前帧的眉毛中心相对于自然状态下眉毛中心的偏移量,分别除以两个BS相对于自然状态下眉毛中心的偏移量,就分别得到了眉毛两个BS的系数,然后对眉毛向下的情形做一下系数值的约束即可。
    • 提取眨眼系数:这个更简单了,以内外眼角平均坐标值为眼睛中心,上下眼框分别计算坐标中心,除一个指定的固定值即可。
  • 所有系数计算完毕以后,即可驱动模型表情

源码简化

上述的整个源码流程中,我最看好那个稀疏编码求解系数的过程,涨知识了。其它的都是利用几何关系计算,没什么技术含量。结果代码还写了好多好多,其中有很大一部分是写界面,介绍如何训练人脸关键点检测模型,以及利用OSC建立pythonblender的实时通信。

所以按照博客宗旨,我们仅分析系数计算这一块内容,跳过关键点检测模型的训练以及通信代码的书写。因此,在实验时候,10个真人表情对应的2D人脸关键点,我直接从对应BS中获取(去掉深度坐标轴),然后随便组合两组BS导出来,作为实时驱动时候的人脸关键点。

随后,在源码的基础上抽取了提取表情系数部分的代码,同时针对性修改和简化了一下:

  • 对齐关键点,嘴巴和眉毛是分开提取系数的,所以它们的位置是分开做中心对齐的,流程就是分别提取关键表情和当前帧中人脸对应部位关键点,然后根据脸宽缩放,最后按照中心坐标对齐

    '''
    将表情基的人脸关键点与当前表情关键点对齐
    可以用于处理局部关键点,源码中分别处理眼、嘴
    '''
    def shiftKeyPoses(new_width, centroid, keyposes, config):   #Scale keypose based on head width to accomodate for translation or different video size.width_keypose = (keyposes[0][67][0]-keyposes[0][51][0]) # 表情基中第一个姿态的人脸宽度width_fac = width_keypose/new_width # 表情基脸宽/真人脸宽keyposes = np.divide(keyposes, [width_fac,width_fac]).astype(int) # 依据比例系数,将所有表情基关键点缩放到真人面部大小new_poses = []for i in range(keyposes.shape[0]): # 遍历所有的表情基#For brows we take average of eyes pointsif config == 'brows': keypose = np.array(keyposes[i][10:22])#Fo mouth we take average of mouth pointsif config == 'mouth':keypose = np.array(keyposes[i][31:51])centroid_keypose = keypose.mean(0) #表情基中眉毛或者嘴部的中心delta = centroid_keypose-centroid # 表情基眉毛或嘴中心与真人眉毛或嘴中心的偏移量new_pose = keyposes[i]-delta.astype(int) # 利用中心偏移量 重新调整表情基的位置new_poses.append(new_pose) # 将新的表情基加入数组中返回return np.array(new_poses)
    
  • 嘴部BS系数直接使用sklearn中的SparseCoder函数进行求解,用法也很简单

    '''
    嘴部BS
    '''
    # 对齐keypose和真人嘴部关键点
    mouth_center = testpose[31:51].mean(0)
    shift_kps_mouth = shiftKeyPoses(width_points,mouth_center,keyposes_mouth,"mouth")
    # 重组嘴部坐标,便于计算
    target_mouth = testpose[31:51].reshape((1,-1))
    dict_2d_mouth = []
    for i in range(shift_kps_mouth.shape[0]):dict_2d_mouth.append(shift_kps_mouth[i][31:51])
    dict_2d_mouth = np.array(dict_2d_mouth).reshape(shift_kps_mouth.shape[0],-1)
    # 提取嘴部运动的系数
    coder = SparseCoder(dictionary=dict_2d_mouth.astype("float32"),transform_n_nonzero_coefs=None,transform_alpha=10,transform_algorithm='lasso_lars')
    coeffs = coder.transform(target_mouth.astype("float32"))
    
  • 计算左右眉毛的上下运动,计算方法上面说过,就是关键表情的关键点和当前帧关键点的眉毛相对于自然状态下眉毛的偏移比例

    # 计算眉毛
    def calBrow(points, keyposes, config, config2):# 眉毛姿态集中,分别有正常,眉毛上,眉毛下if config == 'left':first = 5last = 10if config == 'right':first = 0last = 5if config2 == "up":target = 1 # 眉毛上else:target = 2 # 眉毛下# 计算挑眉的keypose相对于自然表情下眉毛移动deltashifted = keyposes[target][first:last] - keyposes[0][first:last]deltashifted = (sum(sum(abs(deltashifted))))# 计算当前人脸相对于自然表情下眉毛移动deltapoints = (points[first:last]) - (keyposes[target][first:last])deltapoints = (sum(sum(abs(deltapoints))))# 直接相除,得到比例系数if deltapoints < (deltashifted):val = 1 - (deltapoints / deltashifted)else:val = 0.0# 如果是眉毛向下,可以用垂直比例来辅助计算,不然不准if(target==2):ydelt  = keypose[2][first:last] - points[first:last]ydelt = sum(ydelt.T[1])if(ydelt<=0):val = 1.0return val
    

    眉毛向下的时候可能有点难算,或者出问题,所以额外加了个约束:

    # 约束
    def constraint(val,lower,upper):factor = 1 / lowerif lower > val:new_val = 0.0if lower <= val < upper:new_val = (val - lower) * factorif val >= upper:new_val = 1.0return new_val
    

    调用时候如下:

    # 对齐眉毛关键点
    eye_center = testpose[10:22].mean(0)
    shift_kps_eye = shiftKeyPoses(width_points,eye_center,keyposes_brows,"brows")
    # 分别提取左右眉毛上下运动的系数
    val_l_up = calBrow(testpose,shift_kps_eye,"left","up")
    val_r_up = calBrow(testpose,shift_kps_eye,"right","up")
    val_l_down = constraint(calBrow(testpose,shift_kps_eye,"left","down"),0.4,0.8)
    val_r_down = constraint(calBrow(testpose,shift_kps_eye,"right","down"),0.4,0.8)
    
  • 眨眼这个过程就是计算上下眼眶个子的中心坐标以及整个眼眶中心坐标的关系,但是涉及到常量,这个常量应该是依据场景确定出来的,所以代码无法过于深究:

    #左眼
    eye_top_r = testpose[11:13].mean(0)
    eye_mid_r = testpose[[10,13]].mean(0)
    eye_bottom_r = testpose[14:16].mean(0)
    blink_r_coeff = (eye_top_r[1]-eye_mid_r[1]+28)/48
    squint_r_coeff = (eye_mid_r[1]-eye_bottom_r[1]+17)/7.5
    #右眼
    eye_top_l = testpose[17:19].mean(0)
    eye_mid_l = testpose[[16,19]].mean(0)
    eye_bottom_l = testpose[20:22].mean(0)
    blink_l_coeff = (eye_top_l[1]-eye_mid_l[1]+28)/48
    squint_l_coeff = (eye_mid_l[1]-eye_bottom_l[1]+17)/7.5
    

源码计算BS的核心就是上面了,有点技术含量的就是计算嘴部BS使用的稀疏编码。

验证稀疏编码和BS结果

针对感兴趣的部分做一次验证,必须少不了可视化。提取嘴部BS的理论和代码就不重复了。

为了验证结果,上面说过利用BS做了两个测试用的表情模型,对应关键点如下:

关于BS融合变形的原理,通常是基于偏移量来计算的,也就是常看到的一个公式
R=Base+∑iwiOiR = Base + \sum_i w_i O_i R=Base+i∑​wi​Oi​
其中OiO_iOi​代表的是表情基相对于自然表情基的顶点偏移量
Oi=Bi−BaseO_i = B_i-Base Oi​=Bi​−Base
所以利用代码得到BS融合结果,需要先把每个表情基偏移量算出来:

# 获取BS偏移量
# 0:Neutral
basicVerts = getVerts('./data/Basis.obj')
# 1:Jaw Open
jawopenVerts = getVerts('./data/jawOpen.obj')
# 2:Closed Smile
closesmileVerts = getVerts('./data/mouthSmile.obj')
# 3:Smile Left
smileleftVerts = getVerts('./data/mouthSmileLeft.obj')
# 4:Smile Right
smilerightVerts = getVerts('./data/mouthSmileRight.obj')
# 5:Mouth Frown
mouthfrownVerts = getVerts('./data/mouthFrown.obj')
# 6:Lip Funnel
lipfunnelVerts = getVerts('./data/mouthFunnel.obj')
# 7:Lip Pucker
lippuckerVerts = getVerts('./data/mouthPucker.obj')offset = []
offset.append(basicVerts-basicVerts)
offset.append(jawopenVerts-basicVerts)
offset.append(closesmileVerts-basicVerts)
offset.append(smileleftVerts-basicVerts)
offset.append(smilerightVerts-basicVerts)
offset.append(mouthfrownVerts-basicVerts)
offset.append(lipfunnelVerts-basicVerts)
offset.append(lippuckerVerts-basicVerts)offset = np.array(offset,dtype="float32")

然后再去组合得到结果:

# 根据系数组合BS
newVert = basicVerts
for i in range(offset.shape[0]):newVert = newVert + coeffs[0,i]*offset[i]
writeResult(newVert)

把结果写入到OBJ并与测试表情模型做对比,结果如下:

可以发现上下表情几乎一模一样,所以验证成功,使用稀疏编码计算表情系数是可行的。

后记

稀疏编码看起来貌似是挺强大的,后续也可以尝试将整个表情关键点都用系数编码计算一下试试,不要手动计算了,虽然靠谱,但是有点low啊。

还有通常BS是被约束到(0,1)(0,1)(0,1)范围内的,这个库貌似无法保证最终表情系数在此范围,后续再继续探索一下。

完整的python实现放在微信公众号的简介中描述的github中,有兴趣可以去找找。同时文章也同步到微信公众号中,有疑问或者兴趣欢迎公众号私信。

卡通角色表情驱动系列一相关推荐

  1. 卡通角色表情驱动系列二

    前言 之前介绍了使用传统算法求解BS系数的表情驱动方法,其中提到过的三种方法之一是基于网格形变迁移做的,那么这篇文章就是对<Deformation Transfer for Triangle M ...

  2. 3D人脸表情驱动——基于eos库

    前言 之前出过三篇换脸的博文,遇到一个问题是表情那一块不好处理,可行方法是直接基于2D人脸关键点做网格变形,强行将表情矫正到目标人脸,还有就是使用PRNet的思想,使用目标人脸的顶点模型配合源人脸的纹 ...

  3. 基于改进First_order的表情驱动图片系统(源码&教程)

    1.研究背景 早期的Facerig软件可以即时抓取摄像头视频中的人脸关键点,将表情.头部姿态等信息转化到三维虚拟化身上,比如一只猫.一个虚拟卡通人物上.然后在使用即时视频通讯软件时,选择facerig ...

  4. 设计灵感|C4D卡通角色设计作品,你想要的模型集设都有

    三维卡通角色,清新的配色.可爱的小动物,谁不想拥有这样的同款手办呀!就这级别的C4D模型辣么多你说它香不香? 集设网 www.ijishe.com 设计师交流社区还包含很多种类的风格属性,从古代到现代 ...

  5. 二次元卡通角色渲染实现

    基本原理 文中蓝色字体为应用于本案例的技术 一.效果分析 这里的卡通效果针对日式卡通渲染 Cel Shading 首先,角色有明显的轮廓线. 其次,角色身上的色调呈色阶式呈现,有大片的纯色色块和明显的 ...

  6. python制作简单动漫_Blender 2.9简单卡通角色完整制作训练视频教程

    本教程是关于Blender 2.9简单卡通角色完整制作训练视频教程,时长:1小时21分,大小:6.8 GB,格式:MP4高清视频格式,教程使用软件:Blender 2.9,作者:Dino Bandzo ...

  7. 【教程】卡通角色在现实生活中出现视频制作-python代码

    首先准备卡通角色,比如一条鱼,如下图: 为了让其有游动的效果,再准备一个稍微变化的形态,比如一个向上游动的姿态 然后背景的话,就直接在现实生活中随便拍张图片即可,如下: 现在我们要表现出鱼儿在桌子上游 ...

  8. python+unity表情驱动一

    python+unity表情驱动工具一 引言 ui界面 python代码 所需资源 引言 最近看到一个视频关于python控制unity人物表情的视频,大体思路是使用python控制摄像头进行人脸识别 ...

  9. python+unity表情驱动二(打包成exe)

    python+unity表情驱动工具二(打包成exe) 使用pyinstaller在控制台打包 工具的使用效果 使用pyinstaller在控制台打包 这里并没有使用auto-py-to-exe界面工 ...

最新文章

  1. mysql 一键安装 linux_linux下mysql8 shell脚本一键安装
  2. 把ePO4.5或者4.6从32位迁移到64位系统
  3. Verilog初级教程(21)Verilog中的延迟控制语句
  4. 简单的 H5 视频推流解决方案
  5. 多图指南——微信小程序设计规范
  6. [数据结构-严蔚敏版]P48栈的链式表示
  7. 怎么把pdf转换为html,如何将PDF转换成HTML网页格式呢?
  8. python-json操作
  9. CentOS7 基于http服务搭建本地yum仓库
  10. Python3安装cx_Oracle连接oracle数据库实操总结
  11. Linux 命令(113)—— seq 命令
  12. 如何实现数字化转型?麦肯锡:数字化转型四步法
  13. 如何在csdn写博客
  14. 字符类型与Unicode 编码
  15. mysql 设置连接超时_如何配置MySQL数据库超时设置
  16. digest 词根 gest
  17. 常用的http状态码 状态码大全 常见的状态码
  18. web、pc客户端、app测试的区别
  19. VS2022中scanf返回值被忽略的原因及其解决方法
  20. java 矩阵求秩_线性代数精华3——矩阵的初等变换与矩阵的秩

热门文章

  1. iphone投屏_iPhone投屏电视机/投影仪用这个方法很简单,媲美华为PC模式
  2. [蓝桥杯][算法提高VIP]打水问题(贪心)
  3. Servlet使用基本步骤及功能实现
  4. ajax绑定事件页面重复提交,ajax防止用户重复提交点击事件
  5. java字符串是不是整数的函数_java判断字符串是否为整数的方法
  6. 总是想逃避不想去面对(又是发牢骚的一天)
  7. 数学--数论--康托展开与逆康托展开
  8. 十分钟看懂什么是VoLTE【包学包会,否则退款】
  9. 如何在ubuntu 12.04系统下开启nfs网络文件系统
  10. [机器学习]正则化方法 -- Regularization