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

前回の概要編では「スマホ1台でどこまでリハビリの動作分析ができるか」を整理しました。今回はその実装編として、Googleの姿勢推定ライブラリMediaPipeを使い、スマホやWebカメラで撮影した動画から肘・膝・肩・股関節などの角度を自動で計測するPythonコードを、コピペで動く形でまとめています。AIへのプロンプト設計、生成されたコードの読み方、精度に関わる落とし穴まで、実装に必要な流れを一気通貫で解説します。
本記事は実装編です。まず全体像を知りたい方は概要編の「スマホ1台でリハビリの動作分析はどこまでできる?現状と始め方」をご覧ください。センサーを併用したい場合はArduino×Python×AIで作るリハビリセンサー自作ガイド、筋電データの可視化にはDelsys筋電CSVをPythonで可視化しAIで解析を加速する実践ガイドも参考になります。
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フレームずつ関節角度を計算する処理が入る
なぜ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があれば試せる
実装前に整理する設計図

MediaPipeのコードをAIに依頼したり、自分で組み立てたりする際は、「対象関節」「動画ソース」「出力形式」「不正検出時の扱い」の4点を先に決めておくと、手戻りが少なくなります。
| 要素 | 内容 | 例 |
|---|---|---|
| 対象関節 | どのランドマークの組み合わせで角度を出すか | 肩・肘・手首で肘関節角度 |
| 動画ソース | 録画済みファイルかWebカメラのライブ映像か | mp4ファイル/内蔵カメラ |
| 出力形式 | 画面表示のみか、数値として保存するか | 動画に角度を重ね書き+CSV保存 |
| 不正検出時の扱い | ランドマークの信頼度が低い・写っていない場合の処理 | visibilityが低いフレームはNaNとして記録 |
「visibilityが低い時にどう扱うか」まで指定しておくと、AIが生成するコードにエラー処理が最初から含まれ、後から自分で直す手間が減ります。
生成されたコード全文(肘関節角度・完全版)
プロンプトから生成した「肘関節角度を計測し、動画への重ね書きとCSV出力を行う」コードです。この完全版を教材として、次のセクションでブロックごとに読み解きます。実行にはあらかじめpose_landmarker.taskモデルファイル ↗のダウンロードが必要です(詳細はMediaPipe公式のPythonガイド ↗を参照してください)。
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")
コード解説:ブロック単位で読む
関節角度計測にありがちな落とし穴
MediaPipe Poseの推定は基本的に2次元(画面上のx, y座標)に射影した角度です。カメラに対して斜めに動く関節は、実際の可動域よりも小さく(あるいは大きく)計算されることがあります。矢状面の動きを見たい場合は、カメラを動作平面の真横に設置してください。

visibilityのチェックを省略すると、関節が服や体の陰に隠れた瞬間にランドマークが大きくずれ、角度がスパイク状に跳ねる原因になります。本記事のコードのように、閾値以下のフレームはNaNとして除外するか、移動平均でスムージングしてください。
pose_landmarker.taskのモデルファイルをスクリプトと同じディレクトリに置き忘れると、model_asset_pathの指定でエラーになります。lite/full/heavyの3種類があり、精度と処理速度はトレードオフの関係にあるため、リアルタイム処理ではlite、精度重視の事後解析ではfullやheavyを選ぶと良いバランスになります。
実行結果を確認し、対話で直す
実行後は必ず動画の重ね書き表示とCSVの中身を確認します。「角度がおかしい」だけでなく、使用しているmediapipeのバージョン、カメラの設置角度、対象者の服装や照明条件までAIに伝えると、原因の切り分けが早くなります。
8本を実行する前の共通チェック
- 事前に
pip install mediapipe opencv-python numpy matplotlibを実行しておく - MediaPipe公式のモデル一覧 ↗からpose_landmarker.taskをダウンロードし、スクリプトと同じフォルダに置く
- 各コードは1本ずつ別の.pyファイルとして保存する
- ターミナルから
python ファイル名.pyで実行する(動画ファイルのパスは自分の環境に合わせて書き換える) - Webカメラを使うコードは、カメラへのアクセス許可をOSやブラウザ設定で確認しておく
- 思った通りに角度が出ない時は、まずCSVを開いてNaNの割合を確認する
今回使用するのはmediapipe(姿勢推定)、opencv-python(映像入出力)、numpy(角度計算)、matplotlib(グラフ化)です。詳細な使い方はOpenCV公式ドキュメント ↗、NumPy公式ドキュメント ↗、Matplotlib公式ドキュメント ↗、mediapipeのPyPIページ ↗を参照してください。
コピペですぐ試せる関節角度計測コード集8選

