AIによる動画からの人間の行動認識とポーズ推定技術の活用法

脱ストップウォッチ宣言。PythonとMediaPipeで作る「現場作業の自動分析AI」実装ガイド

約11分で読めます
文字サイズ:
脱ストップウォッチ宣言。PythonとMediaPipeで作る「現場作業の自動分析AI」実装ガイド
目次

この記事の要点

  • 動画からの行動認識とポーズ推定で人間の動きをAIが解析
  • 作業効率化、スポーツ分析、ヘルスケアなど幅広い分野で応用
  • PythonとMediaPipeを用いた実践的なAI実装が可能

製造ラインや物流倉庫の現場改善(カイゼン)において、最も基本的かつ重要なプロセスが「現状把握」です。しかし、多くの現場ではいまだに、IE(Industrial Engineering)担当者がストップウォッチを片手に作業員の背後に立ち、紙とペンでタイムを計測している光景が見られます。

このアナログな手法には、常に課題があります。「計測される側」の心理的負担(ホーソン効果による作業スピードの変容)と、「計測する側」の膨大な集計工数。この双方が、真の改善を阻むボトルネックになっているからです。

ここで提案したいのが、AIによるポーズ推定(Pose Estimation)技術の導入です。長年の開発現場で培った知見から言えば、技術の本質を見抜き、ビジネスへの最短距離を描くためには、まず動くものを作って検証することが不可欠です。

従来の「目視・ストップウォッチ」計測の限界

人手による計測には、構造的な限界があります。

  • サンプリングの偏り: 全数調査は物理的に不可能で、特定の時間帯や熟練工のデータに偏りがちです。
  • データの粒度: 「作業開始」と「終了」しか記録できず、その間の「無駄な動き」や「姿勢の負荷」までは数値化できません。
  • タイムラグ: 計測からデータ化、分析までに数日〜数週間のラグが発生し、リアルタイムなフィードバックができません。

実際、ビデオ撮影した映像を目視で分析するのに、録画時間の約3〜5倍の工数を要する事例もあります。1時間の作業を分析するのに3時間以上かかる計算です。これでは、経営層が求めるスピード感でPDCAサイクルを回すことは不可能です。

骨格検知(Pose Estimation)で取得できるデータとは

ポーズ推定は、カメラ映像から人物の関節点(キーポイント)を検出し、その座標(x, y, z)を時系列データとして取得する技術です。

特筆すべきは、プライバシーへの配慮です。顔認識技術とは異なり、ポーズ推定は「誰が」ではなく「どのような動きをしているか」に焦点を当てます。個人の特定を行わずに骨格情報だけを抽出・保存することが可能なため、GDPR(EU一般データ保護規則)や国内の個人情報保護ガイドラインに準拠した形での導入が比較的容易です。データガバナンスの観点からも、経営陣が安心してGOサインを出せる技術と言えるでしょう。

取得できるデータはシンプルですが強力です。

  • 位置情報: 作業員が所定の位置にいるか。
  • 動作タイミング: 部品を取りに行き、組み付けるまでの秒数。
  • 身体負荷: 腰や膝の曲がり具合から、エルゴノミクス(人間工学)的なリスク評価。

本記事で作成するプロトタイプ:『特定の動作時間を自動計測するAI』

今回は理論だけでなく、明日から現場で使えるプロトタイプを一緒に作りましょう。「まず動くものを作る」のが、最も確実な検証アプローチです。目指すゴールは以下の通りです。

  1. Webカメラ映像から作業員の骨格をリアルタイム検知。
  2. 「部品を取って(腕を伸ばす)、手元に戻す(腕を曲げる)」サイクルを自動カウント。
  3. 各サイクルの所要時間を計測し、タイムスタンプ付きでCSV出力。

使用するのはPythonと、Googleが開発したオープンソースライブラリ「MediaPipe」です。高価なGPUサーバーは不要。お手持ちのノートPCで十分動作します。それでは、エンジニアリングの世界へ飛び込みましょう。準備はいいですか?

2. 開発環境のセットアップとMediaPipeの基礎

まずは開発環境を整えます。Python(バージョン3.7〜3.10推奨)がインストールされている前提で進めます。今回使用するライブラリは、画像処理のデファクトスタンダードである「OpenCV」と、GoogleのAI技術が詰まった「MediaPipe」です。

