【実装編】MediaPipeでリハビリ動画の関節角度を測る方法|Pythonコードをコピペして動作分析

医療AI活用ラボ






【実装編】MediaPipeでリハビリ動画の関節角度を測る方法|Pythonコードをコピペして動作分析










MediaPipe × Python Motion Analysis

動画から関節角度を測る、
「見える」動作分析のはじめ方。

前回の概要編では「スマホ1台でどこまでリハビリの動作分析ができるか」を整理しました。今回はその実装編として、Googleの姿勢推定ライブラリMediaPipeを使い、スマホやWebカメラで撮影した動画から肘・膝・肩・股関節などの角度を自動で計測するPythonコードを、コピペで動く形でまとめています。AIへのプロンプト設計、生成されたコードの読み方、精度に関わる落とし穴まで、実装に必要な流れを一気通貫で解説します。

対象読者: PT・OT・ST/Python初級〜中級者 必要環境: Python 3.9以上、mediapipe / opencv-python / numpy / matplotlib コード: 8本+解説例1本
全コードCOPY対応pip installだけで開始関節角度をCSV自動出力スマホ動画に対応
RELATED ARTICLES

本記事は実装編です。まず全体像を知りたい方は概要編の「スマホ1台でリハビリの動作分析はどこまでできる?現状と始め方」をご覧ください。センサーを併用したい場合はArduino×Python×AIで作るリハビリセンサー自作ガイド、筋電データの可視化にはDelsys筋電CSVをPythonで可視化しAIで解析を加速する実践ガイドも参考になります。

pose_setup.py
import cv2
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
options = vision.PoseLandmarkerOptions(base_options=base_options)
detector = vision.PoseLandmarker.create_from_options(options)
# ここに1フレームずつ関節角度を計算する処理が入る

01

なぜMediaPipeでリハビリの動作分析をするのか

関節可動域(ROM)の評価は、ゴニオメーターを使った目視測定が現場の基本です。ただし目視測定は評価者間でばらつきが生じやすく、実際に理学療法士の視覚的な歩行観察を対象にした研究では、経験年数によらず評価者間の一致度が高くない場合があることが報告されています(Reliability and Validity of Observational Gait Analysis by Physical Therapists ↗)。MediaPipe Pose Landmarkerは、1台のRGBカメラ映像から33点の身体ランドマークをリアルタイムに推定できるGoogleの姿勢推定ソリューション ↗で、特別なマーカーや高価なモーションキャプチャ機材を使わずに、動画から関節角度の推移を数値として残せる点が特長です。

精度についても検証が進んでおり、下肢の矢状面キネマティクスでは3Dモーションキャプチャとの間に高い一致度が確認された研究や、在宅でのリハビリモニタリングにおいて臨床家の測定値と近い傾向を示したとする報告があります。一方で、単眼カメラによる2次元推定にはフレームごとのぶれや遮蔽への弱さがあることも指摘されており(Frailty-Focused Movement Monitoring: A Single-Camera System Using Joint Angles ↗)、「万能の代替」ではなく「目視評価を補う数値記録ツール」として捉えるのが実務的です。この位置づけは、概要編のスマホ1台での動作分析の現状と始め方でも触れた考え方と共通しています。

  • 記録の再現性:動画さえ残せば、同じロジックで何度でも同じ角度を再計算できる
  • 時系列での可視化:1回の測定値ではなく、動作中の角度変化をグラフとして残せる
  • 低コストでの導入:追加のハードウェアなしで、スマホとPythonがあれば試せる
02

実装前に整理する設計図

MediaPipeのコードをAIに依頼したり、自分で組み立てたりする際は、「対象関節」「動画ソース」「出力形式」「不正検出時の扱い」の4点を先に決めておくと、手戻りが少なくなります。

要素 内容
対象関節 どのランドマークの組み合わせで角度を出すか 肩・肘・手首で肘関節角度
動画ソース 録画済みファイルかWebカメラのライブ映像か mp4ファイル/内蔵カメラ
出力形式 画面表示のみか、数値として保存するか 動画に角度を重ね書き+CSV保存
不正検出時の扱い ランドマークの信頼度が低い・写っていない場合の処理 visibilityが低いフレームはNaNとして記録
PROMPTPython 3で、MediaPipe Pose Landmarker(Tasks API)を使い、動画ファイルから肘関節(肩・肘・手首の3点)の角度をフレームごとに計算するコードを書いてください。角度は内積とarccosを使って計算し、動画の各フレームに角度の数値と骨格線を重ねて表示してください。ランドマークのvisibilityが0.5未満のフレームでは角度を計算せずNaNとして扱い、最終的に「フレーム番号,時刻(秒),角度(度)」の形式でCSVに保存してください。関数に分割し、日本語コメントを付けてください。
POINT

