1、 前言MS 的 kinec SDK 和 OpenNI 都提供了人体骨骼跟踪的算法,人体骨骼跟踪算法在 kinect 人体行为识别中非常重要,该识别过程通常被用来作为行为识别的第一步, 比如说,通过定位人体中的骨骼支架,可以提取出人手的部位,从而可以把手的部分单独拿出来分析,这样就达到了手势的定位,而后面的手势识别则可以在刚刚定 位出的领域进行处理。总而言之,一套有效的人体骨架追踪算法在 kinect 的一系列应用中非常有用,不过 MS SDK 和 OpenNI 虽然都提供了该算法类的直调用,但是其源码并没有开放,毕竟这是人家最核心的东东。开发环境:QtCreator2.5.1+OpenNI1.
2、5.4.0+Qt4.8.2实验说明在老版本的 OpenNI 中,要对人进行骨架追踪,需要人先摆出 PSI 的姿势,然后系统根据该姿势进行骨骼校正,待校正完成后才进行骨骼的跟踪,其流程图可以参考下面的图:由图可以看出,其完成骨骼跟踪主要分为 3 个部分,首先需检测到人体,然后需要固定的 PSI 姿势来对人体的姿势进行校正,待姿势校正完成后,才能进行人体骨骼的追踪。如果程序开发者用代码实现该过程则可以参考 hersey 的文章透过 OpenNI / NITE 分析人体骨架(上)和透过 OpenNI / NITE 分析人体骨架(下),作者在这 2 篇文章详细介绍了老版本的人体骨架的 OpenNI 实
3、现。在新版本 OpenNI1.5 以后,人体骨架追踪算法更改了不少,其中最大的特点就是骨架跟踪过程中少了姿势校正的那一步骤,新版本中只需要人体 站起来就可以进行跟踪了,http:/ 使用起来方便很多,程序开发也简单不少。另外人体骨骼跟踪的效果也提高了不少,一旦骨骼追踪成功后,即使人体没有保持站立姿势有 时候也还是可以继续跟踪的。新版本的人体骨骼跟踪算法使用流程图如下:下面来看看程序中的 Capability,它不同于前面文章的 generator:在进行骨架的判断和姿态检测是需要用到 OpenNI 延伸的功能,与这种延伸功能相关的类可以称作为 Capability。在进行人体骨骼分析时,use
4、r generator 需要有支援 Skeleton 和 Pose Detection 这 2 个的 capability。在程序中需要绘制骨骼节点之间的连线,而节点的坐标和法向都有函数可以获得,获得的坐标为真实世界中的坐标,画图时需要在平面上绘制,因此需要这 2 个坐标系的转换,转换过程用到下面的函数:XnStatus xn:DepthGenerator:ConvertRealWorldToProjective(XnUInt32 nCount, const XnPoint3D aRealWorld, XnPoint3D aProjective)该函数表示将深度图获取的真实坐标系转换成平面图形
5、显示的投影坐标系上。第 1 个参数表示转换坐标点的个数,第 2 个参数表示真实坐标系中的坐标,第 3 个参数表示投影坐标系下的坐标。本实验的程序分为 3 个类和一个主函数,其中 2 个类的基本部分在前面的文章中已有介绍,只需要更新其部分功能。下面是本实验中这 3 个类的设计。当然这都是参考 heresy 的博客使用 Qt 显示 OpenNI 的人体骨架。COpenNI 类的更新:因为需要对人体进行骨骼跟踪,所以需要用到 OpenNI 的 UserGenerator 这个类。在 private 变量一栏增加这个类对象的声明。然后在类的 Init()函数中使用 Create 方法产生人体的 nod
6、e。同上一篇博客 Kinect+OpenNI 学习笔记之5(使用 OpenNI 自带的类进行简单手势识别)中 类似,这里的人体骨架校正,跟踪等都是通过回调函数的形式进行的,因此还需要在 Init()函数中设置这个node 的检测到有新人进入和骨骼校正完成的回 调函数(其实还有旧人体目标离去,骨骼校正开始这 2 个也可以设置回调函数,但在本程序中因为不需要使用它们,因此可以省略不写,http:/ 老版本的 OpenNI 是不 允许省略的)。另外,由于色彩节点,深度节点,以及人体检测节点都是私有变量,如果该类的对象需要获取该变量的话不方便,因此在共有函数部分分别设置了 3 个共有函数来获取这 3
7、个变量。具体该类的全部代码参加本文后面的代码部分。CSkeletonItem 类的设计:CSkeletonItem 这个类主要来完成骨架节点位置的获取,以及画出 item 中节点之间的连线,同时也在节点位置处画出圆圈代表对应节点的位置。在构造函数中,设计了一个二维的连接表矩阵,矩阵的大小为 14*2,即有14 条边,每条边有 2 个顶点,矩阵中对应位置的值表示的是对应边的节点骨架的标号,在 OpenNI 中人体的骨架节点共分为 15 个,手脚共 12 个,头部 2 个,躯干 1 个。如下图所示:程序中对这 15 个点编了序号,头部为 0, 颈部为 1, 躯干为 2, 左肩膀为 3, 左手肘为
8、4, 左手腕 5,右肩膀为 6,右手肘为 7,右手腕为 8,左臀为9,左膝盖为 10,左脚跟为 11,右臀为 12,右膝盖为 13,右脚跟为 14。该类中需要重写的 boundingRect()函数,函数中设置了一个包含 15 个节点的最小矩形,因为后面的绘图区域需要在这个矩形内进行,很明显,获得的这个矩形不是固定大小的,而是根据人体骨架的位置在不断变化。大小和位置同时都会发生改变。重写的 paint()函数则需要完成 2 个部分的功能, 第一是画出骨骼中节点的位置,用圆圈显示;第二是画出 2 个节点之间的连线,共 14 条,这样通过画出的连线就可以大概看出人的位置和区域了。本文是参考的 he
9、resy 文章不用校正姿势的 NITE 1.5 ,heresy 在设计该类的构造函数时,设计了个 15*2 的连接表,个人感觉设置为 14*2 比较合理,因为 15 个点刚好由 14 条线可以连接起来,并不是 heresy 所说的 15 条线,其实它有 2 条线是重合的。CKinectReader 类的更新:该类是在前面的文章 Kinect+OpenNI 学习笔记之 3(获取 kinect 的数据并在 Qt 中显示的类的设计)中 对应类的更新,前面博文中的该类只是完成了深度图像和颜色图像的显示,而在本实验中,需要完成显示骨架节点之间的连线图,因此该类需要继续更新。其实现过 程主要是获取视野中人
10、体的个数,对检测到的每个人体然后调用 CSkeletonItem 类中的方法 UpdateSkeleton()来更新读取的节点坐 标,因为一旦坐标值发生了改变,CSkeletonItem 类中的boundingRect()内容也会更改,从而其 Item 所在区域的矩形也会变化,最 后导致 paint()函数的执行,在 paint()函数中完成骨骼节点连线和骨骼节点的绘图。实验结果试验效果的截图:蓝色的线表示骨骼节点之间的连线,黄色的圈表示骨骼节点。实验主要部分代码及注释(附录有实验工程 code 下载链接地址):copenni.cpp:#ifndef COPENNI_CLASS#define
11、COPENNI_CLASS#include #include #include using namespace xn;using namespace std;class COpenNIpublic:COpenNI() context.Release();/释放空间bool Initial() /初始化status = context.Init();if(CheckError(“Context initial failed!“) return false;context.SetGlobalMirror(true);/设置镜像xmode.nXRes = 640;xmode.nYRes = 480;
12、xmode.nFPS = 30;/产生颜色 nodestatus = image_generator.Create(context);if(CheckError(“Create image generator error!“) return false;/设置颜色图片输出模式status = image_generator.SetMapOutputMode(xmode);if(CheckError(“SetMapOutputMdoe error!“) return false;/产生深度 nodestatus = depth_generator.Create(context);if(Check
13、Error(“Create depth generator error!“) return false;/设置深度图片输出模式status = depth_generator.SetMapOutputMode(xmode);if(CheckError(“SetMapOutputMdoe error!“) return false;/产生手势 nodestatus = gesture_generator.Create(context);if(CheckError(“Create gesture generator error!“) return false;/*添加手势识别的种类*/gestur
14、e_generator.AddGesture(“Wave“, NULL);gesture_generator.AddGesture(“click“, NULL);gesture_generator.AddGesture(“RaiseHand“, NULL);gesture_generator.AddGesture(“MovingHand“, NULL);/产生人体 nodestatus = user_generator.Create(context);if(CheckError(“Create gesturen generator error!“) return false;/视角校正stat
15、us = depth_generator.GetAlternativeViewPointCap().SetViewPoint(image_generator);if(CheckError(“Cant set the alternative view point on depth generator!“) return false;/设置有人进入视野的回调函数XnCallbackHandle new_user_handle;user_generator.RegisterUserCallbacks(CBNewUser, NULL, NULL, new_user_handle);user_gener
16、ator.GetSkeletonCap().SetSkeletonProfile(XN_SKEL_PROFILE_ALL);/设定使用所有关节(共 15 个)/设置骨骼校正完成的回调函数XnCallbackHandle calibration_complete;user_generator.GetSkeletonCap().RegisterToCalibrationComplete(CBCalibrationComplete, NULL, calibration_complete);return true;bool Start() status = context.StartGeneratin
17、gAll();if(CheckError(“Start generating error!“) return false;return true;bool UpdateData() status = context.WaitNoneUpdateAll();if(CheckError(“Update date error!“) return false;/获取数据image_generator.GetMetaData(image_metadata);depth_generator.GetMetaData(depth_metadata);return true;/得到色彩图像的 nodeImage
18、Generator/得到深度图像的 nodeDepthGenerator/得到人体的 nodeUserGeneratorpublic:DepthMetaData depth_metadata;ImageMetaData image_metadata;GestureGenerator gesture_generator;/外部要对其进行回调函数的设置,因此将它设为 public 类型private:/该函数返回真代表出现了错误,返回假代表正确bool CheckError(const char* error) if(status != XN_STATUS_OK ) QMessageBox:cri
19、tical(NULL, error, xnGetStatusString(status);cerr #include #include “copenni.cpp“class CSkeletonItem : public QGraphicsItempublic:/*构造函数*/CSkeletonItem(XnUserID connections01 = 1;connections10 = 1;connections11 = 2;/左手的 3 条线connections20 = 1;connections21 = 3;connections30 = 3;connections31 = 4;conn
20、ections40 = 4;connections41 = 5;/右手的 3 条线connections50 = 1;connections51 = 6;connections60 = 6;connections61 = 7;connections70 = 7;connections71 = 8;/左腿的 3 条线connections80 = 2;connections81 = 9;connections90 = 9;connections91 = 10;connections100 = 10;connections101 = 11;/右腿的 3 条线connections110 = 2;c
21、onnections111 = 12;connections120 = 12;connections121 = 13;connections130 = 13;connections131 = 14;/*更新 skeleton 里面的数据,分别获得 15 个节点的世界坐标,并转换成投影坐标*/void UpdateSkeleton() XnPoint3D joints_realworld15;joints_realworld0 = getSkeletonPos(XN_SKEL_HEAD);joints_realworld1 = getSkeletonPos(XN_SKEL_NECK);joint
22、s_realworld2 = getSkeletonPos(XN_SKEL_TORSO);joints_realworld3 = getSkeletonPos(XN_SKEL_LEFT_SHOULDER);joints_realworld4 = getSkeletonPos(XN_SKEL_LEFT_ELBOW);joints_realworld5 = getSkeletonPos(XN_SKEL_LEFT_HAND);joints_realworld6 = getSkeletonPos(XN_SKEL_RIGHT_SHOULDER);joints_realworld7 = getSkelet
23、onPos(XN_SKEL_RIGHT_ELBOW);joints_realworld8 = getSkeletonPos(XN_SKEL_RIGHT_HAND);joints_realworld9 = getSkeletonPos(XN_SKEL_LEFT_HIP);joints_realworld10 = getSkeletonPos(XN_SKEL_LEFT_KNEE);joints_realworld11 = getSkeletonPos(XN_SKEL_LEFT_FOOT);joints_realworld12 = getSkeletonPos(XN_SKEL_RIGHT_HIP);
24、joints_realworld13 = getSkeletonPos(XN_SKEL_RIGHT_KNEE);joints_realworld14 = getSkeletonPos(XN_SKEL_RIGHT_FOOT);/将世界坐标系转换成投影坐标系,一定要使用深度信息的节点openni.getDepthGenerator().ConvertRealWorldToProjective(15, joints_realworld, joints_project);public:COpenNIXnUserID/每个 CSkeletonItem 对应一个人体XnPoint3D joints_pro
25、ject15;/15 个关节点的坐标int connections142;/ int connections152;private:XnPoint3D getSkeletonPos(XnSkeletonJoint joint_name) XnSkeletonJointPosition pos;/关节点的坐标/得到指定关节名称的节点的坐标,保存在 pos 中openni.getUserGenerator().GetSkeletonCap().GetSkeletonJointPosition(user_id, joint_name, pos);return xnCreatePoint3D(pos.
26、position.X, pos.position.Y, pos.position.Z);/以 3 维坐标的形式返回节点的坐标/boudintRect 函数的重写QRectF boundingRect() const QRectF rect(joints_project0.X, joints_project0.Y, 0, 0);/定义一个矩形外围框,其长和宽都为 0for(int i = 1; i rect.right() rect.setRight(joints_projecti.X);if(joints_projecti.Y rect.bottom() rect.setBottom(join
27、ts_projecti.Y);return rect;/重绘函数的重写void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) /固定的参数形式/后面要画骨骼直接的连线,首先需要设置画笔QPen pen(QColor:fromRgb(0, 0, 255);/设置蓝色的画笔pen.setWidth(3);painter-setPen(pen);/画骨骼的线,总共是 14 条线for(unsigned int i = 0; i drawLine(p1.X, p1.Y, p2.X, p
28、2.Y);painter-setPen(QPen(Qt:yellow, 3);/每个节点处画个小圆圈for(unsigned int i = 0; i drawEllipse(QPoint(joints_projecti.X, joints_projecti.Y), 5, 5);#endifckinectreader.cpp:#include #include #include #include “copenni.cpp“ /要包含 cpp 文件,不能直接包含类#include “cskeletonitem.cpp“#include using namespace std;class CKin
29、ectReader: public QObjectpublic:/构造函数,用构造函数中的变量给类的私有成员赋值CKinectReader(COpenNI scene.removeItem(depth_item);delete p_depth_argb;bool Start(int interval = 33) openni.Start();/因为在调用 CKinectReader 这个类的之前会初始化好的,所以这里直接调用 Start 了image_item = scene.addPixmap(QPixmap();image_item-setZValue(1);depth_item = sc
30、ene.addPixmap(QPixmap();depth_item-setZValue(2);openni.UpdateData();p_depth_argb = new uchar4*openni.depth_metadata.XRes()*openni.depth_metadata.YRes();startTimer(interval);/这里是继承 QObject 类,因此可以调用该函数return true;private:COpenNI /定义引用同时没有初始化,因为在构造函数的时候用冒号来初始化QGraphicsScene QGraphicsPixmapItem *image_i
31、tem;QGraphicsPixmapItem *depth_item;uchar *p_depth_argb;vector skeletons;/CSkeletonItem 类的使用在此处得到了体现private:void timerEvent(QTimerEvent *) openni.UpdateData();/这里使用 const,是因为右边的函数返回的值就是 const 类型的const XnDepthPixel *p_depth_pixpel = openni.depth_metadata.Data();unsigned int size = openni.depth_metada
32、ta.XRes()*openni.depth_metadata.YRes();/找深度最大值点XnDepthPixel max_depth = *p_depth_pixpel;for(unsigned int i = 1; i max_depth )max_depth = p_depth_pixpeli;/将深度图像格式归一化到 0255int idx = 0;for(unsigned int i = 1; i setPixmap(QPixmap:fromImage(QImage(openni.image_metadata.Data(), openni.image_metadata.XRes(
33、), openni.image_metadata.YRes(),QImage:Format_RGB888);/往 item 中设置深度数据depth_item-setPixmap(QPixmap:fromImage(QImage(p_depth_argb, openni.depth_metadata.XRes(), openni.depth_metadata.YRes(), QImage:Format_ARGB32);/读取骨骼信息UserGenerator XnUInt16 users_num = user_generator.GetNumberOfUsers();/得到视野中人体的个数if
34、(users_num 0) XnUserID *user_id = new XnUserIDusers_num;/开辟users_num 个 XnUserID 类型的内存空间,XnUserID 其实就是一个 XnUInt32 类型user_generator.GetUsers(user_id, users_num);/将获取到的userid 放入 user_id 指向的内存中unsigned int counter = 0;SkeletonCapability /获取骨骼的 capabilityfor(int i = 0; i skeletons.size() /跟踪中人体的数目大于视野中人体
35、的数量时CSkeletonItem *p_skeleton = new CSkeletonItem(user_idi, openni);/重新创建一个骨架对象,并加入到骨架 vector 中scene.addItem(p_skeleton);/在场景中显示该骨架p_skeleton-setZValue(10);skeletons.push_back(p_skeleton);elseskeletonscounter-1-user_id = user_idi;/更新对应人体的骨架信息skeletonscounter-1-UpdateSkeleton();/调用该函数后 boundingRect()
36、函数就会一直在更新,所以 paint()函数也在不断变化skeletonscounter-1-setVisible(true);/将其他没有使用的 item 设置为不显示for(unsigned int i = counter; i setVisible(false);delete user_id;main.cpp:#include #include #include “copenni.cpp“#include “cskeletonitem.cpp“#include “ckinectreader.cpp“using namespace xn;int main(int argc, char *a
37、rgv)COpenNI openni;if(!openni.Initial()return 1;/返回 1 表示不正常返回QApplication app(argc, argv);QGraphicsScene scene;QGraphicsView view(view.resize(650, 540);/view 的尺寸比图片的输出尺寸稍微大一点view.show();CKinectReader kinect_reader(openni, scene);kinect_reader.Start();return app.exec();实验总结:通过本实验学会了简单使用 OpenNI 的库来获取人体的骨骼节点并在 Qt 中显示出来。