標籤:style blog http color 使用 os
像點擊(clicks)是GUI平台的核心,輕點(taps)是觸摸平台的核心那樣,手勢(gestures)是Kinect應用程式的核心
關於手勢的定義的中心在於手勢能夠用來交流,手勢的意義在於講述而不是執行
在人機互動領域,手勢通常被作為傳達一些簡單的指令而不是交流某些事實、描述問題或者陳述想法
使用手勢操作電腦通常是命令式的,這通常不是人們使用手勢的目的。例如,揮手(wave)這一動作,在現實世界中通常是打招呼的一種方式,但是這種打招呼的方式在人機互動中卻不太常用。通常第一次寫程式通常會顯示“hello”,但我們對和電腦打招呼並不感興趣。
但是,在一個繁忙的餐館,揮手這一手勢可能就有不同的含義了。當向服務員招收時,可能是要引起服務員注意,需要他們提供服務。在電腦中,要引起電腦注意有時候也有其特殊意義,比如,電腦休眠時,一般都會敲擊鍵盤或者移動滑鼠來喚醒,以提醒電腦“注意”。當使用Kinect時,可以使用更加直觀的方式,就行少數派報告阿湯哥那樣,抬起雙手,或者簡單的朝電腦揮揮手,電腦就會從休眠狀態喚醒。
我們想要使用的任何Kinect手勢必須基於應用程式的使用者 和應用程式的設計和開發人員之間就某種手勢代表的含義達成一致。
自然使用者介面是一系列技術的合計,他包括:語音辨識,多點觸控以及類似Kinect的動感互動介面,他和Windows和Macs作業系統中滑鼠和鍵盤互動這種很常見圖形互動介面不同
NUI介面的設計充分利用了使用者預先就會的技能,使用者和UI進行互動感到很自然,使得他們甚至忘了是從哪裡學到這些和UI進行互動所需的技能的
自然使用者介面的 依賴於先驗知識和不需要媒介的互動這兩個特徵是每一種NUI介面的共同特徵
圖形化使用者介面中按鈕的一個通常的特徵是他提供了一個懸浮狀態來指示使用者游標已經懸停在的按鈕上方的正確位置。這種懸浮狀態將點(click)這個動作離散開來。懸浮狀態可以為按鈕提供一些額外的資訊。當將按鈕移植到觸控螢幕互動介面時,按鈕不能提供懸浮狀態。觸控螢幕介面只能響應觸摸。因此,和電腦上的映像使用者介面相比,按鈕只能提供“擊”(click)操作,而沒有“點”(point)的能力。
基於Kinect的圖形介面中,按鈕的行為和觸摸介面中的剛好相反,他只提供了懸浮(hover)的“點”(point)的能力,沒有“擊”(click)的能力。按鈕這種更令使用者體驗設計者感到沮喪的弱點,在過去的幾年裡,迫使設計者不斷的對Kinect介面上的按鈕進行改進,以提供更多巧妙的方式來點擊視覺元素。這些改進包括:懸停在按鈕上一段時間、將手掌向外推(笨拙地模仿點擊一個按鈕的行為)
人們通常將自然互動介面劃分為三類:語音互動介面,觸摸互動介面和手勢互動介面。
在手勢互動介面中,純粹的手勢,姿勢和追蹤以及他們之間的組合構成了互動的基本術語。對於Kinect來說,目前可以使用的有8個通用的手勢:揮手(wave),懸浮按鈕(hover button),磁吸按鈕(magnet button),推按鈕(push button),磁吸投影片(magnetic slide),通用暫停(universal pause),垂直捲軸(vertical scrolling)和滑動(swipping)。
在互動設計中,易用性有兩個方面:可用(affordance)和反饋(feedback)。反饋就是說使用者知道當前進行中的操作。在網頁中,點擊按鈕會看到按鈕有一點位移,這就表示互動成功。滑鼠按鍵按下時的聲音在某種意義上也是一種反饋,他表示滑鼠在工作。
如果說反饋發生在操作進行中或者之後,那麼可用性(affordance)就發生在操作之前了。可用性就是一種提示或者引導,告訴使用者某一個可視化元素是可以互動的,指示使用者該元素的用處。在GUI互動介面中,按鈕是能夠最好的完成這些理念的元素。按鈕通過文字或者表徵圖提示來執行一些函數操作。GUI介面上的按鈕通過懸浮狀態可以提示使用者其用途。
以上基本上都是廢話 ~~ 下面是具體的手勢識別實現
有三種基本的方法可以用來識別手勢:基於演算法,基於神經網路和基於手勢樣本庫。每一種方法都有其優缺點。開發人員具體採用那種方法取決與待識別的手勢、項目需求,開發時間以及開發水平。基於演算法的手勢識別相對簡單容易實現,基於神經網路和手勢樣本庫則有些複雜。
使用演算法的基本流程就是定義處理規則和條件,這些處理規則和條件必須符合處理結果的要求。在手勢識別中,這種演算法的結果要求是一個二值型對象,某一手勢要麼符合預定的手勢要麼不符合。但是,最簡單直接的方法也有其缺點。演算法的簡單性限制了其能識別到的手勢的類別。對於揮手(wave)識別較好的演算法不能夠識別扔(throw)和擺(swing)動作。前者動作相對簡單和規整,後者則更加細微且多變。
演算法還有一個內在的擴充性問題。雖然一些代碼可以重用,但是每一種手勢必須使用定製的演算法來進行識別。隨著新的手勢識別演算法加入類庫,類庫的大小會迅速增加。這就對程式的效能產生影響,因為需要使用很多演算法來對某一個手勢進行識別以判斷該手勢的類型。
最後,每一個手勢識別演算法需要不同的參數,例如時間間隔和閾值。尤其是在依據流程識別特定的手勢的時候這一點顯得尤其明顯。開發人員需要不斷測試和實驗以為每一種演算法確定合適的參數值。這本身是一個有挑戰也很乏味的工作。然而每一種手勢的識別有著自己特殊的問題。
例如跳躍手勢,跳躍手勢就是使用者短暫的跳起來,腳離開地面。這個定義不能夠提供足夠的資訊來識別這一動作。咋一看,這個動作似乎足夠簡單,使得可以使用演算法來進行識別。首先,考慮到有很多種不同形式的跳躍:基本跳躍(basic jumping)、 跨欄(hurdling)、 跳遠(long jumping)、 跳躍(hopping),等等。但是這裡有一個大的問題就是,由於受到Kinect視場地區的限制,不可能總是能夠探測到地板的位置,這使得腳部何時離開地板很難確定。想象一下,使用者在膝蓋到下蹲點處彎下,然後跳起來。手勢識別引擎應該認為這是一個手勢還是多個手勢:下蹲或 下蹲跳起或者是跳起?如果使用者在蹲下的時間和跳躍的時間相比過長,那麼這一手勢可能應被識別為下蹲而不是跳躍。這一姿勢很難定義清楚,使得不能夠通過定義一些演算法來進行識別,同時這些演算法由於需要定義過多的規則和條件而變得難以管理和不穩定。使用對或錯的二值策略來識別使用者手勢的演算法太簡單和不夠健壯,不能夠很好的識別出類似跳躍,下蹲等動作。
神經網路的組織和判斷是基於統計和機率的,因此使得像識別手勢這些過程變得容易控制。基於什麼網路的手勢識別引擎對於下蹲然後跳躍動作,80%的機率判斷為跳躍,10%會判定為下蹲。
除了能夠識別複雜和精細的手勢,神經網路方法還能解決基於演算法手勢識別存在的擴充性問題。神經網路包含很多神經元,每一個神經元是一個好的演算法,能夠用來判斷手勢的細微部分的運動。在神經網路中,許多手勢可以共用神經元。但是每一中手勢識別有著獨特的神經元的組合。而且,神經元具有高效的資料結構來處理資訊。這使得在識別手勢時具有很高的效率。
和基於演算法的方法相比,神經網路依賴大量的參數來能得到精確的結果。參數的個數隨著神經元的個數增長。每一個神經元可以用來識別多個手勢,每一個神經遠的參數的變化都會影響其他節點的識別結果。配置和調整這些參數是一項藝術,需要經驗,並沒有特定的規則可循。然而,當神經網路配對機器學習過程中手動調整參數,隨著時間的推移,系統的識別精度會隨之提高。
基於樣本或者基於模版的手勢識別系統能夠將人的手勢和已知的手勢相匹配。使用者的手勢在模板庫中已經正常化了,使得能夠用來計算手勢的匹配精度。有兩種樣本識別方法,一種是儲存一系列的點,另一種方法是使用類似的Kinect SDK中的骨骼追蹤系統。在後面的那個方法中,系統中包含一系列骨骼資料和景深幀資料,能夠使用統計方法對產生的影像幀資料進行匹配以識別出已知的幀資料來。
這種手勢識別方法高度依賴於機器學習。識別引擎會記錄,處理,和重用當前幀資料,所以隨著時間的推移,手勢識別精度會逐步提高。系統能夠更好的識別出你想要表達的具體手勢。這種方法能夠比較容易的識別出新的手勢,而且較其他兩種方法能夠更好的處理比較複雜的手勢。但是建立這樣一個系統也不容易。首先,系統依賴於大量的樣本資料。資料越多,識別精度越高。所以系統需要大量的儲存資源和CPU時間的來進行尋找和匹配。其次系統需要不同高度,不同胖瘦,不同穿著(穿著會影響景深資料提取身體輪廓)的樣本來進行某一個手勢。
識別常見的手勢
選擇手勢識別的方法通常是依賴於項目的需要。如果項目只需要識別幾個簡單的手勢,那麼使用基於演算法或者基於神經網路的手勢識別就足夠了。對於其他類型的項目,如果有興趣的話可以投入時間來建立可複用的手勢識別引擎,或者使用一些人家已經寫好的識別演算法
不論選擇哪種手勢識別的方法,都必須考慮手勢的變化範圍。系統必須具有靈活性,並允許某一個手勢有某個範圍內的變動。技巧就是對於某一手勢,讓儘可能多的人來做,然後試表徵圖准化這一手勢。手勢識別一個比較好的方式就是關注手勢最核心的部分而不是哪些外在的細枝末節。
揮手是最簡單最基本的手勢。使用演算法方法能夠很容易識別這一手勢,但是之前講到的任何方法也能夠使用。雖然揮手是一個很簡單的手勢,但是如何使用代碼來識別這一手勢呢?讀者可以在鏡子前做向自己揮手,然後仔細觀察手的運動,尤其注意觀察手和胳膊之間的關係。繼續觀察手和胳膊之間的關係,然後觀察在做這個手勢事身體的整個姿勢。有些人保持身體和胳膊的不動,使用手腕左右移動來揮手。有些人保持身體和胳膊不動使用手腕前後移動來揮手。可以通過觀察這些姿勢來瞭解其他各種不同揮手的方式。
XBOX中的揮手動作定義為:從胳膊開始到肘部彎曲。使用者以胳膊肘為焦點來回移動前臂,移動平面和肩部在一個平面上,並且胳膊和地面保持平行,在手勢的中部(1),前臂垂直於後臂和地面。
可以觀察得出一些規律,第一個規律就是,手和手腕都是在肘部和肩部之上的,這也是大多是揮手動作的特徵。這也是我們識別揮手這一手勢的第一個標準。第一幅圖展示了揮手這一姿勢的中間位置,前臂和後臂垂直。如果使用者手臂改變了這種關係,前臂在垂直線左邊或者右邊,我們則認為這是該手勢的一個片段。對於揮手這一姿勢,每一個姿勢片段必須來回重複多次,否則就不是一個完整的手勢。這一運動規律就是我們的第二個準則:當某一手勢是揮手時,手或者手腕,必須在中間姿勢的左右來回重複特定的次數。使用這兩點通過觀察得到的規律,我們可以通過演算法建立演算法準則,來識別揮動手勢了。
演算法通過計算手離開中間姿勢地區的次數。中間地區是一個以胳膊肘為原點並給予一定閾值的地區。演算法也需要使用者在一定的時間段內完成這個手勢,否則識別就會失敗。這裡定義的揮動手勢識別演算法只是一個單獨的演算法,不包含在一個多層的手勢識別系統內。演算法維護自身的狀態,並在識別完成時以事件形式告知使用者識別結果。揮動識別監視多個使用者以及兩雙手的揮動手勢。識別演算法計算新產生的每一幀骨骼資料,因此必須記錄這些識別的狀態。
下面的代碼展示了記錄手勢識別狀態的兩個枚舉和一個結構。第一個名為WavePosition的枚舉用來定義手在揮手這一動作中的不同位置。手勢識別類使用WaveGestureState枚舉來追蹤每一個使用者的手的狀態。WaveGestureTracker結構用來儲存手勢識別中所需要的資料。他有一個Reset方法,當使用者的手達不到揮手這一手勢的基本動作條件時,比如當手在胳膊肘以下時,可調用Reset方法來重設手勢識別中所用到的資料。
private enum WavePosition{ None = 0, Left = 1, Right = 2, Neutral = 3}private enum WaveGestureState{ None = 0, Success = 1, Failure = 2, InProgress = 3}private struct WaveGestureTracker{ public int IterationCount; public WaveGestureState State; public long Timestamp; public WavePosition StartPosition; public WavePosition CurrentPosition; public void Reset() { IterationCount = 0; State = WaveGestureState.None; Timestamp = 0; StartPosition = WavePosition.None; CurrentPosition = WavePosition.None; }}
下面代碼顯示了手勢識別類的最基本結構:它定義了五個常量:中間地區閾值,手勢動作期間,手勢離開中間地區左右移動次數,以及左手和右手標識常量。這些常量應該作為設定檔的配置項儲存,在這裡為了簡便,所以以常量聲明。WaveGestureTracker數組儲存每一個可能的遊戲者的雙手的手勢的識別結果。當揮手這一手勢探測到了之後,觸發GestureDetected事件。
當主程式接收到一個新的資料幀時,就調用WaveGesture的Update方法。該方法迴圈遍曆每一個使用者的骨骼資料幀,然後調用TrackWave方法對左右手進行揮手姿勢識別。當骨骼資料不在追蹤狀態時,重設手勢識別狀態。
public class WaveGesture{ private const float WAVE_THRESHOLD = 0.1f; private const int WAVE_MOVEMENT_TIMEOUT = 5000; private const int LEFT_HAND = 0; private const int RIGHT_HAND = 1; private const int REQUIRED_ITERATIONS = 4; private WaveGestureTracker[,] _PlayerWaveTracker = new WaveGestureTracker[6, 2]; public event EventHandler GestureDetected; public void Update(Skeleton[] skeletons, long frameTimestamp) { if (skeletons != null) { Skeleton skeleton; for (int i = 0; i < skeletons.Length; i++) { skeleton = skeletons[i]; if (skeleton.TrackingState != SkeletonTrackingState.NotTracked) { TrackWave(skeleton, true, ref this._PlayerWaveTracker[i, LEFT_HAND], frameTimestamp); TrackWave(skeleton, false, ref this._PlayerWaveTracker[i, RIGHT_HAND], frameTimestamp); } else { this._PlayerWaveTracker[i, LEFT_HAND].Reset(); this._PlayerWaveTracker[i, RIGHT_HAND].Reset(); } } } }}
下面的代碼是揮手姿勢識別的主要邏輯方法TrackWave的主體部分。它驗證我們先前定義的構成揮手姿勢的條件,並更新手勢識別的狀態。方法識別左手或者右手的手勢,第一個條件是驗證,手和肘關節點是否處於追蹤狀態。如果這兩個關節點資訊不可用,則重設追蹤狀態,否則進行下一步的驗證。
如果姿勢期間超過閾值且還沒有進入到下一步驟,在姿勢追蹤逾時,重設追蹤資料。下一個驗證手部關節點是否在肘關節點之上。如果不是,則根據當前的追蹤狀態,揮手姿勢識別失敗或者重設識別條件。如果手部關節點在Y軸上且高於肘部關節點,方法繼續判斷手在Y軸上相對於肘關節的位置。調用UpdatePosition方法並傳入合適的手關節點所處的位置。更新手關節點位置之後,最後判斷定義的重複次數是否滿足,如果滿足這些條件,揮手這一手勢識別成功,觸發GetstureDetected事件。
private void TrackWave(Skeleton skeleton, bool isLeft, ref WaveGestureTracker tracker, long timestamp){ JointType handJointId = (isLeft) ? JointType.HandLeft : JointType.HandRight; JointType elbowJointId = (isLeft) ? JointType.ElbowLeft : JointType.ElbowRight; Joint hand = skeleton.Joints[handJointId]; Joint elbow = skeleton.Joints[elbowJointId]; if (hand.TrackingState != JointTrackingState.NotTracked && elbow.TrackingState != JointTrackingState.NotTracked) { if (tracker.State == WaveGestureState.InProgress && tracker.Timestamp + WAVE_MOVEMENT_TIMEOUT < timestamp) { tracker.UpdateState(WaveGestureState.Failure, timestamp); System.Diagnostics.Debug.WriteLine("Fail!"); } else if (hand.Position.Y > elbow.Position.Y) { //使用 (0, 0) 作為螢幕的中心. 從使用者的視角看, X軸左負右正. if (hand.Position.X <= elbow.Position.X - WAVE_THRESHOLD) { tracker.UpdatePosition(WavePosition.Left, timestamp); } else if (hand.Position.X >= elbow.Position.X + WAVE_THRESHOLD) { tracker.UpdatePosition(WavePosition.Right, timestamp); } else { tracker.UpdatePosition(WavePosition.Neutral, timestamp); } if (tracker.State != WaveGestureState.Success && tracker.IterationCount == REQUIRED_ITERATIONS) { tracker.UpdateState(WaveGestureState.Success, timestamp); System.Diagnostics.Debug.WriteLine("Success!"); if (GestureDetected != null) { GestureDetected(this, new EventArgs()); } } } else { if (tracker.State == WaveGestureState.InProgress) { tracker.UpdateState(WaveGestureState.Failure, timestamp); System.Diagnostics.Debug.WriteLine("Fail!"); } else { tracker.Reset(); } } } else { tracker.Reset(); }}下面的代碼添加到WaveGestureTracker結構中:這些協助方法維護結構中的欄位,使得TrackWave方法易讀。唯一需要注意的是UpdatePosition方法。TrackWave調用該方法判斷手的位置已經移動。他的最主要目的是更新CurrentPosition和Timestamp屬性,該方法也負責更新InterationCount欄位合InPorgress狀態。
public void UpdateState(WaveGestureState state, long timestamp){ State = state; Timestamp = timestamp;}public void Reset(){ IterationCount = 0; State = WaveGestureState.None; Timestamp = 0; StartPosition = WavePosition.None; CurrentPosition = WavePosition.None;}public void UpdatePosition(WavePosition position, long timestamp){ if (CurrentPosition != position) { if (position == WavePosition.Left || position == WavePosition.Right) { if (State != WaveGestureState.InProgress) { State = WaveGestureState.InProgress; IterationCount = 0; StartPosition = position; } IterationCount++; } CurrentPosition = position; Timestamp = timestamp; }}