我是靠谱客的博主 壮观人生,最近开发中收集的这篇文章主要介绍七点人脸姿态估计_人脸姿态估计算法分享,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

5d3d343720f16f60747fec4f6c3019f6.png

人脸姿态估计指的是根据一幅2维的人脸图像,计算出这个人在实际3维空间中的面部朝向。问题的输入条件就是一张人脸图片,输出自然就是可以表示方位的三个旋转角度 (pitch, yaw, roll),其中 pitch 表示俯仰角(关于x轴的旋转角度),yaw 表示偏航角(关于y轴的旋转角度),roll 表示翻滚角(关于z轴的旋转角度),分别如下面 3 图所示:(说句题外话,如果我们把下面图中的物体看作是一架向我们飞来的飞机,就可以理解为何这三个角要如此命名了)

698e574319b94a8f0a0aa73e37f36c3d.png

f857319003b1102840a6221429e80987.png

d2a1ebcd3894b1a19b097d158729eadd.png

算法的思路很简单,可以考虑这样一种场景,假设我们有一张标准的3d人脸模型,如果投影和输入图片的人脸大致重合,那么此时 3d 模型的方位就可以看作是图片中人脸在实际空间中的方位了。如果投影和图像的差异很大,那我们再对 3d 模型进行旋转,平移,拉伸等操作,可以明确的是,只要经过合适的调整,总会出现两者相重合的情况。
所以现在的问题就变成了三个子问题:1. 怎样得到标准的人脸 3d 模型;2. 对 3d 模型的各种变换操作怎样从数字上体现出来;2. 如何量化的表示“重合”这一概念。
我们先来看第二个问题,在数学上,旋转、平移、拉伸操作其实都是矩阵运算,举个简单的例子,在二维平面上旋转一条线段,就是使用旋转目标 src 去乘以二维旋转矩阵,得到的 dst 便是旋转后的线段。

8aeb3377787924b1204bb7c4efddefb3.png

平移操作更简单一点,在坐标分量上加一个偏移量即可。拉伸也同理,只需要在不同的方向上乘以缩放系数。于是,空间中的一个对象,经过上述变换后,最终得到的东西由下式给出

其中 t 是平移矩阵,大小为

,它的每一列数值为常量,是对应方向上的移动距离

s 是缩放向量,具体形式为

这里的

是三个方向上的缩放系数。

下面我们重点讨论一下旋转矩阵

的形式,与二维情况不同的是,三维旋转有三个旋转轴,针对每一个旋转轴都有一个旋转矩阵形式,具体来说

其中

分别是关于 x,y,z 轴的旋转角度,最终的旋转矩阵是上述这三个矩阵相乘之积

于是经过变换后的模型就是

这些变量的函数

搞定了标准 3d 人脸模型的变换之后,接下来我们来解决如何量化投影与人脸图像重叠的问题。由于 3d 模型其实就是一系列点坐标,所以我们首先也应该把二维人脸图像使用点坐标来表示。关于人脸关键点检测的算法有很多,这里我们使用 dlib 框架提供的 68 点检测模型,它的标准图像如下(具体坐标在文末给出)

5baf9aebb3c587cd6f1493ea2d5a26ed.png

有了标准人脸关键点之后,我们需要对其进行空间变换,也就是前面推导的

然后再将其投影到 xy 平面上,当然这个投影矩阵比较简单,只需要提取 dst 中每个点的前两个坐标就可以了

投影后的点集合为

为了衡量投影与图片人脸关键点的重合程度,我们采用平方误差损失函数

其中

是人脸图像第 i 个关键点的坐标。于是最终要求解的问题便为

下面我们采用梯度下降法来求解,参数迭代格式为

其中

是学习速率,
代表一种参数,也就是前面的
等等,经过多次迭代,
应该能收敛到一个稳定值,此时便完成了整个优化过程。

