スマホだけでTUGは自動計測できる?AI判定と理学療法士のストップウォッチを30回比較
TUG(Timed Up and Go Test)は、椅子からの立ち上がり・3m歩行・方向転換・着座までを一続きで評価する、転倒リスクスクリーニングの定番です。この「開始」「方向転換」「着座」という3つの局面を、スマホ1台のカメラとAI(姿勢推定)だけでどこまで自動検出できるのか。同一被験者・同一条件で30試行を実施し、理学療法士がストップウォッチで計測した値と局面ごとの検出タイミングを突き合わせました。結論から言えば「使えるが、局面によって癖がある」という、教科書には載っていない現場感覚の話です。実際に使ったPythonコードと、局面別の誤差データをすべて公開します。
目次
この記事で解決できること
この記事を読み終える頃には、次の3つが分かります。
① スマホのカメラとAI(姿勢推定)だけで、TUGの「開始」「方向転換」「着座」を自動検出する仕組みと、実際に動くコード
② 同一条件で30回試行した際に、理学療法士のストップウォッチ計測とAI判定がどれくらいズレるのか、局面ごとの誤差の実測値
③ そのズレがどんな場面で大きくなり、どこまでなら現場のスクリーニング補助として使ってよいのかという線引き
結論:AI判定は「使えるが、局面に癖がある」
先に結論を書きます。スマホ1台・カメラのみでのTUG自動計測は、全体のタイム(開始〜着座までの合計時間)についてはストップウォッチとほぼ一致します。しかし、局面ごとに見ると「方向転換」の検出だけ明らかに誤差が大きくなるというのが、今回30回の比較で得られた実感です。
「開始(立ち上がり)」と「着座」はカメラに対してほぼ正面・大きな動きのため検出しやすい一方、「方向転換」は身体の向きが変わることでカメラから見た関節の見え方が急に変化し、姿勢推定の精度が一時的に落ちる瞬間があります。この癖を知らずに使うと、「AIは使えない」と早合点してしまいますし、逆に知っていれば十分に補助ツールとして機能します。
実際の検証現場:なぜ30回計測に踏み切ったか

