姿势分类选项

借助 ML Kit Pose Detection API,您可以通过检查各个身体部位的相对位置来推导姿势的有意义的解释。本页面展示了一些示例。

使用 k-NN 算法进行姿势分类和重复计数

姿势检测最常见的应用之一是健身跟踪。对开发者来说,构建一个能够识别特定健身姿势并计算重复次数的姿势分类器可能是一项具有挑战性的工作。

在本部分中,我们将介绍如何使用 MediaPipe Colab 构建自定义姿势分类器,并在机器学习套件示例应用中演示一个可正常运行的分类器。

如果您不熟悉 Google Colaboratory,请参阅简介指南

为了识别姿势,我们使用 k 最近邻算法 (k-NN),因为它很容易上手。该算法会根据训练集中最接近的样本来确定对象的类别。

请按照以下步骤构建和训练识别器:

1. 收集图片样本

我们从各种来源收集了目标运动的图像样本。我们为每个运动选择了几百张图片,例如“俯卧撑”和“下俯卧撑”的姿势。请务必收集涵盖不同镜头角度、环境条件、身体形状和运动变化的样本。

图 1. 上下俯卧撑姿势

2. 对示例图片运行姿势检测

这会生成一组用于训练的姿势特征点。我们对姿势检测本身不感兴趣,因为我们将在下一步中训练自己的模型。

我们为自定义姿势分类选择的 k-NN 算法需要每个样本的特征向量表示,以及一个计算两个向量之间距离的指标,以找到最接近姿势样本的目标。这意味着我们必须转换刚刚获取的姿势地标。

为了将姿势地标转换为特征向量,我们使用预定义的姿势关节列表之间的成对距离,例如手腕和肩膀、脚踝和臀部之间的距离,以及左右手腕之间的距离。由于图像的比例可能不同,因此在转换地标之前,我们对姿势进行了归一化处理,使其拥有相同的躯干尺寸和垂直躯干方向。

3. 训练模型并统计重复次数

我们使用 MediaPipe Colab 访问了分类器的代码并训练模型。

为了统计重复次数,我们使用了另一个 Colab 算法来监控目标姿势位置的概率阈值。例如:

  • 当“按下”姿势类的概率首次超过给定阈值时,该算法会将标记标记为“按下”姿势类。
  • 当概率降至阈值以下时,该算法会标记“向下”姿势类已退出,并增加计数器。
图 2. 重复计数示例

4. 与机器学习套件快速入门应用集成

上面的 Colab 会生成一个 CSV 文件,您可以在其中填充所有姿势示例。在本部分中,您将学习如何将 CSV 文件与 Android 版机器学习套件快速入门应用集成,以便实时查看自定义姿势分类。

使用快速入门应用中捆绑的示例尝试姿势分类

  • 从 GitHub 获取机器学习套件 Android 快速入门应用项目,并确保它能够正常运行。
  • 转到 LivePreviewActivity,然后在“设置”页面中启用姿势检测 Run classification。现在,您应该能够对俯卧撑和下蹲进行分类了。

添加您自己的 CSV 文件

  • 将您的 CSV 文件添加到应用的素材资源文件夹中。
  • PoseClassifierProcessor 中,更新 POSE_SAMPLES_FILEPOSE_CLASSES 变量以匹配您的 CSV 文件和姿势样本。
  • 构建并运行应用。

请注意,如果没有足够的样本,分类可能无法正常运行。通常,每个姿势类需要大约 100 个样本。

如需了解详情并亲自尝试,请参阅 MediaPipe ColabMediaPipe 分类指南

通过计算地标距离来识别简单手势

当两个或多个地标彼此靠近时,它们可用于识别手势。例如,当一只手的一个或多个手指的地标靠近鼻子的地标时,您就可以推断出用户最有可能轻触他们的脸部。

图 3. 解读姿势

通过角度启发法识别瑜伽姿势

您可以通过计算各种关节的角度来识别瑜伽姿势。例如,下面的图 2 显示了勇士 II 瑜伽姿势。用于标识此姿势的近似角度用以下代码编写为:

图 4. 将姿势分解成多个角度

这一姿势可描述为以下近似身体部位角的组合:

  • 两侧肩膀 90 度角
  • 双肘 180 度
  • 前腿和腰部呈 90 度角
  • 后膝关节 180 度角
  • 腰部 135 度角

您可以使用姿势地标来计算这些角度。例如,右前腿和腰部的角度是从右肩到右臀的线与从右臀部到右膝的线之间的夹角。

计算出识别姿势所需的全部角度后,您可以检查是否存在匹配项,确认是否就识别出该姿势。

