基于深度图像的头部姿态估计

1.头部姿态

      头部姿态估计通常是指利用计算机视觉和模式识别的方法在数字图像中判断人头部朝向问题[2]。更严格的说,头部姿态估计是在一个空间坐标系内识别头部的姿态参数,即头部位置参数()和方向度参数。描述头部方向度参数的有三个:水平转动的偏航角(Yaw)、垂直转动仰俯角(Pitch)以及左右转动的旋转角(Roll)。如下图所示,一般而言,一个正常的成年人的头部四周运动的范围为:左右偏角:-40.9度~63.3度,垂直偏角-60.4度~69.6度,水平偏角-79.8度~75.3度  


 图 三维空间坐标系中头部姿态描述

       因此,在很大程度上,头部姿态反映了一个人的眼睛视线方向或注意力方向。当人眼被遮挡时,通过头部姿态估计可以大致辨别出被观察者的注视方向;当人眼未被遮挡时,头部姿态将成为精确预测被观察者注视方向的必要条件。        

2.头部姿态估计方法分类

      从20世纪90年代至今,短短二十多年时间,头部姿态估计一跃成为国内外一大研究热点。围绕这一研究方向,广大学者提出几十种头部姿态估计方法。下面将头部姿态方法按照数据源的不同对方法进行分类。

2.1数据源不同

      从所依赖的数据源的不同,大致地可以将这些方法分为基于二维彩色图像的方法,基于深度图像的方法以及基于三维图像的方法。

2.1.1基于二维彩色图像的头部姿态估计方法

      该方法可以说是最为传统的一种方法,也是使用作为普遍的方法。是传统的主流方法,该类方法的优点是不依赖于任何特殊的硬件设施,只需一个简单的摄像头即可正常工作,成本低易于推广,相关研究成果最多。当然,采用该方法的缺点就是容易受光照变化,表情变化以及遮挡的影响。

2.1.2基于深度图像的头部姿态估计方法

       采用这种方法的能在很大程度上克服对光照变化以及表情的变化,尤其得益于微软发明的Kinect设备,该款设备装备了一个RGB彩色摄像头,红外摄像头以及深度探测器。无论在任何的照明条线下,都可以利用Kinect的CMOS红外探测器来感知空间。它可以收集到视野里的每一个点的信息,并将其生成一张深度图像用以代表环境的信息。Kinect的COMS红外探测器可以实现实时的探测环境中物体的深度。而Kinect中间的CCD摄像头可以用来获取人体或者是物体的二维RGB彩色图像。随着Kinect设备价格不断下降,采用基于深度图像的头部姿态估计方法也是越来越多。

2.1.3基于三维图像的头部姿态估计方法

获得三维图像一般有三种方式:1)通过三维图像传感器直接获得;2)通过结合二维彩色图像的形状,纹理等特征实现三维重建;3)通过结合二维彩色图像和深度图像获取三维点云图像。采用该方法的优点是可以很好的保留彩色纹理信息以及深度信息,对于即使待检测人具有较高程度的化妆都能实现头部姿态估计,这是其他两种方法所实现不了的。但是,这类方法与前面三种方法相比,相关的研究相对来说是最少的,主要是考虑三个方面的原因:1)深度传感器价格昂贵;2)应用层面比较窄;3)三维重建技术目前尚未成熟。

      当然还有根据实现原理和方法的不同对头部姿态方法进行分类,比如基于形状模板的方法,基于非线性回归的方法以及基于特征点几何关系的方法等等。这里就不一一概述了。

3.基于深度图像的头部姿态估计

     这里介绍一下,我从github上下载的代码,进过配置环境,调试修改后能完全运行。以下是代码的主程序:

#include <string>
#include <algorithm>
#include <iostream>
#include <vector>
#include "CRForestEstimator.h"
#include <stdint.h>

#define PATH_SEP "/"

using namespace std;
using namespace cv;

