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 的話,則是會讓關節完全不動。至於要用多大的值?這點就要看個人的應用來決定了。

19 thoughts on “NiTE2 的人體骨架追蹤”

  1. 你好,我想问一下convertJointCoordinatesToDepth()这个函数转换成深度坐标以后得到的结果是二维的,有没有函数可以将骨骼的点转化为三维坐标的函数?

  2. to db

    不了解你的問題?不要轉換就是三維的啊?
    如果你是要深度座標軸加上深度的話,基本上轉換前後的 Z 是不變的,直接用就可以。

  3. 你好~楼主~~请问一下能不能跟我们讲讲关节的方向在nite2里面是怎么一个概念哈~~

  4. @小小小西瓜

    抱歉,因為 Quaternion 的概念解釋起來有點複雜,而且算是比較偏向數學的方面,Heresy 這邊短時間應該不會特別去解釋他…

    不過因為這東西也算是一個滿普遍的東西,如果需要,網路上也可以找到不少相關資訊。

  5. 假如说我要做个动作识别,那我不仅除了要考虑关节点的位置,是不是还要考虑关节的方向。。。我觉得理解起来有点头疼….

  6. @小小小西瓜

    動作識別的概念也有不同的方法,不見得要用關節的方向性。
    如果真的不習慣用 Quaternion,最簡單的方法就是直接套公式,把他轉換成旋轉矩陣來用。轉換公式是可以在網路上找到的。

  7. heresy大大
    我想請問 經由上面得範例我學到頭的座標
    但如果要一次叫出15個骨架點的座標
    可以請問要添加哪嗎?

  8. to 學習者
    NiTE 並沒有一次讀取所有關節點的功能,你必須一個關節、一個關節去讀取。

  9. 感謝
    heresy大大 了解
    但我在再骨架程式修改
    可以一個一個慢慢讀到15點
    但更新速度太快
    請問可以在for迴圈
    把windos 視窗 更新速度變慢?

  10. to 學習者

    抱歉,不太懂你的問題。
    你如果是覺得 NiTE 提供的骨架資料頻率過高、或是顯示畫面更新過快的話,基本上是要自己去控制更新頻率的。

  11. heresy..oepnni2 nite2 是不是一定要用startPoseDetection()且detect成功才可以开始读取这个user的skeleton数据(userData.getSkeleton().getJoint)??

    我试了一下直接不用poseDetection这样拿到的骨骼数据都为零..

  12. to gangan1121

    應該不用才對。
    NiTE 2 應該是不需要強制性的姿勢偵測,而可以直接追蹤骨架;上面的範例基本上就是一個可以追蹤骨架的程式了。

  13. 我想问一下如何去使用Quaternion来驱动模型,就像以前openni1.X的时候,是可以官方用dome驱动模型,直接控制模型,人怎样动模型怎样动,我想知道的是有没有这方面的资料或dome?

  14. 你好,我现在在用NITE同时检测两个录像文件里的骨骼数据,将两个UserTracker分别create两个openni的device,但似乎两个UserTracker取得到的骨骼数据是来自同一个录像文件的。请问这种情况有哪些可能的原因呢?

  15. to db

    這是 NiTE 的問題。
    印象中 PrimeSense 曾說過會解決這個問題,不過現在應該是沒希望了。

  16. 就是说没有任何办法能解决这个问题了是么?如果想要检测两个录像文件里的骨骼数据的话只能分开成两个程序执行的是么?

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *