Kinect 开发 —— 骨骼追踪(下)

Kinect 连线游戏

在纸上将一些列数字(用一个圆点表示)从小到大用线连起来。游戏逻辑很简单,只不过我们在这里要实现的是动动手将这些点连起来,而不是用笔或者鼠标。

在开始写代码之前,需要明确定义我们的游戏目标。连线游戏是一个智力游戏,游戏者需要将数字从小到大连起来。程序可以自定义游戏上面的数字和位置(合称一个关卡)。每一个关卡包括一些列的数字(以点表示)及其位置。我们要创建一个DotPuzzle类来管理这些点对象的集合。可能一开始不需要这个类,仅仅需要一个集合就可以,但是为了以后方便添加其他功能,使用类更好一点。这些点在程序中有两个地方需要用到,一个是最开始的时候在界面上绘制关卡,其次是判断用户是否碰到了这些点。

    当用户碰到点时,程序开始绘制,直线以碰到的点为起始点,直线的终点位用户碰到的下一个点。然后下一个点又作为另一条直线的起点,依次类推。直到最后一个点和第一个点连起来,这样关卡算是通过了,游戏结束。


游戏的用户界面

Polyline对象用来表示点与点之间的连线。当用户在点和点之间移动手时,程序将点添加到Polyline对象中。PuzzleBoardElement Canvas对象用来作为UI界面上所有点的容器。Grid对象下面的Canvas的顺序是有意这样排列的,我们使用另外一个GameBoardElement Canvas对象来存储手势,以Image来表示,并且能够保证这一层总是在点图层之上将每一类对象放在各自层中的另外一个好处是重新开始一个新的游戏变得很容易,只需要将PuzzleBoardElement节点下的所有子节点清除,CrayonElement元素和其他的UI对象不会受到影响。

Viewbox和Grid对象对于UI界面很重要。如上一篇文章中讨论的,骨骼节点数据是基于骨骼空间的。这意味着我们要将骨骼向量转化到UI坐标系中来才能进行绘制。我们将UI控件硬编码,不允许它随着UI窗体的变化而浮动。Grid节点将UI空间大小定义为1920*1200。通常这个是显示器的全屏尺寸,而且他和深度影像数据的长宽比是一致的。这能够使得坐标转换更加清楚而且能够有更加流畅的手势移动体验。

硬编码UI界面也能够简化开发过程,能够使得从骨骼坐标向UI坐标的转化更加简单和快速,只需要几行代码就能完成操作。况且,如果不硬编码,相应主UI窗体大小的改变将会增加额外的工作量。通过将Grid嵌入Viewbox节点来让WPF来帮我们做缩放操作。最后一个UI元素是Image对象,他表示手的位置。在这个小游戏中,我们使用这么一个简单的图标代表手。你可以选择其他的图片或者直接用一个Ellipse对象来代替。本游戏中图片使用的是右手。在游戏中,用户可以选择使用左手或者右手,如果用户使用左手,我们将该图片使用ScaleTransform变换,使得变得看起来像右手。

<Window x:Class="SkeletonGame.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:c="clr-namespace:SkeletonGame"  
        Title="MainWindow" Height="600" Width="800" Background="White">
    <Viewbox>
        <Grid x:Name ="LayoutRoot" Width="1920" Height="1200">
            <c:SkeletonViewer x:Name="SkeletonViewerElement"/>
            <Polyline x:Name="CrayonElement" Stroke="Black" StrokeThickness="3"/>
            <Canvas x:Name="PuzzleBoardElement"/>
            <Canvas x:Name="GameBoardElement">
                <Image x:Name="HandCursorElement" Source="Images/hand.png" Width="75" Height="75" RenderTransformOrigin="0.5,0.5">
                    <Image.RenderTransform>
                        <TransformGroup>
                            <ScaleTransform x:Name="HandCursorScale" ScaleX="1"/>
                        </TransformGroup>
                    </Image.RenderTransform>
                </Image>
            </Canvas>
        </Grid>
    </Viewbox>
</Window>

查找最近的游戏者

private static Skeleton GetPrimarySkeleton(Skeleton[] skeletons)
{
    Skeleton skeleton = null;

    if (skeletons != null)
    {
        //查找最近的游戏者
        for (int i = 0; i < skeletons.Length; i++)
        {
            if (skeletons[i].TrackingState == SkeletonTrackingState.Tracked)
            {
                if (skeleton == null)
                {
                    skeleton = skeletons[i];
                }
                else
                {
                    if (skeleton.Position.Z > skeletons[i].Position.Z)
                    {
                        skeleton = skeletons[i];
                    }
                }
            }
        }
    }
    return skeleton;
}