必要なライブラリのインストール

ターミナル(Windowsならコマンドプロンプト)を開き、以下のコマンドを実行してください。

pip install opencv-python mediapipe numpy pandas
  • opencv-python: 映像の取得、画像処理、描画に使用。
  • mediapipe: 高速かつ高精度な骨格推定モデル。
  • numpy: 角度計算などの数値演算。
  • pandas: 計測データのCSV出力。

最小構成で動かす:Webカメラから骨格を可視化する

環境が整ったら、まずは「正しく骨格が認識されるか」を確認します。以下のコードを pose_test.py という名前で保存し、実行してみてください。

import cv2
import mediapipe as mp

# MediaPipeの描画ユーティリティとPoseモデルの初期化
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose

# Webカメラのキャプチャ開始(0は標準カメラ。外部カメラなら1や2に変更)
cap = cv2.VideoCapture(0)

# Poseモデルのセットアップ
# min_detection_confidence: 検出の信頼度閾値
# 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:
            print("カメラ映像が取得できません。")
            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,
                mp_drawing.DrawingSpec(color=(245,117,66), thickness=2, circle_radius=2),
                mp_drawing.DrawingSpec(color=(245,66,230), thickness=2, circle_radius=2)
            )

        cv2.imshow('MediaPipe Feed', image)

        # 'q'キーで終了
        if cv2.waitKey(10) & 0xFF == ord('q'):
            break

cap.release()
cv2.destroyAllWindows()

実行すると、画面に自分の姿が映り、身体の上にワイヤーフレームが重なって表示されるはずです。もし動きに合わせて線が追従していれば成功です。この「動くワイヤーフレーム」こそが、AIが認識しているあなたの身体データそのものです。少し未来を感じませんか?

33個のランドマークとその意味を理解する

画面上の点はランダムに打たれているわけではありません。MediaPipe Poseは全身33個のランドマークを検出します。各点には固定のインデックス(番号)が割り振られています。

今回の作業分析で特に重要になるのは上半身のデータです。

  • 11: 左肩 (LEFT_SHOULDER)
  • 12: 右肩 (RIGHT_SHOULDER)
  • 13: 左肘 (LEFT_ELBOW)
  • 14: 右肘 (RIGHT_ELBOW)
  • 15: 左手首 (LEFT_WRIST)
  • 16: 右手首 (RIGHT_WRIST)

プログラムの中では results.pose_landmarks.landmark[11] のようにアクセスし、x, y, z 座標(0.0〜1.0の正規化された値)と、visibility(その点が見えているかの信頼度)を取得します。

3. ロジック実装:『動き』を数学的に定義する

開発環境のセットアップとMediaPipeの基礎 - Section Image

骨格が表示されただけでは、まだビジネス価値はありません。ここから「意味のある動作」を抽出するロジックを組み込みます。今回は最も汎用性が高い「関節角度」を用いた判定を行います。

静止画ではなく「動作」を判定するための考え方

「作業中」かどうかをAIに判断させる方法はいくつかありますが、現場実装で最も手軽かつ確実なのがルールベース判定です。

例えば「ピッキング作業」を分解すると、「腕を伸ばして部品を取る」→「腕を曲げて手元に持ってくる」という動作の繰り返しになります。これを幾何学的に言い換えると以下のようになります。

  1. State A (Reach): 肘の角度が160度以上(腕が伸びている)
  2. State B (Retract): 肘の角度が40度以下(腕が曲がっている)

この角度変化を監視し、State AからState Bへ、そして再びState Aに戻った回数を数えれば、作業サイクル数が算出できます。

ベクトルの内積を用いた関節角度の計算アルゴリズム

3点(肩、肘、手首)の座標から肘の角度を求めるには、高校数学で習った「アークタンジェント(逆正接)」を使います。難しく聞こえるかもしれませんが、NumPyを使えば数行のコードで済みます。

コード実装:calculate_angle関数の作成

以下の関数は、あらゆる3点間の角度計算に応用できる便利なパーツです。プロジェクトの共通関数として定義しておきましょう。

import numpy as np

