前言
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下載。