透過 OpenNI / NITE 分析人體骨架(上)


前面幾篇文章,基本上都是單純地讀取 OpenNI 裡的原始影像資料(深度/彩色),並沒有額外去做其他進一步的處理;而這一篇的,則是直接進入可能比較實用、也比較特別的部分,要去讀取使用 NITE 這套 middleware 分析出來的人體骨架資料了~

不過 Heresy 覺得 OpenNI 在這方面的資料其實相對少了許多,目前 Heresy 自己主要是根據 NITE 的範例程式「StickFigure」來做參考的,而整體來說,也算是摸到能正常運作而已;所以如果要看更完整的程式,也可以找這個官方的範例原始碼來看看。

接下來的部分,就是 Heresy 自己對於這部分摸出來的一些東西了。

OpenNI 人體骨架的構成

首先,OpenNI 的人體骨架基本上是由「關節」(joint)來構成的,而每一個關節都有位置(position)和方向(orientation)兩種資料;同時,這兩者也都還包含了對於這個值的「信賴度」(confidence),可以讓程式開發者知道 middleware 所判斷出來的這個關節資訊有多大的可信度。

而在目前的 OpenNI 裡,他以列舉型別的方式總共定義了 24 個關節(XnSkeletonJoint),分別是:

  • XN_SKEL_HEAD, XN_SKEL_NECK, XN_SKEL_TORSO, XN_SKEL_WAIST
  • XN_SKEL_LEFT_COLLAR, XN_SKEL_LEFT_SHOULDER, XN_SKEL_LEFT_ELBOW, XN_SKEL_LEFT_WRIST, XN_SKEL_LEFT_HAND, XN_SKEL_LEFT_FINGERTIP
  • XN_SKEL_RIGHT_COLLAR, XN_SKEL_RIGHT_SHOULDER, XN_SKEL_RIGHT_ELBOW, XN_SKEL_RIGHT_WRIST, XN_SKEL_RIGHT_HAND, XN_SKEL_RIGHT_FINGERTIP
  • XN_SKEL_LEFT_HIP, XN_SKEL_LEFT_KNEE, XN_SKEL_LEFT_ANKLE, XN_SKEL_LEFT_FOOT
  • XN_SKEL_RIGHT_HIP, XN_SKEL_RIGHT_KNEE, XN_SKEL_RIGHT_ANKLE, XN_SKEL_RIGHT_FOOT

不過雖然 OpenNI 定義了這麼多的關節,但是實際上在透過 NITE 這個 middleware 分析骨架時,其實能用的只有上面標記成紅色的十五個(可以透過 xn::SkeletonCapability 的成員函式 EnumerateActiveJoints() 取得可用的關節列表)。而如果把 NITE 有支援的關節畫成圖的話,就是下面的樣子了~

而如果再把這些關節連成線畫出來,結果就會是類似本文最右上方的那張圖裡的樣子了。

 

建立人體骨架的基本流程

要能夠在 OpenNI 的環境裡建立人體骨架,基本上是要靠所謂的 User Generator、也就是 xn::UserGenerator;而由於他的資料來源是深度影像(depth map),所以要使用的話,同時也要建立一個可用的 Depth Generator 出來才行。

不過 Heresy 個人覺得比較奇怪的是,在 OpenNI 的文件中,有提及「Production Chain」這個概念(請參考《Kinect 的軟體開發方案:OpenNI 簡介》),但是在實作時,似乎沒有提供建立這個鏈結的方法?以這邊這個例子來說,似乎只要同時有 User Generator 以及 Depth Generator 這兩種 production node,就會自動讓 User Generator 去存取 Depth Generator 的資料;而這樣的機制或許算是滿方便,但是 Heresy 比較好奇的是,如果存在兩個不同的 Depth Generator 的話,那 User Generator 會去存取哪一個的資料?不過,由於 Heresy 手邊只有一個 Kinect,所以也沒辦法做測試了。

