體感按鈕實作(OpenCV)

| | 2 Comments| 13:54
Categories:

這篇,算是簡單的概念實作測試吧…Heresy 是試著在 OpenCV 的環境下,透過 NiTE 的手部追蹤的功能,來時做體感的按鈕。由於只是概念實作,所以在圖形的部分,算相當地簡單就是了~如果有需要的話,也可以自己根據需求作加強。

程式的原始碼在:http://sdrv.ms/YBLHOM

裡面的檔案,主要是定義了按鈕的類別的 NIButtons.h,以及主程式的 NIButtons.cpp 的部分。

Heresy 基本上實作了兩種不同的按鈕,一種是要按下的 PressButton,另一種則是要把手停在按鈕內一段時間的HoldButton;而為了架構上的一致性,這兩者都繼承自 AbsNIButton

而在操作時,先需要透過 NiTE 定義的 click 和 wave 這兩種手勢,來開始追蹤手部的位置,在偵測到手的位置後,畫面上會有一個白點,代表手的位置。

當把手移到「Press」的按鈕上後,當手往前推,就可以看到按鈕內有一條線往上移動,等移動到頂後,顏色就會變紅色、就代表按下按鈕了;不過這個按鈕的設計,是在放開的時候才會觸發事件,所以要再把手縮回才會真正觸發到事件(命令提示字元會有輸出)。

而如果把手移到「Hold」這個按鈕上後,只要手還在按鈕內,按鈕內的線就會往上移動,一樣是到頂、按鈕變成紅色後就代表按下按鈕了;而這個按鈕的設計和「Press」不同,是一到紅色就會觸發按鈕的事件。

接下來,下面算是進一步的說明:


按鈕的介面定義

首先是 NIButtons.h 裡的三個類別,AbsNIButtonPressButtonHoldButton。其中 AbsNIButton 是抽象類別,是用來定義按鈕的介面用的,而 PressButtonHoldButton 則是繼承自 AbsNIButton,所以在使用上的介面是相同的,之後可以方便使用。

AbsNIButton 這個類別,他對外開放的只有三個成員函式:建構子、Draw()CheckHand(),這也是這一系列按鈕,在外部使用時,唯一會用到的東西。

建構子

建構子的部分,他的介面如下:

AbsNIButton( const std::string& rText,
const cv::Rect& rRect,
std::function<void()> funcToDo )

在建立一個按鈕的時候,需要傳遞三個參數給他,第一個 rText 是一個字串,算是按鈕的標題;第二個 rRect 則是 OpenCV 的 Rect、代表這個按鈕的範圍。第三個 funcToDo 則是 C STL 的 function object(參考),基本上就是傳遞一個函式進來,在按鈕被觸發的時候,自動去執行這個函式。

而由於 PressButtonHoldButton 的建構子的介面也都直接繼承自 AbsNIButton,所以之後要使用的話,就是以這個方法來建立按鈕。

繪圖函式

至於 Draw() 的部分,就是在 cv::Mat 上畫出這個按鈕的函式,在呼叫時需要把一個 cv::Mat 的參考傳進來,讓按鈕知道要把自己畫在哪裡。而裡面的內容,則是根據按鈕的狀態(eState)、會有不同的繪製方法;這邊 Heresy 只有使用 OpenCV 最簡單的繪圖函式來做繪製的動作,其實相當單調,如果有需要的話,則可以修改這個函式,讓按鈕更漂亮。

裡面比較特別的,應該是當按鈕的狀態是 IN_SIDE 的時候,這個時候 Draw() 會根據 fProgress 這個變數、來繪製按鈕按下的進度(就是那條會往上移動的線);至於 fProgress 的算法,則是根據不同的按鈕而有所不同,不過基本上會是在 CheckHand() 內做計算的。

檢查手部位置、更新狀態

最後的 CheckHand() 這個函式,則是傳遞手部的 x、y、z 值進來,讓按鈕判斷自己和手部位置的關係的;包括了手是否在按鈕上、是否已經按下按鈕等等。而由於不同的按鈕的實作方法不同,所以這邊只是定義一個介面而已,並沒有在這個抽象類別裡實作。他回傳的值,則是一個布林變數,代表按鈕是否有被觸發。

而要注意的是,這邊的 x, y 基本上是深度座標系統上的座標,z 則是深度值;而由於 NiTE 的 HandTracker 取得的手部位置是世界座標系統,所以在傳進來之前,需要先做轉換。另外,在這個函式裡面,也需要去計算按鈕目前的進度,也就是 fProgress 這個變數,來做為顯示、以及是否按下按鈕的判斷之用。


PressButton

PressButton 是一個用「按下」(往前推)這個方式來觸發按鈕的實作。基本上,應該算是參考了 Kinect Interaction 後設計出來的東西了~

他的基本概念,就是當手第一次進入按鈕的時候,去記錄下當下的深度值做為參考深度(iInitDepth);而之後,當手往前推的時候,由於越來越靠近感應器,所以深度值會越來越小,當小到一定程度(iPressDepth、這邊是設定成 100、也就是 10cm)後,就算是按下按鈕了。

不過由於這個按鈕的設計,是當按下後、放開時才會去觸發事件;所以實際上,這裡會在按下後、持續去檢查手的深度,直到手又縮回一定程度,才會去執行所指定的 callback function(mFunc)。這邊、當手在按鈕內時的判斷狀態程式碼,基本上如下:

// inside the button
if( eState == OUT_SIDE )
{
//first inside, record initial depth
iInitDepth = iCurrDepth;
eState = IN_SIDE;
}
else if ( eState == IN_SIDE )
{
// press distance long enough
if( iInitDepth - iCurrDepth > iPressDepth )
eState = PRESSED;
}
else if( eState == PRESSED )
{
// release the button, trigger the callback function
if( iInitDepth - iCurrDepth < iPressDepth )
{
mFunc();
eState = IN_SIDE;
return true;
}
}

而中間的進度計算,則是:

float( min( max( iInitDepth-iCurrDepth, 0 ), iPressDepth ) ) / iPressDepth;

基本上就是目前深度(iCurrDepth)和參考深度(iInitDepth)的差異,再除以按下按鈕所需的深度(iPressDepth)了~而為了讓值的範圍確定是在 0 – 1 之間,所以還需要做一些處理。

而如果手不在按鈕內的話,則是直接將狀態設定為 OUT_SIDE 就可以了。這樣的話,如果是單純的誤觸,還可以透過「不把手縮回、往旁邊移出按鈕」的動作,來取消按下的動作。

至於完整的程式碼,就請直接參考 PressButton::CheckHand() 這個函式的內容了~


HoldButton

HoldButton 的設計,是當手在按鈕上的時候,就會開始計時,等到手停留在按鈕上的時間夠久,就算是按下按鈕了;這個設計的概念,應該算是 Xbox 360 的體感遊戲常常拿來用的。

他的 CheckHand() 的函式內容如下:

bool CheckHand( int x, int y, int z )
{
if( CheckInside( x, y ) )
{
iCurrTimes;
// compute the progress
fProgress = float( std::min( iCurrTimes, iHoldTimes ) ) / iHoldTimes;

// inside the button
if( eState == OUT_SIDE )
{
eState = IN_SIDE;
}
else if ( eState == IN_SIDE )
{
if( iCurrTimes > iHoldTimes )
{
// hold long enough, trigger callback function
mFunc();
eState = PRESSED;
return true;
}
}
}
else
{
// outside the button, reset timer
iCurrTimes = 0;
eState = OUT_SIDE;
}
return false;
}

當手在按鈕內的時候,計數器(iCurrTimes)就會在每次檢查時做累加、來計算手維持在按鈕上的次數(時間),而進度則就是目前的次數除以目標次數(iHoldTimes、這邊是設定為 30 次)了;當持續的次數第一次超過目標次數後,就相當於按鈕已經被按下,這時候就會去執行指定的 callback function(mFunc)。

而如果手離開了按鈕的範圍的話,則會去將計數器的值設定為 0,讓下次手進入按鈕後,可以重新計算。

這個實作的方法相當地簡單,只要在每次更新畫面的時候,都去呼叫 CheckHand() 這個函式來進行檢查,就可以做到類似計時的效果。不過實際上,由於他是根據呼叫的次數來做計算的,所以使用上的效果,會受到程式執行速度的影響;比較好的方法,應該是改取時間來做計算,不過這樣寫會稍微複雜一點,所以在這邊就不提了。


主程式

NIButtons.cpp 這個檔案裡,主要就是主程式的部分了。而除了主程式外,這邊還有定義了一個 CHand 的類別,是用來游標繪製的部分;不過現在為了簡化,就只有畫一個白色方塊而已,如果有需要,可以自己改 Draw() 這個函式,讓他看起來更好看。

而在 main() 這個主程式裡面,主要的 NiTE 2 程式架構,實際上就是之前《使用 OpenCV 繪製 NiTE2 的手部資料》的範例,所以這部份就不多做說明了。

按鈕的初始化

比較不一樣的地方,是這邊透過了 vector<AbsNIButton*> 這個按鈕的陣列(vButtons),來記錄整個畫面上的按鈕、作為後續處理之用;由於 AbsNIButtonPressButtonHoldButton 的抽象型別,所以這邊可以把 PressButtonHoldButton 這兩種類型的按鈕都丟進去、一起使用、管理。

在範例程式裡面,Heresy 是兩種按鈕個加入一個,來做示範,程式碼如下:

vector<AbsNIButton*> vButtons;
vButtons.push_back( new PressButton( "Press",
cv::Rect( 70, 50, 100, 50 ),
[](){ cout << "Press Button 1" << endl; } ) );
vButtons.push_back( new HoldButton( "Hold",
cv::Rect( 70, 120, 100, 50 ),
[](){ cout << "Press Button 2" << endl; } ) );

這邊加入的第一個按鈕,是名字叫做「Press」的 PressButton,他的位置是在 ( 70, 50 )、大小是 100 x 50;而按下按鈕後要做的事,Heresy 這邊則是用一個 C 11 的 Lambda expression、產生一個匿名函式傳進去,而這個函式的內容,就是單純地輸出「Press Button 1」而已。

如果不習慣 lambda expression 的語法的話,他實際上就相當於先定義一個函式 function1()

void function1(){  cout << "Press Button 1" << endl;}

然後再呼叫

vButtons.push_back( new PressButton( "Press",
cv::Rect( 70, 50, 100, 50 ),
function1 ) );

這樣的意義是相同的,只是用 lambda expression 比較簡單而已。

而第二個按鈕,則是名稱叫做「Hold」的 HoldButton,位置是在 ( 70, 120 )、大小一樣是 100×50;而按下按鈕後所做的事,也一樣是用 lambda experssion 建立一個匿名函式傳進去,實際上的動作則是會輸出「Press Button 2」這個字串。

如果希望按鈕按下後能做一些有意義的事,基本上只要修改傳進去的函式就可以了~

主迴圈

在主迴圈內,基本上和之前的範例一樣,會去把深度圖轉換成 OpenCV 的格式後、拿來作背景繪製出來。除此之外,也一樣是透過 NiTE2、先使用手勢辨識抓到手部的位置後、再進行手部位置的追蹤。

而這邊,為了確定是哪一隻手有操作權,會把最後一個開始追蹤的手的編號(HandID)記錄在 mActiveHand 這個變數;之後,就僅針對這隻手的位置,來進行處理了。而基本上所做的動作,就是設定游標(mHandImage)的位置,並去讓 vButtons 裡的每個按鈕,都透過 CheckHand() 這個函式,來做手的位置的檢查了。

最後,則是透過呼叫 Draw(),把手的位置、以及各個按鈕都畫出來了~


這個範例程式大概就是這樣了。Heresy 寫這隻程式,主要只是用來驗證,這樣的按鈕設計是否合用、方便,所以實際上不管是畫面,還是程式碼,應該都還有加強的空間(尤其是畫面 XD)。

實際上,HoldButton 的概念,Heresy 在 OpenNI 1 的時候,就已經實作過了,基本上算是可以用的東西。但是當時想做的 PressButton,本來是規劃在空間中設定一個虛擬的平面,當手碰到這個虛擬平面後,就算按下按鈕;不過這樣的架構,實際上由於手在左右移動的時候,一定會有深度上的變化,所以實際上相當難控制,後來也就放棄了。

而現在則是參考 Kinect Interactions 的使用效果,當手移到按鈕上後,才去記錄參考深度,並透過他來判斷是否按下;這樣的設計,在操作上算是相當方便、而且穩定~某方面來說,由於動作不需要像 click 那麼大,所以使用時也更方面;而在按鈕的事件設計上,甚至也可以設計成有 on press 和 on release 兩種事件,彈性相當地大。Heresy 這邊以後開發程式,應該會以這樣的雛型、來做操作介面的繼續開發吧~

另一方面,這樣的按鈕設計最大的缺點,就是這樣的「觸控」機制,基本上都是需要定義一個「可以按的區域」,然後判斷手是否在上面,才能完成這樣的功能;所以這樣的架構,是無法用來模擬一般滑鼠的點擊的事件的。Heresy 之前也有想試著透過去偵測手是否有握住,來當作控制的基準,但是很遺憾,一直沒有很好的結果…不過之後,應該還是會再試試看吧~等到有一定的成果,會再拿出來分享的。

2 thoughts on “體感按鈕實作(OpenCV)”

  1. 您好,向您請教一個問題:不知您有沒有測試過這個API:http://www.openni.org/files/3d-hand-tracking-library/
    我下載測試後發現,我實現的效果和他視頻中的效果差距很大,不知道是什麼原因。希望您能夠抽空測試一下,看看是否能達到視頻中的效果呢? 先謝謝了~

Leave a Reply

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