Programming Guide(编程指导)
本章由cowboylym(酒劍仙采葡萄)编写,转载请注明出处。
作者:酒劍仙采葡萄 邮箱: cowboylym@gmail.com
自然用户界面(NUI)是Kinect for windows API的核心。通过它我们可以在自己的程序中访问传感器数据。
- 音频数据由音频流输出
- 彩色和深度图像数据有彩色和深度流输出。
除了硬件功能,Kinect软件运行库提供:
- 一个软件管线
- 集成了一个微软的语音API可以让我们在Kinect应用程序中实现一个语音识别的引擎,这样我们就可以添加声音指令了。
- 一个整合了脸部跟踪的SDK,使得我们能够追踪人脸。
数据流
如果全部使能,一台Kinect可以捕捉到音频、颜色和深度数据 用来处理骨骼数据。传感器以数据流的形式给应用程序提供数据。
为了避免丢帧,请确保你的应用程序及时的处理和释放每一帧。当初始化NUI API时,应用程序需要:
- 明确 需要哪一种数据流
- 打开每一个流
- 为传感器数据预分配缓存
- 获取每一个数据流每一帧的数据
- 及时释放缓存区,以便在运行时及时的填写下一帧
确保在上一帧还在被使用的情况下,不要被下一帧覆盖掉。通常应用程序为每个数据流的每一帧提供多重缓存。应用程序可以请求4个缓存区。对于大多数使用的场景2个已经足够了。如果在运行中,在应用程序检索并释放掉一个帧之前,所有的缓存都被填充了,那么应用程序会丢弃最先前的一帧,然后重新使用该缓存,这样做会导致丢帧。
骨骼追踪
概述
骨骼跟踪允许Kinect识别并跟踪人的动作。
使用红外线摄像机Kinect最多可以识别感应区的6位用户。其中最多只有2名用户可以被追踪到骨骼细节(20个骨骼关节点)。应用程序可以定位并追踪用户的骨骼节点,能够随时间追踪用户的动作。
图1
骨骼跟踪对 识别用户站立、坐和面向Kinect进行了优化。侧身站给传感器带来了一些挑战,传感器将无法识别。为了别识别到,用户只需要站在传感器前面,确保传感器可以看到他们的头部和上身,被追踪到的用户不需要做一些特别的动作。
图2
视野
Kinect的视野是由,红外线摄像机的设定决定的。通过枚举类型DepthRange来设置。
默认范围,Kinect可以看到站在前方0.8m到4.0m之间的用户,考虑到用户的摆臂动作,减去臂长,最佳范围在1.2m到3.5之间,更多细节详见 k4w_hig_main
水平视野
图3
垂直视野
图4
在近景模式下Kinect可以看到0.4m到3.0m之间的用户,最佳范围在0.8m到2.5m之间。
图5
骨骼追踪精度和多个Kinect传感器
Kinect传感器用红外线发射器产生一个红外光投影。这个投影用来计算视野中人的深度用来辨别不同的用户和用户身体不同的部分。如果你使用一个以上的Kinect来照亮目标区域。你可能会注意到骨骼追踪精度的降低由于红外光源的干扰。为了降低干扰的可能性,我们建议不要超过一个Kinect的传感器(或红外光源)指向骨骼正在被追踪的区域。
跟踪用户
骨骼追踪使能
为了跟踪用户,Kinect需要让骨骼追踪使能。
骨骼追踪使能c++方法
使能骨骼追踪需要调用 INuiSensor::NuiSkeletonTrackingEnable 方法。接收识别到的用户的信息,需要调用INuiSensor::NuiSkeletonGetNextFrame方法。
// Call StartKinectST once at application start.
HRESULT MyApplication::StartKinectST()
{
m_hNextSkeletonEvent = NULL;
// Initialize m_pNuiSensor
HRESULT hr = FindKinectSensor();
if (SUCCEEDED(hr))
{
// Initialize the Kinect and specify that we'll be using skeleton
hr = m_pNuiSensor->NuiInitialize(NUI_INITIALIZE_FLAG_USES_SKELETON);
if (SUCCEEDED(hr))
{
// Create an event that will be signaled when skeleton data is available
m_hNextSkeletonEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
// Open a skeleton stream to receive skeleton data
hr = m_pNuiSensor->NuiSkeletonTrackingEnable(m_hNextSkeletonEvent, 0);
}
}
return hr;
}
// Call UpdateKinectST on each iteration of the application's update loop.
void MyApplication::UpdateKinectST()
{
// Wait for 0ms, just quickly test if it is time to process a skeleton
if ( WAIT_OBJECT_0 == WaitForSingleObject(m_hNextSkeletonEvent, 0) )
{
NUI_SKELETON_FRAME skeletonFrame = {0};
// Get the skeleton frame that is ready
if (SUCCEEDED(m_pNuiSensor->NuiSkeletonGetNextFrame(0, &skeletonFrame)))
{
// Process the skeleton frame
SkeletonFrameReady(&skeletonFrame);
}
}
}
访问骨骼追踪信息
用户识别信息以一组骨骼对象的形式由帧提供。
如何用c++访问骨骼追踪信息
每次 INuiSensor::NuiSkeletonGetNextFrame 都返回一个有效的帧,访问 NUI_SKELETON_FRAME 结构体成员。
void MyApplication::SkeletonFrameReady(NUI_SKELETON_FRAME* pSkeletonFrame)
{
// Access members of pSkeletonFrame...
}
骨骼位置和追踪状态
一帧中的骨骼数据有一个跟踪状态 “位置”或“被追踪的”。一个被追踪到的骨架提供了详细信息关于摄像机视野中人体20个骨骼节点位置的。
当前状态是“position only”时 只有骨骼的位置信息而没有具体关节节点的信息。应用程序可以决定跟踪哪一副骨骼。使用骨骼追踪ID 具体见 Active UserTracking。
被跟踪到的骨骼信息也可以在深度图中被检索到,具体见 PlayerID indepth map部分。
如何用C++访问跟踪到的骨骼数据
循环遍历骨骼数据结构体,在每一帧有确定追踪状态的时候返回。这个值可能是 NUI_SKELETON_TRACKING_STATE枚举类型中的(NUI_SKELETON_NOT_TRACKED, NUI_SKELETON_POSITION_ONLY, 或 NUI_SKELETON_TRACKED)忽略没有被追踪到的骨骼,他们是数组中的占位符。访问SkeletonPostions数组获取关节的位置;对于postion-only的骨骼 访问它的位置。
void MyApplication::SkeletonFrameReady(NUI_SKELETON_FRAME* pSkeletonFrame)
{
for (int i = 0; i < NUI_SKELETON_COUNT; i++)
{
const NUI_SKELETON_DATA & skeleton = pSkeletonFrame->SkeletonData[i];
switch (skeleton.eTrackingState)
{
case NUI_SKELETON_TRACKED:
DrawTrackedSkeletonJoints(skeleton);
break;
case NUI_SKELETON_POSITION_ONLY:
DrawSkeletonPosition(skeleton.Position);
break;
}
}
}
关节位置和追踪状态
对于跟踪到的骨骼,关节数组提供了随时间推移识别到的人身上20个关节点的位置信息。例如:应用程序可以使用手部关节引导光标在屏幕上的移动。或者在屏幕上绘制出关节的位置。关节也拥有一个跟踪状态。“被跟踪”状态表示该骨骼节点清晰可见,“推测”表示该关节的位置不清楚,是Kinect推算出来的,或者无法追踪。例如在近景模式中 位于下面的关节。
用C++访问骨骼数据并绘制骨骼
NUI_SKELETON_DATA 结构体有一个SkeletonPositionTrackingState 数组,包含了每一个关节的位置。
void MyApplication::DrawSkeleton(const NUI_SKELETON_DATA & skeleton)
{
// Render Head and Shoulders
DrawBone(skeleton, NUI_SKELETON_POSITION_HEAD, NUI_SKELETON_POSITION_SHOULDER_CENTER);
DrawBone(skeleton, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SHOULDER_LEFT);
DrawBone(skeleton, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SHOULDER_RIGHT);
// Render Left Arm
DrawBone(skeleton, NUI_SKELETON_POSITION_SHOULDER_LEFT, NUI_SKELETON_POSITION_ELBOW_LEFT);
DrawBone(skeleton, NUI_SKELETON_POSITION_ELBOW_LEFT, NUI_SKELETON_POSITION_WRIST_LEFT);
DrawBone(skeleton, NUI_SKELETON_POSITION_WRIST_LEFT, NUI_SKELETON_POSITION_HAND_LEFT);
// Render Right Arm
DrawBone(skeleton, NUI_SKELETON_POSITION_SHOULDER_RIGHT, NUI_SKELETON_POSITION_ELBOW_RIGHT);
DrawBone(skeleton, NUI_SKELETON_POSITION_ELBOW_RIGHT, NUI_SKELETON_POSITION_WRIST_RIGHT);
DrawBone(skeleton, NUI_SKELETON_POSITION_WRIST_RIGHT, NUI_SKELETON_POSITION_HAND_RIGHT);
// Render other bones...
}
void MyApplication::DrawBone(
const NUI_SKELETON_DATA & skeleton,
NUI_SKELETON_POSITION_INDEX jointFrom,
NUI_SKELETON_POSITION_INDEX jointTo)
{
NUI_SKELETON_POSITION_TRACKING_STATE jointFromState = skeleton.eSkeletonPositionTrackingState[jointFrom];
NUI_SKELETON_POSITION_TRACKING_STATE jointToState = skeleton.eSkeletonPositionTrackingState[jointTo];
if (jointFromState == NUI_SKELETON_POSITION_NOT_TRACKED || jointToState == NUI_SKELETON_POSITION_NOT_TRACKED)
{
return; // nothing to draw, one of the joints is not tracked
}
const Vector4& jointFromPosition = skeleton.SkeletonPositions[jointFrom];
const Vector4& jointToPosition = skeleton.SkeletonPositions[jointTo];
// Don't draw if both points are inferred
if (jointFromState == NUI_SKELETON_POSITION_INFERRED || jointToState == NUI_SKELETON_POSITION_INFERRED)
{
DrawNonTrackedBoneLine(jointFromPosition, jointToPosition); // Draw thin lines if either one of the joints is inferred
}
// We assume all drawn bones are inferred unless BOTH joints are tracked
if (jointFromState == NUI_SKELETON_POSITION_TRACKED && jointToState == NUI_SKELETON_POSITION_TRACKED)
{
DrawTrackedBoneLine(jointFromPosition, jointToPosition); // Draw bold lines if the joints are both tracked
}
}
边缘裁剪
当用户移动到视野的边缘,骨骼追踪系统会警告应用程序,用户骨骼被裁剪掉了。这个信息可以被用来告诉用户移动到屏幕的中心,便于更好的追踪。
如何用C++访问被裁剪的边缘
使用 NUI_SKELETON_DATA结构体重的 dwQualityFlags来验证是否有任何标记被设置。
void MyApplication::RenderClippedEdges(const NUI_SKELETON_DATA & skeleton)
{
if (skeleton.dwQualityFlags & NUI_SKELETON_QUALITY_CLIPPED_BOTTOM)
{
DrawClippedEdges(NUI_SKELETON_QUALITY_CLIPPED_BOTTOM); // Make the border red to show the user is reaching the border
}
if (skeleton.dwQualityFlags & NUI_SKELETON_QUALITY_CLIPPED_TOP)
{
DrawClippedEdges(NUI_SKELETON_QUALITY_CLIPPED_TOP);
}
if (skeleton.dwQualityFlags & NUI_SKELETON_QUALITY_CLIPPED_LEFT)
{
DrawClippedEdges(NUI_SKELETON_QUALITY_CLIPPED_LEFT);
}
if (skeleton.dwQualityFlags & NUI_SKELETON_QUALITY_CLIPPED_RIGHT)
{
DrawClippedEdges(NUI_SKELETON_QUALITY_CLIPPED_RIGHT);
}
}
活跃用户的追踪
Kinect骨骼追踪允许应用程序在6个被识别的用户中指定追踪哪一副骨架。在默认情况下骨骼追踪会选出头两个在视野中被识别出来的用户。这个行为不是随机的。当不是由特定的标准决定的。如果应用程序希望,它可以覆盖默认的行为,自定义的选择哪个用户被追踪的筛选逻辑。例如:你可以追踪离相机最近的用户,或者举手的用户。
这样做,遍历被提取出来的骨架,选择满足筛选规则的tracking IDs,根据这个ID来设置全部跟踪。
当应用程序控制追踪某个用户之后,骨骼追踪系统将不会交出控制权,如果用户走出屏幕,应用程序将选择一个新用户去跟踪。
注意:当一个用户离开之后又返回屏幕时,他会随机获得一个新的ID,这个ID可能不是他离开前的ID。
应用程序也可以选择,只跟踪衣服骨架或者一副骨架都不追踪(通过一个空的追踪ID进行筛选)。
如何用C++选择被追踪的用户
应用程序可以通过调用INuiSensor::NuiSkeletonTrackingEnable方法,设置 NUI_SKELETON_TRACKING_FLAG_TITLE_SETS_TRACKED_SKELETONS这个标记来控制选择用户追踪。对于特定的用户,调用 INuiSensor::SetTrackedSkeletons通过跟踪ID来设置哪个用户会被跟踪到。
void MyApplication::TrackClosestSkeleton(NUI_SKELETON_FRAME* pSkeletonFrame)
{
m_pNuiSensor->NuiSkeletonTrackingEnable(
m_hNextSkeletonEvent,
NUI_SKELETON_TRACKING_FLAG_TITLE_SETS_TRACKED_SKELETONS);
float closestDistance = 10000.0f; // Start with a far enoughdistance
DWORD closestIDs[NUI_SKELETON_MAX_TRACKED_COUNT]= {0, 0};
for (int i = 0; i < NUI_SKELETON_COUNT;i++)
{
const NUI_SKELETON_DATA & skeleton =pSkeletonFrame->SkeletonData[i];
if (skeleton.eTrackingState !=NUI_SKELETON_NOT_TRACKED)
{
if (skeleton.Position.z <closestDistance)
{
closestIDs[0] =skeleton.dwTrackingID;
closestDistance =skeleton.Position.z;
}
}
if (closestIDs[0] > 0)
{
m_pNuiSensor->SetTrackedSkeletons(closestIDs); // Track this skeleton
}
}
void MyApplication::TrackClosestSkeleton(NUI_SKELETON_FRAME* pSkeletonFrame)
{
m_pNuiSensor->NuiSkeletonTrackingEnable(
m_hNextSkeletonEvent,
NUI_SKELETON_TRACKING_FLAG_TITLE_SETS_TRACKED_SKELETONS);
float closestDistance = 10000.0f; // Start with a far enoughdistance
DWORD closestIDs[NUI_SKELETON_MAX_TRACKED_COUNT]= {0, 0};
for (int i = 0; i < NUI_SKELETON_COUNT;i++)
{
const NUI_SKELETON_DATA & skeleton =pSkeletonFrame->SkeletonData[i];
if (skeleton.eTrackingState !=NUI_SKELETON_NOT_TRACKED)
{
if (skeleton.Position.z <closestDistance)
{
closestIDs[0] =skeleton.dwTrackingID;
closestDistance =skeleton.Position.z;
}
}
if (closestIDs[0] > 0)
{
m_pNuiSensor->SetTrackedSkeletons(closestIDs); // Track this skeleton
}
}
深度图中的玩家ID
当骨骼追踪确定视野中的一个用户时,用户的位置也会在深度图中被反馈出来。对于深度图中的每一个像素,最低的三位中包含了用户的索引信息,如果索引为“0”表示被该的像素中没有用户,索引“1”到“6”表示用一个玩家存在。可以通过一个指定深度像素的索引值来查寻当前骨骼所属ID,将索引值的减去1得到的值就是对应骨骼数组中的索引。
如何用C++在深度图像素中找到用户的ID
void MyApplication::FindPlayerInDepthPixel(NUI_IMAGE_FRAME* pDepthFrame, NUI_SKELETON_FRAME* pSkeletonFrame)
{
INuiFrameTexture* pTexture = pDepthFrame->pFrameTexture;
NUI_LOCKED_RECT LockedRect;
pTexture->LockRect(0, &LockedRect, NULL, 0);
if (LockedRect.Pitch != 0)
{
const USHORT * pBufferRun = reinterpret_cast<const USHORT *>(LockedRect.pBits);
const USHORT * pBufferEnd = pBufferRun + (LockedRect.size / sizeof(USHORT));
for (; pBufferRun < pBufferEnd; pBufferRun++)
{
int player = (*pBufferRun & NUI_IMAGE_PLAYER_INDEX_MASK);
if (player > 0 && pSkeletonFrame != NULL)
{
const NUI_SKELETON_DATA & skeletonAtPixel = pSkeletonFrame->SkeletonData[player - 1]; // Found the player at this pixel
// ...
}
}
}
pTexture->UnlockRect(0);
}
To be continued…
—— 貳零壹叁 年 玖月 貳日