每一次事件执行时,我们查找第一个合适的游戏者。程序不会锁定某一个游戏者。如果有两个游戏者,那么靠Kinect最近的那个会是活动的游戏者。这就是GetPrimarySkeleton的功能。如果没有活动的游戏者,手势图标就隐藏。否则,我们使用活动游戏者离Kinect最近的那只手作为控制。

private static Joint GetPrimaryHand(Skeleton skeleton)
{
    Joint primaryHand = new Joint();
    if (skeleton != null)
    {
        primaryHand = skeleton.Joints[JointType.HandLeft];
        Joint righHand = skeleton.Joints[JointType.HandRight];
        if (righHand.TrackingState != JointTrackingState.NotTracked)
        {
            if (primaryHand.TrackingState == JointTrackingState.NotTracked)
            {
                primaryHand = righHand;
            }
            else
            {
                if (primaryHand.Position.Z > righHand.Position.Z)
                {
                    primaryHand = righHand;
                }
            }
        }
    }
    return primaryHand;
}

优先选择的是距离Kinect最近的那只手。但是,代码不单单是比较左右手的Z值来判断选择Z值小的那只手,如前篇文章讨论的,Z值为0表示该点的深度信息不能确定。所以,我们在进行比较之前需要进行验证,检查每一个节点的TrackingState状态。左手是默认的活动手,除非游戏者是左撇子。右手必须显示的追踪,或者被计算认为离Kinect更近。在操作关节点数据时,一定要检查TrackingState的状态,否则会得到一些异常的位置信息,这样会导致UI绘制错误或者是程序异常。

获取的手势值是在骨骼控件坐标系中,我们需要将手在骨骼控件坐标系统中的位置转换到对于的UI坐标系统中去。

private void TrackHand(Joint hand)
{
    if (hand.TrackingState == JointTrackingState.NotTracked)
    {
        HandCursorElement.Visibility = Visibility.Collapsed;
    }
    else
    {
        HandCursorElement.Visibility = Visibility.Visible;
        DepthImagePoint point = this.kinectDevice.MapSkeletonPointToDepth(hand.Position, this.kinectDevice.DepthStream.Format);
        point.X = (int)((point.X * LayoutRoot.ActualWidth / kinectDevice.DepthStream.FrameWidth) - (HandCursorElement.ActualWidth / 2.0));
        point.Y = (int)((point.Y * LayoutRoot.ActualHeight / kinectDevice.DepthStream.FrameHeight) - (HandCursorElement.ActualHeight / 2.0));

        Canvas.SetLeft(HandCursorElement, point.X);
        Canvas.SetTop(HandCursorElement, point.Y);

        if (hand.JointType == JointType.HandRight)
        {
            HandCursorScale.ScaleX = 1;
        }
        else
        {
            HandCursorScale.ScaleX = -1;
        }
    }
}

为了显示绘制游戏的逻辑,我们创建一个新的类DotPuzzle。这个类的最主要功能是保存一些数字,数字在集合中的位置决定了在数据系列中的前后位置。这个类允许序列化,我们能够从xml文件中读取关卡信息来建立新的关卡。

public class DotPuzzle
{
    public List<Point> Dots { get; set; }
    public DotPuzzle()
    {
        this.Dots = new List<Point>();
    }
}

最后一步是在UI界面上绘制点信息。我们创建了一个名为DrawPuzzle的方法,在主窗体加载完成的时候触发改事件。DrawPuzzle遍历集合中的每一个点,然后创建UI元素表示这个点,然后将这个点添加到PuzzleBoardElement节点下面。另一种方法是使用XAML 创建UI界面,将DotPuzzle对象作为ItemControl的ItemSource属性,ItemsControl对象的ItemTemplate对象能够定义每一个点的外观和位置。

