這篇基本上算是之前《NiTE 2 的手勢偵測》和《NiTE 2 的手部追蹤》這兩篇文章的延伸。主要,是透過 OpenCV 來寫個簡單的範例,示範如何把 NiTE2 抓到的手部相關資料畫出來。
首先,這個範例程式 Heresy 有把完整的程式碼放在 SkyDrive 上,有興趣的人可以直接去下載。檔案的位置在:
這個範例的基本架構,都和之前的《NiTE 2 的手部追蹤》相同。為了簡化範例程式,這邊沒有特別去使用 OpenNI 抓出彩色影像,而是只有使用 NiTE 提供的深度影像而已。除了 NiTE 的使用外,這個範例也加上了 OpenCV 來做為顯示,這部分基本上可以先參考《用 OpenCV 畫出 OpenNI 2 的深度、彩色影像》這篇文章。
而比較要說明的,應該就是新加入的部分、也就是為了繪製手部位置所加上的程式碼了~
首先,在建立了 OpenCV 的視窗、進入主迴圈之前,有下面這段程式:
// prepare the data for drawing map< HandId,vector<cv::Point2f> > mapHandData; vector<cv::Point2f> vWaveList; vector<cv::Point2f> vClickList; cv::Point2f ptSize( 3, 3 ); array<cv::Scalar,8> aHandColor; aHandColor[0] = cv::Scalar( 255, 0, 0 ); aHandColor[1] = cv::Scalar( 0, 255, 0 ); aHandColor[2] = cv::Scalar( 0, 0, 255 ); aHandColor[3] = cv::Scalar( 255, 255, 0 ); aHandColor[4] = cv::Scalar( 255, 0, 255 ); aHandColor[5] = cv::Scalar( 0, 255, 255 ); aHandColor[6] = cv::Scalar( 255, 255, 255 ); aHandColor[7] = cv::Scalar( 0, 0, 0 );
這段程式碼,主要是在準備為了繪製手部的相關資料、所需要的變數。這邊,基本上包括:
-
mapHandData
STL 的 map 來針對每隻手、記錄他經過的軌跡(cv::Point2f 的 vector),之後畫的時候,會把一點一點都連起來讓他變連續的線段。
-
vWaveList
用 STL 的 vector 來紀錄偵測到的 WAVE 這個手勢的位置,之後會用 cv::circle() 來畫。
-
vClickList
用 STL 的 vector 來紀錄偵測到的 CLICK 這個手勢的位置,之後會用 cv::rectangle() 來畫;下面的 ptSize 就是定義它的大小。
-
aHandColor
定義八種不同的顏色,用來畫手的軌跡。
而在進入主迴圈、讀取出 HandTrackerFrameRef 的資料後,這邊則是先透過他的 getDepthFrame(),來取得深度的畫面。這邊的程式如下:
// prepare background openni::VideoFrameRef mDepthFrame = mHandFrame.getDepthFrame(); // convert data to OpenCV format const cv::Mat mImageDepth( mDepthFrame.getHeight(), mDepthFrame.getWidth(), CV_16UC1, (void*)mDepthFrame.getData() ); // re-map depth data [0,Max] to [0,255] cv::Mat mScaledDepth, mImageBGR; mImageDepth.convertTo( mScaledDepth, CV_8U, 255.0 / 10000 ); // convert gray-scale to color cv::cvtColor( mScaledDepth, mImageBGR, CV_GRAY2BGR );
基本上,和之前《用 OpenCV 畫出 OpenNI 2 的深度、彩色影像》的範例是相同的。這裡先建立出一個 CV_16UC1 的 cv::Mat 物件(mImageDepth)、來儲存深度的影像,然後再透過 convertTo(),把他從 16bit 轉換成 8bit 的灰階圖(mScaledDepth)。
比較不一樣的是,由於等一下還要畫彩色的東西在這張圖上,所以最後再透過 cvtColor(),把灰階圖轉換成 OpenCV 的 BGR 彩色影像。
而接下來,大致上都和《NiTE 2 的手部追蹤》這篇文章裡的範例相似,一樣是先透過 getGestures() 來讀取手勢偵測的結果,來找到手部的位置,然後在使用發現新的手勢的時候,就呼叫 startHandTracking() 來進行追蹤。不過這邊因為要記錄手勢發生的位置,所以這邊還需要透過 HandTracker 提供的 convertHandCoordinatesToDepth() 這個函式,將偵測到的手勢位置,轉換到深度影像的座標系統上:
const Point3f& rPos = rGesture.getCurrentPosition(); cv::Point2f rPos2D; mHandTracker.convertHandCoordinatesToDepth( rPos.x, rPos.y, rPos.z, &rPos2D.x, &rPos2D.y );
而由於之後都是要用 OpenCV 來繪製,所以這邊 Heresy 就是用 OpenCV 的 Point2f 來儲存轉換的結果了。
接下來,當偵測到 GESTURE_WAVE 或 GESTURE_CLICK 這兩個手勢的時候,則就把這個轉換好的位置(rPos2D),丟到 vWaveList 或 vClickList 裡,記錄下來。
而在手部追蹤的部分,這邊一樣是先透過 getHands() 來取得所有正在進行追蹤的手部資料、並依序各自處理。在之前的範例裡,就是很簡單地透過 iostream 做輸出,而這邊由於要把它紀錄下來、讓 OpenCV 繪製,所以稍微複雜一點。
首先,當偵測到新的手的時候(isNew()),我們就在 mapHandData 這個 map 裡面(參考),以 HandId 為 key、搭配一個空的 vector 建立一組新的資料,用來記錄之後這隻手的移動軌跡;而如果手正在被追蹤的話(isTracking()),則就把轉換到深度影像座標系統的位置、加到這支手對應的 vector 裡、當作歷史軌跡記錄下來。而如果手部狀態是失去(isLost())的話,則是透過 erase(),把這組資料刪除、不要再紀錄。
這樣的程式,大概就會像下面這樣:
const HandData& rHand = aHands[i]; HandId uID = rHand.getId(); // create new hand if( rHand.isNew() ) { mapHandData.insert( make_pair( uID, vector<cv::Point2f>() ) ); } if( rHand.isTracking() ) { // get position and convert to depth const Point3f& rPos = rHand.getPosition(); cv::Point2f rPos2D; mHandTracker.convertHandCoordinatesToDepth( rPos.x, rPos.y, rPos.z, &rPos2D.x, &rPos2D.y ); mapHandData[uID].push_back( rPos2D ); } if( rHand.isLost() ) mapHandData.erase( uID );
到這邊為止,都算是資料準備的部分;實際用來繪製的程式碼,則是像下面這樣。
首先,繪製手的軌跡的部分,程式碼如下:
// draw hand data for( auto itHand = mapHandData.begin(); itHand != mapHandData.end(); ++ itHand ) { const cv::Scalar& rColor = aHandColor[ itHand->first % aHandColor.size() ]; const vector<cv::Point2f>& rPoints = itHand->second; for( int i = 1; i < rPoints.size(); ++ i ) cv::line( mImageBGR, rPoints[i-1], rPoints[i], rColor, 2 ); }
這邊的程式碼,基本上就是掃過整個 mapHandData,去把每隻手的軌跡,都依序畫出來。而在顏色的部分,為了區分不同的手,所以 Heresy 是透過 itHand->first % aHandColor.size()(取餘數)這樣的形式,算出 0 – 7 之間的值,並在 aHandColor 裡取出要使用的顏色(rColor)。
接下來,就是掃過這隻手所有經過過的點,並使用 cv::line() 依序把兩點連成線、畫出來了~
再來,則就是手勢的部分。由於這兩部分的資料,都已經儲存在 vWaveList 和 vClickList 裡了,所以接下來,也只需要使用簡單的 for 迴圈,就可以把裡面每個點都畫出來了~
而為了區隔這兩者的不同,Heresy 是用 cv::cricle() 來把 click 的手勢發生的位置、用紅色的一個半徑為 5 的空心圓畫出來;其程式碼如下:
// draw click gesture data for( auto itPt = vClickList.begin(); itPt != vClickList.end(); ++ itPt ) cv::circle( mImageBGR, *itPt, 5, cv::Scalar( 0, 0, 255 ), 2 );
而在 wave 的手勢的部分,Heresy 則是用一個綠色的空心方塊來畫(大小是 ptSize 的長寬乘 2),其程式碼如下:
// draw wave gesture data for( auto itPt = vWaveList.begin(); itPt != vWaveList.end(); ++ itPt ) { cv::rectangle( mImageBGR, *itPt - ptSize, *itPt + ptSize, cv::Scalar( 0, 255, 0 ), 2 ); }
如此一來程式執行後,就會如同本文右上角的圖,顯示出灰階的深度影像。而當使用者作出 Click 或 Wave 的手勢之後,就會用不同的顏色,畫出手移動的軌跡了~
而由於當手部消失的時候,就會把相關紀錄從 mapHandData 刪除掉,所以當手離開畫面一段時間後,那隻手的軌跡就會消失;但是由於手勢的紀錄並沒有刪除,所以會一直留在畫面上。
请问Heresy,win7下openNI2+ kinect1 按照您的代码, 灰度图中视角没有调整好怎么办(左手显示有两只),
讚讚
抱歉,無法理解你的「視角沒調整好」是什麼意思。
讚讚
請問程式碼連結可以再給一次嗎~感謝
讚讚
網頁連結已更新。
讚讚
請問一下:
編譯這個範例程式完成後,執行程式時出現了應用程式無法啟動(0x000007b)的錯誤,
請問該如何解決呢? 版本是vs2012 我是用debug 64位元 編譯的。
讚讚
抱歉,Heresy 沒碰過這樣的問題。
不過,個人會建議你先試試看用更基本的專案,來試試看是否所有的 Visual Studio 專案都無法正常執行。
讚讚
好的,謝謝你
讚讚
看了您的几篇博文,非常有启发性,也很清晰,楼主什么时候能推出类似手部区域分割和动作识别的博文供大家学习呀~~
讚讚
這東西,短時間機會不大了。
讚讚
[…] NiTE 2 的手部追蹤 / 使用 OpenCV 繪製 NiTE2 的手部資料 […]
讚讚
[…] 而在 main() 這個主程式裡面,主要的 NiTE 2 程式架構,實際上就是之前《使用 OpenCV 繪製 NiTE2 的手部資料》的範例,所以這部份就不多做說明了。 […]
讚讚