而 User Generator 的使用方法和之前介紹過的 depth generator 或 image generator 差比較多,他主要還必須要透過 callback function 的機制,來做事件(event)的處理;而在 OpenNI 裡面,要為一個 production node 加上 callback function,基本上就是透過各種 node 提供、名稱為 RegisterXXXCallbacks() 的成員函式,來「註冊」(register)該 node 的 callback function;如果以這邊要使用的 xn::UserGenerator 來說,就是 RegisterUserCallbacks() 這個函式了。

另外由於骨架的判斷、以及判斷骨架時需要的姿勢偵測在 OpenNI 都是屬於延伸功能的「Capability」,所以在這裡所使用的 user generator 也必須要有支援 Skeleton 和 Pose Detection 這兩個 capability 才行;不過現階段所使用的 user generator 應該都是 NITE 所提供的,所以應該都會有支援。

前置的說明大概告了一個段落,接下來,就來看在 NITE 的 StickFigure 這個範例程式裡,用來建立人體骨架的標準流程了~整個進行人體骨架分析的流程大致如下圖所示,雖然可能不是很精確,但是應該算是可以用來說明了。

在上方的流程圖中,最左邊的紅色方塊是代表 user generator(xn::UserGenerator),裡面的「New User」和「Lost User」則是代表他的兩個事件的 callback function;這兩個函示分別會在「畫面內偵測到新的使用者」、「使用者離開可偵測範圍一段時間」時被呼叫。

而中間偏左的藍色方塊則是 pose detection 這個 capability(xn::PoseDetectionCapability)。在 user generator 偵測到有新的使用者、呼叫「New User」這個 callback function 時,「New User」的程式會去呼叫 pose detection 的「Start Pose Detection」、讓 pose detection 開始偵測 NITE 預先定義的校正用姿勢:「Psi」(如右圖)。在呼叫「Start Pose Detection」前,pose detection 是不會進行姿勢偵測的動作的。

當 pose detection 偵測到使用者擺出「Psi」這個姿勢後,他就會去呼叫自己的「Pose Detected」這個 callback function、以進行下一階段的動作;在這個例子裡,「Pose Detected」會去做兩件事,一個是去呼叫自己的「Stop Pose Detection」來停止繼續偵測使用者的動作、另一個則是去呼叫 skeleton 這個 capability(xn::SkeletonCapability)的「Request Calibration」函式,要求 skeleton 開始進行人體骨架的校正、分析。

xn::SkeletonCapability 的「Request Calibration」被呼叫後,skeleton 就會開始進行骨架的校正、分析。當開始進行骨架校正的時候,skeleton 會去呼叫「Calibration Start」這個 callback function,讓程式開發者可以知道接下來要開始進行骨架的校正了,如果有需要的話,可以在這邊做一些前置處理;而當骨架校正完後,則是會去呼叫「Calibration End」這個 callback function。

不過,當「Calibration End」被呼叫的時候,只代表骨架的校正、辨識的階段工作結束了,並不代表骨架辨識一定成功,也有可能是會失敗的。如果成功的話,就是要進入下一個階段、呼叫 xn::SkeletonCapability 的「StartTracking()」函式,讓系統開始去追蹤校正成功的骨架資料;而如果失敗的話,則是要再讓 pose detection 重新偵測校正姿勢,等到有偵測到校正姿勢後,再進行下一次的骨架校正。

而在骨架校正成功、並開始進行追蹤骨架後,之後只要呼叫 xn::SkeletonCapability 用來讀取關節資料的函式(例如 GetSkeletonJoint()),就可以讀取到最新的關節相關資訊,並建立整個人體的骨架資料了~

 

Callback Function 簡單說明

如果在整個流程圖裡面仔細算一下的話,可以發現整個流程下來,總共有五個不同的 callback function,分別是 xn::UserGenerator 兩個,以及 xn::PoseDetectionCapability 一個、xn::SkeletonCapability 兩個;他們分別是:

  • User Generator:
    • New UserLost User
    • 兩者形式皆為:void (XN_CALLBACK_TYPE* UserHandler)( UserGenerator& generator, XnUserID user, void* pCookie )
  • Pose Detection Capability:
    • Pose Detected
    • 形式為:void (XN_CALLBACK_TYPE* PoseDetection)( PoseDetectionCapability& pose, const XnChar* strPose, XnUserID user, void* pCookie )
  • Skeleton Capability:
    • Calibration StartCalibration End
    • 兩者形式不同,分別為:
      void (XN_CALLBACK_TYPE* CalibrationStart)( SkeletonCapability& skeleton, XnUserID user, void* pCookie )
      void (XN_CALLBACK_TYPE* CalibrationEnd)( SkeletonCapability& skeleton, XnUserID user, XnBool bSuccess, void* pCookie )

