基于Kinect的三维重建.docx
《基于Kinect的三维重建.docx》由会员分享,可在线阅读,更多相关《基于Kinect的三维重建.docx(21页珍藏版)》请在冰豆网上搜索。
基于Kinect的三维重建
基于Kinect-OpenNI-OpenCV-OpenGL的环境三维重构
Wikipedia,自由的百科全书
项目源码详见:
前几天刚入手了期待已久的Kinect,用于实验室机器人项目的视觉导航与环境理解。
首先要做的是破解-->连接PC-->获取深度数据和图像数据-->三维点云显示这么几项基本工作。
开始仿照的是饮水思源[1]博客的方法(使用VS2008在windows平台上试用Kinect[2]),利用CL-NUI-Platform来破解,它的最新版是1.0.0.1210,但我在XP上用会当机,后来换1.0.0.1121版的就可以用了。
CLNUI提供了十分简便易用的接口,在OpenCV上调用很简单,另外它还提供了Kinect底座马达的控制接口和LED灯颜色的选择接口,其例程中可以操控Kinect上下摆动。
如果只需要获取深度数据和图像数据,CLNUI就够用了。
不过要做深入的应用,比如人体姿态识别、骨架提取、深度数据与图像数据的合并等等,就该用到OpenNI了。
国的CNKINECT[3]是个不错的Kinect开发论坛,版块丰富,有很多资料可供借鉴。
我通过论坛介绍的方法[4]成功配置了OpenNI+Kinect,先是用最新版的OpenNI+SensorKinect+NITE,但在XP下不能正常运行,可能跟.net平台有关,老实按上面论坛的方法装就成功了。
另外用CMake+VS2008装了最新的OpenCV_SVN,开始试过在CMake里选择WithTBB,但诡异的是TBB似乎只适用于VS2005,在VS2008编译后试用里面的samples老是提示报错找不到msvcp80.dll,重新用CMake配置取消了WithTBB,就一切正常了。
[编辑]
一、深度摄像机的视角调整与深度/彩色图像的合并
通过研究OpenCV_SVN与OpenNI相关的代码(cap_openni.cpp)发现,OpenCV目前只支持对Kinect的深度图、视差图和彩色/灰度图及相关属性的读取,更进一步的设置还没有实现。
参考Heresy’space[5]的博客文章《透过OpneNI合并Kinect深度以及彩色影像资料》[6],想把深度图和彩色图合并显示,但是由于Kinect的深度摄像机和彩色摄像机是在不同的位置,而且镜头本身的参数也不完全相同,所以两个摄像机所取得的画面会有些微的差异(如图1左下角子图OpenGL三维点云显示窗口所示,天花板的两个日光灯对应深度图和彩色图的区域并没有重合,而是错位了)。
图1
根据Heresy的分析,需要对深度摄像机的视角进行修正,在OpenNI下只需要一行代码就可以实现:
//6.correctviewport
mDepthGenerator.GetAlternativeViewPointCap().SetViewPoint(mImageGenerator);
不过在OpenCV中并没有提供这一项设置,其源代码modules\highgui\src\cap_openni.cpp中setDepthGeneratorProperty并没有任何实质的属性设置。
为此,需要改写该函数,并且要在相应的头文件modules\highgui\include\opencv2\highgui\highgui_c.h中添加新的参数id,具体如下:
1.将cap_openni.cpp第344行的setDepthGeneratorProperty改写如下:
boolCvCapture_OpenNI:
:
setDepthGeneratorProperty(intpropIdx,doublepropValue)
{
boolres=false;
CV_Assert(depthGenerator.IsValid());
switch(propIdx)
{
caseCV_CAP_PROP_OPENNI_VIEW_POINT:
depthGenerator.GetAlternativeViewPointCap().SetViewPoint(imageGenerator);
res=true;
break;
default:
CV_Error(CV_StsBadArg,"Depthgeneratordoesnotsupportsuchparameterforsetting.\n");
res=false;
}
returnres;
}
1.在highgui_c.h的第348行下添加改变视角的参数ID号:
CV_CAP_PROP_OPENNI_VIEW_POINT=24,
然后在第352行下添加:
CV_CAP_OPENNI_DEPTH_GENERATOR_VIEW_POINT=CV_CAP_OPENNI_DEPTH_GENERATOR+CV_CAP_PROP_OPENNI_VIEW_POINT
从而使得OpenCV的VideoCapture属性关于OpenNI的如下所示:
//OpenNImapgenerators
CV_CAP_OPENNI_DEPTH_GENERATOR=0,
CV_CAP_OPENNI_IMAGE_GENERATOR=1<<31,
CV_CAP_OPENNI_GENERATORS_MASK=1<<31,
//PropertiesofcamerasavaliblethroughOpenNIinterfaces
CV_CAP_PROP_OPENNI_OUTPUT_MODE=20,
CV_CAP_PROP_OPENNI_FRAME_MAX_DEPTH=21,//inmm
CV_CAP_PROP_OPENNI_BASELINE=22,//inmm
CV_CAP_PROP_OPENNI_FOCAL_LENGTH=23,//inpixels
'''CV_CAP_PROP_OPENNI_VIEW_POINT=24,'''
CV_CAP_OPENNI_IMAGE_GENERATOR_OUTPUT_MODE=CV_CAP_OPENNI_IMAGE_GENERATOR+CV_CAP_PROP_OPENNI_OUTPUT_MODE,
CV_CAP_OPENNI_DEPTH_GENERATOR_BASELINE=CV_CAP_OPENNI_DEPTH_GENERATOR+CV_CAP_PROP_OPENNI_BASELINE,
CV_CAP_OPENNI_DEPTH_GENERATOR_FOCAL_LENGTH=CV_CAP_OPENNI_DEPTH_GENERATOR+CV_CAP_PROP_OPENNI_FOCAL_LENGTH,
'''CV_CAP_OPENNI_DEPTH_GENERATOR_VIEW_POINT=CV_CAP_OPENNI_DEPTH_GENERATOR+CV_CAP_PROP_OPENNI_VIEW_POINT'''
改写完上述源代码后保存,并且用CMake和VS2008重新编译一次OpenCV,我们就可以用OpenCV的接口来控制Kinect深度摄像头的视角,使其深度数据和图像数据能够很好的重合起来,如图2所示,注意与图1相比,右上角的伪彩色视差图四周出现了黑边,这就是视角调整后的效果:
图2
[编辑]
二、三维点云坐标的计算
在OpenCV中实际上已经提供了三维点云坐标计算的接口,通过Kinect的深度数据可以很快换算出三维点云坐标,cap_openni.cpp中这部分代码具体如下:
IplImage*CvCapture_OpenNI:
:
retrievePointCloudMap()
{
intcols=depthMetaData.XRes(),rows=depthMetaData.YRes();
if(cols<=0||rows<=0)
return0;
cv:
:
Matdepth;
getDepthMapFromMetaData(depthMetaData,depth,noSampleValue,shadowValue);
constfloatbadPoint=0;
cv:
:
MatpointCloud_XYZ(rows,cols,CV_32FC3,cv:
:
Scalar:
:
all(badPoint));
for(inty=0;y{
for(intx=0;x{
unsignedshortd=depth.at(y,x);
//Checkforinvalidmeasurements
if(d==CvCapture_OpenNI:
:
INVALID_PIXEL_VAL)//notvalid
continue;
XnPoint3Dproj,real;
proj.X=x;
proj.Y=y;
proj.Z=d;
depthGenerator.ConvertProjectiveToRealWorld(1,&proj,&real);
pointCloud_XYZ.at(y,x)=cv:
:
Point3f(real.X*0.001f,real.Y*0.001f,real.Z*0.001f);//frommmtometers
}
}
outputMaps[CV_CAP_OPENNI_POINT_CLOUD_MAP].mat=pointCloud_XYZ;
returnoutputMaps[CV_CAP_OPENNI_POINT_CLOUD_MAP].getIplImagePtr();
}
不过可以看到上面核心的代码就一行:
depthGenerator.ConvertProjectiveToRealWorld(1,&proj,&real);
具体是怎样实现的呢?
我们可以进一步找OpenNI的源代码来分析。
不过这里我是从原来双目视觉的经验上自己编写代码来实现三维点云坐标的计算。
实际上Kinect的深度摄像头成像也类似于普通的双目立体视觉,只要获取了两个摄像头之间的基线(baseline)和焦距(focallength)、以及视差数据,通过构造矩阵Q,利用OpenCV的reprojectimageTo3D函数,也可以计算出三维坐标。
下面我们通过以下代码看看矩阵Q的构造过程:
capture.set(CV_CAP_OPENNI_DEPTH_GENERATOR_VIEW_POINT,1.0);//调整深度摄像头视角
doubleb=capture.get(CV_CAP_OPENNI_DEPTH_GENERATOR_BASELINE);//mm
doubleF=capture.get(CV_CAP_OPENNI_DEPTH_GENERATOR_FOCAL_LENGTH);//pixels
doubleq[]=
{
1,0,0,-320.0,
0,1,0,-240.0,
0,0,0,F,
0,0,1./b,0
};
MatmatQ(4,4,CV_64F,q);
而三维点云坐标的计算,以及深度、视差、彩色图像的读取,则如下所示:
//抓取数据
if(!
capture.grab())
{
cout<<"Cannotgrabimages."<return-1;
}
//读取深度数据
capture.retrieve(depthMap,CV_CAP_OPENNI_DEPTH_MAP);
minMaxLoc(depthMap,&mindepth,&maxdepth);
//读取视差数据
capture.retrieve(disparityMap,CV_CAP_OPENNI_DISPARITY_MAP_32F);
colorizeDisparity(disparityMap,colorDisparityMap,maxDisparity);
colorDisparityMap.copyTo(validColorDisparityMap,disparityMap!
=0);
imshow("colorizeddisparitymap",validColorDisparityMap);
//读取彩色图像数据
capture.retrieve(bgrImage,CV_CAP_OPENNI_BGR_IMAGE);
imshow("Colorimage",bgrImage);
//利用视差数据计算三维点云坐标
cv:
:
reprojectImageTo3D(disparityMap,xyzMap,matQ,true);
这里值得注意的是,在计算视差时,如果视差图是采用默认的CV_8UC1格式(参数ID是CV_CAP_OPENNI_DISPARITY_MAP),由于视差被限制在[0-255]整数围,造成一定的误差,所得到的三维坐标数据会出现分层,如图3和图4所示:
图3Matlab绘制的三维点云,不同深度上有明显分层
图4左为直接得到的深度数据Mesh图,中为由8位视差数据计算得到的深度数据,右为对应的视差数据
而采用32位浮点格式来获取视差数据(参数ID:
CV_CAP_OPENNI_DISPARITY_MAP_32F),则消除了这种因视差误差造成深度数值分层的现象,如图5、6所示:
图5深度数值的分层现象基本消除
图6两种方式得到的深度数据一致
[编辑]
三、利用OpenGL显示三维点云数据
这方面主要是基于学习笔记(15)[7]的容,不过当时是通过另设OpenCV窗口设置Trackbar来调整OpenGL的视点位置和摄像机位置。
在这里则主要参考了OpenCV论坛的帖子《[HQ]OpenCV和OpenGL编程:
关于显示点云数据-StereoVision源码分享》[8]中villager5提供的方法来调整摄像机位置,做了一定的修改,使得鼠标左键拖曳能够实现对上下左右的视角变换,鼠标右键拖曳能够实现视距远近的变换,不再需要用Trackbar来调整。
下面是相关的代码:
#defineSIGN(x)((x)<0 ?
-1:
((x)>0?
1:
0))
//////////////////////////////////////////////////////////////////////////
//
//---OpenGL全局变量
floatxyzdata[480][640][3];
floattexture[480][640][3];
intglWinWidth=640,glWinHeight=480;
intwidth=640,height=480;
doubleeyex,eyey,eyez,atx,aty,atz;//eye*-摄像机位置,at*-注视点位置
boolleftClickHold=false,rightClickHold=false;
intmx,my;//鼠标按键时在OpenGL窗口的坐标
intry=90,rx=90;//摄像机相对注视点的观察角度
doublemindepth,maxdepth;//深度数据的极值
doubleradius=6000.0;//摄像机与注视点的距离
/************************************************************************/
/*OpenGL响应函数*/
/************************************************************************/
//////////////////////////////////////////////////////////////////////////
//鼠标按键响应函数
voidmouse(intbutton,intstate,intx,inty)
{
if(button==GLUT_LEFT_BUTTON)
{
if(state==GLUT_DOWN)
{
leftClickHold=true;
}
else
{
leftClickHold=false;
}
}
if(button==GLUT_RIGHT_BUTTON)
{
if(state==GLUT_DOWN)
{
rightClickHold=true;
}
else
{
rightClickHold=false;
}
}
}
//////////////////////////////////////////////////////////////////////////
//鼠标运动响应函数
voidmotion(intx,inty)
{
intrstep=5;
if(leftClickHold==true)
{
if(abs(x-mx)>abs(y-my))
{
rx+=SIGN(x-mx)*rstep;
}
else
{
ry-=SIGN(y-my)*rstep;
}
mx=x;
my=y;
glutPostRedisplay();
}
if(rightClickHold==true)
{
radius+=SIGN(y-my)*100.0;
radius=std:
:
max(radius,100.0);
mx=x;
my=y;
glutPostRedisplay();
}
}
//////////////////////////////////////////////////////////////////////////
//三维图像显示响应函数
voidrenderScene(void)
{
//clearscreenanddepthbuffer
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
//Resetthecoordinatesystembeforemodifying
glLoadIdentity();
//setthecameraposition
atx=0.0f;
aty=0.0f;
atz=(mindepth-maxdepth)/2.0f;
eyex=atx+radius*sin(CV_PI*ry/180.0f)*cos(CV_PI*rx/180.0f);
eyey=aty+radius*cos(CV_PI*ry/180.0f);
eyez=atz+radius*sin(CV_PI*ry/180.0f)*sin(CV_PI*rx/180.0f);
gluLookAt(eyex,eyey,eyez,atx,aty,atz,0.0,1.0,0.0);
glRotatef(0,0,1,0);
glRotatef(-180,1,0,0);
//对点云数据进行三角化
//参考自:
.codeproject./KB/openGL/OPENGLTG.aspx
//wearegoingtoloopthroughallofourterrain'sdatapoints,
//butweonlywanttodrawonetrianglestripforeachsetalongthex-axis.
for(inti=0;i{
glBegin(GL_TRIANGLE_STRIP);
for(intj=0;j{
//foreachvertex,wecalculatethevertexcolor,
//wesetthetexturecoordinate,andwedrawthevertex.
/*
thevertexesaredrawninthisorder:
0--->1
/
/
/
2--->3
*/
//drawvertex0
glTexCoord2f(0.0f,0.0f);
glColor3f(texture[i][j][0]/255.0f,texture[i][j][1]/255.0f,texture[i][j][2]/255.0f);
glVertex3f(xyzdata[i][j][0],xyzdata[i][j][1],xyzdata[i][j][2]);
//drawvertex1
glTexCoord2f(1.0f,0.0f);
glColor3f(texture[i+1][j][0]/255.0f,texture[i+1][j][1]/255.0f,texture[i+1][j][2]/255.0f);
glVertex3f(xyzdata[i+1][j][0],xyzdata[i+1][j][1],xyzdata[i+1][j][2]);
//drawvertex2
glTexCoord2f(0.