private void DrawPuzzle(DotPuzzle puzzle)
{
    PuzzleBoardElement.Children.Clear();

    if (puzzle != null)
    {
        for (int i = 0; i < puzzle.Dots.Count; i++)
        {
            Grid dotContainer = new Grid();
            dotContainer.Width = 50;
            dotContainer.Height = 50;
            dotContainer.Children.Add(new Ellipse { Fill = Brushes.Gray });

            TextBlock dotLabel = new TextBlock();
            dotLabel.Text = (i + 1).ToString();
            dotLabel.Foreground = Brushes.White;
            dotLabel.FontSize = 24;
            dotLabel.HorizontalAlignment = HorizontalAlignment.Center;
            dotLabel.VerticalAlignment = VerticalAlignment.Center;
            dotContainer.Children.Add(dotLabel);

            //在UI界面上绘制点
            Canvas.SetTop(dotContainer, puzzle.Dots[i].Y - (dotContainer.Height / 2));
            Canvas.SetLeft(dotContainer, puzzle.Dots[i].X - (dotContainer.Width / 2));
            PuzzleBoardElement.Children.Add(dotContainer);
        }
    }
}

游戏逻辑实现

到目前为止,我们的游戏已经有了用户界面和基本的数据。移动手,能够看到手势图标会跟着移动。我们要将线画出来。当游戏者的手移动到点上时,开始绘制直线的起点,然后知道手朋到下一个点时,将这点作为直线的终点,并开始另一条直线,并以该点作为起点

private void TrackPuzzle(SkeletonPoint position)
{
    if (this.puzzleDotIndex == this.puzzle.Dots.Count)
    {
        //游戏结束
    }
    else
    {
        Point dot;
        if (this.puzzleDotIndex + 1 < this.puzzle.Dots.Count)
        {
            dot = this.puzzle.Dots[this.puzzleDotIndex + 1];
        }
        else
        {
            dot = this.puzzle.Dots[0];
        }

        DepthImagePoint point = this.kinectDevice.MapSkeletonPointToDepth(position, kinectDevice.DepthStream.Format);
        point.X = (int)(point.X * LayoutRoot.ActualWidth / kinectDevice.DepthStream.FrameWidth);
        point.Y = (int)(point.Y * LayoutRoot.ActualHeight / kinectDevice.DepthStream.FrameHeight);
        Point handPoint = new Point(point.X, point.Y);
        Point dotDiff = new Point(dot.X - handPoint.X, dot.Y - handPoint.Y);
        double length = Math.Sqrt(dotDiff.X * dotDiff.X + dotDiff.Y * dotDiff.Y);

        int lastPoint = this.CrayonElement.Points.Count - 1;
        //手势离点足够近
        if (length < 25)
        {
            if (lastPoint > 0)
            {
                //移去最后一个点
                this.CrayonElement.Points.RemoveAt(lastPoint);
            }

            //设置直线的终点
            this.CrayonElement.Points.Add(new Point(dot.X, dot.Y));

            //设置新的直线的起点
            this.CrayonElement.Points.Add(new Point(dot.X, dot.Y));

            //转到下一个点
            this.puzzleDotIndex++;
            if (this.puzzleDotIndex == this.puzzle.Dots.Count)
            {
                //通知游戏者游戏结束
            }
        }
        else
        {
            if (lastPoint > 0)
            {
                //移除最后一个点,更新界面                   
                Point lineEndpoint = this.CrayonElement.Points[lastPoint];
                this.CrayonElement.Points.RemoveAt(lastPoint);
                //将手势所在的点作为线的临时终点
                lineEndpoint.X = handPoint.X;
                lineEndpoint.Y = handPoint.Y;
                this.CrayonElement.Points.Add(lineEndpoint);
            }
        }
    }
}

有效点击范围应该要比实际的UI元素大。这一点在Kinect或者其他触控设备上都是应该遵循的设计原则。如果用户移动到了这个点击区域,就可以认为用户点击到了这个目标点。


各种坐标空间及变换

在大多数情况下,原始的坐标数据是不能直接使用的。骨骼点数据和深度数据或者彩色影像数据的测量方法不同。每一种类的数据(深度数据,影像数据,骨骼数据)都是在特定的集合坐标或空间内定义的。深度数据或者影像数据用像素来表示,X,Y位置从左上角以0开始。深度数据的Z方位数据以毫米为单位。与这些不同的是,骨骼空间是以米为单位来描述的,以深度传感器为中心,其X,Y值为0。骨骼坐空间坐标系是右手坐标系,X正方向朝右,Y周正方向朝上X轴数据范围为-2.2~2.2,总共范围为4.2米,Y周范围为-1.6~1.6米,Z轴范围为0~4米。下图描述了Skeleton数据流的空间坐标系。

image

