作者 | 李秋键
出品 | AI科技大本营(ID:rgznai100)
近年来,三维人脸重建成为计算机视觉、图像识别等研究领域中的热点问题。三维人脸重建技术分为基于不同视角的多幅图像的重建和基于单幅图像的三维人脸重建。
本项目通过使用PRNet算法,通过训练CNN网络实现对二维图像转变为三维空间图像的转换,其中二维使用UV坐标作为2D的表达,可以实现在UV空间保存完整的人脸3D形状。实现的效果如下图所示:
基本介绍
针对单幅图像的三维人脸重建,传统方法有基于模型的方法、基于明暗形状恢复的方法等。
随着深度学习被引入到三维人脸重建领域,并取得比传统方法更优异的效果,逐渐成为主流的重建方法。JacksonAS等人提出使用VRN(volumetric regression networks)从单个二维图像直接进行三维面部重建的方法。Feng Y等人设计了一个名为UV位置图的二维表示方法,记录UV空间中完整面部的三维形状,然后训练一个简单的卷积神经网络,从单个二维图像中回归。
ChangFJ等人提出了直接应用于人脸图像强度,回归3D表情系数的29D向量的ExpNet CNN模型。Tu等人针对3D标注训练数据短缺问题,提出一种2D辅助自监督学习方法,利用带嘈杂地标信息的无约束二维人脸图像改善三维人脸模型的学习,在密集人脸对齐和三维人脸重建方面取得了突出的效果。刘成攀等人提出一种基于自监督深度学习的人脸表征及三维重建方法,将二维人脸的特征点信息映射到三维空间实现三维人脸重建。
环境要求
本次环境使用的是Python3.6.5 Windows平台。主要用的库有:
skimage模块:skimage包的全称是scikit-imageSciKit,它对scipy.ndimage进行扩展,提供更多的图片处理功能。
opencv-python模块:opencv-python是Python的绑定库,解决计算机视觉问题。其使用Numpy,这是一个高度优化的数据库操作库,具有MATLAB风格的语法。所有Opencv数组结构都转换为Numpy数组。这也使得与使用Numpy的其他库(如Scipy和Matplotlib)集成更容易。
Numpy模块:Numpy是应用Python进行科学计算时的基础模块。它是一个提供多维数组对象的Python库,包含多种衍生的对象以及一系列的为快速计算数组而生的例程,如数学运算、逻辑运算、形状操作、排序、选择、I/O、离散傅里叶变换、基本线性代数、基本统计运算、随机模拟等。
Matplotlib模块:Matplotlib是Python的2D绘图库,以各种硬拷贝格式和跨平台的交互式环境生成出版质量级别的图形。通过Matplotlib,开发者仅用几行代码便可生成绘图。Matplot使用Numpy进行数组运算,调用其他Python库来实现硬件交互。
模型介绍
模型使用的是端到端的PRN多任务方法,能完成稠密人脸对齐和3D人脸形状重建。主要体现在以下几个方面:
1、以端到端方式,实现了高分辨率的3D人脸重建和稠密对齐;
2、设计了UV位置图,来记录人脸的3D位置信息;
3、设计了权重掩模用于loss计算,loss中每个点权重不同,可明显提高网络性能
4、CNN采用轻量级模式,单张人脸任务可达到100FPS
5、在AFLW200-3D和Florence数据集上可达到25%的性能提升
PRNet通过输入一张图片,直接使用神经网络输出一张称为UV position map的 UV 位置映射图。其中UV映射图维度是一个三位矩阵,前面两个维度上输出的纹理图的维度,最后一个维度表示纹理图每个像素在 3D 空间中的位置信息。
其中网络模型代码如下:
代码语言:javascript复制class resfcn256(object):
def __init__(self, resolution_inp = 256, resolution_op = 256, channel = 3, name = 'resfcn256'):
self.name = name
self.channel = channel
self.resolution_inp = resolution_inp
self.resolution_op = resolution_op
def __call__(self, x, is_training = True):
with tf.variable_scope(self.name) as scope:
with arg_scope([tcl.batch_norm], is_training=is_training, scale=True):
with arg_scope([tcl.conv2d,tcl.conv2d_transpose], activation_fn=tf.nn.relu,
normalizer_fn=tcl.batch_norm,
biases_initializer=None,
padding='SAME',
weights_regularizer=tcl.l2_regularizer(0.0002)):
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
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
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
人脸生成
检测人脸关键点:
通过调用人脸模型监测人脸并将人脸裁剪分出:
代码语言:javascript复制cas =cv2.CascadeClassifier('./Data/cv-data/haarcascade_frontalface_alt2.xml')
img = plt.imread('./images/zly.jpg')
img_gray= cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
faces = cas.detectMultiScale(img_gray,2,3,0,(30,30))
bbox = np.array([faces[0,0],faces[0,1],faces[0,0] faces[0,2],faces[0,1] faces[0,3]])
plt.imshow(cv2.rectangle(img.copy(),(bbox[0],bbox[1]),(bbox[2],bbox[3]),(0,255,0),2))
plt.axis('off')
plt.show()
left = bbox[0]; top = bbox[1]; right = bbox[2]; bottom = bbox[3]
old_size = (right - left bottom - top)/2
center = np.array([right- (right - left) / 2.0, bottom - (bottom -top) / 2.0])
size = int(old_size*1.6)
src_pts = np.array([[center[0]-size/2, center[1]-size/2],
[center[0] - size/2, center[1] size/2],
[center[0] size/2, center[1]-size/2]])
DST_PTS = np.array([[0,0], [0,255], [255, 0]]) #图像大小256*256
tform =estimate_transform('similarity', src_pts, DST_PTS)
img = img/255.
cropped_img =warp(img, tform.inverse, output_shape=(256, 256))
获取人脸点云:
三维点云是一个在三维空间关联系统中包含点的数据库,即事物或空间的精确数字记录,主要关于表面点的集合。与相片不同,三维图像是对一类信息的统称,信息还需要有具体的表现形式。
其表现形式包括:深度图(以灰度表达物体与相机的距离),几何模型(由CAD软件建立),点云模型(所有逆向工程设备都将物体采样成点云)。可见,点云数据是最为常见也是最基础的三维模型。点云模型往往由测量直接得到每个点对应一个测量点,未经过其他处理手段,故包含了最大的信息量。
对应代码如下:
代码语言:javascript复制face_ind =np.loadtxt('./Data/uv-data/face_ind.txt').astype(np.int32)
all_vertices = np.reshape(pos, [256*256, -1])
vertices = all_vertices[face_ind, :]
plt.figure(figsize=(8,8))
plt.imshow(draw_kps(img.copy(),vertices[:,:2],1))
plt.axis('off')
plt.show()
texture = cv2.remap(img, pos[:,:,:2].astype(np.float32), None, interpolation=cv2.INTER_NEAREST, borderMode=cv2.BORDER_CONSTANT,borderValue=(0))
生成三维人脸:
首先是获取三角形每个顶点的depth,平均值作为三角形高度。接着获取三角形每个顶点的color,平均值作为三角形颜色。对应代码如下:
代码语言:javascript复制tri_depth = (vertices[triangles[:,0],2 ] vertices[triangles[:,1],2] vertices[triangles[:,2],2])/3.
# tri_tex =(colors[triangles[:,0] ,:] colors[triangles[:,1],:] colors[triangles[:,2],:])/3.
tri_tex = tri_tex*255
img_3D =np.zeros_like(img,dtype=np.uint8)
for i in range(triangles.shape[0]):
cnt =np.array([(vertices[triangles[i,0],0],vertices[triangles[i,0],1]),
(vertices[triangles[i,1],0],vertices[triangles[i,1],1]),
(vertices[triangles[i,2],0],vertices[triangles[i,2],1])],dtype=np.int32)
img_3D =cv2.drawContours(img_3D,[cnt],0,tri_tex[i],-1)
代码链接:
https://pan.baidu.com/s/1s3NjgxuuCnB238GpzN-6Pw
代码语言:javascript复制提取码:ucqj
作者简介:李秋键,CSDN博客专家,CSDN达人课作者。硕士在读于中国矿业大学,开发有taptap竞赛获奖等。