姿势分类选项

借助机器学习套件姿势检测 API,您可以通过检查各个身体部位的相对位置来推导对姿势有意义的解读。本页面演示了一些示例。

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

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

在本部分中,我们将介绍如何使用 MediaPipe Colab 构建自定义姿势分类器,并在机器学习套件示例应用中演示了有效的分类器。

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

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

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

1. 收集图片样本

我们从各种来源收集了目标练习的图片样本。我们为每个练习选择了几百张图片,例如针对俯卧撑的“向上”和“向下”位置。收集涵盖不同相机角度、环境条件、身体形状和锻炼变体的样本非常重要。

图 1. 上下俯卧撑姿势位置

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

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

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

为了将姿态地标转换为特征向量,我们使用预定义的姿态关节列表之间的两对距离,例如手腕与肩部、脚踝和臀部以及左右手腕之间的距离。由于图像的比例可能会变化,因此我们在转换地标之前将姿态归一化,以具有相同的躯干大小和垂直的躯干方向。

3. 训练模型并重复统计

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

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

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

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

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

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

  • 从 GitHub 获取 ML Kit Android 快速入门应用项目,确保它能够正常构建和运行。
  • 前往 LivePreviewActivity,从“设置”页面启用姿势检测 Run classification。现在,您应该能够对俯卧撑和下蹲进行分类。

添加您自己的 CSV

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

请注意,如果样本不足,则分类可能会效果不佳。 通常,每个姿势类大约需要 100 个样本。

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

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

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

图 3. 解读姿势

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

通过计算不同关节角度,您可以识别瑜伽姿势。例如,下面的图 2 显示了 Warrior 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]];