文章目录
Hand Pose Estimation (手的姿态估计) tensorflow 实现教程(一)
谢大家关注,我有空整理一下代码,放到github上,现在代码都不知道放哪里了。
Hand Pose Estimation (手的姿态估计) 简介
本文介绍的Hand Pose Estimation是基于深度图的手势姿态估计方法和tensorflow实现。
手的姿态估计现在越来越引起了学术界和工业届的关注,伴随着深度相机的技术越来越成熟,作为一种新的人机交互方式,应用场景也越来越广泛,比如车内的手势识别(宝马高端车已经有应用),在VR系统中和虚拟环境的交互,在AR系统中的交互等。伴随着深度相机在手机(iPhone x等)上的慢慢普及,基于深度图像的应用也会越来越多。像手机上最多的应用目前还主要是用来做人脸识别,用人脸来解锁等一些应用。但是在VR和AR领域,手势识别能够给这些应用带来更加流畅的交互。所以本文给出一个简单的教程来介绍一下基于深度图的手势姿态估计。
手的姿态估计输入是一幅深度图像,而不是普通摄像头获取的RGB图。那么,为什么要用深度图像来估计手的姿态?主要原因:1. 深度图自带深度信息,更容易恢复出每个关节的三维位置。2. RGB图对光线的要求较高,深度图像对光线不敏感,适用于各种不同场景,甚至是全黑的环境。这些优势主要得益于深度相机技术的发展,有基于双目的(对光线敏感),基于红外的,基于tof的等等不同的深度相机实现方法,我们这里不讨论哪种深度相机更好用,只要输入一个可靠的深度图像,我们就基于这个深度图像来恢复手的姿态。
目前市面上比较容易获得的深度相机有:微软的Kinect,Intel 的 realsense系列,华硕的Xtion系列等。
手势姿态估计示例, 以下图片来源于学术论文。
数据库介绍
在计算机视觉领域,尤其是机器学习领域,一个方向的研究和发展离不开数据库的支持,有了数据研究人员才能基于数据来做方法,做比较,推动这个方向的进步,那么Hand Pose estimation这个方向主流的主要是以下四个数据库:ICVL, NYU, MSRA15, BigHand2.2M。
ICVL
D. Tang, H. J. Chang, A. Tejani, and T.-K. Kim. Latent regression forest: Structured estimation of 3D articulated hand posture. In CVPR, 2014
17604帧,16个关节, 10个人,第三视角,分辨率:320*240
NYU
J. Tompson, M. Stein, Y. Lecun, and K. Perlin. Real-time continuous pose recovery of human hands using convolutional networks. In TOG, 2014
81009帧, 36个关节, 2个人,第三视角, 分辨率:640*480
MSRA15
X. Sun, Y. Wei, S. Liang, X. Tang, and J. Sun. Cascaded hand pose regression. In CVPR, 2015.
76375帧,21个关节,9个人,第三视角,分辨率:320*240
BigHand2.2M
Yuan, Shanxin, et al. “Bighand2. 2m benchmark: Hand pose dataset and state of the art analysis.” Computer Vision and Pattern Recognition (CVPR), 2017 IEEE Conference on. IEEE, 2017.
2200000帧,21个关节,10个人,全视角,分辨率:640*480
估计方法简介
本文训练和测试的输入数据以MSRA15数据库为例, 也就是输入的图分辨率为320*240,标记数据为21个关节,有9个人的数据,共76375帧。
NYU给的标记数据是21个关节每个关节位置的三维位置(x, y, z),对应一个手的深度图像,所谓的Hand Pose Estimation就是根据一个深度图像回归出21*3个数字出来,每三个值代表一个关节的三维位置。
通过以上分析,我们可以得知,这其实是机器学习领域的很常见的回归问题,根据输入回归出一个63维的输出,每一维是一个float数,每三个值代表一个三维位置。
因此,本文参考了一些学术论文,构建一个CNN深度神经网络来完成这个回归任务。另外参考了Deep prior++这篇文章,我们认为手的姿势并不需要63维来表示,可能30维就够用了,所以我们可以先用PCA降维来把训练数据的label从63维降维到30维,然后训练出CNN深度学习模型。在inference阶段,先回归出30维的数据,然后利用PCA的参数将30维升维到63维。其中CNN网络我们使用经典的ResNet网络。
参考文章为:
Oberweger, Markus, and Vincent Lepetit. “Deepprior++: Improving fast and accurate 3d hand pose estimation.” ICCV workshop. Vol. 840. 2017.
如何提取手
生成训练的输入数据(PCA)
from sklearn.decomposition import PCA
pca = PCA(n_components=30)
pca.fit(train_gt3D.reshape((train_gt3D.shape[0], 63)))
# 升维时候的参数
# x*ww + bb
ww = pca.components_
bb = pca.mean_
# 执行降维,降到30维
train_gt3D_embed = pca.transform(train_gt3D.reshape((train_gt3D.shape[0], 63)))
网络结构(tensorflow实现)
# ResNet.py
from collections import namedtuple
import numpy as np
import tensorflow as tf
import six
from tensorflow.python.training import moving_averages
HParams = namedtuple('HParams',
'batch_size, num_classes, min_lrn_rate, lrn_rate, '
'num_residual_units, use_bottleneck, weight_decay_rate, '
'relu_leakiness, optimizer')
class ResNet(object):
"""ResNet model."""
def __init__(self, hps, mode):
"""
ResNet constructor.
Args:
hps:Hyperparameters.
images: Batches of images [batch_size, image_size, image_size, 3]
labels: Batches of labels [batch_size, num_classes]
mode: One of 'train' and 'eval'
"""
self.hps = hps
self.mode = mode
self._extra_train_ops = []
def set_train_params(self, images, labels, ww, bb):
self._images = images
self.labels = labels
self.ww = ww
self.bb = bb
def set_test_params(self, images):
self._images = images
# 构建模型图
# 新建全局step
# 构建ResNet网络模型
# 构建优化训练操作
def build_graph(self):
# self.global_step = tf.contrib.framework.get_or_create_global_step()
self.global_step = tf.train.get_or_create_global_step()
self._build_model()
if self.mode == 'train':
self._build_train_op()
# 合并所有总结
self.summaries = tf.summary.merge_all()
# 构建模型
def _build_model(self):
with tf.variable_scope('init'):
x = self._images
# """第一层卷积(3, 3*3/1, 16) """
x = self._conv('init_conv', x, 5, 1, 32, self._stride_arr(1))
# 残差网络参数
strides = [2, 2, 2, 1]
# 激活前置
activate_before_residual = [False, False, False, False]
if self.hps.use_bottleneck:
# bottleneck 残差单元模块
res_func = self._bottleneck_residual
# 通道数量
filters = [32, 64, 128, 256, 256]
else:
# 标准残差单元模块
res_func = self._residual
# 通道数量
filters = [32, 64, 128, 256, 256]
# 第一组
with tf.variable_scope('unit_1_0'):
x = res_func(x, filters[0], filters[1],
self._stride_arr(strides[0]),
activate_before_residual[0])
for i in six.moves.range(1, self.hps.num_residual_units):
with tf.variable_scope('unit_1_%d' % i):
x = res_func(x, filters[1], filters[1], self._stride_arr(1), False)
# 第二组
with tf.variable_scope('unit_2_0'):
x = res_func(x, filters[1], filters[2],
self._stride_arr(strides[1]),
activate_before_residual[1])
for i in six.moves.range(1, self.hps.num_residual_units):
with tf.variable_scope('unit_2_%d' % i):
x = res_func(x, filters[2], filters[2], self._stride_arr(1), False)
# 第三组
with tf.variable_scope('unit_3_0'):
x = res_func(x, filters[2], filters[3],
self._stride_arr(strides[2]),
activate_before_residual[2])
for i in six.moves.range(1, self.hps.num_residual_units):
with tf.variable_scope('unit_3_%d' % i):
x = res_func(x, filters[3], filters[3], self._stride_arr(1), False)
# 第四组
with tf.variable_scope('unit_4_0'):
x = res_func(x, filters[3], filters[4],
self._stride_arr(strides[3]),
activate_before_residual[3])
for i in six.moves.range(1, self.hps.num_residual_units):
with tf.variable_scope('unit_4_%d' % i):
x = res_func(x, filters[4], filters[4], self._stride_arr(1), False)
# 全局池化层
with tf.variable_scope('unit_last'):
x = self._batch_norm('final_bn', x)
x = self._relu(x, self.hps.relu_leakiness)
# x = self._global_avg_pool(x)
# 全连接层0
with tf.variable_scope('fc0'):
x = self._fully_connected(x, 1024)
x = self._relu(x, self.hps.relu_leakiness)
# 全连接层1
with tf.variable_scope('fc1'):
x = self._fully_connected(x, 1024)
x = self._relu(x, self.hps.relu_leakiness)
# with tf.variable_scope('fc2'):
# x = self._fully_connected(x, 30)
# 全连接层 + Softmax
with tf.variable_scope('logit1'):
logits = self._fully_connected(x, 30)
self.logits = logits
self.predictions = tf.nn.softmax(logits)
# 利用pca的参数进行升维,其实就是一个全连接层
with tf.variable_scope('logit2'):
logits2 = self._fully_connected_2(logits, 63)
self.logits2 = logits2
# 构建训练操作
def _build_train_op(self):
# 构建损失函数
with tf.variable_scope('costs'):
logits_x = tf.reshape(self.logits, (self.hps.batch_size, 30))
labels_x = tf.reshape(self.labels, (self.hps.batch_size