Kinect传感器核心只是发射红外线,并探测红外光反射,从而可以计算出视场范围内每一个像素的深度值。从深度数据中最先提取出来的是物体主体和形状,以及每一个像素点的游戏者索引信息。然后用这些形状信息来匹配人体的各个部分,最后计算匹配出来的各个关节在人体中的位置。这就是我们之前介绍过的骨骼追踪。


用户交互

在底层,鼠标,触摸板或者手写设备都是提供一些X,Y坐标,操作系统将这些X,Y坐标从其在的空间坐标系统转换到计算机屏幕上,这一点和上篇文章讨论的空间变换类似。操作系统的职责是响应这些标准输入设备输入的数据,然后将其转换到图形用户界面或者应用程序中去。操作系统的图形用户界面显示光标位置,并响应用户的输入。在有些时候,这个过程没有那么简单,需要我们了解GUI平台。以WPF应用程序为例,它并没有对Kinect提供像鼠标和键盘那样的原生的支持。这个工作就落到开发者身上了,我们需要从Kinect中获取数据,然后利用这些数据与按钮,下拉框或者其他控件进行交互。根据应用程序或者用户界面的复杂度的不同,这种工作可能需要我们了解很多有关WPF的知识。

WPF 应用程序中输入系统


探测用户的交互


命中测试

Kinect 开发 —— 骨骼追踪进阶(上)-LMLPHP

上图可能帮助我们理解可视元素的分层。图中有三个元素:圆形,矩形和按钮。所有三个元素都在Canvas容器中。圆形和按钮在矩形之上,左边第一幅图中,鼠标位于圆形之上,在这点上的命中测试结果将返回这个圆形。第二幅图,即使矩形最底层,由于鼠标位于矩形上,所以命中测试会返回矩形。这是因为矩形在最底层,他是唯一个占据了鼠标光标象元所在位置的可视化元素。在第三幅图中,光标在按钮的文字上,命中测试将返回TextBlock对象,如果鼠标没有位于按钮的文字上,命中测试将会返回ButtonChrome元素。按钮的可视化表现通常由一个或者多个可视化控件组成,并能够定制。实际上,按钮没有继承可视化样式,它是一个没有可视化表现的控件。上图中的按钮使用的是默认样式,它由TextBlock和ButtonChrome这两个控件构成的。在这个例子中,我们通常会获得到有按钮样式组成的元素,但是永远获取不到实际的按钮控件。

WPF中,命中测试依赖于两个变量,一个是可视化元素,另一个是点。测试首先该点转换到可视化元素所在坐标空间,然后确定是否处于该可视化元素的有效范围内。下图可以更好的理解可视化元素的坐标空间。WPF中的每一个可视化元素,不论其形状和大小,都有一个外轮廓:这个轮廓是一个矩形,它包含可视化元素并定义了可视化元素的宽度和高度。布局系统使用这个外轮廓来确定可视化元素的整体尺寸以及如何将其排列在屏幕上。当开发者使用Canvas,Grid,StackPanel等容器来布局其子元素时,元素的外轮廓是这些容器控件如进行布局计算的基础。用户看不到元素的外轮廓,下图中,可视化元素周围的虚线矩形显示了这些元素的外轮廓。此外,每一个元素有一个X,Y坐标用来指定该元素在其父容器中的位置。可以通过System.Windows.Controls.Primitives命名空间中的LayoutInformation静态类中的GetLayoutSlot方法来获取元素的外轮廓和其位置。举例来说,图中三角形的外轮廓的左上角坐标点为(0,0),三角形的宽和高都是200像素。所以在三角形外轮廓中,三角形的三个点的坐标分别为(100,0),(200,200),(0,200)。并不是在三角形外轮廓中的所有点在命中测试中都会成功,只有在三角形内部的点才会成功。点(0,0)不会命中,而三角形的中心(100,100)则能命中。


“我说你做”游戏

这是个很好的使用Kinect展示如何和用户界面进行交互的例子。这个游戏也有一些规则。下图展示了我们将要做的用户界面,他包含四个矩形,他用来模拟游戏中的按钮。界面上方是游戏标题,中间是游戏的操作指南。