// Path to trees
string g_treepath;
// Number of trees
int g_ntrees;
// Patch width
int g_p_width;
// Patch height
int g_p_height;
//maximum distance form the sensor - used to segment the person
int g_max_z = 0;
//head threshold - to classify a cluster of votes as a head
int g_th = 400;
//threshold for the probability of a patch to belong to a head
float g_prob_th = 1.0f;
//threshold on the variance of the leaves
float g_maxv = 800.f;
//stride (how densely to sample test patches - increase for higher speed)
int g_stride = 5;
//radius used for clustering votes into possible heads
float g_larger_radius_ratio = 1.f;
//radius used for mean shift
float g_smaller_radius_ratio = 6.f;

//pointer to the actual estimator
CRForestEstimator* g_Estimate;
//input 3D image
Mat g_im3D;

std::vector< cv::Vec<float,POSE_SIZE> > g_means; //outputs
std::vector< std::vector< Vote > > g_clusters; //full clusters of votes
std::vector< Vote > g_votes; //all votes returned by the forest

bool loadDepthImageCompressed(Mat& depthImg, const char* fname ){

	//now read the depth image
	FILE* pFile = fopen(fname, "rb");
	if(!pFile){
		cerr << "could not open file " << fname << endl;
		return false;
	}

	int im_width = 0;
	int im_height = 0;
	bool success = true;

	success &= ( fread(&im_width,sizeof(int),1,pFile) == 1 ); // read width of depthmap
	success &= ( fread(&im_height,sizeof(int),1,pFile) == 1 ); // read height of depthmap

	depthImg.create( im_height, im_width, CV_16SC1 );
	depthImg.setTo(0);


	int numempty;
	int numfull;
	int p = 0;

	if(!depthImg.isContinuous())
	{
		cerr << "Image has the wrong size! (should be 640x480)" << endl;
		return false;
	}

	int16_t *data = depthImg.ptr<int16_t>(0);
	while(p < im_width*im_height ){

		success &= ( fread( &numempty,sizeof(int),1,pFile) == 1 );

		for(int i = 0; i < numempty; i++)
			data[ p + i ] = 0;

		success &= ( fread( &numfull,sizeof(int), 1, pFile) == 1 );
		success &= ( fread( &data[ p + numempty ], sizeof(int16_t), numfull, pFile) == (unsigned int) numfull );
		p += numempty+numfull;

	}

	fclose(pFile);

	return success;
}

void loadConfig(const char* filename) {

	ifstream in(filename);
	string dummy;

	if(in.is_open()) {

		// Path to trees
		in >> dummy;
		in >> g_treepath;

		// Number of trees
		in >> dummy;
		in >> g_ntrees;

		in >> dummy;
		in >> g_maxv;

		in >> dummy;
		in >> g_larger_radius_ratio;

		in >> dummy;
		in >> g_smaller_radius_ratio;

		in >> dummy;
		in >> g_stride;

		in >> dummy;
		in >> g_max_z;

		in >> dummy;
		in >> g_th;


	} else {
		cerr << "File not found " << filename << endl;
		exit(-1);
	}
	in.close();

	cout << endl << "------------------------------------" << endl << endl;
	cout << "Estimation:       " << endl;
	cout << "Trees:            " << g_ntrees << " " << g_treepath << endl;
	cout << "Stride:           " << g_stride << endl;
	cout << "Max Variance:     " << g_maxv << endl;
	cout << "Max Distance:     " << g_max_z << endl;
	cout << "Head Threshold:   " << g_th << endl;

	cout << endl << "------------------------------------" << endl << endl;

}

