使用 OpenCV 繪製 NiTE2 的手部資料

| | 0 Comments| 17:36
Categories:

這篇基本上算是之前《NiTE 2 的手勢偵測》和《NiTE 2 的手部追蹤》這兩篇文章的延伸。主要,是透過 OpenCV 來寫個簡單的範例,示範如何把 NiTE2 抓到的手部相關資料畫出來。

首先,這個範例程式 Heresy 有把完整的程式碼放在 SkyDrive 上,有興趣的人可以直接去下載。檔案的位置在:

http://sdrv.ms/106ouYG

這個範例的基本架構,都和之前的《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::Point2fvector),之後畫的時候,會把一點一點都連起來讓他變連續的線段。

  • 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_16UC1cv::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_WAVEGESTURE_CLICK 這兩個手勢的時候,則就把這個轉換好的位置(rPos2D),丟到 vWaveListvClickList 裡,記錄下來。


而在手部追蹤的部分,這邊一樣是先透過 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() 依序把兩點連成線、畫出來了~

再來,則就是手勢的部分。由於這兩部分的資料,都已經儲存在 vWaveListvClickList 裡了,所以接下來,也只需要使用簡單的 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 刪除掉,所以當手離開畫面一段時間後,那隻手的軌跡就會消失;但是由於手勢的紀錄並沒有刪除,所以會一直留在畫面上。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *