體感按鈕實作(OpenCV)


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

程式的原始碼在:https://1drv.ms/f/s!Al8B-ey4Dwfg70iulL28kh-idqK8

裡面的檔案,主要是定義了按鈕的類別的 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 之前也有想試著透過去偵測手是否有握住,來當作控制的基準,但是很遺憾,一直沒有很好的結果…不過之後,應該還是會再試試看吧~等到有一定的成果,會再拿出來分享的。


OpenNI / Kinect 相關文章目錄

廣告

關於 Heresy
https://kheresy.wordpress.com

21 Responses to 體感按鈕實作(OpenCV)

  1. Tiffany says:

    您好,請問一下,因為我現在也在學習設計按鈕,請問一下Heresy大大在設計按鈕時有沒有參考甚麼資料,也想請問一下如果用design pattern 的"Observer"來設計按鈕適合嗎?如果有的話有適合的參考資料嗎?謝謝Heresy大大

    按讚數

    • Heresy says:

      這種事件觸發的架構有很多種,Observer 的確是個方法。
      真要參考的話,應該就是去看別的 GUI 框架是怎麼設計的了。
      像是 Qt 就是用他自己的 Signal / slot。

      按讚數

  2. Timmy says:

    Heresy大大您好 我執行時會出現 “Can’t create hand tracker" 這是為什麼呢??

    按讚數

    • Tiffany says:

      我補充一個問題,就是我的OPENNI跟KINECT似乎還沒連結上。我點擊NI內的SIMPLEVIEWER沒有畫面,還請多多指教,謝謝。

      按讚數

      • Heresy says:

        請先確認你的環境設定正確、NiViewer 能正常運作。
        https://kheresy.wordpress.com/2016/04/08/start-to-use-openni-or-kinect/

        按讚數

        • Tiffany says:

          成功了ㄟ 謝謝!!!!!!

          按讚數

  3. TIMMY says:

    您好 是否可以重新貼原始碼的位置給我 連結失效了

    按讚數

    • Heresy says:

      連結已更新。

      按讚數

  4. titif says:

    您好 Heresy大大
    請問您的程序中的按鈕是否可以用Qt來做呢?
    圖中的手是否可以與滑鼠相連接呢?
    thx

    按讚數

    • Heresy says:

      基本上同樣的概念可以套用,就是看要怎麼寫而已。

      而如果你是要做滑鼠模擬器的話,則是要使用作業系統本身的 SDK 來做控制,這部分目前似乎沒有跨平台的方案可以用。
      所以你如果是要控制 Windows 的滑鼠的話,基本上就是去呼叫 Windows 提供的 API。
      http://msdn.microsoft.com/en-us/library/windows/desktop/ms646310.aspx

      按讚數

      • titif says:

        thank u

        按讚數

  5. titif says:

    您好 Heresy大大
    我試著跑了一下您的原始碼 可是出現以下錯誤信息 希望您能指點一下 謝謝
    我用的是法文版VS2012
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.h(51): warning C4244: ‘initialisation’ : conversion de ‘float’ en ‘int’, perte possible de données
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.h(109): error C2589: ‘(‘ : jeton non conforme à droite de ‘::’
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.h(109): error C2143: erreur de syntaxe : absence de ‘)’ avant ‘::’
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.h(109): error C2059: erreur de syntaxe : ‘)’
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.h(167): error C2589: ‘(‘ : jeton non conforme à droite de ‘::’
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.h(167): error C2143: erreur de syntaxe : absence de ‘)’ avant ‘::’
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.h(167): error C2059: erreur de syntaxe : ‘)’
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.cpp(126): warning C4244: ‘argument’ : conversion de ‘float’ en ‘int’, perte possible de données
    1>c:\users\helene\documents\visual studio 2012\projects\boutonopencv2\boutonopencv2\boutonopencv2.cpp(136): warning C4244: ‘argument’ : conversion de ‘float’ en ‘int’, perte possible de données

    按讚數

    • Heresy says:

      抱歉,法文的錯誤訊息看不懂。
      不過,錯誤訊息應該是在 boutonopencv2.h 的 109 行的地方。
      你有修改過 Heresy 的程式碼嗎?

      按讚數

      • titif says:

        您好 Heresy大大
        我沒有修改過您的程式碼 只是把您的程式碼複製粘貼在一個新的專案裡(boutonopencv2)
        謝謝

        按讚數

        • Heresy says:

          麻煩請確認你的 109 行的內容是什麼。
          目前看起來,比較像是你把 Heresy 最前面的註解都拿掉了,然後錯的地方是 std::min()
          如果是的話,請試著 include <algorithm> 看看。
          http://en.cppreference.com/w/cpp/algorithm/min

          按讚數

          • titif says:

            您好 大大
            我試著加上了include 可是又報了相同的錯誤信息
            非常感謝您的回答

            按讚數

        • titif says:

          您好 Heresy大大
          我的錯誤是在min的m下有個紅色的波浪號
          fProgress = float( std::min( iCurrTimes, iHoldTimes ) ) / iHoldTimes;
          錯誤信息是identifier expected
          thank u

          按讚數

    • Heresy says:

      Heresy 這邊沒碰到這問題,不過根據找到的資料,或許是跟 macro 的 min / max 衝到了
      原因可能是: http://stackoverflow.com/questions/1904635/warning-c4003-and-errors-c2589-and-c2059-on-x-stdnumeric-limitsintmax

      不過,反正只是要取最大值和最小值,就自己修正吧。

      按讚數

      • titif says:

        thank u

        按讚數

      • titif says:

        您好 大大
        我把fProgress = float( std::min( iCurrTimes, iHoldTimes ) ) / iHoldTimes;

        改為

            fProgress = float( (std::min)( iCurrTimes, iHoldTimes ) ) / iHoldTimes;程序就可以跑了

        http://stackoverflow.com/questions/13416418/define-nominmax-using-stdmin-max

        按讚數

        • Heresy says:

          可以正常執行就好。

          按讚數

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: