這篇基本上是《NiTE2 的人體骨架追蹤》的延伸,算是提供一個以 OpenCV 來做顯示的完整地 NiTE 2 + OpenNI 2 的人體骨架追蹤範例;另外,他也算是從《用 OpenCV 畫出 OpenNI 2 的深度、彩色影像》延伸出來的範例,如果還沒看過這兩篇文章的話,建議先看一下。
而這個範例程式所做的事,主要就是透過 OpenNI 2 的 VideoStream 來讀取彩色影像當作背景,並透過 NiTE 2 的 UserTracker 來讀取人體骨架關節點的資訊,並以圓和線、畫出來。最後的結果,應該會像右圖這樣子。
下面就是這個程式的主要架構:
// STL Header #include <iostream> // OpenCV Header #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> // o1. OpenNI Header #include <OpenNI.h> // n1. NiTE Header #include <NiTE.h> // namespace using namespace std; using namespace openni; using namespace nite; int main( int argc, char **argv ) { // o2. Initial OpenNI OpenNI::initialize(); // o3. Open Device Device mDevice; mDevice.open( ANY_DEVICE ); // o4. create depth stream VideoStream mDepthStream; mDepthStream.create( mDevice, SENSOR_DEPTH ); // o4a. set video mode VideoMode mDMode; mDMode.setResolution( 640, 480 ); mDMode.setFps( 30 ); mDMode.setPixelFormat( PIXEL_FORMAT_DEPTH_1_MM ); mDepthStream.setVideoMode( mDMode); // o5. Create color stream VideoStream mColorStream; mColorStream.create( mDevice, SENSOR_COLOR ); // o5a. set video mode VideoMode mCMode; mCMode.setResolution( 640, 480 ); mCMode.setFps( 30 ); mCMode.setPixelFormat( PIXEL_FORMAT_RGB888 ); mColorStream.setVideoMode( mCMode); // o6. image registration mDevice.setImageRegistrationMode( IMAGE_REGISTRATION_DEPTH_TO_COLOR ); // n2. Initial NiTE NiTE::initialize(); // n3. create user tracker UserTracker mUserTracker; mUserTracker.create( &mDevice ); mUserTracker.setSkeletonSmoothingFactor( 0.1f ); // create OpenCV Window cv::namedWindow( "User Image", CV_WINDOW_AUTOSIZE ); // p1. start mColorStream.start(); mDepthStream.start(); while( true ) { // main loop // p2 - p5 ... // p6. check keyboard if( cv::waitKey( 1 ) == 'q' ) break; } // p7. stop mUserTracker.destroy(); mColorStream.destroy(); mDepthStream.destroy(); mDevice.close(); NiTE::shutdown(); OpenNI::shutdown(); return 0; }
基本上,在前面「o2」到「o6」的部分,都是 OpenNI 的初始化與設定。在這段程式碼裡面,除了進行 OpenNI 的初始化外,還建立出深度、以及彩色影像用的 VideoStream;這邊之後雖然都不會直接讀取到深度影像,不過因為 NiTE 2 的 UserTracker 會使用深度影像的資料,所以為了讓彩色影像和深度影相的大小是一致的(這邊是 640×480),所以要建立出深度影像的 VideoStream(mDepthStream),並針對他進行相關的設定。
而之後,「n2」、「n3」的部分,則是 NiTE 2 的初始化,以及 UserTracker 的建立、設定了。
接下來,就是透過 OpenCV,建立一個名為「User Image」的視窗,準備將來用來做顯示之用;都好了之後,就是開始 OpenNI VideoStream 的資料讀取(p1),並進入主迴圈了~在上面的程式碼裡面,這部分是先省略掉的,會在接下來的部分做說明。
而裡面的「p6」的部分,則是去檢查是否有按下鍵盤的「q」,如果有的話,就離開主迴圈,並將 NiTE 和 OpenNI 的所有物件都關閉、並停止程式(p7)。
至於迴圈裡主要處理的部分,程式碼基本上就是下面的樣子:
// p2. prepare background cv::Mat cImageBGR; // p2a. get color frame VideoFrameRef mColorFrame; mColorStream.readFrame( &mColorFrame ); // p2b. convert data to OpenCV format const cv::Mat mImageRGB( mColorFrame.getHeight(), mColorFrame.getWidth(), CV_8UC3, (void*)mColorFrame.getData() ); // p2c. convert form RGB to BGR cv::cvtColor( mImageRGB, cImageBGR, CV_RGB2BGR ); // p3. get user frame UserTrackerFrameRef mUserFrame; mUserTracker.readFrame( &mUserFrame ); // p4. get users data const nite::Array<UserData>& aUsers = mUserFrame.getUsers(); for( int i = 0; i < aUsers.getSize(); ++ i ) { const UserData& rUser = aUsers[i]; // p4a. check user status if( rUser.isNew() ) { // start tracking for new user mUserTracker.startSkeletonTracking( rUser.getId() ); } if( rUser.isVisible() ) { // p4b. get user skeleton const Skeleton& rSkeleton = rUser.getSkeleton(); if( rSkeleton.getState() == SKELETON_TRACKED ) { // p4c. build joints array SkeletonJoint aJoints[15]; aJoints[ 0] = rSkeleton.getJoint( JOINT_HEAD ); aJoints[ 1] = rSkeleton.getJoint( JOINT_NECK ); aJoints[ 2] = rSkeleton.getJoint( JOINT_LEFT_SHOULDER ); aJoints[ 3] = rSkeleton.getJoint( JOINT_RIGHT_SHOULDER ); aJoints[ 4] = rSkeleton.getJoint( JOINT_LEFT_ELBOW ); aJoints[ 5] = rSkeleton.getJoint( JOINT_RIGHT_ELBOW ); aJoints[ 6] = rSkeleton.getJoint( JOINT_LEFT_HAND ); aJoints[ 7] = rSkeleton.getJoint( JOINT_RIGHT_HAND ); aJoints[ 8] = rSkeleton.getJoint( JOINT_TORSO ); aJoints[ 9] = rSkeleton.getJoint( JOINT_LEFT_HIP ); aJoints[10] = rSkeleton.getJoint( JOINT_RIGHT_HIP ); aJoints[11] = rSkeleton.getJoint( JOINT_LEFT_KNEE ); aJoints[12] = rSkeleton.getJoint( JOINT_RIGHT_KNEE ); aJoints[13] = rSkeleton.getJoint( JOINT_LEFT_FOOT ); aJoints[14] = rSkeleton.getJoint( JOINT_RIGHT_FOOT ); // p4d. convert joint position to image cv::Point2f aPoint[15]; for( int s = 0; s < 15; ++ s ) { const Point3f& rPos = aJoints[s].getPosition(); mUserTracker.convertJointCoordinatesToDepth( rPos.x, rPos.y, rPos.z, &(aPoint[s].x), &(aPoint[s].y) ); } // p4e. draw line cv::line( cImageBGR, aPoint[ 0], aPoint[ 1], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 1], aPoint[ 2], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 1], aPoint[ 3], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 2], aPoint[ 4], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 3], aPoint[ 5], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 4], aPoint[ 6], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 5], aPoint[ 7], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 1], aPoint[ 8], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 8], aPoint[ 9], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 8], aPoint[10], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[ 9], aPoint[11], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[10], aPoint[12], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[11], aPoint[13], cv::Scalar( 255, 0, 0 ), 3 ); cv::line( cImageBGR, aPoint[12], aPoint[14], cv::Scalar( 255, 0, 0 ), 3 ); // p4f. draw joint for( int s = 0; s < 15; ++ s ) { if( aJoints[s].getPositionConfidence() > 0.5 ) cv::circle( cImageBGR, aPoint[s], 3, cv::Scalar( 0, 0, 255 ), 2 ); else cv::circle( cImageBGR, aPoint[s], 3, cv::Scalar( 0, 255, 0 ), 2 ); } } } } // p5. show image cv::imshow( "User Image", cImageBGR );
在「p2」的部分,就是從 mColorStream 裡,讀取出彩色感應器的影像,並請轉換成 OpenCV 的格式,也就是 cImageBGR 這個物件,並在之後當作背景來使用。
接下來的「p3」,則是從 mUserTracker 裡,讀取出當下的 UserTracker 分析結果。在「p4」,則是取出分析結果中,使用者的陣列 aUser,並針對裡面每一個 user、依序做處理。而處理的第一部,就是「p4a」,也就是去檢查這個 user 是否是新發現的使用者,如果是的話,就呼叫 UserTracker 的 startSkeletonTracking() 這個函式,開始對這個 user 進行骨架的追蹤。
而如果在使用者是可以看的到的狀況下,就是要開始處理骨架的資料了~在「p4b」就是先取出使用者的骨架資料,並確定目前正在追蹤他的骨架。接下來,則是建立一個大小為 15 的 SkeletonJoint 的陣列,並依序把 15 個關節點的資料都讀取出來(p4c)。
由於關節點的位置是在世界座標系統上,所以如果要用 OpenCV 把他們畫到彩色影像上的話,需要先做座標系統的轉換,把關節點的位置轉換到深度座標系統上;這邊,就是上方「p4d」的部分,轉換過後的點位資料,會儲存在 aPoint 這個 cv::Point2f 型別的陣列裡面。
到上面為止,基本上都算是關節資料的前置處理。處理之後,接下來就是要把關節的相關資訊畫出來了~在「p4e」的部分,是透過 cv::line() 把對應的關節和關節之間,連線連起來畫在 cImageBGR 上。而在「p4f」的部分,則是在把每一個關節點,依序用 cv::circle(),畫出一個一個圓;Heresy 這邊稍微特別一點的,是有去檢查各個關節點位置的可靠度,如果可靠度大於 0.5 的話,就用紅色畫,不然就用綠色。
最後,就是「p5」的部分,用 cv::imshow() 把最後的結果、也就是 cImageBGR 畫出來了~
請問 為什麼世界座標要轉深度座標 才能顯示在彩色呢?
讚讚
請參考: https://kheresy.wordpress.com/2013/01/14/coordinate-converter-in-openni-2/
讚讚
您好,有个问题想请教您。
NiTE能不能建立多个UserTracker,对应不同的Device?
我还是按照单个device的那种写法,但是2个UserTracker都连接到一个device上去了。
谢谢。
讚讚
這個基本上應該是 NiTE 本身的 bug,無解。
讚讚
请问您是否也做过这种尝试呢?后来完全没办法解决吗?
讚讚
這個一般應該無解。
讚讚
那将多个kinect的深度数据分别录制成oni文件,再用UserTracker可以解决这个问题吗?
讚讚
抱歉,Heresy 沒有測試過。也歡迎你分享你測試的結果。
讚讚
您好,我尝试了录制成oni文件,再用nite对应不同的Device的办法。
结果是,必须是处理完一个device的nite必须shutdown之后重新初始化一个新的nite,才能有正常的结果。
讚讚
还有一个问题要请教您。:-)
就是有没有什么办法,可以把用nite的getUserMap处理过一段depth数据流,保存成新的depth数据流,这样就不用每次都用nite去处理。
讚讚
NiTE 處理完的資料,那個格式應該已經不是 depth map 了。
OpenNI 本身沒有定義這方面的儲存方法,所以如果要儲存,就得自己寫了。
最簡單的作法,應該是把它當成圖檔來存取。
讚讚
您好,那请问录制的oni,能不能具体定位到第n帧?
讚讚
請搭配 PlaybackControl 使用。
https://kheresy.wordpress.com/2013/03/04/record-and-playback-of-openni-2/
讚讚
您好,请问 PlaybackControl的seek要怎么用呢,它返回的是一个状态,那怎样把定位到的那一帧输出呢?
讚讚
建議請參考官方的 reference。
他的參數就是要透過 frame index 指定要到哪個時間點了。
讚讚
請問使用一般的視訊頭配上opencv有辦法做出關節辨識嗎?
讚讚
OpenCV 應該沒有提供現成的功能,可以直接做到。
讚讚