前言
微软于2011年6月16日推出的windows平台体感设备kinect的开发包beta版。尽管还有很多不足,许多功能都不完整,但是已经让我们这些期盼了半年多的程序员们兴奋不已了。
我也是初次接触这个SDK,以前一直使用OpenNI。在使用过程中发现一些问题,整体构架什么的,SDK远不如OpenNI完善,甚至于SDK中的例子,也像是匆忙赶制出来的 -_-#
好吧,我承认,即使这样,我也依然要使用这个SDK……
为了让大家的NI没白学,也让新人能快速入门,特意写下一些心得,算是入门级的教程了。
何人适合阅读此教程
只要你C/C++语法没问题,WINDOWS平台下的编程能通过阅读代码理解,就可以了。
但是为了便于提高输出的FPS,我使用了OpenGL而不是例子中自带的D3D,所以你最好有点OpenGL基础(认真研究过NI的童鞋们笑而不语)。不过不会也没关系,我尽量把它们分开讲解。
此教程包含什么
kinect设备包括一个彩色摄像头,一个红外发射摄像头及一个红外接收摄像头,另外,还包括一组由4个高性能的降噪麦克阵列组成的语音设备(可是TYYD为什么不提供降噪API)。
SDK中包含了以上所有设备的访问功能,尤其是降噪麦克的读入加上speech库积累多年的识别训练,它终于不再是摆设了(恐怕所有从其他第三方驱动转型到SDK中的童鞋们,都是冲着这个来的 ^_^)。
此教程不会完整到包含全部使用的程度,看标题就知道了,目的仅仅是让大家能通过此教程,快速学会如何使用SDK获取/使用Kinect设备中的彩色图像数据、深度图像数据、用户数据、骨骼数据。这部分都是用来实现自然用户界面(NUI)的功能的,也就是我们常说的“体感”。因此这些内容统称为NUI。
至于其他诸如audio及语音识别等部分,考虑以后再说吧(如果2012过完我还活着的话 -_-#)
好了,废话讲完,让我们开始吧,首先从读取/显示彩色图像开始 ^_^
一.一些基本设置
1.1 在vs2010项目中,需要设置C++目录
包含目录中加入 $(MSRKINECTSDK)\inc
库目录中加入 $(MSRKINECTSDK)\lib
MSRKINECTSDK是环境变量,正确安装MS KINECT FRO WINDOWS SDK 后,会在计算机中的环境变量中看到。
1.2 添加特定库
除了指定目录外,你还需要在链接器中设置附加依赖项,填入MSRKinectNUI.lib
1.3 头文件
为了使用NUI中的API,首先我们要包含 MSR_NuiApi.h
#include "MSR_NuiApi.h"
切记,在这之前,要保证你已经包含了windows.h
#include <Windows.h>
#include "MSR_NuiApi.h"
否则 msr_nuiapi中很多根据windos平台定义的数据类型及宏都不生效。
二.初始化NUI
接下来,任何想使用微软提供的API来操作KINECT,都必须在所有操作之前,调用NUI的初始化函数
HRESULT NuiInitialize(DWORD dwFlags);
dwFlags参数是以标志位的含义存在的。你可以使用下面几个值来指定你打算使用NUI中的哪些内容。
NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX 使用NUI中的带用户信息的深度图数据
NUI_INITIALIZE_FLAG_USES_COLOR 使用NUI中的彩色图数据
NUI_INITIALIZE_FLAG_USES_SKELETON 使用NUI中的骨骼追踪数据
NUI_INITIALIZE_FLAG_USES_DEPTH 仅仅使用深度图数据(如果你自己有良好的场景分析或物体识别算法,那么你应该用这个)
以上4个标志位,你可以使用一个,也可以用 | 操作符将它们组合在一起。例如:
//只使用彩色图
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);
//使用带用户信息的深度图/使用用户骨骼框架/使用彩色图
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX | NUI_INITIALIZE_FLAG_USES_SKELETON | NUI_INITIALIZE_FLAG_USES_COLOR);
一个应用程序对一个KINECT设备,必须要调用此函数一次,并且也只能调用一次。如果在这之后又调用一次初始化,势必会引起逻辑错误(即使是2个不同程序)。
比如你运行一个SDK的例子,在没关闭它的前提下,再运行一个,那么后运行的就无法初始化成功,但不会影响之前的程序继续运行。
如果你的程序想使用多台KINECT,那么请使用INuiInstance接口来初始化你的设备。(至今为止我还没有测试过多台,留到以后再说吧 -_-#)
另外,作为一名KINECT程序员,你需要记得的是,微软SDK中提供的运行环境在处理KINECT传输数据时,是遵循一条3步骤的运行管线的。
第一阶段只处理彩色和深度数据
第二阶段处理用户索引并根据用户索引将颜色信息追加到深度图中。(这回你明白为什么NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX这个标志位起这么长的名字了吧 -_-#)
第三阶段处理骨骼追踪数据
NuiInitialize就是应用程序用通过传递给dwFlags参数具体值,来初始化这个管线中必须的阶段。因此,我们总是先在标志位中指定图像类型,才可以在接下来的环节中去调用NuiImageStreamOpen之类的函数。如果你初始化的时候没指定NUI_INITIALIZE_FLAG_USES_COLOR,那你以后就别指望NuiImageStreamOpen能打开彩色数据了,它肯定会调用失败,因为没初始化嘛。(我自己都觉得这些东西说的太罗嗦了,不知道看的人懂没懂 -_-#)
好了,现在我们初始化一下NUI,本程序只读取彩色图,那么标志位如何设置?你懂的……
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);
//这是一种处理返回值的方式
if( FAILED( hr ) )
{
cout<<"NuiInitialize failed"<<endl;
return hr;
}
//这是另一种处理返回值的方式
if(hr == S_OK)
{
cout<<"NuiInitialize successfully"<<endl;
}
我特意准备了2种对NuiInitialize返回值进行处理的代码。只是想通过这个例子说明,我推荐使用后者。
NuiInitialize返回值必须是S_OK才可以让你的程序继续下去,你也只应该对返回值判断是否==或者!= S_OK.
三.释放NUI
OK,初始化以后,在我们继续其他深入获取NUI设备的数据之前,先了解一下如何关闭你的程序与NUI之间的联系。
VOID NuiShutdown();
关于这个函数,没什么可说的,你的程序退出时,都应该调用一下。甚至于,你的程序暂时不使用KINECT了,就放开对设备的控制权,好让其他程序可以访问KINECT。
放开后再访问呢??自己想 -_-#
友情提示使用OpenGL的程序员们,如果你们是在使用glut库,那么不要在glMainLoop()后面调用NuiShutdown(),因为它不会执行,你应该在窗口关闭以及任意你执行了退出代码的时刻调用它。
四.打开对NUI设备的访问通道
HRESULT NuiImageStreamOpen(NUI_IMAGE_TYPE eImageType,NUI_IMAGE_RESOLUTION eResolution,DWORD dwImageFrameFlags_NotUsed,DWORD dwFrameLimit,HANDLE hNextFrameEvent,HANDLE *phStreamHandle);
我们使用这个函数来打开kinect彩色或者深度图的访问通道,当然,其内部原理是通过"流"来实现的,因此,你也可以把这个函数理解为,创建一个访问彩色或者深度图的数据流.
似乎从很久远的时候开始,微软就在windows中开始使用流来访问所有硬件设备了,隐约记得那次更新,但忘记具体的原因和细节了,算了追究也没用 -_-#
参数:
eImageType
[in] 这是一个 NUI_IMAGE_TYPE 枚举类型的值,用来详细指定你要创建的流类型。
比如你要打开彩色图,就使用 NUI_IMAGE_TYPE_COLOR。
要打开深度图,就使用 NUI_IMAGE_TYPE_DEPTH。
具体这个枚举有多少个成员,我建议你们仔细阅读API手册。
但是有一点是需要注意的,还记的初始化函数么?对,刚才就提醒过你们,如果你现在要打开的是深度图,但是初始化的时候却没有指定深度图的标志位,那么……你就废了……
不是你的程序废了,是你废了,我都这么强调了你还那么干,谁也救不了你了。
记住!!!你能打开的图像类型,必须是你在初始化的时候指定过的。
eResolution
[in] 这是一个 NUI_IMAGE_RESOLUTION 枚举类型的值,用来指定你要以什么分辨率来打开eImageType(参数1)中指定的图像类别。
假如你在参数eImageType中指定的是彩色图NUI_IMAGE_TYPE_COLOR,那么你可以选择2种分辨率
NUI_IMAGE_RESOLUTION_1280x1024,NUI_IMAGE_RESOLUTION_640x480
如果你在参数eImageType中指定的是深度图NUI_IMAGE_TYPE_DEPTH,那么你可以选择3种分辨率
NUI_IMAGE_RESOLUTION_640x480, NUI_IMAGE_RESOLUTION_320x240, NUI_IMAGE_RESOLUTION_80x60
API手册里,详细描述了这个对照表,各种图像类型都支持什么分辨率,你们应该仔细查看,相信看过一遍之后就会记住的 ^_^
dwImageFrameFlags_NotUsed
[in] 你看参数名就知道了,这是个废物参数,一点用没有,你随便给个整数就行了。至于以后的版本里它会不会有意义,我懒得去猜 -_-#
dwFrameLimit
指定NUI运行时环境将要为你所打开的图像类型建立几个缓冲。最大值是NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM(当前版本为 4)
对于大多数啊程序来说,2就足够了。(关于这个值设置的高低,还有另外的用意,但是我打算以后再讨论 ^_^)
hNextFrameEvent
[in, optional] 一个用来手动重置信号是否可用的事件句柄(event),该信号用来控制KINECT是否可以开始读取下一帧数据。也就是说在这里指定一个句柄后,随着程序往后继续推进,当你在任何时候想要控制kinect读取下一帧数据时,都应该先使用WaitForSingleObject判断一下该句柄。
phStreamHandle
[out] 出参,指定一个句柄的地址。函数成功执行后,将会创建对应的数据访问通道(流),并且让该句柄保存这个通道的地址。也就是说,如果现在创建成功了。那么以后你想读取数据,就要通过这个句柄了。(现在看不懂没关系,下面的代码会比这么描述清晰的多 ^_^)
返回值
只有S_OK表示成功打开,错误原因却有很多,比如打开一个没初始化过的数据流;打开一个已被使用的数据流;参数phStreamHandle为NULL等等。自己查阅API手册吧。
好了,让我们实战一小下吧~
//初始化NUI
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR); //指定要访问彩色图信息
if( hr != S_OK )
{
cout<<"NuiInitialize failed"<<endl;
return hr;
}
HANDLE h1 = CreateEvent( NULL, TRUE, FALSE, NULL ); //创建读取下一帧的信号事件句柄
HANDLE h2 = NULL; //用来保存彩色图通道(流)句柄
//打开彩色图数据流,并用h2保存该流的句柄,以便于以后读取
hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,NUI_IMAGE_RESOLUTION_640x480,0,2,h1,&h2);
if( hr != S_OK )
{
switch(hr)
{
case E_POINTER:
cout<<"The value of phStreamHandle is NULL,please check it"<<endl;
break;
case E_INVALIDARG:
cout<<"The value of dwFrameLimit is outside the range from 1- NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM"<<endl;
break;
//…………
}
cout<<"Could not open image stream video"<<endl;
return hr;
}
五.读取彩色图数据
HRESULT NuiImageStreamGetNextFrame(HANDLE hStream,DWORD dwMillisecondsToWait,CONST NUI_IMAGE_FRAME **ppcImageFrame);
参数:
hStream
[in] 还记得我们前面打开数据流的时候,将流句柄保存到哪了么?这里要的就是流句柄。
dwMillisecondsToWait
[in] 延迟时间,以微秒为单位的整数。当运行环境在读取之前,会先等待这个时间。
ppcImageFrame
[out] 出参,指定一个 NUI_IMAGE_FRAME 结构的指针,当读取成功后,该函数会将读取到的数据地址返回,保存在此参数中。
返回值
同样是S_OK表示成功
好了,让我们读取一帧吧
const NUI_IMAGE_FRAME * pImageFrame = NULL;
hr = NuiImageStreamGetNextFrame(h2,0,&pImageFrame );
if( hr != S_OK )
{
cout<<"Get Image Frame Failed"<<endl;
return hr;
}
如果你没有遇到什么错误的话,那么刚才KINECT就捕获了一副画面,并将该画面的信息保存在一个NUI_IMAGE_FRAME结构中,pImageFrame指向该结构的地址。
pImageFrame包含了很多有用信息,包括:图像类型,分辨率,图像缓冲区,时间戳等等。相关信息翻阅API手册
其中最有用的就是成员
NuiImageBuffer *pFrameTexture;
那么我们就先保存一下这个成员吧
NuiImageBuffer * pTexture = pImageFrame->pFrameTexture; //这是接着前面的代码继续来的
应用程序必须调用NuiImageBuffer:ockRect方法,来获取当前帧中,跟图形有关的缓冲(还记得前面说过,你可以指定1-4个缓冲区么),继续我们的代码
KINECT_LOCKED_RECT LockedRect;
pTexture->LockRect( 0, &LockedRect, NULL, 0 );
好了,现在真正保存图像的对象LockedRect我们已经有了,并且也将图像信息写入这个对象了。
LockedRect的数据类型是KINECT_LOCKED_RECT结构类型,该结构只包含2个成员
INT Pitch;
void * pBits;
其中pBits就是用来存储所有像素点的数组地址。而pitch指明了图像中一行数据的大小(字节)
我们之前指定的分辨率是640x480,也就是说,这307200个像素点,全都被保存在一个很大的数组中,每个像素点的颜色信息都是以32位RGB形式存储的,所以,你可以理解,这个数组一共占用1228800个字节。而数组的起始地址,就是LockedRect->pBits;
你可以用pTexture的成员BufferLen来验证数组大小的有效性。(你们试试pitch又指明什么呢?)
cout<<"当前帧图像占用内存"<<pTexture->BufferLen<<"字节"<<endl;
当然,这没什么实际意义,还是抓紧读取我们的彩色图像素信息吧。
BYTE * pBuffer = (BYTE*) LockedRect.pBits;
//显示x200y400位置上的像素信息
pBuffer += (200+399*640)*4;
printf("x:200 y:400坐标处的像素颜色:r:%d g:%d b:%d\n",pBuffer[2],pBuffer[1],pBuffer[0]);
聪明的童鞋们,200+399*640代表什么含义,你懂的,乘以4又代表什么含义,你也懂的……
终于讲完了,单个点都会读取了,那么从头遍历到结尾,只需要一个嵌套循环而已
BYTE * pBuffer = (BYTE*) LockedRect.pBits;
for (int y = 0; y < 480; ++y)
{
const BYTE * pImage = pBuffer;
for (int x = 0; x < 640; ++x)
{
//第y行第x列像素的信息
//pImage[3] A
//pImage[2] R
//pImage[1] G
//pImage[0] B
//怎么用就看童鞋们自己了,是设置OpenGL的纹理还是直接Paint到窗体DC上我就不管了,你们随便
//但是友情提醒一下,如果你直接Paint到DC上,还是考虑用离屏界面吧 memory DC
pImage+=4;//每读取完一个像素,向后移动到下一个像素点。
}
pBuffer += 640 * 4; //640x4是什么意思?为了尽可能简化代码,我没有定义宏或常量,但是你们懂的……
}