Kinect开发学习笔记之(四)提取颜色数据并用OpenCV显示

Kinect开发学习笔记之(四)提取颜色数据并用OpenCV显示

[email protected]

http://blog.csdn.net/zouxy09

 

我的Kinect开发平台是:

Win7 x86 + VS2010 + Kinect for Windows SDK v1.6 + OpenCV2.3.0

开发环境的搭建见上一文:

http://blog.csdn.net/zouxy09/article/details/8146055

下面这几个大部分是参考“timebomb”的Kinect学习笔记系列:

http://blog.csdn.net/timebomb/article/details/7169372

非常感谢“timebomb”的工作,让我能尽快的进入Kinect的开发。

 

本学习笔记以下面的方式组织:编程前期分析、代码与注释和重要代码解析三部分。

 

要实现目标:通过微软的SDK提取颜色数据(彩色图像)并用OpenCV显示

 

一、编程前期分析

      我们在http://blog.csdn.net/zouxy09/article/details/8145592中提到:

      Kinect有三个镜头,中间的镜头是 RGB 彩色摄影机,用来采集彩色图像。左右两边镜头则分别为红外线发射器和红外线CMOS 摄影机所构成的3D结构光深度感应器,用来采集深度数据(场景中物体到摄像头的距离)。彩色摄像头最大支持1280*960分辨率成像,红外摄像头最大支持640*480成像。那下面我们就是要通过微软提供的SDK的API去读取驱动上面的彩色摄像头来读取彩色图像。

      一个应用程序从Kinect传感器阵列中访问下列图像数据:

1)色彩数据:就是彩色摄像头采集到的数据,我们可以设置采集的分辨率;

2)深度数据:就是红外摄像头采集到的数据,同样可以设置采集的分辨率;

3)带游戏者ID的深度数据:Kinect可以检测6个人,所以深度数据中有携带标示这是哪个游戏者的深度数据的。

4)骨骼点数据:实际上不能算是图像数据,感觉应该是Kinect上层算法分析彩色和深度图像得到的骨骼点数据,包含了跟踪到的人的关节点的位置等信息。

       而对于彩色和深度这些图像数据,SDK是以数据流的方式来组织的,也就是图像数据按顺序的一帧一帧的流过来,你需要的时候就拿。当然,如果你拿的速度比摄像头提供图像的速度要快,那么你就需要等待,等待摄像头产生新的数据给你。那么这个“等”就有了两种方式了:

1)查询方式:反正我也没事干,所以我不停的问摄像头拿数据,通过一个while循环不断地催它,然后一旦有新的图像数据了,我拿到就跑;

2)事件方式:要我不停地催你,我也烦,你没有数据给我,那我先打个瞌睡(休眠了,不用占CPU资源),然后你有新的数据来后,再叫醒我(给个有数据的信号),然后我再拿走数据。那我这个等新数据的过程就叫一个事件,系统通过一个事件的句柄来标示,这样系统才知道下面摄像头有数据来了,系统才知道唤醒谁啊,是吧。而这个事件我们待会编程就遇到了。而目前,大部分是通过这种方式来得到图像数据的。(呵呵,不知道理解得对不对)

    还是通过代码来分析清晰点。

 

二、代码与注释