int main(int argc, char* argv[])
{//data\\frame_00100_depth.bin data//depth.cal

	if( argc != 3 ){

		cout << "usage: ./head_pose_estimation config_file depth_image" << endl;
		exit(-1);
	}

	loadConfig(argv[1]);
	CRForestEstimator estimator;
	if( !estimator.loadForest(g_treepath.c_str(), g_ntrees) ){

		cerr << "could not read forest!" << endl;
		exit(-1);
	}

	string depth_fname(argv[2]);

	//read calibration file (should be in the same directory as the depth image!)
	string cal_filename = depth_fname.substr(0,depth_fname.find_last_of('/'));
	cal_filename += "/depth.cal";
	ifstream is(cal_filename.c_str());
	if (!is){

		cerr << "depth.cal file not found in the same folder as the depth image! " << endl;
		return -1;

	}
	//read intrinsics only
	float depth_intrinsic[9];	
	for(int i =0; i<9; ++i)
		is >> depth_intrinsic[i];
	is.close();

	Mat depthImg;
	//read depth image (compressed!)
	if (!loadDepthImageCompressed( depthImg, depth_fname.c_str() ))
		return -1;


	Mat img3D;
	img3D.create( depthImg.rows, depthImg.cols, CV_32FC3 );

	//get 3D from depth
	for(int y = 0; y < img3D.rows; y++)
	{
		Vec3f* img3Di = img3D.ptr<Vec3f>(y);
		const int16_t* depthImgi = depthImg.ptr<int16_t>(y);

		for(int x = 0; x < img3D.cols; x++){

			float d = (float)depthImgi[x];

			if ( d < g_max_z && d > 0 ){

				img3Di[x][0] = d * (float(x) - depth_intrinsic[2])/depth_intrinsic[0];
				img3Di[x][1] = d * (float(y) - depth_intrinsic[5])/depth_intrinsic[4];
				img3Di[x][2] = d;

			}
			else{

				img3Di[x] = 0;
			}

		}
	}

	g_means.clear();
	g_votes.clear();
	g_clusters.clear();

	string pose_filename(depth_fname.substr(0,depth_fname.find_last_of('_')));
	pose_filename += "_pose.bin";

	cv::Vec<float,POSE_SIZE> gt;
	bool have_gt = false;
	//try to read in the ground truth from a binary file
	FILE* pFile = fopen(pose_filename.c_str(), "rb");
	if(pFile){

		have_gt = true;
		have_gt &= ( fread( >[0], sizeof(float),POSE_SIZE, pFile) == POSE_SIZE );
		fclose(pFile);

	}

	//do the actual estimate
	estimator.estimate( 	img3D,
							g_means,
							g_clusters,
							g_votes,
							g_stride,
							g_maxv,
							g_prob_th,
							g_larger_radius_ratio,
							g_smaller_radius_ratio,
							false,
							g_th
						);

	cout << "Heads found : " << g_means.size() << endl;
	imshow("img3D", img3D);

	//assuming there's only one head in the image!
	if(g_means.size()>0){

		cout << "Estimated: " << g_means[0][0] << " " << g_means[0][1] << " " << g_means[0][2] << " " << g_means[0][3] << " " << g_means[0][4] << " " << g_means[0][5] <<endl;

		float pt2d_est[2];
		float pt2d_gt[2];

		if(have_gt){
			cout << "Ground T.: " << gt[0] << " " << gt[1] << " " << gt[2] << " " << gt[3] << " " << gt[4] << " " << gt[5] <<endl;

			cv::Vec<float,POSE_SIZE> err = (gt-g_means[0]);
			//multiply(err,err,err);
			for(int n=0;n<POSE_SIZE;++n)
				err[n] = err[n]*err[n];

			float h_err = sqrt(err[0]+err[1]+err[2]);
			float a_err = sqrt(err[3]+err[4]+err[5]);

			cout << "Head error : " << h_err << " mm " << endl;
			cout << "Angle error : " << a_err <<" degrees " <<  endl;

			pt2d_gt[0] = depth_intrinsic[0]*gt[0]/gt[2] + depth_intrinsic[2];
			pt2d_gt[1] = depth_intrinsic[4]*gt[1]/gt[2] + depth_intrinsic[5];

		}

		pt2d_est[0] = depth_intrinsic[0]*g_means[0][0]/g_means[0][2] + depth_intrinsic[2];
		pt2d_est[1] = depth_intrinsic[4]*g_means[0][1]/g_means[0][2] + depth_intrinsic[5];

	}
	waitKey(0);
	return 0;

}

实验结果如下图所示


这里说明一下,该代码并非我原创的,可以从github上下载的该代码,请点击这里,不过下载的代码并非一个完整可运行的工程,需要配置一些环境变量Freeglut 2.8.1,修改部分代码才能运行,若是嫌麻烦的话,可以只直接下载我调试好的完整工程。请戳链接http://download.csdn.net/my/uploads下载。



评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值