基于三维重建的人脸融合

一、引言

目前,2D人脸对齐技术已经相当成熟,而3D 人脸重构和3D 人脸对齐一直是一个研究的热点问题。目前普遍的方法是基于3DMM或者3D face template,本文直接通过深度学习,建立从2D图片到3D模版的映射关系。

本项目的目的是将目标人脸融合到源人脸得到新的人脸。其过程包括:
1、人脸密集对齐
2、3D人脸建模
3、人脸融合

二、人脸三维重建

2.1 三维图像

对于一个三维图像来说其关键信息包括三个:顶点(vertices)、颜色(colors)和三角形(triangles)。三角形是构成三维图像的基本单位,顶点是构成三角形的基本单位,颜色表示了图形的基本纹理和颜色特征。例如,blond-2d图,


基于三维重建的人脸融合_第1张图片
blond-2d图

加入已知这个二维图片的三维信息,即顶点、颜色和三角形,并写成obj文件,即可使用软件meshlab打开,如图所示。

基于三维重建的人脸融合_第2张图片
blond-3d图

因此,要想得到二维人脸的三维信息,必须得到该二维人脸的以上三个信息点。本文使用深度学习的方法学习二维图像和三维图像之间的密相关性,在使用密相关性来计算3DMM的参数。

本文使用UV position map作为三维人脸结构的表达,该表达比较简短,而且能够精准的表达三维图像中各个顶点。在以往的研究中,UV空间或UV坐标系是三维空间中表示二维平面图的参数,用来表达人脸的纹理信息,而在本文中我们用UV空间来存储3D人脸模型中点的3D坐标系,如下图所示。在左笛卡尔坐标系上定义了3D人脸点云,3D空间与输入图像的左上方重叠,x轴的正方向指向输入图像的右边。当3D人脸点云投影到x-y平面上时,ground truth3D人脸点云与2D的人脸完全匹配,因此用x, y, x坐标替换纹理图中的r, g, b值,就可以很好的理解了。(即用第二排来表示第一排的三维信息)

基于三维重建的人脸融合_第3张图片
图 1

为了保持position map中点的语义信息,我们使用3DMM来创建UV坐标系。我们想要拟合3D模型的完整结构,因此训练模型时需要无约束的2D人脸和相应的3D形状。

2.2 渲染---z-buffer

渲染是将一个三维的物体投影到一个二维的平面上。即使用二维平面来表示三维图形。也就是已知三维图像的顶点、颜色和三角形信息,得到对应三维图像的二维平面图。

3D空间中点的坐标是(x, y, z), z-buffer保存的是图像内顶点的z坐标值(即顶点的第三维坐标值)。投影后物体会产生近大远小的效果,所以距离眼睛比较近的地方,z坐标的分辨率较大,反之较小,也就是说,投影后的z坐标在其值域上对于离开眼睛的物理距离变化来说不是线性的。通过z-buffer我们可以确定一个点是否在屏幕上显示(当点的深度值大于z-buffer中已有的深度值是,则该点没有被遮挡,即可以显示出来)。如下图所示,z-buffer值可以显示成一张图,图中每个像素点对应着我们实际图像中像素的深度值,即图中越白的地方距离我们越近,z值越大。

z-buffer算法过程(代码见附录):

  • 初始化缓冲区,颜色缓冲区初始化为背景色,深度缓冲区被初始化为最大深度值。

  • 计算每个三角形上每片元的z值,并与对应位置上的深度缓冲区中的值进行比较

    如果z<=z-buffer(x, y)(即距离观察者更近),则需要同时修改两个缓冲区:将对应位置的颜色缓冲区的值修改为该片元的颜色,将对应位置的深度缓冲区的值修改为该片元的深度。即color(x,y) = color; z-buffer(x, y) = z。

上述3D图片经过z-buffer渲染之后得到如下2D图。在三维重建时,只重建了人脸部分,因此渲染回二维的图片时只有人脸部分。


基于三维重建的人脸融合_第4张图片
2d平面图

2.2 三维人脸重建网络

我们的三维人脸重建网络如图所示,该网络将输入的RGB人脸图转换为position map图像,我们使用encoder-decoder结构来学习这个转换过程。

在encoder部分,输入图像经过一个卷积层之后,接着10个残差层块,将输入的2562563的RGB图转换成88512的特征图;在decoder部分,88512的特征图经过17个反卷积层生成2562563的position map。其中所有的卷积核大小都设为4,使用ReLU作为激活函数。position map 包括所有的3D信息和密对齐信息,我们不需要额外的网络去做另外的多任务处理。结构图如下图所示。

