前言
本文主要介绍使用OpenNI中的HandsGenerator来完成对人体手部的跟踪,在前面的文章Kinect+OpenNI学习笔记之5(使用OpenNI自带的类进行简单手势识别)中已经介绍过使用GestureGenerator这个类来完成对几个简单手势的识别,这次介绍的手部跟踪是在上面简单手势识别的结果上开始跟踪的,这是OpenNI的优点,微软的SDK据说是不能单独对手部进行跟踪,因为使用MS的SDK需要检测站立人体的骨骼,然后找出节点再进行跟踪,不懂最新版本的是否支持这个功能。而此节讲的OpenNI完成手部的跟踪就不要求人必须处于站立姿势。
开发环境:QtCreator2.5.1+OpenNI1.5.4.0+Qt4.8.2
实验说明
本次实验是分为2个类来设计的。COpenNI和CKinectReader这2个类。COpenNI类负责完成Kinect的OpenNI驱动,而CKinectReader类负责将kinect读取的信息在Qt中显示出来,且使用定时器定时刷新,此过程中可以在图像中画内容。
进行手势的整体流程大概如下:
有关手势识别和跟踪的回调函数的设置在COpenNI这个类中进行,但是因为回调函数是static类型,所以对应函数里面的变量也必须是static类型,但是我们的变量初始化又放在了类中进行,而static类型的变量不能在类中进行初始化,因此最好将回调函数用到的几个static类型的变量直接放在了类外,这样虽然达到了效果,不过貌似不是一个完整的类的设计。暂时没找到好的解决方法。
从官方文档来看,OpenNI中进行手部跟踪,即采用节点hand generator来跟踪需要搭配手势检测的节点gesture generator,其代码实现流程如下:
先使用gesture generator来侦测特定的手势
当检测到特定的手势后开始进行handsgenerator的starttracking()函数来进行跟踪手部。
当hands generator开始跟踪手部位置时,HandCreate()函数被调用。
以后每当有变化的时候,都会执行HandUpdate()函数。
如果手势超出了可侦测的范围,则其回自动调用HandDestroy()函数。
C/c++知识点总结:
如果一个数据类型声明为auto了,那么说明该数据类型为local局部变量,一般auot关键字可以省略。
map表示的是一个键值对,其中第一个参数为键值对的类型id,这个具有唯一性,第二个是该数据类型的对应值。map的cbegin()方法表示的是返回一个常量迭代器。
array数据类型其实就是一个数组类型,定义它的时为array<int, n>表示,其长度为n,数组中的元素数据类型为int型。
static函数有点类似回调函数,一般是用来记录类对象被引用的次数或者这个函数的地址需要被外部代码调用。静态函数有2个好处,一是只能被其自己的文件使用,不能被其它的文件使用。二是其它文件可以定义相同名字的函数,不会发生冲突。
如果是在类中使用静态函数,则它是为类服务的而不是为了某一个类的具体对象服务。普通的成员函数都隐含了一个this指针,因为普通成员函数总是与具体的某个类的具体对象的。但静态成员函数由于不是与任何对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的某个非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。
由于在本程序中,需要用到回调函数,而回调函数在类中一般需要声明成静态函数,所以在回调函数中调用类的成员变量时这些变量不能够是非静态的成员变量,编程时一定要注意。比如说在回调函数中有代码hands_generator.StartTracking(*pIDPosition);其中hands_generaotr是普通私有变量,这时编译代码时会出现如下错误提示:
另外类中的静态成员变量是属于类的,不是属于对象的,因此在定义对象的时候不能够对其进行初始化,也就是说不能够用构造函数来初始化它,如果在类外来初始化它,应该加上在变量前加上类名,而不是变量名。
Qt知识点总结:
如果需要用QPainter来绘图的话,则需要将绘图部分的代码放在begin()和end()方法中,外,用QPainter 来创建一个新的绘图类时,其内部已经隐含了具有begin()方法。
实验结果:
这是实验中截图的一张结果,该实验可以同时跟踪多个手部,每一个使用不同的颜色来显示其轨迹,当识别到手部后,可以使用手指在空中写字。
实验主要部分代码及注释(附录有实验工程code下载地址):
copenni.cpp:
#ifndef COPENNI_CLASS #define COPENNI_CLASS #include <XnCppWrapper.h> #include <QtGui/QtGui> #include <iostream> #include <map> using namespace xn; using namespace std; static DepthGenerator depth_generator; static HandsGenerator hands_generator; static std::map<XnUserID, vector<XnPoint3D>> hands_track_points; 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 = hands_generator.Create(context); if(CheckError("Create hand generaotr error!")) { return false; } //产生人体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 gesture_cb; gesture_generator.RegisterGestureCallbacks(CBGestureRecognized, CBGestureProgress, NULL, gesture_cb); //设置于手部有关的回调函数 XnCallbackHandle hands_cb; hands_generator.RegisterHandCallbacks(HandCreate, HandUpdate, HandDestroy, NULL, hands_cb); //设置有人进入视野的回调函数 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; } //得到手势姿势node GestureGenerator& getGestureGenerator() { return gesture_generator; } public: DepthMetaData depth_metadata; ImageMetaData image_metadata; // static std::map<XnUserID, vector<XnPoint3D>> hands_track_points; 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 CBGestureRecognized(xn::GestureGenerator &generator, const XnChar *strGesture, const XnPoint3D *pIDPosition, const XnPoint3D *pEndPosition, void *pCookie) { hands_generator.StartTracking(*pIDPosition); } //手势开始检测的回调函数 static void XN_CALLBACK_TYPE CBGestureProgress(xn::GestureGenerator &generator, const XnChar *strGesture, const XnPoint3D *pPosition, XnFloat fProgress, void *pCookie) { hands_generator.StartTracking(*pPosition); } //手部开始建立的回调函数 static void XN_CALLBACK_TYPE HandCreate(HandsGenerator& rHands, XnUserID xUID, const XnPoint3D* pPosition, XnFloat fTime, void* pCookie) { XnPoint3D project_pos; depth_generator.ConvertRealWorldToProjective(1, pPosition, &project_pos); pair<XnUserID, vector<XnPoint3D>> hand_track_point(xUID, vector<XnPoint3D>()); hand_track_point.second.push_back(project_pos); hands_track_points.insert(hand_track_point); } //手部开始更新的回调函数 static void XN_CALLBACK_TYPE HandUpdate(HandsGenerator& rHands, XnUserID xUID, const XnPoint3D* pPosition, XnFloat fTime, void* pCookie) { XnPoint3D project_pos; depth_generator.ConvertRealWorldToProjective(1, pPosition, &project_pos); hands_track_points.find(xUID)->second.push_back(project_pos); } //销毁手部的回调函数 static void XN_CALLBACK_TYPE HandDestroy(HandsGenerator& rHands, XnUserID xUID, XnFloat fTime, void* pCookie) { hands_track_points.erase(hands_track_points.find(xUID)); } //有人进入视野时的回调函数 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; ImageGenerator image_generator; UserGenerator user_generator; GestureGenerator gesture_generator; XnMapOutputMode xmode; }; #endif
ckinectreader.cpp:
#include <QtGui> #include <QDebug> #include <XnCppWrapper.h> #include "copenni.cpp" //要包含cpp文件,不能直接包含类 #include <iostream> #include <QPen> #include <array> using namespace std; class CKinectReader: public QObject { public: //构造函数,用构造函数中的变量给类的私有成员赋值 CKinectReader(COpenNI &openni, QGraphicsScene &scene) : openni(openni), scene(scene) { test = 0.0; { //设置画笔的颜色 pen_array[0].setWidth(3); pen_array[1].setColor(QColor::fromRgb( 255, 0, 0, 128 )); pen_array[1].setWidth( 3 ); pen_array[1].setColor( QColor::fromRgb( 0, 255, 0, 128 ) ); pen_array[2].setWidth( 3 ); pen_array[2].setColor( QColor::fromRgb( 0, 0, 255, 128 ) ); pen_array[3].setWidth( 3 ); pen_array[3].setColor( QColor::fromRgb( 255, 0, 255, 128 ) ); pen_array[4].setWidth( 3 ); pen_array[4].setColor( QColor::fromRgb( 255, 255, 0, 128 ) ); pen_array[5].setWidth( 3 ); pen_array[5].setColor( QColor::fromRgb( 0, 255, 255, 128 ) ); } } ~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; } float test ; private: COpenNI &openni; //定义引用同时没有初始化,因为在构造函数的时候用冒号来初始化 QGraphicsScene &scene; QGraphicsPixmapItem *image_item; QGraphicsPixmapItem *depth_item; uchar *p_depth_argb; array<QPen, 6> pen_array; 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(); QImage draw_depth_image; //找深度最大值点 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]; test = max_depth; //将深度图像格式归一化到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++是一样的 } draw_depth_image = QImage(p_depth_argb, openni.depth_metadata.XRes(), openni.depth_metadata.YRes() , QImage::Format_ARGB32); //该句隐含的调用了begin()方法 QPainter painter(&draw_depth_image); { for(auto itUser = hands_track_points.cbegin(); itUser != hands_track_points.cend(); ++itUser) { painter.setPen(pen_array[itUser->first % pen_array.size()]);//设置画笔的颜色,按照一定的顺序循环不同的颜色 const vector<XnPoint3D> &points = itUser->second; for(vector<XnPoint3D>::size_type i = 1; i < points.size(); ++i) painter.drawLine(points[i-1].X, points[i-1].Y, points[i].X, points[i].Y);//画出轨迹线 } } painter.end();//绘图结束 //往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))); depth_item->setPixmap(QPixmap::fromImage(draw_depth_image)); } };
main.cpp:
#include <QtGui/QtGui> #include "copenni.cpp" #include "ckinectreader.cpp" int main( int argc, char **argv ) { COpenNI openni; if(!openni.Initial())//初始化返回1表示初始化成功 return 1; QApplication app(argc, argv); QGraphicsScene scene; QGraphicsView view; view.setScene(&scene); view.resize(650, 540); view.show(); CKinectReader kinect_reader(openni, scene); kinect_reader.Start();//启动,读取数据 return app.exec(); }
实验总结:通过本次实验可以学会使用openni中手势跟踪的实现。
参考资料:
附录:实验工程code下载。