计算机视觉
图像处理

Kinect开发学习笔记之(七)骨骼数据的提取

我的Kinect开发平台是:
Win7 x86 + VS2010 + Kinect for Windows SDK v1.6 + OpenCV2.3.0
开发环境的搭建见上一文:
http://www.cvvision.cn/1792.html
本学习笔记以下面的方式组织:编程前期分析、代码与注释和重要代码解析三部分。
要实现目标:通过微软的SDK提取骨骼数据并用OpenCV显示


一、编程前期分析
Kinect产生的深度数据作用有限,要利用Kinect创建真正意义上交互,还需要除了深度数据之外的其他数据。这就是骨骼追踪技术的初衷,也是Kinect最神奇,最有作为的地方。骨骼追踪技术通过处理深度数据来建立人体各个关节的坐标,骨骼追踪能够确定人体的各个部分,如那部分是手,头部,以及身体,还能确定他们所在的位置。
 
1.1、骨架空间
先看看啥叫骨架?应该地球人都知道吧。呵呵。在Kinect里面,是通过20个关节点来表示一个骨架的,具体由下图可以看到。当你走进Kinect的视 野范围的时候,Kinect就可以把你的20个关节点的位置找到(当然你得站着),位置通过(x, y, z)坐标来表示。这样,你在Kinect前面做很多复杂的动作的时候,因为人的动作和这些关节点的位置的变化关系还是很大的,那么电脑拿到这些数据后,对 于理解你做什么动作就很有帮助了。


玩家的各关节点位置用(x, y, z)坐标表示。与深度图像空间坐标不同的是,这些坐标单位是米。坐标轴x,y, z是深度感应器实体的空间x, y, z坐标轴。这个坐标系是右手螺旋的,Kinect感应器处于原点上,z坐标轴则与Kinect感应的朝向一致。y轴正半轴向上延伸,x轴正半轴(从 Kinect感应器的视角来看)向左延伸,如下图所示。为了方便讨论,我们称这些坐标的表述为骨架空间(坐标)。


Kinect放置的位置会影响生成的图像。例如,Kinect可能被放置在非水平的表面上或者有可能在垂直方向上进行了旋转调整来优化视野范围。在这种 情况下,y轴就往往不是相对地面垂直的,或者不与重力方向平行。最终得到的图像中,尽管人笔直地站立,在图像中也会显示出事倾斜的。

1.2、骨骼跟踪

Kinect最多可以跟踪两个骨骼,可以最多检测六个人。站立模式可以跟踪20个关节点,坐着的模式的话,可以跟踪10个关节点。
NUI骨骼跟踪分主动和被动两种模式,提供最多两副完整的骨骼跟踪数据。主动模式下需要调用相关帧读取函数获得用户骨骼数据,而被动模式下还支持额外最 多四人的骨骼跟踪,但是在该模式下仅包含了用户的位置信息,不包括详细的骨骼数据。也就是说,假如Kinect面前站着六个人,Kinect能告诉你这六 个人具体站在什么位置,但只能提供其中两个人的关节点的数据(这两个人属于主动模式),也就是他们的手啊,头啊等等的位置都能告诉你,而其他的 人,Kinect只能提供位置信息,也就是你站在哪,Kinect告诉你,但是你的手啊,头啊等具体在什么位置,它就没法告诉你了(这四个人属于被动模 式)。
对于所有获取的骨骼数据,其至少包含以下信息:
1)、相关骨骼的跟踪状态,被动模式时仅包括位置数据(用户所在位置),主动模式包括完整的骨骼数据(用户20个关节点的空间位置信息)。
2)、唯一的骨骼跟踪ID,用于分配给视野中的每个用户(和之前说的深度数据中的ID是一个东西,用以区分现在这个骨骼数据是哪个用户的)。
3)、用户质心位置,该值仅在被动模式下可用(就是标示用户所在位置的)。

