Kinect一个很强大的功能就是它可以侦测到人体的骨骼信息并追踪,在Kinect V2的SDK 2.0中,它最多可以同时获取到6个人、每个人25个关节点的信息,并且通过深度摄像头,可以同时获取到这些关节点的坐标。此时的坐标使用的是Camera Space
,也就是摄像机空间坐标系
,代表了物体距离深度摄像头的距离。
与前面获取数据源稍微不同的是,在把数据读取到IBodyFrame
之后,还需要再从IBodyFrame
里把数据读到一个6*m
的二维数组里,这个数组存储了6个人的关节点信息,在这个数组中确定某一维(人)之后,再从这一维中读取出详细的关节信息。
所以简而言之,获取关节数据的步骤为:Sensor
-> Source
-> Reader
-> Frame
->BodyArr
->JointArr
->Joint
关节点以及此坐标系的结构如下(其中坐标原点就是深度摄像头的大概位置):
#include <iostream>
#include <opencv2\highgui.hpp>
#include <string>
#include <Kinect.h>
using namespace std;
using namespace cv;
const string get_name(int n); //此函数判断出关节点的名字
int main(void)
{
IKinectSensor * mySensor = nullptr;
GetDefaultKinectSensor(&mySensor);
mySensor->Open();
int myBodyCount = 0;
IBodyFrameSource * myBodySource = nullptr;
IBodyFrameReader * myBodyReader = nullptr;
mySensor->get_BodyFrameSource(&myBodySource);
myBodySource->OpenReader(&myBodyReader);
myBodySource->get_BodyCount(&myBodyCount);
IDepthFrameSource * myDepthSource = nullptr;
IDepthFrameReader * myDepthReader = nullptr;
mySensor->get_DepthFrameSource(&myDepthSource);
myDepthSource->OpenReader(&myDepthReader);
int height = 0, width = 0;
IFrameDescription * myDescription = nullptr;;
myDepthSource->get_FrameDescription(&myDescription);
myDescription->get_Height(&height);
myDescription->get_Width(&width); //以上为准备好深度数据和骨骼数据的Reader
IBodyFrame * myBodyFrame = nullptr;
IDepthFrame * myDepthFrame = nullptr;
Mat img16(height, width, CV_16UC1); //为显示深度图像做准备
Mat img8(height, width, CV_8UC1);
while (1)
{
while (myDepthReader->AcquireLatestFrame(&myDepthFrame) != S_OK);
myDepthFrame->CopyFrameDataToArray(width * height,(UINT16 *)img16.data);
img16.convertTo(img8,CV_8UC1,255.0 / 4500);
imshow("Depth Img", img8); //深度图像的转化及显示
while (myBodyReader->AcquireLatestFrame(&myBodyFrame) != S_OK);
int myBodyCount = 0;
IBody ** bodyArr = nullptr;
myBodySource->get_BodyCount(&myBodyCount);
bodyArr = new IBody *[myBodyCount];
for (int i = 0; i < myBodyCount; i++) //bodyArr的初始化
bodyArr[i] = nullptr;
myBodyFrame->GetAndRefreshBodyData(myBodyCount,bodyArr);
for (int i = 0; i < myBodyCount; i++) //遍历6个人(可能用不完)
{
BOOLEAN result = false;
if (bodyArr[i]->get_IsTracked(&result) == S_OK && result) //判断此人是否被侦测到
{
cout << "Body " << i << " tracked!" << endl;
int count = 0;
Joint jointArr[JointType_Count];
bodyArr[i]->GetJoints(JointType_Count,jointArr); //获取此人的关节数据
for (int j = 0; j < JointType_Count; j++)
{
if (jointArr[j].TrackingState != TrackingState_Tracked) //将确定侦测到的关节显示出来
continue;
string rt = get_name(jointArr[j].JointType); //获取关节的名字
if (rt != "NULL") //输出关节信息
{
count++;
cout << " " << rt << " tracked" << endl;
if (rt == "Right thumb")
cout << " Right thumb at " << jointArr[j].Position.X << "," << jointArr[j].Position.Y << "," << jointArr[j].Position.Z << endl;
}
}
cout << count << " joints tracked" << endl << endl;
}
}
myDepthFrame->Release();
myBodyFrame->Release();
delete[] bodyArr;
if (waitKey(30) == VK_ESCAPE)
break;
Sleep(1000); //为避免数据刷太快,每秒钟更新一次
}
myBodyReader->Release();
myDepthReader->Release();
myBodySource->Release();
myDepthSource->Release();
mySensor->Close();
mySensor->Release();
return 0;
}
const string get_name(int n)
{
switch (n)
{
case 2:return "Neck"; break;
case 3:return "Head"; break;
case 4:return "Left shoulder"; break;
case 8:return "Right shoulder"; break;
case 7:return "Left hand"; break;
case 11:return "Right hand"; break;
case 22:return "Left thumb"; break;
case 24:return "Right thumb"; break;
default :return "NULL";
}
}
这是一份判断上半身关节点是否被侦测到的代码,如果侦测到右拇指的话,还会显示右拇指到摄像头的距离。只判断上半身的原因是一共有25个关节点,也就是说全部都判断的话要写25个case
,实在太麻烦了。
在这里,为了方便观察,我不仅使用了骨骼数据,同时也将深度数据显示出来,所以一共用到了两种数据源。而且为了不让代码刷太快,所以控制了时间间隔,每秒1帧。就像之前一样,当同时使用到多种数据源时,为了使得代码清晰,比较好的办法是实现先把各种数据源的Reader
准备好,到时候直接拿来用就行。
在把两种数据源都读入到Frame
之后,首先要做的是利用BodyFrame
的GetAndRefreshBodyData()
这个函数把所有信息输出到数组里。数组的类型是IBody
,这个类同样包含了很多方法,链接在此。下一步所需要用到的,就是用它里面的get_IsTracked()
这个函数来判断当前这一维是否有效。注意不仅需要判断get到的bool
值,同时也需要通过函数的返回值判断是否get成功。如果有效,那么接下来就是利用它里面的GetJoints()
,来把此人的详细关节信息输出到一个Joint
数组里面。注意Joint
是一个结构,而不是一个类。
接下来,就是对关节的详细处理了,首先,每个关节的状态有三种:TrackingState_NotTracked
、TrackingState_Inferred
、TrackingState_Tracked
,其中第一个和第三个代表的是没有侦测到
和侦测到
,第二个代表的是推测
,代表它利用一些不太完全的信息推测出此关节点的位置。为了精确起见,我在代码里只接受Tracked
这种状态。然后,就是通过JointType
这个变量,来取得关节点的类型,但是JointType
是一个枚举量,也就是说只能获得一个int
值,所以要再写一个函数来取得关节名称,这里我只写了上半身的关节点的代码。Joint
里还有一个Position
的重要变量没用到,它包含的是此关节点在摄像机坐标系下的X
、Y
、Z
值,不用太可惜了,所以就用它来判断下右拇指的距离好了。
让人遗憾的是,SDK 2.0对拇指的判断非常非常不精确,且不说距离,状态值就非常难以理解了。当我的双手放到摄像头完全看不到的地方时它还是信誓旦旦的返回了一个Tracked
,连个Inferred
都不是,只好寄希望于微软在下一版的SDK中能解决这个问题。