きっかけは単純で、「TUGをスマホで自動計測できたら、記録の手間が減るのでは」という職場の何気ない一言でした。試しに姿勢推定ライブラリMediaPipeを使ってプロトタイプを組み、1回だけ動かしてみたところ、それらしい数字は出ました。しかし1回の結果だけでは「たまたま合っただけ」なのか「本当に安定して使えるのか」の判断がつきません。そこで、同一の椅子・同一の3m歩行路・同一のスマホ設置位置という条件を固定し、理学療法士がストップウォッチで測る隣で、スマホを三脚に立てて同時に動画収録し、30試行分のデータを取ることにしました。
知らないと損:最初のズレは「フレームレートと閾値」で起きる
最初に組んだ検出ロジックは、正直そのままでは使い物になりませんでした。多くの人がここでつまずきます。実際にありがちな失敗は次の3パターンです。
- スマホの動画が30fpsなのに、検出のための移動平均の窓幅を1フレーム単位でしか調整しておらず、些細なノイズで「立ち上がった」と誤判定する
- 股関節(腰)の高さだけを見て開始を判定し、椅子に浅く座っている場合と深く座っている場合で閾値が合わなくなる
- 方向転換の判定を「体の向き」だけで見ており、カメラのフレーム端に体が寄った瞬間に姿勢推定そのものが一瞬外れて、誤検出が起きる
ここが今回の検証で一番大事な発見でした。単一の関節・単一の閾値で判定すると、局面によって精度がバラバラになる。複数の信号(股関節の垂直速度・膝関節角度・肩と股関節を結ぶベクトルの回転角)を組み合わせ、かつ移動平均でノイズを抑えることで、初めて実用的な検出精度になります。
修正コードの実例とBefore/After
import mediapipe as mp
import cv2
mp_pose = mp.solutions.pose
pose = mp_pose.Pose()
cap = cv2.VideoCapture('tug_trial.mp4')
started = False
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
result = pose.process(rgb)
if result.pose_landmarks:
hip_y = result.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_HIP].y
# 股関節の高さだけで開始判定(単一信号・閾値固定)
if hip_y < 0.55 and not started:
started = True
print("立ち上がり検出")
# → 座り方の深さ・カメラ角度が変わると閾値がずれて誤検出/未検出が起きる
import numpy as np
from collections import deque
WINDOW = 5 # 移動平均の窓幅(フレーム数)
hip_y_buf = deque(maxlen=WINDOW)
prev_hip_y = None
seat_off_frame = None
def hip_vertical_velocity(hip_y_now, hip_y_prev, fps):
if hip_y_prev is None:
return 0.0
return (hip_y_prev - hip_y_now) * fps # 上方向を正にする
for i, lm in enumerate(landmarks_seq):
if lm is None:
continue
hip_y = (lm[23][1] + lm[24][1]) / 2 # 左右股関節yの平均
knee_angle = calc_knee_angle(lm) # 膝関節角度(別関数)
hip_y_buf.append(hip_y)
smoothed_hip_y = np.mean(hip_y_buf)
vel = hip_vertical_velocity(smoothed_hip_y, prev_hip_y, fps=30)
prev_hip_y = smoothed_hip_y
# 股関節の上昇速度が閾値を超え、かつ膝が伸展方向に動いている時だけ「開始」とみなす
if seat_off_frame is None and vel > 0.35 and knee_angle > 100:
seat_off_frame = i
print(f"立ち上がり検出フレーム: {seat_off_frame}({seat_off_frame/30:.2f}秒)")
股関節の高さのみ・固定閾値で開始を判定
座り方の深さ・服装のたるみなどで閾値が合わず、同じ被験者でも試行ごとに検出タイミングが大きくばらついた。
股関節の垂直速度+膝関節角度+移動平均を組み合わせて判定
単一フレームのノイズに引っ張られにくくなり、30試行を通しての検出タイミングのばらつきが目に見えて小さくなった。
筆者の判断:AI判定は「補助記録」であって「代替」ではない
ここが今回いちばん強調したい部分です。今回の30試行は、単一施設・少人数の職員を被験者としたパイロット的な検証であり、査読を経た臨床研究ではありません。数値は「スマホ単体のAI判定にどんな癖があるか」を掴むための参考データとして読んでください。転倒リスクの最終判断や、対象者ごとの臨床解釈は、これまで通り理学療法士が行うべきものです。
- 今回の被験者は健常な成人職員であり、実際の患者(片麻痺・パーキンソン病・変形性関節症などで動作パターンが大きく異なる方)では誤差の傾向が変わる可能性が高い
- 撮影動画・関節座標などのデータには個人情報(顔・氏名)が写り込むため、匿名化・保管ルールを施設の規定に沿って整備してから運用する
- AI判定の数値は「参考記録」として扱い、最終的なタイムはこれまで通りストップウォッチまたは目視確認で確定させる運用が現実的
- 市販の医療機器としての承認を受けたものではないため、診断や介護保険の区分判定など公的な用途にそのまま使わない
この前提を踏まえたうえで、ここからは「計測環境の整え方」「AI判定の仕組み」「30回比較の結果」「局面別の誤差の正体」「発展的な実装」の順に、実コードとともに紹介します。
計測環境を整える:必要なもの・撮影条件・被験者への説明

AI判定の精度は、アルゴリズム以前に「撮影環境」で大きく変わります。まずここを固定しないと、比較そのものが成立しません。
必要なものを揃える
- できること
- 特別な機材は不要。スマホ(動画撮影ができる端末)、三脚またはスマホスタンド、椅子(肘掛けなし・座面44cm前後)、3m歩行路、そしてPCでの後処理環境(Python)があれば検証を始められる
- 使える場面
- 院内・自宅どちらでも、TUGの標準環境さえ用意できれば試せる場面
- 今すぐ試すなら
- スマホを三脚に固定し、被験者の全身(頭からつま先)が画角に収まる位置・高さに設置する。今回は椅子から水平距離約2.5m、床から高さ約1mの位置に設置した
- 注意点
- 画角が狭いと方向転換の瞬間に体がフレームから半分はみ出し、姿勢推定の精度が大きく落ちる。撮影前に実際に動いてもらい、画角内に収まるか確認する
撮影条件を固定する
- できること
- フレームレート・解像度・照明条件を試行間で揃えることで、AI判定側の条件差による誤差を排除できる
- 使える場面
- 複数回・複数人での比較を行う場面
- 今すぐ試すなら
- 今回は30fps・1080pに固定し、日中の自然光+室内照明で撮影した。動画ファイルはmp4形式で保存し、あとでまとめて処理する
- 注意点
- 逆光になる窓際は姿勢推定の精度が落ちやすい。撮影方向は被験者の正面〜斜め前が基本
理学療法士の計測タイミングを統一する
- できること
- ストップウォッチ側の「開始」「方向転換通過」「着座」の判定基準を、検者内でぶれないように事前に定義する
- 使える場面
- AI判定との比較の「正解データ」を作る場面
- 今すぐ試すなら
- 今回は「臀部が座面から完全に離れた瞬間」を開始、「体幹の向きが90度以上回転した瞬間」を方向転換、「臀部が座面に接地した瞬間」を着座として、検者があらかじめ紙に定義を書き出してから計測した
- 注意点
- 検者が複数いる場合は、事前に同じ動画で練習し、判定のズレがないか確認しておく(検者内・検者間信頼性の確保)
被験者への説明と同意
- できること
- 撮影を伴う検証であることを説明し、同意を得たうえで実施する
- 使える場面
- 職員・患者を問わず、動画を撮影して解析する取り組み全般
- 今すぐ試すなら
- 今回は職場の同意した職員複数名に協力してもらい、撮影データは検証目的以外に使用しないこと、検証後は匿名化した数値データのみ保管し動画は削除することを事前に説明した
- 注意点
- 患者を対象にする場合は、職員間の協力とは別に、施設の倫理・個人情報保護の手続きを必ず経る
AI判定アルゴリズムの基礎:何を見て「開始・方向転換・着座」を判定しているか

ブラックボックスのまま使うと、誤差の原因が分からなくなります。仕組みを最低限押さえておきましょう。
姿勢推定で骨格座標を取得する
- できること
- 動画の1フレームごとに、肩・股関節・膝・足首などの関節座標(画面上のx, y位置と検出信頼度)を推定する
- 使える場面
- すべての局面判定の土台となる処理
- 今すぐ試すなら
- MediaPipe PoseやMoveNetなど、スマホ・PC上で動く軽量な姿勢推定モデルを使う。今回はMediaPipe Poseを使用した
- サンプルコード
-
import mediapipe as mp import cv2 mp_pose = mp.solutions.pose pose = mp_pose.Pose(model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5) cap = cv2.VideoCapture('trial.mp4') fps = cap.get(cv2.CAP_PROP_FPS) landmarks_seq = [] while cap.isOpened(): ret, frame = cap.read() if not ret: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result = pose.process(rgb) if result.pose_landmarks: lm = result.pose_landmarks.landmark landmarks_seq.append([(p.x, p.y, p.visibility) for p in lm]) else: landmarks_seq.append(None) cap.release() - 注意点
- visibility(検出信頼度)が低いフレームをそのまま使うと誤差の元になる。閾値未満のフレームは後段で除外・補間する
「開始」は股関節の垂直速度+膝関節角度で見る
- できること
- 座位から立位に移る瞬間、股関節が上方向に速く動き、同時に膝関節が伸展していくという2つの動きを組み合わせて検出する
- 使える場面
- TUGの計測開始タイミングの自動検出
- 今すぐ試すなら
- STAGE1で紹介したコードのように、股関節yの移動平均から速度を計算し、膝角度が一定以上になった最初のフレームを開始点とする
- 注意点
- 浅く座っている被験者は立ち上がり動作そのものが小さく、速度の閾値を超えるまでに時間がかかることがある
「方向転換」は肩-股関節ベクトルの回転角で見る
- できること
- 肩の中点と股関節の中点を結ぶベクトルが、カメラに対してどれだけ回転したかを角度として計算し、一定角度以上の急な回転を方向転換とみなす
- 使える場面
- 3m先の目印での方向転換タイミングの自動検出
- 今すぐ試すなら
- 下記のように、フレームごとの体幹の向き(ベクトルの角度)を計算し、その角速度がピークになるフレームを方向転換とする
- サンプルコード
-
import numpy as np def trunk_angle(lm): shoulder = np.array([(lm[11][0]+lm[12][0])/2, (lm[11][1]+lm[12][1])/2]) hip = np.array([(lm[23][0]+lm[24][0])/2, (lm[23][1]+lm[24][1])/2]) vec = shoulder - hip return np.degrees(np.arctan2(vec[1], vec[0])) angles = [trunk_angle(lm) for lm in landmarks_seq if lm is not None] # atan2は±180度で値が飛ぶため、そのままdiffを取ると方向転換の瞬間に # 疑似的な急角速度が発生する。np.unwrapで連続な角度列に直してから速度を計算する angles_unwrapped = np.degrees(np.unwrap(np.radians(angles))) angular_vel = np.abs(np.diff(angles_unwrapped)) * fps turn_frame = int(np.argmax(angular_vel)) print(f"方向転換ピーク検出フレーム: {turn_frame}({turn_frame/fps:.2f}秒)") - 注意点
- この局面がもっとも姿勢推定の精度が落ちやすい。体が横向きになる一瞬、片方の肩・股関節がカメラから見えにくくなり、座標が飛ぶことがある(STAGE4で詳述)
「着座」は股関節の下降速度の反転で見る
- できること
- 着座直前は股関節が下降し続け、座面に接地した瞬間に垂直速度がほぼゼロになる。この速度反転のタイミングを着座とみなす
- 使える場面
- TUGの計測終了タイミングの自動検出
- 今すぐ試すなら
- 開始検出と同じ速度計算のロジックを使い、今度は下降速度が閾値を下回った最初のフレームを着座とする
- 注意点
- 勢いよく座る被験者と、ゆっくり確認しながら座る被験者とで、速度の反転パターンが変わる。閾値は複数試行で調整する
30回比較プロトコルと結果:ここが本題です
同一条件で30試行を実施し、理学療法士のストップウォッチとAI判定を突き合わせました。数値をすべて公開します。
実施条件:職場の同意した職員(健常成人)を対象に、同一の椅子・同一の3m歩行路・同一のスマホ設置条件で、TUGを30試行実施。各試行で理学療法士が手動ストップウォッチによる開始・方向転換通過・着座のタイミングを計測すると同時に、スマホで動画を撮影し、後日AI判定コードで解析した。
比較方法:全体タイム(開始〜着座)はBland-Altman分析と級内相関係数(ICC)で、局面ごとのタイミング(開始・方向転換・着座それぞれの検出時刻)は理学療法士の目視判定とAI判定の差(誤差、単位ミリ秒)の平均・標準偏差・最大値で評価した。
| 指標 | 結果 | 読み方の目安 |
|---|---|---|
| 全体タイム:級内相関係数(ICC) | 0.97 | 1.0に近いほど一致度が高い。0.97は「非常に高い一致」の水準 |
| 全体タイム:Bland-Altman バイアス | +0.14秒(AIがやや長め) | 開始を早め・着座を遅めに検出する分がそのまま積み上がった値 |
| 全体タイム:一致限界(95%LoA) | -0.13〜+0.41秒 | 個別試行では±0.3秒前後ズレる可能性がある |
| 開始(立ち上がり)検出誤差 | 平均 -78ms(SD 105ms) | AI判定がやや早く検出する傾向。ばらつきは小さめ |
| 方向転換検出誤差 | 平均 +162ms(SD 214ms) | 3局面の中で最も誤差が大きく、ばらつきも大きい |
| 着座検出誤差 | 平均 +58ms(SD 89ms) | 開始と同程度に安定して検出できた |
| 姿勢推定の欠測フレーム率(方向転換前後1秒) | 平均 6.8% | 他の局面(平均1.2%)と比べて明らかに高い |
全体タイムだけを見れば「ICC 0.97」「一致限界はおおむね-0.1〜+0.4秒」と、TUGのスクリーニング用途としては十分実用的な水準です。この+0.14秒というわずかなバイアスは、実は「開始をやや早く検出し、着座をやや遅く検出する」という2つの局面誤差がそのまま積み上がった結果でもあります(-78ms+58ms分の遅れ= AI側の計測区間がPTよりやや長くなる)。そして局面別に分解すると、方向転換の検出誤差だけが他の局面の倍以上(平均162ms、SD 214ms)に膨らんでいることが分かります。方向転換の誤差は開始・終了の位置を動かさないため全体タイムには表れず、局面ごとに見て初めて発見できた癖です。
import numpy as np
import matplotlib.pyplot as plt
# pt_total, ai_total: 30試行分の全体タイム(秒)のリスト
diff = np.array(ai_total) - np.array(pt_total)
mean_diff = np.mean(diff)
sd_diff = np.std(diff, ddof=1)
loa_upper = mean_diff + 1.96 * sd_diff
loa_lower = mean_diff - 1.96 * sd_diff
print(f"バイアス: {mean_diff:.2f}s, 95%一致限界: {loa_lower:.2f}〜{loa_upper:.2f}s")
mean_vals = (np.array(ai_total) + np.array(pt_total)) / 2
plt.scatter(mean_vals, diff)
plt.axhline(mean_diff, color='gray')
plt.axhline(loa_upper, color='red', linestyle='--')
plt.axhline(loa_lower, color='red', linestyle='--')
plt.xlabel('平均タイム [s]'); plt.ylabel('AI - PT [s]')
plt.title('Bland-Altman: TUG全体タイム(AI vs PT, n=30)')
plt.show()
今回のn=30は単一施設・健常職員のみのパイロットデータです。実際の患者、特に片麻痺や運動失調で方向転換動作そのものが不安定な方では、方向転換局面の誤差はさらに大きくなる可能性があります。数値は「傾向をつかむための参考値」として扱ってください。
局面別誤差を分解する:なぜ方向転換だけ誤差が大きいのか
「方向転換だけ誤差が大きい」で終わらせず、原因を分解しておくと、対策の打ちようが見えてきます。
原因①:関節の自己遮蔽(オクルージョン)
- できること
- 方向転換の途中、体が真横〜半回転の向きになる瞬間、片側の肩・股関節がもう片方の身体に隠れて画面上で見えなくなる。この間、姿勢推定モデルは座標を「推測」で補うため、誤差が生じやすい
- 使える場面
- 誤差の原因を特定したい場面
- 今すぐ試すなら
- 各フレームのvisibility(検出信頼度)をログに残し、方向転換の前後1秒だけ抜き出して平均値を比較する。今回は方向転換前後で信頼度が明らかに低下していた
- 注意点
- 信頼度が低いフレームを単純に除外すると、今度はデータの空白ができて別の誤差要因になる。次項の対策と組み合わせる
原因②:カメラ設置角度と回転方向の相性
- できること
- 被験者がカメラに対して手前に回転するか奥に回転するかで、遮蔽の起きやすさが変わる。今回は被験者から見て時計回りに回転する試行の方が、カメラ側から見て身体の厚みが分厚く映り、誤差がやや大きい傾向があった
- 使える場面
- 撮影セッティングを見直す場面
- 今すぐ試すなら
- 回転方向別(時計回り/反時計回り)に誤差を集計し、傾向差があるか確認する
- 注意点
- 今回はn=30のうち回転方向が偏っていたため、統計的に確定した傾向とまでは言えない。台数を増やして検証すべき点として記録している
対策①:カメラ2台構成での相互補完
- できること
- 正面と側面(または斜め上)にもう1台スマホを追加し、片方のカメラで遮蔽が起きても、もう片方のデータで補完する
- 使える場面
- より高い精度が必要な研究用途の場面
- 今すぐ試すなら
- 2台のタイムスタンプを同期させたうえで、visibilityが高い方のカメラのデータを優先的に採用するロジックを組む
- 注意点
- 「スマホだけで」という手軽さは失われる。日常のスクリーニング用途なら1台のままで、癖を理解した上で使う方が現実的な場合も多い
対策②:信頼度が低い区間は補間+前後の動きから推定
- できること
- 信頼度が低い区間は単純に無視せず、直前直後の動きの連続性から座標を補間し、方向転換のピークをより滑らかに推定する
- 使える場面
- 1台のスマホ運用のまま精度を上げたい場面
- 今すぐ試すなら
- Savitzky-Golayフィルタなどで角度データを平滑化し、欠測区間はスプライン補間で埋めてからピーク検出を行う
- 注意点
- 補間はあくまで推定であり、実際の動きを100%再現するものではない。誤差を減らす工夫であって、誤差をゼロにする魔法ではないと理解しておく
実装を発展させる:スマホ単体の先にある選択肢
ここから先は研究・開発寄りの内容です。「スマホだけで」の限界を理解した上で、必要に応じて選べる発展形を紹介します。
スマホのIMU(加速度・ジャイロ)を組み合わせる
- できること
- 被験者にスマホを持たせる、または腰に固定する運用に変えれば、カメラ画像だけに頼らず加速度・角速度データから方向転換のタイミングを直接検出できる
- 使える場面
- 「スマホだけで」の定義を、カメラではなくセンサー内蔵端末として広げる場面
- 今すぐ試すなら
- スマホアプリのセンサーログ機能(多くの計測アプリがCSV/JSON出力に対応)でジャイロのz軸角速度を記録し、ピーク検出で方向転換タイミングを求める
- 注意点
- 被験者にスマホを装着してもらう必要があり、「何も持たず自然に歩く」というTUG本来の条件からは外れる点に注意する
端末上でリアルタイム処理する(オンデバイス推論)
- できること
- 撮影後にPCで処理するのではなく、スマホアプリ内でリアルタイムに姿勢推定・イベント検出まで完結させ、その場でタイムを表示する
- 使える場面
- 臨床現場でその場で結果を確認したい場面
- 今すぐ試すなら
- MediaPipeにはモバイル向けの軽量モデルがあり、iOS/Androidアプリに組み込むことでオンデバイス推論が可能。プロトタイプ段階ではPCでの後処理から始め、精度が固まった段階でアプリ化するのが現実的な順序
- 注意点
- 端末の性能によって処理落ちが起こりやすく、フレームレートが不安定になると検出精度も落ちる。実機での負荷テストが欠かせない
患者データでの再検証を前提に設計する
- できること
- 今回の健常職員データを土台に、実際の対象疾患(脳卒中片麻痺、パーキンソン病、変形性膝関節症など)ごとに閾値を作り直す設計にしておく
- 使える場面
- 研究として発展させ、対象疾患ごとの検証を積み重ねていく場面
- 今すぐ試すなら
- 今回のコードの閾値部分(速度・角度)を外部の設定ファイルに切り出し、対象群ごとに調整できるようにしておく
- 注意点
- 患者データを扱う研究として発表する場合は、必ず倫理審査など院内の手続きを先に確認する
検者間信頼性そのものをAIで底上げする
- できること
- 複数の理学療法士が計測すると、ストップウォッチ側にも検者間のばらつきが生じる。AI判定を「もう一人の検者」として併記することで、検者間差そのものを可視化できる
- 使える場面
- 新人教育や、施設内での計測基準の統一を図りたい場面
- 今すぐ試すなら
- 複数の検者が同じ動画でストップウォッチ計測を行い、AI判定値を基準点としてそれぞれの検者のズレを比較する
- 注意点
- AI判定自体が絶対的な正解ではない点は常に忘れず、あくまで「もう一つの視点」として位置づける
ロードマップ一覧表
| 段階 | 項目 | できること | 今すぐ試すなら | 注意点 |
|---|---|---|---|---|
| 環境 | 1. 必要なものを揃える | スマホ・三脚・椅子・歩行路の準備 | 全身が画角に入る位置に設置 | 方向転換時の画角外れに注意 |
| 環境 | 2. 撮影条件を固定 | フレームレート・解像度・照明の統一 | 30fps・1080pで固定 | 逆光は精度低下の要因 |
| 環境 | 3. PT計測基準の統一 | 開始・方向転換・着座の定義統一 | 判定基準を事前に文書化 | 検者間で事前練習しておく |
| 環境 | 4. 被験者への説明 | 撮影・解析への同意取得 | 目的・データ保管方法を説明 | 患者対象は倫理手続きが必須 |
| 基礎 | 5. 骨格座標の取得 | 姿勢推定で関節座標を得る | MediaPipe Poseで動画を処理 | 低信頼度フレームは後段で扱いに注意 |
| 基礎 | 6. 開始の検出 | 股関節速度+膝角度で判定 | 移動平均後に速度計算 | 浅い座り方は検出が遅れやすい |
| 基礎 | 7. 方向転換の検出 | 体幹角度の角速度ピークで判定 | 肩-股関節ベクトルの回転を計算 | 最も誤差が出やすい局面 |
| 基礎 | 8. 着座の検出 | 下降速度の反転で判定 | 開始検出と同ロジックを流用 | 座り方の勢いで閾値調整が必要 |
| 比較 | 9. 30回比較の実施 | PT計測とAI判定を同時記録 | Bland-Altman・誤差集計で評価 | 単施設パイロットである点を明記 |
| 分解 | 10-12. 誤差原因の分解と対策 | 遮蔽・カメラ角度の影響を特定 | 信頼度ログで区間別に比較 | 対策は精度と手軽さのトレードオフ |
| 発展 | 13-16. 実装の発展 | IMU併用・オンデバイス化・疾患別検証 | 閾値を外部設定に切り出す | 患者データ利用は倫理審査を先に |
そのまま使えるコード集(コピー&ペースト用)
import mediapipe as mp
import cv2
def extract_landmarks(video_path):
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(model_complexity=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
seq = []
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
result = pose.process(rgb)
if result.pose_landmarks:
lm = result.pose_landmarks.landmark
seq.append([(p.x, p.y, p.visibility) for p in lm])
else:
seq.append(None)
cap.release()
return seq, fps
import numpy as np
from collections import deque
def detect_seat_off_and_sit(landmarks_seq, fps, window=5,
v_thresh=0.35, knee_thresh=100):
buf = deque(maxlen=window)
prev_y = None
seat_off, sit_down = None, None
for i, lm in enumerate(landmarks_seq):
if lm is None:
continue
hip_y = (lm[23][1] + lm[24][1]) / 2
knee_angle = calc_knee_angle(lm) # 別途定義した関節角度計算関数
buf.append(hip_y)
smoothed = np.mean(buf)
vel = 0 if prev_y is None else (prev_y - smoothed) * fps
prev_y = smoothed
if seat_off is None and vel > v_thresh and knee_angle > knee_thresh:
seat_off = i
if seat_off is not None and sit_down is None and i > seat_off + fps:
if vel < -v_thresh * 0.3:
sit_down = i
return seat_off, sit_down
import numpy as np
def detect_turn(landmarks_seq, fps):
angles = []
for lm in landmarks_seq:
if lm is None:
angles.append(np.nan)
continue
sh = np.array([(lm[11][0]+lm[12][0])/2, (lm[11][1]+lm[12][1])/2])
hip = np.array([(lm[23][0]+lm[24][0])/2, (lm[23][1]+lm[24][1])/2])
vec = sh - hip
angles.append(np.degrees(np.arctan2(vec[1], vec[0])))
angles = np.array(angles)
valid = ~np.isnan(angles)
angles[~valid] = np.interp(np.flatnonzero(~valid),
np.flatnonzero(valid), angles[valid])
# ±180度境界での飛びを補正してから角速度を計算(unwrapしないと
# 方向転換以外の瞬間にも疑似ピークが立つことがある)
angles_unwrapped = np.degrees(np.unwrap(np.radians(angles)))
angular_vel = np.abs(np.diff(angles_unwrapped)) * fps
return int(np.argmax(angular_vel))
import numpy as np
def summarize_errors(pt_times, ai_times, label):
diff_ms = (np.array(ai_times) - np.array(pt_times)) * 1000
print(f"{label}: 平均誤差 {diff_ms.mean():.0f}ms, "
f"SD {diff_ms.std(ddof=1):.0f}ms, "
f"最大 {np.abs(diff_ms).max():.0f}ms")
# 使用例
summarize_errors(pt_start, ai_start, "開始")
summarize_errors(pt_turn, ai_turn, "方向転換")
summarize_errors(pt_sit, ai_sit, "着座")
% 動画ファイル名や保存フォルダに被験者名をそのまま残してしまう例 yamada_taro_trial01.mp4 % → 検証後に動画を削除し忘れると、個人が特定できる映像データが残り続ける
精度と臨床利用の注意点
- 今回の30試行は単一施設・健常成人職員によるパイロット検証であり、査読を経た臨床研究ではない。数値は傾向をつかむための参考値として扱う
- 実際の患者(片麻痺・パーキンソン病・変形性関節症など)では動作パターンが異なり、特に方向転換局面の誤差は今回より大きくなる可能性がある
- AI判定は「参考記録・補助ツール」であり、転倒リスクの最終判断や介入方針の決定は、これまで通り理学療法士など専門職が行う
- 撮影した動画・関節座標データには個人を特定できる情報が含まれる。匿名化、保存期間、削除ルールを施設の個人情報保護規定に沿って整備する
- 市販の医療機器としての承認を得たものではないため、介護保険の区分判定や診断など公的な用途にそのまま使用しない
- 継続的に運用する場合は、対象とする患者層でのデータを追加取得し、閾値やアルゴリズムを見直すプロセスを組み込む
保存版:スマホTUG自動計測を試す前のチェックリスト
まとめ
スマホ1台とAI(姿勢推定)だけで、TUGの開始・方向転換・着座を自動検出する仕組みと、実際に30回計測した結果を紹介しました。全体タイムはストップウォッチとほぼ一致する一方、方向転換局面だけ誤差が目立つという、実際に手を動かして初めて見えてくる癖がありました。
この結果が示しているのは、「AIは使えない」でも「AIで十分」でもなく、「どこまでなら信頼できて、どこから人の目が必要か」という線引きの重要性です。局面ごとの誤差を公開することで、これから同じ取り組みをする方が、同じ壁で足止めされずに一歩先から始められればと思います。
TUGのAI自動計測は、記録の手間を減らし、理学療法士がより多くの時間を評価の解釈や介入そのものに使えるようにするための道具です。専門職の判断を置き換えるものではなく、判断の材料をより速く、より客観的に用意するためのものだと捉えています。
コメント