将数据从骨骼数据空间转换到深度数据空间很容易。SDK提供了一系列方法来帮助我们进行这两个空间坐标系的转换。KinectSensor对象有一个称之为MapSkeletonPointToDepth的方法能够将骨骼点数据转换到UI空间中去。SDK中也提供了一个相反的MapDepthToSkeletonPoint方法。MapSkeletonPointToDepth方法接受一个SkeletonPoint点和一个DepthImageFormat作为参数。骨骼点数据来自Skeleton对象或者Joint对象的Position属性。方法的名字中有Depth,并不只是字面上的意思。目标空间并不需要Kinect深度影像。事实上,DepthStream不必初始化,方法通过DepthImageFormat来确定如何变化。一旦骨骼点数据被映射到深度空间中去了之后,他能够进行缩放到任意的纬度。

骨骼数据是镜面对称的。在大多数情况下,应用是可行的,因为人对应于显示屏就应该是镜面对称。在上面的连线小游戏中,人对于与屏幕也应该是镜面对称,这样恰好模拟人的手势。但是在一些游戏中,角色代表实际的游戏者,可能角色是背对着游戏者的,这就是所谓的第三人称视角。在有些游戏中,这种镜像了的骨骼数据可能不好在UI上进行表现。一些应用或者游戏希望能够直面角色,不希望有这种镜像的效果。当游戏者挥动左手时也希望角色能够挥动左手。

通过反转骨骼节点数据的X值就可以实现这个效果。要实现X值的反转,只需要将X的值乘以-1即可。

private Point GetJointPoint(Joint joint)
{
    DepthImagePoint point = this.KinectDevice.MapSkeletonPointToDepth(joint.Position, this.KinectDevice.DepthStream.Format);
    point.X *= -1*(int) this.LayoutRoot.ActualWidth / KinectDevice.DepthStream.FrameWidth;
    point.Y *= (int) this.LayoutRoot.ActualHeight / KinectDevice.DepthStream.FrameHeight;
                        
    return new Point(point.X, point.Y);
}

SkeletonView 自定义控件

开发Kinect应用程序进行交互时,在开发阶段,将骨骼关节点数据绘制到UI界面上是非常有帮助的。在调试程序时骨骼数据影像能够帮助我们看到和理解原始输入数据,但是在发布程序时,我们不需要这些信息。一种办法是每一处都复制一份将骨骼数据绘制到UI界面上的代码,这显然不符合DIY原则,所以我们应当把这部分代码独立出来,做成一个自定义控件。

   本节我们的目标是,将骨骼数据查看代码封装起来,并使其在调试时为我们提供更多的实时信息。我们使用自定义控件来实现这一功能点。首先,创建一个名为SkeletonViewer的自定义控件。这个控件可以是任何一个panel对象的一个子节点。创建一个自定义控件,并将其XAML替换成如下代码:

<UserControl x:Class="KinectDrawDotsGame.SkeletonViewer"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid x:Name="LayoutRoot">
        <Grid x:Name="SkeletonsPanel"/>
        <Canvas x:Name="JointInfoPanel"/>
    </Grid>
</UserControl>
SkeletonsPanel就是绘制骨骼节点的panel。JointInfoPanel 是在调试时用来显示额外信息的图层。下一步是需要将一个KinectSnesor对象传递到这个自定义控件中来。为此,我们创建了一个DependencyProperty,使得我们可以使用数据绑定。下面的代码展示了这一属性。KinectDeviceChange静态方法对于任何使用该用户控件的方法和功能非常重要。该方法首先取消之前注册到KinectSensor的SkeletonFrameReady事件上的方法。如果不注销这些事件会导致内存泄漏。
protected const string KinectDevicePropertyName = "KinectDevice";
public static readonly DependencyProperty KinectDeviceProperty = DependencyProperty.Register(KinectDevicePropertyName, typeof(KinectSensor), typeof(SkeletonViewer), new PropertyMetadata(null, KinectDeviceChanged));

private static void KinectDeviceChanged(DependencyObject owner, DependencyPropertyChangedEventArgs e)
{
    SkeletonViewer viewer = (SkeletonViewer)owner;

    if (e.OldValue != null)
    {
        ((KinectSensor)e.OldValue).SkeletonFrameReady -= viewer.KinectDevice_SkeletonFrameReady;
        viewer._FrameSkeletons = null;
    }

    if (e.NewValue != null)
    {
        viewer.KinectDevice = (KinectSensor)e.NewValue;
        viewer.KinectDevice.SkeletonFrameReady += viewer.KinectDevice_SkeletonFrameReady;
        viewer._FrameSkeletons = new Skeleton[viewer.KinectDevice.SkeletonStream.FrameSkeletonArrayLength];
    }
}