#include <windows.h>
#include <iostream> 
#include <NuiApi.h>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
	Mat image;
	image.create(480, 640, CV_8UC3);
 
    //1、初始化NUI 
    HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR); 
    if (FAILED(hr)) 
    { 
        cout<<"NuiInitialize failed"<<endl; 
        return hr; 
    } 

    //2、定义事件句柄 
	//创建读取下一帧的信号事件句柄,控制KINECT是否可以开始读取下一帧数据
    HANDLE nextColorFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
    HANDLE colorStreamHandle = NULL; //保存图像数据流的句柄,用以提取数据 
 
    //3、打开KINECT设备的彩色图信息通道,并用colorStreamHandle保存该流的句柄,以便于以后读取
	hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR, NUI_IMAGE_RESOLUTION_640x480, 
							0, 2, nextColorFrameEvent, &colorStreamHandle); 
    if( FAILED( hr ) )//判断是否提取正确 
    { 
        cout<<"Could not open color image stream video"<<endl; 
        NuiShutdown(); 
        return hr; 
    }
	namedWindow("colorImage", CV_WINDOW_AUTOSIZE);
 
    //4、开始读取彩色图数据 
    while(1) 
    { 
        const NUI_IMAGE_FRAME * pImageFrame = NULL; 

		//4.1、无限等待新的数据,等到后返回
        if (WaitForSingleObject(nextColorFrameEvent, INFINITE)==0) 
        { 
			//4.2、从刚才打开数据流的流句柄中得到该帧数据,读取到的数据地址存于pImageFrame
            hr = NuiImageStreamGetNextFrame(colorStreamHandle, 0, &pImageFrame); 
			if (FAILED(hr))
			{
				cout<<"Could not get color image"<<endl; 
				NuiShutdown();
				return -1;
			}

			INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;
			NUI_LOCKED_RECT LockedRect;

			//4.3、提取数据帧到LockedRect,它包括两个数据对象:pitch每行字节数,pBits第一个字节地址
			//并锁定数据,这样当我们读数据的时候,kinect就不会去修改它
            pTexture->LockRect(0, &LockedRect, NULL, 0); 
			//4.4、确认获得的数据是否有效
            if( LockedRect.Pitch != 0 ) 
            { 
				//4.5、将数据转换为OpenCV的Mat格式
				for (int i=0; i<image.rows; i++) 
                {
					uchar *ptr = image.ptr<uchar>(i);  //第i行的指针
					
					//每个字节代表一个颜色信息,直接使用uchar
                    uchar *pBuffer = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch;
                    for (int j=0; j<image.cols; j++) 
                    { 
                        ptr[3*j] = pBuffer[4*j];  //内部数据是4个字节,0-1-2是BGR,第4个现在未使用 
                        ptr[3*j+1] = pBuffer[4*j+1]; 
                        ptr[3*j+2] = pBuffer[4*j+2]; 
                    } 
				} 
                imshow("colorImage", image); //显示图像 
            } 
            else 
            { 
                cout<<"Buffer length of received texture is bogus\r\n"<<endl; 
            }

			//5、这帧已经处理完了,所以将其解锁
			pTexture->UnlockRect(0);
            //6、释放本帧数据,准备迎接下一帧 
            NuiImageStreamReleaseFrame(colorStreamHandle, pImageFrame ); 
        } 
        if (cvWaitKey(20) == 27) 
            break; 
    } 
    //7、关闭NUI链接 
    NuiShutdown(); 
	return 0;
}

 

三、代码解析

首先,对Kinect,我们必须要包含下面两个头文件:

#include <windows.h>

#include <NuiApi.h>

 

1、初始化NUI

HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);

任何想使用微软提供的API来操作KINECT,都必须在所有操作之前,调用NUI的初始化函数:
HRESULT NuiInitialize(DWORD dwFlags);
    dwFlags参数是以标志位的含义存在的。你可以使用下面几个值来指定你打算使用NUI中的哪些内容。
NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX 提供带用户信息的深度图数据;

NUI_INITIALIZE_FLAG_USES_COLOR  提供色彩图像数据;

NUI_INITIALIZE_FLAG_USES_SKELETON     提供骨骼点数据;

NUI_INITIALIZE_FLAG_USES_DEPTH  提供深度图像数据.

NUI_INITIALIZE_FLAG_USES_AUDIO  提供声音数据;

NUI_INITIALIZE_DEFAULT_HARDWARE_THREAD  初始化默认的硬件线程;

以上的标志位,你可以使用一个,也可以用 | 操作符将它们组合在一起。例如:
//只使用彩色图
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步骤的运行管线的。
第一阶段只处理彩色和深度数据;
第二阶段处理用户索引并根据用户索引将颜色信息追加到深度图中。
第三阶段处理骨骼追踪数据;
       NuiInitialize就是应用程序用通过传递给dwFlags参数具体值,来初始化这个管线中必须的阶段。因此,我们总是先在标志位中指定图像类型,才可以在接下来的环节中去调用NuiImageStreamOpen之类的函数。如果你初始化的时候没指定NUI_INITIALIZE_FLAG_USES_COLOR,那你以后就别指望NuiImageStreamOpen能打开彩色数据了,它肯定会调用失败,因为没初始化嘛。就是说我们后面想要什么数据,得先告诉Kinect,否则后面你要它也不会给你,因为压根我就没启动那部分硬软件,拿什么给你啊。