すべてPython 3とMediaPipe Pose Landmarker(Tasks API)を前提にしたコードです。「単一関節の角度計測」「リアルタイム表示」「データ出力・可視化」の3テーマでまとめました。検索欄に「膝」「Webカメラ」「CSV」などと入力すると対象コードだけに絞れます。
| No. | コード | 対象関節・用途 | 難易度 |
|---|---|---|---|
| 01 | 肘関節角度(屈曲・伸展) | 肩・肘・手首 | 入門 |
| 02 | 膝関節角度(歩行時の下肢) | 股関節・膝・足首 | 入門+ |
| 03 | 肩関節角度(外転・挙上) | 股関節・肩・肘 | 入門+ |
| 04 | 股関節角度(屈曲) | 肩・股関節・膝 | 基礎 |
| 05 | 体幹側屈角度(垂直基準線との比較) | 両肩の中点・両股関節の中点 | 基礎 |
| 06 | 足関節角度(背屈・底屈) | 膝・足首・つま先 | 基礎 |
| 07 | Webカメラでリアルタイム関節角度表示 | 肘関節(応用で他関節にも展開可) | 基礎+ |
| 08 | 角度データをグラフ化+ROMサマリー集計 | CSV出力済みデータの可視化 | 基礎+ |
肘関節角度(屈曲・伸展)
肩・肘・手首の3点から肘関節の角度を計算し、動画に重ね書きします。角度計算の基本形として最初に試すのに向いています。
ランドマーク座標の取得、内積による角度計算、cv2.putTextでの重ね書き
ターミナルで python 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()
膝関節角度(歩行時の下肢)
股関節・膝・足首の3点から膝関節角度を計算します。歩行動画に適用すると、立脚期と遊脚期での角度変化を追えます。
下肢のランドマーク番号、フレームごとのリスト蓄積
ターミナルで python 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()
肩関節角度(外転・挙上)
股関節・肩・肘の3点から肩関節の角度を計算します。体幹に対する上肢の挙上角度をおおまかに追う用途に向いています。
体幹を基準にした角度計算、visibilityによる簡易フィルタ
ターミナルで python 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()
股関節角度(屈曲)
肩・股関節・膝の3点から股関節屈曲角度を計算します。座位からの下肢挙上や立ち上がり動作の確認に使えます。
体幹〜下肢をまたぐ角度計算、フレーム番号の記録
ターミナルで python 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()
体幹側屈角度(垂直基準線との比較)
両肩の中点と両股関節の中点を結ぶ線が、垂直線に対してどれだけ傾いているかを計算します。3点ではなく基準線を使う角度計算の応用形です。
2点の中点の計算、arctan2による傾き角度の算出
ターミナルで python 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()
足関節角度(背屈・底屈)
膝・足首・つま先の3点から足関節角度を計算します。歩行時のクリアランス確認や自主トレの背屈運動チェックに使えます。
足部ランドマーク(つま先)の扱い、基準角度との差分表示
ターミナルで python 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()
Webカメラでリアルタイム関節角度表示
内蔵・外付けのWebカメラから直接映像を取得し、肘関節角度をその場で画面に表示し続けます。訓練中のリアルタイムフィードバックに向いています。
cv2.VideoCapture(0)によるカメラ取得、running_modeの考え方
ターミナルで python 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()
角度データをグラフ化+ROMサマリー集計
c01〜c07で出力したCSVを読み込み、角度の時系列グラフと、最大値・最小値・可動域(ROM)のサマリーを表示します。撮影後のレポート作成に使える集計コードです。
CSV読み込み、NaNの除外、matplotlibでの折れ線グラフ作成
ターミナルで python 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()
よくある質問とまとめ
MediaPipeで測った関節角度は臨床評価にそのまま使えますか?
参考値としての活用は可能ですが、そのまま臨床判断の一次資料にするのは推奨しません。単眼カメラによる2次元推定には奥行き方向の誤差が生じるため、ゴニオメーターや3Dモーションキャプチャとの並行確認を行い、誤差の傾向を把握した上で活用してください。
Pythonやプログラミングの経験がなくても実装できますか?
本記事のコードはコピペで動作する前提で書かれていますが、Pythonの基本文法(変数、関数、for文)を理解していると、エラー発生時の対応がしやすくなります。まずは肘関節など単純な動きのコードから試すのがおすすめです。
スマホで撮影した動画でもそのまま使えますか?
はい。MediaPipe Pose Landmarkerは一般的なRGB動画(mp4など)に対応しているため、スマホで撮影した動画をそのまま入力できます。ただし関節が服や体の一部で隠れる角度、逆光、フレームアウトがあると精度が下がる点に注意してください。
リアルタイムのWebカメラ計測と録画済み動画の解析はどちらがよいですか?
用途によります。訓練中にその場でフィードバックしたい場合はWebカメラのリアルタイム処理、後から複数回の動作を比較・記録したい場合は録画済み動画の解析が向いています。本記事では両方のコードを掲載しています。
まずは部品が少なく検証しやすい「肘関節角度(c01)」を動かし、CSVに数値が記録される様子を確認するところから始めると理解しやすくなります。動いたコードに対して、AIへ「複数関節を同時に計測して」「動作の左右差を比較して」「一定角度を超えたらアラートを出して」と追加依頼すれば、同じロジックから評価シートや自主トレの記録ツールへ発展させられます。関連する取り組みとしてリハビリ職のAI活用17選や、論文を探す際に役立つChatGPTで医学論文を探す5ステップ実践ガイドもあわせてご覧ください。
センサーを併用したより本格的な計測に興味がある方はArduino×Python×AIで作るリハビリセンサー自作ガイド、他のPython実装例はコード知識カテゴリ ↗もあわせてご覧ください。
コメント