这个Kinect版的Simon says游戏追踪游戏者的手部关节。当用户的手碰到了这四个填充了颜色的方框中的任何一个时,程序认为游戏者按下了一个按钮。在Kinect应用程序中,使用悬停或者点击来和按钮进行交互很常见。现在,我们的游戏操作指南还很简单。游戏一开始,我们提示用户将手放在界面上红色矩形中手势图标所在的位置。在用户将双手放到指定位置后,界面开始发出指令。如果游戏者不能够重复这个过程,游戏将会结束,并返回到这个状态。


用户界面设计

将所有的主界面的UI元素包含在Viewbox容器中,让他来帮助我们进行不同显示器分辨率下面的缩放操作。主UI界面分辨率设置为1920*1080。UI界面共分为4个部分:标题及游戏指导,游戏界面,游戏开始界面以及用来追踪手部的手形图标。第一个TextBlock用来显示标题,游戏引导放在接下来的StackPanel元素中。这些元素是用来给游戏者提供当前游戏状态。他们没有功能性的作用,和Kinect或者骨骼追踪没有关系。

GameCanvas,ControlCanvas和HandCanvas包含了所有的和Kienct相关的UI元素,这些元素是基于当前用户手的位置和用户界面进行交互的。手的位置来自骨骼追踪。HandCanvas应该比较熟悉,程序中有两个手形图标,用来追踪游戏者两只手的运动。ControlCanvas存储的UI元素用来触发开始游戏。GameCanvas用来存储这4个矩形,在游戏中,用户需要点击这些矩形。不同的交互元素存储在不同的容器中,使得用户界面能够比较容易使用代码进行控制。比如,当用户开始游戏后,我们需要隐藏所有的ControlCanvas容器内的子元素,显然隐藏这个容器比隐藏其每个子控件容易的多。


添加游戏基本元素


开始新游戏

ProcessGameOver方法的逻辑简单明了:如果游戏者的任何一只手在UI界面上的对应位置,就切换当前游戏所处的状态。GetHitTarget方法用来测试给定的关节点是否在可视化控件有效范围内。他接受关节点数据和可视化控件,返回该点所在的特定的IInputElement对象。


更改游戏状态

在GameOver状态时,矩形框会渐变消失,然后改变操作指示,显示按钮来开始一个新的游戏。SimonInStructing状态不在更新UI界面讨论范围内,他调用了两个方法,用来产生指令集合 (GenerateInstructions),并将这些指令显示到UI界面上(DisplayInstructions),代码中也定义了instructionPosition变量,来维护当前所完成的指令步骤。


显示Simon的指令


执行 Simon 的指令

注意到当故事版动画完成了显示Simon的指令后,程序调用ChangePhase方法使游戏进入PlayerPerforming阶段。当在PlayerPerforming阶段时,应用程序执行ProcessPlayerPerforming方法。表面上,实现该方法很简单。逻辑是游戏者重复Simon给出的操作步骤,将手放在对应矩形上方。这和之前做的命中测试逻辑是一样的。但是,和测试两个静态的UI对象不同,我们测试指令集合中的下一个指令对应的元素。

需要改进:


