上篇博文已经介绍了用Kinect获取彩色摄像头数据,当然,深景摄像头数据是一样的做法,获取图像数据的处理与其他颜色数据的处理大同小异,在此就不展开说明。本博文主要讨论用Kinect获取骨骼关节数据并绘制成图形。
Kinect SDK 中骨骼追踪有一些和其他对象不一样的对象结构和枚举。 在 SDK 中骨骼追踪相关的内容几乎占据了三分之一的内容,可见 Kinect 中骨骼追踪技术的重要性。下图展示了骨骼追踪系统中涉及到的一些主要的对象模型。有四个最主要的对象,他们 是SkeletonStream,SkeletonFrame,Skeleton 和 Joint 。下面将介绍这四个对象。
1 SkeletonStream 对象
SkeletonStream 对象 产生 SkeletonFrame 。从 SkeletonStream 获取 骨骼帧 数据和 从ColorStream 及 DepthStream 中获取数据类似。可以注册 SkeletonFrameReady 事件或 者AllFramesReady 事件通过事件模型来获取数据,或者是使用 OpenNextFrame 方法通过“ 拉”模型来获取数据。不能对同一个 SkeletonStream 同时使用这两种模式。如果注册 了SkeletonFrameReady 事 件 然 后 又 调 用 OpenNextFrame 方 法 将 会 返 回 一 个InvalidOperationException 异常。
/// <summary>
/// Active Kinect sensor
/// </summary>
private KinectSensor sensor;
this.sensor.SkeletonStream.Enable();
2 SkeletonFrame
SkeletonStream 产生 SkeletonFrame 对象。 可以使用事 件模型从事 件参数中调 用OpenSkeletonFrame 方法来获取 SkeletonFrame 对象,或者采用”拉” 模型调用 SkeletonStream的 OpenNextFrame 来获取 SkeletonFrame 对象。 SkeletonFrame 对象会存储骨骼数据一段时间。同以通过调用 SkeletonFrame 对象的 CopySkeletonDataTo 方法将其保存的数据拷贝到骨骼对象数组中。SkeletonFrame 对象有一个 SkeletonArrayLength 的属性,这个属性表示追踪到的骨骼信息的个数。
//Open the Skeleton frame
using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame())
{
// check that a frame is available
if (skeletonFrame != null)
{
// get the skeleton data
skeletons = new Skeleton[skeletonFrame.SkeletonArrayLength];
// get the skeletal information in this frame
skeletonFrame.CopySkeletonDataTo(skeletons);
}
}
3 Skeleton
Skeleton 类定义了一系列字段来描述骨骼信息, 包括描述骨骼的位置以及骨骼中关节可能的位置信息。骨骼数据可以通过调用 SkeletonFrame 对象的 CopySkeletonDataTo 方法获得Skeleton 数组。CopySkeletonDataTo 方法有一些不可预料的行为,可能会影响内存使用和其引用的骨骼数组对象。产生的每一个骨骼数组对象数组都是唯一的。
例如:
Skeleton [] skeletonA = new Skeleton [frame.SkeletonArrayLength];
Skeleton [] skeletonB = new Skeleton [frame.SkeletonArrayLength];
frame.CopySkeletonDataTo(skeletonA);
frame.CopySkeletonDataTo(skeletonB);
Boolean resultA = skeletonA[0] == skeletonB[0]; //false
Boolean resultB = skeletonA[0].TrackingId == skeletonB[0].TrackingId;//true
上面的代码可以看出, 使用 CopySkeletonDataTo 是深拷贝对象, 会产生两个不同的 Skeleton
数组对象。
几个主要的属性:TrackingID,TrackingState, Position,ClippedEdges,Joints
TrackingID
骨骼追踪引擎对于每一个追踪到的游戏者的骨骼信息都有一个唯一编号。当 Kinect 追踪到了一个新的游戏者,他会为其分配一个新的唯一编号,编号值为0表示这个骨骼信息不是游戏者的,他在集合中仅仅是一
个占位符。
TrackingState
该字段表示当前的骨骼数据的状态。下表展示了 SkeletonTrackingState 枚举的可能值机
器含义:
Position
Position 一个 SkeletonPoint 类型的字段,代表所有骨骼的中间点。身体的中间点和脊柱
关节的位置相当。
ClippedEdges
ClippedEdges 字段用来描述追踪者的身体哪部分位于 Kinect 的视野范围外。 他大体上提
供了一个追踪这的位置信息。下面列出了 FrameEdges 的所有可能的值。
Joints
每一个骨骼对象都有一个 Joints 字段。 该字段是一个 JointsCollection 类型, 它存储了一些列
的 Joint 结构来描述骨骼中可追踪的关节点( 如 head,hands,elbow 等等 ) 。应用程序使用
JointsCollection 索引获取特定的关节点, 并通过节点的 JointType 枚举来过滤指定的关节点。
即使 Kinect 视场中没有游戏者 Joints 对象也被填充。
4 Joint
骨骼追踪引擎能够跟踪和获取每个用户的近20个点或者关节点信息。 追踪的数据以关节点数
据展现,它有三个属性。JointType 属性是一个枚举类型。下图描述了可追踪的所有关节点。
Kinect神图:
每一个关节点都有类型为 SkeletonPoint 的 Position 属性,他通过 X,Y,Z 三个值来描述关节点的控件位置。 X,Y 值是相对于骨骼平面空间的位置,他和深度影像, 彩色影像的空间坐标系不一样。KinectSnesor 对象有一些列的坐标转换方法,可以将骨骼坐标点转换到对应的深度数据影像中去。 最后每一个 Skeleton 对象还有一个 JointTrackingState 属性, 他描述了该关节点的跟踪状态及方式,下面列出了所有的可能值。
获取骨骼数据后,就可以绘制火柴人啦,逻辑很简单,将每个关节作一个一个节点,按照人体结构,将具有相对旋转运动的关节运动连接起来就OK啦
首先,我我们要获取Kinect传感器中的骨骼数据。步骤如下
搜寻一台可用的Kinect sensor
foreach (var potentialSensor in KinectSensor.KinectSensors)
{
if (potentialSensor.Status == KinectStatus.Connected)
{
this.sensor = potentialSensor;
break;
}
}
启用骨骼数据流
this.sensor. SkeletonStream.Enable()
注册FrameReady处理函数
this.sensor.SkeletonFrameReady += this.Sensor.SkeletonFrameReady;
开始捕获数据
try
{
this.sensor.Start();
}
catch (IOException)
{
this.sensor = null;
}
然后,要将获得的骨骼对象关节点的信息转换成UI绘图坐标系统的坐标,定义一个方法以骨骼作为参数,然后调用 KinectSensor 对象的 CoordinateMapper.MapSkeletonPointToDepth 方法将骨骼坐标转换到深度影像坐标上去。因为骨骼坐标系和深度坐标及彩色影像坐标系不一样,甚至和 UI 界面上的坐标系不一样。
SkeletonPointToScreen方法的目的就是将骨骼关节点转换到 UI 绘图坐标系统,返回该骨骼关节点在 UI 上的位置
/// <summary>
/// Maps a SkeletonPoint to lie within our render space and converts to Point
/// </summary>
/// <param name="skelpoint">point to map</param>
/// <returns>mapped point</returns>
private Point SkeletonPointToScreen(SkeletonPoint skelpoint)
{
// Convert point to depth space.
// We are not using depth directly, but we do want the points in our 640x480 output resolution.
DepthImagePoint depthPoint = this.sensor.CoordinateMapper.MapSkeletonPointToDepthPoint(skelpoint, DepthImageFormat.Resolution640x480Fps30);
return new Point(depthPoint.X, depthPoint.Y);
}
然后定义一个画骨骼的方法DrawBone,以骨骼对象,画布,和2个关节点为参数。关节点之间连线
/// <summary>
/// Draws a bone line between two joints
/// </summary>
/// <param name="skeleton">skeleton to draw bones from</param>
/// <param name="g">drawing context to draw to</param>
/// <param name="jointType0">joint to start drawing from</param>
/// <param name="jointType1">joint to end drawing at</param>
private void DrawBone(Skeleton skeleton, Graphics g, JointType jointType0, JointType jointType1)
{
Joint joint0 = skeleton.Joints[jointType0];
Joint joint1 = skeleton.Joints[jointType1];
// If we can't find either of these joints, exit
if (joint0.TrackingState == JointTrackingState.NotTracked ||
joint1.TrackingState == JointTrackingState.NotTracked)
{
return;
}
// Don't draw if both points are inferred
if (joint0.TrackingState == JointTrackingState.Inferred &&
joint1.TrackingState == JointTrackingState.Inferred)
{
return;
}
// We assume all drawn bones are inferred unless BOTH joints are tracked
Pen drawPen = this.inferredBonePen;
if (joint0.TrackingState == JointTrackingState.Tracked && joint1.TrackingState == JointTrackingState.Tracked)
{
drawPen = this.trackedBonePen;
}
g.DrawLine(drawPen, this.SkeletonPointToScreen(joint0.Position), this.SkeletonPointToScreen(joint1.Position));
}
再然后就是火柴人啦,定义一个画火柴人的方法DrawBonesAndJoints,以骨骼对象作为参数,将有关联运动的关节点调用DrawBone方法连接起来,并在每个关节点上画一个圆。
/// <summary>
/// Draws a skeleton's bones and joints
/// </summary>
/// <param name="skeleton">skeleton to draw</param>
/// <param name="g">Graphics to draw to</param>
private void DrawBonesAndJoints(Skeleton skeleton, Graphics g)
{
// Render Torso
this.DrawBone(skeleton, g, JointType.Head, JointType.ShoulderCenter);
this.DrawBone(skeleton, g, JointType.ShoulderCenter, JointType.ShoulderLeft);
this.DrawBone(skeleton, g, JointType.ShoulderCenter, JointType.ShoulderRight);
this.DrawBone(skeleton, g, JointType.ShoulderCenter, JointType.Spine);
this.DrawBone(skeleton, g, JointType.Spine, JointType.HipCenter);
this.DrawBone(skeleton, g, JointType.HipCenter, JointType.HipLeft);
this.DrawBone(skeleton, g, JointType.HipCenter, JointType.HipRight);
// Left Arm
this.DrawBone(skeleton, g, JointType.ShoulderLeft, JointType.ElbowLeft);
this.DrawBone(skeleton, g, JointType.ElbowLeft, JointType.WristLeft);
this.DrawBone(skeleton, g, JointType.WristLeft, JointType.HandLeft);
// Right Arm
this.DrawBone(skeleton, g, JointType.ShoulderRight, JointType.ElbowRight);
this.DrawBone(skeleton, g, JointType.ElbowRight, JointType.WristRight);
this.DrawBone(skeleton, g, JointType.WristRight, JointType.HandRight);
// Left Leg
this.DrawBone(skeleton, g, JointType.HipLeft, JointType.KneeLeft);
this.DrawBone(skeleton, g, JointType.KneeLeft, JointType.AnkleLeft);
this.DrawBone(skeleton, g, JointType.AnkleLeft, JointType.FootLeft);
// Right Leg
this.DrawBone(skeleton, g, JointType.HipRight, JointType.KneeRight);
this.DrawBone(skeleton, g, JointType.KneeRight, JointType.AnkleRight);
this.DrawBone(skeleton, g, JointType.AnkleRight, JointType.FootRight);
// Render Joints
foreach (Joint joint in skeleton.Joints)
{
Pen drawPen = null;
if (joint.TrackingState == JointTrackingState.Tracked)
{
drawPen = this.trackedJointPen;
}
else if (joint.TrackingState == JointTrackingState.Inferred)
{
drawPen = this.inferredJointPen;
}
if (drawPen != null)
{
g.DrawEllipse(drawPen,this.SkeletonPointToScreen(joint.Position).X, this.SkeletonPointToScreen(joint.Position).Y, (int)JointThickness, (int)JointThickness);
}
}
}
最后就是调用画火柴人的方法了,为了界面不闪烁,将动作分解为位图一张张地画在窗体上。
//Initialize background panel
Image myImage = new Bitmap(panel1.Width, panel1.Height);
// Create the Graphics we'll use for drawing
Graphics g = Graphics.FromImage(myImage);
if (skeletons.Length != 0)
{
foreach (Skeleton skel in skeletons)
{
if (skel.TrackingState == SkeletonTrackingState.Tracked)
{
g.Clear(this.panel1.BackColor);
this.DrawBonesAndJoints(skel, g);
//为了界面不闪,将动作以一张张位图的形式显现
Graphics gg = panel1.CreateGraphics();
gg.DrawImage(myImage, 0, 0);
}
else if (skel.TrackingState == SkeletonTrackingState.PositionOnly)
{
g.DrawEllipse(
this.centerPointPen,
this.SkeletonPointToScreen(skel.Position).X, this.SkeletonPointToScreen(skel.Position).Y,
BodyCenterThickness,
BodyCenterThickness);
Graphics gg = panel1.CreateGraphics();
gg.DrawImage(myImage, 0, 0);
}
}
}
}
运行示例:
友情提示:该项目需要有Kinect SDK的支持才能运行。
Kinect SDK下载地址:http://www.microsoft.com/en-us/kinectforwindowsdev/default.aspx
程序所有源代码和项目已打包上传到:
http://download.csdn.net/detail/u011458382/6348531
下一篇博客分析Kinect的简单姿势识别,
传送门:http://yacare.iteye.com/blog/1950112
希望大家多多支持和指正。
Kinect开发系列博文:
Kinect开发之 Interaction交互设计
http://yacare.iteye.com/blog/1979683
Kinect开发之结合Unity3D进行游戏应用开发
http://yacare.iteye.com/blog/1950164
Kinect开发之体感举起手来程序设计(Kinect俄罗斯方块)
http://yacare.iteye.com/blog/1950133
Kinect开发之简单姿势识别
http://yacare.iteye.com/blog/1950112
Kinect开发之获取骨骼关节数据并绘制成火柴人
http://yacare.iteye.com/blog/1950085
Kinect开发之获取彩色摄像头数据
http://yacare.iteye.com/blog/1921786