Kinect开发学习笔记之(四)提取颜色数据并用OpenCV显示
我的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格式,便于我们处理和显示。
至此,目标达成。