「visibilityが低い時にどう扱うか」まで指定しておくと、AIが生成するコードにエラー処理が最初から含まれ、後から自分で直す手間が減ります。

03

生成されたコード全文(肘関節角度・完全版)

プロンプトから生成した「肘関節角度を計測し、動画への重ね書きとCSV出力を行う」コードです。この完全版を教材として、次のセクションでブロックごとに読み解きます。実行にはあらかじめpose_landmarker.taskモデルファイル ↗のダウンロードが必要です(詳細はMediaPipe公式のPythonガイド ↗を参照してください)。

elbow_angle_full.py
import cv2
import csv
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
VISIBILITY_THRESHOLD = 0.5
def calc_angle(a, b, c):
    """3点a-b-cの角度(度)を計算する。bが関節の頂点。"""
    a, b, c = np.array(a), np.array(b), np.array(c)
    ba = a - b
    bc = c - b
    cos_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9)
    return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
def get_point(landmarks, index, width, height):
    """ランドマーク座標をピクセル単位のタプルに変換する。"""
    lm = landmarks[index]
    return (lm.x * width, lm.y * height), lm.visibility
def process_video(video_path, model_path, output_csv):
    base_options = python.BaseOptions(model_asset_path=model_path)
    options = vision.PoseLandmarkerOptions(base_options=base_options)
    detector = vision.PoseLandmarker.create_from_options(options)
    cap = cv2.VideoCapture(video_path)
    fps = cap.get(cv2.CAP_PROP_FPS) or 30
    frame_index = 0
    rows = [["フレーム番号", "時刻(秒)", "肘関節角度(度)"]]
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        height, width = frame.shape[:2]
        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        mp_image = Image(image_format=ImageFormat.SRGB, data=rgb_frame)
        result = detector.detect(mp_image)
        angle_text = "NaN"
        if result.pose_landmarks:
            landmarks = result.pose_landmarks[0]
            shoulder, v1 = get_point(landmarks, 12, width, height)
            elbow, v2 = get_point(landmarks, 14, width, height)
            wrist, v3 = get_point(landmarks, 16, width, height)
            if min(v1, v2, v3) >= VISIBILITY_THRESHOLD:
                angle = calc_angle(shoulder, elbow, wrist)
                angle_text = f"{angle:.1f}"
                rows.append([frame_index, round(frame_index / fps, 3), angle_text])
                cv2.putText(frame, f"Elbow: {angle_text} deg", (30, 50),
                            cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
            else:
                rows.append([frame_index, round(frame_index / fps, 3), "NaN"])
        else:
            rows.append([frame_index, round(frame_index / fps, 3), "NaN"])
        cv2.imshow("Elbow Angle", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
        frame_index += 1
    cap.release()
    cv2.destroyAllWindows()
    with open(output_csv, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerows(rows)
    print(f"角度データを {output_csv} に保存しました。")
if __name__ == "__main__":
    process_video("rehab_video.mp4", "pose_landmarker.task", "elbow_angle.csv")
04

コード解説:ブロック単位で読む

def calc_angle(a, b, c):
3点の座標から頂点bの角度を求める共通関数です。ベクトルの内積とarccosを使う方法は、肘・膝・肩などどの関節でも同じ関数を使い回せるため、8選のコードすべてでこの形を採用しています。
if min(v1, v2, v3) >= VISIBILITY_THRESHOLD:
3点すべてのvisibility(信頼度)が閾値を超えている場合だけ角度を計算しています。腕が体の後ろに隠れるなど推定が不安定なフレームでは、無理に数値を出さずNaNとして記録することで、後段のグラフやROM集計に誤差が混入するのを防ぎます。
mp_image = Image(image_format=ImageFormat.SRGB, data=rgb_frame)
OpenCVはBGR形式で画像を扱いますが、MediaPipeはRGB形式を前提にしているため、cv2.cvtColor()で変換してからmediapipe.Imageオブジェクトに変換しています。この変換を忘れると色味だけでなく検出精度にも影響します。
with open(output_csv, “w”, newline=””, encoding=”utf-8-sig”) as f:
Excelで文字化けせず開けるようencoding=”utf-8-sig”を指定しています。newline=””はcsvモジュールを使う際のPython公式推奨設定で、改行コードの二重挿入を防ぎます。
05

関節角度計測にありがちな落とし穴

CHECK 1

MediaPipe Poseの推定は基本的に2次元(画面上のx, y座標)に射影した角度です。カメラに対して斜めに動く関節は、実際の可動域よりも小さく(あるいは大きく)計算されることがあります。矢状面の動きを見たい場合は、カメラを動作平面の真横に設置してください。

CHECK 2

visibilityのチェックを省略すると、関節が服や体の陰に隠れた瞬間にランドマークが大きくずれ、角度がスパイク状に跳ねる原因になります。本記事のコードのように、閾値以下のフレームはNaNとして除外するか、移動平均でスムージングしてください。

CHECK 3

pose_landmarker.taskのモデルファイルをスクリプトと同じディレクトリに置き忘れると、model_asset_pathの指定でエラーになります。lite/full/heavyの3種類があり、精度と処理速度はトレードオフの関係にあるため、リアルタイム処理ではlite、精度重視の事後解析ではfullやheavyを選ぶと良いバランスになります。

06

実行結果を確認し、対話で直す

実行後は必ず動画の重ね書き表示とCSVの中身を確認します。「角度がおかしい」だけでなく、使用しているmediapipeのバージョン、カメラの設置角度、対象者の服装や照明条件までAIに伝えると、原因の切り分けが早くなります。

FOLLOW-UP PROMPT提示コードで肘関節角度を計測していますが、腕を体の正面でひねる動作の時だけ角度が実際より小さく表示されます。カメラは被写体の正面1台のみです。2次元射影による誤差が原因として考えられるか、コード側で改善できる点があれば教えてください。
07

8本を実行する前の共通チェック

  • 事前に pip install mediapipe opencv-python numpy matplotlib を実行しておく
  • MediaPipe公式のモデル一覧 ↗からpose_landmarker.taskをダウンロードし、スクリプトと同じフォルダに置く
  • 各コードは1本ずつ別の.pyファイルとして保存する
  • ターミナルからpython ファイル名.pyで実行する(動画ファイルのパスは自分の環境に合わせて書き換える)
  • Webカメラを使うコードは、カメラへのアクセス許可をOSやブラウザ設定で確認しておく
  • 思った通りに角度が出ない時は、まずCSVを開いてNaNの割合を確認する
LIBRARY

今回使用するのはmediapipe(姿勢推定)、opencv-python(映像入出力)、numpy(角度計算)、matplotlib(グラフ化)です。詳細な使い方はOpenCV公式ドキュメント ↗NumPy公式ドキュメント ↗Matplotlib公式ドキュメント ↗mediapipeのPyPIページ ↗を参照してください。

08

コピペですぐ試せる関節角度計測コード集8選

すべてPython 3とMediaPipe Pose Landmarker(Tasks API)を前提にしたコードです。「単一関節の角度計測」「リアルタイム表示」「データ出力・可視化」の3テーマでまとめました。検索欄に「膝」「Webカメラ」「CSV」などと入力すると対象コードだけに絞れます。

8 / 8件を表示
No. コード 対象関節・用途 難易度
01 肘関節角度(屈曲・伸展) 肩・肘・手首 入門
02 膝関節角度(歩行時の下肢) 股関節・膝・足首 入門+
03 肩関節角度(外転・挙上) 股関節・肩・肘 入門+
04 股関節角度(屈曲) 肩・股関節・膝 基礎
05 体幹側屈角度(垂直基準線との比較) 両肩の中点・両股関節の中点 基礎
06 足関節角度(背屈・底屈) 膝・足首・つま先 基礎
07 Webカメラでリアルタイム関節角度表示 肘関節(応用で他関節にも展開可) 基礎+
08 角度データをグラフ化+ROMサマリー集計 CSV出力済みデータの可視化 基礎+
該当するコードがありません。別のキーワードで検索してください。
CODE 01

肘関節角度(屈曲・伸展)

肩・肘・手首の3点から肘関節の角度を計算し、動画に重ね書きします。角度計算の基本形として最初に試すのに向いています。

入門mediapipe / opencv

学べる基本

ランドマーク座標の取得、内積による角度計算、cv2.putTextでの重ね書き

実行方法

ターミナルで python c01.py

コードを表示・コピー
c01.py
import cv2
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
def calc_angle(a, b, c):
    a, b, c = np.array(a), np.array(b), np.array(c)
    ba, bc = a - b, c - b
    cos_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9)
    return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
def main():
    base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
    detector = vision.PoseLandmarker.create_from_options(vision.PoseLandmarkerOptions(base_options=base_options))
    cap = cv2.VideoCapture("rehab_video.mp4")
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        result = detector.detect(Image(image_format=ImageFormat.SRGB, data=rgb))
        if result.pose_landmarks:
            lm = result.pose_landmarks[0]
            shoulder = (lm[12].x * w, lm[12].y * h)
            elbow = (lm[14].x * w, lm[14].y * h)
            wrist = (lm[16].x * w, lm[16].y * h)
            angle = calc_angle(shoulder, elbow, wrist)
            cv2.putText(frame, f"Elbow: {angle:.1f} deg", (30, 50),
                        cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
        cv2.imshow("Elbow Angle", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    cap.release()
    cv2.destroyAllWindows()
if __name__ == "__main__":
    main()

調整ポイント:ランドマーク番号を12/14/16(右腕)から11/13/15(左腕)に変えると、反対側の腕の角度を計測できます。

8選の一覧へ戻る ↑

CODE 02

膝関節角度(歩行時の下肢)

股関節・膝・足首の3点から膝関節角度を計算します。歩行動画に適用すると、立脚期と遊脚期での角度変化を追えます。

入門+mediapipe / opencv

学べる基本

下肢のランドマーク番号、フレームごとのリスト蓄積

実行方法

ターミナルで python c02.py

コードを表示・コピー
c02.py
import cv2
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
def calc_angle(a, b, c):
    a, b, c = np.array(a), np.array(b), np.array(c)
    ba, bc = a - b, c - b
    cos_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9)
    return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
def main():
    base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
    detector = vision.PoseLandmarker.create_from_options(vision.PoseLandmarkerOptions(base_options=base_options))
    cap = cv2.VideoCapture("gait_video.mp4")
    angles = []
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        result = detector.detect(Image(image_format=ImageFormat.SRGB, data=rgb))
        if result.pose_landmarks:
            lm = result.pose_landmarks[0]
            hip = (lm[24].x * w, lm[24].y * h)
            knee = (lm[26].x * w, lm[26].y * h)
            ankle = (lm[28].x * w, lm[28].y * h)
            angle = calc_angle(hip, knee, ankle)
            angles.append(angle)
            cv2.putText(frame, f"Knee: {angle:.1f} deg", (30, 50),
                        cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
        cv2.imshow("Knee Angle", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    cap.release()
    cv2.destroyAllWindows()
    if angles:
        print(f"最大屈曲角度: {max(angles):.1f}度 / 最小角度: {min(angles):.1f}度")
if __name__ == "__main__":
    main()

調整ポイント:歩行のように周期的な動きでは、angles配列の増減を見るだけで1歩行周期ごとの最大屈曲角度を把握できます。

8選の一覧へ戻る ↑

CODE 03

肩関節角度(外転・挙上)

股関節・肩・肘の3点から肩関節の角度を計算します。体幹に対する上肢の挙上角度をおおまかに追う用途に向いています。

入門+mediapipe / opencv

学べる基本

体幹を基準にした角度計算、visibilityによる簡易フィルタ

実行方法

ターミナルで python c03.py

コードを表示・コピー
c03.py
import cv2
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
VISIBILITY_THRESHOLD = 0.5
def calc_angle(a, b, c):
    a, b, c = np.array(a), np.array(b), np.array(c)
    ba, bc = a - b, c - b
    cos_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9)
    return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
def main():
    base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
    detector = vision.PoseLandmarker.create_from_options(vision.PoseLandmarkerOptions(base_options=base_options))
    cap = cv2.VideoCapture("shoulder_video.mp4")
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        result = detector.detect(Image(image_format=ImageFormat.SRGB, data=rgb))
        if result.pose_landmarks:
            lm = result.pose_landmarks[0]
            visibilities = [lm[24].visibility, lm[12].visibility, lm[14].visibility]
            if min(visibilities) >= VISIBILITY_THRESHOLD:
                hip = (lm[24].x * w, lm[24].y * h)
                shoulder = (lm[12].x * w, lm[12].y * h)
                elbow = (lm[14].x * w, lm[14].y * h)
                angle = calc_angle(hip, shoulder, elbow)
                cv2.putText(frame, f"Shoulder: {angle:.1f} deg", (30, 50),
                            cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
        cv2.imshow("Shoulder Angle", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    cap.release()
    cv2.destroyAllWindows()
if __name__ == "__main__":
    main()

調整ポイント:正面からの撮影では外転・屈曲の区別がつきにくいため、評価したい運動面に合わせてカメラの設置方向を変えてください。

8選の一覧へ戻る ↑

CODE 04

股関節角度(屈曲)

肩・股関節・膝の3点から股関節屈曲角度を計算します。座位からの下肢挙上や立ち上がり動作の確認に使えます。

基礎mediapipe / opencv

学べる基本

体幹〜下肢をまたぐ角度計算、フレーム番号の記録

実行方法

ターミナルで python c04.py

コードを表示・コピー
c04.py
import cv2
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
def calc_angle(a, b, c):
    a, b, c = np.array(a), np.array(b), np.array(c)
    ba, bc = a - b, c - b
    cos_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9)
    return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
def main():
    base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
    detector = vision.PoseLandmarker.create_from_options(vision.PoseLandmarkerOptions(base_options=base_options))
    cap = cv2.VideoCapture("hip_video.mp4")
    frame_index = 0
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        result = detector.detect(Image(image_format=ImageFormat.SRGB, data=rgb))
        if result.pose_landmarks:
            lm = result.pose_landmarks[0]
            shoulder = (lm[12].x * w, lm[12].y * h)
            hip = (lm[24].x * w, lm[24].y * h)
            knee = (lm[26].x * w, lm[26].y * h)
            angle = calc_angle(shoulder, hip, knee)
            cv2.putText(frame, f"Hip: {angle:.1f} deg (frame {frame_index})", (30, 50),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
        cv2.imshow("Hip Angle", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
        frame_index += 1
    cap.release()
    cv2.destroyAllWindows()
if __name__ == "__main__":
    main()

調整ポイント:立ち上がり動作を評価する場合は、座面の高さが分かるよう椅子ごと画角に入れて撮影すると、後から動画を見返す際の解釈がしやすくなります。

8選の一覧へ戻る ↑

CODE 05

体幹側屈角度(垂直基準線との比較)

両肩の中点と両股関節の中点を結ぶ線が、垂直線に対してどれだけ傾いているかを計算します。3点ではなく基準線を使う角度計算の応用形です。

基礎mediapipe / opencv

学べる基本

2点の中点の計算、arctan2による傾き角度の算出

実行方法

ターミナルで python c05.py

コードを表示・コピー
c05.py
import cv2
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
def midpoint(p1, p2):
    return ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)
def tilt_from_vertical(top, bottom):
    """topからbottomへ伸びる線が垂直線となす角度(度)を返す"""
    dx = top[0] - bottom[0]
    dy = top[1] - bottom[1]
    return np.degrees(np.arctan2(dx, -dy))
def main():
    base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
    detector = vision.PoseLandmarker.create_from_options(vision.PoseLandmarkerOptions(base_options=base_options))
    cap = cv2.VideoCapture("trunk_video.mp4")
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        result = detector.detect(Image(image_format=ImageFormat.SRGB, data=rgb))
        if result.pose_landmarks:
            lm = result.pose_landmarks[0]
            shoulder_mid = midpoint((lm[11].x * w, lm[11].y * h), (lm[12].x * w, lm[12].y * h))
            hip_mid = midpoint((lm[23].x * w, lm[23].y * h), (lm[24].x * w, lm[24].y * h))
            angle = tilt_from_vertical(shoulder_mid, hip_mid)
            cv2.putText(frame, f"Trunk tilt: {angle:.1f} deg", (30, 50),
                        cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
        cv2.imshow("Trunk Lateral Flexion", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    cap.release()
    cv2.destroyAllWindows()
if __name__ == "__main__":
    main()

調整ポイント:この計算は正面からの撮影を前提としています。被写体が正面を向いた状態で撮影できているか、動画の最初のフレームで確認してください。

8選の一覧へ戻る ↑

CODE 06

足関節角度(背屈・底屈)

膝・足首・つま先の3点から足関節角度を計算します。歩行時のクリアランス確認や自主トレの背屈運動チェックに使えます。

基礎mediapipe / opencv

学べる基本

足部ランドマーク(つま先)の扱い、基準角度との差分表示

実行方法

ターミナルで python c06.py

コードを表示・コピー
c06.py
import cv2
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
NEUTRAL_ANGLE = 90.0  # 足関節中間位を基準角度とする
def calc_angle(a, b, c):
    a, b, c = np.array(a), np.array(b), np.array(c)
    ba, bc = a - b, c - b
    cos_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9)
    return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
def main():
    base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
    detector = vision.PoseLandmarker.create_from_options(vision.PoseLandmarkerOptions(base_options=base_options))
    cap = cv2.VideoCapture("ankle_video.mp4")
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        result = detector.detect(Image(image_format=ImageFormat.SRGB, data=rgb))
        if result.pose_landmarks:
            lm = result.pose_landmarks[0]
            knee = (lm[26].x * w, lm[26].y * h)
            ankle = (lm[28].x * w, lm[28].y * h)
            toe = (lm[32].x * w, lm[32].y * h)
            angle = calc_angle(knee, ankle, toe)
            diff = angle - NEUTRAL_ANGLE
            label = "背屈" if diff > 0 else "底屈"
            cv2.putText(frame, f"Ankle: {angle:.1f} deg ({label} {abs(diff):.1f})", (20, 50),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
        cv2.imshow("Ankle Angle", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    cap.release()
    cv2.destroyAllWindows()
if __name__ == "__main__":
    main()

調整ポイント:NEUTRAL_ANGLEはあくまで簡易的な目安です。実際の中間位はカメラアングルや個人差の影響を受けるため、事前にゴニオメーターで測った値と付き合わせて調整してください。

8選の一覧へ戻る ↑

CODE 07

Webカメラでリアルタイム関節角度表示

内蔵・外付けのWebカメラから直接映像を取得し、肘関節角度をその場で画面に表示し続けます。訓練中のリアルタイムフィードバックに向いています。

基礎+mediapipe / opencv

学べる基本

cv2.VideoCapture(0)によるカメラ取得、running_modeの考え方

実行方法

ターミナルで python c07.py(カメラへのアクセス許可が必要)

コードを表示・コピー
c07.py
import cv2
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe import Image, ImageFormat
def calc_angle(a, b, c):
    a, b, c = np.array(a), np.array(b), np.array(c)
    ba, bc = a - b, c - b
    cos_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-9)
    return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
def main():
    base_options = python.BaseOptions(model_asset_path="pose_landmarker.task")
    options = vision.PoseLandmarkerOptions(
        base_options=base_options,
        running_mode=vision.RunningMode.VIDEO,
    )
    detector = vision.PoseLandmarker.create_from_options(options)
    cap = cv2.VideoCapture(0)
    timestamp_ms = 0
    print("Webカメラを起動しました。qキーで終了します。")
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        h, w = frame.shape[:2]
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        mp_image = Image(image_format=ImageFormat.SRGB, data=rgb)
        result = detector.detect_for_video(mp_image, timestamp_ms)
        timestamp_ms += 33  # 約30fps相当
        if result.pose_landmarks:
            lm = result.pose_landmarks[0]
            shoulder = (lm[12].x * w, lm[12].y * h)
            elbow = (lm[14].x * w, lm[14].y * h)
            wrist = (lm[16].x * w, lm[16].y * h)
            angle = calc_angle(shoulder, elbow, wrist)
            cv2.putText(frame, f"Elbow: {angle:.1f} deg", (30, 50),
                        cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2)
        cv2.imshow("Realtime Elbow Angle", frame)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break
    cap.release()
    cv2.destroyAllWindows()
if __name__ == "__main__":
    main()

調整ポイント:ライブ映像を処理する際はrunning_mode=vision.RunningMode.VIDEOとdetect_for_video()を組み合わせるのが公式に推奨される構成です。他の関節に応用したい場合は、c01〜c06のランドマーク番号をこのテンプレートに差し替えてください。

8選の一覧へ戻る ↑

CODE 08

角度データをグラフ化+ROMサマリー集計

c01〜c07で出力したCSVを読み込み、角度の時系列グラフと、最大値・最小値・可動域(ROM)のサマリーを表示します。撮影後のレポート作成に使える集計コードです。

基礎+numpy / matplotlib

学べる基本

CSV読み込み、NaNの除外、matplotlibでの折れ線グラフ作成

実行方法

ターミナルで python c08.py

コードを表示・コピー
c08.py
import csv
import numpy as np
import matplotlib.pyplot as plt
def load_angles(csv_path):
    times, angles = [], []
    with open(csv_path, encoding="utf-8-sig") as f:
        reader = csv.reader(f)
        next(reader)  # ヘッダー行を読み飛ばす
        for row in reader:
            time_sec, angle_text = float(row[1]), row[2]
            if angle_text == "NaN":
                continue
            times.append(time_sec)
            angles.append(float(angle_text))
    return np.array(times), np.array(angles)
def summarize(angles):
    return {
        "最大角度": np.max(angles),
        "最小角度": np.min(angles),
        "可動域(ROM)": np.max(angles) - np.min(angles),
        "平均角度": np.mean(angles),
    }
def main():
    times, angles = load_angles("elbow_angle.csv")
    if len(angles) == 0:
        print("有効な角度データがありません。visibilityの閾値や動画を確認してください。")
        return
    stats = summarize(angles)
    for label, value in stats.items():
        print(f"{label}: {value:.1f} 度")
    plt.figure(figsize=(9, 4))
    plt.plot(times, angles, color="#1f77b4", linewidth=1.5)
    plt.xlabel("時刻 (秒)")
    plt.ylabel("角度 (度)")
    plt.title("関節角度の時系列変化")
    plt.grid(alpha=0.3)
    plt.tight_layout()
    plt.savefig("angle_plot.png", dpi=150)
    plt.show()
if __name__ == "__main__":
    main()

調整ポイント:複数回の測定を比較したい場合は、CSVファイル名をリストにしてfor文で繰り返し読み込み、1つのグラフに重ねて描画すると訓練前後の変化を可視化できます。

8選の一覧へ戻る ↑

09

よくある質問とまとめ

MediaPipeで測った関節角度は臨床評価にそのまま使えますか?

参考値としての活用は可能ですが、そのまま臨床判断の一次資料にするのは推奨しません。単眼カメラによる2次元推定には奥行き方向の誤差が生じるため、ゴニオメーターや3Dモーションキャプチャとの並行確認を行い、誤差の傾向を把握した上で活用してください。

Pythonやプログラミングの経験がなくても実装できますか?

本記事のコードはコピペで動作する前提で書かれていますが、Pythonの基本文法(変数、関数、for文)を理解していると、エラー発生時の対応がしやすくなります。まずは肘関節など単純な動きのコードから試すのがおすすめです。

スマホで撮影した動画でもそのまま使えますか?

はい。MediaPipe Pose Landmarkerは一般的なRGB動画(mp4など)に対応しているため、スマホで撮影した動画をそのまま入力できます。ただし関節が服や体の一部で隠れる角度、逆光、フレームアウトがあると精度が下がる点に注意してください。

リアルタイムのWebカメラ計測と録画済み動画の解析はどちらがよいですか?

用途によります。訓練中にその場でフィードバックしたい場合はWebカメラのリアルタイム処理、後から複数回の動作を比較・記録したい場合は録画済み動画の解析が向いています。本記事では両方のコードを掲載しています。

まずは部品が少なく検証しやすい「肘関節角度(c01)」を動かし、CSVに数値が記録される様子を確認するところから始めると理解しやすくなります。動いたコードに対して、AIへ「複数関節を同時に計測して」「動作の左右差を比較して」「一定角度を超えたらアラートを出して」と追加依頼すれば、同じロジックから評価シートや自主トレの記録ツールへ発展させられます。関連する取り組みとしてリハビリ職のAI活用17選や、論文を探す際に役立つChatGPTで医学論文を探す5ステップ実践ガイドもあわせてご覧ください。

センサーを併用したより本格的な計測に興味がある方はArduino×Python×AIで作るリハビリセンサー自作ガイド、他のPython実装例はコード知識カテゴリ ↗もあわせてご覧ください。

MEDIAPIPE × PYTHON REHAB GUIDE — 動画を数値に変えて、動作分析を日常の記録に育てていきましょう。



関連記事

特集記事

コメント

この記事へのコメントはありません。

TOP
CLOSE