public KinectSensor KinectDevice
{
    get { return (KinectSensor)GetValue(KinectDeviceProperty); }
    set { SetValue(KinectDeviceProperty, value); }
}

将这个自定义控件加到应用中很简单。由于是自定义控件,自需要在应用程序的XAML文件中声明自定义控件,然后在程序中给SkeletonViewer的KinectDevice赋值


namespace SkeletonGame
{
    public partial class SkeletonViewer : UserControl
    {
        private readonly Brush[] _SkeletonBrushes = new Brush[] { Brushes.Black, Brushes.Crimson, Brushes.Indigo, Brushes.DodgerBlue, Brushes.Purple, Brushes.Pink };
        private Skeleton[] _FrameSkeletons;
        protected const string KinectDevicePropertyName = "KinectDevice";
        public static readonly DependencyProperty KinectDeviceProperty = DependencyProperty.Register(KinectDevicePropertyName, typeof(KinectSensor), typeof(SkeletonViewer), new PropertyMetadata(null, KinectDeviceChanged));
            // 依赖属性就是一种可以自己没有值,并能通过使用Binding从数据源获得值(依赖在别人身上)的属性。拥有依赖属性的对象称为“依赖对象”。
            // WPF开发中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来。依赖对象的概念被DependencyObject类所实现,依赖属性的概念则由DependencyProperty类所实现

        public KinectSensor KinectDevice
        {
            get { return (KinectSensor)GetValue(KinectDeviceProperty);}
            set { SetValue(KinectDeviceProperty, value); }
        }

        public SkeletonViewer()
        {
            InitializeComponent();
        }

        private static void KinectDeviceChanged(DependencyObject owner, DependencyPropertyChangedEventArgs e)
        {
            SkeletonViewer viewer = (SkeletonViewer)owner;

            if (e.OldValue !=null)  // 释放资源
            {
                ((KinectSensor)e.OldValue).SkeletonFrameReady -= viewer.KinectDevice_SkeletonFrameReady;
                viewer._FrameSkeletons = null;
            }

            if (e.NewValue != null) 
            {
                viewer.KinectDevice = (KinectSensor)e.NewValue; // Set
                viewer.KinectDevice.SkeletonFrameReady += viewer.KinectDevice_SkeletonFrameReady;
                viewer._FrameSkeletons = new Skeleton[viewer.KinectDevice.SkeletonStream.FrameSkeletonArrayLength];
            }
        }


        private void KinectDevice_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            SkeletonsPanel.Children.Clear();
            JointInfoPanel.Children.Clear();

            using (SkeletonFrame frame = e.OpenSkeletonFrame())
            {
                if (frame!=null)
                {
                    if (this.IsEnabled)
                    {
                        frame.CopySkeletonDataTo(this._FrameSkeletons);

                        for (int i = 0; i < this._FrameSkeletons.Length;i++ )
                        {
                            DrawSkeleton(this._FrameSkeletons[i], this._SkeletonBrushes[i]);

                            TrackJoint(this._FrameSkeletons[i].Joints[JointType.HandLeft], this._SkeletonBrushes[i]);
                            TrackJoint(this._FrameSkeletons[i].Joints[JointType.HandRight], this._SkeletonBrushes[i]);
                        }
                    }
                }
            }
        }


        private void TrackJoint(Joint joint, Brush brush)
        {
            if (joint.TrackingState != JointTrackingState.NotTracked)
            {
                Canvas container = new Canvas();
                Point jointPoint = GetJointPoint(joint);

                double z = joint.Position.Z;

                Ellipse element = new Ellipse();
                element.Height = 15;
                element.Width = 15;
                element.Fill = brush;
                Canvas.SetLeft(element, 0 - (element.Width / 2));
                Canvas.SetTop(element, 0 - (element.Height / 2));
                container.Children.Add(element);    // 检测到手之后,添加椭圆

                TextBlock positionText = new TextBlock();
                positionText.Text = string.Format("<{0:0.00}, {1:0.00}, {2:0.00}>", jointPoint.X, jointPoint.Y, z);
                positionText.Foreground = brush;
                positionText.FontSize = 24;
                positionText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                Canvas.SetLeft(positionText, 35);
                Canvas.SetTop(positionText, 15);
                container.Children.Add(positionText);   // 用TextBlock来显示手坐标位置

                Canvas.SetLeft(container, jointPoint.X);
                Canvas.SetTop(container, jointPoint.Y); // 将Canvas移动到相应位置

                JointInfoPanel.Children.Add(container);
            }
        }

