姿勢分類選項

有了 ML Kit Pose Detection API,您可以透過檢查不同身體部位的相對位置,取得有意義的姿勢解讀。本頁將示範幾個範例。

使用 k-NN 演算法進行姿勢分類和重複計數

姿勢偵測最常見的用途之一是健身追蹤。對開發人員來說,建構能夠辨識特定健身姿勢並計算重複次數的姿勢分類器對開發人員來說可能是一大挑戰。

在本節中,我們會說明如何使用 MediaPipe Colab 建構自訂姿勢分類器,並在 ML Kit 範例應用程式中示範有效的分類器。

如果不熟悉 Google Colaboratory,請參閱簡介指南

為了辨識姿勢,我們使用 k-Narest eighbors 演算法 (k-NN) 工具,因為它簡單又容易上手。演算法會根據訓練集內最相近的樣本,判斷物件的類別。

請按照下列步驟建構並訓練辨識工具:

1. 收集圖片樣本

我們從多個來源收集目標練習的圖片樣本。我們會為每次運動選擇幾百張圖片,例如為伏地挺身選擇「向上」和「下」的位置。請務必收集涵蓋不同相機角度、環境條件、身體形狀和運動變化的範例。

圖 1.上下伏地挺身姿勢

2. 對範例圖片執行姿勢偵測

這會產生一組要用於訓練的姿勢地標。我們沒有興趣偵測姿勢偵測本身,因為我們會在下一個步驟中訓練自己的模型。

我們針對自訂姿勢分類選擇的 k-NN 演算法需要每個樣本的特徵向量表示法,以及計算兩個向量之間的距離的指標,以便找出最接近姿勢樣本的目標。也就是說,我們必須轉換剛剛取得的姿勢地標。

如要將姿勢地標轉換成特徵向量,我們會使用預先定義的姿勢接頭清單之間的配對距離,例如手腕和肩膀之間的距離、腳踝和臀部,以及左右手腕。由於圖片比例可能不同,在轉換地標之前,我們會先將姿勢正規化為相同的軀幹大小和垂直軀幹。

3. 訓練模型並計算重複次數

我們使用 MediaPipe Colab 存取分類器的程式碼,並訓練模型。

為了計算重複次數,我們使用其他 Colab 演算法來監控目標姿勢的機率門檻。例如:

  • 當「向下」姿勢類別第一次通過特定門檻時,演算法會標記已輸入「向下」姿勢類別。
  • 如果機率降到門檻以下,演算法會標記「向下」姿勢類別已退出,並增加計數器。
圖 2.重複計算示例

4. 與 ML Kit 快速入門導覽課程應用程式整合

上述 Colab 會產生 CSV 檔案,您可以將這個檔案填入所有姿勢範例。本節將說明如何將您的 CSV 檔案與 ML Kit Android 快速入門導覽課程應用程式整合,以便即時查看自訂姿勢分類。

嘗試使用快速入門導覽課程應用程式隨附的範例進行姿勢分類

新增自己的 CSV

  • 將 CSV 檔案加入應用程式的素材資源資料夾。
  • PoseClassifierProcessor 中,更新 POSE_SAMPLES_FILEPOSE_CLASSES 變數以符合您的 CSV 檔案和姿勢範例。
  • 建構並執行應用程式。

請注意,如果樣本數量不足,分類功能可能會無法正常運作。一般來說,每個姿勢類別大約需要 100 個樣本。

如要進一步瞭解並自行嘗試,請參閱 MediaPipe ColabMediaPipe 分類指南

計算地標距離,識別簡單的手勢

當兩個以上的地標彼此相鄰時,就可用於辨識手勢。舉例來說,如果手上一根或多隻手指的地標靠近鼻子的地標,就可推論使用者很可能觸碰人臉。

圖 3.解讀姿勢

使用角度經驗法則辨識瑜珈姿勢

你可以計算各種關節的角度,藉此辨別瑜珈姿勢。例如,下方的圖 2 顯示了勇士二戰瑜珈姿勢。識別此姿勢的近似角度會以下列方式寫入:

圖 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]];