MediaPipeによるしぐさ検出(ソースコードと実行結果)

概要

動画やウェブカメラの映像から、人物の全身543点のランドマーク(顔468点、両手21点ずつの計42点、体33点)を検出し、その時間変化からしぐさを判定するプログラムである。まばたきの判定にはEAR(Eye Aspect Ratio)、手の震えの判定には高周波成分の抽出、肩の緊張の判定には位置の変動係数を用いる。ランドマークの検出にはMediaPipe Tasks の Holistic Landmarkerを使用する。

目次

関連する外部ページ

サイト内の関連情報

第1章 Python開発環境、ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.html で解説している。

Build Tools for Visual Studio 2026(ビルドツール)のインストール

Build Tools for Visual Studio 2026(ビルドツール)のインストールを行い、C/C++ コードのビルド環境を整える。

[Build Tools for Visual Studio 2026(ビルドツール)のインストール手順を見るには、ここをクリック]

Windows での Build Tools for Visual Studio 2026 のインストール

Build Tools for Visual Studio は,Visual Studio の IDE を含まない C/C++ コンパイラ,ライブラリ,ビルドツール等のコマンドライン向け開発ツールセットである。インストール済みの場合,この手順は不要である。

以下のコマンドは、Build Tools が未インストールの場合は winget で新規インストールし、インストール済みの場合は setup.exe modify でコンポーネントを追加する(バージョンは変更しない)。

インストールコマンドの実行方法

管理者権限コマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。

REM ============================================================
REM Visual C++ 再頒布可能パッケージ (VCRedist 2015-)
REM ============================================================
winget install --scope machine --id Microsoft.VCRedist.2015+.x64 -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "/quiet /norestart"
if not "%ERRORLEVEL%"=="0" ( color 0c & echo VCRedist のインストールに失敗しました & ping 127.0.0.1 -n 6 >nul & color )

REM ============================================================
REM Visual Studio Build Tools + Desktop development with C++
REM (VCTools、MSBuildTools、CMake連携、Clang、Windows 11 SDK)
REM ============================================================
REM 進行中のインストーラーを停止(ロック競合回避。対象プロセスが存在しない場合は
REM taskkillがエラーを返すが、これは想定内であるため出力のみ抑制する)
taskkill /F /IM vs_setup.exe /T >nul 2>&1
taskkill /F /IM vs_installer.exe /T >nul 2>&1
taskkill /F /IM vs_installerservice.exe /T >nul 2>&1
taskkill /F /IM msiexec.exe /T >nul 2>&1

