姿势(POSE)识别方案(含简单算法)
简析:身体以及各个关节点的位置定义了一个姿势。更具体的来说,是某些关节点相对于其他关节点的位置定义了一个姿势。 姿势的类型和复杂度决定了识别算法的复杂度。 通过关节点位置的交叉或者关节点之间的角度都可以进行姿势识别。
通过关节点交叉进行姿势识别就是对关节点进行命中测试。在上一篇博文中,我们可以确定某一个关节点的位置是否在 UI 界面上某一个可视化元素的有效范围内。我们可以对关节点做同样的测试。 但是需要的工作量要少的多, 因为所有的关节点都是在同一个坐标空间中, 这使得计算相对容易。 例如叉腰动作(hand-on-hip) , 可以从骨骼追踪的数据获取左右髋关节和左右手的位置。然后计算左手和左髋关节的位置。如果这个距离小于某一个阈值,就认为这两个点相交。那么,这样就变得简单多了。
但是,由于Kinect的精度问题,但即使通过一些平滑参数设置,从 Kinect 中获取的关节点数据要完全匹配也不太现实。另外,不可能期望用户做出一些连贯一致的动作, 或者保持一个姿势一段时间。 简而言之, 用户运动的精度以及数据的精度使得这种简单计算不适用。 因此, 计算两个点的长度, 并测试长度是否在一个阈值内是唯一的选择。角度原理也与之类似。
当两个关节点比较接近时, 会导致关节点位置精度进一步下降, 这使得使用骨骼追踪引擎判断一个关节点的开始是否是另一个关节点的结束点变得困难。比如,很难将手放在脸的前面,手放在头上, 和手捂住耳朵这几个姿势区分开来。 要摆出一个确切的姿势也很困难, 用户是否会按照程序显示的姿势来做也是一个问题。
一些姿势使用其他方法识别精度会更高。例如,用户伸开双臂和肩膀在一条线上这个姿势,称之为 T 姿势。可以使用节点相交技术,判断手、肘、以及肩膀是否在 Y 轴上处于近乎相同的位置。另一种方法是计算某些关节点连线之间的角度。骨骼追踪引擎能够识别多达20个关节点数据。任何三个关节点就可以组成一个三角形。使用三角几何就可以计算出他们之间的角度。
实际上我们只需要根据两个关节点即可绘制一个三角形, 第三个点有时候可以这两个关节点来决定的。 知道每个节点的坐标就可以计算每个边长的值。 然后使用余弦定理就可以计算出角度了。公式如下图
为了演示使用关节点三角形方法来识别姿势, 考虑在健美中常看到了展示肱二头肌姿势。用户肩部和肘在一条线上并且和地面平行,手腕与肘部与胳膊垂直。在这个姿势中,可以很容易看到有一个直角或者锐角三角形。 我们可以使用上面所说的方法来计算三角形的每一个角度,如下图所示:
上图中, 组成三角形的三个关节点为。肩膀, 轴和手腕。根据这三个关节点的坐标可以计算三个角度。
有两种使用节点三角形的方法。 最明显的如上面的例子那样, 使用三个节点来构造一个三角形。 另一个方法就是使用两个节点, 第三个节点手动指定一个点。 这种方法取决于姿势的限制和复杂度。 在上面的例子中, 我们使用三个及节点的方法, 因为需要的角度可以由手腕-肘- 肩部构成。不论其他部位如何变化,这三者所构成的三角形相形状相对不变。
此文论述第二种方法,因为方法简单明了,不需太多的计算,方便以后开发调用。
获取每一个节点在主 UI 布局空间中的坐标的方法,由于Kinect精确度的原因和便于日后进一步开发,所以添加一个位置偏移坐标offset。
////// 获取每一个节点在主 UI 布局空间中的坐标的方法 /// /// /// /// /// ///private static Point GetJointPoint(KinectSensor kinectDevice, Joint joint, Point offset) { //得到节点在主 UI 布局空间中的坐标 //DepthImagePoint point = kinectDevice.MapSkeletonPointToDepth(joint.Position, kinectDevice.DepthStream.Format); DepthImagePoint point = kinectDevice.CoordinateMapper.MapSkeletonPointToDepthPoint(joint.Position, kinectDevice.DepthStream.Format); point.X = (int)(point.X - offset.X); point.Y = (int)(point.Y - offset.Y); return new Point(point.X, point.Y); }
利用数学算法,计算两关节点与水平X轴间的夹角。如下图
关键代码如下:
////// 计算2关节点之间的夹角 /// /// /// ///private double GetJointAngle(Joint centerJoint, Joint angleJoint) { Point primaryPoint = GetJointPoint(this.kinect, centerJoint, new Point()); Point anglePoint = GetJointPoint(this.kinect, angleJoint, new Point()); Point x = new Point(primaryPoint.X + anglePoint.X, primaryPoint.Y); double a; double b; double c; a = Math.Sqrt(Math.Pow(primaryPoint.X - anglePoint.X, 2) + Math.Pow(primaryPoint.Y - anglePoint.Y, 2)); b = anglePoint.X; c = Math.Sqrt(Math.Pow(anglePoint.X - x.X, 2) + Math.Pow(anglePoint.Y - x.Y, 2)); double angleRad = Math.Acos((a * a + b * b - c * c) / (2 * a * b)); double angleDeg = angleRad * 180 / Math.PI; //如果计算角度大于180度,将其转换到0-180度 if (primaryPoint.Y < anglePoint.Y) { angleDeg = 360 - angleDeg; } return angleDeg; }
检测并判断姿势:循环遍历,直到找到满足条件的角度值。
////// 判断与指定姿势是否匹配的方法 /// /// /// ///private bool IsPose(Skeleton skeleton, Pose pose) { bool isPose = true; double angle; double poseAngle; double poseThreshold; double loAngle; double hiAngle; //遍历一个姿势中所有poseAngle,判断是否符合相应的条件 for (int i = 0; i < pose.Angles.Length && isPose; i++) { poseAngle = pose.Angles[i].Angle; poseThreshold = pose.Angles[i].Threshold; //调用 GetJointAngle 方法来计算两个关节点之间的角度 angle = GetJointAngle(skeleton.Joints[pose.Angles[i].CenterJoint], skeleton.Joints[pose.Angles[i].AngleJoint]); hiAngle = poseAngle + poseThreshold; loAngle = poseAngle - poseThreshold; //判断角度是否在360范围内,如果不在,则转换到该范围内 if (hiAngle >= 360 || loAngle < 0) { loAngle = (loAngle < 0) ? 360 + loAngle : loAngle; hiAngle = hiAngle % 360; isPose = !(loAngle > angle && angle > hiAngle); } else { isPose = (loAngle <= angle && hiAngle >= angle); } } //如果判断角度一致,则返回true return isPose; }