另外,Kinect提供了两种处理返回值的方式,就是判断上面的函数是否执行成功。

//这是一种处理返回值的方式
if( FAILED( hr ) )
{
     cout<<"NuiInitialize failed"<<endl;
     return hr;
}
//这是另一种处理返回值的方式
if(hr == S_OK)
{
     cout<<"NuiInitialize successfully"<<endl;
}

 

2、定义事件句柄

      HANDLE nextColorFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );

CreateEvent()创建一个windows事件对象,创建成功则返回事件的句柄。事件有两个状态,有信号和没有信号!上面说到了。就是拿来等待新数据的。

CreateEvent函数需要4个参数:

·设定为NULL的安全描述符;

·一个设定为true的布尔值,因为应用程序将重置事件消息;

·一个未指定的事件消息初始状态的布尔值;

·一个空字符串,因为事件未命名。

 

3、打开KINECT设备的彩色数据流

       hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR, NUI_IMAGE_RESOLUTION_640x480,

0, 2, nextColorFrameEvent, &colorStreamHandle);

       我们使用这个函数来打开kinect彩色或者深度图的访问通道,当然,其内部原理是通过"流"来实现的,因此,你也可以把这个函数理解为,创建一个访问彩色或者深度图的数据流。
参数:
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手册吧。

 

4、无限等待新的数据,等到后返回

      WaitForSingleObject(nextColorFrameEvent, INFINITE)==0    

      和刚才说的一样,程序运行都这里,这个事件有信号,就是说有数据,那么程序往下执行,如果没有数据,就会等待。函数第二个参数表示你愿意等多久,具体的数据的话就表示你愿意等多少毫秒,还不来,我就不要了,继续往下走。如果是INFINITE的话,就表示无限等待新数据,直到海枯石烂,一定等到为止。等到有信号后就返回0 。

 

5、从数据流中拿数据

       hr = NuiImageStreamGetNextFrame(colorStreamHandle, 0, &pImageFrame);

      从刚才打开数据流的流句柄中得到该帧数据,读取到的数据地址存于pImageFrame。第二个参数表示你延时多少微秒拿数据,0表示,我立刻拿。

如果你没有遇到什么错误的话,那么刚才KINECT就捕获了一副画面,并将该画面的信息保存在一个NUI_IMAGE_FRAME结构中,pImageFrame指向该结构的地址。

pImageFrame包含了很多有用信息,包括:图像类型,分辨率,图像缓冲区,时间戳等等。

 

6、INuiFrameTexture接口

      INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;

      一个容纳图像帧数据的对象,类似于Direct3D纹理,但是只有一层(不支持mip-maping)。

其公有方法包含以下:

AddRef---增加一个对象上接口的引用数目;该方法在每复制一个指向该对象上接口的指针时都要调用一次;

BufferLen---获得缓冲区的字节长度;

GetLevelDesc---获得缓冲区的描述;

LockRect---给缓冲区上锁;

Pitch---返回一行的字节数;

QueryInterface---获取指向对象所支持的接口的指针,该方法对其所返回的指针调用AddRef函数;

Release---减少一个对象上接口的引用计数;

UnlockRect---对缓冲区解锁;

 

7、提取数据帧到LockedRect并锁定数据

      pTexture->LockRect(0, &LockedRect, NULL, 0);

      提取数据帧到LockedRect,它包括两个数据对象:pitch每行字节数,pBits第一个字节地址。另外,其还锁定数据,这样当我们读数据的时候,kinect就不会去修改它

   好了,现在真正保存图像的对象LockedRect我们已经有了,并且也将图像信息写入这个对象了。

 

8、将数据转换为OpenCV的Mat格式

      然后我们就将其保存图像的对象LockedRect的格式,转化为OpenCV的Mat格式,便于我们处理和显示。

 

至此,目标达成。

 

 

你可能感兴趣的:(Kinect开发学习笔记之(四)提取颜色数据并用OpenCV显示)