微软Kinect for windows SDK 使用教程 (NUI部分)

如何使用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++目录
    包含目录中加入 $(KINECTSDK10_DIR)\inc;

    库目录中加入    $(KINECTSDK10_DIR)\lib\x86

    (注意安装SDK1.7以后,路径不一样了,环境变量名变成KINECTSDK10_DIR)

    MSRKINECTSDK是环境变量,正确安装MS KINECT FRO WINDOWS SDK 后,会在计算机中的环境变量中看到。
1.2 添加特定库
    除了指定目录外,你还需要在链接器中设置附加依赖项,填入KinectNUI.lib
1.3 头文件
    为了使用NUI中的API,首先我们要包含 NuiApi.h
    #include "NuiApi.h" 以前是"NuiApi.h
    切记,在这之前,要保证你已经包含了windows.h
    #include <Windows.h>
    #include "NuiApi.h"
    否则 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,本程序只读取彩色图,那么标志位如何设置?你懂的……
  1. HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);  
  2. //这是一种处理返回值的方式  
  3. if( FAILED( hr ) )  
  4. {  
  5.     cout<<"NuiInitialize failed"<<endl;  
  6.     return hr;  
  7. }  
  8. //这是另一种处理返回值的方式  
  9. if(hr == S_OK)  
  10. {  
  11.       cout<<"NuiInitialize successfully"<<endl;  
  12. }  

我特意准备了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手册吧。

好了,让我们实战一小下吧~

  1. //初始化NUI  
  2. HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR); //指定要访问彩色图信息  
  3. if( hr != S_OK )  
  4. {  
  5.   cout<<"NuiInitialize failed"<<endl;  
  6.   return hr;  
  7. }  
  8. HANDLE h1 = CreateEvent( NULL, TRUE, FALSE, NULL ); //创建读取下一帧的信号事件句柄  
  9. HANDLE h2 = NULL;                                                        //用来保存彩色图通道(流)句柄  
  10. //打开彩色图数据流,并用h2保存该流的句柄,以便于以后读取  
  11. hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,NUI_IMAGE_RESOLUTION_640x480,0,2,h1,&h2);  
  12. if( hr != S_OK )  
  13. {  
  14.   switch(hr)  
  15.   {  
  16.   case E_POINTER:  
  17.    cout<<"The value of phStreamHandle is NULL,please check it"<<endl;  
  18.                         break;  
  19.                 case E_INVALIDARG:  
  20.    cout<<"The value of dwFrameLimit is outside the range from 1- NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM"<<endl;  
  21.    break;  
  22.   //…………  
  23.   }  
  24.   cout<<"Could not open image stream video"<<endl;  
  25.   return hr;  
  26. }  


五.读取彩色图数据

HRESULT NuiImageStreamGetNextFrame(HANDLE hStream,DWORD dwMillisecondsToWait,CONST NUI_IMAGE_FRAME **ppcImageFrame);
参数:
hStream 
[in] 还记得我们前面打开数据流的时候,将流句柄保存到哪了么?这里要的就是流句柄。

dwMillisecondsToWait 
[in] 延迟时间,以微秒为单位的整数。当运行环境在读取之前,会先等待这个时间。

ppcImageFrame 
[out] 出参,指定一个 NUI_IMAGE_FRAME 结构的指针,当读取成功后,该函数会将读取到的数据地址返回,保存在此参数中。

返回值
同样是S_OK表示成功

好了,让我们读取一帧吧
 
  1. const NUI_IMAGE_FRAME * pImageFrame = NULL;  
  2.  hr = NuiImageStreamGetNextFrame(h2,0,&pImageFrame );  
  3.  if( hr != S_OK )  
  4.  {  
  5.   cout<<"Get Image Frame Failed"<<endl;  
  6.   return hr;  
  7.  }  


如果你没有遇到什么错误的话,那么刚才KINECT就捕获了一副画面,并将该画面的信息保存在一个NUI_IMAGE_FRAME结构中,pImageFrame指向该结构的地址。
pImageFrame包含了很多有用信息,包括:图像类型,分辨率,图像缓冲区,时间戳等等。相关信息翻阅API手册
其中最有用的就是成员
NuiImageBuffer *pFrameTexture;
那么我们就先保存一下这个成员吧
  NuiImageBuffer * pTexture = pImageFrame->pFrameTexture; //这是接着前面的代码继续来的

应用程序必须调用NuiImageBuffer::L 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又代表什么含义,你也懂的……

