第三篇 KinectV2骨骼获取原理和获取方法及源代码

发布于 2019-09-26 作者 风铃 255次 浏览 版块 前端

首先声明一下,本系统所使用的开发环境版本是计算机系统Windows 10、Visual Studio 2013、Opencv3.0和Kinect SDK v2.0。这些都可以在百度上找到,download下来安装一下即可。



通过上一篇文章,我们已经基本学会如何获取Kinect 的各种数据源,对于骨骼数据的获取是稍微复杂的那一个,将在本篇文章详细讲解和深度剖析Kinect v2 获取骨骼数据的原理,最后将利用骨骼数据做一个好玩的东西。废话少说,我们开始吧。


 


一、骨骼跟踪的原理


1.骨骼图是由深度图获取得到的


(1)Kinect v1生成3D深度的图像的原理


   采用的是PrimeSence公司Light Coding技术。Light Coding技术理论是利用连续光(近红外线)对测量空间进行编码,经感应器读取编码的光线,交由晶片运算进行解码后,产生成一张具有深度的图像。 Light Coding技术的关键是Laser Speckle雷射光散斑,当雷射光照射到粗糙物体、或是穿透毛玻璃后,会形成随机的反射斑点,称之为散斑。散斑具有高度随机性,也会随着距离而变换图案,空间中任何两处的散斑都会是不同的图案,等于是将整个空间加上了标记,所以任何物体进入该空间、以及移动时,都可确切纪录物体的位置。Light Coding发出雷射光对测量空间进行编码,就是指产生散斑。Kinect就是以红外线发出人眼看不见的class 1雷射光,透过镜头前的diffuser(光栅、扩散片)将雷射光均匀分布投射在测量空间中,再透过红外线摄影机记录下空间中的每个散斑,撷取原始资料后,再透过晶片计算成具有3D深度的图像。 


 


(2)Kinect v2则采用了更为先进的TOF技术。红外发射器主动投射经调制的近红外光线,红外光线照到视野里的物体上就会发生反射,红外相机接收反射回来的红外线,采用TOF技术测量深度,计算光的时间差(通常是通过相位差来计算的),根据,可得物体的深度(即物体到深度相机的距离)。


 


2、Kinect骨骼跟踪的原理


  了解Kinect如何获得影像后,接下来就是进行辨识的工作。透过Light Coding技术所获得的只是基本的影像资料,重点还是要辨识影像,转换为动作指令。


 


   微软将侦测到的3D深度图像,转换到骨架追踪系统。该系统最多可同时侦测到6个人,包含同时辨识2个人的动作;每个人共可记录20组细节,包含躯干、四肢以及手指等都是追踪的范围,达成全身体感操作。为了看懂使用者的动作,微软也用上机器学习技术(machine learning),建立出庞大的图像资料库,形成智慧辨识能力,尽可能理解使用者的肢体动作所代表的涵义。


下面更加详细的来探讨一下骨骼跟踪的原理:


Kinect骨骼跟踪不受周围光照的影响,主要是因为红外信息,产生3D深度图像,上文已经介绍。


另外,Kinect采用分隔策略将人体从复杂的背景中区分出来,在这个阶段,为每个跟踪的人在深度图像中创建所谓的分割遮罩(分割遮罩为了排除人体以外背景图像,采取的图像分割的方法),如图1这是一个将背景图像(比如椅子和宠物等)剔除后的景深图像。在后面的处理流程中仅仅转送人体图像即可,以减轻体感计算量。







                    图1 剔除背景后的景深图像


Kinect需做的下一件事情就是寻找图像中较可能是人体的物体,接下来kinect会对景深图像(机器学习)进行评估,来判别人体的不同部位。


在识别人体的各部位之前,微软是通过开发的一个人工智能(被称为Exemplar(模型)系统),数以TB计的数据输入到集群系统训练模型,图2就是用来训练和测试Exemplar的数据之一。







                   图2  测试和训练数据


训练分类器的分方法,提出的是一种含有许多深度特征的分类器,来识别物体,该特征虽然简单却包含必要的信息,来确定身体的部位,其公式如(1)所示:


···········(1)


其中x是像素值,d1(x)是像素值在图像I中的深度值,参数θ=(u,v),u和v是一对偏移向量(怎么理解?),1/d1(x)是偏移正规化,用来处理人体尺寸的缩放,这是一个非常简单的特征,也就是简化目标像素u和v值这两个像素深度偏移的不同。很显然,这些特征测量与像素周围的区域的3D外形相关,这足以说明手臂和腿之间的区别。如图3所示,其中十字架代表被像素被分的类别,而圆圈表示公式(1)计算出的偏移像素。若偏移像素是背景,d1(x)深度值将会是正无穷大。 

 






a、两个具有较大响应的特征 






 

b、两个具有较小响应值的特征 

图3 深度图像特征


接下来训练一个决策树分类器。决策树森林即众多决策树的集合,每棵树用一组预先标签的身体部位的深度图像来训练,决策树被修改更新,知道决策树为特定的身体部位上的测试集的图像给出了正确的分类。用100w幅图像训练3颗数,利用GPU加速,在1000个核的集群去分析。根据微软实验,大概耗时一天。这些训练过的分类器指定每一个像素在每一个身体部分的可能性。下一个阶段的算法简单的为每一个身体部位挑选最大几率的区域。因此,如果“手臂”分类器是最大的几率,这个区域则被分配到“手臂”类别。最后一个阶段是计算分类器建议的关节位置(节点)相对位置作为特别的身体部位,如图4所示。


 




图4 Kinect 人体节点识别过程


另外,只要有大字形的物体,Kinect都会努力去追踪,如图5所示。当然,这个物体也必须是接近人体的大小比例,尺寸小的玩具是无法识别的。


 




图5 骨骼跟踪结果


在Kinect前放一个没有体温的塑料人体模特,或者一件挂着衬衣的衣架,Kinect会认为那是一个静止的人。红外传感器所能捕捉的只是一个人体轮廓。


模型匹配:生成骨架的系统


处理流程的最后一步是使用之前阶段输出的结果,根据追踪到的20个关节点来生成一幅骨架系统。Kinect会评估Exemplar输出的每一个可能的像素来确定关节点。通过这种方式Kinect能够基于充分的信息最准确地评估人体实际所处位置。另外模型匹配阶段还做了一些附加输出滤镜来平滑输出以及处理闭塞关节等特殊事件。


参考资料:


[1] Shotton J, Sharp T, Kipman A, et al. Real-time human pose recognition in parts from single depth images[J]. Communications of the ACM, 2013, 56(1): 116-124.


[2] http://blog.csdn.net/u014365862/article/details/46849309


 


 


二、骨骼数据获取方法


还是老规矩,先上代码后面分析



#include <iostream>
#include <opencv2imgproc.hpp> //opencv头文件
#include <opencv2calib3d.hpp>
#include <opencv2highgui.hpp>
#include <Kinect.h> //Kinect头文件

using namespace std;
using namespace cv;

void draw(Mat & img, Joint & r_1, Joint & r_2, ICoordinateMapper * myMapper);
int main(void)
{
IKinectSensor * mySensor = nullptr;
GetDefaultKinectSensor(&mySensor);
mySensor->Open();

IColorFrameSource   * myColorSource = nullptr;
    mySensor-&gt;get_ColorFrameSource(&amp;myColorSource);
    
    IColorFrameReader   * myColorReader = nullptr;
    myColorSource-&gt;OpenReader(&amp;myColorReader);
    
    int colorHeight = 0, colorWidth = 0;
    IFrameDescription   * myDescription = nullptr;
    myColorSource-&gt;get_FrameDescription(&amp;myDescription);
    myDescription-&gt;get_Height(&amp;colorHeight);
    myDescription-&gt;get_Width(&amp;colorWidth);
    
    IColorFrame * myColorFrame = nullptr;
    Mat original(colorHeight, colorWidth, CV_8UC4);
    
    //**********************以上为ColorFrame的读取前准备**************************
    
    IBodyFrameSource    * myBodySource = nullptr;
    mySensor-&gt;get_BodyFrameSource(&amp;myBodySource);
    
    IBodyFrameReader    * myBodyReader = nullptr;
    myBodySource-&gt;OpenReader(&amp;myBodyReader);
    
    int myBodyCount = 0;
    myBodySource-&gt;get_BodyCount(&amp;myBodyCount);
    
    IBodyFrame  * myBodyFrame = nullptr;
    
    ICoordinateMapper   * myMapper = nullptr;
    mySensor-&gt;get_CoordinateMapper(&amp;myMapper);
    
    //**********************以上为BodyFrame以及Mapper的准备***********************
    while (1)
    {
    
        while (myColorReader-&gt;AcquireLatestFrame(&amp;myColorFrame) != S_OK);
        myColorFrame-&gt;CopyConvertedFrameDataToArray(colorHeight * colorWidth * 4, original.data, ColorImageFormat_Bgra);
        Mat copy = original.clone();        //读取彩色图像并输出到矩阵
    
        while (myBodyReader-&gt;AcquireLatestFrame(&amp;myBodyFrame) != S_OK); //读取身体图像
        IBody   **  myBodyArr = new IBody *[myBodyCount];       //为存身体数据的数组做准备
        for (int i = 0; i &lt; myBodyCount; i++)
            myBodyArr[i] = nullptr;
    
        if (myBodyFrame-&gt;GetAndRefreshBodyData(myBodyCount, myBodyArr) == S_OK)     //把身体数据输入数组
            for (int i = 0; i &lt; myBodyCount; i++)
            {
                BOOLEAN     result = false;
                if (myBodyArr[i]-&gt;get_IsTracked(&amp;result) == S_OK &amp;&amp; result) //先判断是否侦测到
                {
                    Joint   myJointArr[JointType_Count];
                    if (myBodyArr[i]-&gt;GetJoints(JointType_Count, myJointArr) == S_OK)   //如果侦测到就把关节数据输入到数组并画图
                    {
                        draw(copy, myJointArr[JointType_Head], myJointArr[JointType_Neck], myMapper);
                        draw(copy, myJointArr[JointType_Neck], myJointArr[JointType_SpineShoulder], myMapper);
    
                        draw(copy, myJointArr[JointType_SpineShoulder], myJointArr[JointType_ShoulderLeft], myMapper);
                        draw(copy, myJointArr[JointType_SpineShoulder], myJointArr[JointType_SpineMid], myMapper);
                        draw(copy, myJointArr[JointType_SpineShoulder], myJointArr[JointType_ShoulderRight], myMapper);
    
                        draw(copy, myJointArr[JointType_ShoulderLeft], myJointArr[JointType_ElbowLeft], myMapper);
                        draw(copy, myJointArr[JointType_SpineMid], myJointArr[JointType_SpineBase], myMapper);
                        draw(copy, myJointArr[JointType_ShoulderRight], myJointArr[JointType_ElbowRight], myMapper);
    
                        draw(copy, myJointArr[JointType_ElbowLeft], myJointArr[JointType_WristLeft], myMapper);
                        draw(copy, myJointArr[JointType_SpineBase], myJointArr[JointType_HipLeft], myMapper);
                        draw(copy, myJointArr[JointType_SpineBase], myJointArr[JointType_HipRight], myMapper);
                        draw(copy, myJointArr[JointType_ElbowRight], myJointArr[JointType_WristRight], myMapper);
    
                        draw(copy, myJointArr[JointType_WristLeft], myJointArr[JointType_ThumbLeft], myMapper);
                        draw(copy, myJointArr[JointType_WristLeft], myJointArr[JointType_HandLeft], myMapper);
                        draw(copy, myJointArr[JointType_HipLeft], myJointArr[JointType_KneeLeft], myMapper);
                        draw(copy, myJointArr[JointType_HipRight], myJointArr[JointType_KneeRight], myMapper);
                        draw(copy, myJointArr[JointType_WristRight], myJointArr[JointType_ThumbRight], myMapper);
                        draw(copy, myJointArr[JointType_WristRight], myJointArr[JointType_HandRight], myMapper);
    
                        draw(copy, myJointArr[JointType_HandLeft], myJointArr[JointType_HandTipLeft], myMapper);
                        draw(copy, myJointArr[JointType_KneeLeft], myJointArr[JointType_FootLeft], myMapper);
                        draw(copy, myJointArr[JointType_KneeRight], myJointArr[JointType_FootRight], myMapper);
                        draw(copy, myJointArr[JointType_HandRight], myJointArr[JointType_HandTipRight], myMapper);
                    }
                }
            }
        delete[]myBodyArr;
        myBodyFrame-&gt;Release();
        myColorFrame-&gt;Release();
    
        imshow(&#34;TEST&#34;, copy);
        if (waitKey(30) == VK_ESCAPE)
            break;
    }
    myMapper-&gt;Release();
    
    myDescription-&gt;Release();
    myColorReader-&gt;Release();
    myColorSource-&gt;Release();
    
    myBodyReader-&gt;Release();
    myBodySource-&gt;Release();
    mySensor-&gt;Close();
    mySensor-&gt;Release();
    
    return  0;
    

}

void draw(Mat & img, Joint & r_1, Joint & r_2, ICoordinateMapper * myMapper)
{
//用两个关节点来做线段的两端,并且进行状态过滤
if (r_1.TrackingState == TrackingState_Tracked && r_2.TrackingState == TrackingState_Tracked)
{
ColorSpacePoint t_point; //要把关节点用的摄像机坐标下的点转换成彩色空间的点
Point p_1, p_2;
myMapper->MapCameraPointToColorSpace(r_1.Position, &t_point);
p_1.x = t_point.X;
p_1.y = t_point.Y;
myMapper->MapCameraPointToColorSpace(r_2.Position, &t_point);
p_2.x = t_point.X;
p_2.y = t_point.Y;

    line(img, p_1, p_2, Vec3b(0, 255, 0), 5);
        circle(img, p_1, 10, Vec3b(255, 0, 0), -1);
        circle(img, p_2, 10, Vec3b(255, 0, 0), -1);
    }
    

}




 


注意:同上一节一样,为了方便起见,此段代码略掉了大部分错误和异常检测,只是为了做个示例获取Kinect的骨骼图像。


可以看到,大部分代码同上一节是一样的。但由于骨骼部分要更麻烦一点,Kinect对于骨骼点的处理,最多只能同时处理6*25个骨骼点,6个人,每个人的25的骨骼点。在写程序的时候,需要对每个人的每个骨骼点进行循环访问就可以了,每个骨骼点有三种状态,跟踪到的,推测的,没有跟踪到的。把骨骼点用opencv里的画实心点(circle函数)表示,骨骼用线段(line函数)表示,可以看到一幅近似人体模型的骨架。在演示程序里头把人体骨架画在Kinect获取的彩色图像上,可以看到,骨骼点的位置和彩色图像的真实位置非常接近,也就是说骨骼点的三维坐标还是比较准确的,得到25个骨骼点的空间三维坐标,就可以用来做一些体感交互的东西了,可玩度比较大。在下一章我将举一个例子,采用Kinect获取的骨骼点数据做一个抠图和体感拍照的小作品。如图所示


 


 


好了,本篇文章到这里就要结束了。通过本篇文章,主要可以掌握Kinect v2最强大的功能–获取人体骨骼数据,骨骼数据的获取原理和获取方法、代码一一详细介绍。下面附上更加完整和规范的源码下载地址,0积分下载。


http://download.csdn.net/detail/baolinq/9616276


下一篇见。


以一幅美图来结束本篇文章。豪车真养眼。


 


收藏
暂无回复