🧎‍♂️

MediaPipeでスクワットを指導してもらうアプリを作る

に公開

みなさんはおなか周りのお肉気になってませんか?私は少し気になってます。
スクワットは脂肪燃焼にいいと聞いたことがあるのでスクワットをしようと思いました。
しかし、スクワットってどうやるんだろう・深さがわからないなど疑問がありますよね。
そこで私は昔使ったことのあるMediaPipeを使って指導してくれる先生を作ることにしました。

1.指導のキモ! MediaPipeとは?

今回「先生」になってもらうのは、Googleが開発した「MediaPipe」というAIライブラリです。

これは、難しいAIの知識がなくても、Webカメラの映像を渡すだけで、リアルタイムに 人間の骨格(Pose) や手の形(Hands)、顔(Face Mesh)を検出してくれるスグレモノです。

今回はこの中の MediaPipe Pose という機能を使います。 これにWebカメラの映像を見せるだけで、スクワワットに必要な 「左腰」「左膝」「左足首」の3つの点の座標(位置)を瞬時に教えてくれます。

2.どうやって「指導」するのか?(回数カウントの仕組み)

先生はただ回数を数えるだけではありません。「正しいフォーム」を指導してくれます。 今回は、「しゃがむ深さ」に注目しました。

MediaPipeから教えてもらった「腰」「膝」「足首」の3つの点の座標を使います。

「膝」を頂点として、「太もも(腰-膝)」と「ふくらはぎ(膝-足首)」の間の角度を計算します。

この角度を使って、先生は2つのルールで指導します。

ルール1(しゃがみ判定): 膝の角度が 100度未満 になるまで深くしゃがんだら、「しゃがんだ状態(down)」と認定します。浅いスクワットはカウントしません。

ルール2(立ち上がり判定): 「しゃがんだ状態(down)」から、膝が 160度以上 になるまでしっかり立ち上がったら、「立ち上がった状態(up)」と認定し、ここで初めて「1回」とカウントします。

この仕組みで、浅いスクワットや中途半端な動きを防ぎ、しっかりとした運動だけをカウントする「厳しい先生」ができました。

3.コード

