这一篇就来针对如何使用OpenNI 读取微软的 Kinect 的影像数据吧!
而先说明一下,Heresy 这篇文章是使用 Visual C++ 2010,针对目前微软的 Kinect、SensorKinect 的驱动程序,搭配 1.0.0.23 版的 OpenNI 版写的;如果使用其他版本、或是其他支持 OpenNI 的装置,那可能会要做一些对应的修改。同时,在开始阅读这篇文章前,建议也请先参考《在 WIndows 上安装 Kinect(含 MMD 使用 Kinect 简易教学)》来安装 Kinect 和 OpenNI,并确定可以正常运作。
首先,OpenNI 他默认的安装路径是在「C:\Program Files\OpenNI」,要开发 OpenNI 程序所有必要的档案,都会在这里;而在文件夹内,除了「Documentation」里有提供两份文件可以用来当作开发程序的依据外,在「Samples」目录下,也有提供不少范例可以用来参考。
OpenNI 的核心基本上是 C 语言,不过他有提供 C++ 的Wrapper 来当作 C++ 使用;基于个人习惯的关系,Heresy在这边会以 C++ 的形式,来使用 OpenNI。而必要的 header 档,都会在 OpenNI 的「Include」目录内,链接程序时所需的 openNI.lib 这个档案则是在「Lib」里。不过要注意的是,OpenNI 目前在 Windows 环境下只有 32 位的版本、没有 64 位版,所以目前只能编译 32 位的 OpenNI 程序。
而要设定一个使用 OpenNI 的 Visual C++ 项目也很简单,只要在项目「C/C++ \ AdditionalInclude Directories」里加入「$(OPEN_NI_INCLUDE)」、「Linker \ Additional Library Directories」里加上「$(OPEN_NI_LIB)」,并在「Linker \ Additional Dependencies」里加上「OpenNI.lib」,这样就可以了。
而接下来,Heresy 就先以读取 Kinect 的深度影像信息为目标,来写一个 C++ 的范例程序了~他的程序代码如下:
#include <stdlib.h>
#include <iostream>
#include <string>
#include <XnCppWrapper.h>
using namespacestd;
void CheckOpenNIError( XnStatus eResult, string sStatus )
{
if( eResult != XN_STATUS_OK )
cerr << sStatus << " Error: " << xnGetStatusString( eResult ) << endl;
}
int main( intargc, char**argv )
{
XnStatus eResult = XN_STATUS_OK;
// 2. initial context
xn::Context mContext;
eResult = mContext.Init();
CheckOpenNIError( eResult, "initialize context" );
// set map mode
XnMapOutputMode mapMode;
mapMode.nXRes = 640;
mapMode.nYRes = 480;
mapMode.nFPS = 30;
// 3. create depth generator
xn::DepthGenerator mDepthGenerator;
eResult = mDepthGenerator.Create( mContext );
CheckOpenNIError( eResult, "Create depth generator" );
eResult = mDepthGenerator.SetMapOutputMode(mapMode );
// 4. start generatedata
eResult = mContext.StartGeneratingAll();
// 5. read data
eResult = mContext.WaitAndUpdateAll();
if( eResult == XN_STATUS_OK )
{
// 5. get the depthmap
const XnDepthPixel* pDepthMap = mDepthGenerator.GetDepthMap();
// 6. Do somethingwith depth map
}
// 7. stop
mContext.StopGeneratingAll();
mContext.Shutdown();
return 0;
}
这个程序的功能,基本上就是去透过OpenNI 读取一张分辨率 640 x 480 的深度信息影像;但是在读取到数据后,并没有针对取得的数据做任何事,所以如果没有问题的话,这个程序是会直接结束,而没有任何产出的。
接下来,就来仔细看程序代码的部分。
首先,要以 C++ 的形式使用 OpenNI 的话,只需要加入「XnCppWrapper.h」这个头文件就好了,不用再 include 其他的档案。而 OpenNI 定义了名为「xn」的 namespace,所有的对象,大多都在这个 namespace 内,而不在 namespace 内的东西,也都有 XN 这个 prefix,所以应该还算满好区分的。
要使用 OpenNI,要先建立一个型别为「xn::Context」的 conext 对象(这里就是「mContext」),用来管理整个 OpenNI 的环境状态以及资源;而在开始使用前,必须要呼叫它的成员函式「Init()」来进行起始化(上方程式码「initial context 」的部分)。在进行起始化的时候,所有 OpenNI 相关的模块会被读取、分析,直到呼叫「Shutdown()」这个函式,才会把所使用的资源释放出来。
在 context 起始化成功后,接下来是要建立所要使用的 production node 了。由于这个范例的目的只是要读取深度传感器的数据,所以这里要建立的就只有「depth generator」一种,他的型别是「xn::DepthGenerator」。 而建立一个 production node 的方法,则是先宣告出他的对象(这里就是「mDepthGenerator」),然后再去呼叫他的「Create()」函式,并把 context 传入,这样就可以了(上方程式码中「createdepth generator 」的部分)。
不过要注意的是,有的时候在建立出 node 后,还需要对这个 node 作一些设定。像在这边,就还必须要透过「SetMapOutputMode()」这个函式,来设定 mDepthGenerator 这个 depth generator 的输出模式;而以 Kinect 来说,是要设定成为 640 x 480、30FPS。
在必要的 production node(这边只有一个)都建立好了以后,接下来就是开始产生数据(generatedata)了!由于 OpenNI 的概念是所以属于generator 的 production node(名称里有 generator 的都是)在使用时,都会不停地产生数据,所以得透过 context 来统一控制数据读取的开关。
而控制的方法很简单,就是透过 context 的成员函式「StartGeneratingAll()」来开始、并透过「StopGeneratingAll()」停止。在一个 context 执行「StartGeneratingAll()」开始读取后,属于他的 generator node 都会开始产生数据,直到呼叫「StopGeneratingAll()」才会停止。
在开始产生数据后,就可以读取各个不同的 production node 的资料了~不过不同类型的 generator 必须要透过不同的函式来读取数据,像这边的 depth generator 就是要用「GetDepthMap()」这个函式,来取得目前的 depth map。而 Depth Generator 取得的数据,会是一个「XnDepthPixel」的 const 指针,指向他实际数据的空间。
不过这边另外要注意的就是,generator 虽然是会不停地读取新的数据,但透过「GetDepthMap()」这类的函式,是有可能会拿到旧的数据的。而为了确保能取得最新的数据,在读取 Generator 的数据前,都必须要先呼叫 context 的 wait / update 这一系列的函式,来进行 node 数据的更新。
这系列的函示有四个:WaitAnyUpdateAll()、WaitOneUpdateAll()、WaitNoneUpdateAll() 和这边所使用的 WiatAndUpdateAll()。这四者都会更新 context 下所有的 node 的数据,差别只在于更新的条件;Heresy 这边所使用的 WiatAndUpdateAll() 会等到所有的 node 都取得新数据后,再统一更新所有的 node 的数据;而WaitAnyUpdateAll() 是等到随便一个 node 有新数据时就会更新、WaitOneUpdateAll() 则是等到指定的 node 有新数据时再更新、WaitNoneUpdateAll() 则是不管有没有新数据就强制更新。基本上,这四个不同的函式就是自己看时机、需求使用了。
前面已经有提过了,Depth Generator 取得的资料,会是一个「XnDepthPixel」的 const 指标、而实际上它就是一个大小是 640 x 480 的一维数组(因为现在的输出模式是 640 x480),基本上可以把它看作一张 640 x480 的灰阶图片,其中每一个点都代表他的在这个位置的深度、型别是「XnDepthPixel」;而他的深度值在 Windows 32 位的平台上,型别应该等同于「unsignedshort」。基本上,这里的深度值越大、代表距离越远(0 则是代表该点深度无法判别),如果透过 OpenNI 的函式,也可以换算出绝对距离,不过在这篇文章暂时不会提到就是了。
而在这个范例程序里,Heresy什么事都没有做。如果要额外处理这个深度图的数据的话,只要在「//6. Do something with depth map」那里,读取「pDepthMap」这个指针的数据来做处理就可以了。像如果把直接它的深度信息由 XnDepthPixel 转换为一般的 256 灰阶图输出的话,就会是类似右边的结果;而当然,这样的图意义不大,但是其实这些深度信息还可以拿来做很多应用,这点就看程序开发者怎么发挥了~
(Heresy 本来有想连储存图档一起写,不过由于牵扯到储存图档的话,程序代码会变得比较复杂,所以在这边也就先跳过了)
当读取完数据,不再继续读取数据后,就要把 OpenNI 停下来;而这边为了停止继续产生数据所呼叫的函示,就是之前已经提到过的「StopGeneratingAll()」。而如果完全不打算继续使用 OpenNI 的环境的话,则也要记得呼叫「Shutdown()」这个函式,把 OpenNI 所使用的资源释放出来。
如果仔细看前面的程序代码应该可以发现,Heresy 在大部分的地方都用一个型别是「XnStatus」的变量「eResult」来接 OpenNI 函式的回传值,而实际上,这就是用来判断 OpenNI 的函式是否正确执行的依据;如果一个 OpenNI 函式的回传值式「XN_STATUS_OK」的话,就代表他执行结果是正确的,但是如果不是的话,就代表可能出问题了~而要知道出了什么问题,则可以透过「xnGetStatusString()」这个函式,来取得文字的错误讯息;像上面 Heresy 自己定义的「CheckOpenNIError()」,就是在做这件事的。
这篇 Kinect +OpenNI 的第一个范例,就大概先写到这了。基本上,这篇算是透过抓取深度的范例,来大概解释一下怎么使用OpenNI 里的 map generator 了~而这边的程序也相当单纯,之后还会再慢慢写一些更进阶的应用的。