1.3、关于编程处理过程
在上篇文章中,我们讨论了如何获取像素点的深度值以及如何根据深度值产生影像。
彩色图像数据,深度数据分别来自ColorImageSteam和DepthImageStream,同样地,骨骼数据来自 SkeletonStream。要使用骨架数据,应用程序必须在初始化NUI的时候声明,并且要启用骨架追踪。访问骨骼数据和访问彩色图像数据、深度数据 一样,也有事件模式和查询模式两种方式。在本例中我们采用基于事件的方式,因为这种方式简单,代码量少,并且是一种很普通基本的方法。当 SkeletonStream中有新的骨骼数据产生时就会触发该事件。
我们初始化并打开骨骼跟踪后,就可以从SkeletonStream中拿骨骼数据了。SkeletonStream产生的每一帧数据 skeletonFrame都是一个骨骼对象集合。包含了一个骨架数据结构的数组,其中每一个元素代表着一个被骨架追踪系统所识别的一个骨架信息。每一个 骨架信息包含有描述骨骼位置以及骨骼关节的数据。每一个关节有一个唯一标示符如头(head)、肩(shoulder)、肘(dlbow)等信息和对应的 三维坐标数据。
Kinect能够追踪到的骨骼数量是一个常量。这使得我们在整个应用程序中能够一次性的为数组分配内存。循环遍历skeletonFrame,每一次处理 一个骨骼。那么跟踪的骨骼也有跟得好与不好之分吧,你的姿势、是否有阻挡等等情况,都会使得跟踪不那边好。所以在处理之前需要判断一下是否是一个追踪好的 骨骼,可以使用Skeleton对象的TrackingState属性来判断,只有骨骼追踪引擎追踪到的骨骼我们才进行处理,忽略哪些不是游戏者的骨骼信 息即过滤掉那些TrackingState不等于SkeletonTrackingState.Tracked的骨骼数据。
Kinect能够探测到6个游戏者,但是同时只能够追踪到2个游戏者的骨骼关节位置信息。处理骨骼数据相对简单,首先,我们根据Kinect追踪到的游戏者的编号,用不同的颜色把游戏者的骨架画出来。
涉及的东西还是比较多的,但是代码思路还是比较清晰的,我们可以看代码,再看看第三部分的解析就会比较好理解了。

二、代码与注释   #include
#include
#include
#include

using namespace std;
using namespace cv;

//通过传入关节点的位置,把骨骼画出来
void drawSkeleton(Mat &image, CvPoint pointSet[], int whichone);