REM Build Tools + Desktop development with C++(VCTools)+ 追加コンポーネント(一括)
REM 未インストール時: winget で新規インストール
REM インストール済み時: setup.exe modify でコンポーネント追加(バージョンは変更しない)
winget list --id Microsoft.VisualStudio.BuildTools 2>nul | findstr /i "BuildTools" >nul 2>&1
if %ERRORLEVEL% EQU 0 (
    for /f "usebackq delims=" %P in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products Microsoft.VisualStudio.Product.BuildTools -property installationPath`) do start /wait "" "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath "%P" --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100 --add Microsoft.VisualStudio.Component.VC.v143.x86.x64 --includeRecommended --quiet --norestart --nocache
    if not "%ERRORLEVEL%"=="0" ( color 0c & echo Build Tools のコンポーネント追加に失敗しました & ping 127.0.0.1 -n 6 >nul & color )
) else (
    winget install --scope machine --id Microsoft.VisualStudio.BuildTools -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "--quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100 --add Microsoft.VisualStudio.Component.VC.v143.x86.x64"
    if not "%ERRORLEVEL%"=="0" ( color 0c & echo Build Tools のインストールに失敗しました & ping 127.0.0.1 -n 6 >nul & color )
)

REM
REM BuildTools のインストールパスを vswhere.exe で取得(メジャーバージョンに依存しない)
set "VSWHERE=C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
set "BT_PATH="
for /f "usebackq delims=" %P in (`"%VSWHERE%" -products Microsoft.VisualStudio.Product.BuildTools -property installationPath`) do set "BT_PATH=%P"
if not defined BT_PATH ( color 0c & echo Build Tools のインストールパスを取得できませんでした & ping 127.0.0.1 -n 6 >nul & color )

REM 破損時の修復(任意、動作がおかしくなった場合)
REM if defined BT_PATH "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" repair --installPath "%BT_PATH%" --quiet --norestart
REM 導入確認(インストールパスが表示されれば正常)
"%VSWHERE%" -products * -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath

上記のコマンドでは、Build Tools 本体と Visual C++ 再頒布可能パッケージをインストールし、続いて以下のコンポーネントを追加している。

追加のコンポーネントが必要になった場合は Visual Studio Installer で個別にインストールできる。

インストール完了の確認

winget list Microsoft.VisualStudio.BuildTools

Visual Studio を必要とするとき

Visual Studio の機能を必要とする場合は,追加インストールできる。

第2章 Python 3.12 のインストール

Pythonのインストールを行い、Pythonのプログラムを実行する環境を整える。扱う環境は、Windows搭載パソコンである。金子研究室では、Python 3.12.10を推奨する。

[Windows での Python 3.12 のインストール手順を見るには、ここをクリック]

Windows での Python 3.12 のインストール

以下のいずれかの方法でPython 3.12をインストールする。Pythonがインストール済みの場合、この手順は不要である。

方法 1:winget によるインストール

インストールコマンドの実行方法

管理者権限コマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。

--scope machine を指定することで、システム全体(全ユーザー向け)にインストールされる。このオプションの実行には管理者権限が必要である。インストール完了後、コマンドプロンプトを再起動するとPATHが反映される。

REM Python 3.12 をシステム領域にインストール
winget install --id Python.Python.3.12 -e --scope machine --silent --accept-source-agreements --accept-package-agreements --override "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 Include_launcher=1 InstallLauncherAllUsers=1 TargetDir=\"C:\Program Files\Python312\""
if not "%ERRORLEVEL%"=="0" ( color 0c & echo Python 3.12 のインストールに失敗しました & ping 127.0.0.1 -n 6 >nul & color )

REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; if(Test-Path $p){$k=[Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment',$true); $c=$k.GetValue('Path','',[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames); $t=$k.GetValueKind('Path'); $new=$c; if((';'+$new+';') -notlike \"*;$p;*\"){$new=$p+';'+$new}; if((';'+$new+';') -notlike \"*;$s;*\"){$new=$s+';'+$new}; if($new -ne $c){$k.SetValue('Path',$new,$t)}; $k.Close()}"

REM 現在のセッションにも反映(システムPATHを再取得して連結)
for /f "usebackq tokens=2,*" %A in (`reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path`) do set "PATH=%B"

REM pip / wheel の更新
python -m pip install --no-user -U pip wheel
if not "%ERRORLEVEL%"=="0" ( color 0c & echo pip / wheel の更新に失敗しました & ping 127.0.0.1 -n 6 >nul & color )

方法 2:インストーラーによるインストール

  1. Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
  2. ダウンロードしたインストーラーを実行する。
  3. 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから python コマンドを実行できない。
  4. 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。

インストールの確認

コマンドプロンプトで以下を実行する。

python --version

バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。

第3章 Python の開発環境 Visual Studio Code のインストールと Python 用の設定

Python の開発環境Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を整える。

[Windows での Visual Studio Code のインストールと Python 用の設定手順を見るには、ここをクリック]

Windows での Visual Studio Code のインストールと Python 用の設定手順

1. VS Code と拡張機能のインストール

以下のコマンドにより,既存の VS Code を削除し,全ユーザー共有の設定で再インストールしたうえで,拡張機能(VS Code に機能を追加するソフトウェア)をまとめて導入する.

インストールコマンドの実行方法

管理者権限コマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして,コマンド全体をコマンドプロンプトにコピー&ペーストする。

インストールコマンド


REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
REM Build Tools + Desktop development with C++(VCTools)+ 追加コンポーネント(一括)
REM 未インストール時: winget で新規インストール
REM インストール済み時: setup.exe modify でコンポーネント追加(バージョンは変更しない)
winget list --id Microsoft.VisualStudio.BuildTools 2>nul | findstr /i "BuildTools" >nul 2>&1
if %ERRORLEVEL% EQU 0 (
    for /f "usebackq delims=" %P in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products Microsoft.VisualStudio.Product.BuildTools -property installationPath`) do start /wait "" "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath "%P" --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100 --add Microsoft.VisualStudio.Component.VC.v143.x86.x64 --includeRecommended --quiet --norestart --nocache
    if not "%ERRORLEVEL%"=="0" ( color 0c & echo Build Tools のコンポーネント追加に失敗しました & ping 127.0.0.1 -n 6 >nul & color )
) else (
    winget install --scope machine --id Microsoft.VisualStudio.BuildTools -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "--quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100 --add Microsoft.VisualStudio.Component.VC.v143.x86.x64"
    if not "%ERRORLEVEL%"=="0" ( color 0c & echo Build Tools のインストールに失敗しました & ping 127.0.0.1 -n 6 >nul & color )
)