def calculate_angle(a, b, c):
    """
    3点の座標から角度を計算する関数
    a: 始点 [x, y] (例: 肩)
    b: 中心点 [x, y] (例: 肘)
    c: 終点 [x, y] (例: 手首)
    戻り値: 角度(0〜180度)
    """
    a = np.array(a) # First
    b = np.array(b) # Mid
    c = np.array(c) # End
    
    # アークタンジェント2を使ってラジアン(弧度法)を計算
    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

この関数にランドマークの座標を渡せば、現在の関節角度が数値として返ってきます。これをしきい値と比較することで、プログラムは「今、腕が伸びている」と理解できるようになります。

4. アプリケーション化:作業サイクルの自動計測と状態管理

ロジックの準備ができたら、アプリケーション全体を組み上げます。単に回数を数えるだけでなく、「いつ、どのくらいの時間をかけて1回の作業が行われたか」を記録し、CSVファイルに書き出す機能を実装します。これこそが、現場が求めている「分析データ」であり、経営改善に直結するインサイトの源泉です。

状態遷移(ステートマシン)の導入

人間の動きは連続的で、ノイズを含みます。少し手が震えただけでカウントが増えてしまわないよう、チャタリング防止の仕組みが必要です。ここではシンプルなステートマシン(状態遷移)を導入します。

  • State: DOWN: 腕が伸びている状態(待機・作業開始前)
  • State: UP: 腕が曲がっている状態(作業中)

「DOWN」から「UP」に変わり、再び「DOWN」に戻った瞬間を「1サイクル完了」と定義します。

動作の開始・終了判定とカウンティング処理

以下が、計測ロジックを組み込んだメインコードです。ここでは左腕(肩・肘・手首)の動きを計測対象としています。

import cv2
import mediapipe as mp
import numpy as np
import pandas as pd
import time

# 前述のcalculate_angle関数をここに定義(省略)

# --- 変数初期化 ---
counter = 0 
stage = None
start_time = 0
cycle_times = [] # 作業データ記録用リスト

# MediaPipe初期化
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose

cap = cv2.VideoCapture(0)

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

        # 画像処理
        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)

        # ランドマーク抽出とロジック処理
        try:
            landmarks = results.pose_landmarks.landmark
            
            # 左腕の座標を取得
            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)
            
            # 画面上に角度を表示(デバッグ用)
            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)
            
            # --- カウンティングロジック ---
            # 腕が伸びた状態(160度以上)
            if angle > 160:
                # 直前までUP状態だった場合=1サイクル終了とみなす
                if stage == 'up':
                    stage = 'down'
                    counter += 1
                    end_time = time.time()
                    duration = end_time - start_time
                    print(f"Cycle {counter}: {duration:.2f} sec")
                    
                    # データを記録
                    cycle_times.append({
                        'cycle_id': counter,
                        'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
                        'duration': round(duration, 2)
                    })
                else:
                    stage = 'down'
            
            # 腕が曲がった状態(30度以下)かつ、現在DOWN状態の場合
            if angle < 30 and stage == 'down':
                stage = 'up'
                start_time = time.time() # 作業開始時間を記録
                
        except Exception as e:
            pass # フレームアウト等で検出できない場合はスキップ

        # --- 情報表示パネルの描画 ---
        cv2.rectangle(image, (0,0), (250,80), (245,117,16), -1)
        
        # 回数表示
        cv2.putText(image, 'REPS', (15,25), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
        cv2.putText(image, str(counter), (10,70), 
                    cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 2, cv2.LINE_AA)
        
        # 状態表示
        cv2.putText(image, 'STAGE', (90,25), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
        cv2.putText(image, str(stage), (85,70), 
                    cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 2, cv2.LINE_AA)

        cv2.imshow('Work Analysis AI', image)

        if cv2.waitKey(10) & 0xFF == ord('q'):
            break

cap.release()
cv2.destroyAllWindows()

# --- CSV保存 ---
if cycle_times:
    df = pd.DataFrame(cycle_times)
    df.to_csv('work_analysis_log.csv', index=False)
    print("\n計測データを 'work_analysis_log.csv' に保存しました。")
else:
    print("\n計測データはありませんでした。")

結果の可視化とCSVエクスポート機能の実装

このコードを実行し、カメラに向かって左手を「伸ばす→曲げる→伸ばす」と動かしてみてください。画面左上のカウンターが増え、コンソールに「Cycle 1: X.XX sec」と表示されれば成功です。

プログラムを終了('q'キー)すると、同じフォルダに work_analysis_log.csv が生成されます。これを開けば、「何時何分に」「何秒かかったか」が一目瞭然です。このCSVさえあれば、Excelでヒストグラムを作って作業のバラつきを分析したり、標準時間を算出したりすることが容易になります。

5. 現場導入に向けた課題とチューニングのヒント

アプリケーション化:作業サイクルの自動計測と状態管理 - Section Image

プロトタイプが動いたからといって、すぐに現場投入できるほど甘くはありません。ラボ(実験室)とフィールド(現場)の間には、越えなければならない壁があります。実務の現場で直面しやすい課題と、その対策を共有します。

照明条件やカメラアングルによる精度の変化

工場内は意外と暗かったり、逆光だったりします。MediaPipeは比較的ロバスト(頑健)ですが、極端な照明条件では検出率が低下します。

  • 照明: 作業エリアの照度が500ルクス以下の場合、安価なUSB LEDライトを作業スペースに追加するだけで精度が改善する可能性があります。
  • アングル: 作業者の真横や真後ろではなく、斜め前方45度からの撮影が、最も関節の重なり(オクルージョン)が少なく、安定して検出できる傾向があります。

遮蔽(オクルージョン)への対策

作業中に体が機械の影に入ったり、手元が商品で隠れたりすることは日常茶飯事です。ランドマークが見えなくなると、座標データが異常値を示し、誤検知につながります。

  • 対策: コード内の results.pose_landmarks.landmark[i].visibility を活用しましょう。この値(0.0〜1.0)が低い場合(例:0.5以下)は、計測ロジックを一時停止したり、前回の座標を保持したりする例外処理を加えることで、データの信頼性を担保できます。

処理速度の最適化(軽量モデルへの切り替え)

古いPCやRaspberry Piのようなエッジデバイスで動かす場合、処理落ち(FPS低下)が発生することがあります。

  • 対策: mp_pose.Pose のパラメータ model_complexity を調整してください。
    • 0: 軽量モデル(高速だが精度は低い)
    • 1: 標準モデル(デフォルト)
    • 2: 高精度モデル(重いが正確)
      リアルタイム性が求められる現場計測では、まず 1 を試し、遅延が気になるなら 0 に切り替えるのが定石です。

次のステップ:カスタムモデル学習への道

今回紹介したのは、単純な「角度」によるルールベース判定です。しかし、「ネジの締め忘れ」や「不安全行動」など、角度だけでは定義しきれない複雑な動きもあると考えられます。

その場合は、今回取得したランドマークデータを特徴量として、LSTM(Long Short-Term Memory)などの時系列データを扱える機械学習モデルを別途トレーニングするステップに進むことになります。ここまで来れば、単なる「計測ツール」を超え、熟練工の技能継承や異常検知システムへと進化させることができます。

まとめ:データが現場の景色を変える

前述のcalculate_angle関数をここに定義(省略) - Section Image 3

たった数十行のPythonコードで、人間の動きをデジタルデータに変換できることを実感いただけたでしょうか。

  • 可視化: 目に見えなかった「作業時間」が数値になる。
  • 自動化: 計測の手間がゼロになり、全数データ収集が可能になる。
  • 客観化: 属人的な評価から脱却し、データに基づいたカイゼン議論ができるようになる。

これが、AI駆動開発がもたらす現場の変革です。

もちろん、今回のコードはあくまで出発点です。「自社の特殊な作業工程に合わせたい」「既存の生産管理システムと連携させたい」「もっと複雑な異常検知をしたい」といった具体的な要望が出てくるはずです。現場ごとの制約条件や特殊な動作に対応するには、さらなるチューニングと専門的な知見が必要です。

現場のDXは、まず「測る」ことから始まります。あなたの書くコードが、現場の景色を変える第一歩になることを応援しています。

脱ストップウォッチ宣言。PythonとMediaPipeで作る「現場作業の自動分析AI」実装ガイド - Conclusion Image

コメント

コメントは1週間で消えます
コメントを読み込み中...