Kinect 开发 —— 常见手势识别(下)

划动(Swipe)html

划动手势和挥手(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事件。编程

xOutOfBoundsLength和initialSwipeX用来设置划动手势的开始位置。一般,咱们并不关心挥划动手势的开始位置,只用在gesturePoints中寻找必定数量连续的点,而后进行模式匹配就能够了。可是有时候,咱们只从某一个划动开始点来进行划动识别也颇有用。例如若是在屏幕的边缘,咱们实现水平滚动,在这种状况下,咱们须要一个偏移阈值使得咱们能够忽略在屏幕外的点,由于这些点不能产生手势。ide

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;

 

下面的代码展现了一些帮助方法以及公共属性来管理手势追踪。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); 
    } 
}

 

ResetGesturePoint 从下划姿式的底部点向上遍历,不等于point(传入的匹配结点)的保留 —— 下划过程的所有点,等于以及以上的点无效,所有删除 ~~this

划动(swipe)手势识别的核心算法在HandleGestureTracking方法中,代码以下。将KinectCursorManager中的UpdateCursor方法和Kinect中的骨骼追踪事件绑定。每一次当获取到新的坐标点时,HandGestureTracking方法将最新的GesturePoint数据添加到gesturePoints集合中去。而后执行一些列条件检查,首先判断新加入的点是否以手势开始位置为起点参考,偏离Y轴过远。若是是,抛出一个超出范围的事件,而后将全部以前累积的点清空,而后开始下一次的划动识别。其次,检查手势开始的时间和当前的时间,若是时间差大于阈值,那么移除开始处手势点,而后将紧接着的点做为手势识别的起始点。若是新的手的位置在这个集合中,就很好。紧接着,判断划动起始点的位置和当前位置的X轴上的距离是否超过了连续划动距离的阈值,若是超过了,则触发SwipeDetected事件,若是没有,咱们能够有选择性的判断,当前位置的X点是否超过了划动识别的最大区间返回,而后触发对于的事件。而后咱们等待新的手部点传到HandleGestureTracking方法中去。spa

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 });
    }
}

磁性幻灯片设计

他比磁性按钮好的地方就是,不须要用户等待一段时间。在Xbox游戏中,没有人愿意去等待。而下压按钮又有自身的缺点,最主要的是用户体验不是很好。磁性幻灯片和磁性按钮同样,一旦用户进入到按钮的有效区域,光标就会自定锁定到某一点上。可是在这一点上,能够有不一样的表现。除了悬停在按钮上方一段时间触发事件外,用户能够划动收来激活按钮。3d

从编程角度看,磁性幻灯片基本上是磁性按钮和划动手势(swipe)的组合。要开发一个磁性幻灯片按钮,咱们能够简单的在可视化树中的悬浮按钮上声明一个计时器,而后再注册滑动手势识别事件。下面的代码展现了磁性幻灯片按钮的基本结构。其构造函数已经在基类中为咱们声明好了计时器。InitializeSwipe和DeinitializeSwipe方法负责注册KinectCursorManager类中的滑动手势识别功能。code

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);
}

垂直滚动条 (Vertical Scroll)

传统上,垂直滚动条一直是交互界面设计的一个禁忌。可是垂直滚动条在划动触摸界面中获得了很好的应用。因此Xbox和Sony PlayStation系统中都使用了垂直滚动条来构建菜单。Harmonix’s的《舞林大会》(Dance Central)这一系列游戏使用了垂直滚动条式的菜单系统。Dance Central第一次成功的使用了垂直滚动界面做为手势交互界面。在下面的手势交互图中,当用户抬起或者放下手臂时会使得屏幕的内容垂直滚动。胳膊远离身体,抬起手臂会使得屏幕或者菜单从下往上移动,放下手臂会使得从上往下移动。

image

当用户作划动手势时,在整个手的划动过程当中会手的位置在水平方向会保持相对一致。这就使得若是想进行屡次连续的划动手势时会产生一些问题。有时候会产生一些比较尴尬的场景,那就是会无心中撤销前一次的划动手势。例如,用户使用右手从右向左进行划动手势,使得页面会跳转到下一页,如今用户的右手在身体的左边位置了,而后用户想将手移动回原始的开始位置以准备下一次的从右向左的挥动手势。可是,若是用于依然保持手在水平位置大体一致的话,应用程序会探测到一次从左向右的划动操做而后又将界面切换到了以前的那一页。这就使得用户必须建立一个循环的运动来避免没必要要的误读。更进一步,频繁的划动手势也容易使得用户疲劳,而垂直方向的划动也只会加重这一问题。

可是垂直滚动条则不会有上述的这些用户体验上的缺点。他比较容易使用,对用户来讲也更加友好,另外,用户也不须要为了保持手在水平或者垂直方向一致而致使的疲劳。从技术方面来说,垂直滚动操做识别较划动识别简单。垂直滚动在技术上是一个姿式而不是手势。滚动操做的探测是基于当前手臂的位置而不是手臂的运动。滚动的方向和大小由手臂和水平方向的夹角来肯定。下图演示了垂直滚动。

使用以前的姿式识别那篇文章中的内容,咱们可以计算从用户的身体到肩部和手腕的夹角,定义一个角度区间做为中间姿式,当用户手臂在这一区间内时,不会产生任何动做,如上图中的,当手臂天然处于-5度或者355度时,做为偏移的零点。建议在实际开发中,将零点的偏移上下加上20度左右。当用户的手臂离开这一区域时,离开的夹角及变化的幅度根据需求而定。可是建议至少在0度区间上下有两个区间来表示小幅和大幅的增长。这使得可以更好的实现传统的人机交互界面中的垂直滚动条的逻辑



通用暂停按钮 (Universal Pause)

暂停按钮,一般做为引导手势或者退出手势,是微软建议在给用户提供引导时不多的几个手势之一。这个手势是经过将左臂保持和身体45度角来完成的。在不少Kinect的游戏中都使用到了这一手势,用来暂停动做或者唤出Xbox菜单。和本文以前介绍的手势不同,这个手势并无什么符号学上的含义,是一个认为设计的动做。通用暂停手势很容易实现,也不必定要限制手臂,而且不容易和其余手势混淆。

相关文章
相关标签/搜索