REM 全ユーザー共有の拡張機能フォルダ
if not exist "C:\ProgramData\vscode-extensions" mkdir "C:\ProgramData\vscode-extensions"
icacls "C:\ProgramData\vscode-extensions" /grant "Everyone:(OI)(CI)M" /T

REM スタートメニューのショートカットを --extensions-dir 付きで再作成
if exist "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code"
if exist "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk"
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); $lnk.TargetPath='C:\Program Files\Microsoft VS Code\Code.exe'; $lnk.Arguments='--extensions-dir \"C:\ProgramData\vscode-extensions\"'; $lnk.Save()"
REM ショートカットの検証
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); Write-Host 'TargetPath:' $lnk.TargetPath; Write-Host 'Arguments:' $lnk.Arguments"

REM ファイル / フォルダ右クリックの「Code で開く」を登録
reg add "HKLM\SOFTWARE\Classes\*\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\Background\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%V\"" /f

REM --extensions-dir 付きで起動する code.cmd ラッパを作成
REM (%* を echo で書くと対話的 cmd で失われるため、PowerShell で [char]37+'*' を書き出す)
powershell -NoProfile -Command "$pct=[char]37; $q=[char]34; $c='@echo off'+[char]13+[char]10+$q+'C:\Program Files\Microsoft VS Code\bin\code.cmd'+$q+' --extensions-dir '+$q+'C:\ProgramData\vscode-extensions'+$q+' '+$pct+'*'+[char]13+[char]10; [IO.File]::WriteAllText('C:\ProgramData\vscode-extensions\vscode.cmd',$c,[Text.Encoding]::ASCII)"

REM 拡張機能のインストール
set "CODE=C:\Program Files\Microsoft VS Code\bin\code.cmd"
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot-chat
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.python
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.vscode-pylance
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.debugpy
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension MS-CEINTL.vscode-language-pack-ja
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension saoudrizwan.claude-dev
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension rust-lang.rust-analyzer
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension tamasfe.even-better-toml
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension anthropic.claude-code
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension almenon.arepl
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --list-extensions --show-versions

REM settings.json を作成(自動更新オフ、Python、Claude Code 設定)
if not exist "%APPDATA%\Code\User" mkdir "%APPDATA%\Code\User"
python -c "import json,os;data={'update.mode':'none','update.enableWindowsBackgroundUpdates':False,'extensions.autoUpdate':False,'python.defaultInterpreterPath':r'C:\Program Files\Python312\python.exe','claudeCode.environmentVariables':[{'name':'ANTHROPIC_API_KEY','value':'not-needed'},{'name':'ANTHROPIC_AUTH_TOKEN','value':'ollama'},{'name':'ANTHROPIC_BASE_URL','value':'http://localhost:11434'},{'name':'ANTHROPIC_MODEL','value':'glm-4.7-flash'},{'name':'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC','value':'1'}]};p=os.path.join(os.environ['APPDATA'],'Code','User','settings.json');open(p,'w',encoding='utf-8').write(json.dumps(data,indent=4));print('Done:',p)"

REM 自動更新の抑止ポリシー(settings.json に加えて、レジストリ側でも明示的にオフ)
reg add "HKLM\SOFTWARE\Policies\Microsoft\VSCode" /v "UpdateMode" /t REG_SZ /d "none" /f
echo === セットアップ完了 ===

2. Python インタプリタの選択

