5、 透過 OpenNI / NITE 分析人體骨架(上)(非原创)
http://viml.nchc.org.tw/blog/paper_info.php?CLASS_ID=1&SUB_ID=1&PAPER_ID=219
前面幾篇文章,基本上都是單純地讀取 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 User、Lost 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 Start、Calibration 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
bool bGetSkeleton = false;
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 所抓出來的人體骨架資料了!而這一部分,也等到下一篇再來講了∼