终于讲完了,单个点都会读取了,那么从头遍历到结尾,只需要一个嵌套循环而已

  1. BYTE * pBuffer = (BYTE*) LockedRect.pBits;  
  2.   for (int y = 0; y < 480; ++y)  
  3.   {  
  4.    const BYTE * pImage = pBuffer;  
  5.    for (int x = 0; x < 640; ++x)  
  6.    {  
  7.     //第y行第x列像素的信息  
  8.     //pImage[3]   A  
  9.     //pImage[2]   R  
  10.     //pImage[1]   G  
  11.     //pImage[0]   B  
  12.     //怎么用就看童鞋们自己了,是设置OpenGL的纹理还是直接Paint到窗体DC上我就不管了,你们随便  
  13.     //但是友情提醒一下,如果你直接Paint到DC上,还是考虑用离屏界面吧 memory DC  
  14.       pImage+=4;//每读取完一个像素,向后移动到下一个像素点。  
  15.    }  
  16.    pBuffer += 640 * 4; //640x4是什么意思?为了尽可能简化代码,我没有定义宏或常量,但是你们懂的……  
  17.   }  



最后,上完整代码,考虑到E文问题,所以我把所有注释换成中文了

  1.   
  1. // kongzhitai.cpp : 定义控制台应用程序的入口点。  
  2.   
  3. //  
  4.   
  5. //#include "stdafx.h"  
  6.   
  7. #include "opencv2/highgui/highgui.hpp"  
  8. #include "opencv2/imgproc/imgproc.hpp"  
  9. #include <iostream>  
  10.   
  11. using namespace std;  
  12. using namespace cv;  
  13.   
  14. //KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK  
  15. #include "windows.h"  
  16. #include "NuiApi.h"  
  17. //KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK  
  18.   
  19. int main(int argc,char * argv[])  
  20.   
  21. {  
  22.     //初始化NUI  
  23.     HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);  
  24.   
  25.     if( hr != S_OK )  
  26.     {  
  27.         cout<<"NuiInitializefailed"<<endl;  
  28.         return hr;  
  29.     }  
  30.   
  31.     //打开KINECT设备的彩色图信息通道  
  32.   
  33.     HANDLE h1 = CreateEvent( NULL,TRUE, FALSE, NULL );  
  34.     HANDLE h2 = NULL;  
  35.     hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,NUI_IMAGE_RESOLUTION_640x480,0,4,h1,&h2);  
  36.   
  37.     if( FAILED( hr ) )  
  38.     {  
  39.         cout<<"Could not open image stream video"<<endl;  
  40.         return hr;  
  41.     }  
  42.   
  43.     //开始读取彩色图数据;  
  44.     Mat img;  
  45.     img.create(480,640,CV_8UC3);  
  46.   
  47.     uchar *pData;  
  48.     while(1)  
  49.     {  
  50.         WaitForSingleObject(h1,INFINITE);  
  51.   
  52.         const NUI_IMAGE_FRAME *pImageFrame = NULL;  
  53.   
  54.         hr = NuiImageStreamGetNextFrame(h2, 0, &pImageFrame );  
  55.   
  56.         if( FAILED( hr ) )  
  57.   
  58.         {  
  59.   
  60.             cout<<"GetImage Frame Failed"<<endl;  
  61.   
  62.             continue;  
  63.   
  64.         }  
  65.   
  66.   
  67.   
  68.         INuiFrameTexture * pTexture =pImageFrame->pFrameTexture;  
  69.   
  70.         NUI_LOCKED_RECT LockedRect;  
  71.   
  72.         pTexture->LockRect( 0,&LockedRect, NULL, 0 );  
  73.   
  74.         if( LockedRect.Pitch != 0 )  
  75.         {  
  76.             BYTE * pBuffer = (BYTE*)LockedRect.pBits;  
  77.             //赋值文艺青年方式;  
  78.             for(int i=0;i<480;i++)  
  79.             {  
  80.                 pData=img.ptr<uchar>(i);  
  81.                 for(int j=0;j<640;j++)  
  82.                 {  
  83.                     for(int k=0;k<3;k++)  
  84.                         *(pData+j*3+k)=*(pBuffer+i*4*640+j*4+k);  
  85.                 }  
  86.             }  
  87.   
  88.             imshow("rgb",img);  
  89.             waitKey(36);  
  90.             imwrite("F:\\out.jpg",img);  
  91.         }  
  92.   
  93.         if( LockedRect.Pitch != 0 )  
  94.   
  95.         {  
  96.   
  97.             BYTE * pBuffer = (BYTE*)LockedRect.pBits;  
  98.   
  99.             //显示x200y400位置上的像素信息  
  100.   
  101.             pBuffer +=(200+399*640)*4;  
  102.   
  103.             printf("x:200 y:400坐标处的像素颜色:r:%d g:%d b:%d\n",pBuffer[2],pBuffer[1],pBuffer[0]);  
  104.   
  105.         }  
  106.   
  107.         else  
  108.   
  109.         {  
  110.   
  111.             cout<<"Bufferlength of received texture is bogus\r\n"<<endl;  
  112.   
  113.         }  
  114.   
  115.         //释放本帧数据,准备迎接下一帧  
  116.   
  117.         NuiImageStreamReleaseFrame(h2, pImageFrame );  
  118.   
  119.     }  
  120.   
  121.     //关闭NUI链接  
  122.   
  123.     NuiShutdown();  
  124.   
  125.     return 0;  
  126.   
  127. }  


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值