2014-11-08

[OF]Kinect v2 學習筆記(三) 基礎軀體與骨架資訊

承接著上一篇的Depth Map
這一篇要來進入Kinect最實用的功能!「Skeleton Tracking(骨架追蹤)
是說在3D Model的建立上似乎也有很大的幫助,不過本系列沒打算講到這塊就是了XD
話不多說,來看Demo結果先:



耶~~長頸鹿終於出現了!!(撒花
關於環境的設定以及基本的SDK的運用概念,請參考前一篇

簡單條列一下上面的Demo到底做了什麼
  • 運用BodyIndexFrame(軀體資訊)對Depth Map做出「去背」的效果
  • 透過BodyFrame(骨架資訊)得到Joints(關節)的3D資訊
  • 將Joints的3D資訊透過CoordinateMapper(座標對應)轉換到Depth Map的2D空間中,並繪製出關節點
  • 根據關節點來套入長頸鹿的頭(這才是重點)

眼尖的你一定看出,這篇要介紹的三個新東西就是
BodyIndexFrame、BodyFrame、CoordinateMapper

以下簡單介紹一下這三個新東西

=BodyIndexFrame=

其資料是根據Depth Map計算而來,因此是相同大小的8-bit 2d unsigned char array(0~255)
Kinect V2最大辨識人數為6人,所以其數值的範圍為"0~5"
若數值為255,表示這部份是背景
非常適合用來做去背

=BodyFrame=

個人還是比較喜歡用Skeleton(骨架)這個詞
整個骨架包含了25個Joints(關節),數量上比起Kinect SDK 1.8時期的20個多了5個
我想直接用大家最常見的圖來說明就夠清楚了!
首先是Kinect V1:

再來是Kinect V2:

主要幾個差異點就是

  • 多了HAND_TIP_RIGHT、THUMB_RIGHT、HAND_TIP_LEFT、THUMB_LEFT、NECK
  • SHOULDER_CENTER→SPINE_SHOULDER,位置往下調整
  • SPINE→SPINE_MID
  • HIP_CNETER→SPINE_BASE,其中關於HIP的三個位置都更有往下調整(之前根本就是腰

關於第一點的部份,可以發現Joints主要都增加在手部
這點我猜也是他可以做手部狀態判定的主因,這個就留到之後再來說明。

每個Joints包含兩種資料結構"Joint" & "JointOrientation"
前者包含關節點在Camera Space(相機空間)中座標,以及它的Tracking State
後者包含關節點的旋轉資訊,它是以四元數的方式表示。
至於什麼是Camera Space呢?請繼續往後看
(由於這次還停留在平面空間中,因此就先不解釋旋轉的部份)

=CoordinateMapper=

相信有看第一篇筆記的會都知道
無論是Kinect V1還Kinect V2,它都有兩組以上的鏡頭(RGB、IR)
既然是兩組不一樣的鏡頭,就會有兩組不同的2D空間(Depth Space、Color Space)
此外,還有一個是以Kinect為原點的3D空間,用來表現像是Joints的資訊
在官方的用法上稱為Camera Space(相機空間)

由於我們時常需要將各種不同資訊交互使用(在Depth Map上畫出Joints、對Color Image做去背...etc.)
因此Kinect SDK提供了讓大家方便轉換的機制,也就是CoordinateMapper


實際的用法我想還是在後續有用到時再跟大家說明
以目前Kinect SDK 2.0來說,各空間之間的轉換並沒有準備的非常齊全
不過針對這次的Demo來說,使用上就還算單純

===============分隔線================

基本的介紹都已經講完,該進入快樂的程式碼了
首先是標頭檔:

1:  typedef struct _stSCREEN_SKELETON  
2:  {  
3:       bool          bIsTracking;  
4:       ofVec2f       aJoints[JointType_Count];  
5:  }stSCREEN_SKELETON;  
6:  typedef map<UINT64, stSCREEN_SKELETON>          SKELETON_MAP;  
7:    
8:  class ofBodyDemo : public ofBaseApp{  
9:    
10:  ...  
11:    
12:  //Base Kinect componer  
13:  private:       
14:       IKinectSensor*              _pKinectSensor;  
15:       ICoordinateMapper*          _pCoordinateMapper;  
16:    
17:  //Depth & Body Index  
18:  public:  
19:       bool setupDepth();  
20:       bool setupBodyIndex();  
21:       void updateDepth();  
22:       void drawDepth();  
23:  private:  
24:       ofImage                     _Display;  
25:       IDepthFrameReader*          _pDepthFrameReader;  
26:       IBodyIndexFrameReader*     _pBodyIndexFrameReader;  
27:    
28:  //Body  
29:  public:  
30:       bool setupBody();  
31:       void updateBody();  
32:       void drawBody();  
33:  private:  
34:       SKELETON_MAP               _SkeletonMgr;  
35:       IBodyFrameReader*          _pBodyFrameReader;  
36:  };  

由於我懶惰,所以還是先放在同一個Class中
下次應該就會有心把它拿出來了吧...吧?

1:6:宣告儲存骨架資訊用的struct
為了後面方便儲存資料,先用一個struct專門表示視窗上的骨架,以ofVec2f來儲存畫面上的位置。

10:OF基本的一些function
為了版面就先省略了

12:26:DepthFrame & BodyIndexFrame的部份
這邊的作法是在處理Depth的同時就參考BodyIndex的資訊來去背,因此會將兩者放在一起。

================================================

接下來是原始檔中,初始化的部份:

1:  bool ofBodyDemo::InitialKinectV2()  
2:  {  
3:       HRESULT hr_;  
4:       _bSubtrace = false;  
5:       _bDrawSkeleton = false;  
6:    
7:       hr_ = GetDefaultKinectSensor(&_pKinectSensor);  
8:       if(FAILED(hr_))  
9:       {  
10:            ofLog(OF_LOG_ERROR, "Get Kinect sensor failed!");  
11:            return false;  
12:       }  
13:         
14:       if(_pKinectSensor)  
15:       {       
16:            hr_ = _pKinectSensor->Open();  
17:    
18:            //Coordinate  
19:            if(SUCCEEDED(hr_))  
20:            {  
21:                 hr_ = _pKinectSensor->get_CoordinateMapper(&_pCoordinateMapper);  
22:            }  
23:            if(!this->setupDepth())  
24:            {  
25:                 ofLog(OF_LOG_ERROR, "Get Kinect Depth failed!");  
26:            }  
27:            if(!this->setupBodyIndex())  
28:            {  
29:                 ofLog(OF_LOG_ERROR, "Get Kinect Body Index failed!");  
30:            }  
31:            if(!this->setupBody())  
32:            {  
33:                 ofLog(OF_LOG_ERROR, "Get Kinect Body failed!");  
34:            }  
35:       }  
36:    
37:       if(!_pKinectSensor || FAILED(hr_))  
38:       {  
39:            ofLog(OF_LOG_ERROR, "Initial failed!");  
40:            return false;  
41:       }  
42:       else  
43:       {  
44:            return true;  
45:       }  
46:  }  
47:    
48:  //--------------------------------------------------------------  
49:  bool ofBodyDemo::setupDepth()  
50:  {  
51:       HRESULT hr_;  
52:       IDepthFrameSource* pDepthFrameSource_ = nullptr;  
53:    
54:       hr_ = _pKinectSensor->get_DepthFrameSource(&pDepthFrameSource_);  
55:    
56:       if(SUCCEEDED(hr_))  
57:       {  
58:            hr_ = pDepthFrameSource_->OpenReader(&_pDepthFrameReader);  
59:       }  
60:       if(pDepthFrameSource_ != nullptr)  
61:       {  
62:            pDepthFrameSource_->Release();  
63:       }  
64:    
65:       return SUCCEEDED(hr_);  
66:  }  
67:    
68:  //--------------------------------------------------------------  
69:  bool ofBodyDemo::setupBodyIndex()  
70:  {  
71:       HRESULT hr_;  
72:       IBodyIndexFrameSource* pBodyIndexFrameSource_ = nullptr;  
73:    
74:       hr_ = _pKinectSensor->get_BodyIndexFrameSource(&pBodyIndexFrameSource_);  
75:    
76:       if(SUCCEEDED(hr_))  
77:       {  
78:            hr_ = pBodyIndexFrameSource_->OpenReader(&_pBodyIndexFrameReader);  
79:       }  
80:       if(pBodyIndexFrameSource_ != nullptr)  
81:       {  
82:            pBodyIndexFrameSource_->Release();  
83:       }  
84:    
85:       return SUCCEEDED(hr_);  
86:  }  
87:    
88:  //--------------------------------------------------------------  
89:  bool ofBodyDemo::setupBody()  
90:  {  
91:       HRESULT hr_;  
92:       IBodyFrameSource* pBodyFrameSource_ = nullptr;  
93:    
94:       hr_ = _pKinectSensor->get_BodyFrameSource(&pBodyFrameSource_);  
95:       if(SUCCEEDED(hr_))  
96:       {  
97:            hr_ = pBodyFrameSource_->OpenReader(&_pBodyFrameReader);  
98:       }  
99:       if(pBodyFrameSource_ != nullptr)  
100:       {  
101:            pBodyFrameSource_->Release();  
102:       }  
103:       return SUCCEEDED(hr_);  
104:  }  

好長好噁心啊!!照理來說應該用Template重新包裝才是...

1:46:初始化的主要Function
跟前一篇有點不同的是,我將各個Reader的initial獨立成各個function,以方便閱讀。可以看到CoordinateMapper的初始化方式不同於Reader,單純一行get_CoordinateMapper()即可解決

49:104:各種初始化
詳細請參考前篇喔

寫到這邊突然覺得自己浪費好多篇幅...下次改進

================================================

接下來是Depth & BodyIndex的處理

1:  void ofBodyDemo::updateDepth()  
2:  {  
3:       if (!_pDepthFrameReader || !_pBodyIndexFrameReader)  
4:       {  
5:            return;  
6:       }  
7:    
8:       HRESULT hr_;  
9:       IDepthFrame* pDepthFrame_ = nullptr;  
10:       IBodyIndexFrame* pBodyIndexFrame_ = nullptr;  
11:    
12:       //Get depth frame  
13:       hr_ = _pDepthFrameReader->AcquireLatestFrame(&pDepthFrame_);  
14:    
15:       //Get body index frame  
16:       if(SUCCEEDED(hr_))  
17:       {  
18:            hr_ = _pBodyIndexFrameReader->AcquireLatestFrame(&pBodyIndexFrame_);  
19:       }  
20:    
21:       if (SUCCEEDED(hr_))  
22:       {  
23:            //Get the depth information  
24:            UINT iDepthBufferSize_ = 0;  
25:            UINT16 *pDepthBuffer_ = nullptr;  
26:            int iDepthWidth_ = 0;  
27:            int iDepthHeight_ = 0;  
28:            USHORT usMinDepth_ = 0;  
29:            USHORT usMaxDepth_ = USHRT_MAX;  
30:    
31:            IFrameDescription* pDepthFrameDescription_ = nullptr;  
32:            pDepthFrame_->get_FrameDescription(&pDepthFrameDescription_);  
33:            pDepthFrameDescription_->get_Width(&iDepthWidth_);  
34:            pDepthFrameDescription_->get_Height(&iDepthHeight_);  
35:            pDepthFrame_->get_DepthMinReliableDistance(&usMinDepth_);  
36:            pDepthFrame_->get_DepthMaxReliableDistance(&usMaxDepth_);  
37:            pDepthFrame_->AccessUnderlyingBuffer(&iDepthBufferSize_, &pDepthBuffer_);  
38:    
39:            //Get the body index information  
40:            UINT iBodyIndexBufferSize_ = 0;  
41:            BYTE *pBodyIndexBuffer_ = nullptr;  
42:    
43:            pBodyIndexFrame_->AccessUnderlyingBuffer(&iBodyIndexBufferSize_, &pBodyIndexBuffer_);  
44:    
45:            //Process depth infomation  
46:            ofPixels TmpDisplay_;  
47:            TmpDisplay_.allocate(iDepthWidth_, iDepthHeight_, ofImageType::OF_IMAGE_GRAYSCALE);  
48:            unsigned char * acDisplay_ = TmpDisplay_.getPixels();  
49:              
50:            for(int iBufferIndex_ = 0; iBufferIndex_ < iDepthBufferSize_; iBufferIndex_++)  
51:            {  
52:                 if(_bSubtrace)  
53:                 {  
54:                      if(pBodyIndexBuffer_[iBufferIndex_] != 255)  
55:                      {  
56:                           acDisplay_[iBufferIndex_] = 0xff * (float)pDepthBuffer_[iBufferIndex_] / usMaxDepth_;  
57:                      }  
58:                      else  
59:                      {  
60:                           acDisplay_[iBufferIndex_] = 0;  
61:                      }  
62:                 }  
63:                 else  
64:                 {  
65:                      acDisplay_[iBufferIndex_] = 0xff * (float)pDepthBuffer_[iBufferIndex_] / usMaxDepth_;  
66:                 }  
67:            }  
68:            _Display.setFromPixels(acDisplay_, iDepthWidth_, iDepthHeight_, OF_IMAGE_GRAYSCALE);  
69:    
70:            //Release frame description  
71:            if(pDepthFrameDescription_ != NULL)  
72:            {  
73:                 pDepthFrameDescription_->Release();  
74:            }  
75:       }  
76:    
77:       //Release frame  
78:       if(pDepthFrame_ != nullptr)  
79:       {  
80:            pDepthFrame_->Release();  
81:       }  
82:       if(pBodyIndexFrame_ != nullptr)  
83:       {  
84:            pBodyIndexFrame_->Release();  
85:       }  
86:  }  

21:35:取得DepthFrame的基本資料
取得DepthFrame的相關資料,包含長、寬、最近深度、最遠深度以及Depth Frame Buffer

37:43:取得BodyIndexFrame的基本資料
跟DepthFrame的運作方式一樣,由於這裡沒有去取得BodyIndexFrame的長寬資訊,所以就沒有使用IFrameDescription。建議上還是都要取出來檢查是否與DepthFrame的相等才是。

45:68:透過BodyIndex來對DepthFrame做去背,並將結果存入_Display中
這次的功能之一來了!去背。由於DepthFrame與BodyIndexFrame,理論上應該一樣大小,因此就是在這個部份補上BodyIndex的檢查,來改變要填入灰階值,還是填入0。當然,這裡也可以運用這個BodyIndex,讓不同的人填入不同顏色。

================================================

最後是Body!

1:  void ofBodyDemo::updateBody()  
2:  {  
3:       if (!_pBodyFrameReader)  
4:       {  
5:            return;  
6:       }  
7:    
8:       HRESULT hr_;  
9:       IBodyFrame* pBodyFrame_ = nullptr;  
10:    
11:       //Get body frame  
12:       hr_ = _pBodyFrameReader->AcquireLatestFrame(&pBodyFrame_);  
13:         
14:       if (SUCCEEDED(hr_))  
15:       {  
16:            //Clear all skeleton  
17:            for(auto Iter_ = _SkeletonMgr.begin();Iter_ != _SkeletonMgr.end(); ++Iter_)  
18:            {  
19:                 Iter_->second.bIsTracking = false;  
20:            }  
21:              
22:            //Get skeleton  
23:            IBody* pBodies_[BODY_COUNT] = {0};  
24:            if(SUCCEEDED( pBodyFrame_->GetAndRefreshBodyData(_countof(pBodies_), pBodies_) ) )  
25:            {  
26:                 for(int idx_ = 0; idx_ < BODY_COUNT; ++idx_)  
27:                 {  
28:                      IBody* pBody_ = pBodies_[idx_];  
29:                      BOOLEAN     bTracked_ = false;  
30:                      UINT64     uint64ID_ = 0;  
31:                      if(pBody_)  
32:                      {  
33:                           pBody_->get_IsTracked(&bTracked_);  
34:                           pBody_->get_TrackingId(&uint64ID_);  
35:    
36:                           if(bTracked_)  
37:                           {       
38:                                auto Iter_ = _SkeletonMgr.find(uint64ID_);  
39:                                if(Iter_ == _SkeletonMgr.end())  
40:                                {  
41:                                     stSCREEN_SKELETON     stNewSkeleton_;  
42:                                     _SkeletonMgr[uint64ID_] = stNewSkeleton_;  
43:                                }  
44:    
45:                                Joint     joints_[JointType_Count];  
46:                                pBody_->GetJoints(JointType_Count, joints_);  
47:                                  
48:                                for(int iJointIdx_ = 0; iJointIdx_ < JointType_Count; ++iJointIdx_)  
49:                                {  
50:                                     ofVec2f Pos_;  
51:                                     DepthSpacePoint     DepthPoint_ = {0};  
52:                                     _pCoordinateMapper->MapCameraPointToDepthSpace(joints_[iJointIdx_].Position, &DepthPoint_);  
53:                                     Pos_.x = DepthPoint_.X;  
54:                                     Pos_.y = DepthPoint_.Y;  
55:                                  
56:                                     _SkeletonMgr[uint64ID_].aJoints[iJointIdx_] = Pos_;  
57:                                }  
58:                                _SkeletonMgr[uint64ID_].bIsTracking = true;  
59:                           }  
60:                      }  
61:                 }  
62:    
63:                 //Release body data  
64:                 for(int iBody_ = 0; iBody_ < BODY_COUNT; ++iBody_)  
65:                 {  
66:                      if(pBodies_[iBody_] != nullptr)  
67:                      {  
68:                           pBodies_[iBody_]->Release();  
69:                      }  
70:                 }  
71:            }  
72:              
73:            //Remove lost skeleton  
74:            auto Iter_ = _SkeletonMgr.begin();  
75:            while(Iter_ != _SkeletonMgr.end())  
76:            {  
77:                 if(Iter_->second.bIsTracking)  
78:                 {  
79:                      ++Iter_;  
80:                 }  
81:                 else  
82:                 {  
83:                      Iter_ = _SkeletonMgr.erase(Iter_);  
84:                 }  
85:            }  
86:       }  
87:    
88:       //Release frame  
89:       if(pBodyFrame_ != nullptr)  
90:       {  
91:            pBodyFrame_->Release();  
92:       }  
93:  }  


36:60:確定此IBody處於Tracking狀態後,轉存到_SkeletonMgr中
Kinect並不會一抓到骨架就進入Tracking,會等到穩定後才會進入。每個Body都會有一個獨立的Tracking ID,因此在資料管理上,我們就用這個Tracking ID做為Key來存入map中。有了Body以後,就能取得他的Joints。就像前面所提到,Joints是屬於Camera Space的3D座標,由於我們要將他畫在Depth Map上,因此使用

_pCoordinateMapper->MapCameraPointToDepthSpace(joints_[iJointIdx_].Position, &DepthPoint_);

將其轉到Depth Space上,並存入_SkeleotnMgr中。
※特別注意,BodyIndex的值為0~5,Tracking ID為一個UINT64的超大數字,兩者不相同喔!

16:20 & 73:85:清除已失去Tracking的Skeleton機制
在16:20的部份會先將所有存入_SkekeltonMgr中的bIsTracking通通設為False,在處理完這輪的Tracking更新後,若其bIsTracking還是False,就表示此Skeleton已經不處於被Tracking的狀態了,因此就予以排除。

> ================================================

到此,就已經取得了去背後的Depth Map以及Joints的資訊了。
Draw的部份礙於篇幅就不全部寫出來了
不過長頸鹿的頭就不能放過了!畢竟這才是重點!!
這也算是怎麼取出Joints資訊的範例吧

1:  ofVec2f HeadPos_ = Iter_->second.aJoints[JointType_Head];  
2:  ofVec2f NeckPos_ = Iter_->second.aJoints[JointType_Neck];  
3:  ofVec2f VecHead_ = (HeadPos_ - NeckPos_).normalized();  
4:  float fRotate_ = VecHead_.angle(ofVec2f(0, -1));  
5:    
6:  //Draw head  
7:  int iWidth_ = _Head.getWidth()/2;  
8:  int iHeight_ = _Head.getHeight()/2;  
9:    
10:  ofPushMatrix();  
11:  {  
12:       ofTranslate(HeadPos_);  
13:       ofRotateZ(-fRotate_);  
14:    
15:       ofSetColor(255);  
16:       _Head.draw(-iWidth_, -iHeight_);  
17:  }  
18:  ofPopMatrix();  
19:    

簡單來說就是取出頭與脖子的Position並得到Vector,有了這個Vector就能計算與垂直線的角度
之後只要畫上去,就有一個會跟著自己動的長頸鹿頭嘍!
※特別注意,OF中ofVec2f與ofPoint的angle function,算出來的結果不一樣喔!

=============分隔線==============

以上就是BodyIndexFrame做去背運用,以及透過BodyFrame去取得骨架
開始有點後悔應該要將這兩個主題分開來寫,搞的整篇滿滿的都是程式碼...
下一篇的目標就小一點,就只講ColorFrame吧!

1 則留言:

  1. 您好請問一下這個有完整的程式碼可以參考嗎?
    能否提供給我作為學習用途 我的信箱是b03502121@ntu.edu.tw

    回覆刪除