Kinect+OpenNI学习笔记之6(获取人体骨架并在Qt中显示)
前言
MS的kinec SDK和OpenNI都提供了人体骨骼跟踪的算法,人体骨骼跟踪算法在kinect人体行为识别中非常重要,该识别过程通常被用来作为行为识别的第一步,比如说,通过定位人体中的骨骼支架,可以提取出人手的部位,从而可以把手的部分单独拿出来分析,这样就达到了手势的定位,而后面的手势识别则可以在刚刚定位出的领域进行处理。总而言之,一套有效的人体骨架追踪算法在kinect的一系列应用中非常有用,不过MS SDK和OpenNI虽然都提供了该算法类的直调用,但是其源码并没有开放,毕竟这是人家最核心的东东。
开发环境:QtCreator2.5.1+OpenNI1.5.4.0+Qt4.8.2
实验说明
在老版本的OpenNI中,要对人进行骨架追踪,需要人先摆出PSI的姿势,然后系统根据该姿势进行骨骼校正,待校正完成后才进行骨骼的跟踪,其流程图可以参考下面的图:
由图可以看出,其完成骨骼跟踪主要分为3个部分,首先需检测到人体,然后需要固定的PSI姿势来对人体的姿势进行校正,待姿势校正完成后,才能进行人体骨骼的追踪。如果程序开发者用代码实现该过程则可以参考hersey的文章透过OpenNI / NITE 分析人体骨架(上)和透过OpenNI / NITE 分析人体骨架(下),作者在这2篇文章详细介绍了老版本的人体骨架的OpenNI实现。
在新版本OpenNI1.5以后,人体骨架追踪算法更改了不少,其中最大的特点就是骨架跟踪过程中少了姿势校正的那一步骤,新版本中只需要人体站起来就可以进行跟踪了,使用起来方便很多,程序开发也简单不少。另外人体骨骼跟踪的效果也提高了不少,一旦骨骼追踪成功后,即使人体没有保持站立姿势有时候也还是可以继续跟踪的。新版本的人体骨骼跟踪算法使用流程图如下:
下面来看看程序中的Capability,它不同于前面文章的generator:
在进行骨架的判断和姿态检测是需要用到OpenNI延伸的功能,与这种延伸功能相关的类可以称作为Capability。在进行人体骨骼分析时,user generator需要有支援Skeleton和Pose Detection这2个的capability。
在程序中需要绘制骨骼节点之间的连线,而节点的坐标和法向都有函数可以获得,获得的坐标为真实世界中的坐标,画图时需要在平面上绘制,因此需要这2个坐标系的转换,转换过程用到下面的函数:
XnStatus xn::DepthGenerator::ConvertRealWorldToProjective(XnUInt32 nCount, const XnPoint3D aRealWorld[], XnPoint3D aProjective[])
该函数表示将深度图获取的真实坐标系转换成平面图形显示的投影坐标系上。第1个参数表示转换坐标点的个数,第2个参数表示真实坐标系中的坐标,第3个参数表示投影坐标系下的坐标。
本实验的程序分为3个类和一个主函数,其中2个类的基本部分在前面的文章中已有介绍,只需要更新其部分功能。下面是本实验中这3个类的设计。当然这都是参考heresy的博客使用Qt 显示OpenNI 的人体骨架。
COpenNI类的更新:
因为需要对人体进行骨骼跟踪,所以需要用到OpenNI的UserGenerator这个类。在private变量一栏增加这个类对象的声明。然后在类的Init()函数中使用Create方法产生人体的node。同上一篇博客Kinect+OpenNI学习笔记之5(使用OpenNI自带的类进行简单手势识别)中类似,这里的人体骨架校正,跟踪等都是通过回调函数的形式进行的,因此还需要在Init()函数中设置这个node的检测到有新人进入和骨骼校正完成的回调函数(其实还有旧人体目标离去,骨骼校正开始这2个也可以设置回调函数,但在本程序中因为不需要使用它们,因此可以省略不写,老版本的OpenNI是不允许省略的)。另外,由于色彩节点,深度节点,以及人体检测节点都是私有变量,如果该类的对象需要获取该变量的话不方便,因此在共有函数部分分别设置了3个共有函数来获取这3个变量。具体该类的全部代码参加本文后面的代码部分。
CSkeletonItem类的设计:
CSkeletonItem这个类主要来完成骨架节点位置的获取,以及画出item中节点之间的连线,同时也在节点位置处画出圆圈代表对应节点的位置。
在构造函数中,设计了一个二维的连接表矩阵,矩阵的大小为14*2,即有14条边,每条边有2个顶点,矩阵中对应位置的值表示的是对应边的节点骨架的标号,在OpenNI中人体的骨架节点共分为15个,手脚共12个,头部2个,躯干1个。如下图所示:
程序中对这15个点编了序号,头部为0, 颈部为1, 躯干为2, 左肩膀为3, 左手肘为4, 左手腕5,右肩膀为6,右手肘为7,右手腕为8,左臀为9,左膝盖为10,左脚跟为11,右臀为12,右膝盖为13,右脚跟为14。
该类中需要重写的boundingRect()函数,函数中设置了一个包含15个节点的最小矩形,因为后面的绘图区域需要在这个矩形内进行,很明显,获得的这个矩形不是固定大小的,而是根据人体骨架的位置在不断变化。大小和位置同时都会发生改变。
重写的paint()函数则需要完成2个部分的功能, 第一是画出骨骼中节点的位置,用圆圈显示;第二是画出2个节点之间的连线,共14条,这样通过画出的连线就可以大概看出人的位置和区域了。本文是参考的heresy文章不用校正姿势的NITE 1.5 ,heresy在设计该类的构造函数时,设计了个15*2的连接表,个人感觉设置为14*2比较合理,因为15个点刚好由14条线可以连接起来,并不是heresy所说的15条线,其实它有2条线是重合的。
CKinectReader类的更新:
该类是在前面的文章Kinect+OpenNI学习笔记之3(获取kinect的数据并在Qt中显示的类的设计)中对应类的更新,前面博文中的该类只是完成了深度图像和颜色图像的显示,而在本实验中,需要完成显示骨架节点之间的连线图,因此该类需要继续更新。其实现过程主要是获取视野中人体的个数,对检测到的每个人体然后调用CSkeletonItem类中的方法UpdateSkeleton()来更新读取的节点坐标,因为一旦坐标值发生了改变,CSkeletonItem类中的boundingRect()内容也会更改,从而其Item所在区域的矩形也会变化,最后导致paint()函数的执行,在paint()函数中完成骨骼节点连线和骨骼节点的绘图。
实验结果
试验效果的截图:
蓝色的线表示骨骼节点之间的连线,黄色的圈表示骨骼节点。
实验主要部分代码及注释(附录有实验工程code下载链接地址):
copenni.cpp:
#ifndef COPENNI_CLASS #define COPENNI_CLASS #include <XnCppWrapper.h> #include <QtGui/QtGui> #include <iostream> using namespace xn; using namespace std; class COpenNI { public: ~COpenNI() { context.Release();//释放空间 } bool Initial() { //初始化 status = context.Init(); if(CheckError("Context initial failed!")) { return false; } context.SetGlobalMirror(true);//设置镜像 xmode.nXRes = 640; xmode.nYRes = 480; xmode.nFPS = 30; //产生颜色node status = image_generator.Create(context); if(CheckError("Create image generator error!")) { return false; } //设置颜色图片输出模式 status = image_generator.SetMapOutputMode(xmode); if(CheckError("SetMapOutputMdoe error!")) { return false; } //产生深度node status = depth_generator.Create(context); if(CheckError("Create depth generator error!")) { return false; } //设置深度图片输出模式 status = depth_generator.SetMapOutputMode(xmode); if(CheckError("SetMapOutputMdoe error!")) { return false; } //产生手势node status = gesture_generator.Create(context); if(CheckError("Create gesture generator error!")) { return false; } /*添加手势识别的种类*/ gesture_generator.AddGesture("Wave", NULL); gesture_generator.AddGesture("click", NULL); gesture_generator.AddGesture("RaiseHand", NULL); gesture_generator.AddGesture("MovingHand", NULL); //产生人体node status = user_generator.Create(context); if(CheckError("Create gesturen generator error!")) { return false; } //视角校正 status = depth_generator.GetAlternativeViewPointCap().SetViewPoint(image_generator); if(CheckError("Can't set the alternative view point on depth generator!")) { return false; } //设置有人进入视野的回调函数 XnCallbackHandle new_user_handle; user_generator.RegisterUserCallbacks(CBNewUser, NULL, NULL, new_user_handle); user_generator.GetSkeletonCap().SetSkeletonProfile(XN_SKEL_PROFILE_ALL);//设定使用所有关节(共15个) //设置骨骼校正完成的回调函数 XnCallbackHandle calibration_complete; user_generator.GetSkeletonCap().RegisterToCalibrationComplete(CBCalibrationComplete, NULL, calibration_complete); return true; } bool Start() { status = context.StartGeneratingAll(); 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; } //得到色彩图像的node ImageGenerator& getImageGenerator() { return image_generator; } //得到深度图像的node DepthGenerator& getDepthGenerator() { return depth_generator; } //得到人体的node UserGenerator& getUserGenerator() { return user_generator; } public: DepthMetaData depth_metadata; ImageMetaData image_metadata; GestureGenerator gesture_generator;//外部要对其进行回调函数的设置,因此将它设为public类型 private: //该函数返回真代表出现了错误,返回假代表正确 bool CheckError(const char* error) { if(status != XN_STATUS_OK ) { QMessageBox::critical(NULL, error, xnGetStatusString(status)); cerr << error << ": " << xnGetStatusString( status ) << endl; return true; } return false; } //有人进入视野时的回调函数 static void XN_CALLBACK_TYPE CBNewUser(UserGenerator &generator, XnUserID user, void *p_cookie) { //得到skeleton的capability,并调用RequestCalibration函数设置对新检测到的人进行骨骼校正 generator.GetSkeletonCap().RequestCalibration(user, true); } //完成骨骼校正的回调函数 static void XN_CALLBACK_TYPE CBCalibrationComplete(SkeletonCapability &skeleton, XnUserID user, XnCalibrationStatus calibration_error, void *p_cookie) { if(calibration_error == XN_CALIBRATION_STATUS_OK) { skeleton.StartTracking(user);//骨骼校正完成后就开始进行人体跟踪了 } else { UserGenerator *p_user = (UserGenerator*)p_cookie; skeleton.RequestCalibration(user, true);//骨骼校正失败时重新设置对人体骨骼继续进行校正 } } private: XnStatus status; Context context; DepthGenerator depth_generator; ImageGenerator image_generator; UserGenerator user_generator; XnMapOutputMode xmode; }; #endif
cskeletonitem.cpp:
#ifndef CSKELETONITEM_CLASS #define CSKELETONITEM_CLASS #include <QtGui> #include <XnCppWrapper.h> #include "copenni.cpp" class CSkeletonItem : public QGraphicsItem { public: /*构造函数*/ CSkeletonItem(XnUserID &user_id, COpenNI& openni) : QGraphicsItem(), user_id(user_id), openni(openni) { /*创建关节相连的二维表 connections[i]表示第i条线(2个节点之间表示一条线),connections[i][0]和connections[i][1]分别表示 第i条线的2个端点*/ //头部和身体的2条线 { connections[0][0] = 0; connections[0][1] = 1; connections[1][0] = 1; connections[1][1] = 2; } //左手的3条线 { connections[2][0] = 1; connections[2][1] = 3; connections[3][0] = 3; connections[3][1] = 4; connections[4][0] = 4; connections[4][1] = 5; } //右手的3条线 { connections[5][0] = 1; connections[5][1] = 6; connections[6][0] = 6; connections[6][1] = 7; connections[7][0] = 7; connections[7][1] = 8; } //左腿的3条线 { connections[8][0] = 2; connections[8][1] = 9; connections[9][0] = 9; connections[9][1] = 10; connections[10][0] = 10; connections[10][1] = 11; } //右腿的3条线 { connections[11][0] = 2; connections[11][1] = 12; connections[12][0] = 12; connections[12][1] = 13; connections[13][0] = 13; connections[13][1] = 14; } } /*更新skeleton里面的数据,分别获得15个节点的世界坐标,并转换成投影坐标*/ void UpdateSkeleton() { XnPoint3D joints_realworld[15]; joints_realworld[0] = getSkeletonPos(XN_SKEL_HEAD); joints_realworld[1] = getSkeletonPos(XN_SKEL_NECK); joints_realworld[2] = getSkeletonPos(XN_SKEL_TORSO); joints_realworld[3] = getSkeletonPos(XN_SKEL_LEFT_SHOULDER); joints_realworld[4] = getSkeletonPos(XN_SKEL_LEFT_ELBOW); joints_realworld[5] = getSkeletonPos(XN_SKEL_LEFT_HAND); joints_realworld[6] = getSkeletonPos(XN_SKEL_RIGHT_SHOULDER); joints_realworld[7] = getSkeletonPos(XN_SKEL_RIGHT_ELBOW); joints_realworld[8] = getSkeletonPos(XN_SKEL_RIGHT_HAND); joints_realworld[9] = getSkeletonPos(XN_SKEL_LEFT_HIP); joints_realworld[10] = getSkeletonPos(XN_SKEL_LEFT_KNEE); joints_realworld[11] = getSkeletonPos(XN_SKEL_LEFT_FOOT); joints_realworld[12] = getSkeletonPos(XN_SKEL_RIGHT_HIP); joints_realworld[13] = getSkeletonPos(XN_SKEL_RIGHT_KNEE); joints_realworld[14] = getSkeletonPos(XN_SKEL_RIGHT_FOOT); //将世界坐标系转换成投影坐标系,一定要使用深度信息的节点 openni.getDepthGenerator().ConvertRealWorldToProjective(15, joints_realworld, joints_project); } public: COpenNI& openni; XnUserID& user_id;//每个CSkeletonItem对应一个人体 XnPoint3D joints_project[15];//15个关节点的坐标 int connections[14][2]; // int connections[15][2]; private: XnPoint3D getSkeletonPos(XnSkeletonJoint joint_name) { XnSkeletonJointPosition pos;//关节点的坐标 //得到指定关节名称的节点的坐标,保存在pos中 openni.getUserGenerator().GetSkeletonCap().GetSkeletonJointPosition(user_id, joint_name, pos); return xnCreatePoint3D(pos.position.X, pos.position.Y, pos.position.Z);//以3维坐标的形式返回节点的坐标 } //boudintRect函数的重写 QRectF boundingRect() const { QRectF rect(joints_project[0].X, joints_project[0].Y, 0, 0);//定义一个矩形外围框,其长和宽都为0 for(int i = 1; i < 15; i++) { //下面的代码是找出能够围住15个节点的最小矩形框 //rect.left()等返回的是一个实数 if(joints_project[i].X < rect.left()) { //小于矩形框左边点的横坐标时 rect.setLeft(joints_project[i].X); } if(joints_project[i].X > rect.right()) { rect.setRight(joints_project[i].X); } if(joints_project[i].Y < rect.top()) { rect.setTop(joints_project[i].Y); } if(joints_project[i].Y > rect.bottom()) { rect.setBottom(joints_project[i].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 < 14; i++) { XnPoint3D &p1 = joints_project[connections[i][0]]; XnPoint3D &p2 = joints_project[connections[i][1]]; painter->drawLine(p1.X, p1.Y, p2.X, p2.Y); } painter->setPen(QPen(Qt::yellow, 3)); //每个节点处画个小圆圈 for(unsigned int i = 0; i < 15; i++ ) { painter->drawEllipse(QPoint(joints_project[i].X, joints_project[i].Y), 5, 5); } } }; #endif
ckinectreader.cpp:
#include <QtGui> #include <QDebug> #include <XnCppWrapper.h> #include "copenni.cpp" //要包含cpp文件,不能直接包含类 #include "cskeletonitem.cpp" #include <iostream> using namespace std; class CKinectReader: public QObject { public: //构造函数,用构造函数中的变量给类的私有成员赋值 CKinectReader(COpenNI &openni, QGraphicsScene &scene) : openni(openni), scene(scene) { } ~CKinectReader() { scene.removeItem(image_item); 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 = scene.addPixmap(QPixmap()); depth_item->setZValue(2); openni.UpdateData(); p_depth_argb = new uchar[4*openni.depth_metadata.XRes()*openni.depth_metadata.YRes()]; startTimer(interval);//这里是继承QObject类,因此可以调用该函数 return true; } private: COpenNI &openni; //定义引用同时没有初始化,因为在构造函数的时候用冒号来初始化 QGraphicsScene &scene; QGraphicsPixmapItem *image_item; QGraphicsPixmapItem *depth_item; uchar *p_depth_argb; vector<CSkeletonItem*> skeletons;//CSkeletonItem类的使用在此处得到了体现 private: void timerEvent(QTimerEvent *) { openni.UpdateData(); //这里使用const,是因为右边的函数返回的值就是const类型的 const XnDepthPixel *p_depth_pixpel = openni.depth_metadata.Data(); unsigned int size = openni.depth_metadata.XRes()*openni.depth_metadata.YRes(); //找深度最大值点 XnDepthPixel max_depth = *p_depth_pixpel; for(unsigned int i = 1; i < size; ++i) if(p_depth_pixpel[i] > max_depth ) max_depth = p_depth_pixpel[i]; //将深度图像格式归一化到0~255 int idx = 0; for(unsigned int i = 1; i < size; ++i) { //一定要使用1.0f相乘,转换成float类型,否则该工程的结果会有错误,因为这个要么是0,要么是1,0的概率要大很多 float fscale = 1.0f*(*p_depth_pixpel)/max_depth; if((*p_depth_pixpel) != 0) { p_depth_argb[idx++] = 255*(1-fscale); //蓝色分量 p_depth_argb[idx++] = 0; //绿色分量 p_depth_argb[idx++] = 255*fscale; //红色分量,越远越红 p_depth_argb[idx++] = 255*(1-fscale); //距离越近,越不透明 } else { p_depth_argb[idx++] = 0; p_depth_argb[idx++] = 0; p_depth_argb[idx++] = 0; p_depth_argb[idx++] = 255; } ++p_depth_pixpel;//此处的++p_depth_pixpel和p_depth_pixpel++是一样的 } //往item中设置图像色彩数据 image_item->setPixmap(QPixmap::fromImage( QImage(openni.image_metadata.Data(), openni.image_metadata.XRes(), 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 &user_generator = openni.getUserGenerator(); XnUInt16 users_num = user_generator.GetNumberOfUsers();//得到视野中人体的个数 if(users_num > 0) { XnUserID *user_id = new XnUserID[users_num];//开辟users_num个XnUserID类型的内存空间,XnUserID其实就是一个XnUInt32类型 user_generator.GetUsers(user_id, users_num);//将获取到的userid放入user_id指向的内存中 unsigned int counter = 0; SkeletonCapability &skeleton_capability = user_generator.GetSkeletonCap();//获取骨骼的capability for(int i = 0; i < users_num; i++) { if(skeleton_capability.IsTracking(user_id[i])) { ++counter; if(counter > skeletons.size()) { //跟踪中人体的数目大于视野中人体的数量时 CSkeletonItem *p_skeleton = new CSkeletonItem(user_id[i], openni);//重新创建一个骨架对象,并加入到骨架vector中 scene.addItem(p_skeleton);//在场景中显示该骨架 p_skeleton->setZValue(10); skeletons.push_back(p_skeleton); } else skeletons[counter-1]->user_id = user_id[i]; //更新对应人体的骨架信息 skeletons[counter-1]->UpdateSkeleton(); //调用该函数后boundingRect()函数就会一直在更新,所以paint()函数也在不断变化 skeletons[counter-1]->setVisible(true); } } //将其他没有使用的item设置为不显示 for(unsigned int i = counter; i < skeletons.size(); ++i) { skeletons[i]->setVisible(false); } delete [] user_id; } } };
main.cpp:
#include <QtGui> #include <QtCore> #include "copenni.cpp" #include "cskeletonitem.cpp" #include "ckinectreader.cpp" using namespace xn; int main(int argc, char **argv) { COpenNI openni; if(!openni.Initial()) return 1;//返回1表示不正常返回 QApplication app(argc, argv); QGraphicsScene scene; QGraphicsView view(&scene); view.resize(650, 540);//view的尺寸比图片的输出尺寸稍微大一点 view.show(); CKinectReader kinect_reader(openni, scene); kinect_reader.Start(); return app.exec(); }
实验总结:
通过本实验学会了简单使用OpenNI的库来获取人体的骨骼节点并在Qt中显示出来。
参考资料:
附录:实验工程code下载。