Kinect 开发 —— 手势识别(下)
基本手势追踪
手部追踪在技术上和手势识别不同,但是它和手势识别中用到的一些基本方法是一样的。在开发一个具体的手势控件之前,我们先建立一个可重用的追踪手部运动的类库以方便我们后续开发。这个手部追踪类库包含一个以动态光标显示的可视化反馈机制。手部追踪和手势控件之间的交互高度松耦合。
首先在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(, );
brush.StartPoint = new Point(, );
brush.GradientStops.Add(new GradientStop(_backColor, ));
brush.GradientStops.Add(new GradientStop(_foreColor, )); var cursor = new Ellipse()
{
Width=,
Height=,
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/));
_cursor.SetValue(Canvas.LeftProperty, position.Y - (_cursor.ActualHeight / ));
} public void UpdateCursor(Pointposition)
{
if(_isOverriden) return;
_cursor.SetValue(Canvas.LeftProperty, position.X - (_cursor.ActualWidth / ));
_cursor.SetValue(Canvas.LeftProperty, position.Y - (_cursor.ActualHeight / ));
}
最后,添加光标对象动画效果。当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[];
varstop2 = brush.GradientStops[];
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[]) { }
private KinectCursorManager(Window window, FrameworkElement cursor) : this(window, KinectSensor.KinectSensors[], 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 > )
{
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 == ) 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应用中也有类似的倾向,每一次将鼠标移动到按钮上不用点击就会激活按钮。这种用户界面很容易不能使用,这也提醒了一个潜在的值得注意的问题,那就是将按钮从图形用户界面中移植到其他界面中可能存在的问题。悬浮按钮是微软试图解决这一特殊问题的一个尝试。
Kinect 开发 —— 手势识别(下)的更多相关文章
- Kinect 开发 —— 手势识别(上)
		像点击(clicks)是GUI平台的核心,轻点(taps)是触摸平台的核心那样,手势(gestures)是Kinect应用程序的核心 关于手势的定义的中心在于手势能够用来交流,手势的意义在于讲述而不是 ... 
- Kinect开发文章目录
		整理了一下去年为止到现在写的和翻译的Kinect的相关文章,方便大家查看.另外,最近京东上微软在搞活动, 微软 Kinect for Windows 京东十周年专供礼包 ,如果您想从事Kinect开发 ... 
- Kinect开发学习笔记之(一)Kinect介绍和应用
		Kinect开发学习笔记之(一)Kinect介绍和应用 zouxy09@qq.com http://blog.csdn.net/zouxy09 一.Kinect简单介绍 Kinectfor Xbox ... 
- Kinect 开发 —— 控制PPT播放
		实现Kinect控制幻灯片播放很简单,主要思路是:使用Kinect捕捉人体动作,然后根据识别出来的动作向系统发出点击向前,向后按键的事件,从而使得幻灯片能够切换. 这里的核心功能在于手势的识别,我们在 ... 
- Kinect 开发 —— 全息图
		Kinect的另一个有趣的应用是伪全息图(pseudo-hologram).3D图像可以根据人物在Kinect前面的各种位置进行倾斜和移动.如果方法够好,可以营造出3D控件中3D图像的效果,这样可以用 ... 
- Kinect 开发 —— 进阶指引(上)
		本文将会介绍一些第三方类库如何来帮助处理Kinect传感器提供的数据.使用不同的技术进行Kinect开发,可以发掘出Kinect应用的强大功能.另一方面如果不使用这些为了特定处理目的而开发的一些类库, ... 
- Kinect开发 —— 基础知识
		转自:http://www.cnblogs.com/yangecnu/archive/2012/04/02/KinectSDK_Application_Fundamentals_Part2.html ... 
- Kinect 开发 —— ColorBasic
		创建一个Kincet项目通常需要: 1. 创建一个VS项目,一般为了展示通常创建一个wpf项目. 2. 添加Microsoft.Kinect.dll引用,如果是早期版本的SDK,这个名称可能不同. 3 ... 
- Kinect开发笔记之三Kinect开发环境配置具体解释
		0.前言: 首先说一下我的开发环境,Visual Studio是2013的,系统是win8的64位版本号,SDK是Kinect for windows SDK 1.8版本 ... 
随机推荐
- iOS——扬声器与听筒的切换
			1.扬声器模式: NSError *error; [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPor ... 
- 用xmanager6启动Linux上的图形界面程序
			1.下载Xmanager6 并自行安装,这里不赘述了 2.打开Xmanager.启动Xstart 3.按提示输入:主机IP,协议,用户名,命令,完成后点击“保存”,接着点击“运行”,运行xmanage ... 
- 监控web服务(http,本地 / 远程监控nginx)
			监控 httpd 服务一: #!/bin/bash #描述: 秒级别监控 http 服务 while [ 1 -lt 2 ] do sleep 10 ai=`netstat -ntl | grep & ... 
- python Web抓取(二)selenium模块的使用、对浏览器的按键操作及错误处理
			建议以下帖子: 教你在Windows上搭建Python+Selenium环境:https://blog.csdn.net/huilan_same/article/details/52888262 py ... 
- 紫书 例题 10-24 UVa 1641(面积计算)
			遍历一遍,遇到边界为奇数次时,格子在多边形内 偶数次时,在多边形外 #include<cstdio> #define REP(i, a, b) for(int i = (a); i < ... 
- 今日SGU 5.29
			sgu 299 题意:给你n个线段,然后问你能不能选出其中三个组成一个三角形,数字很大 收获:另一个大整数模板 那么考虑下为什么如果连续三个不可以的话,一定是不存在呢? 连续上个不合法的话,一定是 a ... 
- error C2440: “static_cast”: 无法从“LRESULT (__thiscall CTextProgressCtrl::* )(UINT,LPCTSTR)”转换为“LRESULT (__thiscall CWnd::* )(WPARAM,LPARAM)
			转自原文 error C2440 “static_cast” 无法从“void (__thiscall C* )(void)... error C2440: “static_cast”: 无法从“LR ... 
- hdu 5380 Travel with candy(双端队列)
			pid=5380">题目链接:hdu 5380 Travel with candy 保持油箱一直处于满的状态,维护一个队列,记录当前C的油量中分别能够以多少价格退货,以及能够推货的量. ... 
- cocos2d-x 3.1.1学习笔记[23]寻找主循环 mainloop
			文章出自于 http://blog.csdn.net/zhouyunxuan cocos2d到底是怎样把场景展示给我们的,我一直非常好奇. 凭个人猜想,引擎内部的结构类似于这样 while(true ... 
- vue12 循环添加重复数据
			<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ... 