基于三维重建的人脸融合_第5张图片
图 2

为了学习模型的参数,我们定义了一个新的损失函数来测量生成的position map和ground truth之间的差。如下图所示,权重mask表示position map中每个点的权重,根据我们的目标,我们将position map中的点分成四个部分(眼睛,鼻子,嘴巴,脸),没有部分共享他们自己的权重。人脸的68个脸部关键点享有最高的权重,以保证网络学习的准确率。脖子、头发和衣服等是不太关心的区域,以此设置为0。因此,损失函数定义为:

图 3

权重比例设置68个关键点:(眼睛,鼻子,嘴巴):脸部:脖子=16:4:3:0。

基于三维重建的人脸融合_第6张图片
图 4

2.3 网络结构主要代码解析

import tensorflow as tf
import tensorflow.contrib.layers as tcl
from tensorflow.contrib.framework import arg_scope
import numpy as np

size = 16  
# x: s x s x 3
se = tcl.conv2d(x, num_outputs=size, kernel_size=4, stride=1) # 256 x 256 x 16
# 10 resblock described as followed
se = resBlock(se, num_outputs=size * 2, kernel_size=4, stride=2) # 128 x 128 x 32
se = resBlock(se, num_outputs=size * 2, kernel_size=4, stride=1) # 128 x 128 x 32
se = resBlock(se, num_outputs=size * 4, kernel_size=4, stride=2) # 64 x 64 x 64
se = resBlock(se, num_outputs=size * 4, kernel_size=4, stride=1) # 64 x 64 x 64
se = resBlock(se, num_outputs=size * 8, kernel_size=4, stride=2) # 32 x 32 x 128
se = resBlock(se, num_outputs=size * 8, kernel_size=4, stride=1) # 32 x 32 x 128
se = resBlock(se, num_outputs=size * 16, kernel_size=4, stride=2) # 16 x 16 x 256
se = resBlock(se, num_outputs=size * 16, kernel_size=4, stride=1) # 16 x 16 x 256
se = resBlock(se, num_outputs=size * 32, kernel_size=4, stride=2) # 8 x 8 x 512
se = resBlock(se, num_outputs=size * 32, kernel_size=4, stride=1) # 8 x 8 x 512
# 18 transpose conv
pd = tcl.conv2d_transpose(se, size * 32, 4, stride=1) # 8 x 8 x 512 
pd = tcl.conv2d_transpose(pd, size * 16, 4, stride=2) # 16 x 16 x 256 
pd = tcl.conv2d_transpose(pd, size * 16, 4, stride=1) # 16 x 16 x 256 
pd = tcl.conv2d_transpose(pd, size * 16, 4, stride=1) # 16 x 16 x 256 
pd = tcl.conv2d_transpose(pd, size * 8, 4, stride=2) # 32 x 32 x 128 
pd = tcl.conv2d_transpose(pd, size * 8, 4, stride=1) # 32 x 32 x 128 
pd = tcl.conv2d_transpose(pd, size * 8, 4, stride=1) # 32 x 32 x 128 
pd = tcl.conv2d_transpose(pd, size * 4, 4, stride=2) # 64 x 64 x 64 
pd = tcl.conv2d_transpose(pd, size * 4, 4, stride=1) # 64 x 64 x 64 
pd = tcl.conv2d_transpose(pd, size * 4, 4, stride=1) # 64 x 64 x 64              
pd = tcl.conv2d_transpose(pd, size * 2, 4, stride=2) # 128 x 128 x 32
pd = tcl.conv2d_transpose(pd, size * 2, 4, stride=1) # 128 x 128 x 32
pd = tcl.conv2d_transpose(pd, size, 4, stride=2) # 256 x 256 x 16
pd = tcl.conv2d_transpose(pd, size, 4, stride=1) # 256 x 256 x 16
pd = tcl.conv2d_transpose(pd, 3, 4, stride=1) # 256 x 256 x 3
pd = tcl.conv2d_transpose(pd, 3, 4, stride=1) # 256 x 256 x 3
pos = tcl.conv2d_transpose(pd, 3, 4, stride=1, activation_fn = tf.nn.sigmoid)#, padding='SAME', weights_initializer=tf.random_normal_initializer(0, 0.02))
return pos

resblock 代码块:

