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


延續上一篇,這一篇主要就是把之前礙於篇幅而先跳過的部分,做個完整的說明了~而基本上,主要的內容會分兩大部分,第一部分是「Callback Function 的細節」,來講設定 callback function 的細節;第二部分,則是「讀取骨架資料」、也就是之前 main() 裡面迴圈裡的程式了。

而在文章的最後,還有一小段,是大概描述一下這個程式執行後的狀況。

Callback Function 的細節

User Generator

在「5. Register callback functions of user generator」的部分,是使用 xn::UserGenerator 的成員函式 RegisterUserCallbacks(),來進行 callback function 的「註冊」(register);這個函式他有四個參數,他們的型別和意義分別是:

  • UserHandler NewUserCB:偵測到新使用者的 callback function
  • UserHandler LostUserCB:使用者消失的 callback function
  • void* pCookie:額外傳遞給 callback function 使用的資料
  • XnCallbackHandle& hCallback:管理 callback function 用的 handle 值,如果要取消 callback function 設定時,需要用到這個變數。

而在這裡,我們給他註冊的兩個 callback function,就是之前定義的 NewUser()LostUser(),由於這兩個 callback function 都不需要其他的資料,所以 pCookie 就直接給他 NULL 就可以了;而最後的 callback handle,雖然在這個程式裡沒有打算取消註冊(unregister),但是還是需要給他一個變數來儲存(這邊就是 hUserCB)。

其中,在 callback function 的部分,NewUser()LostUser() 在被呼叫的時候,都會取得三個變數:xn::UserGenerator& generatorXnUserID uservoid* pCookie

其中第一個參數 generator 就是 user generator 本身、也就是 main() 裡面所建立出來的 mUserGenerator;而 user 則是代表目前新抓到的使用者,當場景裡有大於一個使用者的話,就是要靠這個 XnUserID 來做識別了~最後的 pCookie 就是在透過 RegisterUserCallbacks() 來註冊 callback function 時的額外參數;不過由於在這個例子裡,並沒有需要額外傳資料進入 callback function,所以他的值會是我們在設定時的 NULL

而在 callback function 本身的內容部分,LostUser() 雖然裡面只有輸出訊息,但是在目前版本的 OpenNI 是不可以省略的;而在 NewUser() 裡,則是會透過 GetPoseDetectionCap() 去取得 pose detection 這個 capability,並使用 StartPoseDetection() 這個函式、來讓他針對目前新抓到的使用者(user)開始進行姿勢偵測。

 

Skeleton Capability

接下來,則是「6. Register callback functions of skeleton capability」的部分;這一部分的程式,是針對 mUserGenerator 的 skeleton capability 來做設定。由於之後對於 skeleton capability 的操作比較頻繁,所以在這邊 Heresy 是先用一個物件 mSC,來做為 skeleton capability 的操作物件;而取得的方法則和其他 capability 類似,是使用 GetSkeletonCap() 這個函式。

而要使用 skeleton capability 前,也還要使用 SetSkeletonProfile() 這個函式,來設定所謂的「skeleton profile」,決定要使用那些關節。在 OpenNI 是定義了五種不同的 profile:

  • XN_SKEL_PROFILE_NONE:沒有任何關節。
  • XN_SKEL_PROFILE_ALL:代表所有關節,以目前的 NITE 來說就是 15 個(列表請見前一篇)。
  • XN_SKEL_PROFILE_UPPER:含軀幹(torso)的上半身、 以目前的 NITE 來說總共是九個關節。
  • XN_SKEL_PROFILE_LOWER:含軀幹(torso)的下半身、 以目前的 NITE 來說理論上要有七個關節。(注意!以程式內的註解來看,應該是要包含 torso 共七個,不過 Heresy 自己試著用 EnumerateActiveJoints() 來做查詢,卻只有列出不含 torso 的六個關節)
  • XN_SKEL_PROFILE_HEAD_HANDS:只有頭和雙手這三個關節。

在這邊,Heresy 是使用完整的「XN_SKEL_PROFILE_ALL」,來抓所有可用的關節。

接下來,就是要註冊 mSC 的 callback function 了。這邊使用的函式,是 xn::SkeletonCapabilityRegisterCalibrationCallbacks(),他和前面 xn::UserGeneratorRegisterUserCallbacks() 類似,都是有包含兩個 callback function 的四個參數:

  • CalibrationStart CalibrationStartCB:開始進行骨架校正的 callback function
  • CalibrationEnd CalibrationEndCB:骨架校正完的 callback function
  • void* pCookie:額外傳遞給 callback function 使用的資料
  • XnCallbackHandle& hCallback:管理 callback function 用的 handle 值

