5. Kinect 骨骼追踪

目标:学习如何从 Kinect 获取骨骼 (skeleton) 跟踪数据,尤其是关节点 (joints) 位置。
源码:点此查看 4_SkeletalTracking.zip

5.1. 概述

本章教程相当简单,展示了如何获取在 Kinect 视角下的基本人体信息。我们将会展示如何提取身体关节的 3D 位置,这样的信息经过进一步处理,可以实现从简单的绘制骨骼、到复杂的姿势识别在内的各种事情。

为此,我们将从我们在上一章中创建的框架开始,并添加一些内容。

5.2. Kinect 代码

5.2.1. 全局变量

我们将维护一个全局变量,来保存最近捕捉到的身体的所有关节点。

// Body tracking variables
Vector4 skeletonPosition[NUI_SKELETON_POSITION_COUNT];

总共有NUI_SKELETON_POSITION_COUNT = 20个关节点会被 Kinect 追踪,具体可以看这里。每个关节点用一个Vector4类型来描述其在相机坐标系下的 3D 位置。例如,要看右肘的位置,可以使用skeletonPosition[NUI_SKELETON_POSITION_ELBOW_RIGHT]

注解

译者注:上面的链接已失效,替代的网页快照可以看这里

5.2.2. Kinect 初始化

我们在 Kinect 初始化中添加了两个新的部分。首先,我们在初始化 Kinect 时请求了骨骼追踪数据,然后,我们启用了追踪。

默认情况下,Kinect 希望人们能够面对传感器站立,以便更好地跟踪。在函数NuiSkeletonTrackingEnable()中,通过设置第二个参数,也可以指示设备跟踪对面坐着的人(比如坐在沙发上)。

5.2.3. 从 Kinect 中获取关节数据

获取骨骼数据要比获取彩色数据和深度数据更简单——它没有锁定或其它繁琐的操作。我们只需从传感器获取一串NUI_SKELETON_FRAME类型的数据。关节点追踪的噪声可能会很大,所以为了降低关节点位置随时间的抖动,我们还调用了一个内置的平滑函数。

void getSkeletalData() {
    NUI_SKELETON_FRAME skeletonFrame = {0};
    if (sensor->NuiSkeletonGetNextFrame(0, &skeletonFrame) >= 0) {
        sensor->NuiTransformSmooth(&skeletonFrame, NULL);
        // Process skeletal frame (see below)...
    }
}

Kinect 可以同时追踪最多NUI_SKELETON_COUNT个用户(在 SDK 中,NUI_SKELETON_COUNT == 6)。骨骼数据结构NUI_SKELETON_DATA可以在帧的SkeletonData数组字段中访问。注意,每个骨骼不一定指向 Kinect 可以看到的实际的人,第一个被跟踪的身体也不一定是数组中的第一个元素。因此,我们需要检查数组中的每个元素是否是被跟踪的身体。通过检查骨骼的追踪状态来做这件事。

// Loop over all sensed skeletons
for (int z = 0; z < NUI_SKELETON_COUNT; ++z) {
    const NUI_SKELETON_DATA& skeleton = skeletonFrame.SkeletonData[z];
    // Check the state of the skeleton
    if (skeleton.eTrackingState == NUI_SKELETON_TRACKED) {
        // Get skeleton data (see below)...
    }
}

一旦我们有了一个有效的跟踪骨骼,我们就可以将所有的关节点数据复制到我们的关节点位置数组中。鉴于 Kinect 可能会跟丢一部分关节点(比如用户的手臂藏到了背后,或遇到其它遮挡),我们还单独检查每个关节的追踪状态。我们使用Vector4关节位置的 w 坐标来记录它是否是一个有效的跟踪关节。

// For the first tracked skeleton
{
    // Copy the joint positions into our array
    for (int i = 0; i < NUI_SKELETON_POSITION_COUNT; ++i) {
        skeletonPosition[i] = skeleton.SkeletonPositions[i];
        if (skeleton.eSkeletonPositionTrackingState[i] == NUI_SKELETON_POSITION_NOT_TRACKED) {
            skeletonPosition[i].w = 0;
        }
    }
    return; // Only take the data for one skeleton
}

5.3. OpenGL 显示

我们用关节点数组中的坐标绘制一些简单的线条,来显示人体的上肢。也就是说,我们要从右肩到右肘画一条线,然后从右肘到右手腕画一条线;左边也是一样的道理。当然,只有在 Kinect 检测到人的情况下才需要画线,所以我们要先检查向量的 w 坐标是否有效。

void drawKinectData() {
    // ...
    const Vector4& lh = skeletonPosition[NUI_SKELETON_POSITION_HAND_LEFT];
    const Vector4& le = skeletonPosition[NUI_SKELETON_POSITION_ELBOW_LEFT];
    const Vector4& ls = skeletonPosition[NUI_SKELETON_POSITION_SHOULDER_LEFT];
    const Vector4& rh = skeletonPosition[NUI_SKELETON_POSITION_HAND_RIGHT];
    const Vector4& re = skeletonPosition[NUI_SKELETON_POSITION_ELBOW_RIGHT];
    const Vector4& rs = skeletonPosition[NUI_SKELETON_POSITION_SHOULDER_RIGHT];
    glBegin(GL_LINES);
        glColor3f(1.f, 0.f, 0.f);
        if (lh.w > 0 && le.w > 0 && ls.w > 0) {
            // lower left arm
            glVertex3f(lh.x, lh.y, lh.z);
            glVertex3f(le.x, le.y, le.z);
            // upper left arm
            glVertex3f(le.x, le.y, le.z);
            glVertex3f(ls.x, ls.y, ls.z);
        }
        if (rh.w > 0 && re.w > 0 && rs.w > 0) {
            // lower right arm
            glVertex3f(rh.x, rh.y, rh.z);
            glVertex3f(re.x, re.y, re.z);
            // upper right arm
            glVertex3f(re.x, re.y, re.z);
            glVertex3f(rs.x, rs.y, rs.z);
        }
    glEnd();
}

结束!构建并运行,确保你的 Kinect 已经插入。你应该会看到一个包含 Kinect 所拍摄的旋转的彩色点云的(视频流)窗口,当 Kinect 捕捉到人体时,则绘制红线来展示这个人的上肢姿态。