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="" Width="" Background="White">
<Viewbox>
<Grid x:Name ="LayoutRoot" Width="" Height="">
<c:SkeletonViewer x:Name="SkeletonViewerElement"/>
<Polyline x:Name="CrayonElement" Stroke="Black" StrokeThickness=""/>
<Canvas x:Name="PuzzleBoardElement"/>
<Canvas x:Name="GameBoardElement">
<Image x:Name="HandCursorElement" Source="Images/hand.png" Width="" Height="" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="HandCursorScale" ScaleX=""/>
</TransformGroup>
</Image.RenderTransform>
</Image>
</Canvas>
</Grid>
</Viewbox>
</Window>
查找最近的游戏者
private static Skeleton GetPrimarySkeleton(Skeleton[] skeletons)
{
Skeleton skeleton = null; if (skeletons != null)
{
//查找最近的游戏者
for (int i = ; 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 = ;
}
else
{
HandCursorScale.ScaleX = -;
}
}
}
为了显示绘制游戏的逻辑,我们创建一个新的类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 = ; i < puzzle.Dots.Count; i++)
{
Grid dotContainer = new Grid();
dotContainer.Width = ;
dotContainer.Height = ;
dotContainer.Children.Add(new Ellipse { Fill = Brushes.Gray }); TextBlock dotLabel = new TextBlock();
dotLabel.Text = (i + ).ToString();
dotLabel.Foreground = Brushes.White;
dotLabel.FontSize = ;
dotLabel.HorizontalAlignment = HorizontalAlignment.Center;
dotLabel.VerticalAlignment = VerticalAlignment.Center;
dotContainer.Children.Add(dotLabel); //在UI界面上绘制点
Canvas.SetTop(dotContainer, puzzle.Dots[i].Y - (dotContainer.Height / ));
Canvas.SetLeft(dotContainer, puzzle.Dots[i].X - (dotContainer.Width / ));
PuzzleBoardElement.Children.Add(dotContainer);
}
}
}
游戏逻辑实现
到目前为止,我们的游戏已经有了用户界面和基本的数据。移动手,能够看到手势图标会跟着移动。我们要将线画出来。当游戏者的手移动到点上时,开始绘制直线的起点,然后知道手朋到下一个点时,将这点作为直线的终点,并开始另一条直线,并以该点作为起点
private void TrackPuzzle(SkeletonPoint position)
{
if (this.puzzleDotIndex == this.puzzle.Dots.Count)
{
//游戏结束
}
else
{
Point dot;
if (this.puzzleDotIndex + < this.puzzle.Dots.Count)
{
dot = this.puzzle.Dots[this.puzzleDotIndex + ];
}
else
{
dot = this.puzzle.Dots[];
} 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 - ;
//手势离点足够近
if (length < )
{
if (lastPoint > )
{
//移去最后一个点
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 > )
{
//移除最后一个点,更新界面
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数据流的空间坐标系。
将数据从骨骼数据空间转换到深度数据空间很容易。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 *= -*(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 = ; 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 = ;
element.Width = ;
element.Fill = brush;
Canvas.SetLeft(element, - (element.Width / ));
Canvas.SetTop(element, - (element.Height / ));
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 = ;
positionText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Canvas.SetLeft(positionText, );
Canvas.SetTop(positionText, );
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 = ;
figure.Stroke = brush; for (int i = ; 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(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, ));
this.puzzle.Dots.Add(new Point(, )); this.puzzleDotIndex = -; 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 = ; 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 = ;
}
else
{
HandCursorScale.ScaleX = -; // 如果是右手,图像水平反转
}
}
} private void DrawPuzzle(DotPuzzle puzzle)
{
PuzzleBoardElement.Children.Clear(); if (puzzle != null)
{
for (int i = ; i < puzzle.Dots.Count; i++)
{
Grid dotContainer = new Grid();
dotContainer.Width = ;
dotContainer.Height = ;
dotContainer.Children.Add(new Ellipse { Fill = Brushes.Gray }); TextBlock dotLabel = new TextBlock();
dotLabel.Text = (i + ).ToString();
dotLabel.Foreground = Brushes.White;
dotLabel.FontSize = ;
dotLabel.HorizontalAlignment = HorizontalAlignment.Center;
dotLabel.VerticalAlignment = VerticalAlignment.Center;
dotContainer.Children.Add(dotLabel); //在UI界面上绘制点
Canvas.SetTop(dotContainer, puzzle.Dots[i].Y - (dotContainer.Height / ));
Canvas.SetLeft(dotContainer, puzzle.Dots[i].X - (dotContainer.Width / ));
PuzzleBoardElement.Children.Add(dotContainer);
}
}
} private void TrackPuzzle(SkeletonPoint position)
{
if (this.puzzleDotIndex == this.puzzle.Dots.Count)
{
//游戏结束
}
else
{
Point dot;
if (this.puzzleDotIndex + < this.puzzle.Dots.Count)
{
dot = this.puzzle.Dots[this.puzzleDotIndex + ];
}
else
{
dot = this.puzzle.Dots[];
} 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 - ;
//手势离点足够近
if (length < )
{
if (lastPoint > )
{
//移去最后一个点
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 > )
{
//移除最后一个点,更新界面
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>
Kinect 开发 —— 骨骼追踪(下)的更多相关文章
- Kinect 开发 —— 骨骼追踪进阶(上)
Kinect传感器核心只是发射红外线,并探测红外光反射,从而可以计算出视场范围内每一个像素的深度值.从深度数据中最先提取出来的是物体主体和形状,以及每一个像素点的游戏者索引信息.然后用这些形状信息来匹 ...
- Kinect 开发 —— 骨骼追踪
骨骼追踪技术通过处理景深数据来建立人体各个关节的坐标,骨骼追踪能够确定人体的各个部分,如那部分是手,头部,以及身体.骨骼追踪产生X,Y,Z数据来确定这些骨骼点.骨骼追踪系统采用的景深图像处理技术使用更 ...
- Kinect 开发 —— 骨骼追踪 (下)
基于景深数据的用户交互 骨骼数据中关节点不仅有X,Y值,还有一个深度值 除了使用WPF的3D特性外,在布局系统中可以根据深度值来设定可视化元素的尺寸大小来达到某种程序的立体效果. 下面的例子使用Can ...
- Kinect 开发 —— 手势识别(下)
基本手势追踪 手部追踪在技术上和手势识别不同,但是它和手势识别中用到的一些基本方法是一样的.在开发一个具体的手势控件之前,我们先建立一个可重用的追踪手部运动的类库以方便我们后续开发.这个手部追踪类库包 ...
- Kinect 开发 —— 语音识别(下)
使用定向麦克风进行波束追踪 (Beam Tracking for a Directional Microphone) 可以使用这4个麦克风来模拟定向麦克风产生的效果,这个过程称之为波束追踪(beam ...
- Kinect 开发 —— 面部追踪
SDK1.5中新增了人脸识别类库:Microsoft.Kinect.Toolkit.FaceTracking使得在Kinect中进行人脸识别变得简单,该类库的源代码也在Developer Toolki ...
- Kinect 开发 —— 骨骼数据与彩色影像和深度影像的对齐
在显示彩色影像和深度影像时最好使用WriteableBitmap对象: 要想将骨骼数据影像和深度影像,或者彩色影像叠加到一起,首先要确定深度影像的分辨率和大小,为了方便,这里将深度影像数据和彩色影像数 ...
- ]Kinect for Windows SDK开发入门(六):骨骼追踪基础 上
原文来自:http://www.cnblogs.com/yangecnu/archive/2012/04/06/KinectSDK_Skeleton_Tracking_Part1.html Kinec ...
- Kinect 开发 —— 近距离探测
如何将Kinect设备作为一个近距离探测传感器.为了演示这一点,我们处理的场景可能在以前看到过.就是某一个人是否站在Kinect前面,在Kinect前面移动的是人还是什么其他的物体.当我们设置的触发器 ...
随机推荐
- c# IndexOf()用法
IndexOf()用法 查找字符串中字符或者字符串首次出现的位置,返回的是索引值; str1.indexOf('字');//查找“字”在字符串中首次出现的索引值 str1.indexOf(" ...
- 一篇文章助你理解Python3中字符串编码问题
前几天给大家介绍了unicode编码和utf-8编码的理论知识,以及Python2中字符串编码问题,没来得及上车的小伙伴们可以戳这篇文章:浅谈unicode编码和utf-8编码的关系和一篇文章助你理解 ...
- 【agc004f】Namori Grundy
那个问一下有人可以解释以下这个做法嘛,看不太懂QwQ~ Description 有一个n个点n条边的有向图,点的编号为从1到n. 给出一个数组p,表明有(p1,1),(p2,2),…,(pn,n)这n ...
- linux驱动编译时候出现的问题
1.在编译驱动的时候,提示错误,找不到<asm/xxxx.h>这些类的头文件? 答:因为在内核编译的时候,会在内核目录的include中创建一个asm文件再软链接到对应的一些架构.比如我当 ...
- python Web抓取(一)[没写完]
需要的模块: python web抓取通过: webbrowser:是python自带的,打开浏览器获取指定页面 requests:从因特网上下载文件和网页 Beautiful Soup:解析HTML ...
- 紫书 例题 10-9 UVa 1636 (概率计算)
小学数学问题 记得分数比较的时候可以交叉相乘(同号) #include<cstdio> #include<cstring> #define REP(i, a, b) for(i ...
- 【SRM 717 div2 B】LexmaxReplace
Problem Statement Alice has a string s of lowercase letters. The string is written on a wall. Alice ...
- CJOI 05新年好 (最短路+枚举)
CJOI 05新年好 (最短路+枚举) 重庆城里有n个车站,m条双向公路连接其中的某些车站.每两个车站最多用一条公路连接,从任何一个车站出发都可以经过一条或者多条公路到达其他车站,但不同的路径需要花费 ...
- resin后台输出中文乱码的解决的方法!
近期从tomcat移植到resin,发现这东西不错啊! 仅仅是后台输出时有时候中文会乱码. 如今找到resin后台输出中文乱码的解决的方法: 编辑conf/resin.con文件: <!--ja ...
- CentOS用rpm升级glibc
CentOS用rpm升级glibc #! /bin/sh # update glibc to 2.23 for CentOS 6 wget http://cbs.centos.org/kojifile ...