今回作成した squat_counter.py の全コードです。 (※別途 pip install opencv-python mediapipe numpy が必要です)

    Python
    
    import cv2
    import mediapipe as mp
    import numpy as np
    

    def calculate_angle(a, b, c):
        """3つのランドマーク(関節)座標から角度を計算する。
    
        Args:
            a (list): 1番目の点 (x, y座標) (例: 腰)
            b (list): 2番目の点 (x, y座標) (例: 膝)
            c (list): 3番目の点 (x, y座標) (例: 足首)
    
        Returns:
            float: 2つのベクトル (b-a と b-c) がなす角度 (0〜180度)。
                   座標が取得できない場合は0を返す。
        """
        try:
            # numpy配列に変換して計算を容易にする
            a = np.array(a) 
            b = np.array(b) 
            c = np.array(c) 
    
            # ベクトル演算 (bからaへのベクトル、bからcへのベクトル)
            radians = np.arctan2(c[1] - b[1], c[0] - b[0]) - \
                      np.arctan2(a[1] - b[1], a[0] - b[0])
            
            # 角度をラジアンから度に変換
            angle = np.abs(radians * 180.0 / np.pi)
    
            # 角度が180度を超えた場合の補正
            if angle > 180.0:
                angle = 360 - angle
                
            return angle
        except:
            # ランドマークが検出できなかった場合など
            return 0
    

    
    
    # 1. 初期設定
    mp_drawing = mp.solutions.drawing_utils  # 骨格を描画するユーティリティ
    mp_pose = mp.solutions.pose              # MediaPipeのPoseモデル
    
    # カウンターと状態の初期化
    counter = 0 
    stage = "up"  # 現在の状態 (up: 立っている, down: しゃがんでいる)
    
    # Webカメラからの入力を開始
    cap = cv2.VideoCapture(0) # 0はPC内蔵カメラを意味します
    if not cap.isOpened():
        print("エラー: カメラが開きませんでした。")
        exit()
    
    # MediaPipe Poseモデルのインスタンス化
    try:
        with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
            
            # 2. メインループ (カメラが起動している間、繰り返し実行)
            while cap.isOpened():
                ret, frame = cap.read() # カメラから1フレーム(1枚の画像)読み込む
                if not ret:
                    print("カメラフレームの取得に失敗しました。")
                    break
    
                # 映像を左右反転(自撮りモードのようにする)
                frame = cv2.flip(frame, 1)
    
                # 3. MediaPipeでの骨格検出
                image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                image.flags.writeable = False # 処理を高速化
                results = pose.process(image)
                image.flags.writeable = True
                image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    
                # 4. ランドマーク(関節)座標の取得
                try:
                    landmarks = results.pose_landmarks.landmark
                    
                    hip = [landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].x, 
                           landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y]
                    knee = [landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].x, 
                            landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y]
                    ankle = [landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].x, 
                             landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].y]
    
                    # 5. 角度の計算
                    angle = calculate_angle(hip, knee, ankle)
                    
                    # 角度を画面に表示
                    cv2.putText(image, f"Angle: {int(angle)}", 
                                (10, 100), 
                                cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)
    
                    # 6. カウンターロジック(ここが先生の指導!)
                    if angle < 100:
                        stage = "down"
                    if angle > 160 and stage == 'down':
                        stage = "up"
                        counter += 1
                        # print(f"Count: {counter}") 
    
                except:
                    pass
    
                # 7. 画面表示
                cv2.rectangle(image, (0, 0), (280, 73), (245, 117, 16), -1)
                cv2.putText(image, 'REPS', (15, 25), 
                            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1, cv2.LINE_AA)
                cv2.putText(image, str(counter), (10, 60), 
                            cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, cv2.LINE_AA)
                cv2.putText(image, 'STAGE', (130, 25), 
                            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1, cv2.LINE_AA)
                cv2.putText(image, stage, (130, 60), 
                            cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, cv2.LINE_AA)
    
                # 骨格を描画
                mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
    
                # 結果をウィンドウに表示
                cv2.imshow('Mediapipe Squat Counter', image)
    
                # 8. 終了処理 ('q' キーが押されたらループを抜ける)
                if cv2.waitKey(10) & 0xFF == ord('q'):
                    break
    
    except Exception as e:
        print(f"MediaPipe Poseの初期化中または実行中にエラーが発生しました: {e}")
    
    finally:
        # 3. 後処理
        cap.release() # カメラを解放
        cv2.destroyAllWindows() # すべてのウィンドウを閉じる
        print("プログラムを終了します。")

4.実際に動かしてみた

こんな感じで、カメラに向かって横向きになってスクワットをすると、左上に回数(REPS)と現在の状態(STAGE)、膝の角度(Angle)が表示されます。(画面に映りながらスクリーンショットできなかったのでイメージ画像です。)

5.今後の展望とまとめ

ひとまず「しゃがむ深さ」を指導してくれる先生はできましたが、スクワットは奥が深いです。 今後は、先生をもっと強化していきたいです。

背中の曲がりチェック: 「腰」「肩」「耳」の角度を計算して、「背中が丸まっています!」と警告してくれる機能。

膝の向きチェック(ニーイン): カメラに正面を向いたときに、「膝が内側に入っています!」と注意してくれる機能。

応援・賞賛機能: 最初にやりたかったことですが、キャラクターが「その調子!」「もっと深く!」「あと3回!」「よく頑張った!!」と応援・賞賛してくれる機能。

環境構築では(それぞれのバージョンの不一致など)苦戦しましたが、無事に動いてよかったです。 まずはこの先生と一緒に、おなか周りのお肉撃退を頑張ろうと思います!この記事を見ていただいたあなたもおなか周りのお肉撃退頑張りましょう!

Discussion