上面這五個 callback function,就是在進行人體骨架校正時,所需要用到的所有 callback fucntion、以及他們的形式了~而由於 OpenNI 有定義 XN_CALLBACK_TYPE 來定義 callback function 的 calling convention(參考 MSDN),所以在自己編寫的 callback functions,也要用同樣的形式。

不過,雖然這邊列了五個 callback function,但是其實這些 callback function 在意義上,不見得是必須的;其中「Lost User」和「Calibration Start」實際上由於沒有額外的動作,所以應該是沒有必要性;但是由於目前版本的 OpenNI 在沒有給這兩個 callback 的情況下進行骨架的校正會讓程式出問題,所以就算不想做任何事、也要給他一個空的 callback function,而不能給 NULL。這個在 Heresy 來看,應該算是 OpenNI 現行版本的錯誤,只能希望之後的版本可以修正了。

 

程式碼

前面大致把整個人體骨架校正的流程都講過了,接下來,就是看程式的部分了!下面的程式碼是 Heresy 根據 NITE 的範例程式「StickFigure」來做簡化、改寫的,裡面的輸出只有用簡單的文字輸出,來顯示目前的狀態;如果想看有圖形結果的版本,則可以直接去找 NITE 的範例來看。

#include <stdlib.h>
#include <iostream>
#include <vector>

#include <XnCppWrapper.h>

using namespace std;

// callback function of user generator: new user void XN_CALLBACK_TYPE NewUser( xn::UserGenerator& generator, XnUserID user, void* pCookie ) { cout << "New user identified: " << user << endl; generator.GetPoseDetectionCap().StartPoseDetection("Psi", user); }
// callback function of user generator: lost user void XN_CALLBACK_TYPE LostUser( xn::UserGenerator& generator, XnUserID user, void* pCookie ) { cout << "User " << user << " lost" << endl; }
// callback function of skeleton: calibration start void XN_CALLBACK_TYPE CalibrationStart( xn::SkeletonCapability& skeleton, XnUserID user, void* pCookie ) { cout << "Calibration start for user " <<  user << endl; }
// callback function of skeleton: calibration end void XN_CALLBACK_TYPE CalibrationEnd( xn::SkeletonCapability& skeleton, XnUserID user, XnBool bSuccess, void* pCookie ) { cout << "Calibration complete for user " <<  user << ", "; if( bSuccess ) { cout << "Success" << endl; skeleton.StartTracking( user ); } else { cout << "Failure" << endl; ((xn::UserGenerator*)pCookie)->GetPoseDetectionCap().StartPoseDetection( "Psi", user ); } }
// callback function of pose detection: pose start void XN_CALLBACK_TYPE PoseDetected( xn::PoseDetectionCapability& poseDetection, const XnChar* strPose, XnUserID user, void* pCookie) { cout << "Pose " << strPose << " detected for user " <<  user << endl; ((xn::UserGenerator*)pCookie)->GetSkeletonCap().RequestCalibration( user, FALSE ); poseDetection.StopPoseDetection( user ); }
int main( int argc, char** argv ) { // 1. initial context xn::Context mContext; mContext.Init(); // 2. map output mode XnMapOutputMode mapMode; mapMode.nXRes = 640; mapMode.nYRes = 480; mapMode.nFPS = 30; // 3. create depth generator xn::DepthGenerator mDepthGenerator; mDepthGenerator.Create( mContext ); mDepthGenerator.SetMapOutputMode( mapMode ); // 4. create user generator xn::UserGenerator mUserGenerator; mUserGenerator.Create( mContext );
// 5. Register callback functions of user generator XnCallbackHandle hUserCB; mUserGenerator.RegisterUserCallbacks( NewUser, LostUser, NULL, hUserCB ); // 6. Register callback functions of skeleton capability xn::SkeletonCapability mSC = mUserGenerator.GetSkeletonCap(); mSC.SetSkeletonProfile( XN_SKEL_PROFILE_ALL ); XnCallbackHandle hCalibCB; mSC.RegisterCalibrationCallbacks( CalibrationStart, CalibrationEnd, &mUserGenerator, hCalibCB ); // 7. Register callback functions of Pose Detection capability XnCallbackHandle hPoseCB; mUserGenerator.GetPoseDetectionCap().RegisterToPoseCallbacks( PoseDetected, NULL, &mUserGenerator, hPoseCB );
// 8. start generate data mContext.StartGeneratingAll(); while( true ) { // 9. Update date mContext.WaitAndUpdateAll();
// 10. get user information XnUInt16 nUsers = mUserGenerator.GetNumberOfUsers(); if( nUsers > 0 ) { // 11. get users XnUserID* aUserID = new XnUserID[nUsers]; mUserGenerator.GetUsers( aUserID, nUsers ); // 12. check each user for( int i = 0; i < nUsers; ++i ) { // 13. if is tracking skeleton if( mSC.IsTracking( aUserID[i] ) ) { // 14. get skeleton joint data XnSkeletonJointTransformation mJointTran; mSC.GetSkeletonJoint( aUserID[i], XN_SKEL_HEAD, mJointTran ); // 15. output information cout << "The head of user " << aUserID[i] << " is at ("; cout << mJointTran.position.position.X << ", "; cout << mJointTran.position.position.Y << ", "; cout << mJointTran.position.position.Z << ")" << endl; } } delete [] aUserID; }
} // 16. stop and shutdown mContext.StopGeneratingAll(); mContext.Shutdown(); return 0; }

在這段程式碼裡,一開始的五個函式,就是這邊要給 OpenNI 用的 callback function 了~他們分別是給 user generator 用的 NewUser()LostUser(),給 skeleton capability 用的 CalibrationStart()CalibrationEnd() 和給 pose detection capability 用的 PoseDetected()。而這幾個函式在這邊至少都有透過 cout 來輸出現在的狀態,做為測試以及錯誤偵測的依據。不過其中,NewUser()PoseDetected()CalibrationEnd() 是還有其他功能的~而這些功能,在前面其實已經有提過了,在之後也還會再做解釋。

大概先帶過 callback function 的部分,接下來,就繼續看 main() 的內容了~

首先,「1. initial context」、「2. map output mode」、「3. create depth generator」的部分,和之前都是一樣的,只是稍微把錯誤偵測的部分省略掉,所以在這邊就不特別做說明了。而「4. create user generator」的部分,也就是建立一個xn::UserGenerator 的 production node 物件:mUserGenerator 了;他基本的建立方法和 xn::DephGenerator 也是相同的,一樣是透過 Create() 這個函式來建立。

接下來程式裡面比較特別的,就是程式碼裡的 5 – 7、用來設定 callback function 的部分了~這一部分,包括註冊 callback function 以及每一個 callback function 的內容,請跳到下一篇文章的「Callback Function 的細節」這個段落、參考比較詳細的說明。

而到了「8. start generate data」時,整個 OpenNI 要做人體骨架分析的環境已經算是建置完成了~接下來,基本上也和之前的程式相同類似,先透過 context 的 StartGeneratingAll() 來開始產生資料、再透過 WaitAndUpdateAll() 來更新各個 production node 的資料了~

這部分的寫法,Heresy 也還和之前的範例相同,用一個無窮迴圈來跑、不停地進行資料的更新。不過這邊可能要注意一下的是,雖然 user generator 的 callback 是使採用事件導向(event driven)的方式來進行的,但是如果沒有不停地去執行 WaitAndUpdateAll() 來更新 production node 的資料的話,似乎是不會有任何 event 產生的

再來「10. get user information」開始的部分,就是要讀取 user generator 所抓出來的人體骨架資料了!而這一部分,也等到下一篇再來講了~


OpenNI / Kinect 相關文章目錄

對「透過 OpenNI / NITE 分析人體骨架(上)」的想法

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料