【Kinect】2nd-深度数据的提取代码分析

一、不带游戏者ID的深度数据的提取

Kinect开发学习笔记之(五)不带游戏者ID的深度数据的提取

深度数据流所提供的图像帧中,每一个像素点代表的是在深度感应器的视野中,该特定的(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)这个位掩码能够帮助我们从深度数据中获取到游戏者索引值(这个编程将在下一节)。

应用程序可以使用深度数据流中的深度数据来支持各种各样的用户特性,如追踪用户的运动并在程序中识别和忽略背景物体的信息等。

要实现目标:通过微软的SDK提取不带游戏者ID的深度数据并用OpenCV显示

#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再增加,就会导致数组访问越界了。导致的结果是,我们的深度图像显示都一半的时候,程序就死掉了。


关于ptr[j] = 255 - (uchar)(256 * pBuffer[j]/0x0fff):
我们获取到的是两个字节的深度数据,然后这两个字节数据的低12位表示一个深度值,高四位为空。我们需要用一个灰度图来显示这个深度信息,所以就需要将深度值归一化到[0, 255]的范围。255再减去它就相当于,距离越近,深度值越小的时候,灰度越大


二、带游戏者ID的深度数据的提取

Kinect开发学习笔记之(六)带游戏者ID的深度数据的提取

首先,Kinect传感器核心是发射红外结构光,并探测红外光反射,从而可以计算出视场范围内每一个像素的深度值。从深度数据中最先提取出来的是物体主体和形状,以及每一个像素点的游戏者索引信息。然后用这些形状信息来匹配人体的各个部分,最后计算匹配出来的各个关节在人体中的位置。而Kinect具有一次识别多达6个游戏者的能力,并能跟踪最多两个人的骨骼。
Kinect的深度图像数据有两种格式,一种是带游戏者ID的,一种是不带的。两种格式都是用两个字节来保存一个像素的深度值,而两方式的差别在于:

(1)唯一表示深度值:那么像素的低12位表示一个深度值,高4位未使用;

(2)既表示深度值又含有游戏者ID:Kinect为每一个追踪到的游戏者编号作为索引。而这个方式中,像素值的高13位保存了深度值,低三位保存用户序号,7
(0000 0111)这个位掩码能够帮助我们从深度数据中获取到游戏者索引值。

要注意的是,不要对特定的游戏者索引位进行编码,因为他们是会变化的。实际的游戏者索引位并不总是和Kinect前面的游戏者编号一致。啥意思呢?例如,Kinect视野中只有一个游戏者,但是返回的游戏者索引位值可能是3或者4。也就是说有时候第一个游戏者的游戏者索引位可能不是1。还有,如果走进Kinect视野再走出去,然后再走进来,虽然你还是你,但是Kinect给你的索引ID可能就和原来的不一样了,例如之前返回的索引位是1,走出去后再次走进,可能索引位变为其他值了。所以开发Kinect应用程序的时候应该注意到这一点。

#include <windows.h>
#include <iostream> 
#include <NuiApi.h>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

//处理深度数据的每一个像素,如果属于同一个用户的ID,那么像素就标为同种颜色,不同的用户,
//其ID不一样,颜色的标示也不一样,如果不属于某个用户的像素,那么就采用原来的深度值
RGBQUAD shortDepth2RGBquad( USHORT depthID )
{ 
    //每像素共16bit的信息,其中最低3位是ID(所捕捉到的人的ID),剩下的13位才是信息
    USHORT realDepth = (depthID & 0xfff8) >> 3; //提取距离信息,高13位 
    USHORT player =  depthID & 0x07 ;  //提取ID信息 ,低3位

    //因为提取的信息是距离信息,为了便于显示,这里归一化为0-255
    BYTE depth = 255 - (BYTE)(256*realDepth/0x0fff); 

    RGBQUAD q; 
    q.rgbRed = q.rgbBlue = q.rgbGreen = 0; 

    //RGB三个通道的值都是相等的话,就是灰度的
    //Kinect系统能够处理辨识传感器前多至6个人物的信息,但同一时刻最多只有2个玩家可被追踪(即骨骼跟踪)
    switch( player ) 
    { 
        case 0:  
            q.rgbRed = depth / 2; 
            q.rgbBlue = depth / 2; 
            q.rgbGreen = depth / 2; 
            break; 
        case 1: 
            q.rgbRed = depth; 
            break; 
        case 2: 
            q.rgbGreen = depth;  
            break; 
        case 3: 
            q.rgbRed = depth / 4; 
            q.rgbGreen = depth; 
            q.rgbBlue = depth; 
            break; 
        case 4: 
            q.rgbRed = depth; 
            q.rgbGreen = depth; 
            q.rgbBlue = depth / 4; 
            break; 
        case 5: 
            q.rgbRed = depth; 
            q.rgbGreen = depth / 4; 
            q.rgbBlue = depth; 
            break; 
        case 6: 
            q.rgbRed = depth / 2; 
            q.rgbGreen = depth / 2; 
            q.rgbBlue = depth; 
            break; 
        case 7: 
            q.rgbRed = 255 - ( depth / 2 ); 
            q.rgbGreen = 255 - ( depth / 2 ); 
            q.rgbBlue = 255 - ( depth / 2 ); 
        } 

    return q; 
}

int main(int argc, char *argv[])
{
    Mat image;
    image.create(240, 320, CV_8UC3); 

    //1、初始化NUI,注意这里是DEPTH_AND_PLAYER_INDEX
    HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX); 
    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_AND_PLAYER_INDEX, 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行的指针

                    //其二是既表示深度值又含有人物序号,则像素值的高13位保存了深度值,低三位保存用户序号,
                    //注意这里需要转换,因为每个数据是2个字节,存储的同上面的颜色信息不一样,
                    uchar *pBufferRun = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch;
                    USHORT * pBuffer = (USHORT*) pBufferRun;

                    for (int j=0; j<image.cols; j++) 
                    {
                        //对于每一个像素,我们通过它的深度数据去修改它的RGB值;
                        RGBQUAD rgb = shortDepth2RGBquad(pBuffer[j]);
                        ptr[3*j] = rgb.rgbBlue; 
                        ptr[3*j+1] = rgb.rgbGreen; 
                        ptr[3*j+2] = rgb.rgbRed; 
                    } 
                } 
                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)初始化和打开深度数据流的时候传入的参数是不同的,这个需要注意下,我们需要的是DEPTH_AND_PLAYER_INDEX数据;

(2)每个像素的深度数据由两个字节来保存,高13位保存了深度值,低三位保存用户序号。

(3)具体显示的时候我们是这样处理的:
对于每一个像素,我们通过它的深度数据去修改它的RGB值,如果属于同一个用户的ID,那么像素就标为同种颜色,不同的用户,其ID不一样,颜色的标示也不一样,如果不属于某个用户的像素,那么就采用原来的深度值。

首先,这里涉及到了:
USHORTrealDepth = (depthID & 0xfff8) >> 3; //提取距离信息,高13位
USHORTplayer = depthID & 0x07 ; //提取ID信息,低3位

然后RGBQUAD是一个结构体,其保存一个像素点的RGB值,定义如下:
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;


OpenCV学习笔记(20)Kinect + OpenNI + OpenCV + OpenGL 组合体验
RGBD物体识别(4)–使用opencv3读取kinect数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值