同一マシンに複数の Python がインストールされている場合,VS Code で使用する Python 本体(インタプリタ:Python プログラムを解釈・実行するソフトウェア)を選択する必要がある.

  1. コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(Ctrl+Shift+P
  2. Python: Select Interpreter と入力する
  3. 表示される一覧から,使用する Python(例:C:\Program Files\Python312\python.exe)を選択する.

必要なライブラリをシステム領域にインストール

管理者権限コマンドプロンプトを起動する (手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。

起動したコマンドプロンプトで次を実行する。--no-user は、ユーザ領域ではなくシステム領域へインストールするためのオプションである。

pip install -U --no-user opencv-python mediapipe numpy scipy pillow

学習済みモデルのファイル holistic_landmarker.task は、プログラムの初回実行時に自動的にダウンロードされ、プログラムと同じディレクトリに保存される。site-packages 内のディレクトリを事前に作成したり、アクセス権を変更したりする作業は不要である

第4章 MediaPipeによるしぐさ検出プログラム

1. 概要

このプログラムは、動画から人物の全身543点のランドマークを検出し、その時間変化を追跡することでしぐさを判定する。まばたきの判定にはEAR(Eye Aspect Ratio、目の縦横比)アルゴリズム[1]、手の震えの判定には高周波成分の抽出、肩の緊張の判定には位置の変動係数を用いる。

2. 主要技術

MediaPipe Holistic Landmarker

Googleが提供する機械学習フレームワーク MediaPipe [2] のタスクの1つである。顔468点、両手21点ずつ(計42点)、体33点の合計543点のランドマークを1つのタスクで同時に検出する。顔・手・姿勢のそれぞれの学習済みモデルを1つのモデルファイル(holistic_landmarker.task)に統合しており、MediaPipe Tasks API(mediapipe.tasks.python.vision.HolisticLandmarker)から利用する。かつて提供されていた mediapipe.solutions 系のAPI(MediaPipe Legacy Solutions)は、2023年3月1日にサポートが終了し、その後のバージョンのPythonパッケージからは削除されている。

Eye Aspect Ratio (EAR)

目の開き具合を数値化するアルゴリズムである。目の周囲の6点のランドマークから、縦方向の距離と横方向の距離の比を計算する[1]。まばたき検出の標準的な手法として使われている。目の縦方向の距離を p2−p6、p3−p5、横方向の距離を p1−p4 とすると、EAR は (‖p2−p6‖ + ‖p3−p5‖) / (2‖p1−p4‖) で求まる。目を閉じると縦方向の距離が小さくなるため、EARの値が下がる。

3. 技術的特徴

4. 参考文献

5. ソースコード

# プログラム名: MediaPipeによるしぐさ検出プログラム
# 特徴技術名: MediaPipe Holistic Landmarker(MediaPipe Tasks)
# 出典: Lugaresi, C., Tang, J., Nash, H., McClanahan, C., Uboweja, E., Hays, M., Zhang, F., Chang, C. L., Yong, M. G., Lee, J., Chang, W. T., Hua, W., Georg, M., & Grundmann, M. (2019). MediaPipe: A framework for building perception pipelines. arXiv preprint arXiv:1906.08172.
# 特徴機能: 全身543点のランドマーク検出機能。顔468点、両手21点ずつ(計42点)、体33点の合計543点のランドマークを1つのタスクで同時に検出し、人体の姿勢と動作を認識する。
# 学習済みモデル: holistic_landmarker.task(float16)。顔・手・姿勢の学習済みモデルを1つのファイルに統合したもの。
#   取得元: https://storage.googleapis.com/mediapipe-models/holistic_landmarker/holistic_landmarker/float16/latest/holistic_landmarker.task
# 方式設計:
#   関連利用技術: OpenCV(動画入力・表示・描画)、NumPy(数値計算・配列処理)、SciPy(バターワース型ハイパスフィルタ)、tkinter(ファイル選択)、Pillow(日本語フォント描画)、urllib(モデルとサンプル動画の取得)
#   入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.aviを使用)、出力: OpenCV画面でのリアルタイム表示、各フレームごとのprint()表示、終了時にresult.txtへ保存
#   処理手順: 1.動画フレーム取得 2.Holistic Landmarkerによる543点ランドマーク検出 3.EARによるまばたき検出 4.手の震え検出(高周波成分抽出) 5.肩の緊張検出(変動係数計算) 6.結果の画面表示
#   前処理、後処理: 前処理: RGB色空間への変換(MediaPipe要求仕様)、後処理: 検出結果のBGR画像への描画
#   追加処理: EAR計算による目の開き具合の数値化、手ランドマークの高周波成分抽出による震え検出、肩ランドマークの変動係数による緊張検出
#   調整を必要とする設定値: EAR_THRESHOLD(まばたき検出しきい値、通常0.25、個人差により調整)、HIGH_FREQ_THRESHOLD(手の震え検出しきい値)、TENSION_THRESHOLD(肩の緊張検出しきい値)
#   算出・計算処理の検証: EAR計算式、高周波成分抽出、変動係数計算が正しく実装されていることを確認
# 将来方策: しきい値の自動調整機能、個人ごとの校正機能
# その他の重要事項: リアルタイム処理性能、照明条件への対応
# 前準備: pip install -U --no-user opencv-python mediapipe numpy scipy pillow

import cv2
import mediapipe as mp
import numpy as np
import tkinter as tk
from tkinter import filedialog
import urllib.request
import os
import time
import math
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
from scipy import signal
from mediapipe.tasks import python as mp_python
from mediapipe.tasks.python import vision as mp_vision

# 調整可能な設定値
EAR_THRESHOLD = 0.25          # まばたき判定のしきい値
HIGH_FREQ_THRESHOLD = 0.5     # 手の震え判定のしきい値
TENSION_THRESHOLD = 0.01      # 肩の緊張判定のしきい値
HISTORY_SIZE = 30             # 時系列データの保持フレーム数
MIN_FILTER_LENGTH = 18        # ハイパスフィルタ適用に必要な最小フレーム数
HIGH_CUTOFF = 2.0             # ハイパスフィルタの遮断周波数(Hz)

# 学習済みモデル
MODEL_URL = 'https://storage.googleapis.com/mediapipe-models/holistic_landmarker/holistic_landmarker/float16/latest/holistic_landmarker.task'
MODEL_FILE = 'holistic_landmarker.task'

# 目のランドマーク番号(p1, p2, p3, p4, p5, p6 の順)
RIGHT_EYE_LANDMARKS = [33, 159, 158, 133, 153, 145]
LEFT_EYE_LANDMARKS = [362, 380, 374, 263, 386, 385]

# 日本語フォント設定
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)

# グローバル変数
frame_count = 0
results_log = []
fps_estimate = 30.0
frame_times = []
left_hand_history = []
right_hand_history = []
left_shoulder_history = []
right_shoulder_history = []


def draw_japanese_text(img, text, position, color=(0, 255, 0)):
    """OpenCV画像(BGR)に日本語テキストを描画する"""
    img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    ImageDraw.Draw(img_pil).text(position, text, font=font, fill=color[::-1])
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)


