Kinect学习(三):获取RGB颜色数据
前言
在前面的文章中介绍了如何搭建Kinect开发环境:Kinect学习(一):开发环境搭建。搭建好环境后,首先要做的当然就是试着读取Kinect中的数据了。
Kinect有三个镜头,中间的是RGB摄像头,左边的是红外线发射器,右边的是红外线CMOS摄像头构成的3D结构光摄像头,用来采集深度数据。彩色摄像头最大支持1280*960分辨率成像,红外摄像头最大支持640*480成像。
接下来就要通过微软提供的SDK来读取Kinect中的彩色摄像头的数据了。
代码
先上代码,里面有注释,后面再详细介绍。
#include <windows.h>
#include <NuiApi.h>
#include <iostream>
#include <opencv2/opencv.hpp>
/*
几个常用的头文件:
1、NuiApi.h ---包含所有的NUI(自然用户界面) API头文件和定义基本的初始化和函数访问入口。这是我们C++工程的主要头文件,它已经包含了NuiImageCamera.h 和 NuiSkeleton.h。
2、NuiImageCamera.h ---定义了图像和摄像头服务的API,包括调整摄像头的角度和仰角,打开数据流和读取数据流等。
3、NuiSkeleton.h ---骨架有关的API,包括使能骨架跟踪,获取骨架数据,骨架数据转换和平滑渲染等。
4、NuiSensor.h ---音频API,包括ISoundSourceLocalizer接口,用于返回声源的方向(波束形成)和音频的位置。
*/
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
cv::Mat img;
img.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;
}
cv::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 < img.rows; i++)
{
uchar *ptr = img.ptr<uchar>(i); //第i行的指针
//每个字节代表一个颜色信息,直接使用uchar
uchar *pBuffer = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch;
for (int j = 0;j < img.cols;j++)
{
//内部数据是4个字节,0-1-2是BGR,第4个现在未使用
ptr[3 * j] = pBuffer[4 * j];
ptr[3 * j + 1] = pBuffer[4 * j + 1];
ptr[3 * j + 2] = pBuffer[4 * j + 2];
}
}
cv::imshow("colorImage", img); //显示图像
}
else
{
cout << "Buffer length of received texture is bogus\r\n" << endl;
}
//5、这帧已经处理完了,所以将其解锁
pTexture->UnlockRect(0);
//6、释放本帧数据,准备获取下一帧
NuiImageStreamReleaseFrame(colorStreamHandle, pImageFrame);
}
if (cv::waitKey(20) == 27)
break;
}
//7、关闭NUI连接
NuiShutdown();
return 0;
}
运行结果
说明
整个程序可以分为以下流程:
- 初始化NUI接口;
- 定义事件句柄;
- 打开Kinect设备的数据流(彩色RGB);
- 等待数据更新,若更新完成则进行下一步;
- 从数据流中拿出图像数据;
- 提取数据帧并锁定数据;
- 将数据转换为OpenCV的Mat格式。
1、初始化NUI接口
//1、初始化NUI
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);
要使用微软提供的SDK中的SDK来操作Kinect,必须先调用NUI初始化函数。
函数原型为:
HRESULT NuiInitialize(DWORD dwFlags);
dwFlags
表示标志位,有以下几种情况:
-
NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX
: 提供带用户信息的深度图数据; -
NUI_INITIALIZE_FLAG_USES_COLOR
:提供RGB彩色图像数据; -
NUI_INITIALIZE_FLAG_USES_SKELETON
:提供骨骼点数据; -
NUI_INITIALIZE_FLAG_USES_DEPTH
:提供深度图像数据; -
NUI_INITIALIZE_FLAG_USES_AUDIO
:提供声音数据; -
NUI_INITIALIZE_DEFAULT_HARDWARE_THREAD
:初始化默认的硬件线程;
以上的都各自对应一个标志位,使用时可以使用|
将它们组合起来。
注意到,它还返回了一个HRESULT
类型的参数,通过它可以判断初始化函数是否执行成功。
if (FAILED(hr))
{
cout << "NuiInitialize failed" << endl;
return hr;
}
或者判断是否等于S_OK
:
if(hr == S_OK)
{
cout << "NuiInitialize successfully" << endl;
}
2、定义事件句柄
//2、定义事件句柄
//创建读取下一帧的信号事件句柄,控制KINECT是否可以开始读取下一帧数据
HANDLE nextColorFrameEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
该函数会创建一个windows事件对象,创建成功则返回事件的句柄。这里的这个事件是用来判断是否有新数据的。
其中有四个参数:
- 第一个是安全属性,设定为
NULL
的安全描述符; - 第二个表示设置信号复位方式为自动恢复为无信号状态(FALSE)还是手动恢复为无信号状态(TRUE),设为
TRUE
,因为后面应用程序会重置事件消息; - 第三个是事件消息初始状态的布尔值,为
FALSE
; -
最后一个是信号名称,可以直接设置为
NULL
;3、打开Kinect设备的彩色图像数据流
//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;
}
使用这个函数可以打开Kinect设备的彩色图或是深度图的访问通道。也可以理解为,创建一个访问彩色图或深度图的数据流。
函数原型:
_Check_return_ HRESULT NUIAPI NuiImageStreamOpen(
_In_ NUI_IMAGE_TYPE eImageType,
_In_ NUI_IMAGE_RESOLUTION eResolution,
_In_ DWORD dwImageFrameFlags,
_In_ DWORD dwFrameLimit,
_In_opt_ HANDLE hNextFrameEvent,
_Out_ HANDLE *phStreamHandle
);
参数说明:
-
eImageType
:这是一个NUI_IMAGE_TYPE
枚举类型的变量,用来指定要创建的数据流的类型。比如,NUI_IMAGE_TYPE_COLOR
对应彩色图,NUI_IMAGE_TYPE_DEPTH
对应深度图。注意,这里指定的图像的类型,必须是前面初始化是已经指定过的,否则无法打开。 -
eResolution
:这是一个NUI_IMAGE_RESOLUTION
枚举类型的变量,用来指定打开图像的分别率,但是由于3D结构光摄像头与RGB摄像头的分辨率不同,所以根据前面eImageType
参数指定的图像类型不同,这里也有所不同。如果eImageType
指定为彩色图NUI_IMAGE_TYPE_COLOR
,那么就有两种分辨率:NUI_IMAGE_RESOLUTION_1280x960
、NUI_IMAGE_RESOLUTION_640x480
;如果eImageType
指定为深度图NUI_IMAGE_TYPE_DEPTH
,那么就有三种分辨率:NUI_IMAGE_RESOLUTION_640x480
、NUI_IMAGE_RESOLUTION_320x240
、NUI_IMAGE_RESOLUTION_80x60
。 -
dwImageFrameFlags_NotUsed
:看名字就知道了,没有用到这个参数,随便给个数就可以了。 -
dwFrameLimit
:指定NUI运行时环境将要为你所打开的图像类型建立几个缓冲。最大值是NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM
,即4。大多数程序中,定为2就足够了。 -
hNextFrameEvent
: 一个用来手动重置信号是否可用的事件句柄(event),该信号用来控制Kinect是否可以开始读取下一帧数据。也就是说在这里指定一个句柄后,随着程序往后继续运行,当你在任何时候想要控制kinect读取下一帧数据时,都应该先使用WaitForSingleObject
函数判断一下该句柄,判断是否有数据可拿。 -
phStreamHandle
:函数执行成功后,会创建对应的数据流,并让这个句柄保存其地址。后面可以通过这个句柄来从Kinect读取数据。 - 返回值:
S_OK
表示成功。
4、等待数据更新,若更新完成则进行下一步;
//4.1、无线等待新的数据,等到就返回
if (WaitForSingleObject(nextColorFrameEvent, INFINITE) == 0)
{
...
}
前面也提到了这个函数,如果事件(对应nextColorFrameEvent
)有信号,即有数据,那么返回值为0,程序也会往下执行;如果没有数据,就会等待。函数的第二个参数表示等待时间,单位为ms,这里设为INFINITE
,表示一直等待。
5、从数据流中拿出图像数据;
//4.2、从刚才打开数据流的流句柄中得到该帧的数据,读取到的数据地址存于pImageFrame中
hr = NuiImageStreamGetNextFrame(colorStreamHandle, 0, &pImageFrame);
colorStreamHandle
为前面保存了Kinect设备的彩色信息通道的句柄,这个函数会从colorStreamHandle
中取出RGB图像数据,并保存在pImageFrame
中。第二个参数,表示延时多久获取数据,直接取为0,就是不等待直接取数据。
成功调用完这个函数之后,从Kinect捕获到的一帧图像,会保存在一个NUI_IMAGE_FRAME
结构体中,pImageFrame
为指向那个结构体的指针,其中包含了很多信息,如:图像类型,分辨率,图像缓冲区,时间戳等等。
6、提取数据帧并锁定数据;
INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;
NUI_LOCKED_RECT LockedRect;
//4.3、提取数据帧到LockedRect(它包括两个数据对象:pitch每行字节数,pBits第一个字节地址)
//并锁定数据,这样当我们读取数据的时候,kinect就不会去修改它
pTexture->LockRect(0, &LockedRect, NULL, 0);
INuiFrameTexture
是一个保存图像帧数据的对象,主要要用到他的下面两个共有方法:
-
LockRect
:给缓冲区上锁; -
UnlockRect
:给缓冲区解锁;
因为图像帧是保存在缓冲区的,如果不上锁的话,缓冲区中还有的图像可能会导致Kinect修改要取出的图像。
提取数据帧到LockedRect
后,它包含两个数据对象:pitch
,每行字节数;pBits
,第一个字节地址。
7、将数据转换为OpenCV的Mat格式。
//4.4、确认获得的数据是否有效
if (LockedRect.Pitch != 0)
{
//4.5、将数据转换为OpenCV的Mat格式
for (int i = 0; i < img.rows; i++)
{
uchar *ptr = img.ptr<uchar>(i); //第i行的指针
//每个字节代表一个颜色信息,直接使用uchar
uchar *pBuffer = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch;
for (int j = 0;j < img.cols;j++)
{
//内部数据是4个字节,0-1-2是BGR,第4个现在未使用
ptr[3 * j] = pBuffer[4 * j];
ptr[3 * j + 1] = pBuffer[4 * j + 1];
ptr[3 * j + 2] = pBuffer[4 * j + 2];
}
}
这一部分没什么说的了,就是把LockedRect
中的数据取出来,保存为OpenCV支持的Mat格式。
参考资料
- https://blog.csdn.net/zouxy09/article/details/8146266
- https://blog.csdn.net/timebomb/article/details/7169372
后记
这个笔记总体来说不难,主要是套路,微软官网的文档早就撤了,毕竟用的还是Kinect v1.0的,靠着博客和看看源码大概还能用用。
前段时间直到最近感觉都挺多事情的,很多笔记和写好的代码都没时间去整理,还要加把劲了。这段时间又有世界杯,熬夜看球什么的真的挺“伤”的。