namespace SimpleSay
{
public enum GamePhase
{
GameOver = ,
SimonInstructing = ,
PlayerPerforming =
} /// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
/// public partial class MainWindow : Window
{ private KinectSensor kinectDevice;
private Skeleton[] frameSkeletons;
private GamePhase currentPhase; // 所处的当前游戏的状态
private UIElement[] instructionSequence;
private int instructionPosition; //
private int currentLevel; // 描述游戏者成功的次数或者游戏等级
private Random rnd = new Random();
private IInputElement leftHandTarget;
private IInputElement rightHandTarget; public KinectSensor KinectDevice
{
get { return this.kinectDevice; }
set
{
if (this.kinectDevice!=null)
{
// Uninitialize
this.kinectDevice.Stop();
this.kinectDevice.SkeletonFrameReady -= KinectDevice_SkeletonFrameReady;
this.kinectDevice.SkeletonStream.Disable();
this.frameSkeletons = null;
} this.kinectDevice = value;
if (this.kinectDevice!=null)
{
// Initialize
if (this.kinectDevice.Status == KinectStatus.Connected)
{
this.kinectDevice.SkeletonStream.Enable();
this.frameSkeletons = new Skeleton[this.kinectDevice.SkeletonStream.FrameSkeletonArrayLength]; this.kinectDevice.Start(); // SkeletonViewerElement.KinectDevice = this.kinectDevice; // 属性依赖
this.kinectDevice.SkeletonFrameReady += KinectDevice_SkeletonFrameReady;
}
}
}
} public MainWindow()
{
InitializeComponent();
KinectSensor.KinectSensors.StatusChanged += KinectSensors_StatusChanged;
this.KinectDevice = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected); ChangePhase(GamePhase.GameOver);
this.currentLevel = ;
} 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())
{
frame.CopySkeletonDataTo(this.frameSkeletons);
Skeleton skeleton = GetPrimarySkeleton(this.frameSkeletons); if (skeleton == null)
{
ChangePhase(GamePhase.GameOver);
}
else
{
if (this.currentPhase == GamePhase.SimonInstructing)
{
LeftHandElement.Visibility = Visibility.Collapsed;
RightHandElement.Visibility = Visibility.Collapsed;
}
else
{
TrackHand(skeleton.Joints[JointType.HandLeft], LeftHandElement, LayoutRoot);
TrackHand(skeleton.Joints[JointType.HandRight], RightHandElement, LayoutRoot); switch (this.currentPhase)
{
case GamePhase.GameOver:
ProcessGameOver(skeleton);
break;
case GamePhase.PlayerPerforming:
ProcessPlayerPerforming(skeleton);
break;
}
}
}
} } private void TrackHand(Joint hand, FrameworkElement cursorElement, FrameworkElement container)
{
if (hand.TrackingState == JointTrackingState.NotTracked)
{
cursorElement.Visibility = Visibility.Collapsed; // 不显示元素,且不为其保留布局空间
}
else
{
cursorElement.Visibility = Visibility.Visible;
Point jointPoint = GetJointPoint(this.KinectDevice, hand, container.RenderSize, new Point(cursorElement.ActualWidth / 2.0, cursorElement.ActualHeight / 2.0));
Canvas.SetLeft(cursorElement, jointPoint.X);
Canvas.SetTop(cursorElement, jointPoint.Y);
}
} private void ProcessGameOver(Skeleton skeleton)
{
//判断用户是否想开始新的游戏
if (HitTest(skeleton.Joints[JointType.HandLeft], LeftHandStartElement) && HitTest(skeleton.Joints[JointType.HandRight], RightHandStartElement))
{
ChangePhase(GamePhase.SimonInstructing);
}
} private bool HitTest(Joint joint, UIElement target)
{
return (GetHitTarget(joint, target) != null);
} private IInputElement GetHitTarget(Joint joint, UIElement target)
{
Point targetPoint = LayoutRoot.TranslatePoint(GetJointPoint(this.KinectDevice, joint, LayoutRoot.RenderSize, new Point()), target);
return target.InputHitTest(targetPoint); // 返回指定坐标上的当前元素中的输入元素(相对于当前元素的源)
} private static Skeleton GetPrimarySkeleton(Skeleton[] skeletons)
{
Skeleton skeleton = null; if (skeletons != null)
{
//Find the closest skeleton
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 Point GetJointPoint(KinectSensor kinectDevice, Joint joint, Size containerSize, Point offset)
{
DepthImagePoint point = kinectDevice.MapSkeletonPointToDepth(joint.Position, kinectDevice.DepthStream.Format);
point.X = (int)((point.X * containerSize.Width / kinectDevice.DepthStream.FrameWidth) - offset.X);
point.Y = (int)((point.Y * containerSize.Height / kinectDevice.DepthStream.FrameHeight) - offset.Y); return new Point(point.X, point.Y);
} private void ChangePhase(GamePhase newPhase)
{
if (newPhase != this.currentPhase)
{
this.currentPhase = newPhase; switch (this.currentPhase)
{
case GamePhase.GameOver:
this.currentLevel = ;
RedBlock.Opacity = 0.2;
BlueBlock.Opacity = 0.2;
GreenBlock.Opacity = 0.2;
YellowBlock.Opacity = 0.2; GameStateElement.Text = "GAME OVER!";
ControlCanvas.Visibility = Visibility.Visible;
GameInstructionsElement.Text = "将手放在对象上开始新的游戏。";
break; case GamePhase.SimonInstructing:
this.currentLevel++;
GameStateElement.Text = string.Format("Level {0}", this.currentLevel);
ControlCanvas.Visibility = Visibility.Collapsed;
GameInstructionsElement.Text = "注意观察Simon的指示。";
GenerateInstructions();
DisplayInstructions();
break; case GamePhase.PlayerPerforming:
this.instructionPosition = ;
GameInstructionsElement.Text = "请重复 Simon的指示";
break;
}
}
} private void GenerateInstructions()
{
this.instructionSequence = new UIElement[this.currentLevel]; for (int i = ; i < this.currentLevel; i++)
{
switch (rnd.Next(, ))
{
case :
this.instructionSequence[i] = RedBlock;
break; case :
this.instructionSequence[i] = BlueBlock;
break; case :
this.instructionSequence[i] = GreenBlock;
break; case :
this.instructionSequence[i] = YellowBlock;
break;
}
}
} private void DisplayInstructions()
{ // Storyboard 为容器的子动画提供对象和属性目标信息的容器时间线。
Storyboard instructionsSequence = new Storyboard();
DoubleAnimationUsingKeyFrames animation; // 对一组 KeyFrames 中的 Double 属性的值进行动画处理 for (int i = ; i < this.instructionSequence.Length; i++)
{
this.instructionSequence[i].ApplyAnimationClock(FrameworkElement.OpacityProperty, null); animation = new DoubleAnimationUsingKeyFrames();
animation.FillBehavior = FillBehavior.Stop;
animation.BeginTime = TimeSpan.FromMilliseconds(i * );
Storyboard.SetTarget(animation, this.instructionSequence[i]);
Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
instructionsSequence.Children.Add(animation); animation.KeyFrames.Add(new EasingDoubleKeyFrame(0.3, KeyTime.FromTimeSpan(TimeSpan.Zero)));
animation.KeyFrames.Add(new EasingDoubleKeyFrame(, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds())));
animation.KeyFrames.Add(new EasingDoubleKeyFrame(, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds())));
animation.KeyFrames.Add(new EasingDoubleKeyFrame(0.3, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds())));
} instructionsSequence.Completed += (s, e) => { ChangePhase(GamePhase.PlayerPerforming); };
instructionsSequence.Begin(LayoutRoot);
} private void ProcessPlayerPerforming(Skeleton skeleton)
{
//判断用户是否手势是否在目标对象上面,且在指定中的正确顺序
UIElement correctTarget = this.instructionSequence[this.instructionPosition];
IInputElement leftTarget = GetHitTarget(skeleton.Joints[JointType.HandLeft], GameCanvas);
IInputElement rightTarget = GetHitTarget(skeleton.Joints[JointType.HandRight], GameCanvas);
bool hasTargetChange = (leftTarget != this.leftHandTarget) || (rightTarget != this.rightHandTarget); if (hasTargetChange)
{
if (leftTarget != null && rightTarget != null)
{
ChangePhase(GamePhase.GameOver);
}
else if ((leftHandTarget == correctTarget && rightHandTarget == null) ||
(rightHandTarget == correctTarget && leftHandTarget == null))
{
this.instructionPosition++; if (this.instructionPosition >= this.instructionSequence.Length)
{
ChangePhase(GamePhase.SimonInstructing);
}
}
else if (leftTarget != null || rightTarget != null)
{
//Do nothing - target found
}
else
{
ChangePhase(GamePhase.GameOver);
} if (leftTarget != this.leftHandTarget)
{
if (this.leftHandTarget != null)
{
((FrameworkElement)this.leftHandTarget).Opacity = 0.2;
} if (leftTarget != null)
{
((FrameworkElement)leftTarget).Opacity = ;
} this.leftHandTarget = leftTarget;
} if (rightTarget != this.rightHandTarget)
{
if (this.rightHandTarget != null)
{
((FrameworkElement)this.rightHandTarget).Opacity = 0.2;
} if (rightTarget != null)
{
((FrameworkElement)rightTarget).Opacity = ;
} this.rightHandTarget = rightTarget;
}
}
} } }
05-11 10:50
查看更多