三维人脸实践:基于Face3D的人脸生成、渲染与三维重建 <三>

news2024/12/29 14:21:37

face3d: Python tools for processing 3D face

git code: https://github.com/yfeng95/face3d
paper list: PaperWithCode

基于BFM模型,估计3DMM的参数,可以实现线性的人脸表征,该方法可用于基于关键点的人脸生成、位姿检测以及渲染等。推荐!!!


目录


3DMM模型生成的人脸1:平常表情
在这里插入图片描述
3DMM模型生成的人脸2:微笑表情
在这里插入图片描述

3DMM模型是如何运行的?其原理是怎样的?如何实现三维人脸定制生成呢?
要回答上述问题,必须要弄清楚3DMM提供了哪些信息?如何编辑这些信息以达到特定的人脸生成。

一、回顾

在Face3d中的求解过程可以概述如下:

  1. 初始化α , β 为0;
  2. 利用黄金标准算法得到一个仿射矩阵{P_A},分解得到 s , R , t 2 d {s,R,t_{2d}} s,R,t2d
  3. 将(2)中求出的 s , R , t 2 d s,R,t_{2d} s,R,t2d 带入能量方程,解得β;
  4. 将(2)和(3)中求出的α代入能量方程,解得α;
  5. 更新α , β的值,重复(2)-(4)进行迭代更新

我们在上一个系列中已经介绍了算法的前两步骤。这里继续分析如何求解α , β

1.1 将(2)中求出的 s , R , t 2 d s,R,t_{2d} s,R,t2d 带入能量方程,解得β

代码分析
在上一篇文章我们通过黄金标准算法求解出了仿射矩阵 P A P_A PA并将它分解的到了 s , R , t 2 d s,R,t_{2d} s,R,t2d,这部分将继续按照求解步骤进行源码分析:

for i in range(max_iter):
        X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)
        X = np.reshape(X, [int(len(X)/3), 3]).T
        
        #----- estimate pose
        P = mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T)
        s, R, t = mesh.transform.P2sRt(P)
        rx, ry, rz = mesh.transform.matrix2angle(R)
        # print('Iter:{}; estimated pose: s {}, rx {}, ry {}, rz {}, t1 {}, t2 {}'.format(i, s, rx, ry, rz, t[0], t[1]))

        #----- estimate shape
        # expression
        shape = shapePC.dot(sp)
        shape = np.reshape(shape, [int(len(shape)/3), 3]).T
        ep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 0.002)

        # shape
        expression = expPC.dot(ep)
        expression = np.reshape(expression, [int(len(expression)/3), 3]).T
        sp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 0.004)

    return sp, ep, s, R, t

对于公式:
在这里插入图片描述
其中形状部分为 ∑ i = 1 m α i S i \sum_{i=1}^{m}\alpha_iS_i i=1mαiSi,通过
shape = shapePC.dot(sp)
来定义shape的加成。
shape的格式为(159645,1),再通过
shape = np.reshape(shape, [int(len(shape)/3), 3]).T
将shape的XYZ坐标分开,转为(53215,3)格式。

下段代码
ep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 0.002)
其中ep即是β, estimate_expression的源码如下:

def estimate_expression(x, shapeMU, expPC, expEV, shape, s, R, t2d, lamb = 2000):
    '''
    Args:
        x: (2, n). image points (to be fitted)
        shapeMU: (3n, 1)
        expPC: (3n, n_ep)
        expEV: (n_ep, 1)
        shape: (3, n)
        s: scale
        R: (3, 3). rotation matrix
        t2d: (2,). 2d translation
        lambda: regulation coefficient

    Returns:
        exp_para: (n_ep, 1) shape parameters(coefficients)
    '''
    x = x.copy()
    assert(shapeMU.shape[0] == expPC.shape[0])
    assert(shapeMU.shape[0] == x.shape[1]*3)

    dof = expPC.shape[1]

    n = x.shape[1]
    sigma = expEV
    t2d = np.array(t2d)
    P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)
    A = s*P.dot(R) #(2,3)

    # --- calc pc
    pc_3d = np.resize(expPC.T, [dof, n, 3]) 
    pc_3d = np.reshape(pc_3d, [dof*n, 3]) # (29n,3)
    pc_2d = pc_3d.dot(A.T) #(29n,2)
    pc = np.reshape(pc_2d, [dof, -1]).T # 2n x 29

    # --- calc b
    # shapeMU
    mu_3d = np.resize(shapeMU, [n, 3]).T # 3 x n
    # expression
    shape_3d = shape
    # 
    b = A.dot(mu_3d + shape_3d) + np.tile(t2d[:, np.newaxis], [1, n]) # 2 x n
    b = np.reshape(b.T, [-1, 1]) # 2n x 1

    # --- solve
    equation_left = np.dot(pc.T, pc) + lamb * np.diagflat(1/sigma**2)
    x = np.reshape(x.T, [-1, 1])
    equation_right = np.dot(pc.T, x - b)

    exp_para = np.dot(np.linalg.inv(equation_left), equation_right)
    
    return exp_para

