NiTE2 的人體骨架追蹤


在前一篇《NiTE2 基本使用》裡,已經大致講了在 OpenNI 2 的架構下,NiTE2 的基本使用方法。不過,在那篇文章裡,主要是在講整個 NiTE 的架構和使用概念,並沒有講到細節;尤其一般人會使用 NiTE,大多都是為了去追蹤人的骨架,而這點在上一篇文章中,並沒有提到。所以這一篇,就來補充這一部分,來針對 NiTE 2 的 UserTracker 來做比較完整的說明~

在 OpenNI 1.x 的時候,OpenNI 是採取了一種比較複雜的 callback 事件,來做為人體骨架的辨識、追蹤的開發模式(參考),在使用上其實相對繁瑣;而在 OpenNI 2 + NiTE 2 的架構,要進行人體骨架的辨識、追蹤,算是相對簡單不少了~下面就是一段簡單的讀取頭部位置的範例程式:

// STL Header
#include <iostream>
  
// 1. include NiTE Header
#include <NiTE.h>
  
// using namespace
using namespace std;
  
int main( int argc, char** argv )
{
  // 2. initialize NiTE
  nite::NiTE::initialize();
  
  // 3. create user tracker
  nite::UserTracker mUserTracker;
  mUserTracker.create();
  
  nite::UserTrackerFrameRef mUserFrame;
  for( int i = 0; i < 300; ++ i )
  {
    // 4. get user frame
    mUserTracker.readFrame( &mUserFrame );
  
    // 5. get users' data
    const nite::Array<nite::UserData>& aUsers = mUserFrame.getUsers();
    for( int i = 0; i < aUsers.getSize(); ++ i )
    {
      const nite::UserData& rUser = aUsers[i];
      if( rUser.isNew() )
      {
        cout << "New User [" << rUser.getId() << "] found." << endl;
        // 5a. start tracking skeleton
        mUserTracker.startSkeletonTracking( rUser.getId() );
      }
      if( rUser.isLost() )
      {
        cout << "User [" << rUser.getId()  << "] lost." << endl;
      }
  
// 5b. get skeleton const nite::Skeleton& rSkeleton = rUser.getSkeleton(); if( rSkeleton.getState() == nite::SKELETON_TRACKED ) { // if is tracked, get joints const nite::SkeletonJoint& rHead = rSkeleton.getJoint( nite::JOINT_HEAD ); const nite::Point3f& rPos = rHead.getPosition(); cout << " > " << rPos.x << "/" << rPos.y << "/" << rPos.z; cout << " (" << rHead.getPositionConfidence() << ")" << endl; }
} } nite::NiTE::shutdown();   return 0; }

基本說明

這個範例程式基本上是從《NiTE2 基本使用》這個範例做延伸的,黃底的部分,就是增加的部分。可以看到,如果只是單純要針對使用者做骨架的追蹤,以及關節位置資料的讀取,其實程式真的相當單純,比 OpenNI 1.x 的時候,簡單了許多。

首先,在主迴圈內,每次透過 UserTrackerreadFrame() 來取得新的資料的時候,在讀取出來的 mUserFrame 內,都可以透過 getUsers() 這個函式,來取得當下的使用者列表;而每一個使用者的資料,都是一個 UserData 的物件,裡面儲存著使用者的資料、以及狀態。由於使用者的偵測,是 UserTracker 自己會進行的,所以這邊不需要其他的步驟,只要把使用者列表讀出來就可以了。

而在骨架追蹤的部分,由於 NiTE 基本上是讓程式開發者自行決定要針對那些使用者進行骨架的追蹤,所以並不會在找到使用者的時候,就自行開始追蹤骨架;因此,如果要針對使用者進行骨架追蹤的話,就需要呼叫 UserTrackerstartSkeletonTracking() 這個函式,指定要針對哪一個使用者,進行骨架的追蹤

在最簡單的狀況下,就是在每一次更新的時候,針對每一個使用者,都透過 isNew() 這個函式來判斷是否為新的使用者,如果是新的使用者的話,就開始追蹤這個使用者的人體骨架;這部分,就是上面範例程式裡面,「5a」的部分了。如果有需要停止使用者的骨架追蹤的話,也可以使用 stopSkeletonTracking() 來針對個別的使用者,停止骨架的追蹤。

