使用 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 的作法是先把手的位置,透過 HandTracker 的 convertHandCoordinatesToDepth(),從世界座標系統轉換到深度影像座標系統;然後,再乘上 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 的動作的話,滑鼠的位置就會往下位移、而偏離本來的位置…

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

8 thoughts on “使用 PrimeSense NiTE 和 GrabDetector 的簡易版滑鼠模擬器”

  1. 你好,我编译上面的代码时会报错,提示为 “INPUT”: 未声明的标识符。你遇到过这种情况吗?

  2. INPUT 是 Windows API 的一部分,你需要 include 對應的 header 檔(Windows.h)才能使用。
    建議你先試看看放在 SkyDrive 的完整範例

  3. 嗯 问题解决了 是我少加了和 这两个头文件 现在可以用了 谢谢 😀

  4. 发现NITE2的手势检测方法有一个问题,都需要挥手来确定手的位置,这个在实际人机交互里比较不人性化,微软SDK1.8里面不需要这样操作可以实现握拳识别,不知博主有没有注意这个问题^.^

  5. to wilson
    這是因為兩者的實作方法不同所造成的。
    實際上,如果需要你也可以直接去追蹤骨架,然後僅取兩手來用,不一定要用 HandTracker。

  6. 编译你的范例时显示:LINK : fatal error LNK1104: 无法打开文件“GrabDetector.lib”

  7. to chao

    請確認你有正確地在專案內設定 linking 的參數,讓編譯器編譯時可以正確連結到 GrabDetector.lib 這個檔案。
    以 Heresy 提供的範例來說,GrabDetector 這個函式庫的檔案是放在外層目錄的,你也必須要下載。

發佈回覆給「heresy」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。