int main(int argc, char *argv[])
{
    Mat skeletonImage;
    skeletonImage.create(240, 320, CV_8UC3);
    CvPoint skeletonPoint[NUI_SKELETON_COUNT][NUI_SKELETON_POSITION_COUNT]={cvPoint(0,0)};
    bool tracked[NUI_SKELETON_COUNT]={FALSE};
 
    //1、初始化NUI,注意这里是USES_SKELETON
    HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_SKELETON);
    if (FAILED(hr))
    {
        cout<<"NuiInitialize failed"<

三、代码解析
像上面说的过程一样,我们先初始化,告诉Kinect我需要骨骼数据,然后创建一个骨骼事件,在打开骨骼跟踪功能。当骨架追踪启用后,运行时,库将处理 一幅图像和深度数据来传递包含骨架数据的帧。你可以在处理过程中的任何时候打开或关闭骨架追踪。这样有骨骼数据后,系统就会通知我们了。然后我们通过调用 NuiSkeletonGetNextFrame拿到骨骼数据。然后就开始我们的处理了:
骨骼帧数据保存在NUI_SKELETON_FRAME结构体中,我们首先来分析下这个最重要的结构体:
typedef struct _NUI_SKELETON_FRAME {
LARGE_INTEGER liTimeStamp;
DWORD dwFrameNumber;
DWORD dwFlags;  //没用到
Vector4 vFloorClipPlane;
Vector4 vNormalToGravity;
NUI_SKELETON_DATA SkeletonData[NUI_SKELETON_COUNT];
} NUI_SKELETON_FRAME;
时间标记字段:
SkeletonFrame的dwFrameNumber和liTimestamp字段表示当前记录中的帧序列信息。FrameNumber是深度数据帧 中的用来产生骨骼数据帧的帧编号。帧编号通常是不连续的,但是之后的帧编号一定比之前的要大。骨骼追踪引擎在追踪过程中可能会忽略某一帧深度数据,这跟应 用程序的性能和每秒产生的帧数有关。例如,在基于事件获取骨骼帧信息中,如果事件中处理帧数据的时间过长就会导致这一帧数据还没有处理完就产生了新的数 据,那么这些新的数据就有可能被忽略了。如果采用查询模型获取帧数据,那么取决于应用程序设置的骨骼引擎产生数据的频率,即取决于深度影像数据产生骨骼数 据的频率。
Timestap字段记录自Kinect传感器初始化(调用NuiInitialize函数)以来经过的累计毫秒时间。不用担心FrameNumber 或者Timestamp字段会超出上限。FrameNumber是一个32位的整型,Timestamp是64位整型。如果应用程序以每秒30帧的速度产 生数据,应用程序需要运行2.25年才会达到FrameNumber的限,此时Timestamp离上限还很远。另外在Kinect传感器每一次初始化 时,这两个字段都会初始化为0。可以认为FrameNumber和Timestamp这两个值是唯一的。
这两个字段在分析处理帧序列数据时很重要,比如进行关节点值的平滑,手势识别操作等。在多数情况下,我们通常会处理帧时间序列数据,这两个字段就显得很有用。目前SDK中并没有包含手势识别引擎。在未来SDK中加入手势引擎之前,我们需要自己编写算法来对帧时间序列进行处理来识别手势,这样就会大量依赖这两个字段。
骨骼数据段:
最重要的要数这个成员了。首先,Kinect可以检测到6个骨骼,在NuiSensor.h中有宏定义:#define    NUI_SKELETON_COUNT  ( 6 )。所以SkeletonData[NUI_SKELETON_COUNT]定义了六个骨骼的数据。骨骼数据是通过一个 NUI_SKELETON_DATA类型的结构体保存的:
typedef struct _NUI_SKELETON_DATA {
NUI_SKELETON_TRACKING_STATE eTrackingState;
DWORD dwTrackingID;
DWORD dwEnrollmentIndex;
DWORDdwUserIndex;
Vector4 Position;
Vector4 SkeletonPositions[20];
NUI_SKELETON_POSITION_TRACKING_STATE eSkeletonPositionTrackingState[20];
DWORD dwQualityFlags;
} NUI_SKELETON_DATA;
其中:
eTrackingState
eTrackingState字段表示当前的骨骼数据的状态,是一个枚举类型。
typedef enum _NUI_SKELETON_TRACKING_STATE
{
NUI_SKELETON_NOT_TRACKED = 0,
NUI_SKELETON_POSITION_ONLY,
NUI_SKELETON_TRACKED
} NUI_SKELETON_TRACKING_STATE;
下表展示了SkeletonTrackingState枚举的可能值的含义:
NUI_SKELETON_NOT_TRACKED   表示骨架没有被跟踪到,这个状态下,骨骼数据的Position字段和相关的关节点数据中的每一个位置点值都是0 。
NUI_SKELETON_POSITION_ONLY   检测到了骨骼对象,但是跟踪没有激活,也就是说骨骼数据的Position字段有值,但是相关的关节点数据中的每一个位置点值都是0(对应被动模式,被动模式只提供骨骼的位置,不提供关节点的位置)。
NUI_SKELETON_TRACKED   所有骨骼点的位置都被跟踪,骨骼数据的Position字段和相关的关节点数据中的每一个位置点值都非零(对应主动模式,骨骼位置和关节点位置都提供)。
dwTrackingID
骨骼追踪引擎对于每一个追踪到的游戏者的骨骼信息都有一个唯一编号。这个值是整型,他会随着新的追踪到的游戏者的产生添加增长。另外,这个编号的产生是不 确定的。如果骨骼追踪引擎失去了对游戏者的追踪,比如说游戏者离开了Kinect的视野,那么这个对应的唯一编号就会过期。当Kinect追踪到了一个新 的游戏者,他会为其分配一个新的唯一编号。编号值为0表示这个骨骼信息不是游戏者的。
Position
Position是一个Vector4类型的字段,代表所有骨骼的中间点。身体的中间点和脊柱关节的位置相当。该字段提供了一个最快且最简单的所有视野 范围内的游戏者位置的信息,而不管其是否在追踪状态中。在一些应用中,如果不用关心骨骼中具体的关节点的位置信息,那么该字段对于确定游戏者的位置状态已经足够。该字段对于手动选择要追踪的游戏者也是一个参考。例如,应用程序可能需要追踪距离Kinect最近的且处于追踪状态的游戏者,那么该字段就可以用来过滤掉其他的游戏者。
Vector4类型是一个空间坐标的类型:
Vector4typedef struct _Vector4 {
FLOAT x;  //X coordinate
FLOAT y;  //Y coordinate
FLOAT z;  //Z coordinate
FLOAT w;  //W coordinate
} Vector4;
SkeletonPositions[20]
这个数组记录的是主动模式下骨骼的20个关节点对应的空间位置信息。每一个关节点都有类型为Vector4的位置属性,他通过X,Y,Z三个值来描述关 节点的位置。X,Y值是相对于骨骼平面空间的位置,他和深度影像,彩色影像的空间坐标系不一样。KinectSnesor对象有一系列的坐标转换方法,可 以将骨骼坐标点转换到对应的深度数据影像中去。
SkeletonPositionTrackingState[20]
上面说到骨骼有跟踪的好与不好,那么关节点也是跟踪和分析的,那也有好与不好之分吧。而SkeletonPositionTrackingState属性就是标示对应的每一个关节点的跟踪状态的:
typedef enum _NUI_SKELETON_POSITION_TRACKING_STATE
{
NUI_SKELETON_POSITION_NOT_TRACKED = 0,
NUI_SKELETON_POSITION_INFERRED,
NUI_SKELETON_POSITION_TRACKED
} NUI_SKELETON_POSITION_TRACKING_STATE;
各个状态表示的含义:
NUI_SKELETON_POSITION_NOT_TRACKED   关节点位置没有跟踪到,而且也不能通过一些信息推断出来,所以关节点的Position值都为0 。
NUI_SKELETON_POSITION_INFERRED   骨骼的关节点位置可以由上一帧数据、已经跟踪到的其他点位置和骨架的几何假设这三个信息推测出来。
NUI_SKELETON_POSITION_TRACKED   关节点被成功跟踪,它的位置是基于当前帧的计算得到的。
NUI_SKELETON_POSITION_INDEX
另外,SkeletonPositions[20]这个数组是保存20个关节点的位置的,那么哪个数组元素对应哪个关节点呢?实际上,这个数组的保存是有顺序的,然后,我们可以通过下面的枚举值做为这个数组的下标来访问相应的关节点位置信息:
typedef enum _NUI_SKELETON_POSITION_INDEX
{
NUI_SKELETON_POSITION_HIP_CENTER = 0,
NUI_SKELETON_POSITION_SPINE,
NUI_SKELETON_POSITION_SHOULDER_CENTER,
NUI_SKELETON_POSITION_HEAD,
NUI_SKELETON_POSITION_SHOULDER_LEFT,
NUI_SKELETON_POSITION_ELBOW_LEFT,
NUI_SKELETON_POSITION_WRIST_LEFT,
NUI_SKELETON_POSITION_HAND_LEFT,
NUI_SKELETON_POSITION_SHOULDER_RIGHT,
NUI_SKELETON_POSITION_ELBOW_RIGHT,
NUI_SKELETON_POSITION_WRIST_RIGHT,
NUI_SKELETON_POSITION_HAND_RIGHT,
NUI_SKELETON_POSITION_HIP_LEFT,
NUI_SKELETON_POSITION_KNEE_LEFT,
NUI_SKELETON_POSITION_ANKLE_LEFT,
NUI_SKELETON_POSITION_FOOT_LEFT,
NUI_SKELETON_POSITION_HIP_RIGHT,
NUI_SKELETON_POSITION_KNEE_RIGHT,
NUI_SKELETON_POSITION_ANKLE_RIGHT,
NUI_SKELETON_POSITION_FOOT_RIGHT,
NUI_SKELETON_POSITION_COUNT
} NUI_SKELETON_POSITION_INDEX;


好了,感觉把这些说完,代码里面的东西就很容易读懂了。所以也没必要赘述了。但是还需要提到的几点是:
平滑化:
NuiTransformSmooth(&skeletonFrame,NULL);
在骨骼跟踪过程中,有些情况会导致骨骼运动呈现出跳跃式的变化。例如游戏者的动作不够连贯,Kinect硬件的性能等等。骨骼关节点的相对位置可能在帧与帧之间变动很大,这回对应用程序产生一些负面的影响。例如会影响用户体验和给控制造成意外等。
而这个函数就是解决这个问题的,它对骨骼数据进行平滑,通过将骨骼关节点的坐标标准化来减少帧与帧之间的关节点位置差异。
HRESULT NuiTransformSmooth(
NUI_SKELETON_FRAME *pSkeletonFrame,
const NUI_TRANSFORM_SMOOTH_PARAMETERS *pSmoothingParams
)
这个函数可以传入一个NUI_TRANSFORM_SMOOTH_PARAMETERS类型的参数:
typedef struct_NUI_TRANSFORM_SMOOTH_PARAMETERS {
FLOAT fSmoothing;
FLOAT fCorrection;
FLOAT fPrediction;
FLOAT fJitterRadius;
FLOAT fMaxDeviationRadius;
} NUI_TRANSFORM_SMOOTH_PARAMETERS;
NUI_TRANSFORM_SMOOTH_PARAMETERS这个结构定义了一些属性:
fSmoothing:平滑值(Smoothing)属性,设置处理骨骼数据帧时的平滑量,接受一个0-1的浮点值,值越大,平滑的越多。0表示不进行平滑
fCorrection:修正值(Correction)属性,接受一个从0-1的浮点型。值越小,修正越多。
fJitterRadius:抖动半径(JitterRadius)属性,设置修正的半径,如果关节点“抖动”超过了设置的这个半径,将会被纠正到这个半径之内。该属性为浮点型,单位为米。
fMaxDeviationRadius:最大偏离半径(MaxDeviationRadius)属性,用来和抖动半径一起来设置抖动半径的最大边界。任何超过这一半径的点都不会认为是抖动产生的,而被认定为是一个新的点。该属性为浮点型,单位为米。
fPrediction:预测帧大小(Prediction)属性,返回用来进行平滑需要的骨骼帧的数目。
空间坐标转换:
NuiTransformSkeletonToDepthImage(pSkel->SkeletonPositions[i], &fx, &fy );
由于深度图像数据和彩色图像数据来自于不同的摄像头,而这两个摄像头所在的位置不一样,而且视场角等也不也一样,所以产生的图像也会有差别(就好像你两 个眼睛看到的东西都不一样,这样大脑才能通过他们合成三维场景理解),也就是说这两幅图像上的像素点并不严格一一对应。例如深度图像中,你在图像的位置可 能是(x1,y1),但在彩色图像中,你在图像的位置是(x2,y2),而两个坐标一般都是不相等的。另外,骨骼数据的坐标又和深度图像和彩色图像的不一 样。所以就存在了彩色坐标空间、深度坐标空间、骨骼坐标空间和你的UI坐标空间四个不同的坐标空间了。那么他们各个空间之前就需要交互,例如我在骨骼空间 找到这个人在(x1,y1)坐标上,那么对应的深度图像,这个人在什么坐标呢?另外,我们需要把骨骼关节点的位置等画在我们的UI窗口上,那么它们的对应 关系又是什么呢?
微软SDK提供了一系列方法来帮助我们进行这几个空间坐标系的转换。例如:
voidNuiTransformSkeletonToDepthImage(
Vector4 vPoint,  //骨骼空间中某点坐标
FLOAT *pfDepthX, //对应的深度空间中的坐标
FLOAT *pfDepthY
)//将骨骼坐标转换到深度图像坐标上去。

HRESULTNuiImageGetColorPixelCoordinatesFromDepthPixel(
NUI_IMAGE_RESOLUTION eColorResolution,
const NUI_IMAGE_VIEW_AREA *pcViewArea,
LONG lDepthX,
LONG lDepthY,
USHORT usDepthValue,
LONG *plColorX,
LONG *plColorY
)//获取在深度图中具体坐标位置的像素在相应彩色空间中的像素的坐标。

Vector4NuiTransformDepthImageToSkeleton(
LONG lDepthX,
LONG lDepthY,
USHORT usDepthValue
)//传入深度图像坐标,返回骨骼空间坐标

至此,目标完成,效果如下:

转载注明来源:CV视觉网 » Kinect开发学习笔记之(七)骨骼数据的提取

分享到:更多 ()
扫描二维码,给作者 打赏
pay_weixinpay_weixin

请选择你看完该文章的感受:

0不错 0超赞 0无聊 0扯淡 0不解 0路过

评论 3

评论前必须登录!