http://www.ituring.com.cn/article/196144
作者/ 吴国斌
博士,PMP,微软亚洲研究院学术合作经理。负责中国高校及科研机构Kinect for Windows学术合作计划及微软精英大挑战Kinect主题项目。曾担任微软TechEd2011 Kinect论坛讲师,微软亚洲教育高峰会Kinect分论坛主席,中国计算机学会学科前沿讲习班Kinect主题学术主任。
骨骼追踪技术是Kinect的核心技术,它可以准确标定人体的20个关键点,并能对这20个点的位置进行实时追踪。利用这项技术,可以开发出各种基于体感人机交互的有趣应用。
骨骼追踪数据的结构
目前,Kinect for Windows SDK中的骨骼API可以提供位于Kinect前方至多两个人的位置信息,包括详细的姿势和骨骼点的三维坐标信息。另外,Kinect for Windows SDK最多可以支持20个骨骼点。数据对象类型以骨骼帧的形式提供,每一帧最多可以保存20个点,如图1所示。
图1 20个骨骼点示意图
在SDK中每个骨骼点都是用Joint
类型来表示的,每一帧的20个骨骼点组成基于Joint
类型的集合。此类型包含3个属性,具体内容如下所示。
-
JointType
:骨骼点的类型,这是一种枚举类型,列举出了20个骨骼点的特定名称,比如“HAND_LEFT”表示该骨骼点是左手节点。 -
Position
:SkeletonPoint
类型表示骨骼点的位置信息。SkeletonPoint
是一个结构体,包含X、Y、Z三个数据成员,用以存储骨骼点的三维坐标。 -
TrackingState
:JointTrackingState
类型也是一种枚举类型,表示该骨骼点的追踪状态。其中,Tracked
表示正确捕捉到该骨骼点,NotTracked
表示没有捕捉到骨骼点,Inferred
表示状态不确定。
半身模式
如果应用程序只需要捕捉上半身的姿势动作,就可以采用Kinect for Windows SDK提供的半身模式(Seated Mode)。在半身模式下,系统只捕捉人体上半身10个骨骼点的信息,而忽略下半身另外10个骨骼点的位置信息,这样就解决了用户坐在椅子上时无法被Kinect识别的问题,即使下半身骨骼点的数据不稳定或是不存在也不会对上半身的骨骼数据造成影响。而且当用户距离Kinect设备只有0.4米时,应用程序仍能正常地进行骨骼追踪,这就大幅提高了骨骼追踪的性能。
半身模式定义在枚举类型SkeletonTrackingMode
中,该类型包含两个枚举值:Default和Seated。前者为默认的骨骼追踪模式,会正常捕捉20个骨骼点;后者为半身模式,选择该值则只捕捉上半身的10个骨骼点。
开发者可以通过改变SkeletonStream
对象的TrackingMode
属性来设置骨骼追踪的模式,代码如下:
kinectSensor.SkeletonStream.TrackingMode = SkeletonTrackingMode.Seated;
骨骼追踪数据的获取方式
应用程序获取下一帧骨骼数据的方式同获取彩色图像和深度图像数据的方式一样,都是通过调用回调函数并传递一个缓存实现的,获取骨骼数据调用的是OpenSkeletonFrame()
函数。如果最新的骨骼数据已经准备好了,那么系统就会将其复制到缓存中;但如果应用程序发出请求时,新的骨骼数据还未准备好,此时可以选择等待下一个骨骼数据直至其准备完毕,或者立即返回稍后再发送请求。对于NUI骨骼API而言,相同的骨骼数据只会提供一次。
NUI骨骼API提供了两种应用模型,分别是轮询模型和时间模型,简要介绍如下。
-
轮询模型是读取骨骼事件最简单的方式,通过调用
SkeletonStream
类的OpenNextFrame()
函数即可实现。OpenNextFrame()
函数的声明如下所示。public SkeletonFrame OpenNextFrame ( int millisecondsWait )
可以传递参数指定等待下一帧骨骼数据的时间。当新的数据准备好或是超出等待时间时,
OpenNextFrame()
函数才会返回。 -
时间模型以事件驱动的方式获取骨骼数据,更加灵活、准确。应用程序传递一个事件处理函数给
SkeletonFrameReady
事件,该事件定义在KinectSensor
类中。当下一帧的骨骼数据准备好时,会立即调用该事件回调函数。因此Kinect应用应该通过调用OpenSkeletonFrame()
函数来实时获取骨骼数据。
实例——调用API获取骨骼数据并实时绘制
本实例程序将实现获取骨骼数据,然后将骨骼点的坐标作为Ellipse控件的20个位置坐标,同时用线段将相应的点连接起来,最后将绘制出的骨架映射到彩色图像上。读者可以在实例1的基础上开始本实例,具体操作步骤如下所示。
1. 在Window_Loaded()
函数中添加下列骨骼数据流的启动函数,并添加kinectSensor_SkeletonFrameReady
事件处理函数相应的SkeletonFrameReady
事件。
kinectSensor.SkeletonStream.Enable(); kinectSensor.SkeletonFrameReady += new EventHandler<SkeletonFrameReadyEventArgs>(kinectSensor_SkeletonFrameReady);
2. 准备WPF界面。通过以下代码在界面上添加20个小圆点,分别跟踪由Kinect for Windows SDK获取到的人体的20个关键点,并将这20个点标记为不同的颜色。
<Canvas Name="SkeletonCanvas" Visibility="Visible"> <Ellipse Canvas.Left="0" Canvas.Top="0" Height="10" Name="headPoint" Width="10" Fill="Red" /> <Ellipse Canvas.Left="10" Canvas.Top="0" Height="10" Name="shouldercenterPoint" Width="10" Fill="Blue" /> <Ellipse Canvas.Left="20" Canvas.Top="0" Height="10" Name="shoulderrightPoint" Width="10" Fill="Orange" /> …省略中间的Ellipse定义 <Image Canvas.Left="303" Canvas.Top="161" Height="150" Name="image1" Stretch="Fill" Width="200" /> </Canvas>
此时,设计窗口如图2所示。
图2 WPF设计界面
3. 编写kinectSensor_SkeletonFrameReady()
事件处理函数。正确连接Kinect后,当用户站在Kinect前并且Kinect能够正确识别人体时,将触发该事件处理函数,其代码如下:
private void kinectSensor_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e) { using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame()) { if (skeletonFrame != null) { skeletonData = new Skeleton[kinectSensor.SkeletonStream.FrameSkeletonArrayLength]; skeletonFrame.CopySkeletonDataTo(this.skeletonData); Skeleton skeleton = (from s in skeletonData where s.TrackingState == SkeletonTrackingState.Tracked select s).FirstOrDefault(); if (skeleton!=null) { SetAllPointPosition(skeleton); } } } }
上述代码使用LINQ语句来获取TrackingState
等于Tracked
的骨骼数据。目前SDK最多可以追踪两幅骨骼。为了简化起见,本实例只对捕捉到的第一幅骨骼进行追踪和显示。
4. 在Skeleton
对象的Joints
属性集合中保存了所有骨骼点的信息,每个骨骼点的信息都是一个Joint
对象。为了得到特定的骨骼点,同样使用LINQ语句对Joint
的JointType
属性进行筛选,相关代码如下:
Joint headJoint = (from j in skeleton.Joints where j.JointType == JointType.Head select j).FirstOrDefault();
在本实例程序中,需要遍历每个骨骼点,并分别对其进行处理。这里使用foreach
语句来实现,并根据JointType
属性进行处理。在SetAllPointPosition()
函数中可以看到具体的实现细节。
foreach (Joint joint in skeleton.Joints) { Point jointPoint = GetDisplayPosition(joint); switch (joint.JointType) { case JointType.Head: SetPointPosition(headPoint, joint); headPolyline.Points.Add(jointPoint); break; ... } }
5. 前面提到,Joint
的Position
属性的X、Y、Z表示该骨骼点的三维位置,其中X和Y的范围都是-1~1,而Z是Kinect到识别物体的距离。
为了能更好地将这20个点显示出来,需要对Position
的X值和Y值进行缩放,可以通过以下函数实现。
private Point GetDisplayPosition(Joint joint) { var scaledJoint = joint.ScaleTo(640, 480); return new Point(scaledJoint.Position.X, scaledJoint.Position.Y); }
上面语句中,ScaleTo
函数的最后两个参数640和480分别代表原始数据X和Y的最大值,通过该语句可以将X坐标放大到0~640范围内的任意值,将Y坐标放大到0~480范围内的任意值。该坐标是相对于应用程序窗口的左上角(0,0)而言的,窗口的宽和高分别是640和480,以保证彩色图像和骨骼绘制的结果相匹配。
其中,ScaleTo()
函数是Coding4Fun
的Help
类中的方法。Coding4Fun
是一个Kinect开发辅助类库。读者可以从http://c4fkinect.codeplex.com/下载该类库,并通过“Add Reference”菜单项将Coding4Fun.Kinect.Wpf.dll添加到项目中。
6. 编写一个函数,将每个骨骼点转换后的(X,Y)坐标值分别映射到相应的Ellipse控件的Left
和Top
属性上,其代码如下:
private void SetPointPosition(FrameworkElement ellipse, Joint joint) { var scaledJoint = joint.ScaleTo(640, 480); Canvas.SetLeft(ellipse, scaledJoint.Position.X); Canvas.SetTop(ellipse, scaledJoint.Position.Y); SkeletonCanvas.Children.Add(ellipse); }
使用Polyline
类表示骨架线,显而易见,骨架由5条多段线组成,分别定义它们,并在遍历所有骨骼点时分类存储相应的点。详见SetAllPointPosition()
函数,相关代码如下:
Polyline headPolyline = new Polyline(); Polyline handleftPolyline = new Polyline(); Polyline handrightPolyline = new Polyline(); Polyline footleftPolyline = new Polyline(); Polyline footrightPolyline = new Polyline(); private void SetAllPointPosition(Skeleton skeleton) { SkeletonCanvas.Children.Clear(); headPolyline.Points.Clear(); handleftPolyline.Points.Clear(); handrightPolyline.Points.Clear(); footleftPolyline.Points.Clear(); footrightPolyline.Points.Clear(); foreach (Joint joint in skeleton.Joints) { Point jointPoint = GetDisplayPosition(joint); switch (joint.JointType) { case JointType.Head: SetPointPosition(headPoint, joint); headPolyline.Points.Add(jointPoint); break; case JointType.ShoulderCenter: SetPointPosition(shouldercenterPoint, joint); headPolyline.Points.Add(jointPoint); handleftPolyline.Points.Add(jointPoint); handrightPolyline.Points.Add(jointPoint); break; case JointType.ShoulderLeft: SetPointPosition(shoulderleftPoint, joint); handleftPolyline.Points.Add(jointPoint); break; ... case JointType.FootRight: SetPointPosition(footrightPoint, joint); footrightPolyline.Points.Add(jointPoint); break;