在這邊,所註冊的兩個 callback function,就是在自己定義的 CalibrationStart()CalibrationEnd();而和前面 user generator 時不同,這邊的 pCookie 由於有另外的需要,所以 Heresy 是把目前使用的 user generator 的位址(&mUserGenerator)當作額外的參數,傳進去給 callback function 使用。最後、第四個參數 callback handle 的部分,一樣是要用一個變數來儲存他,在這邊用的就是 hCalibCB

在這樣設定後,當 skeleton capability 開始進行骨架校正的時候,就會去呼叫 CalibrationStart() 這個函式,而當骨架校正完後,則是會再去呼叫 CalibrationEnd() 這個函式。其中,CalibrationStart() 這個函式也是只有輸出資訊而已,並沒有實際的用途(但是在目前的 OpenNI 環境不指定的話,程式的執行會有問題),所以在這邊就不多做說明了。

CalibrationEnd() 這個函式,則是有實際用處的函式,他必須要根據是否有正確地辨識出人體骨架,做出下一階段的動作;而他會接受四個參數,分別是:

  • xn::SkeletonCapability& skeleton:目前所使用的 skeleton capability,在這個例子裡就是 main() 裡面透過的 mUserGenerator.GetSkeletonCap() 取得的 mSC
  • XnUserID user:目前進行骨架校正的使用者。
  • XnBool bSuccess:人體骨架校正校正是否成功。
  • void* pCookie:額外傳遞的資料,由於在註冊 callback function 時是傳入 &mUserGenerator,所以這邊 pCookie 代表的意義,就是一個指向目前使用的 user generator、也就是 mUserGenerator 的指標。

CalibrationEnd() 實際的內容呢,就是先透過 bSuccess 來判斷是否有正確骨架,如果成功的話,就呼叫 skeleton.StartTracking( user ),來要求 skeleton capability 開始進行這個使用者的骨架追蹤;而如果失敗的話,則是要求 pose detectioncapability 重新開始偵測「Psi」校正姿勢,等到偵測到下一次的校正姿勢,再重新開始分析人體骨架。

不過由於 CalibrationEnd() 取得的只有 skeleton capability,並沒有 pose detection capability 的物件,所以要對 pose detection capability 做操作的話,就是要透過額外傳進來的 pCookie 來取得;在這邊,由於傳進來的是 mUserGenerator 的指標,但是由於型別已經被轉型為 void* 了,所以要使用的話,要再透過強制轉型來把他轉回 xn::UserGenerator* 才行。

而實際上,由於在這個範例裡,CalibrationEnd() 只有用到 skeleton capability 和 pose detection capability,而他本身就可以直接取得 skeleton capability,拿不到的只有 pose detection capability,所以其實也可以考慮不是把 user generator 傳進來,而只傳 pose detection capability 也是可以的。

附註:

  • 在官方的 StickFigure 範例裡,是將 user generator 宣告為全域變數,所以在 callback funciton 裡也可以直接存取,而不必用到 pCookie
  • xn::SkeletonCapability 除了 RegisterCalibrationCallbacks() 可以設定兩個 callback function 外,還有 RegisterToJointConfigurationChange() 可以設定另一個 callback function,不過因為這邊用不到,所以就不提了。

 

Pose Detection Capability

這部分的最後,則是「7. Register callback functions of Pose Detection capability」,也就是 pose detection capability 的設定;這邊所用來註冊 callback function的,是 xn::PoseDetectionCapability 的成員函式 RegisterToPoseCallbacks()。其實他和前面 user generator 或 skeleton capability 類似,也是可以註冊兩個 callback function、要指定四個參數:

  • PoseDetection PoseStartCB:偵測到姿勢時會被呼叫的 callback funtion
  • PoseDetection PoseEndCB:當偵測到的姿勢結束的時候會被呼叫的 callback function
  • void* pCookie:額外傳遞的資料
  • XnCallbackHandle& hCallback:管理 callback function 用的 handle 值

而和前面兩項相比,比較特別的一點是,在這個程式裡,我們只註冊了第一個 callback function,也就是 PoseStartCBPoseDetected()!對於第二個 callback function PoseEndCB,在這邊是沒有去使用,而僅只有給他一個 NULL 的。這是由於 PoseEndCB 在這個例子裡面並沒有要做任何事、也沒有任何用處,而且在目前的 OpenNI 版本,可以把它省略而不會有問題。