接下来我们对estimate_expression函数分析:

  • 数据处理
 x = x.copy()
    assert(shapeMU.shape[0] == expPC.shape[0])
    assert(shapeMU.shape[0] == x.shape[1]*3)

    dof = expPC.shape[1]

    n = x.shape[1]
    sigma = expEV
    t2d = np.array(t2d)
    P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)

首先是确认输入的格式正确:
assert(shapeMU.shape[0] == expPC.shape[0])
assert(shapeMU.shape[0] == x.shape[1]*3)
然后此时输入的表情主成分expPC格式为(159645,29)
令dof=29
dof = expPC.shape[1]
令n=68
n = x.shape[1]
另sigma=expEV 即表情主成分方差σ
sigma = expEV
t 2 d t_{2d} t2d转化为array数组
t2d = np.array(t2d)
P即正交投影矩阵 P o r t h P_{orth} Porth
P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)

	A = s*P.dot(R) #(2,3)

    # --- calc pc
    pc_3d = np.resize(expPC.T, [dof, n, 3]) 
    pc_3d = np.reshape(pc_3d, [dof*n, 3]) # (29n,3)
    pc_2d = pc_3d.dot(A.T) #(29n,2)
    pc = np.reshape(pc_2d, [dof, -1]).T # 2n x 29

    # --- calc b
    # shapeMU
    mu_3d = np.resize(shapeMU, [n, 3]).T # 3 x n
    # expression
    shape_3d = shape
    # 
    b = A.dot(mu_3d + shape_3d) + np.tile(t2d[:, np.newaxis], [1, n]) # 2 x n
    b = np.reshape(b.T, [-1, 1]) # 2n x 1

已知公式
在这里插入图片描述
定义和计算A、pc和b:

  • 定义A:
    A = s*P.dot(R)
    在这里插入图片描述
  • 计算pc:
    这里的pc计算相当于下式
    在这里插入图片描述
    将表情主成分expPC转换为(29,68,3)的新矩阵pc_3d
    pc_3d = np.resize(expPC.T, [dof, n, 3])

注意:这里的expPC经过了
expPC = model['expPC'][valid_ind, :n_ep]
运算,只包含特征点的表情主成分,格式为(683,29)
将pc_3d转换为(29
68,3)的格式:
pc_3d = np.reshape(pc_3d, [dof*n, 3])
计算出 p c 2 d = p c 3 d ⋅ A T pc_{2d}= pc_{3d}\cdot A^T pc2d=pc3dAT,pc_2d格式为(29*68,2):
pc_2d = pc_3d.dot(A.T)

得出将pc_2d展开后得pc:
pc = np.reshape(pc_2d, [dof, -1]).T

  • 定义b
    b的公式如下:
    在这里插入图片描述
    计算时由于矩阵的格式问题要先进行一些变换:
    这里的shapeMU也是只包含了68个特征点的
    将格式为(68*3,1)的shapeMU转换为格式(3,68):
    mu_3d = np.resize(shapeMU, [n, 3]).T
    这里的shape = shapePC.dot(sp),即 ∑ i = 1 m a i S i \sum_{i=1}^{m}a_iS_i i=1maiSi
    shape_3d = shape

到这里b就可以按照公式计算出来了,得到的b格式为(2,68)
b = A.dot(mu_3d + shape_3d) + np.tile(t2d[:, np.newaxis], [1, n])
然后将b转换为格式(68*2,1)
b = np.reshape(b.T, [-1, 1])

