Kinect 开发 —— 常见手势识别(上)
悬浮按钮 (Hover Button)
悬浮按钮通过将鼠标点击换成悬浮然后等待(hover-and-wait)动作,解决了不小心点击的问题。当光标位于按钮之上时,意味着用户通过将光标悬浮在按钮上一段时间来表示想选中按钮。另一个重要特点是悬浮按钮在用户悬浮并等待时,多少提供了视觉反馈。
必须使用一个计时器来记录当前用户光标停留在按钮上的时间。一旦用户的手的光标和按钮的边界交叉就开始计时。如果某一个时间阈值内用户光标还没有移除,那么就触发点击事件。
创建一个名为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中骨骼识别本身的问题。当在运动状态时,Kinect能够很好的对这些抖动进行平滑,因为即使在快速移动状态下,Kinect中的软件使用了一系列预测和平滑技术来对抖动进行处理。姿势,和上面的悬停一样,因为是静止的,所以可能存在抖动的问题。另外,用户一般不会保持手势静止,即使他们想哪样做。Kinect将这些小的运动返回给用户。当用户什么都没做时,抖动的手可能会破坏手势的动画效果。对悬浮按钮的一个改进就是磁性按钮(Magnet 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;
}
}
磁性按钮 (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(,)); varx=point.X+this.ActualWidth/;
vary=point.Y+this.ActualHeight/; varcursor=e.Cursor;
cursor.UpdateCursor(newPoint(e.X,e.Y),true); //找到目的位置
PointlockPoint=newPoint(x-cursor.CursorVisual.ActualWidth/,y-cursor.CursorVisual.ActualHeight/);
//当前位置
PointcursorPoint=newPoint(e.X-cursor.CursorVisual.ActualWidth/,e.Y-cursor.CursorVisual.ActualHeight/);
//将光标从当前位置传送到目的位置
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(,)); varx=point.X+this.ActualWidth/;
vary=point.Y+this.ActualHeight/; varcursor=e.Cursor; //找到目的位置
PointlockPoint=newPoint(x-cursor.CursorVisual.ActualWidth/,y-cursor.CursorVisual.ActualHeight/);
//当前位置
PointcursorPoint=newPoint(e.X-cursor.CursorVisual.ActualWidth/,e.Y-cursor.CursorVisual.ActualHeight/); 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);
}
Kinect 开发 —— 常见手势识别(上)的更多相关文章
- Kinect 开发 —— 常见手势识别(下)
划动(Swipe) 划动手势和挥手(wave)手势类似.识别划动手势需要不断的跟踪用户手部运动,并保持当前手的位置之前的手的位置.因为手势有一个速度阈值,我们需要追踪手运动的时间以及在三维空间中的坐标 ...
- Kinect 开发 —— 语音识别(上)
Kinect的麦克风阵列在Kinect设备的下方.这一阵列由4个独立的水平分布在Kinect下方的麦克风组成.虽然每一个麦克风都捕获相同的音频信号,但是组成阵列可以探测到声音的来源方向.使得能够用来识 ...
- Kinect 开发 —— 进阶指引(上)
本文将会介绍一些第三方类库如何来帮助处理Kinect传感器提供的数据.使用不同的技术进行Kinect开发,可以发掘出Kinect应用的强大功能.另一方面如果不使用这些为了特定处理目的而开发的一些类库, ...
- C后端设计开发 - 第6章-武技-常见组件上三路
正文 第6章-武技-常见组件上三路 后记 如果有错误, 欢迎指正. 有好的补充, 和疑问欢迎交流, 一块提高. 在此谢谢大家了.
- Kinect 开发 —— 控制PPT播放
实现Kinect控制幻灯片播放很简单,主要思路是:使用Kinect捕捉人体动作,然后根据识别出来的动作向系统发出点击向前,向后按键的事件,从而使得幻灯片能够切换. 这里的核心功能在于手势的识别,我们在 ...
- Kinect开发文章目录
整理了一下去年为止到现在写的和翻译的Kinect的相关文章,方便大家查看.另外,最近京东上微软在搞活动, 微软 Kinect for Windows 京东十周年专供礼包 ,如果您想从事Kinect开发 ...
- Kinect开发学习笔记之(一)Kinect介绍和应用
Kinect开发学习笔记之(一)Kinect介绍和应用 zouxy09@qq.com http://blog.csdn.net/zouxy09 一.Kinect简单介绍 Kinectfor Xbox ...
- Kinect 开发 —— 全息图
Kinect的另一个有趣的应用是伪全息图(pseudo-hologram).3D图像可以根据人物在Kinect前面的各种位置进行倾斜和移动.如果方法够好,可以营造出3D控件中3D图像的效果,这样可以用 ...
- Kinect开发资源汇总
Kinect开发资源汇总 转自: http://www.sigvc.org/bbs/forum.php?mod=viewthread&tid=254&highlight=kinec ...
随机推荐
- Linux下通过rdesktop连接Windows远程桌面
rdesktop是linux下支持Windows远程桌面连接的客户端程序,在linux系统下可通过它远程访问Windows桌面,支持多种版本.rdesktop是sourceforge下支持GPL协议的 ...
- 负载均衡集群总结(Haproxy)
环境:Centos 6.9,Mysql 8.0 首先要先配置mysql主从复制集,可以参考我的上一篇>>Mysql 主从复制总结(详细) 我的主节点在(master):192.168.11 ...
- 四舍五入VS银行家舍入法
在学习python的时候,遇见了一个颠覆了我传统观念的四舍五入. 看下面,round()的结果和我们以前根深蒂固的四舍五入是不同的. >>> round(0.5) 0 >> ...
- Java Web MVC 一个实例的手动实现
平台: tomcat7.0 Servlet3.0 Windows命令行编译 实现的功能: 在网页上可以进行对Product类的三个属性的输入,点击保存之后跳转到另一个显示输入内容的界面 文 ...
- vue-router 实现无效路由(404)的友好提示
最近在做一个基于vue-router的SPA,想对无效路由(404)页面做下统一处理.这次我真的没有在官方文档找到具体的说明[捂脸]所以本文仅是我DIY的一个思路,求轻虐=_= 在我的理解中,vue- ...
- OpenJDK源码研究笔记(九)-可恨却又可亲的的异常(NullPointerException)
可恨的异常 程序开发过程中,最讨厌异常了. 异常代表着程序出了问题,一旦出现,控制台会出现一屏又一屏的堆栈错误信息. 看着就让人心烦. 对于一个新人来讲,遇到异常经常会压力大,手忙脚乱,心生畏惧. 可 ...
- WHU 1540 Fibonacci 递推
武大邀请赛的网络预选赛,就去做了个签到题,居然连这个递推都没推出来,真是惭愧. 而且好久没写矩阵乘法了,来回顾一下. 题意: 求Fibonacci数列的,前n项立方和. 思路: 可以求得一下递推公式: ...
- android 推断是否支持闪光灯
近期在做录制视频功能,在找一些资料时发现 要推断是否支持闪关灯,在这记录下来,怕以后忘记 public static boolean isSupportCameraLedFlash(PackageMa ...
- Runtime类中的freeMemory,totalMemory,maxMemory等几个方法
最近在网上看到一些人讨论到Java.lang.Runtime类中的freeMemory(),totalMemory(),maxMemory ()这几个方法的一些题目,很多人感到很迷惑,为什么,在jav ...
- HTTP 各种特性应用(三)
一. 数据协商 分类: 客户端请求: Accept: Accept:表明 我想要什么样的数据 Accept-Encoding:数据是什么样的编码方式 进行传输.主要限制 服务端怎样进行数据的压缩. A ...