上文簡要介紹了手勢識別的基本概念和手勢識別的基本方法,並以八種手勢中的揮手(wave)為例講解了如何使用算法對手勢進行識別,本文接上文,繼續介紹如何建立一個手部追蹤類庫,並以此為基礎,對剩余7中常用的手勢進行識別做一些介紹。
1. 基本的手勢追蹤
手部追蹤在技術上和手勢識別不同,但是它和手勢識別中用到的一些基本方法是一樣的。在開發一個具體的手勢控件之前,我們先建立一個可重用的追蹤手部運動的類庫以方便我們後續開發。這個手部追蹤類庫包含一個以動態光標顯示的可視化反饋機制。手部追蹤和手勢控件之間的交互高度松耦合。
首先在Visual Studio中創建一個WPF控件類庫項目。然後添加四個類: KinectCursorEventArgs.cs,KinectInput.cs,CusrorAdorner.cs和KinectCursorManager.cs這四個類之間通過相互調用來基於用戶手所在的位置來完成光標位置的管理。KinectInput類包含了一些事件,這些事件可以在KinectCursorManager和一些控件之間共享。KinectCursorEventArgs提供了一個屬性集合,能夠用來在事件觸發者和監聽者之間傳遞數據。KinectCursorManager用來管理從Kinect傳感器中獲取的骨骼數據流,然後將其轉換到WPF坐標系統,提供關於轉換到屏幕位置的可視化反饋,並尋找屏幕上的控件,將事件傳遞到這些控件上。最後CursorAdorner.cs類包含了代表手的圖標的可視化元素。
KinectCursorEventArgs繼承自RoutedEventArgs類,它包含四個屬性:X、Y、Z和Cursor。X、Y、Z是一個小數,代表待轉換的用戶手所在位置的寬度,高度和深度值。Cursor用來存儲CursorAdorner類的實例,後面將會討論,下面的代碼展示了KinectCursorEventArgs類的基本結構,其中包含了一些重載的構造器。
public class KinectCursorEventArgs:RoutedEventArgs { public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public CursorAdorner Cursor { get; set; } public KinectCursorEventArgs(double x, double y) { X = x; Y = y; } public KinectCursorEventArgs(Point point) { X = point.X; Y = point.Y; } }
RoutedEventArgs基類有一個構造函數能夠接收RoutedEvent作為參數。這是一個有點特別的簽名,WPF中的UIElement使用這種特殊的語法觸發事件。下面的代碼是KinectCursorEventArgs類對這一簽名的實現,以及其他一些重載方法。
public KinectCursorEventArgs(RoutedEventroutedEvent) : base(routedEvent) { } publicKinectCursorEventArgs(RoutedEventroutedEvent, doublex, doubley, doublez) : base(routedEvent) { X = x; Y = y; Z = z; } publicKinectCursorEventArgs(RoutedEventroutedEvent, Pointpoint) : base(routedEvent) { X = point.X; Y = point.Y; } publicKinectCursorEventArgs(RoutedEventroutedEvent, Pointpoint,doublez) : base(routedEvent) { X = point.X; Y = point.Y; Z = z; } publicKinectCursorEventArgs(RoutedEventroutedEvent, objectsource) : base(routedEvent, source) {} publicKinectCursorEventArgs(RoutedEventroutedEvent,objectsource,doublex,doubley,doublez) : base(routedEvent, source) { X = x; Y = y; Z = z; } publicKinectCursorEventArgs(RoutedEventroutedEvent, objectsource, Pointpoint) : base(routedEvent, source) { X = point.X; Y = point.Y; } publicKinectCursorEventArgs(RoutedEventroutedEvent, objectsource, Pointpoint,doublez) : base(routedEvent, source) { X = point.X; Y = point.Y; Z = z; }
接下來,要在KinectInput類中創建事件來將消息從KinectCursorManager中傳遞到可視化控件中去。這些事件傳遞的數據類型為KinectCursorEventArgs類型。
在KinectInput類中添加一個KinectCursorEventHandler的代理類型:(1) 添加一個靜態的routed event聲明。(2) 添加KinectCursorEnter,KinectCursorLeave,KinectCursorMove,KinectCursorActive和KinectCursorDeactivated事件的add和remove方法。下面的代碼展示了三個和cursor相關的事件,其他的如KinectCursorActivated和KinectCursorDeactivated事件和這個結構相同:
public delegate void KinectCursorEventHandler(object sender,KinectCursorEventArgs e); public static class KinectInput { public static readonly RoutedEvent KinectCursorEnterEvent=EventManager.RegisterRoutedEvent("KinectCursorEnter",RoutingStrategy.Bubble, typeof(KinectCursorEventHandler),typeof(KinectInput)); public static void AddKinectCursorEnterHandler(DependencyObject o, KinectCursorEventHandler handler) { ((UIElement)o).AddHandler(KinectCursorEnterEvent, handler); } public static void RemoveKinectCursorEnterHandler(DependencyObject o, KinectCursorEventHandler handler) { ((UIElement)o).RemoveHandler(KinectCursorEnterEvent, handler); } public static readonly RoutedEvent KinectCursorLeaveEvent=EventManager.RegisterRoutedEvent("KinectCursorLeave",RoutingStrategy.Bubble, typeof(KinectCursorEventHandler),typeof(KinectInput)); public static void AddKinectCursorLeaveHandler(DependencyObject o, KinectCursorEventHandler handler) { ((UIElement)o).AddHandler(KinectCursorEnterEvent,handler); } public static void RemoveKinectCursorLeaveHandler(DependencyObject o, KinectCursorEventHandler handler) { ((UIElement)o).RemoveHandler(KinectCursorEnterEvent, handler); } }
注意到以上代碼中沒有聲明任何GUI編程中的Click事件。這是因為在設計控件類庫時,Kinect中並沒有點擊事件,相反Kinect中兩個重要的行為是enter和leave。手勢圖標可能會移入和移出某一個可視化控件的有效區域。如果要實現普通GUI控件的點擊效果的話,必須在Kinect中對這一事件進行模擬,因為Kinect原生並不支持點擊這一行為。
CursorAdorner類用來保存用戶手勢圖標可視化元素,它繼承自WPF的Adorner類型。之所以使用這個類型是因為它有一個特點就是總是在其他元素之上繪制,這在我們的項目中非常有用,因為我們不希望我們的光標會被其他元素遮擋住。代碼如下所示,我們默認的adorner對象將繪制一個默認的可視化元素來代表光標,當然也可以傳遞一個自定義的可視化元素。
public class CursorAdorner:Adorner { private readonly UIElement _adorningElement; private VisualCollection _visualChildren; private Canvas _cursorCanvas; protected FrameworkElement _cursor; StroyBoard _gradientStopAnimationStoryboard; readonly static Color _backColor = Colors.White; readonly static Color _foreColor = Colors.Gray; public CursorAdorner(FrameworkElement adorningElement) : base(adorningElement) { this._adorningElement = adorningElement; CreateCursorAdorner(); this.IsHitTestVisible = false; } public CursorAdorner(FrameworkElement adorningElement, FrameworkElement innerCursor) : base(adorningElement) { this._adorningElement = adorningElement; CreateCursorAdorner(innerCursor); this.IsHitTestVisible = false; } public FrameworkElement CursorVisual { get { return _cursor; } } public void CreateCursorAdorner() { var innerCursor = CreateCursor(); CreateCursorAdorner(innerCursor); } protected FrameworkElement CreateCursor() { var brush = new LinearGradientBrush(); brush.EndPoint = new Point(0, 1); brush.StartPoint = new Point(0, 0); brush.GradientStops.Add(new GradientStop(_backColor, 1)); brush.GradientStops.Add(new GradientStop(_foreColor, 1)); var cursor = new Ellipse() { Width=50, Height=50, Fill=brush }; return cursor; } public void CreateCursorAdorner(FrameworkElement innerCursor) { _visualChildren = new VisualCollection(this); _cursorCanvas = new Canvas(); _cursor = innerCursor; _cursorCanvas.Children.Add(this._cursorCanvas); _visualChildren.Add(this._cursorCanvas); AdornerLayer layer = AdornerLayer.GetAdornerLayer(_adorningElement); layer.Add(this); } }
因為繼承自Adorner基類,我們需要重寫某些基類的方法,下面的代碼展示了基類中的方法如何和CreateCursorAdorner方法中實例化的_visualChildren和_cursorCanvas字段進行綁定。
protected override int VisualChildrenCount { get { return _visualChildren.Count; } } protected override Visual GetVisualChild(int index) { return _visualChildren[index]; } protected override Size MeasureOverride(Size constraint) { this._cursorCanvas.Measure(constraint); return this._cursorCanvas.DesiredSize; } protected override Size ArrangeOverride(Size finalSize) { this._cursorCanvas.Arrange(new Rect(finalSize)); return finalSize; }
CursorAdorner對象也負責找到手所在的正確的位置,該對象的UpdateCursor方法如下,方法接受X,Y坐標位置作為參數。然後方法在X,Y上加一個偏移量以使得圖像的中心在X,Y之上,而不是在圖像的邊上。另外,我們提供了該方法的一個重載,該重載告訴光標對象一個特殊的坐標會傳進去,所有的普通方法調用UpdateCursor將會被忽略。當我們在磁性按鈕中想忽略基本的手部追蹤給用戶更好的手勢體驗時很有用。
public void UpdateCursor(Pointposition, boolisOverride) { _isOverriden = isOverride; _cursor.SetValue(Canvas.LeftProperty,position.X-(_cursor.ActualWidth/2)); _cursor.SetValue(Canvas.LeftProperty, position.Y - (_cursor.ActualHeight / 2)); } public void UpdateCursor(Pointposition) { if(_isOverriden) return; _cursor.SetValue(Canvas.LeftProperty, position.X - (_cursor.ActualWidth / 2)); _cursor.SetValue(Canvas.LeftProperty, position.Y - (_cursor.ActualHeight / 2)); }
最後,添加光標對象動畫效果。當Kinect控件需要懸浮於一個元素之上,在用戶等待的時候,給用戶反饋一些信息告知正在發生的事情,這一點很有好處。下面了的代碼展示了如何使用代碼實現動畫效果:
public virtual void AnimateCursor(doublemilliSeconds) { CreateGradientStopAnimation(milliSeconds); if(_gradientStopAnimationStoryboard != null) _gradientStopAnimationStoryboard.Begin(this, true); } public virtual void StopCursorAnimation(doublemilliSeconds) { if(_gradientStopAnimationStoryboard != null) _gradientStopAnimationStoryboard.Stop(this); } public virtual void CreateGradientStopAnimation(doublemilliSeconds) { NameScope.SetNameScope(this, newNameScope()); varcursor = _cursor asShape; if(cursor == null) return; varbrush = cursor.Fill asLinearGradientBrush; varstop1 = brush.GradientStops[0]; varstop2 = brush.GradientStops[1]; this.RegisterName("GradientStop1", stop1); this.RegisterName("GradientStop2", stop2); DoubleAnimationoffsetAnimation = newDoubleAnimation(); offsetAnimation.From = 1.0; offsetAnimation.To = 0.0; offsetAnimation.Duration = TimeSpan.FromMilliseconds(milliSeconds); Storyboard.SetTargetName(offsetAnimation, "GradientStop1"); Storyboard.SetTargetProperty(offsetAnimation, newPropertyPath(GradientStop.OffsetProperty)); DoubleAnimationoffsetAnimation2 = newDoubleAnimation(); offsetAnimation2.From = 1.0; offsetAnimation2.To = 0.0; offsetAnimation2.Duration = TimeSpan.FromMilliseconds(milliSeconds); Storyboard.SetTargetName(offsetAnimation2, "GradientStop2"); Storyboard.SetTargetProperty(offsetAnimation2, newPropertyPath(GradientStop.OffsetProperty)); _gradientStopAnimationStoryboard = newStoryboard(); _gradientStopAnimationStoryboard.Children.Add(offsetAnimation); _gradientStopAnimationStoryboard.Children.Add(offsetAnimation2); _gradientStopAnimationStoryboard.Completed += delegate{ _gradientStopAnimationStoryboard.Stop(this); }; }
為了實現KinectCursorManager類,我們需要幾個幫助方法,代碼如下,GetElementAtScreenPoint方法告訴我們哪個WPF對象位於X,Y坐標下面,在這個高度松散的結構中,GetElementAtScreenPoint方法是主要的引擎,用來從KinectCurosrManager傳遞消息到自定義控件,並接受這些事件。另外,我們使用兩個方法來確定我們想要追蹤的骨骼數據以及我們想要追蹤的手。
private static UIElement GetElementAtScreenPoint(Point point, Window window) { if (!window.IsVisible) return null; Point windowPoint = window.PointFromScreen(point); IInputElement element = window.InputHitTest(windowPoint); if (element is UIElement) return (UIElement)element; else return null; } private static Skeleton GetPrimarySkeleton(IEnumerable<Skeleton> skeletons) { Skeleton primarySkeleton = null; foreach (Skeleton skeleton in skeletons) { if (skeleton.TrackingState != SkeletonTrackingState.Tracked) { continue; } if (primarySkeleton == null) primarySkeleton = skeleton; else if (primarySkeleton.Position.Z > skeleton.Position.Z) primarySkeleton = skeleton; } return primarySkeleton; } private static Joint? GetPrimaryHand(Skeleton skeleton) { Joint leftHand=skeleton.Joints[JointType.HandLeft]; Joint rightHand=skeleton.Joints[JointType.HandRight]; if (rightHand.TrackingState == JointTrackingState.Tracked) { if (leftHand.TrackingState != JointTrackingState.Tracked) return rightHand; else if (leftHand.Position.Z > rightHand.Position.Z) return rightHand; else return leftHand; } if (leftHand.TrackingState == JointTrackingState.Tracked) { return leftHand; } else return null; }
KinectCursorManager應該是一個單例類。這樣設計是能夠使得代碼實例化起來簡單。任何和KinectCursorManager工作的控件在KinectCursorManager沒有實例化的情況下可以獨立的進行KinectCursorManager的實例化。這意味著任何開發者使用這些控件不需要了解KinectCursorManager對象本身。相反,開發者能夠簡單的將控件拖動到應用程序中,控件負責實例化KinectCursorManager對象。為了使得這種自服務功能能和KinectCursorMange類一起使用,我們需要創建一個重載的Create方法來將應用程序的主窗體類傳進來。下面的代碼展示了重載的構造函數以及特殊的單例模式的實現方法。
public class KinectCursorManager { private KinectSensor kinectSensor; private CursorAdorner cursorAdorner; private readonly Window window; private UIElement lastElementOver; private bool isSkeletonTrackingActivated; private static bool isInitialized; private static KinectCursorManager instance; public static void Create(Window window) { if (!isInitialized) { instance = new KinectCursorManager(window); isInitialized = true; } } public static void Create(Window window,FrameworkElement cursor) { if (!isInitialized) { instance = new KinectCursorManager(window,cursor); isInitialized = true; } } public static void Create(Window window, KinectSensor sensor) { if (!isInitialized) { instance = new KinectCursorManager(window, sensor); isInitialized = true; } } public static void Create(Window window, KinectSensor sensor, FrameworkElement cursor) { if (!isInitialized) { instance = new KinectCursorManager(window, sensor, cursor); isInitialized = true; } } public static KinectCursorManager Instance { get { return instance; } } private KinectCursorManager(Window window) : this(window, KinectSensor.KinectSensors[0]) { } private KinectCursorManager(Window window, FrameworkElement cursor) : this(window, KinectSensor.KinectSensors[0], cursor) { } private KinectCursorManager(Window window, KinectSensor sensor) : this(window, sensor, null) { } private KinectCursorManager(Window window, KinectSensor sensor, FrameworkElement cursor) { this.window = window; if (KinectSensor.KinectSensors.Count > 0) { window.Unloaded += delegate { if (this.kinectSensor.SkeletonStream.IsEnabled) this.kinectSensor.SkeletonStream.Disable(); }; window.Loaded += delegate { if (cursor == null) cursorAdorner = new CursorAdorner((FrameworkElement)window.Content); else cursorAdorner = new CursorAdorner((FrameworkElement)window.Content, cursor); this.kinectSensor = sensor; this.kinectSensor.SkeletonFrameReady += SkeletonFrameReady; this.kinectSensor.SkeletonStream.Enable(new TransformSmoothParameters()); this.kinectSensor.Start(); }; } } ……
下面的代碼展示了KinectCursorManager如何和窗體上的可視化元素進行交互。當用戶的手位於應用程序可視化元素之上時,KinectCursorManager對象始終保持對當前手所在的可視化元素以及之前手所在的可視化元素的追蹤。當這一點發生改變時,KinectCursorManager會觸發之前控件的leave事件和當前控件的enter事件。我們也保持對KinectSensor對象的追蹤,並觸發activated和deactivated事件。
private void SetSkeletonTrackingActivated() { if (lastElementOver != null && isSkeletonTrackingActivated == false) { lastElementOver.RaiseEvent(new RoutedEventArgs(KinectInput.KinectCursorActivatedEvent)); } isSkeletonTrackingActivated = true; } private void SetSkeletonTrackingDeactivated() { if (lastElementOver != null && isSkeletonTrackingActivated == false) { lastElementOver.RaiseEvent(new RoutedEventArgs(KinectInput.KinectCursorDeactivatedEvent)); } isSkeletonTrackingActivated = false ; } private void HandleCursorEvents(Point point, double z) { UIElement element = GetElementAtScreenPoint(point, window); if (element != null) { element.RaiseEvent(new KinectCursorEventArgs(KinectInput.KinectCursorMoveEvent, point, z) {Cursor=cursorAdorner }); if (element != lastElementOver) { if (lastElementOver != null) { lastElementOver.RaiseEvent(new KinectCursorEventArgs(KinectInput.KinectCursorLeaveEvent, point, z) { Cursor = cursorAdorner }); } element.RaiseEvent(new KinectCursorEventArgs(KinectInput.KinectCursorEnterEvent, point, z) { Cursor = cursorAdorner }); } } lastElementOver = element; }
最後需要兩個核心的方法來管理KinectCursorManger類。SkeletonFrameReady方法與之前一樣,用來從Kinect獲取骨骼數據幀時觸發的事件。在這個項目中,SkeletonFrameReady方法負責獲取合適的骨骼數據,然後獲取合適的手部關節點數據。然後將手部關節點數據傳到UpdateCusror方法中,UpdateCursor方法執行一系列方法將Kinect骨骼空間坐標系轉化到WPF的坐標系統中,Kinect SDK中MapSkeletonPointToDepth方法提供了這一功能。SkeletonToDepthImage方法返回的X,Y值,然後轉換到應用程序中實際的寬和高。和X,Y不一樣,Z值進行了不同的縮放操作。簡單的從Kinect深度攝像機中獲取的毫米數據。代碼如下,一旦這些坐標系定義好了之後,將他們傳遞到HandleCursorEvents方法然後CursorAdorner對象將會給用戶以反饋。相關代碼如下:
private void SkeletonFrameReady(objectsender, SkeletonFrameReadyEventArgse) { using(SkeletonFrameframe = e.OpenSkeletonFrame()) { if(frame == null|| frame.SkeletonArrayLength == 0) return; Skeleton[] skeletons = newSkeleton[frame.SkeletonArrayLength]; frame.CopySkeletonDataTo(skeletons); Skeletonskeleton = GetPrimarySkeleton(skeletons); if(skeleton == null) { SetHandTrackingDeactivated(); } else { Joint? primaryHand = GetPrimaryHand(skeleton); if(primaryHand.HasValue) { UpdateCursor(primaryHand.Value); } else { SetHandTrackingDeactivated(); } } } } private voidSetHandTrackingDeactivated() { cursorAdorner.SetVisibility(false); if(lastElementOver != null&& isHandTrackingActivated == true) {lastElementOver.RaiseEvent(newRoutedEventArgs(KinectInput.KinectCursorDeactivatedEvent)); }; isHandTrackingActivated = false; } private voidUpdateCursor(Jointhand) { varpoint = kinectSensor.MapSkeletonPointToDepth(hand.Position, kinectSensor.DepthStream.Format); floatx = point.X; floaty = point.Y; floatz = point.Depth; x = (float)(x * window.ActualWidth / kinectSensor.DepthStream.FrameWidth); y = (float)(y * window.ActualHeight / kinectSensor.DepthStream.FrameHeight); PointcursorPoint = newPoint(x, y); HandleCursorEvents(cursorPoint, z); cursorAdorner.UpdateCursor(cursorPoint); }
至此,我們已經簡單實現了一些基礎結構,這些僅僅是實現了將用戶手部的運動顯示在屏幕上。現在我們要創建一個基類來監聽光標對象的事件,首先創建一個KinectButton對象,該對象繼承自WPF Button類型。定義三個之前在KinectInput中定義好的事件,同時創建這些事件的添加刪除方法,代碼如下:
public class KinectButton:Button { public static readonlyRoutedEventKinectCursorEnterEvent = KinectInput.KinectCursorEnterEvent.AddOwner(typeof(KinectButton)); public static readonlyRoutedEventKinectCursorLeaveEvent = KinectInput.KinectCursorLeaveEvent.AddOwner(typeof(KinectButton)); public static readonlyRoutedEventKinectCursorMoveEvent = KinectInput.KinectCursorMoveEvent.AddOwner(typeof(KinectButton)); public static readonlyRoutedEventKinectCursorActivatedEvent = KinectInput.KinectCursorActivatedEvent.AddOwner(typeof(KinectButton)); public static readonlyRoutedEventKinectCursorDeactivatedEvent = KinectInput.KinectCursorDeactivatedEvent.AddOwner(typeof(KinectButton)); public eventKinectCursorEventHandlerKinectCursorEnter { add{ base.AddHandler(KinectCursorEnterEvent, value); } remove{ base.RemoveHandler(KinectCursorEnterEvent, value); } } public eventKinectCursorEventHandlerKinectCursorLeave { add{ base.AddHandler(KinectCursorLeaveEvent, value); } remove{ base.RemoveHandler(KinectCursorLeaveEvent, value); } } public eventKinectCursorEventHandlerKinectCursorMove { add{ base.AddHandler(KinectCursorMoveEvent, value); } remove{ base.RemoveHandler(KinectCursorMoveEvent, value); } } public eventRoutedEventHandlerKinectCursorActivated { add{ base.AddHandler(KinectCursorActivatedEvent, value); } remove{ base.RemoveHandler(KinectCursorActivatedEvent, value); } } public eventRoutedEventHandlerKinectCursorDeactivated { add{ base.AddHandler(KinectCursorDeactivatedEvent, value); } remove{ base.RemoveHandler(KinectCursorDeactivatedEvent, value); } } }
在KinectButton的構造函數中,首先檢查當前控件是否運行在IDE或者一個實際的應用程序中。如果沒有在設計器中,如果KinectCursorManager對象不存在,我們實例化KinectCursorManager對象。通過這種方式,我們可以在同一個窗體上添加多個Kinect 按鈕。這些按鈕自動創建KinectCursorManager的實例而不用開發者去創建。下面的代碼展示了如何實現這一功能。KinectCursorManager類中的HandleCursorEvents方法負責處理這些事件。
public KinectButton() { if(!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) KinectCursorManager.Create(Application.Current.MainWindow); this.KinectCursorEnter+=newKinectCursorEventHandler(OnKinectCursorEnter); this.KinectCursorLeave+=newKinectCursorEventHandler(OnKinectCursorLeave); this.KinectCursorMove+=newKinectCursorEventHandler(OnKinectCursorMove); } protected virtual voidOnKinectCursorLeave(Objectsender, KinectCursorEventArgse) { } protected virtual voidOnKinectCursorMove(Objectsender, KinectCursorEventArgse) { }
下面的代碼中,KinectCursorEnter事件中觸發ClickEvent,將其改造成了一個標准的點擊事件。使得KinectButton能夠在鼠標移入時觸發Click事件。Kinect中應用程序的交互術語還是使用之前GUI交互界面中的術語,這使得讀者能夠更容易理解。更重要的是,也能夠使得開發者更容易理解,因為我們之前有很多使用按鈕來構造用戶界面的經驗。當然終極的目標是捨棄這些各種各樣的控件,改而使用純粹的手勢交互界面,但是按鈕在現階段的交互界面中還是很重要的。另外,這樣也能夠使用按鈕來布局圖形用戶界面,只需要將普通的按鈕換成Kinect按鈕就可以了。
protected virtual void OnKinectCursorEnter(object sender, KinectCursorEventArgs e) { RaiseEvent(new RoutedEventArgs(ClickEvent)); }
這種控件有一個最大的問題,在大多數基於Kinect的應用程序中你看不到這個問題,那就是,你不能區分開是有意的還是無意的點擊。在傳統的基於鼠標的GUI應用中也有類似的傾向,每一次將鼠標移動到按鈕上不用點擊就會激活按鈕。這種用戶界面很容易不能使用,這也提醒了一個潛在的值得注意的問題,那就是將按鈕從圖形用戶界面中移植到其他界面中可能存在的問題。懸浮按鈕是微軟試圖解決這一特殊問題的一個嘗試。
2. 剩余七種常見手勢的識別
前面的文章中已經講述了揮手手勢的識別,本文接下來講解余下7中常見手勢的識別。
2.1懸浮按鈕(Hover Button)
懸浮按鈕是微軟在2010年為Kinect對Xbox的操縱盤進行改進而引入的。 懸浮按鈕通過將鼠標點擊換成懸浮然後等待(hover-and-wait)動作,解決了不小心點擊的問題。當光標位於按鈕之上時,意味著用戶通過將光標懸浮在按鈕上一段時間來表示想選中按鈕。另一個重要特點是懸浮按鈕在用戶懸浮並等待時,多少提供了視覺反饋。
在Kinect中實現懸浮按鈕和在Windows Phone開發中實現輕點然後維持(tap-and-hold)這一手勢在技術上比較類似。必須使用一個計時器來記錄當前用戶光標停留在按鈕上的時間。一旦用戶的手的光標和按鈕的邊界交叉就開始計時。如果某一個時間阈值內用戶光標還沒有移除,那麼就觸發點擊事件。
創建一個名為HoverButton的類,他繼承自之前創建的KinectButton類,在類中添加一個名為hoverTimer的DispatcherTime實例,代碼如下。另外創建一個布爾型的timerEnable字段,將其設置為true。雖然目前不會用到這個字段,但是在後面部分將會用到,當我們想使用HoverButton的某些功能,但是不需要DispatcherTimer時就會非常有用。最後創建一個HoverInterval的依賴屬性,使得運行我們將懸浮時間用代碼或者xaml進行定義。默認設置為2秒,這是在大多是Xbox游戲中的時間。
public class HoverButton:KinectButton { readonlyDispatcherTimerhoverTimer = newDispatcherTimer(); protected booltimerEnabled = true; public doubleHoverInterval { get{ return(double)GetValue(HoverIntervalProperty); } set { SetValue(HoverIntervalProperty, value); } } public static readonlyDependencyPropertyHoverIntervalProperty = DependencyProperty.Register("HoverInterval", typeof(double), typeof(HoverButton), newUIPropertyMetadata(2000d)); …… }
要實現懸浮按鈕的核心功能,我們必須覆寫基類中的OnKinectCursorLeave和OnKinectCursorEnter方法,所有和KinectCursorManger進行交互的部分在KinectButton中已經實現了,因此我們在這裡不用操心。在類的構造方法中,只需要實例化DispathcerTimer對象,HoverInterval依賴屬性和注冊hoverTimer_Tick方法到計時器的Tick事件上即可。計時器在一定的間隔時間會觸發Tick事件,該事件簡單的處理一個Click事件,在OnKinectCursorEnter方法中啟動計數器,在OnKinectCursorLeave事件中停止計數器。另外,重要的是,在enter和leave方法中啟動和停止鼠標光標動畫效果。
public HoverButton() { hoverTimer.Interval = TimeSpan.FromMilliseconds(HoverInterval); hoverTimer.Tick += newEventHandler(hoverTimer_Tick); hoverTimer.Stop(); } voidhoverTimer_Tick(objectsender, EventArgse) { hoverTimer.Stop(); RaiseEvent(newRoutedEventArgs(ClickEvent)); } protected override voidOnKinectCursorLeave(objectsender, KinectCursorEventArgse) { if(timerEnabled) { e.Cursor.StopCursorAnimation(); hoverTimer.Stop(); } } protected override voidOnKinectCursorEnter(objectsender, KinectCursorEventArgse) { if(timerEnabled) { hoverTimer.Interval = TimeSpan.FromMilliseconds(HoverInterval); e.Cursor.AnimateCursor(HoverInterval); hoverTimer.Start(); } }
懸浮按鈕在基於Kinect的Xbox游戲中幾乎無處不在。懸浮按鈕唯一存在的問題是,光標手勢懸停在按鈕上時會抖動,這可能是Kinect中骨骼識別本身的問題。當在運動狀態時,Kinect能夠很好的對這些抖動進行平滑,因為即使在快速移動狀態下,Kinect中的軟件使用了一系列預測和平滑技術來對抖動進行處理。姿勢,和上面的懸停一樣,因為是靜止的,所以可能存在抖動的問題。另外,用戶一般不會保持手勢靜止,即使他們想哪樣做。Kinect將這些小的運動返回給用戶。當用戶什麼都沒做時,抖動的手可能會破壞手勢的動畫效果。對懸浮按鈕的一個改進就是磁性按鈕(Magnet Button),隨著體感游戲的升級,這種按鈕逐漸取代了之前的懸浮按鈕,後面我們將看到如何實現磁性按鈕。
2.2 下壓按鈕(Push Button)
就像懸浮按鈕在Xbox中那樣普遍一樣,一些Kinect開發者也想創建一些類似PC上的那種交互方式的按鈕,這種按鈕稱之為下壓按鈕(push button)。下壓按鈕試圖將傳統的GUI界面上的按鈕移植到Kinect上去。為了代替鼠標點擊,下壓按鈕使用一種將手向前推的手勢來表示按下這一動作。
這種手勢,手掌張開向前,在形式上有點像動態鼠標。下壓按鈕的核心算法就是探測手勢在Z軸上有一個向負方向的運動。另外,相符方向必須有一個距離阈值,使得超過這一阈值就認為用戶想要執行下壓指令。代碼如下所示:下壓按鈕有一個稱之為Threshold的依賴屬性,單位為毫米,這個值可以由開發者來根據動作的靈敏度來進行設置。當用戶的手移動到下壓按鈕的上方時,我們記錄一下當前位置手的Z值,以此為基准,然後比較手的深度值和阈值,如果超過阈值,就觸發點擊事件。
public class PushButton:KinectButton { protected double handDepth; public double PushThreshold { get { return (double)GetValue(PushThresholdProperty); } set { SetValue(PushThresholdProperty, value); } } public static readonly DependencyProperty PushThresholdProperty = DependencyProperty.Register("PushThreshold", typeof(double), typeof(PushButton), new UIPropertyMetadata(100d)); protected override void OnKinectCursorMove(object sender, KinectCursorEventArgs e) { if (e.Z < handDepth - PushThreshold) { RaiseEvent(new RoutedEventArgs(ClickEvent)); } } protected override void OnKinectCursorEnter(object sender, KinectCursorEventArgs e) { handDepth = e.Z; } }
2.3 磁性按鈕(Magnet Button)
如前面所討論的,磁性按鈕是對懸浮按鈕的一種改進。他對用戶懸浮在按鈕上的這一體驗進行了一些改進。他試圖追蹤用戶手的位置,然後自動將光標對齊到磁性按鈕的中間。當用戶的手離開磁性按鈕的區域是,手勢追蹤又恢復正常。在其他方面磁性按鈕和懸浮按鈕的行為一樣。考慮到磁性按鈕和懸浮按鈕在功能方面差異很小,而我們將他單獨作為一個完全不同的控件來對待可能有點奇怪。但是,在用戶體驗設計領域(UX),這一點差異就是一個完全不同的概念。從編碼角度看,這一點功能性的差異也使得代碼更加復雜。
首先,創建一個繼承自HoverButton的名為MagnetButton的類。磁性按鈕需要一些額外的事件和屬性來管理手進入到磁性按鈕區域和手自動對齊到磁性按鈕中間區域的時間。我們需要在KinectInput類中添加新的lock和unlock事件,代碼如下:
public static readonly RoutedEvent KinectCursorLockEvent = EventManager.RegisterRoutedEvent("KinectCursorLock", RoutingStrategy.Bubble, typeof(KinectCursorEventHandler), typeof(KinectInput)); public static void AddKinectCursorLockHandler(DependencyObject o, KinectCursorEventHandler handler) { ((UIElement)o).AddHandler(KinectCursorLockEvent, handler); } public static readonly RoutedEvent KinectCursorUnlockEvent = EventManager.RegisterRoutedEvent("KinectCursorUnlock", RoutingStrategy.Bubble, typeof(KinectCursorEventHandler), typeof(KinectInput)); public static void RemoveKinectCursorUnlockHandler(DependencyObject o, KinectCursorEventHandler handler) { ((UIElement)o).RemoveHandler(KinectCursorUnlockEvent, handler); } public class MagnetButton : HoverButton { protected bool isLockOn = true; public static readonly RoutedEvent KinectCursorLockEvent = KinectInput.KinectCursorUnlockEvent.AddOwner(typeof(MagnetButton)); public static readonly RoutedEvent KinectCursorUnlockEvent = KinectInput.KinectCursorLockEvent.AddOwner(typeof(MagnetButton)); private Storyboard move; public event KinectCursorEventHandler KinectCursorLock { add { base.AddHandler(KinectCursorLockEvent, value); } remove { base.RemoveHandler(KinectCursorLockEvent, value); } } public event KinectCursorEventHandler KinectCursorUnLock { add { base.AddHandler(KinectCursorUnlockEvent, value); } remove { base.RemoveHandler(KinectCursorUnlockEvent, value); } } public double LockInterval { get { return (double)GetValue(LockIntervalProperty); } set { SetValue(LockIntervalProperty, value); } } public static readonly DependencyProperty LockIntervalProperty = DependencyProperty.Register("LockInterval", typeof(double), typeof(MagnetButton), new UIPropertyMetadata(200d)); public double UnlockInterval { get { return (double)GetValue(UnlockIntervalProperty); } set { SetValue(UnlockIntervalProperty, value); } } public static readonly DependencyProperty UnlockIntervalProperty = DependencyProperty.Register("UnlockInterval", typeof(double), typeof(MagnetButton), new UIPropertyMetadata(80d)); ……}
磁性按鈕的代碼中,核心地方在於光標從當前位置移動到磁性按鈕的中心位置。看起來很簡單,實際上實現起來有點麻煩。需要重寫基類中的OnKinectCursorEnter和OnKinectCursorLeave方法。確定磁性按鈕的鎖定位置第一步需要找到磁性按鈕本身所處的位置。代碼如下,我們使用WPF中最常見名為FindAncestor幫助方法來遍歷可視化對象樹來進行查找,需要找到承載該磁性按鈕的Windows對象,匹配磁性按鈕的當前實例到Windows上,然後將其賦給名為Point的變量。但是point對象只保存了當前磁性按鈕的左上角的位置。所以,我們需要給在這個點上加一個磁性按鈕一半長寬的偏移值,才能獲取到磁性按鈕的中心位置x,y。
private T FindAncestor<T>(DependencyObjectdependencyObject) whereT:class { DependencyObjecttarget=dependencyObject; do { target=VisualTreeHelper.GetParent(target); } while(target!=null&&!(target isT)); returntarget asT; } protected override void OnKinectCursorEnter(objectsender, KinectCursorEventArgse) { //獲取按鈕位置 varrootVisual=FindAncestor<Window>(this); varpoint=this.TransformToAncestor(rootVisual).Transform(newPoint(0,0)); varx=point.X+this.ActualWidth/2; vary=point.Y+this.ActualHeight/2; varcursor=e.Cursor; cursor.UpdateCursor(newPoint(e.X,e.Y),true); //找到目的位置 PointlockPoint=newPoint(x-cursor.CursorVisual.ActualWidth/2,y-cursor.CursorVisual.ActualHeight/2); //當前位置 PointcursorPoint=newPoint(e.X-cursor.CursorVisual.ActualWidth/2,e.Y-cursor.CursorVisual.ActualHeight/2); //將光標從當前位置傳送到目的位置 AnimateCursorToLockPosition(e,x,y,cursor,reflockPoint,refcursorPoint); base.OnKinectCursorEnter(sender,e); } protected override void OnKinectCursorLeave(objectsender, KinectCursorEventArgse) { base.OnKinectCursorLeave(sender, e); e.Cursor.UpdateCursor(newPoint(e.X,e.Y),false); varrootVisual=FindAncestor<Window>(this); varpoint=this.TransformToAncestor(rootVisual).Transform(newPoint(0,0)); varx=point.X+this.ActualWidth/2; vary=point.Y+this.ActualHeight/2; varcursor=e.Cursor; //找到目的位置 PointlockPoint=newPoint(x-cursor.CursorVisual.ActualWidth/2,y-cursor.CursorVisual.ActualHeight/2); //當前位置 PointcursorPoint=newPoint(e.X-cursor.CursorVisual.ActualWidth/2,e.Y-cursor.CursorVisual.ActualHeight/2); AnimateCursorAwayFromLockPosition(e,cursor,reflockPoint,refcursorPoint); }
接下來,我們用手所在的X,Y位置替換手勢圖標的位置。然而,我們也傳入了第二個參數,告訴手勢圖標自動停止追蹤手的位置一段時間。當用戶看到光標不聽手的使喚自動對齊到磁性按鈕的中心,這可能有點不太友好。
雖然我們現在有了磁性按鈕的中心位置,但是我們仍不能很好的將手勢光標定位到中心。我們必須額外的給手勢光標本身給一個一半長寬的偏移值,以使得手在光標的中心位置而不是在左上角。在完成這些操作之後,我們將最終的值賦給lockPoint變量。我們也執行了同樣的操作來查找光標目前的左上角位置以及偏移量,並將其賦值給cursorPoint變量。有了這兩個值,我們就可以從當前的位置使用動畫移動到目標位置了。動畫方法代碼如下:
private void AnimateCursorAwayFromLockPosition(KinectCursorEventArgse,CursorAdornercursor,refPointlockPoint,refPointcursorPoint) { DoubleAnimationmoveLeft = newDoubleAnimation(lockPoint.X, cursorPoint.X, newDuration(TimeSpan.FromMilliseconds(UnlockInterval))); Storyboard.SetTarget(moveLeft, cursor.CursorVisual); Storyboard.SetTargetProperty(moveLeft, newPropertyPath(Canvas.LeftProperty)); DoubleAnimationmoveTop = newDoubleAnimation(lockPoint.Y, cursorPoint.Y, newDuration(TimeSpan.FromMilliseconds(UnlockInterval))); Storyboard.SetTarget(moveTop, cursor.CursorVisual); Storyboard.SetTargetProperty(moveTop, newPropertyPath(Canvas.TopProperty)); move = newStoryboard(); move.Children.Add(moveTop); move.Children.Add(moveLeft); move.Completed += delegate{ move.Stop(cursor); cursor.UpdateCursor(newPoint(e.X, e.Y), false); this.RaiseEvent(newKinectCursorEventArgs(KinectCursorUnlockEvent, newPoint(e.X, e.Y), e.Z) { Cursor = e.Cursor }); }; move.Begin(cursor, true); } private voidAnimateCursorToLockPosition(KinectCursorEventArgse,doublex,doubley,CursorAdornercursor,refPointlockPoint,refPointcursorPoint) { DoubleAnimationmoveLeft=newDoubleAnimation(cursorPoint.X,lockPoint.X,newDuration(TimeSpan.FromMilliseconds(LockInterval))); Storyboard.SetTarget(moveLeft,cursor.CursorVisual); Storyboard.SetTargetProperty(moveLeft,newPropertyPath(Canvas.LeftProperty)); DoubleAnimationmoveTop=newDoubleAnimation(cursorPoint.Y,lockPoint.Y,newDuration(TimeSpan.FromMilliseconds(LockInterval))); Storyboard.SetTarget(moveTop,cursor.CursorVisual); Storyboard.SetTargetProperty(moveTop,newPropertyPath(Canvas.TopProperty)); move=newStoryboard(); move.Children.Add(moveTop); move.Children.Add(moveLeft); move.Completed+=delegate { this.RaiseEvent(newKinectCursorEventArgs(KinectCursorLockEvent,newPoint(x,y),e.Z){Cursor=e.Cursor}); }; if(move!=null) move.Stop(e.Cursor); move.Begin(cursor,false); }
在上面的lock和unlock動畫中,我們等到動畫結束時觸發KinectCursorLock和KinectCursorUnlock事件。對於磁性按鈕本身,這些事件用處不大。但是在後面可以給磁性幻燈片按鈕提供一些幫助。
2.4 劃動(Swipe)
劃動手勢和揮手(wave)手勢類似。識別劃動手勢需要不斷的跟蹤用戶手部運動,並保持當前手的位置之前的手的位置。因為手勢有一個速度阈值,我們需要追蹤手運動的時間以及在三維空間中的坐標。下面的代碼展示了存儲手勢位置點的X,Y,Z坐標以及時間值。如果熟悉圖形學中的矢量計算,可以將這個認為是一個四維向量。將下面的結構添加到類庫中。
public struct GesturePoint { public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public DateTime T { get; set; } public override bool Equals(object obj) { var o = (GesturePoint)obj; return (X == o.X) && (Y == o.Y) && (Z == o.Z)&&(T==o.T); } public override int GetHashCode() { return base.GetHashCode(); } }
我們將在KinectCursorManager對象中實現劃動手勢識別的邏輯,這樣在後面的磁吸幻燈片按鈕中就可以復用這部分邏輯。實現代碼如下,代碼中為了支持劃動識別,需要向KinectCurosrManager對象中添加幾個字段。GesturePoints集合存儲路徑上的所有點,雖然我們會一邊移除一些點然後添加新的點,但是該集合不可能太大。SwipeTime和swipeDeviation分別提供了劃動手勢經歷的時間和劃動手勢在y軸上的偏移阈值。劃動手勢經歷時間過長和劃動手勢路徑偏移y值過大都會使得劃動手勢識別失敗。我們會移除之前的路徑上的點,然後添加新的劃動手勢上的點。SwipeLength提供了連續劃動手勢的阈值。我們提供了兩個事件來處理劃動手勢識別成功和手勢不合法兩種情況。考慮到這是一個純粹的手勢,與GUI界面無關,所以在實現過程中不會使用click事件。
private List<GesturePoint> gesturePoints; private bool gesturePointTrackingEnabled; private double swipeLength, swipeDeviation; private int swipeTime; public event KinectCursorEventHandler swipeDetected; public event KinectCursorEventHandler swipeOutofBoundDetected; private double xOutOfBoundsLength; private static double initialSwipeX;
xOutOfBoundsLength和initialSwipeX用來設置劃動手勢的開始位置。通常,我們並不關心揮劃動手勢的開始位置,只用在gesturePoints中尋找一定數量連續的點,然後進行模式匹配就可以了。但是有時候,我們只從某一個劃動開始點來進行劃動識別也很有用。例如如果在屏幕的邊緣,我們實現水平滾動,在這種情況下,我們需要一個偏移阈值使得我們可以忽略在屏幕外的點,因為這些點不能產生手勢。
下面的代碼展示了一些幫助方法以及公共屬性來管理手勢追蹤。GesturePointTrackingInitialize方法用來初始化各種手勢追蹤的參數。初始化好了劃動手勢之後,需要調用GesturePointTrackingStart方法。自然需要一個相應的GesturePointTrackingStop方法來結束揮動手勢識別。最後我們需要提供兩個重載的幫助方法ResetGesturePoint來管理一系列的我們不需要的手勢點。
public void GesturePointTrackingInitialize(double swipeLength, double swipeDeviation, int swipeTime, double xOutOfBounds) { this.swipeLength = swipeLength; this.swipeDeviation = swipeDeviation; this.swipeTime = swipeTime; this.xOutOfBoundsLength = xOutOfBounds; } public void GesturePointTrackingStart() { if (swipeLength + swipeDeviation + swipeTime == 0) throw new InvalidOperationException("揮動手勢識別參數沒有初始化!"); gesturePointTrackingEnabled = true; } public void GesturePointTrackingStop() { xOutOfBoundsLength = 0; gesturePointTrackingEnabled = false; gesturePoints.Clear(); } public bool GesturePointTrackingEnabled { get { return gesturePointTrackingEnabled ; } } private void ResetGesturePoint(GesturePoint point) { bool startRemoving = false; for (int i= gesturePoints.Count; i >=0; i--) { if (startRemoving) gesturePoints.RemoveAt(i); else if (gesturePoints[i].Equals(point)) startRemoving = true; } } private void ResetGesturePoint(int point) { if (point < 1) return; for (int i = point-1; i >=0; i--) { gesturePoints.RemoveAt(i); } }
劃動(swipe)手勢識別的核心算法在HandleGestureTracking方法中,代碼如下。將KinectCursorManager中的UpdateCursor方法和Kinect中的骨骼追蹤事件綁定。每一次當獲取到新的坐標點時,HandGestureTracking方法將最新的GesturePoint數據添加到gesturePoints集合中去。然後執行一些列條件檢查,首先判斷新加入的點是否以手勢開始位置為起點參考,偏離Y軸過遠。如果是,拋出一個超出范圍的事件,然後將所有之前累積的點清空,然後開始下一次的劃動識別。其次,檢查手勢開始的時間和當前的時間,如果時間差大於阈值,那麼移除開始處手勢點,然後將緊接著的點作為手勢識別的起始點。如果新的手的位置在這個集合中,就很好。緊接著,判斷劃動起始點的位置和當前位置的X軸上的距離是否超過了連續劃動距離的阈值,如果超過了,則觸發SwipeDetected事件,如果沒有,我們可以有選擇性的判斷,當前位置的X點是否超過了劃動識別的最大區間返回,然後觸發對於的事件。然後我們等待新的手部點傳到HandleGestureTracking方法中去。
private void HandleGestureTracking(float x, float y, float z) { if (!gesturePointTrackingEnabled) return; // check to see if xOutOfBounds is being used if (xOutOfBoundsLength != 0 && initialSwipeX == 0) { initialSwipeX = x; } GesturePoint newPoint = new GesturePoint() { X = x, Y = y, Z = z, T = DateTime.Now }; gesturePoints.Add(newPoint); GesturePoint startPoint = gesturePoints[0]; var point = new Point(x, y); //check for deviation if (Math.Abs(newPoint.Y - startPoint.Y) > swipeDeviation) { //Debug.WriteLine("Y out of bounds"); if (swipeOutofBoundDetected != null) swipeOutofBoundDetected(this, new KinectCursorEventArgs(point) { Z = z, Cursor = cursorAdorner }); ResetGesturePoint(gesturePoints.Count); return; } if ((newPoint.T - startPoint.T).Milliseconds > swipeTime) //check time { gesturePoints.RemoveAt(0); startPoint = gesturePoints[0]; } if ((swipeLength < 0 && newPoint.X - startPoint.X < swipeLength) // check to see if distance has been achieved swipe left || (swipeLength > 0 && newPoint.X - startPoint.X > swipeLength)) // check to see if distance has been achieved swipe right { gesturePoints.Clear(); //throw local event if (swipeDetected != null) swipeDetected(this, new KinectCursorEventArgs(point) { Z = z, Cursor = cursorAdorner }); return; } if (xOutOfBoundsLength != 0 && ((xOutOfBoundsLength < 0 && newPoint.X - initialSwipeX < xOutOfBoundsLength) // check to see if distance has been achieved swipe left || (xOutOfBoundsLength > 0 && newPoint.X - initialSwipeX > xOutOfBoundsLength)) ) { if (swipeOutofBoundDetected != null) swipeOutofBoundDetected(this, new KinectCursorEventArgs(point) { Z = z, Cursor = cursorAdorner }); } }
2.5 磁性幻燈片(Magnetic Slide)
磁性幻燈片是Kinect手勢中的精華(holy grail)。他由Harmonix公司的交互設計師們在開發《舞林大會》(Dance Central)這一款游戲時創造的。最初被用在菜單系統中,現在作為一種按鈕在很多地方有應用,包括Xbox自身的操作面板。他比磁性按鈕好的地方就是,不需要用戶等待一段時間。在Xbox游戲中,沒有人願意去等待。而下壓按鈕又有自身的缺點,最主要的是用戶體驗不是很好。磁性幻燈片和磁性按鈕一樣,一旦用戶進入到按鈕的有效區域,光標就會自定鎖定到某一點上。但是在這一點上,可以有不同的表現。除了懸停在按鈕上方一段時間觸發事件外,用戶可以劃動收來激活按鈕。
從編程角度看,磁性幻燈片基本上是磁性按鈕和劃動手勢(swipe)的組合。要開發一個磁性幻燈片按鈕,我們可以簡單的在可視化樹中的懸浮按鈕上聲明一個計時器,然後再注冊滑動手勢識別事件。下面的代碼展示了磁性幻燈片按鈕的基本結構。其構造函數已經在基類中為我們聲明好了計時器。InitializeSwipe和DeinitializeSwipe方法負責注冊KinectCursorManager類中的滑動手勢識別功能。
public class MagneticSlide:MagnetButton { private bool isLookingForSwipes; public MagneticSlide() { base.isLockOn = false; } private void InitializeSwipe() { if (isLookingForSwipes) return; var kinectMgr = KinectCursorManager.Instance; kinectMgr.GesturePointTrackingInitialize(SwipeLength, MaxDeviation, MaxSwipeTime, xOutOfBoundsLength); kinectMgr.swipeDetected += new KinectCursorEventHandler(kinectMgr_swipeDetected); kinectMgr.swipeOutofBoundDetected += new KinectCursorEventHandler(kinectMgr_swipeOutofBoundDetected); kinectMgr.GesturePointTrackingStart(); } private void DeInitializeSwipe() { var KinectMgr = KinectCursorManager.Instance; KinectMgr.swipeDetected -= new KinectCursorEventHandler(kinectMgr_swipeDetected); KinectMgr.swipeOutofBoundDetected -= new KinectCursorEventHandler(kinectMgr_swipeOutofBoundDetected); KinectMgr.GesturePointTrackingStop(); isLookingForSwipes = false; }
另外,我們也需要將控件的滑動手勢的初始化參數暴露出來,這樣就可以根據特定的需要進行設置了。下面的代碼展示了SwipeLength和XOutOfBoundsLength屬性,這兩個都是默認值的相反數。這是因為磁性幻燈片按鈕一般在屏幕的右側,需要用戶向左邊劃動,因此,相對於按鈕位置的識別偏移以及邊界偏移是其X坐標軸的相反數。
public static readonly DependencyProperty SwipeLengthProperty = DependencyProperty.Register("SwipeLength", typeof(double), typeof(MagneticSlide), new UIPropertyMetadata(-500d)); public double SwipeLength { get { return (double)GetValue(SwipeLengthProperty); } set { SetValue(SwipeLengthProperty, value); } } public static readonly DependencyProperty MaxDeviationProperty = DependencyProperty.Register("MaxDeviation", typeof(double), typeof(MagneticSlide), new UIPropertyMetadata(100d)); public double MaxDeviation { get { return (double)GetValue(MaxDeviationProperty); } set { SetValue(MaxDeviationProperty, value); } } public static readonly DependencyProperty XOutOfBoundsLengthProperty = DependencyProperty.Register("XOutOfBoundsLength", typeof(double), typeof(MagneticSlide), new UIPropertyMetadata(-700d)); public double XOutOfBoundsLength { get { return (double)GetValue(XOutOfBoundsLengthProperty); } set { SetValue(XOutOfBoundsLengthProperty, value); } } public static readonly DependencyProperty MaxSwipeTimeProperty = DependencyProperty.Register("MaxSwipeTime", typeof(int), typeof(MagneticSlide), new UIPropertyMetadata(300)); public int MaxSwipeTime { get { return (int)GetValue(MaxSwipeTimeProperty); } set { SetValue(MaxSwipeTimeProperty, value); } }
要實現磁性幻燈片按鈕的邏輯,我們只需要處理基類中的enter事件,以及劃動手勢識別事件即可。我們不會處理基類中的leave事件,因為當用戶做劃動手勢時,極有可能會不小心觸發leave事件。我們不想破壞之前初始化好了的deactivate算法邏輯,所以取而代之的是,我們等待要麼下一個劃動識別成功,要麼在關閉劃動識別前劃動手勢超出識別范圍。當探測到劃動時,觸發一個標准的click事件。
public static readonly RoutedEvent SwipeOutOfBoundsEvent = EventManager.RegisterRoutedEvent("SwipeOutOfBounds", RoutingStrategy.Bubble, typeof(KinectCursorEventHandler), typeof(KinectInput)); public event RoutedEventHandler SwipeOutOfBounds { add { AddHandler(SwipeOutOfBoundsEvent, value); } remove { RemoveHandler(SwipeOutOfBoundsEvent, value); } } void KinectMgr_swipeOutofBoundDetected(object sender, KinectCursorEventArgs e) { DeInitializeSwipe(); RaiseEvent(new KinectCursorEventArgs(SwipeOutOfBoundsEvent)); } void KinectMgr_swipeDetected(object sender, KinectCursorEventArgs e) { DeInitializeSwipe(); RaiseEvent(new RoutedEventArgs(ClickEvent)); } protected override void OnKinectCursorEnter(object sender, KinectCursorEventArgs e) { InitializeSwipe(); base.OnKinectCursorEnter(sender, e); }
2.6 垂直滾動條(Vertical Scroll)
並不是所有的內容都能夠在一屏之內顯示完。有時候可能有一些內容會大於屏幕的實際尺寸,這就需要用戶來滾動屏幕或者列表控件來顯示在屏幕之外的內容。傳統上,垂直滾動條一直是交互界面設計的一個禁忌。但是垂直滾動條在劃動觸摸界面中得到了很好的應用。所以Xbox和Sony PlayStation系統中都使用了垂直滾動條來構建菜單。Harmonix’s的《舞林大會》(Dance Central)這一系列游戲使用了垂直滾動條式的菜單系統。Dance Central第一次成功的使用了垂直滾動界面作為手勢交互界面。在下面的手勢交互圖中,當用戶抬起或者放下手臂時會使得屏幕的內容垂直滾動。胳膊遠離身體,抬起手臂會使得屏幕或者菜單從下往上移動,放下手臂會使得從上往下移動。
水平的劃動在Kinect應用中似乎很常見(尤其是在Metro風格的Xbox游戲交互界面中,水平劃動是占主導的手勢),但是垂直滾動用戶體驗更加友好,也是用戶交互界面更好的選擇。水平或者垂直劃動手勢有一些小的用戶體驗問題。另外,劃動手勢在識別上也較困難,因為揮動的形式和動作因人而異,且差別很大。就算同一個人,劃動手勢也不是一直不變的。劃動手勢在觸摸屏設備上能夠較好的工作是因為除非不觸摸到屏幕,那麼動作就不會發生。但是在手勢識別界面上,用戶的手是和視覺元素進行交互的,這時手就是在某一特定的坐標空間中的視覺元素。
當用戶做劃動手勢時,在整個手的劃動過程中會手的位置在水平方向會保持相對一致。這就使得如果想進行多次連續的劃動手勢時會產生一些問題。有時候會產生一些比較尴尬的場景,那就是會無意中撤銷前一次的劃動手勢。例如,用戶使用右手從右向左進行劃動手勢,使得頁面會跳轉到下一頁,現在用戶的右手在身體的左邊位置了,然後用戶想將手移動回原始的開始位置以准備下一次的從右向左的揮動手勢。但是,如果用於依然保持手在水平位置大致一致的話,應用程序會探測到一次從左向右的劃動操作然後又將界面切換到了之前的那一頁。這就使得用戶必須創建一個循環的運動來避免不必要的誤讀。更進一步,頻繁的劃動手勢也容易使得用戶疲勞,而垂直方向的劃動也只會加劇這一問題。
但是垂直滾動條則不會有上述的這些用戶體驗上的缺點。他比較容易使用,對用戶來說也更加友好,另外,用戶也不需要為了保持手在水平或者垂直方向一致而導致的疲勞。從技術方面來講,垂直滾動操作識別較劃動識別簡單。垂直滾動在技術上是一個姿勢而不是手勢。滾動操作的探測是基於當前手臂的位置而不是手臂的運動。滾動的方向和大小由手臂和水平方向的夾角來確定。下圖演示了垂直滾動。
使用之前的姿勢識別那篇文章中的內容,我們能夠計算從用戶的身體到肩部和手腕的夾角,定義一個角度區間作為中間姿勢,當用戶手臂在這一區間內時,不會產生任何動作,如上圖中的,當手臂自然處於-5度或者355度時,作為偏移的零點。建議在實際開發中,將零點的偏移上下加上20度左右。當用戶的手臂離開這一區域時,離開的夾角及變化的幅度根據需求而定。但是建議至少在0度區間上下有兩個區間來表示小幅和大幅的增加。這使得能夠更好的實現傳統的人機交互界面中的垂直滾動條的邏輯。
2.7 通用暫停按鈕(Universal Pause)
暫停按鈕,通常作為引導手勢或者退出手勢,是微軟建議在給用戶提供引導時很少的幾個手勢之一。這個手勢是通過將左臂保持和身體45度角來完成的。在很多Kinect的游戲中都使用到了這一手勢,用來暫停動作或者喚出Xbox菜單。和本文之前介紹的手勢不一樣,這個手勢並沒有什麼符號學上的含義,是一個認為設計的動作。通用暫停手勢很容易實現,也不一定要限制手臂,並且不容易和其他手勢混淆。
通用暫停手勢的識別和垂直滾動手勢的識別有點類似,就是計算左臂和身體的夾角,然後加上一個阈值即可,相信很簡單,在這裡就不再贅述了。
2.8測試Demo
結合前篇文章中的揮動(wave)手勢識別,以及上文將的幾種按鈕,做了一個小的例子,使用之前開發的手勢識別庫,以及手勢識別按鈕。這部分代碼很簡單,直接引用之前在類庫中定義好的控件即可。大家可以下載本文後面的代碼自己回去實驗一下。 截圖如下:
查看本欄目
3. 手勢識別的未來
我們進入商場可能對各種商品不會有很深的印象,同樣,隨著時間的流逝,Kinect也會變得不那麼令人印象深刻,甚至被大多數人忘記。但是,隨著軟硬件技術的發展,Kinect或者相似的技術會融入到生活的各個方面,讓你看不到Kinect的存在。
當我們進入一個購物商場時,當你靠近入口時,門會自動打開。這在當時很讓人印象深刻,但沒有人會注意,觀察甚至意識到這一特性。在未來的某一天,Kinect也會像自動門一樣融入生活的方方面面。使得我們感受不到他的存在。
Kinect以及NUI的世界才剛剛開始,隨著時間的推移,這種交互體驗會發生巨大變化。在電影《少數派報告》(Minority Report)中,湯姆克魯斯使用手勢浏覽和打開大屏幕上的各種文件和影像資料,這一場景現在已經是基於Kinect的應用程序的要實現目標之一。 有時候,科幻片中比現實有更好的想像力,也能提供比現實生活中更好的科技。在星際迷航(Star Trek),星球大戰(Star Wars)或者 2001:太空漫游(2001: A Space Odyssey)這些科幻電影中,電腦能夠看到和感應人的存在。在這些電影中,用戶使用語音和手勢無縫的和電腦進行交互。當然這種交互也有負面因素,應該設置一些限制。
雖然科幻電影變成現實的這一前景會引發一些理性的擔憂,但是這種變化正在到來。意識到這種變化帶來的好處很重要。Kinect及其類似的技術使得我們的生活環境更加智能化。它使得應用程序能夠識別用戶的手勢,進而能夠分析出用戶的意圖,而不需要用戶明確的給出這些信息或者指令。現在的Kinect游戲是基於應用程序查找特定的用戶的手勢而進行操作的,用戶必須主動的和應用程序進行交流或者發送指令。但是,還是有很多用戶的信息沒有被捕捉和處理。如果應用程序能夠探測到其他信息,確切的說,如用戶的情緒,那麼就可以提供更加人性化的定制服務了。現在我們所識別的姿勢都很簡單,我們只是在學習如何建立交互界面,可以預見在未來,隨著基於手勢交互的應用程序越來越多,這種用戶界面就會消失,就像移動設備中觸摸界面逐漸取代光標那樣。
想象一下,下班後回到家,走到臥室,說一句“電腦,放點music”。於是電腦就會識別語音指令開始播放音樂。但是,電腦也能夠識別到你工作了一天,需要一些音樂來放松心情,於是電腦自動的根據你的心情選擇一些歌曲。語音成了首要的發送指令的形式,手勢來對一些指令進行增強。在上面的例子中,電腦能夠根據你的身體語言,識別你的情緒,這樣,手勢是一種主動的,有上下文情景的和電腦進行交互的方法。這並不意味這手勢變得不那麼重要,相反重要性增加了,只不過是以一種間接的方式。
如今有一些聲控傳感器,例如能夠根據聲音探測到人進入到房間,然後開燈或者關燈的聲控開關。這是一種比較笨拙的系統,他沒有上下文提供。如果使用Kinect技術,它能夠識別用戶的運動,然後根據情形調整燈光的亮度。例如,如果在凌晨2點,你想起床喝點水,電腦可能會將燈光調整的比較暗,以至於不會太刺眼。但是如果某一天你凌晨2點鐘從外面回來,Kinect識別到你是清醒的,就會把燈全部打開。
目前,Kinect仍然是一種比較新的技術,我們仍然試圖理解然後能夠更好的發揮它的潛能。在最開始Kinect出來的時候只是觀看或者體驗。隨著他作為Xbox外設的發布,游戲的主題也有了一些限制。大多數游戲都是運動型的,這些游戲都只能識別用戶的一些基本手勢,如跑,跳,踢,劃動,扔等這些手勢或動作。早期的Kinect體感游戲也只有一些簡單的菜單系統,使用手來進行操作。
雖然用戶體驗設計發生了巨大變化,但是目前基於手勢的游戲和應用比較簡單。我們仍然處在學習如何定義和識別手勢的階段。這使得我們的手勢有些粗糙。我們仍需要擴大手勢的幅度才能達到好的識別效果。當我們能夠識別到手勢的細微方面時,應用程序所帶來的沉浸感將會大大提升。
現在的足球游戲只能識別到基本的踢球動作,游戲不能夠判斷用戶是使用腳趾,腳背,腳踝還是腳跟跟球進行交互的。這些不同的姿勢可能對球產生完全不同的影響,從而使得游戲產生不同的結果。更進一步,游戲應該能夠根據用戶踢球的動作,腳的位置,應用一些物理特性,給予球一些真實的加速度,旋轉,弧度等特性,這樣會使得游戲更加真實,玩家也更加有沉浸感。
目前的這些限制主要是由Kinect攝像頭的分辨率決定的。下一代的Kinect硬件設備可能會使用更高分辨率的攝像頭來提供更好的深度影像數據。微軟已經放出了關於第二代Kinect硬件方面的相關信息。這使得更精確的手勢識別變為可能,給基於Kinect的應用程序開發帶來了兩個方面的改進。首先是骨骼關節精度的提升,這不但能夠提升手勢識別的精度,也能夠擴大手勢識別的種類。另一個改進是使得能夠產生一些額外的關節點,如手指的信息,以及一些非關節點如嘴唇,鼻子,耳朵,眼睛等位置信息。如今這些信息都能夠識別的出來,只是需要使用一些第三方的類庫,官方的SDK中沒有原生的對這些特征進行支持。
對手指進行追蹤和識別能夠大大的提高符號語言的作用。如果應用程序能夠識別手指的運動,用戶就能夠使用手指進行更高精度和更自然的操作。手指手勢交互信息很復雜,在用戶進行交流的過程中能夠提供豐富的上下文信息。即使能夠識別到手指手勢,今天的基於Kinect的間交互體驗也沒有發生革命性的變化,這是因為用戶依然必須意識到Kinect的存在,還要知道如何和這些硬件交互,需要做什麼樣的手勢才能達到想要的結果。當你看別人玩Kinect游戲時,你會注意到他是如何在Kinect前面做動作的。用戶的姿勢有時候比較僵硬和不自然。很多姿勢並不能識別,一些需要重復多次,更壞的情況下姿勢會被錯誤識別,這樣就會導致一些意想不到的操作。更進一步,用戶的手勢通常需要過分的誇大才能被kinect識別的到。這些問題可能都是暫時的。在未來,隨著軟硬件的不斷升級和改進,在使用基於手勢的人機交互界面時,用戶會越來越感到舒服和自然。從這方面講,Kinect是一個神奇的設備,以至於會像自動門那樣被廣大的用戶所知道和接受。
4.結語
在上一篇文章介紹手勢識別概念和識別方法的基礎上,本文進一步解釋了如何進行手勢識別。首先,構建了一個基本的手勢識別的框架,然後在此基礎上對常用的8中手勢中的剩余7中手勢進行逐一講解與實現。最後展望了Kienct未來在手勢識別方面的前景和應用。希望這些知識對您了解和掌握Kinect SDK手勢識別有所幫助!
作者: yangecnu(yangecnu's Blog on 博客園)
出處:http://www.cnblogs.com/yangecnu/