以下代码段演示了如何使用 X 和 Y 坐标来计算两个身体部位之间的角度。这种分类方法存在一些限制。通过只检查 X 和 Y,计算出的角度因对象与相机之间的角度而异。使用水平、直立的正面图片,可以获得最佳结果。您还可以尝试使用 Z 坐标来扩展此算法,看看它能否更适合您的用例。

在 Android 上计算地标角度

以下方法可计算任意三个地标之间的角度。它会确保返回的角度介于 0 度到 180 度之间。

Kotlin

fun getAngle(firstPoint: PoseLandmark, midPoint: PoseLandmark, lastPoint: PoseLandmark): Double {
        var result = Math.toDegrees(atan2(lastPoint.getPosition().y - midPoint.getPosition().y,
                lastPoint.getPosition().x - midPoint.getPosition().x)
                - atan2(firstPoint.getPosition().y - midPoint.getPosition().y,
                firstPoint.getPosition().x - midPoint.getPosition().x))
        result = Math.abs(result) // Angle should never be negative
        if (result > 180) {
            result = 360.0 - result // Always get the acute representation of the angle
        }
        return result
    }

Java

static double getAngle(PoseLandmark firstPoint, PoseLandmark midPoint, PoseLandmark lastPoint) {
  double result =
        Math.toDegrees(
            atan2(lastPoint.getPosition().y - midPoint.getPosition().y,
                      lastPoint.getPosition().x - midPoint.getPosition().x)
                - atan2(firstPoint.getPosition().y - midPoint.getPosition().y,
                      firstPoint.getPosition().x - midPoint.getPosition().x));
  result = Math.abs(result); // Angle should never be negative
  if (result > 180) {
      result = (360.0 - result); // Always get the acute representation of the angle
  }
  return result;
}

以下为计算右臀的角度的方法:

Kotlin

val rightHipAngle = getAngle(
                pose.getPoseLandmark(PoseLandmark.Type.RIGHT_SHOULDER),
                pose.getPoseLandmark(PoseLandmark.Type.RIGHT_HIP),
                pose.getPoseLandmark(PoseLandmark.Type.RIGHT_KNEE))

Java

double rightHipAngle = getAngle(
                pose.getPoseLandmark(PoseLandmark.Type.RIGHT_SHOULDER),
                pose.getPoseLandmark(PoseLandmark.Type.RIGHT_HIP),
                pose.getPoseLandmark(PoseLandmark.Type.RIGHT_KNEE));

在 iOS 上计算地标角度

以下方法可计算任意三个地标之间的角度。它会确保返回的角度介于 0 度到 180 度之间。

Swift

func angle(
      firstLandmark: PoseLandmark,
      midLandmark: PoseLandmark,
      lastLandmark: PoseLandmark
  ) -> CGFloat {
      let radians: CGFloat =
          atan2(lastLandmark.position.y - midLandmark.position.y,
                    lastLandmark.position.x - midLandmark.position.x) -
            atan2(firstLandmark.position.y - midLandmark.position.y,
                    firstLandmark.position.x - midLandmark.position.x)
      var degrees = radians * 180.0 / .pi
      degrees = abs(degrees) // Angle should never be negative
      if degrees > 180.0 {
          degrees = 360.0 - degrees // Always get the acute representation of the angle
      }
      return degrees
  }

Objective-C

(CGFloat)angleFromFirstLandmark:(MLKPoseLandmark *)firstLandmark
                      midLandmark:(MLKPoseLandmark *)midLandmark
                     lastLandmark:(MLKPoseLandmark *)lastLandmark {
    CGFloat radians = atan2(lastLandmark.position.y - midLandmark.position.y,
                            lastLandmark.position.x - midLandmark.position.x) -
                      atan2(firstLandmark.position.y - midLandmark.position.y,
                            firstLandmark.position.x - midLandmark.position.x);
    CGFloat degrees = radians * 180.0 / M_PI;
    degrees = fabs(degrees); // Angle should never be negative
    if (degrees > 180.0) {
        degrees = 360.0 - degrees; // Always get the acute representation of the angle
    }
    return degrees;
}

以下为计算右臀的角度的方法:

Swift

let rightHipAngle = angle(
      firstLandmark: pose.landmark(ofType: .rightShoulder),
      midLandmark: pose.landmark(ofType: .rightHip),
      lastLandmark: pose.landmark(ofType: .rightKnee))

Objective-C

CGFloat rightHipAngle =
    [self angleFromFirstLandmark:[pose landmarkOfType:MLKPoseLandmarkTypeRightShoulder]
                     midLandmark:[pose landmarkOfType:MLKPoseLandmarkTypeRightHip]
                    lastLandmark:[pose landmarkOfType:MLKPoseLandmarkTypeRightKnee]];