美文网首页Science相关 杂stereo
基于三维重建的人脸融合

基于三维重建的人脸融合

作者: LuDon | 来源:发表于2018-12-20 16:55 被阅读252次

    一、引言

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

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

    二、人脸三维重建

    2.1 三维图像

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


    blond-2d图

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

    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值,就可以很好的理解了。(即用第二排来表示第一排的三维信息)

    图 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图。在三维重建时,只重建了人脸部分,因此渲染回二维的图片时只有人脸部分。


    2d平面图

    2.2 三维人脸重建网络

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

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

    图 2

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

    图 3

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

    图 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)的脸上。

    图 5 图 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)
    # 注意:使用的是源图的顶点信息和模板图的颜色信息,说明顶点信息包含了姿态,而颜色信息表明任务的主要特征
    
    图 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)
    
    图 8

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

    new_image = image*(1 - face_mask[:,:,np.newaxis]) + new_image*face_mask[:,:,np.newaxis]
    
    图 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)
    
    图 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<umin or vmax<vmin:
                continue
    
            for u in range(umin, umax+1):
                for v in range(vmin, vmax+1):
                    if not isPointInTri([u,v], vertices[tri, :2]): 
                        continue
                    w0, w1, w2 = get_point_weight([u, v], vertices[tri, :2]) # 获得重心权重
                    point_depth = w0*vertices[tri[0], 2] + w1*vertices[tri[1], 2] + w2*vertices[tri[2], 2]
    
                    if point_depth > 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<umin or vmax<vmin:
                continue
            for u in range(umin, umax+1):
                for v in range(vmin, vmax+1):
                    if tri_depth[i] > 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
    
    

    相关文章

      网友评论

        本文标题:基于三维重建的人脸融合

        本文链接:https://www.haomeiwen.com/subject/yckyqqtx.html