        private void DrawSkeleton(Skeleton skeleton, Brush brush)
        {
            if (skeleton != null && skeleton.TrackingState == SkeletonTrackingState.Tracked)
            {
                //Draw head and torso
                Polyline figure = CreateFigure(skeleton, brush, new[] { JointType.Head, JointType.ShoulderCenter, JointType.ShoulderLeft, JointType.Spine,
                                                                             JointType.ShoulderRight, JointType.ShoulderCenter, JointType.HipCenter});
                SkeletonsPanel.Children.Add(figure);

                figure = CreateFigure(skeleton, brush, new[] { JointType.HipLeft, JointType.HipRight });
                SkeletonsPanel.Children.Add(figure);

                //Draw left leg
                figure = CreateFigure(skeleton, brush, new[] { JointType.HipCenter, JointType.HipLeft, JointType.KneeLeft, JointType.AnkleLeft, JointType.FootLeft });
                SkeletonsPanel.Children.Add(figure);

                //Draw right leg
                figure = CreateFigure(skeleton, brush, new[] { JointType.HipCenter, JointType.HipRight, JointType.KneeRight, JointType.AnkleRight, JointType.FootRight });
                SkeletonsPanel.Children.Add(figure);

                //Draw left arm
                figure = CreateFigure(skeleton, brush, new[] { JointType.ShoulderLeft, JointType.ElbowLeft, JointType.WristLeft, JointType.HandLeft });
                SkeletonsPanel.Children.Add(figure);

                //Draw right arm
                figure = CreateFigure(skeleton, brush, new[] { JointType.ShoulderRight, JointType.ElbowRight, JointType.WristRight, JointType.HandRight });
                SkeletonsPanel.Children.Add(figure);
            }
        }

        private Polyline CreateFigure(Skeleton skeleton, Brush brush, JointType[] joints)
        {
            Polyline figure = new Polyline();

            figure.StrokeThickness = 18;
            figure.Stroke = brush;

            for (int i = 0; i < joints.Length; i++)
            {
                figure.Points.Add(GetJointPoint(skeleton.Joints[joints[i]]));
            }

            return figure;
        }

        private Point GetJointPoint(Joint joint)    // 骨骼点的坐标转化
        {
            DepthImagePoint point = this.KinectDevice.MapSkeletonPointToDepth(joint.Position, this.KinectDevice.DepthStream.Format);
            point.X *= (int)this.LayoutRoot.ActualWidth / KinectDevice.DepthStream.FrameWidth;
            point.Y *= (int)this.LayoutRoot.ActualHeight / KinectDevice.DepthStream.FrameHeight;

            return new Point(point.X, point.Y);
        }
    }
}

namespace SkeletonGame
{
    public partial class MainWindow : Window
    {
        private KinectSensor kinectDevice;
        private int puzzleDotIndex;
        private Skeleton[] frameSkeletons;
        private DotPuzzle puzzle;

        public KinectSensor KinectDevice
        {
            get { return this.kinectDevice; }
            set
            {
                if (this.kinectDevice != value)
                {
                    //Uninitialize
                    if (this.kinectDevice != null)
                    {
                        this.kinectDevice.Stop();
                        this.kinectDevice.SkeletonFrameReady -= KinectDevice_SkeletonFrameReady;
                        this.kinectDevice.SkeletonStream.Disable();
                        SkeletonViewerElement.KinectDevice = null;
                        this.frameSkeletons = null;
                    }

                    this.kinectDevice = value;

                    //Initialize
                    if (this.kinectDevice != null)
                    {
                        if (this.kinectDevice.Status == KinectStatus.Connected)
                        {
                            this.kinectDevice.SkeletonStream.Enable();
                            this.frameSkeletons = new Skeleton[this.kinectDevice.SkeletonStream.FrameSkeletonArrayLength];
                            SkeletonViewerElement.KinectDevice = this.KinectDevice;
                            this.kinectDevice.Start();
                            this.KinectDevice.SkeletonFrameReady += KinectDevice_SkeletonFrameReady;
                        }
                    }
                }
            }
        }

