使用 PrimeSense NiTE 和 GrabDetector 的簡易版滑鼠模擬器


在 OpenNI 1 的時候,Heresy 曾經在《OpenNI 的手部追蹤、外加簡單的滑鼠模擬》一文章,介紹過 OpenNI 1.x 的 HandsGenerator,並用他來做簡單的滑鼠模擬功能;而在當時,是直接使用 OpenNI 定義的「click」手勢,來當作左鍵,不過效果並不是很好…

而這一篇呢,Heresy 則是試著在 OpenNI 2 的環境下,使用 PrimeSense NiTE 2 的 HandTracker 來追蹤手的位置,並用 PrimeSense 另一套 middleware Grab Detector,來透過「grab」這個手勢,作為滑鼠左鍵的觸發條件。完整的範例程式,已經放到 Heresy 的 OpenNI 2 範例程式集裡面了,有興趣的話可以到 http://sdrv.ms/14VWxlb 下載。

這個程式並沒有圖形介面,在執行後,命令提示字元的視窗也不會立刻有任何訊息。而這個時候,只要對著感應器做出 click 或 wave 的手勢,就可以偵測到手的位置、並開始追蹤,而此時滑鼠游標也會跟著手移動了。而接下來,只要對著螢幕做出 grab 的手勢,就可壓下滑鼠左鍵、放開後就放開滑鼠的左鍵。

不過在開始介紹程式的內容之前,首先先講一下預備知識。如同前面所說的,這個小程式是使用 NiTE 2 的 HandTracker、以及 Grab Detector 來實作的,所以除了要理解這個程式,除了要先知道怎麼寫 OpenNI 2 的程式,也需要了解 Grab Detector 和 NiTE 2 的 HandTracker 怎麼用。所以如果還不知道的話,麻煩請先參考下列文章:


程式的初始化

首先,這份程式的主體,是以之前《PrimeSense Grab Detector 簡單範例》一文中的範例程式為主體,做修改而成的,所以基本上大部分的內容,都和當時的一樣,所以就不細部說明了。

基本上,這邊一開始,就是進行 OpenNI 2 與 NiTE 2 初始化,建立出所需要的深度影像和彩色影像的 VideoStream,以及 HandTracker 的物件。

接下來,則就是針對 Grab Detector 做初始化的設定。這邊 Heresy 沒有去使用 Listener 的架構來實作,而是直接在主迴圈裡面去呼叫 GetLastEvent(),來取得最新的資料;不過如果要改寫成 Listener 的模式,基本上也是沒問題的。

如此一來,程式在執行後,就會等待使用者對著感應器做出「Wave」或「click」的手勢,然後開始追蹤使用者的手、並且偵測是否做出「grab」的手勢了。


送出滑鼠事件

接下來,就是主迴圈的部分。基本上主迴圈的內容和之前也大致相同,也就是先透過 HandTrackerreadFrame() 來讀取新的資料,然後進行手勢、手部的分析;而如果手正在被追蹤的話,接下來就是再去讀取出彩色影像和深度影像,交給 Grab Detector 作分析。

而和之前的程式不同的地方,主要也就是在這邊,要視狀況、透過 Windows 提供的 API、送出滑鼠的事件了~這邊的程式碼基本上如下:

if( rHand.isTracking() )
{
  // build mouse event
  INPUT mWinEvent;
  mWinEvent.type = INPUT_MOUSE;
  mWinEvent.mi.time = 0;
  mWinEvent.mi.mouseData = 0;
  mWinEvent.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE;
 
  // update hand position
  const Point3f& rPos =rHand.getPosition();
  pGrabDetector->SetHandPosition( rPos.x, rPos.y, rPos.z );
 
  // read color frame
  VideoFrameRef mColor;
  vsColorStream.readFrame( &mColor );
 
  // update depth and color image
  VideoFrameRef mDepth = mHandFrame.getDepthFrame();
  pGrabDetector->UpdateFrame( mDepth, mColor );
 
  // check last event if not using listener
  PSLabs::IGrabEventListener::EventParams mEvent;
  if( pGrabDetector->GetLastEvent( &mEvent ) == openni::STATUS_OK )
  {
    // if status changed
    if( mEvent.Type != eLastEvent )
    {
      switch( mEvent.Type )
      {
      case PSLabs::IGrabEventListener::GRAB_EVENT:
        mWinEvent.mi.dwFlags |= MOUSEEVENTF_LEFTDOWN;
        cout << "Grab" << endl;
        break;

      case PSLabs::IGrabEventListener::RELEASE_EVENT:
        mWinEvent.mi.dwFlags |= MOUSEEVENTF_LEFTUP;
        cout << "Release" << endl;
        break;
      }
    }
    eLastEvent = mEvent.Type;
  }
 
  // compute mouse position
  float  fX, fY, fW = mDepth.getWidth(), fH = mDepth.getHeight();
  mHandTracker.convertHandCoordinatesToDepth( rPos.x, rPos.y, rPos.z, 
                                              &fX, &fY );
  mWinEvent.mi.dx = 65535 * fX / fW;
  mWinEvent.mi.dy = 65535 * fY / fH;
 
  // send input data
  SendInput( 1, &mWinEvent, sizeof(mWinEvent) );
}

其中,黃底的部分,就是這次為了送出滑鼠事件加上的程式。

這邊用來送出滑鼠事件的函式,是 SendInput() 這個函式(MSDN);要使用這個函式,需要 include Windows.h 這個 Header 檔,並且 link User32.lib 這個檔案。

他基本上是 Windows 提供、用來送鍵盤、滑鼠,還有其他硬體事件給電腦用的,而要送進去的資料形式是 INPUT 這個類別(MSDN);而要執行這個函式,則需要三個參數,第一個是輸入的資料數量,第二個是輸入資料的陣列(也就是 INPUT 的陣列),最後則是輸入資料的大小。

INPUT 這個類別,裡面可以記錄 MOUSEINPUTKEYBDINPUTHARDWAREINPUT 三種不同的事件資訊,所以在使用的時候,需要先指定 INPUTtype,然後再填入自己要使用的事件資訊。

在上面的程式碼一開始「build mouse event」的區段,就是在做 INPUT 最簡單的配置。這邊先建立出一個 INPUT 的物件 mWinEvent,然後指定他的 typeINPUT_MOUSE,代表他是要使用 MOUSEINPUT 的資料、也就是它的成員 mi

MOUSEINPUT 要填的資料(MSDN),包括了滑鼠的座標/位移(dxdy)、基本控制的 flag(dwFlags)、其他資料(mouseData)、額外資料(dwExtraInfo)、時間(time)。這裡是先把 timemouseData 都歸零後,再來設定 dwFlags,也就是設定這個滑鼠事件要做什麼事,這邊指定的 MOUSEEVENTF_MOVE 是代表移動滑鼠的位置,而指定的移動方法,則是 MOUSEEVENTF_ABSOLUTE、也就是指定絕對的座標位置;如果不設定的 MOUSEEVENTF_ABSOLUTE 的話,則會變成是相對移動。

接下來,在讀取 Grab Detector 的結果後,這邊是去針對所得到的結果,來做不同的處理。Heresy 的設計,是當手捏起來(grab)的時候,就相當於按下滑鼠的左鍵(壓住),這邊的作法,就是在 dwFlags 再加上 MOUSEEVENTF_LEFTDOWN;而手放開,就相當於放開滑鼠左鍵,也就是幫 dwFlags 再加上 MOUSEEVENTF_LEFTUP。在這樣的設計下,只要手捏起來、放開,就相當於按下滑鼠左鍵、再放開的這個動作了!

最後,則是要計算滑鼠的座標。在指定滑鼠位置是絕對座標的情況下,MOUSEINPUTdxdy 代表的就是滑鼠在桌面上的位置,只是不管是 x 或 y,值的範圍都是 0 – 65,535。所以在這邊,Heresy 的作法是先把手的位置,透過 HandTrackerconvertHandCoordinatesToDepth(),從世界座標系統轉換到深度影像座標系統;然後,再乘上 65535、除以寬/高,來做轉換。

不過實際上,由於手的位置無法真的碰到深度影像的周邊,所以如果照這樣計算的話,會發現滑鼠永遠碰不到四邊和四個角落。因此,這邊就需要再做一些額外的轉換,才能讓滑鼠的位置,可以涵蓋到整個桌面的範圍。下面就是 Heresy 這邊用的轉換方式:

65535 * min( max( ( fX - 0.1f * fW ) / ( 0.8f * fW ), 0.0f ), 1.0f )

這邊基本上,就是只取整個深度影像裡、寬和高的 80% 的區域,先把 fXfY 換算成到 0.0 – 1.0 之間的浮點數,並透過 min()max(),來確定他不會超出範圍,最後再乘上 65535,讓值分布在 0 – 65,535 的範圍內。如此一來,就大致可以對應到到整個桌面了~如果覺得還是沒辦法涵蓋整個需要的操作範圍的話,就是要再去修改這邊調整的方法了。

而當 INPUT 的所有參數都設定好了後,接下來就可以透過 SendInput() 把他送出去了~


這片大概就這樣了。基本上,這樣勉強算是一種用手來操作滑鼠的方法。不過,實際在使用,應該也可以發現:雖然改用 Grab 來做左鍵的觸發條件,比用 click 的手勢來的好點,可以做到按下、放開的區隔;但是另一方面,他和 click 一樣,在做出 grab 的動作的時候,其實位置也是會偏掉的。也就是說,當透過手把滑鼠移到定位後,如果因為按下左鍵而做出 grab 的動作的話,滑鼠的位置就會往下位移、而偏離本來的位置…

所以,基本上這邊這個範例程式,實用性應該還是不高,大概只能用在按鈕超大的應用了…真正要用手勢來操作滑鼠的話,可能還是得再想其他方法來觸發滑鼠左鍵了。


OpenNI / Kinect 相關文章目錄

對「使用 PrimeSense NiTE 和 GrabDetector 的簡易版滑鼠模擬器」的想法

  1. Heresy 你好:

    關於使用 Grab Detector 來模擬簡易的滑鼠功能,如果我想要將體感偵測的時間做修改(例如:偵測動作時間:1秒偵測4次),請問我需要修改哪些部份的 frame 值,才能放慢 or 加快體感偵測器的偵測動作速度呢?

    以下是我找到的深度 & 彩色影像的讀取大小和速度,不確定是否要修改此處,或是其他地方?

    // set video mode
    VideoMode mMode;
    mMode.setResolution( 640, 480 );
    mMode.setFps( 30 );
    mMode.setPixelFormat( PIXEL_FORMAT_DEPTH_1_MM )

    // set video mode
    VideoMode mMode;
    mMode.setResolution( 640, 480 );
    mMode.setFps( 30 );
    mMode.setPixelFormat( PIXEL_FORMAT_RGB888 );

    請Heresy指教 謝謝

    • OpenNI 提供的 VideoMode 設定 FPS 的功能是要看裝置有支援哪幾種,並不能任意修改值。
      如果要自己控制更新頻率或處理頻率的話,需要自己透過計時器之類的功能來做,OpenNI 並沒有提供這樣的介面。

發表迴響

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

WordPress.com 標誌

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

Google photo

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

Twitter picture

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

Facebook照片

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

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.