前言
本文主要介紹使用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下載。