        public MainWindow()
        {
            InitializeComponent();
            puzzle = new DotPuzzle();
            this.puzzle.Dots.Add(new Point(200, 300));
            this.puzzle.Dots.Add(new Point(1600, 300));
            this.puzzle.Dots.Add(new Point(1650, 400));
            this.puzzle.Dots.Add(new Point(1600, 500));
            this.puzzle.Dots.Add(new Point(1000, 500));
            this.puzzle.Dots.Add(new Point(1000, 600));
            this.puzzle.Dots.Add(new Point(1200, 700));
            this.puzzle.Dots.Add(new Point(1150, 800));
            this.puzzle.Dots.Add(new Point(750, 800));
            this.puzzle.Dots.Add(new Point(700, 700));
            this.puzzle.Dots.Add(new Point(900, 600));
            this.puzzle.Dots.Add(new Point(900, 500));
            this.puzzle.Dots.Add(new Point(200, 500));
            this.puzzle.Dots.Add(new Point(150, 400));

            this.puzzleDotIndex = -1;

            this.Loaded += (s, e) =>
            {
                KinectSensor.KinectSensors.StatusChanged += KinectSensors_StatusChanged;
                this.KinectDevice = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected);

                DrawPuzzle(this.puzzle);
            };
        }

        private void KinectSensors_StatusChanged(object sender, StatusChangedEventArgs e)
        {
            switch (e.Status)
            {
                case KinectStatus.Initializing:
                case KinectStatus.Connected:
                case KinectStatus.NotPowered:
                case KinectStatus.NotReady:
                case KinectStatus.DeviceNotGenuine:
                    this.KinectDevice = e.Sensor;
                    break;
                case KinectStatus.Disconnected:
                    //TODO: Give the user feedback to plug-in a Kinect device.                    
                    this.KinectDevice = null;
                    break;
                default:
                    //TODO: Show an error state
                    break;
            }
        }

        private void KinectDevice_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            using (SkeletonFrame frame = e.OpenSkeletonFrame())
            {
                if (frame != null)
                {
                    frame.CopySkeletonDataTo(this.frameSkeletons);
                    Skeleton skeleton = GetPrimarySkeleton(this.frameSkeletons);    // this.frameSkeletons 会包含多个骨架

                    Skeleton[] dataSet2 = new Skeleton[this.frameSkeletons.Length];
                    frame.CopySkeletonDataTo(dataSet2);

                    if (skeleton == null)
                    {
                        HandCursorElement.Visibility = Visibility.Collapsed;
                    }
                    else
                    {
                        Joint primaryHand = GetPrimaryHand(skeleton);
                        TrackHand(primaryHand);
                        TrackPuzzle(primaryHand.Position);
                    }
                }
            }
        }

        private static Skeleton GetPrimarySkeleton(Skeleton[] skeletons)
        {
            Skeleton skeleton = null;

            if (skeletons != null)
            {
                //查找最近的游戏者
                for (int i = 0; i < skeletons.Length; i++)
                {
                    if (skeletons[i].TrackingState == SkeletonTrackingState.Tracked)
                    {
                        if (skeleton == null)
                        {
                            skeleton = skeletons[i];
                        }
                        else
                        {
                            if (skeleton.Position.Z > skeletons[i].Position.Z)
                            {
                                skeleton = skeletons[i];
                            }
                        }
                    }
                }
            }

            return skeleton;
        }

        private static Joint GetPrimaryHand(Skeleton skeleton)
        {
            Joint primaryHand = new Joint();

            if (skeleton != null)
            {
                primaryHand = skeleton.Joints[JointType.HandLeft];
                Joint righHand = skeleton.Joints[JointType.HandRight];


                if (righHand.TrackingState != JointTrackingState.NotTracked)
                {
                    if (primaryHand.TrackingState == JointTrackingState.NotTracked)
                    {
                        primaryHand = righHand;
                    }
                    else
                    {
                        if (primaryHand.Position.Z > righHand.Position.Z)
                        {
                            primaryHand = righHand;
                        }
                    }
                }
            }

            return primaryHand;
        }

        private void TrackHand(Joint hand)
        {
            if (hand.TrackingState == JointTrackingState.NotTracked)
            {
                HandCursorElement.Visibility = Visibility.Collapsed;
            }
            else
            {
                HandCursorElement.Visibility = Visibility.Visible;


                DepthImagePoint point = this.kinectDevice.MapSkeletonPointToDepth(hand.Position, this.kinectDevice.DepthStream.Format);
                point.X = (int)((point.X * LayoutRoot.ActualWidth / kinectDevice.DepthStream.FrameWidth) - (HandCursorElement.ActualWidth / 2.0));
                point.Y = (int)((point.Y * LayoutRoot.ActualHeight / kinectDevice.DepthStream.FrameHeight) - (HandCursorElement.ActualHeight / 2.0));

                Canvas.SetLeft(HandCursorElement, point.X);
                Canvas.SetTop(HandCursorElement, point.Y);

                if (hand.JointType == JointType.HandRight)
                {
                    HandCursorScale.ScaleX = 1;
                }
                else
                {
                    HandCursorScale.ScaleX = -1;    // 如果是右手,图像水平反转
                }
            }
        }

        private void DrawPuzzle(DotPuzzle puzzle)
        {
            PuzzleBoardElement.Children.Clear();

            if (puzzle != null)
            {
                for (int i = 0; i < puzzle.Dots.Count; i++)
                {
                    Grid dotContainer = new Grid();
                    dotContainer.Width = 50;
                    dotContainer.Height = 50;
                    dotContainer.Children.Add(new Ellipse { Fill = Brushes.Gray });

                    TextBlock dotLabel = new TextBlock();
                    dotLabel.Text = (i + 1).ToString();
                    dotLabel.Foreground = Brushes.White;
                    dotLabel.FontSize = 24;
                    dotLabel.HorizontalAlignment = HorizontalAlignment.Center;
                    dotLabel.VerticalAlignment = VerticalAlignment.Center;
                    dotContainer.Children.Add(dotLabel);

                    //在UI界面上绘制点
                    Canvas.SetTop(dotContainer, puzzle.Dots[i].Y - (dotContainer.Height / 2));
                    Canvas.SetLeft(dotContainer, puzzle.Dots[i].X - (dotContainer.Width / 2));
                    PuzzleBoardElement.Children.Add(dotContainer);
                }
            }
        }

        private void TrackPuzzle(SkeletonPoint position)
        {
            if (this.puzzleDotIndex == this.puzzle.Dots.Count)
            {
                //游戏结束
            }
            else
            {
                Point dot;
                if (this.puzzleDotIndex + 1 < this.puzzle.Dots.Count)
                {
                    dot = this.puzzle.Dots[this.puzzleDotIndex + 1];
                }
                else
                {
                    dot = this.puzzle.Dots[0];
                }

                DepthImagePoint point = this.kinectDevice.MapSkeletonPointToDepth(position, kinectDevice.DepthStream.Format);
                point.X = (int)(point.X * LayoutRoot.ActualWidth / kinectDevice.DepthStream.FrameWidth);
                point.Y = (int)(point.Y * LayoutRoot.ActualHeight / kinectDevice.DepthStream.FrameHeight);
                Point handPoint = new Point(point.X, point.Y);
                Point dotDiff = new Point(dot.X - handPoint.X, dot.Y - handPoint.Y);
                double length = Math.Sqrt(dotDiff.X * dotDiff.X + dotDiff.Y * dotDiff.Y);

                int lastPoint = this.CrayonElement.Points.Count - 1;
                //手势离点足够近
                if (length < 25)
                {
                    if (lastPoint > 0)
                    {
                        //移去最后一个点
                        this.CrayonElement.Points.RemoveAt(lastPoint);
                    }

                    //设置直线的终点
                    this.CrayonElement.Points.Add(new Point(dot.X, dot.Y));

                    //设置新的直线的起点
                    this.CrayonElement.Points.Add(new Point(dot.X, dot.Y));

                    //转到下一个点
                    this.puzzleDotIndex++;
                    if (this.puzzleDotIndex == this.puzzle.Dots.Count)
                    {
                        //通知游戏者游戏结束
                    }
                }
                else
                {
                    if (lastPoint > 0)
                    {
                        //移除最后一个点,更新界面                   
                        Point lineEndpoint = this.CrayonElement.Points[lastPoint];
                        this.CrayonElement.Points.RemoveAt(lastPoint);
                        //将手势所在的点作为线的临时终点
                        lineEndpoint.X = handPoint.X;
                        lineEndpoint.Y = handPoint.Y;
                        this.CrayonElement.Points.Add(lineEndpoint);
                    }
                }
            }
        }


    }
}

<UserControl x:Class="SkeletonGame.SkeletonViewer"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid x:Name="LayoutRoot">
        <Grid x:Name="SkeletonsPanel"/>
        <Canvas x:Name="JointInfoPanel"/>
    </Grid>
</UserControl>
原文地址:https://www.cnblogs.com/sprint1989/p/3842268.html