承接著上一篇的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的狀態了,因此就予以排除。
>
================================================在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吧!
您好請問一下這個有完整的程式碼可以參考嗎?
回覆刪除能否提供給我作為學習用途 我的信箱是b03502121@ntu.edu.tw