另外,NiTE 2 也捨棄了 OpenNI 1.x 可以選擇要追蹤那些關節的功能,現在都是固定去追蹤全身的骨架,不能像以前一樣,可以只追蹤上半身或下半身了。


讀取骨架、關節資料

針對每一個有被追蹤的使用者,則可以透過 UserDatagetSkeleton() 這個函式,來取得該使用者的骨架資料;getSkeleton() 回傳的資料會是 nite::Skeleton 這個型別的資料,他基本上只有兩個函式可以用,一個是 getState(),是用來取得目前的骨架資料的狀態的另一個則是 getJoint(),是用來取得特定關節點的資訊的

基本上,在使用讀取骨架的資料之前,最好先對骨架資料的狀態,做一個簡單的確認;如果是有被正確追蹤的話,得到的狀態應該會是 nite::SKELETON_TRACKED,這樣才有繼續使用的意義。

當確定這筆骨架資料是有用的之後,接下來就可以透過 getJoint() 這個函式,來取得各個關節點的資料了~在 NiTE 2 裡可以使用的關節點,是定義成 nite::JointType 這個列舉型別,它包含了下列十五個關節:

  1. JOINT_HEAD
  2. JOINT_NECK
  3. JOINT_LEFT_SHOULDER
  4. JOINT_RIGHT_SHOULDER
  5. JOINT_LEFT_ELBOW
  6. JOINT_RIGHT_ELBOW
  7. JOINT_LEFT_HAND
  8. JOINT_RIGHT_HAND
  9. JOINT_TORSO
  10. JOINT_LEFT_HIP
  11. JOINT_RIGHT_HIP
  12. JOINT_LEFT_KNEE
  13. JOINT_RIGHT_KNEE
  14. JOINT_LEFT_FOOT
  15. JOINT_RIGHT_FOOT

這十五個關節點,基本上和 OpenNI 1.x 所支援的是相同的,很遺憾,還是不支援手腕和腳踝。

而各關節點透過 getJoint() 這個函式所取得出來的的資料,型別則是 nite::SkeletonJoint,裡面記錄了他是哪一個關節(JointType),以及這個關節目前的位置(position)和方向(orientation);而和 OpenNI 1.x 時相同,他也同時有紀錄位置和方向的可靠度(confidence)。

而如果是要取得關節點的位置的話,就是使用 SkeletonJoint 所提供的 getPosition() 這個函式, 來取得該關節點的位置;而得到的資料的型別會是 nite::Point3f,裡面包含了 xyz 三軸的值,代表他在空間中的位置。他的座標系統基本上就是之前介紹過的、在三度空間內所使用的「世界座標系統」(參考《OpenNI 2 的座標系統轉換》),如果需要把它轉換到深度影像上的話,可以使用 UserTracker 所提供的 convertJointCoordinatesToDepth() 這個函式來進行轉換。(如果要用 OpenNI 的 CoordinateConverter 應該也是可以的。)

不過,由於關節不見得一定準確,如果肢體根本是在攝影機的範圍之外的話,NiTE 也就只能靠猜的,來判斷位置了…而在這種狀況下,位置的準確性會相當低。所以在使用關節位置的時候,個人會建議最好也要透過 getPositionConfidence() 這個函式,來確認該關節位置的可靠度,作為後續處理的參考;他回傳的值會是一個浮點數,範圍是 0 ~ 1 之間,1 代表最可靠、而 0 則是代表純粹是用猜的。

而在上面的例子裡,就是去讀取頭部這個關節點(JOINT_HEAD)的資料,並把它的位置、可靠度都做輸出;如果要得到全身的骨架的資料的話,只要依序針對 15 個關節做讀取就可以了。

而至於關節的方向性的部分,如果需要的話,則是使用 getOrientation() 來做讀取;而讀取出來的資料,則不是像之前 OpenNI 1.x 一樣是一個陣列,而是採用「Quaternion」來代表他的方向。由於他在概念上算是比較複雜一點的東西,所以在這邊就先不提了,等之後有機會再來講吧…