相較於此,如果把 skeleton capability 的 CalibrationStartCB 省略掉的話,是會導致程式執行到一半當掉的;理論上,這是不該發生問題的,所以只能希望之後 OpenNI 能修正這個問題了。

所以,在這邊執行 RegisterToPoseCallbacks() 註冊 callback function 時,傳進去的四個參數依序為:PoseDetectedNULL&mUserGeneratorhPoseCB;其中後面兩者的意義和設定 skeleton capability 時是相同的,所以在這邊就不多加介紹了。

回到 PoseDetected() 這個函式本身,他在被呼叫時,會接受到四個參數:

  • xn::PoseDetectionCapability& poseDetection:pose detection capability 本身的物件參考,在這邊是等同於 main() 裡面,透過 mUserGenerator.GetPoseDetectionCap() 取得的 pose detection capability 物件。
  • const XnChar* strPose:偵測到的姿勢,這邊是以字串的形式來告訴 callback function,這邊偵測到的是哪種姿勢(不過目前似乎也只有「Psi」一種)。
  • XnUserID user:偵測到該姿勢的使用者
  • void* pCookie:額外傳遞的資料,這邊代表的意義就是一個指向目前使用的 user generator、也就是 mUserGenerator 的指標。

而在 OpenNI 偵測到校正用的姿勢、並呼叫 PoseDetected() 後,他做了兩件事。第一件事是去呼叫 user generator 的 skeleton capability 的 RequestCalibration() 函式,要求 skeleton capability 開始針對擺出校正姿勢的使用者(user)進行人體骨架的校正;而之後,由於已經偵測到姿勢、並且進入下一個處理階段了,所以也要透過 StopPoseDetection() 這個函式,來停止對這個使用者繼續做姿勢的偵測。

 

讀取骨架資料

讀取骨架資料的部分,在程式碼裡面就是「10. get user information」開始的部分了。

首先,要讀取 user generator 的相關資料時,大多都需要使用 XnUserID 來指定是要針對哪一個使用者來做操作;而要取得使用者的資料,則是要透過 user generator 的 GetUsers() 來讀取。

而 OpenNI 對於這種未知數量的資料讀取,基本上都是採用同樣的形式,像以 GetUsers() 來說,就是要給他一個已經分配好記憶體位址的陣列、讓他寫入資料,並給他分配好的空間大小、讓他知道可以寫多少東西進去。所以 GetUsers() 的介面,就是

XnStatus xn::UserGenerator::GetUsers( XnUserID aUsers[], XnUInt16& nUsers )
 

而使用的話,大致上就是(程式碼摘錄自 NITE 範例的 StickFigure.cpp):

XnUserID Users[10];
XnUInt16 nUsers = 10;
g_UserGenerator.GetUsers( Users, nUsers );

這樣寫的話,他就會試著去由取得 g_UserGenerator 目前偵測到的前十個使用者,並將這些使用者的資料,儲存在 Users 這個 XnUserID 陣列裡;而在執行之後,nUsers 的值也會跟著被修改成取得的使用者資料數量。

比如說 g_UserGenerator 有五個使用者的話,在執行過上面的程式後,Users 的前五項就會被寫入資料、而 nUsers 的值也會從 10 變成 5;如果 g_UserGenerator 有十五個使用者的話,在執行過後,Users 的十項都會被寫入資料、而 nUsers 的值則會維持是 10,後面五個使用者的資料就沒讀出來了。

而實際上比較好的方法,應該是先去取得使用者的數量,再來讀取相關的資料;所以 Heresy 在這邊的做法,就是先透過 user generator 的 GetNumberOfUsers() 函式,來取得目前的使用者數量(「10. get user information」的部分),之後再根據這個已知的數量,去取得使用者的資料(「11. get users」的部分)。

接下來,Heresy 則是用迴圈的方式,去針對每一個偵測到的使用者都做處理。在迴圈內的第一步,就是透過 skeleton capability 的 IsTracking() 函式,來判斷使否正在追蹤該使用者的骨架資料(「13. if is tracking skeleton」的部分);如果有的話,他的骨架資料才是有意義、有需要去讀取的。

如同前一篇文章所提過的,OpenNI 的關節資料標括了位置(position)和方向(orientation),而在讀取骨架資料的時候,也可以透過使用不同的函式,來讀取不同的資料;OpenNI 的 xn::SkeletonCapability 有提供三個函式,分別是:

  • GetSkeletonJointPosition():讀到的資料是只有關節位置資訊的 XnSkeletonJointPosition
  • GetSkeletonJointOrientation():讀到的資料是只有關節方向資訊的 XnSkeletonJointOrientation
  • GetSkeletonJoint():讀到的資料是包括關節位置和方向的 XnSkeletonJointTransformation

