您的位置:首页 > 运维架构

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

2014-11-05 15:29 405 查看
我的Kinect开发平台是:

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

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

/article/2890208.html

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

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

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

一、编程前期分析

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

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

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

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

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

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

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

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

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

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

二、代码与注释

[cpp] view
plaincopy

#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格式,便于我们处理和显示。

至此,目标达成。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