Kinect开发学习笔记之(五)不带游戏者ID的深度数据的提取
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
本学习笔记以下面的方式组织:编程前期分析、代码与注释和重要代码解析三部分。
要实现目标:通过微软的SDK提取不带游戏者ID的深度数据并用OpenCV显示
一、编程前期分析
深度数据的获取和彩色图像数据的获取基本上是一样的,所以关于采集过程就不赘述了,具体见:
Kinect开发学习笔记之(四)提取颜色数据并用OpenCV显示
http://blog.csdn.net/zouxy09/article/details/8146266
这里需要了解下深度数据:
深度数据流所提供的图像帧中,每一个像素点代表的是在深度感应器的视野中,该特定的(x, y)坐标处物体到离摄像头平面最近的物体到该平面的距离(以毫米为单位)。
Kinect中深度值最大为4096mm,0值通常表示深度值不能确定,一般应该将0值过滤掉。微软建议在开发中使用1220mm~3810mm范围内的值。在进行其他深度图像处理之前,应该使用阈值方法过滤深度数据至1220mm-3810mm这一范围内。
下图显示了Kinect Sensor的感知范围,其中的default range对Xbox 360和Kinect for Windows都适用,而near range仅对后者适用:
深度数据的存储:
Kinect的深度图像数据含有两种格式,两种格式都是用两个字节来保存一个像素的深度值,而两方式的差别在于:
(1)唯一表示深度值:那么像素的低12位表示一个深度值,高4位未使用;
(2)既表示深度值又含有游戏者ID:Kinect SDK具有分析深度数据和探测人体或者游戏者轮廓的功能,它一次能够识别多达6个游戏者。SDK为每一个追踪到的游戏者编号作为索引。而这个方式中,像素值的高13位保存了深度值,低三位保存用户序号,7 (0000 0111)这个位掩码能够帮助我们从深度数据中获取到游戏者索引值(这个编程将在下一节)。
应用程序可以使用深度数据流中的深度数据来支持各种各样的用户特性,如追踪用户的运动并在程序中识别和忽略背景物体的信息等。
二、代码与注释
#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(240, 320, CV_8UC1); //1、初始化NUI,注意:这里传入的参数就不一样了,是DEPTH HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH); if (FAILED(hr)) { cout<<"NuiInitialize failed"<<endl; return hr; } //2、定义事件句柄 //创建读取下一帧的信号事件句柄,控制KINECT是否可以开始读取下一帧数据 HANDLE nextColorFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL ); HANDLE depthStreamHandle = NULL; //保存图像数据流的句柄,用以提取数据 //3、打开KINECT设备的深度图信息通道,并用depthStreamHandle保存该流的句柄,以便于以后读取 hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_DEPTH, NUI_IMAGE_RESOLUTION_320x240, 0, 2, nextColorFrameEvent, &depthStreamHandle); if( FAILED( hr ) )//判断是否提取正确 { cout<<"Could not open color image stream video"<<endl; NuiShutdown(); return hr; } namedWindow("depthImage", CV_WINDOW_AUTOSIZE); //4、开始读取深度数据 while(1) { const NUI_IMAGE_FRAME * pImageFrame = NULL; //4.1、无限等待新的数据,等到后返回 if (WaitForSingleObject(nextColorFrameEvent, INFINITE)==0) { //4.2、从刚才打开数据流的流句柄中得到该帧数据,读取到的数据地址存于pImageFrame hr = NuiImageStreamGetNextFrame(depthStreamHandle, 0, &pImageFrame); if (FAILED(hr)) { cout<<"Could not get depth 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行的指针 //深度图像数据含有两种格式,这里像素的低12位表示一个深度值,高4位未使用; //注意这里需要转换,因为每个数据是2个字节,存储的同上面的颜色信息不一样, uchar *pBufferRun = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch; USHORT * pBuffer = (USHORT*) pBufferRun; for (int j=0; j<image.cols; j++) { ptr[j] = 255 - (uchar)(256 * pBuffer[j]/0x0fff); //直接将数据归一化处理 } } imshow("depthImage", image); //显示图像 } else { cout<<"Buffer length of received texture is bogus\r\n"<<endl; } //5、这帧已经处理完了,所以将其解锁 pTexture->UnlockRect(0); //6、释放本帧数据,准备迎接下一帧 NuiImageStreamReleaseFrame(depthStreamHandle, pImageFrame ); } if (cvWaitKey(20) == 27) break; } //7、关闭NUI链接 NuiShutdown(); return 0; }
三、代码解析
首先,这里基本上和彩色图像数据的获取的流程和API都是一样的,只有有几个不同点:
(1) 因为我们需要的是深度数据,所以在NuiInitialize(DWORD dwFlags);初始化时,应告知Kinect我要的是深度数据:NUI_INITIALIZE_FLAG_USES_DEPTH;
(2)我们需要打开的是Kinect设备的深度数据流,所以调用NuiImageStreamOpen传入了类型是NUI_IMAGE_TYPE_DEPTH, 另外,深度图像的分辨率一般用NUI_IMAGE_RESOLUTION_320x240。
(3)另外,Kinect的深度图像数据含有两种格式,其一是唯一表示深度值:那么像素的低12位表示一个深度值,高4位未使用;其二是既表示深度值又含有游戏者ID,则像素值的高13位保存了深度值,低三位保存用户序号,其中序号为0则表示无用户,1和2分别表示两个不同的用户(这个会在下一文中编程)。
所以这里深度数据转换为OpenCV的Mat数据类型就和彩色的不一样了。在显示中,我们通过一个单通道的灰度图像来描述深度图像,像素点越白表示场景中目标里摄像头越近。
转换时候有一个地方需要特别注意的,代码里面用的是:
uchar *pBufferRun = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch;
USHORT * pBuffer = (USHORT*) pBufferRun;
那为什么不直接下面这样呢:
USHORT * pBuffer = (USHORT*) (LockedRect.pBits) + i * LockedRect.Pitch;
因为每个深度数据是2个字节,存储的同上面的颜色信息不一样,而pitch是以字节为单位的,然后地址的偏移是按LockedRect.pBits的地址类型来偏移的。也就是说假如按第二种方式来编写,那么当i等于图像的中间那一行的时候,pBuffer指针已经指向图像的最后一行了(因为ushort是两个字节,每偏移一个i,地址就偏移两个字节),这时候i再增加,就会导致数组访问越界了。导致的结果是,我们的深度图像显示都一半的时候,程序就死掉了。
至此,目标达成。