Leap Motion作為一款手勢識別設備,相比於Kniect,長處在於准確度。
在我的畢業設計《場景漫游器》的開發中。Leap Motion的手勢控制作為重要的一個環節。以此,談談開發中使用Leap Motion進行手勢識別的實現方式以及須要注意的地方。
一、對Leap Motion的能力進行評估
在設定手勢之前。我們必須知道Leap Motion能做到哪種程度,以免在設定方案之后發現非常難實現。
這個評估依靠實際對設備的使用體驗。主要從三個方面:
1.Leap Motion提供的可視化的手勢識別界面
2.SDK文檔說明
3.Leap商店中的APP
基本能夠的得出:
1.Leap Motion的識別對於水平方向或者以水平方向為基礎手勢可以較好的識別。
2.對於握拳或者垂直的行為識別會出現誤差。這樣的誤差和詳細的手勢行為有關。
3.不應該過分依賴高准確度,Leap Motion能檢測到毫米級別是沒錯的,可是有時候會把你伸直的手指識別成彎曲的。所以要做好最壞的打算。
二、實際的須要
移動、旋轉、點擊button、縮放和旋轉物體、關閉程序、暫停,主要的功能需求是這樣。
有一些原則:
1.同樣環境下的手勢應該接近和方便的轉換。旋轉和移動的之間的轉換應該設計的非常自然。
2.手勢避免沖突,手勢過於相似不是什么好事。
比方三個伸直的手指和四個伸直的手指不應該被設計成兩個手勢。當然這不是絕對的。假設你進行一個緩慢的動作而且動作是面向Leap Motion的攝像頭,這時候應該相信它。至少要針對這個手勢做一個單獨的測試。
三、考慮主要的數據結構和算法的輪廓
Leap Motion的SDK在第一部分的時候已經瀏覽過。最起碼能知道Leap Motion能夠包括的信息。從SDK看來這是非常豐富的,既然設計自己的手勢,那么最好不要依賴於SKD開發包的炫酷的手勢。非常可能,這些手勢僅僅是官方用來演示或者炫耀的。自己設計手勢的基本數據結構也有另外的優點,比方更換了體感設備,可是功能是相似的。這時候僅僅須要更改獲取數據的方式就能夠了(從一個SDK更換到還有一個SDK),而不要改動算法。
算法的輪廓與基本數據有非常大的關系。所以數據結構一定要盡量的精簡而且同意改動(可能某個算法占領了決定性因素,可是開始沒考慮到)。
public class HandAndFingersPoint : MonoBehaviour { const int BUFFER_MAX=5; Controller m_LeapCtrl; <span style="white-space:pre"> </span>public E_HandInAboveView m_AboveView = E_HandInAboveView.None; //手指-數據 ,[0]表示左手,[1]表示右手 private Dictionary<Finger.FingerType,FingerData>[] m_FingerDatas = new Dictionary<Finger.FingerType, FingerData>[2]; //buffer,[0]表示左手,[1]表示右手,[,n](n屬於0,3。表示第n次緩存) private Dictionary<Finger.FingerType,FingerData>[,] m_FingerDatasBuffer=new Dictionary<Finger.FingerType, FingerData>[2,BUFFER_MAX]; private int m_CurBufIndex=0; //palm 0:左手 和1:右手 private PointData[] m_PalmDatas = new PointData[2]; private readonly PointData m_DefaultPointData = new PointData(Vector.Zero, Vector.Zero); private readonly FingerData m_DefaultFingerData = new FingerData(Vector.Zero,Vector.Zero,Vector.Zero);HandAndFingersPoint類中剩下的部分是對數據的填充、清除、刷新等方法。E_HandInAboveView記錄哪僅僅手先進入Leap Motion的視野。用於設定優先級。
另外兩個主要的數據結構PointData和FingerData:
//一個手指的數據包括一個指尖點數據和手指根骨的位置數據 public struct FingerData { public PointData m_Point;//指尖的位置和指向 public Vector m_Position;//手指根骨的位置,對於拇指來說是Proximal phalanges近端指骨的位置 public FingerData(PointData pointData, Vector pos) { m_Point = pointData; m_Position = pos; } public FingerData(Vector pointPos, Vector pointDir, Vector pos) { m_Point.m_Position = pointPos; m_Point.m_Direction = pointDir; m_Position = pos; } public void Set(FingerData fd) { m_Point = fd.m_Point; m_Position = fd.m_Position; } } //一個點的數據,包括方向和位置 public struct PointData { public Vector m_Position;//位置 public Vector m_Direction;//方向 public PointData(Vector pos,Vector dir) { m_Position = pos; m_Direction = dir; } public void Set(PointData pd) { m_Position = pd.m_Position; m_Direction = pd.m_Direction; } public void Set(Vector pos,Vector dir) { m_Position = pos; m_Direction = dir; } } //先被看到的手 public enum E_HandInAboveView { None, Left, Right }
基本數據定義好之后,最好確認數據的填充是沒問題的。實際通過Frame frame = Leap.Controller.Frame();來獲取最新的數據。
這時候並不急着寫完和基本數據相關的方法。如今終於要的是手勢算法的合理性。要推斷是否合理,最好先寫一個算法。
最簡單的是伸掌手勢,在控制中水平的伸掌用於漫游,垂直的伸掌用於暫停。我發現手掌依賴於手指,而手指包含兩個狀態——伸直和彎曲。
另外,其它的手勢,也都是手指的伸直或者彎曲,外加方向的判定累積出各種效果。理所當然的,應該單獨寫出手指的彎曲和伸直判定算法:
/// <summary> /// 該方法提供對於單個手指匹配的算法,如伸直。彎曲 /// 以后可能的改變:對於不同的場景可能要求有所不同。這里的閾值或許會隨之改變 /// </summary> public class FingerMatch { //彎曲狀態的角度閾值 static readonly float FingerBendState_Radian = Mathf.PI*4f / 18 ;//40度 //伸直狀態的角度閾值 static readonly float FingerStrightState_Radian = Mathf.PI/12;//15度 /// <summary> /// 手指伸直的狀態,當根骨-指尖的方向和指向的偏差小於閥值時,判定手指為伸直狀態。 /// 注意無效的方向為零向量。先判定是零向量 /// </summary> /// <param name="adjustBorder">對閾值做的微調</param> /// <returns></returns> public static bool StrightState(FingerData fingerData, float adjustBorder=0f) { bool isStright =false; Vector disalDir = fingerData.m_Point.m_Direction; //假設指尖方向為0向量,表示無效的數據 if (!disalDir.Equals(Vector.Zero)) { Vector fingerDir = fingerData.m_Point.m_Position - fingerData.m_Position;//指尖位置減去指根位置,由指根指向指尖的向量 float radian = fingerDir.AngleTo(disalDir); if (radian < FingerStrightState_Radian + adjustBorder) { isStright = true; } } return isStright; } /// <summary> /// 推斷一根手指是否處於彎曲狀態 /// </summary> /// <param name="fingerData">須要判定的手指數據</param> /// <param name="bandBorder">彎曲的閾值</param> /// <returns></returns> public static bool BendState(FingerData fingerData, float adjustBorder=0f)//,out float eulerAugle) { bool isBend = false; //eulerAugle = -1f; Vector disalDir = fingerData.m_Point.m_Direction; if( !disalDir.Equals(Vector.Zero) ) { Vector fingerDir = fingerData.m_Point.m_Position - fingerData.m_Position;//指尖位置減去指根位置,指跟到指尖的向量 float radian = fingerDir.AngleTo(disalDir); //eulerAugle = radian*180/Mathf.PI; //夾角超過定義的閾值時,認定為彎曲狀態 if (radian > FingerBendState_Radian + adjustBorder) { isBend = true; } } return isBend; } }
上面包括了一個重要的概念——閾值。它是描寫敘述究竟何種程度算是伸直,何種程度算是彎曲。閾值的確定是須要實際測試來決定的。
寫到這里也是時候進行一次簡單的測試了,畢竟算法的輪廓已經確定。我甚至沒寫出手掌伸直的判定算法。就確定是可行的。
基本數據結構相關的操作——HandAndFingersPoint類:源碼GitHub鏈接
該類使用基本數據。在Unity Editor中執行會展示了一個手掌的輪廓,藍色表示手指的方向。紅色表示手指骨根到掌心和指尖的連線,黃色表示掌心到指尖的連線:
四、手勢實現中簡要的概括
其它代碼都能夠在我的GitHub:Leap Motion In Unity3D倉庫中獲取。在手勢的實現中,也包括了一些小的技巧。比方對於動作的匹配要防止手指的顫抖引起的誤差。採用離散的數據取樣——每隔一定時間做一次取樣。
使用和觀察這些腳本的方式:能夠把這些腳本放在一個GameObject中。通過Leap Motion會看到腳本的屬性在匹配成功時會發生變化。另外,腳本中包括了事件的注冊功能,換句話說。外部能夠向隨意的手勢注冊一個事件,以便手勢完畢匹配或者到達某種匹配狀態時做一些額外的處理。這些腳本如今並不能直接完畢我們的需求,如暫停。我們須要在這些手勢狀態或者動作上做進一步的限定,如依據掌心的方向設定垂直向前的手掌為暫停,水平的手掌為平移之類的。