至於到底要用哪種讀取方法,就是看個人需求了。Heresy 在這邊的程式裡,是使用 GetSkeletonJoint() 來讀取關節的資料(「14. get skeleton joint data」的部分),他的方法基本上就是要指定要讀取「哪個使用者」的「哪個關節」,也就是必須要一個關節、一個關節來做讀取;而 Heresy 為了簡化程式,這邊就只有讀取頭部、也就是 XN_SKEL_HEAD 的資料了。

最後,由於 Heresy 沒有寫圖形顯示的部分,所以在把關節資料讀取出來後,也就只有很簡單地用文字的形式,把讀到的資料做輸出了(「15. output information」的部分)~

 

小結

基本上,這個程式還是只有純文字的 console 介面。在執行起來後,他會在有事件發生的時候,輸出一些對應事件的訊息,像是「New user identified」、「User xxx lost」之類的。而當使用者擺出校正姿勢、並且校正成功、成功地抓到骨架後,程式就會不斷地輸出目前抓到的骨架的頭部位置,直到使用者離開為止。

雖然這樣的程式基本上沒什麼意義,但是要拿來做基本的 OpenNI 人體骨架的範例、測試,應該也算是夠了~如果要延伸的話,就是在去根據讀取到的骨架資料,看看是要拿來幹嘛了~


OpenNI / Kinect 相關文章目錄

廣告

關於 Heresy
https://kheresy.wordpress.com

59 Responses to 透過 OpenNI / NITE 分析人體骨架(下)

  1. Jeff says:

    您好
    請問如何將骨架關節點的三度空間資料轉換並標示至影像上?

    喜歡

  2. Chien-Lung Su says:

    Heresy 您好!

    想請教您一些有關 OpenNI user (skeleton) tracking 的問題,希望您能不吝指教,謝謝!

    目前我使用 OpenNI 進行人體骨架追蹤應用,發現使用 SetSkeletonProfile 設定為其他「非」
    XN_SKEL_PROFILE_ALL 這個 profile 進行追蹤時,例如使用 XN_SKEL_PROFILE_UPPER
    這個 profile 進行人體上半身骨架追蹤,但『下半身有遮蔽物遮住人體』,此時追蹤結果很差,
    常無法找到人體,或者在 New User 事件後,該 User 就在校正階段不斷重複,無法成功被校正
    並進入追蹤狀態。(但下半身肢體沒有遮蔽物就很快被識別出來並追蹤了!)

    我的問題是:
    『OpenNI user tracking 中的 Skeleton Profile 是不是只是設定它要輸出的人體骨架關節點,
    而與它內部取得骨架資料演算方式無關呢?所以即便使用了如 XN_SKEL_PROFILE_UPPER、
    XN_SKEL_PROFILE_HEAD_HANDS 的 profiles,也得要求偵測範圍內的待辨識追蹤人體要露
    出全身肢體呢?』

    會對您有此一問,實在是先前我有使用過 Microsoft Kinect SDK 的「seated mode」來追蹤
    人體上半身肢體,在那種模式下,下半身被遮蔽,仍然可以很成功地辨識追蹤上半身骨架,
    然而相較之下,使用 OpenNI 的 「XN_SKEL_PROFILE_UPPER」 profile 的骨架追蹤結果
    就完全無法讓人接受。

    希望您能解答!謝謝!

    喜歡

    • Heresy says:

      你的問題基本上是 PrimeSense NITE 演算法實作的問題了。
      喊遺憾的是,PrimeSense 並沒有詳細說明他所採用的演算法的細節,所以 Heresy 也不能確定確實的狀況。

      而相較於 NITE,微軟的 Kinect for Windows SDK,在許多方面,的確次能提供更好的效果。

      喜歡

  3. 引用通告: Kinect+OpenNI学习笔记之6(获取人体骨架并在Qt中显示) « 成人免费资源三级分享网站

  4. 引用通告: Kinect+OpenNI学习笔记之6(获取人体骨架并在Qt中显示) | Kinect体感开发

  5. HsinJung says:

    您好
    您的程式我放到vs2010上執行了
    可以追蹤到使用者
    使用者離開也會顯示出來
    但是完全沒有顯示關節的資訊耶
    請問應該是偵測到使用者後就會一直跑出關節的資訊嗎?

    喜歡

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: