1. イントロダクション:骨格検知が「動作」を理解する仕組み
AI技術をビジネスの現場に実装するプロジェクトにおいて、近年、カメラだけで人間の動きをデジタル化できる「骨格検知(Pose Estimation)」技術が急速に身近になっています。スマートフォンのアプリで姿勢矯正を行ったり、工場の作業員の安全管理に導入したりと、その応用範囲は広がる一方です。
しかし、開発現場でしばしば課題となるのが、「ライブラリを動かして線画を表示するところまではできたが、そこから『特定の動作』をどう判定すればいいのか分からない」という点です。
画像認識と骨格検知の決定的な違い
多くの人が最初に躓くのは、「画像認識(Classification)」と「骨格検知(Pose Estimation)」のアプローチの違いです。
画像認識では、画像全体の特徴量から「これは猫」「これは犬」と判別します。一方、骨格検知が提供してくれるのは、関節点(ランドマーク)の座標データです。つまり、AIが出力するのは「今、右手が上がっている」という事実ではなく、「右手首の座標は (x: 0.8, y: 0.2) にある」という数値データに過ぎません。
ここから「右手を挙げている」という意味(セマンティクス)を抽出するのは、エンジニアが実装すべきロジックの領域なのです。
「座標データ」から人間の動きを定義するアプローチ
本記事では、単にAIモデルに大量の画像を学習させてブラックボックス的に判定させるのではなく、取得した座標データを数学的(幾何学的)に処理することで動作を定義するアプローチを深掘りします。
例えば、「腕組み」という動作を考えてみましょう。
これをAIに画像として学習させることも可能ですが、ロジックで考えると以下のように分解できます。
- 左手首と右肘の距離が近い
- 右手首と左肘の距離が近い
- 両肘の角度が鋭角である
このように動作を座標とベクトルの関係性に落とし込むことで、学習データがなくても判定が可能になり、何より「なぜその判定になったのか」が説明可能になります。
本チュートリアルのゴール
今回は、Googleが提供する高速なMLソリューション「MediaPipe」とPythonを使って、以下のステップで実装を進めていきます。
- 環境構築とデータ理解:MediaPipeが出力するデータの構造を把握する
- 静的ポーズ判定:幾何学計算を用いて「特定のポーズ」を検知する
- 動的ジェスチャー判定:時系列データを扱い「動き」を検知する
ブラックボックスなAI利用から一歩踏み出し、ロジックで動作を記述する面白さを体験してください。
2. 開発環境のセットアップとMediaPipeの基礎
まずは実際にコードを動かすための準備を整えましょう。ここではPython環境(ローカルまたはGoogle Colab)を想定しています。
ライブラリの導入
画像処理の定番であるOpenCVと、今回の主役であるMediaPipeをインストールします。
pip install opencv-python mediapipe numpy
MediaPipe Poseモジュールの導入とHello World
まずは、Webカメラ(または静止画)から骨格を検出し、その結果を描画する最小限のコードを動かしてみましょう。これがすべての出発点です。
import cv2
import mediapipe as mp
import numpy as np
# MediaPipeの初期化
mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils
# Webカメラのキャプチャ開始(0は標準カメラ)
cap = cv2.VideoCapture(0)
# Poseモデルのセットアップ
# min_detection_confidence: 検出の信頼度閾値(0.5 = 50%)
# min_tracking_confidence: トラッキングの信頼度閾値
with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# MediaPipeはRGB画像を期待するため、BGRからRGBへ変換
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image.flags.writeable = False
# 推論実行
results = pose.process(image)
# 描画のためにBGRに戻す
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
# ランドマークが検出された場合、描画する
if results.pose_landmarks:
mp_drawing.draw_landmarks(
image,
results.pose_landmarks,
mp_pose.POSE_CONNECTIONS
)
cv2.imshow('MediaPipe Feed', image)
if cv2.waitKey(10) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
このコードを実行すると、カメラ映像に身体の骨格ラインがオーバーレイ表示されるはずです。
ランドマーク(関節点)データの構造を解剖する
ここで重要なのは、画面に線が出たことよりも、results.pose_landmarks の中身を理解することです。
MediaPipe Poseは、身体の各部位に対応する33個のランドマークを返します。
各ランドマークには以下の属性があります。
- x: 画面左端を0.0、右端を1.0とした水平位置
- y: 画面上端を0.0、下端を1.0とした垂直位置
- z: 腰の中心を原点とした深度(カメラに近いほど負の値)
- visibility: その点が画面内に見えているかの確率(0.0〜1.0)
例えば、「左手首」の座標を取得するには以下のようにアクセスします。
# ランドマークリストを取得
landmarks = results.pose_landmarks.landmark
# 左手首(インデックス15)のデータを取得
left_wrist = landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value]
# 座標を表示
print(f"左手首 X: {left_wrist.x}, Y: {left_wrist.y}, Z: {left_wrist.z}")
この「0.0〜1.0の正規化された座標」が手に入れば、あとは数学の世界です。画像の解像度やカメラの距離に依存しないロジックを組む準備が整いました。
3. ロジックの実装1:幾何学計算による「静的ポーズ」の判定
ここからが本題です。AIが出力した座標を使って、特定のポーズを判定するロジックを実装します。
例として、フィットネスアプリなどでよくある「ダンベルカール」の動作判定、つまり「肘が曲がっているか、伸びているか」を数値化してみましょう。
座標からベクトルを生成し、角度を計算する
肘の曲がり具合を知るには、「肩」「肘」「手首」の3点の座標が必要です。これら3点が成す角度を計算します。
数学的には、ベクトル解析や逆三角関数(アークタンジェント)を使用します。ここでは numpy の arctan2 関数を使って、3点間の角度を算出する関数を作成します。
def calculate_angle(a, b, c):
"""
3点の座標(a, b, c)から、bを中心とした角度(0〜180度)を計算する関数
a: 始点(例:肩)
b: 中心点(例:肘)
c: 終点(例:手首)
"""
a = np.array(a) # First
b = np.array(b) # Mid
c = np.array(c) # End
# アークタンジェントを使って各ベクトルの絶対角度を計算
# atan2(y, x) はラジアンを返す
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
この関数の中で行われているのは、「肘から手首へのベクトル」と「肘から肩へのベクトル」が成す角度の計算です。atan2 は座標成分から角度をダイレクトに算出できるため、非常に便利です。
実践:『肘が曲がっているか』を数値で判定する
作成した関数をメインループに組み込みます。
# ...(前略:MediaPipeのループ内)...
if results.pose_landmarks:
landmarks = results.pose_landmarks.landmark
# 必要な3点の座標を取得(xとyのみ使用)
# [0]がx, [1]がyに対応するようにリスト化
shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
# 角度を計算
angle = calculate_angle(shoulder, elbow, wrist)
# 判定ロジック
# 閾値を設定:30度以下なら「完全に曲げた」、160度以上なら「伸ばした」
stage = None
if angle > 160:
stage = "DOWN"
if angle < 30 and stage == "DOWN":
stage = "UP"
print("回数カウントアップ!")
# 画面に角度を表示
cv2.putText(image, str(int(angle)),
tuple(np.multiply(elbow, [640, 480]).astype(int)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2, cv2.LINE_AA
)
なぜこのアプローチが重要なのか
この実装のポイントは、「曲がっている」という曖昧な状態を「角度 < 30度」という明確な数値ルールに変換したことです。
これにより、「もう少し深く曲げてください」といったフィードバック機能を実装したり、リハビリテーションにおける可動域の測定に応用したりすることが可能になります。
4. ロジックの実装2:時系列データを扱う「動的ジェスチャー」の分類
静止画での判定ができたら、次は「動き(モーション)」の判定です。例えば「手を振る(バイバイ)」という動作は、一瞬の画像だけでは判定できません。時間の経過とともに座標がどう変化したかを見る必要があります。
「点の状態」から「線の動き」へ:フレーム間の差分
動的ジェスチャーを検知するためには、過去のフレーム情報を記憶しておく必要があります。Pythonの collections.deque を使うと、一定数のデータを保持するキュー(待ち行列)を簡単に作れます。
ここでは「手を振る」動作を、手首のX座標が一定時間内に左右に大きく変動することと定義して実装してみましょう。
from collections import deque
# 過去30フレーム分の手首のX座標を保存するバッファ
history_length = 30
wrist_x_history = deque(maxlen=history_length)
# ...(MediaPipeループ内)...
if results.pose_landmarks:
# 現在の左手首X座標を取得
current_x = landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x
wrist_x_history.append(current_x)
# バッファが溜まったら解析
if len(wrist_x_history) == history_length:
# 最大値と最小値の差(振れ幅)を計算
movement_range = max(wrist_x_history) - min(wrist_x_history)
# 振れ幅が一定以上(例:画面幅の20%以上)あれば「手を振った」とみなす
# さらに、単純な移動ではなく「振動」しているかを確認するために
# ゼロ交差数(増減の反転回数)を数えるロジックを入れるとなお良い
if movement_range > 0.2:
cv2.putText(image, "WAVING HAND!", (50, 50),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
移動平均を使ったデータのスムージング(ノイズ除去)
実際の現場、特にWebカメラ環境では、じっとしていても照明の影響などで座標が微妙に震える「ジッター(Jitter)」というノイズが発生します。これが誤検知の原因になります。
これを防ぐために、実務の現場では移動平均フィルタやカルマンフィルタを適用します。簡易的な移動平均の実装は以下の通りです。
# ノイズ除去:直近5フレームの平均値を「現在の位置」として扱う
smoothed_x = sum(list(wrist_x_history)[-5:]) / 5
このように生データをそのまま使わず、フィルタリング処理を挟むことが、実用的なアプリケーションを作るための鉄則です。
5. 応用と発展:機械学習モデルとの連携(概要)
ここまで、幾何学計算とルールベースによる動作判定の手法を解説しました。しかし、実用的なアプリケーションを開発する現場では、ルールベースのアプローチだけでは対応しきれない複雑な壁に直面することは珍しくありません。
- 複雑な動作: 「テニスのサーブ」や「ダンスのステップ」など、時系列的な変化や多数の関節の関係性が複雑に絡み合う場合、すべての条件をルールとして記述するのは困難です。
- 個人差: 人によって骨格の比率やフォームの癖は大きく異なります。そのため、一律の閾値を設定するだけでは、多様なユーザーに対して正確な判定を行うことができません。
このような課題に直面したとき、条件分岐のルールを手作業で追加し続けるのではなく、機械学習(Machine Learning)の力を借りるアプローチへの転換が極めて有効です。
ルールベースの限界と機械学習の出番
ここまでの幾何学計算の知識があれば、機械学習への接続は非常にスムーズに行えます。なぜなら、機械学習モデルに入力すべき質の高い「特徴量」が何であるかを、すでに明確に理解できているからです。
機械学習モデルに入力するのは、データ容量の大きい画像データそのものではありません。これまで扱ってきた「33個のランドマーク座標(x, y, z)」や、そこから計算した「角度」「距離」といった意味のある数値データです。これらを特徴量として扱うことで、ノイズが少なく、非常に効率的な学習プロセスを実現できます。画像から直接学習させるよりも、骨格という本質的な情報に絞り込むことが、精度向上の鍵となります。
座標データを特徴量としてCSVに保存する
機械学習モデルを作成するための第一歩は、良質なデータセットの構築です。MediaPipeを活用すれば、以下の手順で独自のデータセットを効率的に作成できます。
- ポーズの実演: カメラの前で判定したい特定のポーズ(例:「正解のスクワットフォーム」「膝が内側に入った不正解フォーム」)をとります。
- 座標の抽出: リアルタイムで取得される33個のランドマーク座標を正確に抽出します。
- 正規化と保存: ユーザーの立ち位置やカメラとの距離、画像サイズに依存しないよう座標データを正規化し、CSVファイルなどに正解ラベルを付与して記録します。
このプロセスを経ることで、複雑な画像処理の問題を、より扱いやすく計算負荷の低い「数値データの分類問題」へと変換できます。適切なデータの前処理こそが、後続の機械学習モデルの性能を決定づけます。
scikit-learnを用いた簡易分類器の作成フロー
良質なデータさえ揃えば、Pythonの代表的な機械学習ライブラリである scikit-learn などを用いて、手軽に専用の分類器を作成できます。実装の大まかな流れは以下の通りです。
- データ収集:
MediaPipeを稼働させながら対象のポーズをとり、正規化された座標データをCSVに蓄積します。- CSVのカラム構成例:
label, x1, y1, z1, x2, y2, z2, ..., x33, y33, z33
- CSVのカラム構成例:
- 学習:
収集したCSVデータを読み込み、分類アルゴリズム(Random Forest、SVM、KNNなど)を用いてモデルに学習させます。入力が軽量な数値データであるため、膨大な時間を要することなく、学習は瞬時に完了することが大半です。 - 推論:
アプリケーションの実行時に、リアルタイムで取得した座標データを学習済みモデルに入力し、予測ラベル(例:正しいフォームか否か)を即座に受け取ります。
この「座標データを特徴量として扱う」というアプローチの最大の利点は、圧倒的な計算コストの低さにあります。
画像全体を入力として画素単位で解析するCNN(畳み込みニューラルネットワーク)などのディープラーニング手法は、強力な特徴抽出能力を持ちます。近年ではNVIDIAのTAO Toolkitなどを活用し、エッジAIデバイス(Jetsonなど)向けにCNNモデルを最適化・転移学習させる高度なアプローチも普及しています。
しかし、最初からデータ量が極めて少ない「座標データ」のみを抽出して特徴量として扱う本手法であれば、特別なハードウェア最適化や複雑な環境構築を行わずとも、一般的なCPU環境やモバイルデバイス上で十分に高速な推論が可能です。
高度なGPU環境を必要とせず、ブラウザやスマートフォン上で軽快に動作するAIアプリケーションを構築したい場合、この「MediaPipe × 軽量機械学習モデル」という組み合わせは、現時点でも非常に合理的で実用性の高い選択肢と言えます。
6. まとめとトラブルシューティング
骨格検知AIの実装は、ライブラリの呼び出しから始まり、幾何学的なロジック構築、そして機械学習モデルへの応用へと深まっていきます。
最後に、現場でよく直面する課題とその対策をまとめておきます。
よくある誤検知とその対策
- オクルージョン(遮蔽): 手が身体の後ろに隠れたり、机の下に入ったりすると、座標が暴れることがあります。
- 対策: ランドマークの
visibility属性を確認し、信頼度が低い(例:0.5未満)場合は判定処理をスキップするロジックを入れます。
- 対策: ランドマークの
- カメラアングル: 正面からの映像を想定したロジックは、横からの映像では破綻します。
- 対策: ロジック内で3次元座標(z軸)も考慮するか、学習データに多様なアングルを含める必要があります。
- 処理速度: 高解像度の画像をそのまま処理するとFPSが落ちます。
- 対策: MediaPipeへの入力前に画像をリサイズしたり、毎フレーム処理するのではなく、2〜3フレームに1回推論を行ったりする間引き処理が有効です。
次のアクション
今回解説したコードは、あくまで基本の「型」です。これをベースに、「スクワットの深さを判定する」「工場のライン作業で禁止動作を検知する」といった具体的なソリューションへ発展させることができます。
実装の際は、公式ドキュメントや動作判定に使用できる計算ロジックのリファレンスを参照しながら進めることをおすすめします。
骨格検知AI 実装完全ガイドブック(無料)
骨格検知AIの実装をさらに深めるためには、以下のようなトピックについて体系的にまとめたガイドブックや技術資料を参照することが有効です。
- 主要ポーズ(スクワット、腕立て、挙手など)の判定コード実装例
- 誤検知を防ぐためのフィルタリング処理 実装パターン
- 自作データセット作成用 CSV出力スクリプト
これらは、これから動作解析AIの開発に取り組むエンジニアにとって重要な知識となります。
コメント