前面试着提取了Kinect的彩色数据:Kinect学习(三):获取RGB颜色数据。这次,要试着提取深度数据。
Depth Map(深度图)是包含与视点的场景对象的表面的距离有关的信息的图像或图像通道。其中,Depth Map 类似于灰度图像,只是它的每个像素值是传感器距离物体的实际距离。通常RGB图像和Depth图像是配准的,因而像素点之间具有一对一的对应关系。
先上代码。
#include <Windows.h> #include <iostream> #include <NuiApi.h> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main(int argc, char *argv[]) { cv::Mat img; // 深度图,使用灰度值来表示深度数据,越远灰度越小则越暗 img.create(480, 640, CV_8UC1); // 1、初始化NUI,使用深度数据 HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH); if (FAILED(hr)) { cout << "NuiIntialize failed" << endl; return hr; } // 2、定义事件句柄 // 创建读取下一帧的信号事件句柄,用来控制Kinect是否可以开始读取下一帧数据 HANDLE nextDepthFrameEvent = CreateEvent(NULL, TRUE, FALSE, NULL); HANDLE depthStreamHandle = NULL; // 用来保存图像数据流的句柄,用于提取数据 // 3、打开Kinect设备的深度图数据通道,使用depthStreamHandle保存该数据流的句柄,以便于后续读取 hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_DEPTH, NUI_IMAGE_RESOLUTION_640x480, 0, 2, nextDepthFrameEvent, &depthStreamHandle); if (FAILED(hr)) { cout << "Could not open color image stream video" << endl; NuiShutdown(); return hr; } cv::namedWindow("depthImage", CV_WINDOW_AUTOSIZE); // 4、开始读取深度数据 while (1) { const NUI_IMAGE_FRAME * pImageFrame = NULL; // 4.1、无限等待新的数据,等到后就返回 if (WaitForSingleObject(nextDepthFrameEvent, INFINITE) == 0) { // 4.2、从刚才打开数据流的流句柄中得到该帧的数据,读取到的数据地址存在pImageFrame hr = NuiImageStreamGetNextFrame(depthStreamHandle, 0, &pImageFrame); if (FAILED(hr)) { cout << "Could not get depth image" << endl; NuiShutdown(); return hr; } 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); //深度图像数据含有两种格式,这里像素的低12位表示一个深度值,高4位未使用; //注意这里需要转换,因为每个数据是2个字节,存储的同上面的颜色信息不一样, uchar *pBufferRun = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch; USHORT * pBuffer = (USHORT*)pBufferRun; for (int j = 0;j < img.cols;j++) { // ptr[j] = 255 - (uchar)(255 * pBuffer[j] / 0x0fff); //直接将数据归一化处理 ptr[j] = (uchar)(255 * pBuffer[j] / 0x0fff); //直接将数据归一化处理 } } cv::imshow("depthImage", img); } else { cout << "Buffer length of received texture is bogus\r\n" << endl; } // 5、这帧已经处理完了,将其解锁,更新下一帧数据 pTexture->UnlockRect(0); // 6、释放这一阵数据,准备接受下一帧 NuiImageStreamReleaseFrame(depthStreamHandle, pImageFrame); } if (cv::waitKey(20) == 27) { break; } } // 7、关闭NUI连接 NuiShutdown(); return 0; }
基本的程序流程都与获取彩色图像数据的流程一样,下面主要介绍不同点,类似的地方就不详细说明了。
- 初始化NUI接口;
- 定义事件句柄;
- 打开Kinect设备的数据流(深度数据);
- 等待数据更新,若更新完成则进行下一步;
- 从数据流中拿出图像数据;
- 提取数据帧并锁定数据;
- 将数据转换为OpenCV的Mat格式。
// 1、初始化NUI,使用深度数据 HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH); if (FAILED(hr)) { cout << "NuiIntialize failed" << endl; return hr; }
对NUI接口初始化,注意初始化的参数是NUI_INITIALIZE_FLAG_USES_DEPTH
,代表要使用深度数据。
// 2、定义事件句柄 // 创建读取下一帧的信号事件句柄,用来控制Kinect是否可以开始读取下一帧数据 HANDLE nextDepthFrameEvent= CreateEvent(NULL, TRUE, FALSE, NULL);
创建事件句柄,nextDepthFrameEvent
对应信号事件,在打开Kinect设备的数据流之后,如果有信号(数据),则WaitForSingleObject(nextDepthFrameEvent, INFINITE)
返回0。
// 3、打开Kinect设备的深度图数据通道,使用depthStreamHandle保存该数据流的句柄,以便于后续读取 hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_DEPTH, NUI_IMAGE_RESOLUTION_640x480, 0, 2, nextDepthFrameEvent, &depthStreamHandle); if (FAILED(hr)) { cout << "Could not open color image stream video" << endl; NuiShutdown(); return hr; }
打开深度图的数据流,注意是:NUI_IMAGE_TYPE_DEPTH
。
// 4.1、无限等待新的数据,等到后就返回 if (WaitForSingleObject(nextDepthFrameEvent, INFINITE) == 0) { ... }
访问前面定义的信号事件:nextDepthFrameEvent
,如果有数据则程序往后走,没有数据,等待。
// 4.2、从刚才打开数据流的流句柄中得到该帧的数据,读取到的数据地址存在pImageFrame hr = NuiImageStreamGetNextFrame(depthStreamHandle, 0, &pImageFrame); if (FAILED(hr)) { cout << "Could not get depth image" << endl; NuiShutdown(); return hr; }
从数据流depthStreamHandle
中取出深度图数据,保存在pImageFrame
中。
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
,第一个字节地址。
//4.5、将数据转换为OpenCV的Mat格式 for (int i = 0;i < img.rows;i++) { uchar * ptr = img.ptr<uchar>(i); //深度图像数据含有两种格式,这里像素的低12位表示一个深度值,高4位未使用; //注意这里需要转换,因为每个数据是2个字节,存储的同上面的颜色信息不一样, uchar *pBufferRun = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch; USHORT * pBuffer = (USHORT*)pBufferRun; for (int j = 0;j < img.cols;j++) { // ptr[j] = 255 - (uchar)(255 * pBuffer[j] / 0x0fff); //直接将数据归一化处理 ptr[j] = (uchar)(255 * pBuffer[j] / 0x0fff); //直接将数据归一化处理 } }
把LockedRect
中的数据取出来,保存为OpenCV支持的Mat格式。
深度数据是用16位数据的前12位表示,这里赋值时直接做了归一化,调整到了0-255。