def resBlock(x, num_outputs, kernel_size = 4, stride=1, activation_fn=tf.nn.relu, normalizer_fn=tcl.batch_norm, scope=None):
   assert num_outputs%2==0 #num_outputs must be divided by channel_factor(2 here)
   with tf.variable_scope(scope, 'resBlock'):
       shortcut = x
       if stride != 1 or x.get_shape()[3] != num_outputs:
           shortcut = tcl.conv2d(shortcut, num_outputs, kernel_size=1, stride=stride, 
                       activation_fn=None, normalizer_fn=None, scope='shortcut')
       x = tcl.conv2d(x, num_outputs/2, kernel_size=1, stride=1, padding='SAME')
       x = tcl.conv2d(x, num_outputs/2, kernel_size=kernel_size, stride=stride, padding='SAME')
       x = tcl.conv2d(x, num_outputs, kernel_size=1, stride=1, activation_fn=None, padding='SAME', normalizer_fn=None)

       x += shortcut       
       x = normalizer_fn(x)
       x = activation_fn(x)
   return x

三、人脸融合过程

将模板图(图5)的脸融合到源图(图6)的脸上。

基于三维重建的人脸融合_第7张图片
图 5
基于三维重建的人脸融合_第8张图片
图 6

过程:将模板图的转成原图的姿势,然后粘贴在原图的脸上。

step1:获得原图的姿势,将原图经过转换网络得到position map,通过position map得到顶点的信息。

pos = prn.process(image) 
vertices = prn.get_vertices(pos)

step2:使用相同的方法得到模板图的position,进而得到其color。

ref_image = imread(args.ref_path)
ref_pos = prn.process(ref_image)
ref_vertices = prn.get_vertices(ref_pos)
new_texture = ref_texture#(texture + ref_texture)/2.
new_colors = prn.get_colors_from_texture(new_texture)

step3:将模板人脸姿态转换成原图人脸的姿态。使用原图的顶点信息和模板图的color,及其三角形可以渲染成2D的人脸姿态,如下图所示。

new_image = render_texture(vertices.T, new_colors.T, prn.triangles.T, h, w, c = 3)
# 注意:使用的是源图的顶点信息和模板图的颜色信息,说明顶点信息包含了姿态,而颜色信息表明任务的主要特征
基于三维重建的人脸融合_第9张图片
图 7

step4:获得原图人脸的mask图,以便拼图。

## 将姿态部分设为1,其他部分设为0
vis_colors = np.ones((vertices.shape[0], 1))
face_mask = render_texture(vertices.T, vis_colors.T, prn.triangles.T, h, w, c = 1)
基于三维重建的人脸融合_第10张图片
图 8

step5:将渲染之后的2D人脸姿态贴在原图的脸上,如下图所示。

new_image = image*(1 - face_mask[:,:,np.newaxis]) + new_image*face_mask[:,:,np.newaxis]
基于三维重建的人脸融合_第11张图片
图 9

step6:通过OpenCV将颜色调和一下。

