在前一篇《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 的時候,簡單了許多。
首先,在主迴圈內,每次透過 UserTracker 的 readFrame() 來取得新的資料的時候,在讀取出來的 mUserFrame 內,都可以透過 getUsers() 這個函式,來取得當下的使用者列表;而每一個使用者的資料,都是一個 UserData 的物件,裡面儲存著使用者的資料、以及狀態。由於使用者的偵測,是 UserTracker 自己會進行的,所以這邊不需要其他的步驟,只要把使用者列表讀出來就可以了。
而在骨架追蹤的部分,由於 NiTE 基本上是讓程式開發者自行決定要針對那些使用者進行骨架的追蹤,所以並不會在找到使用者的時候,就自行開始追蹤骨架;因此,如果要針對使用者進行骨架追蹤的話,就需要呼叫 UserTracker 的 startSkeletonTracking() 這個函式,指定要針對哪一個使用者,進行骨架的追蹤。
在最簡單的狀況下,就是在每一次更新的時候,針對每一個使用者,都透過 isNew() 這個函式來判斷是否為新的使用者,如果是新的使用者的話,就開始追蹤這個使用者的人體骨架;這部分,就是上面範例程式裡面,「5a」的部分了。如果有需要停止使用者的骨架追蹤的話,也可以使用 stopSkeletonTracking() 來針對個別的使用者,停止骨架的追蹤。
另外,NiTE 2 也捨棄了 OpenNI 1.x 可以選擇要追蹤那些關節的功能,現在都是固定去追蹤全身的骨架,不能像以前一樣,可以只追蹤上半身或下半身了。
讀取骨架、關節資料
針對每一個有被追蹤的使用者,則可以透過 UserData 的 getSkeleton() 這個函式,來取得該使用者的骨架資料;getSkeleton() 回傳的資料會是 nite::Skeleton 這個型別的資料,他基本上只有兩個函式可以用,一個是 getState(),是用來取得目前的骨架資料的狀態的,另一個則是 getJoint(),是用來取得特定關節點的資訊的。
基本上,在使用讀取骨架的資料之前,最好先對骨架資料的狀態,做一個簡單的確認;如果是有被正確追蹤的話,得到的狀態應該會是 nite::SKELETON_TRACKED,這樣才有繼續使用的意義。
當確定這筆骨架資料是有用的之後,接下來就可以透過 getJoint() 這個函式,來取得各個關節點的資料了~在 NiTE 2 裡可以使用的關節點,是定義成 nite::JointType 這個列舉型別,它包含了下列十五個關節:

- JOINT_HEAD
- JOINT_NECK
- JOINT_LEFT_SHOULDER
- JOINT_RIGHT_SHOULDER
- JOINT_LEFT_ELBOW
- JOINT_RIGHT_ELBOW
- JOINT_LEFT_HAND
- JOINT_RIGHT_HAND
- JOINT_TORSO
- JOINT_LEFT_HIP
- JOINT_RIGHT_HIP
- JOINT_LEFT_KNEE
- JOINT_RIGHT_KNEE
- JOINT_LEFT_FOOT
- JOINT_RIGHT_FOOT
這十五個關節點,基本上和 OpenNI 1.x 所支援的是相同的,很遺憾,還是不支援手腕和腳踝。
而各關節點透過 getJoint() 這個函式所取得出來的的資料,型別則是 nite::SkeletonJoint,裡面記錄了他是哪一個關節(JointType),以及這個關節目前的位置(position)和方向(orientation);而和 OpenNI 1.x 時相同,他也同時有紀錄位置和方向的可靠度(confidence)。
而如果是要取得關節點的位置的話,就是使用 SkeletonJoint 所提供的 getPosition() 這個函式, 來取得該關節點的位置;而得到的資料的型別會是 nite::Point3f,裡面包含了 x、y、z 三軸的值,代表他在空間中的位置。他的座標系統基本上就是之前介紹過的、在三度空間內所使用的「世界座標系統」(參考《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 的話,則是會讓關節完全不動。至於要用多大的值?這點就要看個人的應用來決定了。
比較完整,有圖形介面的範例,可以參考:
UserTracker 初始化始终为空 是怎么回事
讚讚
建議請先參考上一篇,確定是否有正確讀到必要的檔案。
讚讚
你好,我想问你一下,你文章最后说到了setSkeletonSmoothingFactor()这个平滑方法。我想知道NITE2中具体使用的是什么滤波方法,比如是中值滤波、扩展卡尔曼滤波这些吗?
讚讚
這部分官方沒有說明。
讚讚
我也遇到同樣的問題,我只想偵測一個受測者A的骨架
但是受測者A有可能會離開攝影機外,等到受測者A又進到攝影機的視角內時
便回復受測者A的骨架偵測。在沒有偵測到受測者A的期間內不做任何的骨架追蹤。
想請教Heresy老師,能給我一些建議嗎,感謝
讚讚
這部分基本上應該還是得要自己處理的。
理論上,如果你的場景裡只會有一個人的話,應該不會太麻煩;但是如果感應器可以會拍到受測者以外的人的話,在處理上可能就會相當麻煩了。
因為如果在受測者離開後,有其他人又進來,單靠 NiTE 是無法辨別新進來的人是否是受測者的。
讚讚
如果場景內會有多人進出的話,我可以在受測者A的身上配戴類似什麼樣的東西,單用深度影像區別出是受測者,還是另外要用到RGB的資訊才比較好分辨呢?
讚讚
這取決你要怎麼去識別受測者。
如果能以衣服或帽子之類物品的色彩來做區隔的確是一個方法,但是前提就是顏色不會和路人的相同。
讚讚
[…] 之前已經在《NiTE2 的人體骨架追蹤》,提過怎麼在 OpenNI 2 的環境下,使用 NiTE2 這個函式庫,來做人體骨架的追蹤了。當時 Heresy 只有提到怎麼去處理骨架的關節點位置資料,而跳過了他的方向性資訊;而 NiTE 2 採用的 Quaternion 表示法似乎對不少人造成問題,所以雖然 NTE 已經不會再維護、發布了,但是這邊還是稍微解釋一下吧… […]
讚讚
[…] PrimeSense NiTE 2 的 UserTacker 和 HandTracker,也都有提供 NewFrameListener 可以使用,所以有需要的話,NiTE […]
讚讚
請問可以讓他只track一個人就好嗎??
我有試過用userID分別
可是有另外一個人在旁邊時,
可能會抓到另外一個人,原本的那一個就沒有被track到了@@"
讚讚
你如果是指骨架追蹤的話,要讓 NiTE 追蹤那些使用者的骨架,是你可以自己決定的,而區隔的方法,就是靠 UseID。
至於鄰近的使用者(甚至可能有交錯、重疊)可能會被誤判這件事,基本上是 Compter Vision 上的一個滿普遍的問題,基本上要完全克服也是有難度的。
個人是建議想辦法在操作的時候避免這樣的問題,會比較單純。
讚讚
謝謝! 如果人消失在攝影機前,怎麼reset他知前抓到的User呢@@??
讚讚
不了解你的問題?
如果是使用者消失的話,最後會有一個畫面會是 isLost() 為真的狀態。
在這個時候可以知道是哪個使用者消失。
或者另一個方法,就是自己去紀錄有哪些使用者。
讚讚
[…] NiTE2 的人體骨架追蹤/使用 OpenCV 畫出 NiTE2 的人體骨架 […]
讚讚
[…] NiTE 2 的人體骨架追蹤,以及姿勢的偵測,以範例的方法做了說明。而接下來這篇,則是來說明 […]
讚讚
[…] 在之前的文章裡,已經介紹過 NiTE 2 在 OpenNI 2 架構下的人體骨架分析的方法。而接下來這一篇,則是來介紹一下,怎樣來使用 NiTE 2 提供的姿勢偵測功能。 […]
讚讚
[…] 這篇基本上是《NiTE2 的人體骨架追蹤》的延伸,算是提供一個以 OpenCV 來做顯示的完整地 NiTE 2 + OpenNI 2 的人體骨架追蹤範例;另外,他也算是從《用 OpenCV 畫出 OpenNI 2 的深度、彩色影像》延伸出來的範例,如果還沒看過這兩篇文章的話,建議先看一下。 […]
讚讚