接下来,利用上述思路,我们来实际编程试验一下,由于目标函数
的形式还是有点复杂,手动求导不太现实,所以这里我们采用 pytorch 来完成自动求导的功能,首先是定义上面提到的几个参数
alpha = torch.tensor(0.0, requires_grad=True, dtype=torch.float64)
beta = torch.tensor(0.0, requires_grad=True, dtype=torch.float64)
gamma = torch.tensor(0.0, requires_grad=True, dtype=torch.float64)
lambdaX = torch.tensor(1.0, requires_grad=True, dtype=torch.float64)
lambdaY = torch.tensor(1.0, requires_grad=True, dtype=torch.float64)
lambdaZ = torch.tensor(1.0, requires_grad=True, dtype=torch.float64)
dx = torch.tensor(1.0, requires_grad=True, dtype=torch.float64)
dy = torch.tensor(1.0, requires_grad=True, dtype=torch.float64)
dz = torch.tensor(1.0, requires_grad=True, dtype=torch.float64)

由于要对它们求导,所以需要 requires_grad 为 True。

然后定义投影到 xy 平面的矩阵

## 投影矩阵
prj = torch.tensor([[1, 0],
                   [0, 1], 
                   [0, 0]], dtype=torch.float64)

以及旋转矩阵

def rx(a): 
    """
     关于x轴的旋转矩阵
    """
    RX = torch.zeros(3, 3, dtype=torch.float64)
    RX[0, 0] = 1
    RX[1, 1] = a.cos()
    RX[1, 2] = -a.sin()
    RX[2, 1] = a.sin()
    RX[2, 2] = a.cos()
    return RX

def ry(b): 
    """
     关于y轴的旋转矩阵
    """
    RY = torch.zeros(3, 3, dtype=torch.float64)
    RY[0, 0] = b.cos()
    RY[0, 2] = b.sin()
    RY[1, 1] = 1
    RY[2, 0] = -b.sin()
    RY[2, 2] = b.cos()
    return RY

def rz(r): 
    """
     关于z轴的旋转矩阵
    """
    RZ = torch.zeros(3, 3, dtype=torch.float64)
    RZ[0, 0] = r.cos()
    RZ[0, 1] = -r.sin()
    RZ[1, 0] = r.sin()
    RZ[1, 1] = r.cos()
    RZ[2, 2] = 1
    return RZ

def r(a, b, r):
    """
    空间旋转矩阵
    """
    R = torch.mm(rx(a), ry(b))
    R = torch.mm(R, rz(r))
    return R

这里我们给旋转矩阵赋值的方式有点怪异,没有直接从数组建立,这是因为旋转矩阵也需要包含梯度信息。
接下来定义空间变换函数和目标函数

def transform(src):
    """
    空间变换
    """
    ## 缩放矩阵
    s=torch.zeros(3, 3, dtype=torch.float64)
    s[0, 0] = lambdaX
    s[1, 1] = lambdaY
    s[2, 2] = lambdaZ
    
    ## 位移向量
    t = torch.zeros(1, 3, dtype=torch.float64)
    t[0, 0] = dx
    t[0, 1] = dy
    t[0, 2] = dz

    R = r(alpha, beta, gamma)
    dst = torch.mm(src, R)+ t
    dst = torch.mm(dst, s)
    return dst

def L(src, p):
    """
    目标函数
    """
    dst = transform(src)
    dst = torch.mm(dst, prj)    
    L = dst - p
    
    return (L[::,0]**2+L[::, 1]**2).sum()/len(src)

最后,以迭代的方式优化目标函数

src = np.loadtxt("/path/to/src.csv")
src = torch.from_numpy(src)

p = np.loadtxt("/path/to/p.csv")
p = torch.from_numpy(p)

iter=128000
lr = 0.00001

def gd(params):
    """
    梯度下降
    """
    for p in params:
        p.data -= lr * p.grad
        p.grad.data.zero_()

for i in range(iter):
    l = L(src, p)
    if i % 1000 ==0:
        print(l)    

    l.backward()
    
    gd([alpha, beta, gamma, lambdaX, lambdaY, lambdaZ, dx, dy, dz])

这里我们把学习率设的很小,否则结果不收敛。下面我们用 zack 的脸来跑个例子

6210e595cf5b34878bcd637386e6798c.png

首先根据原图来看,zack 脸部大致是朝左斜向下的,具体来说,俯仰角(绕 x 轴)

,偏航角(绕y轴)
,翻滚角(绕z轴)几乎为 0。当然我这里给的范围比较粗糙,主要是为了检验算法是否会得出离谱的结果。

bc0e6da435a9f65a8e117dbcfdd9eb43.png


上图左边是原图的人脸关键点,右边是学习出来的结果,两者的面部方向有点差异,但大致上是相同的,可见,我们这个算法有一定效果。当然这只是比较粗糙的实验,用于说明方案的可行性,这里具体的优化算法显得有点暴力,所以时间效率不是很好,后面我们将研究 opencv 所使用的方法。


最后附上68点标准3d人脸坐标

-73.393523,29.801432,-47.667532
-72.775014,10.949766,-45.909403
-70.533638,-7.929818,-44.84258
-66.850058,-26.07428,-43.141114
-59.790187,-42.56439,-38.635298
-48.368973,-56.48108,-30.750622
-34.121101,-67.246992,-18.456453
-17.875411,-75.056892,-3.609035
0.098749,-77.061286,0.881698
17.477031,-74.758448,-5.181201
32.648966,-66.929021,-19.176563
46.372358,-56.311389,-30.77057
57.34348,-42.419126,-37.628629
64.388482,-25.45588,-40.886309
68.212038,-6.990805,-42.281449
70.486405,11.666193,-44.142567
71.375822,30.365191,-47.140426
-61.119406,49.361602,-14.254422
-51.287588,58.769795,-7.268147
-37.8048,61.996155,-0.442051
-24.022754,61.033399,6.606501
-11.635713,56.686759,11.967398
12.056636,57.391033,12.051204
25.106256,61.902186,7.315098
38.338588,62.777713,1.022953
51.191007,59.302347,-5.349435
60.053851,50.190255,-11.615746
0.65394,42.19379,13.380835
0.804809,30.993721,21.150853
0.992204,19.944596,29.284036
1.226783,8.414541,36.94806
-14.772472,-2.598255,20.132003
-7.180239,-4.751589,23.536684
0.55592,-6.5629,25.944448
8.272499,-4.661005,23.695741
15.214351,-2.643046,20.858157
-46.04729,37.471411,-7.037989
-37.674688,42.73051,-3.021217
-27.883856,42.711517,-1.353629
-19.648268,36.754742,0.111088
-28.272965,35.134493,0.147273
-38.082418,34.919043,-1.476612
19.265868,37.032306,0.665746
27.894191,43.342445,-0.24766
37.437529,43.110822,-1.696435
45.170805,38.086515,-4.894163
38.196454,35.532024,-0.282961
28.764989,35.484289,1.172675
-28.916267,-28.612716,2.24031
-17.533194,-22.172187,15.934335
-6.68459,-19.029051,22.611355
0.381001,-20.721118,23.748437
8.375443,-19.03546,22.721995
18.876618,-22.394109,15.610679
28.794412,-28.079924,3.217393
19.057574,-36.298248,14.987997
8.956375,-39.634575,22.554245
0.381549,-40.395647,23.591626
-7.428895,-39.836405,22.406106
-18.160634,-36.677899,15.121907
-24.37749,-28.677771,4.785684
-6.897633,-25.475976,20.893742
0.340663,-26.014269,22.220479
8.444722,-25.326198,21.02552
24.474473,-28.323008,5.712776
8.449166,-30.596216,20.671489
0.205322,-31.408738,21.90367
-7.198266,-30.844876,20.328022

最后

以上就是壮观人生为你收集整理的七点人脸姿态估计_人脸姿态估计算法分享的全部内容,希望文章能够帮你解决七点人脸姿态估计_人脸姿态估计算法分享所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部