vis_ind = np.argwhere(face_mask>0)
vis_min = np.min(vis_ind, 0)
vis_max = np.max(vis_ind, 0)
center = (int((vis_min[1] + vis_max[1])/2+0.5), int((vis_min[0] vis_max[0])/2+0.5))
output = cv2.seamlessClone((new_image*255).astype(np.uint8), (image*255).astype(np.uint8), (face_mask*255).astype(np.uint8)
基于三维重建的人脸融合_第12张图片
图 10

四、总结

涉及到的技术领域

1. 人脸三维重建

2. 从三维图像到二维人脸的渲染过程

3. 深度学习

参考文献

[1] Joint 3D Face Reconstruction and Dense Alignment with Position Map Regression Network.
[2] Regressing Robust and Discriminative 3D Morphable Models with a very Deep Neural Network.
[3] BFM
[4] Face Alignment Across Large Poses: A 3D Solution

附录

UV空间坐标

UV空间坐标是指所有的图像文件是二维的一个平面,水平方向是U(对应于x轴),垂直方向是V(对应于y轴)。有助于将图像纹理贴图在3D的曲面上。UV作为标记点,用于控制纹理贴图上的像素点与网格中的顶点对应。

本文的position map是 UV position map是一个二维的平面图像,可以表示图像在UV空间中所有点的坐标。

本文的UV position map的生成过程如下:

伪代码:
输入:二维人脸图片,顶点, 三角形,颜色
输出:对应的position map
### 三角形triangle的shape为(num, 3)
### 每行的三个元素代表顶点vertices中的索引

for i in range(三角形个数):
      获取表示三角形的相应顶点的索引,tri1(x1, y1), tri2(x2, y2), tri3(x3, y3)
      在顶点坐标,0和图像长宽中,取横坐标的最大最小值vmin, vmax,纵坐标的最大最小值umin, umax
      for x in range(vmin, vmax+1):
            for y in range(umin, umax+1):
                   获取点(x, y)和三个顶点组成的立体图形的重心坐标相对于三个顶点的权重w1, w2, w3
                   获取点的深度信息,即三个顶点深度信息的权重之和
                   判断该点的深度信息与缓存区的深度信息的大小,并用较高的深度值更新缓存区的深度值。
                   输出图像点(x, y)的RGB值为三个顶点的三颜色的加权和
### 函数
def render_colors(vertices, triangles, colors, h, w, c = 3):
    ''' render mesh with colors
    Args:
        vertices: [nver, 3]
        triangles: [ntri, 3] 
        colors: [nver, 3]
        h: height
        w: width    
    Returns:
        image: [h, w, c]. 
    '''
    assert vertices.shape[0] == colors.shape[0]
    # initial 
    image = np.zeros((h, w, c))
    depth_buffer = np.zeros([h, w]) - 999999.
    colors = np.array(colors)
    vertices = np.array(vertices)
    # triangles = np.array(triangles)

    for i in range(triangles.shape[0]):
        tri = triangles[i, :] # 3 vertex indices

        # the inner bounding box
        umin = max(int(np.ceil(np.min(vertices[tri, 0]))), 0)
        umax = min(int(np.floor(np.max(vertices[tri, 0]))), w-1)

        vmin = max(int(np.ceil(np.min(vertices[tri, 1]))), 0)
        vmax = min(int(np.floor(np.max(vertices[tri, 1]))), h-1)

        if umax depth_buffer[v, u]:
                    depth_buffer[v, u] = point_depth
                    image[v, u, :] = w0*colors[tri[0], :] + w1*colors[tri[1], :] + w2*colors[tri[2], :]
    return image

求立体图形的重心权重

## 获得重心权重的函数
def get_point_weight(point, tri_points):
    ''' Get the weights of the position
    Args:
        point: (2,). [u, v] or [x, y] 
        tri_points: (3 vertices, 2 coords). three vertices(2d points) of a triangle. 
    Returns:
        w0: weight of v0
        w1: weight of v1
        w2: weight of v3
     '''
    tp = np.array(tri_points)
    # vectors
    v0 = tp[2,:] - tp[0,:]
    v1 = tp[1,:] - tp[0,:]
    v2 = np.array(point) - tp[0,:]

    # dot products
    dot00 = np.dot(v0.T, v0) # distance of p2 and p0
    dot01 = np.dot(v0.T, v1) # 
    dot02 = np.dot(v0.T, v2)
    dot11 = np.dot(v1.T, v1)
    dot12 = np.dot(v1.T, v2)

    # barycentric coordinates
    if dot00*dot11 - dot01*dot01 == 0:
        inverDeno = 0
    else:
        inverDeno = 1/(dot00*dot11 - dot01*dot01)

    u = (dot11*dot02 - dot01*dot12)*inverDeno
    v = (dot00*dot12 - dot01*dot02)*inverDeno

    w0 = 1 - u - v
    w1 = v
    w2 = u

    return w0, w1, w2

texture渲染

def render_texture(vertices, colors, triangles, h, w, c = 3):
    ''' render mesh by z buffer
    # 初始化渲染好的二维图像
    image = np.zeros((h, w, c))
    # 初始化缓存区
    depth_buffer = np.zeros([h, w]) - 999999.
    # 定义每个三角形的深度:组成三角形的每个顶点的深度的平均值
    tri_depth = (vertices[2, triangles[0,:]] + vertices[2,triangles[1,:]] + vertices[2, triangles[2,:]])/3. 
    # 定义每个三角形的颜色:组成三角形的每个顶点的颜色的平均值
    tri_tex = (colors[:, triangles[0,:]] + colors[:,triangles[1,:]] + colors[:, triangles[2,:]])/3.

    for i in range(triangles.shape[1]):
        tri = triangles[:, i] #  顶点的三个索引值
        # the inner bounding box
        umin = max(int(np.ceil(np.min(vertices[0,tri]))), 0)
        umax = min(int(np.floor(np.max(vertices[0,tri]))), w-1)
        vmin = max(int(np.ceil(np.min(vertices[1,tri]))), 0)
        vmax = min(int(np.floor(np.max(vertices[1,tri]))), h-1)

        if umax depth_buffer[v, u] and isPointInTri([u,v], vertices[:2, tri]): 
                    depth_buffer[v, u] = tri_depth[i]
                    image[v, u, :] = tri_tex[:, i]    ### 与color渲染的不同之处
    return image

你可能感兴趣的:(基于三维重建的人脸融合)