def calculate_ear(eye_landmarks):
    """EAR = (||p2-p6|| + ||p3-p5||) / (2 * ||p1-p4||)"""
    p1, p2, p3, p4, p5, p6 = eye_landmarks

    a = math.dist(p2, p6)
    b = math.dist(p3, p5)
    c = math.dist(p1, p4)

    if c == 0:
        return 0.0

    return (a + b) / (2.0 * c)


def update_fps_estimate():
    """直近のフレーム間隔からフレームレートを推定する"""
    global fps_estimate

    frame_times.append(time.time())
    if len(frame_times) > 10:
        frame_times.pop(0)

    if len(frame_times) >= 2:
        dt = frame_times[-1] - frame_times[0]
        if dt > 0:
            fps_estimate = (len(frame_times) - 1) / dt


def extract_high_frequency_component(hand_landmarks, hand_history):
    """手の重心位置の時系列から高周波成分の強度を求める"""
    hand_history.append([np.mean([lm[0] for lm in hand_landmarks]),
                         np.mean([lm[1] for lm in hand_landmarks])])
    if len(hand_history) > HISTORY_SIZE:
        hand_history.pop(0)

    if len(hand_history) < MIN_FILTER_LENGTH:
        return 0.0

    x_data = np.array([pos[0] for pos in hand_history])
    y_data = np.array([pos[1] for pos in hand_history])

    # 遮断周波数がナイキスト周波数を超えないように調整する
    nyquist = 0.5 * fps_estimate
    high_cutoff = HIGH_CUTOFF if HIGH_CUTOFF < nyquist else nyquist * 0.8

    b, a = signal.butter(2, high_cutoff / nyquist, btype='high')
    x_filtered = signal.filtfilt(b, a, x_data)
    y_filtered = signal.filtfilt(b, a, y_data)

    return np.sqrt(np.mean(x_filtered ** 2) + np.mean(y_filtered ** 2))


