像點擊(clicks)是GUI平臺的核心,輕點(taps)是觸摸平臺的核心那樣,手勢(gestures)是Kinect應用程序的核心算法
關於手勢的定義的中心在於手勢可以用來交流,手勢的意義在於講述而不是執行數組
在人機交互領域,手勢一般被做爲傳達一些簡單的指令而不是交流某些事實、描述問題或者陳述想法網絡
使用手勢操做電腦一般是命令式的,這一般不是人們使用手勢的目的。例如,揮手(wave)這一動做,在現實世界中一般是打招呼的一種方式,可是這種打招呼的方式在人機交互中卻不太經常使用。一般第一次寫程序一般會顯示「hello」,但咱們對和電腦打招呼並不感興趣。數據結構
可是,在一個繁忙的餐館,揮手這一手勢可能就有不一樣的含義了。當向服務員招收時,多是要引發服務員注意,須要他們提供服務。在計算機中,要引發計算機注意有時候也有其特殊意義,好比,計算機休眠時,通常都會敲擊鍵盤或者移動鼠標來喚醒,以提醒計算機「注意」。當使用Kinect時,可使用更加直觀的方式,就行少數派報告阿湯哥那樣,擡起雙手,或者簡單的朝計算機揮揮手,計算機就會從休眠狀態喚醒。機器學習
咱們想要使用的任何Kinect手勢必須基於應用程序的用戶 和應用程序的設計和開發者之間就某種手勢表明的含義達成一致。ide
天然用戶界面是一系列技術的合計,他包括:語音識別,多點觸控以及相似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; } }