求取β
完成A、pc和b的定义和计算之后 X p r o j e c t i o n X_{projection} Xprojection 的公式就可以写成:
在这里插入图片描述
带入pc的式子可以写成:
在这里插入图片描述

X p r o j e c t i o n X_{projection} Xprojection 的公式带入能量方程:
在这里插入图片描述
得到对β 进行求导,得到导数为零时β 的取值。
L2范数求导可以使用公式:
在这里插入图片描述
得到:
在这里插入图片描述
化简得到:
在这里插入图片描述
接着就是求取β 的代码:

equation_left = np.dot(pc.T, pc) + lamb * np.diagflat(1/sigma**2)
x = np.reshape(x.T, [-1, 1])
equation_right = np.dot(pc.T, x - b)
exp_para = np.dot(np.linalg.inv(equation_left), equation_right)

1.2 (4).将(2)和(3)中求出的β代入能量方程,解得α

同理,求取α 的代码如下:

expression = expPC.dot(ep)
expression = np.reshape(expression, [int(len(expression)/3), 3]).T
sp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 0.004)

算法过程与求取β 相同,但是带入的β 是经过上面计算后的新值。

1.3 (5)更新α , β 的值,重复(2)-(4)进行迭代更新

到这里,循环迭代部分的代码告一段落,经过多次迭代计算(程序中给的迭代次数为三次),获得了所需要的sp, ep, s, R, t。

回到例程

回到bfm.fit从
fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
继续向下执行:

def fit(self, x, X_ind, max_iter = 4, isShow = False):
        ''' fit 3dmm & pose parameters
        Args:
            x: (n, 2) image points
            X_ind: (n,) corresponding Model vertex indices
            max_iter: iteration
            isShow: whether to reserve middle results for show
        Returns:
            fitted_sp: (n_sp, 1). shape parameters
            fitted_ep: (n_ep, 1). exp parameters
            s, angles, t
        '''
        if isShow:
            fitted_sp, fitted_ep, s, R, t = fit.fit_points_for_show(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
            angles = np.zeros((R.shape[0], 3))
            for i in range(R.shape[0]):
                angles[i] = mesh.transform.matrix2angle(R[i])
        else:
            fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
            angles = mesh.transform.matrix2angle(R)
        return fitted_sp, fitted_ep, s, angles, t

将旋转矩阵转换为XYZ角度angles = mesh.transform.matrix2angle(R)
之后返回fitted_sp, fitted_ep, s, angles, t。

回到3DMM例程
fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3)
部分执行完毕,继续向下执行:

x = projected_vertices[bfm.kpt_ind, :2] # 2d keypoint, which can be detected from image
X_ind = bfm.kpt_ind # index of keypoints in 3DMM. fixed.

# fit
fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3)

# verify fitted parameters
fitted_vertices = bfm.generate_vertices(fitted_sp, fitted_ep)
transformed_vertices = bfm.transform(fitted_vertices, fitted_s, fitted_angles, fitted_t)

image_vertices = mesh.transform.to_image(transformed_vertices, h, w)
fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)

接下来是
fitted_vertices = bfm.generate_vertices(fitted_sp, fitted_ep)
根据计算出的α , β 代入
在这里插入图片描述
算出 S n e w M o d e l S_{newModel} SnewModel ,对应的源码如下:

 def generate_vertices(self, shape_para, exp_para):
        '''
        Args:
            shape_para: (n_shape_para, 1)
            exp_para: (n_exp_para, 1) 
        Returns:
            vertices: (nver, 3)
        '''
        vertices = self.model['shapeMU'] + \
                   self.model['shapePC'].dot(shape_para) + \
                   self.model['expPC'].dot(exp_para)
        vertices = np.reshape(vertices, [int(3), int(len(vertices)/3)], 'F').T

        return vertices

算出 S n e w M o d e l S_{newModel} SnewModel 后再对三维模型进行相似变换:
transformed_vertices = bfm.transform(fitted_vertices, fitted_s, fitted_angles, fitted_t)

S t r a n f o r m e d = s ⋅ R ⋅ S n e w M o d e l + t 3 d S_{tranformed}=s\cdot R\cdot S_{newModel}+t_{3d} Stranformed=sRSnewModel+t3d

 def transform(self, vertices, s, angles, t3d):
        R = mesh.transform.angle2matrix(angles)
        return mesh.transform.similarity_transform(vertices, s, R, t3d)

之后的代码:

image_vertices = mesh.transform.to_image(transformed_vertices, h, w)
fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)

分别是将三维模型转为二维图像的格式并用自带的颜色信息对模型上色,这里不做过多解读了。

结果展示
生成的新的人脸图片被保存到results/3dmm目录下

# ------------- print & show 
print('pose, groudtruth: \n', s, angles[0], angles[1], angles[2], t[0], t[1])
print('pose, fitted: \n', fitted_s, fitted_angles[0], fitted_angles[1], fitted_angles[2], fitted_t[0], fitted_t[1])

save_folder = 'results/3dmm'
if not os.path.exists(save_folder):
    os.mkdir(save_folder)

io.imsave('{}/generated.jpg'.format(save_folder), image)
io.imsave('{}/fitted.jpg'.format(save_folder), fitted_image)

还可以通过生成一个gif展示特征点拟合的过程:

# fit

fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3, isShow = True)

# verify fitted parameters
for i in range(fitted_sp.shape[0]):
	fitted_vertices = bfm.generate_vertices(fitted_sp[i], fitted_ep[i])
	transformed_vertices = bfm.transform(fitted_vertices, fitted_s[i], fitted_angles[i], fitted_t[i])

	image_vertices = mesh.transform.to_image(transformed_vertices, h, w)
	fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)
	io.imsave('{}/show_{:0>2d}.jpg'.format(save_folder, i), fitted_image)

options = '-delay 20 -loop 0 -layers optimize' # gif. need ImageMagick.
subprocess.call('convert {} {}/show_*.jpg {}'.format(options, save_folder, save_folder + '/3dmm.gif'), shell=True)
subprocess.call('rm {}/show_*.jpg'.format(save_folder), shell=True)

下面是结果展示:

  • generated.jpg
    在这里插入图片描述

  • fitted.jpg
    在这里插入图片描述

  • 3dmm.gif

在这里插入图片描述
当然,每次执行程序生成的新的随机模型的样子也会不同。


总结

这里主要介绍基于3DMM模型的反向过程求解α , β 的过程。很多细节还是要对照代码去理解公式与原理,结合公式去指导代码阅读。

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

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

相关文章

信息收集之搜索引擎

Google Hacking 也可以用百度,不过谷歌的搜索引擎更强大 site 功能:搜索指定域名的网页内容,可以用来搜索子域名、跟此域名相关的内容 示例: site:zhihu.com 搜索跟zhihu.com相关的网页“web安全” site:zhihu.com 搜索zhihu…

提升学习 Prompt 总结

NLP现有的四个阶段: 完全有监督机器学习完全有监督深度学习预训练:预训练 -> 微调 -> 预测提示学习:预训练 -> 提示 -> 预测 阶段1,word的本质是特征,即特征的选取、衍生、侧重上的针对性工程。 阶段2&…

C++核心编程

一、内存分区模型概述:C程序在执行时,将内存划分为4个区域程序运行前:代码区:存放函数体的二进制代码,由操作系统管理①共享。共享的目的是对于频繁被执行的程序,在内存中只需有一份代码即可②只读。使其只…

组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比

组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比 目录 组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比预测效果基本介绍模型描述程序设计参考资料预测效果 基本介绍 MATLAB实现EMD-KP

传输层协议 TCP UDP

目录 协议前菜 端口号 ​编辑端口号范围划分 认识知名端口号(Well-Know Port Number) netstat pidof 传输层协议 UDP协议 UDP协议端格式 UDP的特点 面向数据报 UDP的缓冲区 UDP使用注意事项 基于UDP的应用层协议 TCP协议 TCP协议概念 TCP协议段格式 标志…

深度分析中国高端投教市场第一股“九方财富”的投资价值

来源:猛兽财经 作者:猛兽财经 猛兽财经获悉,九方财富(09636)已于3月10在港交所成功IPO上市,并成为了“中国在线高端投教市场第一股”。 作为中国领先的在线投资决策方案提供商,九方财富…

一起来学习配置Combo接口吧!

Combo接口是一个光电复用的逻辑接口,一个Combo接口对应设备面板上一个GE电接口和一个GE光接口。电接口与其对应的光接口是光电复用关系,两者不能同时工作(当激活其中一个接口时,另一个接口就自动处于禁用状态)&#xf…

常用存储芯片-笔记本上固态硬盘PTS11系列推荐

在存储领域中,除了存储颗粒之外,还有一种极其重要的芯片:存储控制芯片。存储控制芯片是CPU与存储器之间数据交换的中介,决定了存储器最大容量、存取速度等多个重要参数。特别是在AI、5G、自动驾驶时代,对于数据处理及存…

2.HTML页面组成

html页面组成html简介-基础-元素html属性-标题-段落html链接-头部-图像html表格-列表-区块html表单-框架-颜色html字符实体-url前言: 在学习爬虫前,我们还需要了解HTML页面,学习它的组成部分以及各部分的意思和使用方法,代码我放在…

C++回顾(十八)—— 文件操作

18.1 I/O流概念和流类库结构 1 概念 程序的输入指的是从输入文件将数据传送给程序,程序的输出指的是从程序将数据传送给输出文件。 C输入输出包含以下三个方面的内容: (1)对系统指定的标准设备的输入和输出。即从键盘输入数据&am…

前端初学者 sdk 的使用

目录结构: package.json {"name": "ivanfor666","version": "1.0.0","description": "","main": "dist/index.cjs.js","module": "dist/index.esm.js",&quo…

Kafka 分区机制

Kafka 分区机制分区策略轮询策略随机策略按消息键保序策略基于地理位置的分区策略主题 (Topic) :承载真实数据的逻辑容器,主题下还分 n 个分区 Kafka 消息的三级结构: 主题 - 分区 - 消息主题下的每条消息只会保存在某个分区中,…

案例06-复用思想的接口和SQL

目录 一:背景介绍 二:思路&方案 三:过程 1.Controller层接口的复用 2.Mapper层sql语句的复用 四:总结 一:背景介绍 我们在开发项目的过程中非常容易出现的一种现象就是用什么我就直接写什么,就像我…

VPC专有网络介绍

基本概念 VPC:专有网络,云上用户自定义的私有网络。 vSwitch:交换机,组成专有网络的基础网络设备,交换机可以连接不同的云资源,在专有网络内创建云资源时,必须指定云资源所在连接的交换机。 vR…

微服务与分布式——SpringCloud

前提: 当项目逐渐变得庞大起来,简单的 spring 框架可能就不够用了,所以就需要用到分布式架构,我们这里简单介绍一下 springcloud 以及 springcloud 需要依赖的一些组件 目录: 1、分布式简介 2、Eureka 注册中心 3、R…

什么样的人更适合创业?那类人创业更容易成功?

创业是一项充满风险和机遇的事业,成功的创业者需要具备一定的素质和能力。那么,什么样的人更适合创业?哪类人创业更容易成功呢?本文将为您介绍几个适合创业的人群和成功创业者的共同特点。 具有创新精神的人 创业需要不断创新&am…

产品大本营丨计讯物联智慧灯杆网关全系列

在随着智慧城市不断推进,数字信息基础设施已然成为重要驱动。面对新一轮的智慧城市建设,技术赋能结合机制创新,驱动着追城市中生产方式、生活方式、管理方式的变革以及生产要素的配置,全面提升城市的信息化水平。 在新型智慧城市的…

webpack指南(项目篇)——webpack在项目中的运用

系列文章目录 webpack指南(基础篇)——手把手教你配置webpack webpack指南(优化篇)——webpack项目优化 文章目录系列文章目录前言一、配置拆分二、修改启动命令三、定义环境变量四、配置路径别名总结前言 前面我们对webpack的基…

Baumer工业相机堡盟相机如何使用自动曝光功能(自动曝光优点和行业应用)(C++)

项目场景 Baumer工业相机堡盟相机是一种高性能、高质量的工业相机,可用于各种应用场景,如物体检测、计数和识别、运动分析和图像处理。 Baumer的万兆网相机拥有出色的图像处理性能,可以实时传输高分辨率图像。此外,该相机还具…

Microsoft Word RTF Font Table Heap Corruption - 漏洞解析(CVE-2023-21716)

写在最前 如果你是信息安全爱好者,如果你想考一些证书来提升自己的能力,那么欢迎大家来我的 Discord 频道 Northern Bay。邀请链接在这里: https://discord.gg/9XvvuFq9Wb我拥有 OSCP,OSEP,OSWE,OSED&…