關節資訊的平滑化

由於關節點的資訊在計算的時候,有可能會因為各式各樣的因素,導致有誤差的產生,進一步在人沒有動的情況下,有抖動的問題,所以這個時候,就可能會需要針對計算出來的骨架資訊,做平滑化的動作。和在 OpenNI 1.x 的時候相同,NiTE 2 一樣可以控制人體骨架追蹤的平滑化的參數。在 NiTE 2 裡,透過 UserTracker 提供的 setSkeletonSmoothingFactor(),設定一個 0 ~ 1 之間的福點數,就可以調整關節資訊的平滑化程度了~

如果給 0 的話,就是完全不進行平滑化,值愈大、平滑化的程度越高,但是如果給 1 的話,則是會讓關節完全不動。至於要用多大的值?這點就要看個人的應用來決定了。


比較完整,有圖形介面的範例,可以參考:


OpenNI / Kinect 相關文章目錄

對「NiTE2 的人體骨架追蹤」的想法

  1. 你好,我想问你一下,你文章最后说到了setSkeletonSmoothingFactor()这个平滑方法。我想知道NITE2中具体使用的是什么滤波方法,比如是中值滤波、扩展卡尔曼滤波这些吗?

  2. 我也遇到同樣的問題,我只想偵測一個受測者A的骨架
    但是受測者A有可能會離開攝影機外,等到受測者A又進到攝影機的視角內時
    便回復受測者A的骨架偵測。在沒有偵測到受測者A的期間內不做任何的骨架追蹤。
    想請教Heresy老師,能給我一些建議嗎,感謝

    • 這部分基本上應該還是得要自己處理的。
      理論上,如果你的場景裡只會有一個人的話,應該不會太麻煩;但是如果感應器可以會拍到受測者以外的人的話,在處理上可能就會相當麻煩了。
      因為如果在受測者離開後,有其他人又進來,單靠 NiTE 是無法辨別新進來的人是否是受測者的。

      • 如果場景內會有多人進出的話,我可以在受測者A的身上配戴類似什麼樣的東西,單用深度影像區別出是受測者,還是另外要用到RGB的資訊才比較好分辨呢?

        • 這取決你要怎麼去識別受測者。

          如果能以衣服或帽子之類物品的色彩來做區隔的確是一個方法,但是前提就是顏色不會和路人的相同。

  3. […] 之前已經在《NiTE2 的人體骨架追蹤》,提過怎麼在 OpenNI 2 的環境下,使用 NiTE2 這個函式庫,來做人體骨架的追蹤了。當時 Heresy 只有提到怎麼去處理骨架的關節點位置資料,而跳過了他的方向性資訊;而 NiTE 2 採用的 Quaternion 表示法似乎對不少人造成問題,所以雖然 NTE 已經不會再維護、發布了,但是這邊還是稍微解釋一下吧… […]

  4. 請問可以讓他只track一個人就好嗎??
    我有試過用userID分別
    可是有另外一個人在旁邊時,
    可能會抓到另外一個人,原本的那一個就沒有被track到了@@"

    • 你如果是指骨架追蹤的話,要讓 NiTE 追蹤那些使用者的骨架,是你可以自己決定的,而區隔的方法,就是靠 UseID。

      至於鄰近的使用者(甚至可能有交錯、重疊)可能會被誤判這件事,基本上是 Compter Vision 上的一個滿普遍的問題,基本上要完全克服也是有難度的。
      個人是建議想辦法在操作的時候避免這樣的問題,會比較單純。

        • 不了解你的問題?

          如果是使用者消失的話,最後會有一個畫面會是 isLost() 為真的狀態。
          在這個時候可以知道是哪個使用者消失。

          或者另一個方法,就是自己去紀錄有哪些使用者。

  5. […] 這篇基本上是《NiTE2 的人體骨架追蹤》的延伸,算是提供一個以 OpenCV 來做顯示的完整地 NiTE 2 + OpenNI 2 的人體骨架追蹤範例;另外,他也算是從《用 OpenCV 畫出 OpenNI 2 的深度、彩色影像》延伸出來的範例,如果還沒看過這兩篇文章的話,建議先看一下。 […]

發表留言

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