像點擊(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; } }