雖然最近一直在寫 Kinect 相關的程式了,不過已經很久沒寫這系列的文章了…而這一篇呢,基本上算是延續上一篇、《Part 7:偵測、追蹤人體骨架》,來簡單用範例來說明,怎麼把整個人的骨架畫出來。
這邊提供兩個版本,一個是用 OpenCV、以 2D 的形式來把骨架畫出來,程式在這裡;另一個則是使用 OpenGL,把骨架用 3D 的形式來繪製,程式在這邊。
在開始之前,先回過頭看一下之前在《Part 7:偵測、追蹤人體骨架》裡提到的內容。
K4W SDK v2 的人體架分析的介面,主要是 IBodyFrameSource、IBodyFrameReader、IBodyFrame、IBody 這系列以「Body」為名的類別,在使用上和 K4W SDK v2 其他的介面類似,要先透過 IKinectSensor 的物件來取得 IBodyFrameSource、然後開啟 IBodyFrameReader,之後在主迴圈裡面去取得封包成 IBodyFrame 這個介面的資料。
在 IBodyFrame 的物件裡面,則是包含了當下所有有在追蹤的使用者資訊,如果要取得的話,則是要透過 GetAndRefreshBodyData() 這個韓式,把資料寫入 IBody 的物件陣列中,之後則就可以個別讀取 IBody 內的資料了。這邊的程式可以寫成下面的形式:
if (pBodyFrameReader->AcquireLatestFrame(&pFrame) == S_OK)
{
// update body data
pFrame->GetAndRefreshBodyData(iBodyCount, aBody);
pFrame->Release();
pFrame = nullptr;
for (int i = 0; i < iBodyCount; ++i)
{
IBody* pBody = aBody[i];
// Do things
}
}
其中,iBodyCount 是可以追蹤的最大人數,在目前來說應該會是 6 人,可以透過 IBodyFrameSource 的 get_BodyCount() 這個函式來取得;aBody 的型別則是 IBody**,代表一個 IBody 指標的陣列,在使用前必須先初始化好。這部分的程式可以寫成:
pFrameSource->get_BodyCount(&iBodyCount);
IBody** aBody = new IBody*[iBodyCount];
for (int i = 0; i < iBodyCount; ++i)
aBody[i] = nullptr;
在把新的資料寫入 aBody 後,接下來就可以依序去讀取每個人的骨架資料了~不過這邊也要記得要先用 IBody 的get_IsTracked() 這個函式,來確認這個 IBody 是否正被追蹤。
之後呢,則就可以透過 GetJoints() 這個函式,來把所有的關節點的位置資訊都讀取出來了。而現行版本的 K4W SDK v2 提供的關節數量共有 25 個,算是相當多的了~
這些關節點被定義在 JointType 這個列舉型別,完整的列表可以參考前一篇文章、或是 MSDN。
個別關節點的型別是「Joint」,裡面記錄了關節點的類型(JointType)、位置(Position)以及狀態(TrackingState)。
其中,位置的型別是 CameraSpacePoint、是以「攝影機空間座標系統」裡的座標系統、來記錄關節點在三度空間中的位置;所以如果是要把相關的資料用在 2D 影像上的話,是需要做座標系統的轉換的。(請參考《使用 OpenGL 繪製場景》)
如果要直接透過 OpenGL、來把關節點連線畫出來的話,程式基本上可以寫成下面的形式:
if (pBody->GetJoints(JointType::JointType_Count, aJoints) == S_OK)
{
glLineWidth(1.0f);
glBegin(GL_LINES);
glColor3f(1.0f, 1.0f, 1.0f);
DrawLine(aJoints[JointType_SpineBase], aJoints[JointType_SpineMid]);
DrawLine(aJoints[JointType_SpineMid], aJoints[JointType_SpineShoulder]);
DrawLine(aJoints[JointType_SpineShoulder], aJoints[JointType_Neck]);
DrawLine(aJoints[JointType_Neck], aJoints[JointType_Head]);
DrawLine(aJoints[JointType_SpineShoulder], aJoints[JointType_ShoulderLeft]);
DrawLine(aJoints[JointType_ShoulderLeft], aJoints[JointType_ElbowLeft]);
DrawLine(aJoints[JointType_ElbowLeft], aJoints[JointType_WristLeft]);
DrawLine(aJoints[JointType_WristLeft], aJoints[JointType_HandLeft]);
DrawLine(aJoints[JointType_HandLeft], aJoints[JointType_HandTipLeft]);
DrawLine(aJoints[JointType_HandLeft], aJoints[JointType_ThumbLeft]);
DrawLine(aJoints[JointType_SpineShoulder], aJoints[JointType_ShoulderRight]);
DrawLine(aJoints[JointType_ShoulderRight], aJoints[JointType_ElbowRight]);
DrawLine(aJoints[JointType_ElbowRight], aJoints[JointType_WristRight]);
DrawLine(aJoints[JointType_WristRight], aJoints[JointType_HandRight]);
DrawLine(aJoints[JointType_HandRight], aJoints[JointType_HandTipRight]);
DrawLine(aJoints[JointType_HandRight], aJoints[JointType_ThumbRight]);
DrawLine(aJoints[JointType_SpineBase], aJoints[JointType_HipLeft]);
DrawLine(aJoints[JointType_HipLeft], aJoints[JointType_KneeLeft]);
DrawLine(aJoints[JointType_KneeLeft], aJoints[JointType_AnkleLeft]);
DrawLine(aJoints[JointType_AnkleLeft], aJoints[JointType_FootLeft]);
DrawLine(aJoints[JointType_SpineBase], aJoints[JointType_HipRight]);
DrawLine(aJoints[JointType_HipRight], aJoints[JointType_KneeRight]);
DrawLine(aJoints[JointType_KneeRight], aJoints[JointType_AnkleRight]);
DrawLine(aJoints[JointType_AnkleRight], aJoints[JointType_FootRight]);
glEnd();
}
這邊的作法,基本上就是把 25 個關節點、根據對應的關係來連線、並把線畫出來。可以看到在 glBegin() 和 glEnd() 之間的 24 個 DrawLine() 被分成五個區段,他們分別代表軀幹、左手、右手、左腳、右腳。
而 DrawLine() 這個函式的內容則如下:
{
if (rJ1.TrackingState == TrackingState_NotTracked || rJ2.TrackingState == TrackingState_NotTracked)
return;
glVertex3f(rJ1.Position.X, rJ1.Position.Y, rJ1.Position.Z);
glVertex3f(rJ2.Position.X, rJ2.Position.Y, rJ2.Position.Z);
}
在這裡面,會試著去檢察關節點的狀態,如果其中一個關節典謨有被追蹤到,就不會把線畫出來。
如果想要強調關節點的話,也可以以點的形式,把 25 個關節點都畫出來,程式可以寫成:
glBegin(GL_POINTS);
for (int i = 0; i < JointType_Count; ++ i )
{
const Joint& rJoint = aJoints[i];
if (rJoint.TrackingState == TrackingState_NotTracked)
continue;
else if(rJoint.TrackingState == TrackingState_Tracked)
glColor3f(1.0f, 0.0f, 0.0f);
else
glColor3f(0.0f, 1.0f, 0.0f);
glVertex3f(rJoint.Position.X, rJoint.Position.Y, rJoint.Position.Z);
}
glEnd();
這邊也會去檢查各關節點的狀態,並以不同的顏色來做繪製。
這樣繪製出來的話,就會像下圖的樣子(左邊大大的三原色線條是另外畫的座標軸):
完整的範例程式,請參考這邊。
而如果想要用 OpenCV 來畫的話,則需要先把每個關節點的位置、由「攝影機空間座標系統」轉換到「深度空間座標系統」或「彩色空間座標系統」才行;而至於要轉換到哪一個,就是看要和哪種影像作套疊了。
在這邊的例子,Heresy 是把本來的彩色影像先畫出來(請參考之前的《Part 3:讀取彩色影像與紅外線影像》),然後在把骨架畫上去。
這邊的程式 Heresy 是寫成:
if (pBody->GetJoints(JointType::JointType_Count, aJoints) == S_OK)
{
DrawLine(mImg, aJoints[JointType_SpineBase], aJoints[JointType_SpineMid], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_SpineMid], aJoints[JointType_SpineShoulder], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_SpineShoulder], aJoints[JointType_Neck], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_Neck], aJoints[JointType_Head], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_SpineShoulder], aJoints[JointType_ShoulderLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_ShoulderLeft], aJoints[JointType_ElbowLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_ElbowLeft], aJoints[JointType_WristLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_WristLeft], aJoints[JointType_HandLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_HandLeft], aJoints[JointType_HandTipLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_HandLeft], aJoints[JointType_ThumbLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_SpineShoulder], aJoints[JointType_ShoulderRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_ShoulderRight], aJoints[JointType_ElbowRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_ElbowRight], aJoints[JointType_WristRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_WristRight], aJoints[JointType_HandRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_HandRight], aJoints[JointType_HandTipRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_HandRight], aJoints[JointType_ThumbRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_SpineBase], aJoints[JointType_HipLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_HipLeft], aJoints[JointType_KneeLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_KneeLeft], aJoints[JointType_AnkleLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_AnkleLeft], aJoints[JointType_FootLeft], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_SpineBase], aJoints[JointType_HipRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_HipRight], aJoints[JointType_KneeRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_KneeRight], aJoints[JointType_AnkleRight], pCoordinateMapper);
DrawLine(mImg, aJoints[JointType_AnkleRight], aJoints[JointType_FootRight], pCoordinateMapper);
}
可以看到,程式的部分大致上就是把 OpenGL 範例裡的 OpenGL 程式拿掉、然後一樣去呼叫 DrawLine() 這個函式。不過由於額外的需求,所以 DrawLine() 的參數有些變化,他的程式是:
{
if (rJ1.TrackingState == TrackingState_NotTracked || rJ2.TrackingState == TrackingState_NotTracked)
return;
ColorSpacePoint ptJ1, ptJ2;
pCMapper->MapCameraPointToColorSpace(rJ1.Position, &ptJ1);
pCMapper->MapCameraPointToColorSpace(rJ2.Position, &ptJ2);
cv::line(rImg, cv::Point(ptJ1.X, ptJ1.Y), cv::Point(ptJ2.X, ptJ2.Y), cv::Vec3b(0, 0, 255), 5);
}
這邊一樣會先針對關節的狀態做檢查,如果沒有被追蹤到的話,就可以不用畫了。
而之後呢,則是需要先透過 pCMapper 這個 ICoordinateMapper 的物件,來將關節點的位置由「攝影機空間座標系統」轉換到「彩色空間座標系統」;這邊使用的函式是 MapCameraPointToColorSpace()、一次轉換一個點。
轉換好了之後,接下來就是使用 OpenCV 的 cv::line() 這個函式,在指定的影像上畫線了~這邊的 rImg 就是代表之前的彩色影像。
在所有的線條都畫好之後,接下來再用把整張畫好的彩色影像顯示出來就可以了~完整的範例程式可以參考這裡。下面就是程式執行的截圖:
這邊比較有趣的,應該是 K4W SDK 把椅子也當成一個人了! XD
所以可以看到,除了左邊的人有追蹤到骨架之外,椅子上也有一個骨架。 ^^"
其實這邊的東西在前一篇都已經寫了,這邊主要是簡單解釋一下範例啦~
而實際上,Heresy 在使用 Kinect for Windows SDK v2 的人體骨架追蹤的時候,是覺得在這部分微軟真的進步很多!他的人體骨架的追蹤速度變得很快,常常只要進到攝影機視角內、稍微動一下就馬上能被追蹤到了~甚至可能只要坐在椅子上舉個手,都可以被追蹤到!而且,當有被追蹤到、且關節點都在視野內的時候,追蹤的品質大多不錯~
不過相對的,他還是有一定程度的誤判的可能性。像上面的圖片就是一個很好的例子。這部分,目前還沒研究是否有辦法透過參數的調整來避免,不過個人覺得可以的機會應該不大就是了…
您好,我看了GitHub上的代码后发现iBodyCount是在循环外确定的。这样如果场景中有人数的变动,iBodyCount是不是会保持不变。如果一开始场景中只有2个人,那iBodyCount=2。如果接着有人进入场景,新进入的人的骨骼信息是不是就不能被画出来了啊。
讚讚
這篇文章有說明 iBodyCount 的意義,建議請先詳細閱讀。
讚讚
老師您好!在您的例子中,使用opencv繪製骨骼,如果想在骨骼點處都加上圓點,應該怎麼做呢?
讚讚
老師我懂了,使用cv::circle()函數就行。不得不說老師寫代碼的風格真是專業,整齊明了,讓我們這些初學者能很快舉一反三,非常受益!
讚讚
你好,
這邊主要是針對 Kinect for Windows SDK 做說明,針對 OpenCV 的部分建議請自行參考官方教學。
讚讚
大神您好
有一個還滿棘手的問題,想請教你一下。
就目前我看大神發布有關KinectV2的相關範例,例如BodyIndex或人體骨架等
我想請問一下,如果今天把輸入端,從Camera換成是video or 連續depth image
這樣還有辦法使用Kinect提供的這些function 或是有其他替代的function 可以使用?
讚讚
不行。微軟沒有開放使用非 Kinect 的感應器。
讚讚
您好 如果我想用kinectV2的手部节点来做类似1-2-3-4-5-0这种数字识别,三个节点好像不够,有什么好的建议吗。
讚讚
這個是看你到底想怎麼做,能用的資料就這些,看要怎麼組合了。
讚讚
我运行了您的程序 显示cv vec3b有错误。我改成scalar也不对 求解! 谢谢大神—
Error 2 error C2664: ‘void cv::line(cv::Mat &,cv::Point,cv::Point,const cv::Scalar &,int,int,int)’ : cannot convert argument 4 from ‘cv::Vec’ to ‘const cv::Scalar &’
3 IntelliSense: no suitable user-defined conversion from “cv::Vec3b" to “const cv::Scalar" exists
讚讚
請問你是在什麼環境編譯的?
編譯的是哪個程式?如果是在 VisualStudio 下,cvBody.cpp 搭配 OpenCV 3.0 應該是要可以編譯。
讚讚
大神您好
請教一下,如果要顯示BodyFrame FPS 有相關範例可以參考嗎?
謝謝
讚讚
FPS 基本上就是計算兩次更新之間的時間差而已啊…
然後時間差的倒數,就是 FPS 了。
至於你要顯示的話,以 OpenCV 來說,最簡單就是直接用 puText() 來顯示文字吧
http://docs.opencv.org/2.4/modules/core/doc/drawing_functions.html#puttext
讚讚
Heresy 你好:
謝謝你的教學~
想要請問 以這個程式的基礎下做Button
停留五秒後進入下個畫面(播放影片)
這個部分有參考的網頁或書嗎?
我找不到kinectv2 的想關資料。
謝謝!
讚讚
概念上可以參考:
https://kheresy.wordpress.com/2013/03/25/natural-interaction-buttons-with-opencv/
當時是用 NiTE 當手的位置來源,只要改掉這部分就可以了
讚讚
謝謝Heresy的回覆,
想請問所以我只要改掉NiTE所定義的click 和 wave兩個手勢嗎?
改成VGB所定義的嗎?
不好意思,因為新接觸到v2 發現許多程式都需要新學習,
謝謝你~
讚讚
如果你是要用停留時間當作按鈕觸發條件的話,手勢是完全不需要的。
重點是把手部位置的取得,從 NiTE 換成 K4W SDK 而已。
讚讚
謝謝Heresy~上面問題解決了!!
超級感謝~你的教學~~
但很不好意思我又遇到了問題…
我想要按下按鈕後,在螢幕中顯示圖片(同個視窗內)
並同時 持續繪製人體骨架?
找了一些資料後,
我僅能按下按鈕後,跑出新視窗顯示圖片,
但主程式的kinect v2的人體追蹤就停止了(畫面暫停)
我在Button按下的程式碼中加入了
IplImage*image = cvLoadImage(“SCORE.jpg",0);
namedWindow(“Score",CV_WINDOW_AUTOSIZE);
cvShowImage(“Score",image);
cvReleaseImage(&image);
想請問我應該如何進行下一步?
再次感謝您~~
讚讚
這取決你整個程式怎麼寫的,光是這樣描述是無法知道問題所在的。
理論上只要你的主迴圈還在執行,就會繼續執行。
讚讚
终于全部学完啦,在网上实在很难找到写得如此清晰明了的教程,写得十分优秀,非常感谢!
讚讚
也請多多幫忙推廣囉~ :)
讚讚
如果是貓或狗直接走進偵測範圍
以樣也會被偵測到的話
想請教大大不知道有沒有可以防偵測動物的程式
沒有的話能否給個方向
讚讚
個人沒碰過這樣的狀況,不過會建議試試看能不能從骨架的大小、方向等資訊下去,看看有沒有可能過濾掉…
讚讚
想請問一下
#include “../common/OpenGLCamera.h"
這個.h檔案 是在哪邊可以取得呢?
覺得你文章對我很有幫助
希望接下來有 HD face的教學
讚讚
Heresy 都有放在 GitHub 上,建議可以依據對應的路徑去找
https://github.com/KHeresy/KinectForWindows2Sample/tree/v2-naming/common
HD Face…有玩過,但是說實話個人覺得要完成初始化還滿麻煩的…
等 Fusion 寫到一個段落應該會整理一下吧。
讚讚
这个系列写的真好!受教了。
讚讚
感謝支持本站
讚讚