def calculate_shoulder_tension(shoulder_history):
    """肩の位置の変動係数(標準偏差 / 平均)を求める"""
    if len(shoulder_history) < 5:
        return 0.0

    positions = np.array(shoulder_history)
    x_coords = positions[:, 0]
    y_coords = positions[:, 1]
    x_mean = np.mean(x_coords)
    y_mean = np.mean(y_coords)

    if abs(x_mean) <= 1e-6 or abs(y_mean) <= 1e-6:
        return 0.0

    return (np.std(x_coords) / abs(x_mean) + np.std(y_coords) / abs(y_mean)) / 2


def draw_eye_landmarks(image, eye_landmarks, color=(0, 255, 0)):
    """目のランドマーク6点と、その輪郭を描画する"""
    for point in eye_landmarks:
        cv2.circle(image, (int(point[0]), int(point[1])), 2, color, -1)

    cv2.polylines(image, [np.array(eye_landmarks, np.int32)], True, color, 1)


def draw_hand_landmarks(image, hand_landmarks, color=(255, 0, 0)):
    """手のランドマークを描画する"""
    for point in hand_landmarks:
        cv2.circle(image, (int(point[0]), int(point[1])), 2, color, -1)


def draw_shoulder_landmarks(image, left_shoulder, right_shoulder):
    """左右の肩のランドマークと、それを結ぶ線を描画する"""
    cv2.circle(image, (int(left_shoulder[0]), int(left_shoulder[1])), 5, (0, 0, 255), -1)
    cv2.circle(image, (int(right_shoulder[0]), int(right_shoulder[1])), 5, (0, 0, 255), -1)
    cv2.line(image, (int(left_shoulder[0]), int(left_shoulder[1])),
             (int(right_shoulder[0]), int(right_shoulder[1])), (0, 0, 255), 2)


def to_pixel(landmarks, frame):
    """正規化座標のランドマークを画素座標に変換する"""
    return [(lm.x * frame.shape[1], lm.y * frame.shape[0]) for lm in landmarks]


def video_frame_processing(frame):
    global frame_count
    current_time = time.time()
    frame_count += 1

    update_fps_estimate()

    rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_frame)
    results = landmarker.detect_for_video(mp_image, int(frame_count * 1000 / 30))

    processed_frame = frame.copy()

    right_ear = left_ear = 0.0
    left_hand_tremor = right_hand_tremor = 0.0
    left_shoulder_tension = right_shoulder_tension = 0.0

    # まばたき検出(EAR計算)
    if results.face_landmarks:
        face_landmarks = to_pixel(results.face_landmarks, frame)
        right_eye = [face_landmarks[i] for i in RIGHT_EYE_LANDMARKS]
        left_eye = [face_landmarks[i] for i in LEFT_EYE_LANDMARKS]

        right_ear = calculate_ear(right_eye)
        left_ear = calculate_ear(left_eye)

        draw_eye_landmarks(processed_frame, right_eye, (0, 255, 0))
        draw_eye_landmarks(processed_frame, left_eye, (0, 255, 0))

    # 手の震え検出(高周波成分抽出)
    if results.left_hand_landmarks:
        left_hand_landmarks = to_pixel(results.left_hand_landmarks, frame)
        left_hand_tremor = extract_high_frequency_component(left_hand_landmarks, left_hand_history)
        draw_hand_landmarks(processed_frame, left_hand_landmarks, (255, 0, 0))

    if results.right_hand_landmarks:
        right_hand_landmarks = to_pixel(results.right_hand_landmarks, frame)
        right_hand_tremor = extract_high_frequency_component(right_hand_landmarks, right_hand_history)
        draw_hand_landmarks(processed_frame, right_hand_landmarks, (255, 0, 0))

    # 肩の緊張検出(変動係数)
    if len(results.pose_landmarks) >= 13:
        pose_landmarks = to_pixel(results.pose_landmarks, frame)
        left_shoulder = pose_landmarks[11]
        right_shoulder = pose_landmarks[12]

        left_shoulder_history.append(left_shoulder)
        right_shoulder_history.append(right_shoulder)
        if len(left_shoulder_history) > HISTORY_SIZE:
            left_shoulder_history.pop(0)
        if len(right_shoulder_history) > HISTORY_SIZE:
            right_shoulder_history.pop(0)

        left_shoulder_tension = calculate_shoulder_tension(left_shoulder_history)
        right_shoulder_tension = calculate_shoulder_tension(right_shoulder_history)

        draw_shoulder_landmarks(processed_frame, left_shoulder, right_shoulder)

    # 状態判定
    blink_status = 'まばたき' if (right_ear + left_ear) / 2 < EAR_THRESHOLD else '正常'
    right_hand_status = '震え' if right_hand_tremor > HIGH_FREQ_THRESHOLD else '正常'
    left_hand_status = '震え' if left_hand_tremor > HIGH_FREQ_THRESHOLD else '正常'
    right_shoulder_status = '緊張' if right_shoulder_tension > TENSION_THRESHOLD else '正常'
    left_shoulder_status = '緊張' if left_shoulder_tension > TENSION_THRESHOLD else '正常'

    text_lines = [
        f'右目EAR: {right_ear:.3f} 左目EAR: {left_ear:.3f}',
        f'右手震え: {right_hand_tremor:.3f} 左手震え: {left_hand_tremor:.3f}',
        f'右肩緊張: {right_shoulder_tension:.3f} 左肩緊張: {left_shoulder_tension:.3f}',
        f'目: {blink_status}',
        f'右手: {right_hand_status} 左手: {left_hand_status}',
        f'右肩: {right_shoulder_status} 左肩: {left_shoulder_status}'
    ]

    y_offset = 10
    for line in text_lines:
        processed_frame = draw_japanese_text(processed_frame, line, (10, y_offset))
        y_offset += 30

    result = (f'右目EAR:{right_ear:.3f}, 左目EAR:{left_ear:.3f}, '
              f'右手震え:{right_hand_tremor:.3f}, 左手震え:{left_hand_tremor:.3f}, '
              f'右肩緊張:{right_shoulder_tension:.3f}, 左肩緊張:{left_shoulder_tension:.3f}')

    return processed_frame, result, current_time


