如何使用Kinect for windows SDK中的NUI(彩色图像获取) 前言 微软于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是什么意思?为了尽可能简化代码,我没有定义宏或常量,但是你们懂的…… } Read more http://codevisualization.com/ |