# 学習済みモデルの取得
if not os.path.exists(MODEL_FILE):
    print('学習済みモデルをダウンロードします')
    urllib.request.urlretrieve(MODEL_URL, MODEL_FILE)

landmarker = mp_vision.HolisticLandmarker.create_from_options(
    mp_vision.HolisticLandmarkerOptions(
        base_options=mp_python.BaseOptions(model_asset_path=MODEL_FILE),
        running_mode=mp_vision.RunningMode.VIDEO))

# ガイダンス表示
print('\n=== MediaPipeによるしぐさ検出プログラム ===')
print('概要: 全身543点(顔468点、両手42点、体33点)のランドマークを検出し、')
print('      まばたき(EAR)・手の震え(高周波成分)・肩の緊張(変動係数)を判定します')
print('操作方法:')
print('  q キー: プログラム終了')
print('注意事項:')
print('  検出結果は自動的にresult.txtに保存されます')

root = tk.Tk()
root.withdraw()

print('\n0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')

choice = input('選択: ')
is_camera = (choice == '1')

if choice == '0':
    path = filedialog.askopenfilename()
    if not path:
        exit()
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
else:
    SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
    SAMPLE_FILE = 'vtest.avi'
    urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
    cap = cv2.VideoCapture(SAMPLE_FILE)

if not cap.isOpened():
    print('動画ファイル・カメラを開けませんでした')
    exit()

print('\n=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')

MAIN_FUNC_DESC = 'MediaPipe しぐさ検出'

try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        processed_frame, result, current_time = video_frame_processing(frame)
        cv2.imshow(MAIN_FUNC_DESC, processed_frame)

        if is_camera:
            print(datetime.fromtimestamp(current_time).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3], result)
        else:
            print(frame_count, result)

        results_log.append(result)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
finally:
    cap.release()
    cv2.destroyAllWindows()
    landmarker.close()

print('\n=== プログラム終了 ===')
if results_log:
    with open('result.txt', 'w', encoding='utf-8') as f:
        f.write('=== 処理結果 ===\n')
        f.write(f'処理フレーム数: {frame_count}\n')
        f.write('\n')
        f.write('\n'.join(results_log))
    print('処理結果をresult.txtに保存しました')