MediaPipe による15種類のAIタスク実行(Windows)

【概要】

  1. MediaPipe Tasks API(Googleが提供する事前学習済みAIタスクを数行のコードで呼び出せるライブラリ)を用いて,物体検出,画像セグメンテーション(画像を領域ごとに分割する処理),姿勢・手・顔・ジェスチャー認識,音声分類,テキスト処理等の15種類のAIタスクを実行する
  2. Pythonプログラムは,処理対象ファイル(画像・音声)の自動ダウンロード,モデル自動ダウンロード,ファイル選択ダイアログ,AIによる推論,結果表示の手順で統一し,理解と改変を容易にする
  3. Windowsマシンでのローカル実行に対応する.繰り返し検証,Webカメラによるリアルタイム処理等への発展に適する
  4. 期待した精度・性能が得られない場合,設定の変更や学習済みモデルの選択を行う.データの前処理,他の技術の利用,追加学習等の検討も考えられる

【説明資料】

[PDF], [パワーポイント] (同じ内容,クリックしてダウンロード), [動画]

【目次】

  1. Python の実行環境を整える
  2. MediaPipe の概要、インストール、動作確認
  3. プログラム実行手順
  4. Pythonプログラムの共通構造
  5. 下記の15プログラムの実行時の留意事項
  6. タスク1:物体検出(Object Detector)
  7. タスク2:画像分類(Image Classifier)
  8. タスク3:画像セグメンテーション(Image Segmenter)
  9. タスク4:姿勢推定(Pose Landmarker)
  10. タスク5:手のランドマーク検出(Hand Landmarker)
  11. タスク6:ジェスチャー認識(Gesture Recognizer)
  12. タスク7:顔ランドマーク検出(Face Landmarker)
  13. タスク8:顔検出(Face Detector)
  14. タスク9:画像埋め込み(Image Embedder)
  15. タスク10:音声分類(Audio Classifier)
  16. タスク11:テキスト分類(Text Classifier,画像不要)
  17. タスク12:言語検出(Language Detector,画像不要)
  18. タスク13:ホリスティック検出(Holistic Landmarker)
  19. タスク14:手の3D可視化(Hand Landmarker world landmarks)
  20. タスク15:姿勢の3D可視化(Pose Landmarker world landmarks)
  21. MediaPipe の用途例

Python の実行環境を整える

本章では、Python のプログラムを作成して実行する環境を整える。扱う環境は、Windows 搭載パソコンである。

Windows では、実行環境として、まず、Python のインストールが必要である。金子研究室では、Python 3.12.10 を推奨する。Windows では、Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を推奨する。

[Windows で実行環境を整える手順を見るには、ここをクリック]

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\""

REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; $c=[Environment]::GetEnvironmentVariable('Path','Machine'); if((Test-Path $p) -and (';'+$c+';' -notlike \"*;$p;*\") -and (';'+$c+';' -notlike \"*;$s;*\")){[Environment]::SetEnvironmentVariable('Path',\"$p;$s;$c\",'Machine')}"

方法 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' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は,インストールが正常に完了していない.

Windows での Visual Studio Code のインストール、設定

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

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

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

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

インストールコマンド


REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
winget uninstall -e --id Microsoft.VisualStudioCode --silent --disable-interactivity --accept-source-agreements
rmdir /s /q C:\ProgramData\vscode-extensions 2>nul
rmdir /s /q "%APPDATA%\Code" 2>nul
rmdir /s /q "%USERPROFILE%\.vscode" 2>nul
rmdir /s /q "%LOCALAPPDATA%\Microsoft\vscode-update" 2>nul

REM VS Code をシステム領域に新規インストール
winget install --scope machine --id Microsoft.VisualStudioCode -e --silent --accept-source-agreements --accept-package-agreements

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

REM スタートメニューのショートカットを --extensions-dir 付きで再作成
rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" 2>nul
del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" 2>nul
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
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)を選択する.

以降の章では、必要に応じて題材に応じた必要なソフトウェアを追加する。

MediaPipe の概要、インストール、動作確認

MediaPipe は Google が提供するクロスプラットフォーム(複数の OS・端末で動作する性質)の機械学習フレームワークであり,Python に対応する.Tasks(学習済みモデルを利用する API),Models(学習済みモデルのファイル群),Model Maker(独自データでモデルを再学習するツール),Studio(ブラウザ上で動作確認を行う GUI 環境)の四要素で構成される.物体検出,顔・手・姿勢のランドマーク検出(特徴点の座標を検出する処理),画像セグメンテーション(画像を領域ごとに分割する処理),画像生成,テキスト分類,音声分類,LLM(Large Language Model:大規模言語モデル)推論等のタスクを API として提供する.Gemma 3n(Google が提供するオンデバイス向けの軽量モデル)等のモデル,マルチモーダル入力(画像・音声・テキスト等,複数種類の入力),NPU(Neural Processing Unit:AI 推論専用プロセッサ)アクセラレーション(処理の高速化)にも対応する.

Google AI Edge 公式ドキュメント「MediaPipe Solutions guide」(https://ai.google.dev/edge/mediapipe/solutions/guide)

Windows での MediaPipe のインストールと動作確認

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

そして,以下のコマンドを実行し,MediaPipe および関連ライブラリをインストールしたうえで,動作確認を行う.

REM MediaPipe 標準機能のインストール
pip uninstall -y opencv-python-headless opencv-contrib-python-headless opencv-python
pip install --no-user --force-reinstall opencv-contrib-python
pip install -U --no-user mediapipe onnxruntime-gpu opencv-contrib-python protobuf==6.33.6
python -c "import mediapipe as mp; print('mediapipe:', mp.__version__)"
python -c "import onnxruntime as ort; print('providers:', ort.get_available_providers())"
python -c "import cv2; print('opencv:', cv2.__version__)"
REM
REM MediaPipe Tasks API 動作確認
python -c "from mediapipe.tasks import python as mp_py; from mediapipe.tasks.python import vision; print('tasks API OK')"

プログラム実行手順

[Windows でのプログラム実行手順を見るには、ここをクリック]

Windows での実行手順(Visual Studio Code)

本節では,Python プログラムを Visual Studio Code で実行する手順を示す.

プログラムファイルの作成と保存

  1. 左サイドバーの「エクスプローラー」アイコン(Ctrl+Shift+E)をクリックする
  2. 「NO FOLDER OPENED」(作業対象フォルダが未選択の状態)と表示される場合は,「Open Folder」をクリックし,プログラムを保存するフォルダを選択する

    続いて「フォルダを信用するか」を確認する画面(フォルダ内のコードを実行してよいか確認する VS Code の仕組み)が表示されるので,チェックして Yes を選択する

  3. フォルダ名の右側に表示される「新しいファイル」アイコンをクリックする
  4. ファイル名(例:aitask.py.ファイル名は何でも良い)を入力し Enter を押す.拡張子は .py(Python ファイルを示す拡張子)とする
  5. 実行したいコードを選択し,Ctrl+C でコピーする.VS Code のエディタ領域に Ctrl+V で貼り付ける
  6. Ctrl+S で保存する

プログラムの実行

  1. エディタ右上の三角形「▷」アイコン(Run Python File:現在開いている Python ファイルを実行するボタン)をクリックする.または,エディタ上で右クリックし「ターミナルで Python ファイルを実行」を選択する
  2. VS Code 下部のターミナル(コマンドの入出力を表示する画面)に,実行結果(print 関数の出力等)が表示される
  3. tkinter(Python 標準の GUI ライブラリ)のファイル選択ダイアログを使うプログラムを実行した場合は,ダイアログが開くので対象画像を選択する
  4. VS Code 下部のターミナルで実行結果を確認する.OpenCV ウィンドウ(OpenCV が画像を表示するために開く専用ウィンドウ)が開いた場合はそちらも確認する.OpenCV ウィンドウは,マウスクリックでウィンドウをアクティブ(操作対象の状態)にしてからキーを押すと終了する

Pythonプログラムの共通構造

すべてのPythonプログラムの構造は,以下のとおり共通化している.

  1. 処理対象ファイル(画像・音声)の自動ダウンロード(存在チェックを含む).保存先は C:\image\ 配下のサブフォルダ(opencvultralyticsmediapipeaudiosetpixabay)である.これらのファイルを使用できる
  2. 学習済みモデル(AIモデルファイル)の自動ダウンロード(存在チェックを含む).保存先はスクリプトと同じディレクトリである
  3. 画像・音声ファイルの選択:tkinter filedialog(Python標準GUIライブラリtkinterのファイル選択ダイアログ)を用いる.テキスト系タスクでは simpledialog による文字列入力を用いる
  4. 結果表示:OpenCV ウィンドウ(cv2.imshow)を用いる.matplotlib またはprintによる表示も用いる

処理対象ファイルおよびモデルは,初回実行時にコード内で自動ダウンロードする.ファイル選択ダイアログにより,ダウンロード済みファイル以外の任意のローカルファイルも処理対象とできる.

下記の15プログラムの実行時の留意事項

  1. 初回実行時はインターネット接続が必要である(モデルおよびサンプル画像のダウンロードのため)
  2. 初回実行時は処理開始までに数十秒から数分程度かかる場合がある(モデルおよびサンプル画像のダウンロードのため)
  3. 学習済みモデルの保存先はスクリプトと同じディレクトリ,処理対象ファイルの保存先は C:\image\ 配下のサブフォルダである(2回目以降の実行ではダウンロードを省略する)
  4. OpenCV ウィンドウが画面奥に隠れる場合は,タスクバーから前面に出す
  5. tkinter ダイアログが応答しなくなった場合は,VSCode下部のターミナルで Ctrl+C(実行中の処理を中断するキー操作)を押して中断する
  6. タスク14およびタスク15ではmatplotlibを使用する.未インストール環境では pip install --no-user matplotlib を実行する

【MediaPipe版】タスク1:物体検出(Object Detector)

機能説明:画像内の物体をバウンディングボックス(物体を囲む矩形領域)で検出し,COCOデータセット(Microsoftが公開した画像認識用の代表的データセット)の80クラス(人,車,犬,椅子等)から分類する.EfficientDet-Lite2(448×448入力,BiFPN特徴ネットワーク採用の物体検出モデル)をベースモデル(推論の基盤となる学習済みモデル)とする.防犯カメラの物体カウント,写真整理の自動タグ付け,在庫管理等に利用される.

# task_object_detector
import os, urllib.request
from pathlib import Path
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定(一部サーバのアクセス制限回避のため)
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://ultralytics.com/images/bus.jpg"
IMG_DIR = get_image_dir("ultralytics")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/object_detector/efficientdet_lite2/float16/latest/efficientdet_lite2.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 画像選択
selected = select_file(
    title="物体検出する画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.ObjectDetectorOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    score_threshold=0.5)
detector = vision.ObjectDetector.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = detector.detect(image)

# 表示
img = cv2.imread(selected)
for det in result.detections:
    bb = det.bounding_box
    cat = det.categories[0]
    cv2.rectangle(img, (bb.origin_x, bb.origin_y),
                  (bb.origin_x + bb.width, bb.origin_y + bb.height), (0, 255, 0), 2)
    cv2.putText(img, f"{cat.category_name} {cat.score:.2f}",
                (bb.origin_x, bb.origin_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
    print(f"{cat.category_name}: {cat.score:.2f}")
show_image("Object Detector", img)
detector.close()

【MediaPipe版】タスク2:画像分類(Image Classifier)

機能説明:画像全体に対して単一の分類ラベルを推定する.EfficientNet-Lite2(EfficientNetアーキテクチャによる画像分類モデル)をベースモデルとし,ImageNet(約120万枚・1000クラスの画像分類用大規模データセット)の1000クラスに対応する.画像検索の自動タグ付け,コンテンツモデレーション(不適切コンテンツの選別),アルバム整理等に利用される.

# task_image_classifier
import os, urllib.request
from pathlib import Path
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://raw.githubusercontent.com/opencv/opencv/4.x/samples/data/squirrel_cls.jpg"
IMG_DIR = get_image_dir("opencv")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/image_classifier/efficientnet_lite2/float32/latest/efficientnet_lite2.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 画像選択
selected = select_file(
    title="分類する画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.ImageClassifierOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)), max_results=5)
classifier = vision.ImageClassifier.create_from_options(options)
result = classifier.classify(mp.Image.create_from_file(selected))

# 表示
print("分類結果(上位5件):")
for cat in result.classifications[0].categories:
    print(f"  {cat.category_name}: {cat.score:.4f}")
classifier.close()

【MediaPipe版】タスク3:画像セグメンテーション(Image Segmenter)

機能説明:画像を画素単位で領域分割する(各画素がどのクラスに属するかを判定する処理).DeepLab V3(Googleが開発したセマンティックセグメンテーション用モデル)をベースモデルとし,Pascal VOC 2012(物体認識評価のための代表的データセット)の21クラス(背景,人,車,犬,猫,自転車,椅子,植木鉢等)に対応する.背景除去,写真合成,自動運転の領域認識等に利用される.本タスクでは output_confidence_masks を有効化し,各クラスの確信度マスクを参照することで,境界の不確実領域を識別できる構成とする.

# task_image_segmenter
import os, urllib.request
from pathlib import Path
import cv2, numpy as np
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://ultralytics.com/images/zidane.jpg"  # 'person'クラスで認識される
IMG_DIR = get_image_dir("ultralytics")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/image_segmenter/deeplab_v3/float32/latest/deeplab_v3.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 画像選択
selected = select_file(
    title="セグメンテーションする画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論(カテゴリマスクと確信度マスクの両方を出力)
options = vision.ImageSegmenterOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    output_category_mask=True,
    output_confidence_masks=True)
segmenter = vision.ImageSegmenter.create_from_options(options)
result = segmenter.segment(mp.Image.create_from_file(selected))

# 表示
mask = result.category_mask.numpy_view()
print(f"検出クラス: {np.unique(mask)}")
# 各クラスの確信度マスクから低確信度領域を背景化
confidence_threshold = 0.5
refined = mask.copy()
for cls in np.unique(mask):
    if cls == 0:
        continue
    conf = result.confidence_masks[cls].numpy_view()
    refined[(mask == cls) & (conf < confidence_threshold)] = 0
colored = cv2.applyColorMap((refined * 12 % 255).astype(np.uint8), cv2.COLORMAP_JET)
img = cv2.resize(cv2.imread(selected), (colored.shape[1], colored.shape[0]))
overlay = cv2.addWeighted(img, 0.5, colored, 0.5, 0)
show_image("Image Segmenter", overlay)
segmenter.close()

【MediaPipe版】タスク4:姿勢推定(Pose Landmarker)

機能説明:人体の33個の3Dランドマーク(特徴点の3次元座標.鼻,肩,肘,手首,腰,膝,足首等)を検出する.BlazePose GHUM Heavy(Googleの高精度姿勢推定モデル)をベースモデルとする.フィットネス分析,モーションキャプチャ(身体動作のデジタル記録),リハビリ動作評価等に利用される.本タスクではx, y座標による2D描画に加え,代表5点(鼻,両肩,両手首)のx, y, z値を画面およびコンソールに表示することで,正規化座標としての3D性を直接確認できる構成とする.

# task_pose_landmarker
import os, urllib.request
from pathlib import Path
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://ultralytics.com/images/bus.jpg"
IMG_DIR = get_image_dir("ultralytics")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# BlazePoseの33点接続関係(公式定義)
POSE_CONNECTIONS = [
    (0, 1), (1, 2), (2, 3), (3, 7), (0, 4), (4, 5), (5, 6), (6, 8),
    (9, 10), (11, 12), (11, 13), (13, 15), (15, 17), (15, 19), (15, 21),
    (17, 19), (12, 14), (14, 16), (16, 18), (16, 20), (16, 22), (18, 20),
    (11, 23), (12, 24), (23, 24), (23, 25), (24, 26), (25, 27), (26, 28),
    (27, 29), (28, 30), (29, 31), (30, 32), (27, 31), (28, 32)]

# 代表5点(インデックス, 表示名)
POSE_KEY_POINTS = [(0, "Nose"), (11, "L-Shoulder"), (12, "R-Shoulder"),
                   (15, "L-Wrist"), (16, "R-Wrist")]

# 画像選択
selected = select_file(
    title="姿勢推定する画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.PoseLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    min_pose_detection_confidence=0.7,
    min_pose_presence_confidence=0.7,
    min_tracking_confidence=0.7)
landmarker = vision.PoseLandmarker.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = landmarker.detect(image)

# 表示
img = cv2.imread(selected)
h, w = img.shape[:2]
for pose_landmarks in result.pose_landmarks:
    points = [(int(lm.x * w), int(lm.y * h)) for lm in pose_landmarks]
    for s, e in POSE_CONNECTIONS:
        cv2.line(img, points[s], points[e], (0, 255, 0), 2)
    for p in points:
        cv2.circle(img, p, 4, (0, 0, 255), -1)
    print("33個のランドマーク検出完了")
    # 代表点のx, y, z値を画面とコンソールに表示
    y_offset = 25
    for idx, name in POSE_KEY_POINTS:
        lm = pose_landmarks[idx]
        text = f"{name}: x={lm.x:.2f} y={lm.y:.2f} z={lm.z:.2f}"
        cv2.putText(img, text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 3)
        cv2.putText(img, text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
        print(f"  {text}")
        y_offset += 22
show_image("Pose Landmarker", img)
landmarker.close()

【MediaPipe版】タスク5:手のランドマーク検出(Hand Landmarker)

機能説明:両手それぞれについて21個のランドマーク(手首,各指の関節,指先)を検出し,左右の判定(handedness:検出した手が左右いずれかの識別)も同時に行う.手話認識,VR/ARのジェスチャー入力,リモコン代替等に利用される.本タスクではx, y座標による2D描画に加え,5指先(親指・人差し指・中指・薬指・小指)のx, y, z値を画面およびコンソールに表示することで,正規化座標としての3D性を直接確認できる構成とする.

# task_hand_landmarker
import os, urllib.request
from pathlib import Path
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://storage.googleapis.com/mediapipe-tasks/hand_landmarker/woman_hands.jpg"
IMG_DIR = get_image_dir("mediapipe")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 手の21点接続関係(公式定義:手首・親指・人差指・中指・薬指・小指の各骨格)
HAND_CONNECTIONS = [
    (0, 1), (1, 2), (2, 3), (3, 4),
    (0, 5), (5, 6), (6, 7), (7, 8),
    (5, 9), (9, 10), (10, 11), (11, 12),
    (9, 13), (13, 14), (14, 15), (15, 16),
    (13, 17), (0, 17), (17, 18), (18, 19), (19, 20)]

# 5指先(インデックス, 表示名)
HAND_KEY_POINTS = [(4, "Thumb"), (8, "Index"), (12, "Middle"),
                   (16, "Ring"), (20, "Pinky")]

# 画像選択
selected = select_file(
    title="手の画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.HandLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    num_hands=2,
    min_hand_detection_confidence=0.7,
    min_hand_presence_confidence=0.7,
    min_tracking_confidence=0.7)
landmarker = vision.HandLandmarker.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = landmarker.detect(image)

# 表示
img = cv2.imread(selected)
h, w = img.shape[:2]
y_offset = 25
for hand_landmarks, handedness in zip(result.hand_landmarks, result.handedness):
    points = [(int(lm.x * w), int(lm.y * h)) for lm in hand_landmarks]
    for s, e in HAND_CONNECTIONS:
        cv2.line(img, points[s], points[e], (0, 255, 0), 2)
    for p in points:
        cv2.circle(img, p, 4, (0, 0, 255), -1)
    label = handedness[0].category_name
    print(f"{label} の手を検出(信頼度: {handedness[0].score:.2f})")
    # 5指先のx, y, z値を画面とコンソールに表示
    for idx, name in HAND_KEY_POINTS:
        lm = hand_landmarks[idx]
        text = f"{label[0]}-{name}: x={lm.x:.2f} y={lm.y:.2f} z={lm.z:.2f}"
        cv2.putText(img, text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 3)
        cv2.putText(img, text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
        print(f"  {text}")
        y_offset += 22
show_image("Hand Landmarker", img)
landmarker.close()

【MediaPipe版】タスク6:ジェスチャー認識(Gesture Recognizer)

機能説明:Hand Landmarkerを基盤として手の形状を分類するタスクである.標準7種類のジェスチャー(Closed_Fist:グー,Open_Palm:パー,Pointing_Up:指差し,Thumb_Down:サムズダウン,Thumb_Up:サムズアップ,Victory:ピース,ILoveYou:アイラブユー)に対応する.非接触UI(手を触れずに操作するインタフェース),プレゼンテーション制御,アクセシビリティ用途(身体的制約のあるユーザの操作支援)等に利用される.

# task_gesture_recognizer
import os, urllib.request
from pathlib import Path
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe.tasks.python.components import processors

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://storage.googleapis.com/mediapipe-tasks/gesture_recognizer/thumbs_up.jpg"
IMG_DIR = get_image_dir("mediapipe")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 画像選択
selected = select_file(
    title="ジェスチャー画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.GestureRecognizerOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    num_hands=2,
    min_hand_detection_confidence=0.7,
    canned_gesture_classifier_options=processors.ClassifierOptions(score_threshold=0.5))
recognizer = vision.GestureRecognizer.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = recognizer.recognize(image)

# 表示(OpenCV-MediaPipe間の色変換:MediaPipeのRGB配列をOpenCVのBGRに変換)
img = cv2.cvtColor(image.numpy_view(), cv2.COLOR_RGB2BGR).copy()
for i, gestures in enumerate(result.gestures):
    gesture = gestures[0]
    handedness = result.handedness[i][0].category_name
    text = f"{handedness}: {gesture.category_name} ({gesture.score:.2f})"
    cv2.putText(img, text, (10, 30 + i * 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    print(text)
show_image("Gesture Recognizer", img)
recognizer.close()

【MediaPipe版】タスク7:顔ランドマーク検出(Face Landmarker)

機能説明:顔の478個の3Dランドマーク(顔メッシュ:顔表面を細かい多角形で覆う点群)を検出する.加えて52種類の表情ブレンドシェイプ係数(blendshape:表情の基本要素ごとの強さを0〜1で表す値.笑顔,目を閉じる,眉を上げる等)と頭部姿勢の変換行列(顔の向きを表す4×4行列)を出力する.アバター制御,AR効果,表情解析,運転手の眠気検知等に利用される.

# task_face_landmarker
import os, urllib.request
from pathlib import Path
import cv2, numpy as np
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://storage.googleapis.com/mediapipe-assets/portrait.jpg"
IMG_DIR = get_image_dir("mediapipe")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 画像選択
selected = select_file(
    title="顔の画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.FaceLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    output_face_blendshapes=True,
    output_facial_transformation_matrixes=True,
    num_faces=1,
    min_face_detection_confidence=0.7,
    min_face_presence_confidence=0.7,
    min_tracking_confidence=0.7)
landmarker = vision.FaceLandmarker.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = landmarker.detect(image)

# 表示
img = cv2.imread(selected)
h, w = img.shape[:2]
for face_landmarks in result.face_landmarks:
    for lm in face_landmarks:
        cv2.circle(img, (int(lm.x * w), int(lm.y * h)), 1, (0, 255, 0), -1)

if result.face_blendshapes:
    print("ブレンドシェイプ(上位5件):")
    for bs in sorted(result.face_blendshapes[0], key=lambda x: x.score, reverse=True)[:5]:
        print(f"  {bs.category_name}: {bs.score:.4f}")

if result.facial_transformation_matrixes:
    print("頭部姿勢の変換行列(4×4):")
    print(np.array(result.facial_transformation_matrixes[0]))
show_image("Face Landmarker", img)
landmarker.close()

【MediaPipe版】タスク8:顔検出(Face Detector)

機能説明:画像内の顔を矩形で検出し,両目・鼻先・口・両耳の耳珠点(じじゅてん:耳の前方にある軟骨突起)の6点キーポイントを出力する.BlazeFace full-range(CenterNet系の構造を採用した遠距離・広範囲向け顔検出モデル)をベースモデルとし,遠距離の被写体や背面カメラ画像にも対応する.プライバシー保護のための顔ぼかし,参加人数カウント等に利用される.

# task_face_detector
import os, urllib.request
from pathlib import Path
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URL = "https://storage.googleapis.com/mediapipe-assets/portrait.jpg"
IMG_DIR = get_image_dir("mediapipe")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_full_range/float16/latest/blaze_face_full_range.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 画像選択
selected = select_file(
    title="顔の画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.FaceDetectorOptions(base_options=python.BaseOptions(model_asset_path=str(model)))
detector = vision.FaceDetector.create_from_options(options)
result = detector.detect(mp.Image.create_from_file(selected))

# 表示
img = cv2.imread(selected)
for det in result.detections:
    bb = det.bounding_box
    cv2.rectangle(img, (bb.origin_x, bb.origin_y),
                  (bb.origin_x + bb.width, bb.origin_y + bb.height), (0, 255, 0), 2)
    for kp in det.keypoints:
        cv2.circle(img, (int(kp.x * img.shape[1]), int(kp.y * img.shape[0])), 3, (0, 0, 255), -1)
    print(f"顔検出: スコア {det.categories[0].score:.2f}")
show_image("Face Detector", img)
detector.close()

【MediaPipe版】タスク9:画像埋め込み(Image Embedder)

機能説明:画像を高次元の特徴ベクトル(埋め込み:画像の内容を数百次元程度の数値の並びで表現したもの)に変換する.MobileNet V3 Large(高表現能力を持つMobileNet V3アーキテクチャ)をベースモデルとする.2画像間のコサイン類似度(2つのベクトルがなす角度に基づく類似度.1に近いほど類似)を計算することで類似画像検索が可能となる.画像検索エンジン,重複画像検出,クラスタリング(類似データを自動的にグループ化する処理)等に利用される.

画像を2枚選択するため,選択操作を2回行う(ダイアログを2回表示する).

# task_image_embedder
import os, urllib.request
from pathlib import Path
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード
IMG_URLS = [
    "https://raw.githubusercontent.com/opencv/opencv/4.x/samples/data/aero1.jpg",
    "https://raw.githubusercontent.com/opencv/opencv/4.x/samples/data/aero3.jpg"]
IMG_DIR = get_image_dir("opencv")
for url in IMG_URLS:
    p = IMG_DIR / Path(url).name
    if not p.exists():
        urllib.request.urlretrieve(url, p)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/image_embedder/mobilenet_v3_large/float32/latest/mobilenet_v3_large.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 画像選択(2枚)
path1 = select_file(
    title="比較する1枚目の画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])
path2 = select_file(
    title="比較する2枚目の画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = vision.ImageEmbedderOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)), l2_normalize=True)
embedder = vision.ImageEmbedder.create_from_options(options)
emb1 = embedder.embed(mp.Image.create_from_file(path1))
emb2 = embedder.embed(mp.Image.create_from_file(path2))

# 表示
similarity = vision.ImageEmbedder.cosine_similarity(emb1.embeddings[0], emb2.embeddings[0])
print(f"画像間のコサイン類似度: {similarity:.4f}")
print("(1.0に近いほど類似)")
embedder.close()

【MediaPipe版】タスク10:音声分類(Audio Classifier)

機能説明:音声波形を入力として,AudioSet(Googleが公開した約200万件の音響イベントのデータセット)オントロジー(音響イベントの階層的な分類体系)の521種類の音響イベント(音楽,会話,犬の鳴き声,車のクラクション等)を分類する.YAMNet(AudioSetで学習された音響イベント分類モデル)をベースモデルとする.スマートホームの異常音検知,音響シーン解析,自動字幕用の音響タグ付け等に利用される.本タスクではYAMNetの前提に従い,入力WAVは16kHzモノラル16ビットPCM(音声をデジタル化する代表的な符号化方式)であることを前提とする.本タスクではフレームごとの分類結果をクラスごとに平均化することで,入力音声全体の主たる音響イベントを判定する構成とする.

# task_audio_classifier
import os, urllib.request, wave
from collections import defaultdict
from pathlib import Path
import numpy as np
from mediapipe.tasks import python
from mediapipe.tasks.python import audio
from mediapipe.tasks.python.components import containers

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 音声ダウンロード
WAV_URL = "https://storage.googleapis.com/audioset/miaow_16k.wav"
WAV_DIR = get_image_dir("audioset")
wav_path = WAV_DIR / Path(WAV_URL).name
if not wav_path.exists():
    urllib.request.urlretrieve(WAV_URL, wav_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/audio_classifier/yamnet/float32/latest/yamnet.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 音声選択
selected = select_file(
    title="WAVファイルを選択", initialdir=WAV_DIR,
    filetypes=[("WAV", "*.wav"), ("すべて", "*.*")])

# WAV読み込み(16kHzモノラル16ビットPCM前提)
with wave.open(selected, 'rb') as wf:
    n_channels = wf.getnchannels()
    sr = wf.getframerate()
    frames = wf.readframes(wf.getnframes())
samples = np.frombuffer(frames, dtype=np.int16)
if n_channels > 1:
    samples = samples.reshape(-1, n_channels).mean(axis=1)
samples = samples.astype(np.float32) / 32768.0

# 推論
options = audio.AudioClassifierOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)), max_results=5)
classifier = audio.AudioClassifier.create_from_options(options)
audio_data = containers.AudioData.create_from_array(samples, sr)
result = classifier.classify(audio_data)

# 全フレームの結果をクラスごとに平均化
sums = defaultdict(float)
counts = defaultdict(int)
for frame_result in result:
    for cat in frame_result.classifications[0].categories:
        sums[cat.category_name] += cat.score
        counts[cat.category_name] += 1
averaged = sorted(((name, sums[name] / counts[name]) for name in sums),
                  key=lambda x: x[1], reverse=True)

# 表示
print("音声分類結果(全フレーム平均上位5件):")
for name, score in averaged[:5]:
    print(f"  {name}: {score:.4f}")
classifier.close()

【MediaPipe版】タスク11:テキスト分類(Text Classifier,画像不要)

機能説明:入力テキストの感情を肯定的・否定的に分類する.BERT-classifier(MobileBERTベースの分類モデル)をベースモデルとし,SST-2(Stanford Sentiment Treebank:映画レビューの感情ラベル付きデータセット)で学習され,英語の感情分析(二値分類)に対応する.レビュー分析,SNS投稿のセンチメント解析(文章から書き手の感情の肯定・否定度を推定する分析),カスタマーサポートのトリアージ(問い合わせの重要度・種別による振り分け)等に利用される.

テキスト入力には,tkinter.simpledialog(tkinterの文字列入力用簡易ダイアログ)を用いる.本コードでは入力前処理(前後の空白除去,連続空白の単一化)を行う.

# task_text_classifier
import os, re, urllib.request
from pathlib import Path
from mediapipe.tasks import python
from mediapipe.tasks.python import text

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def input_text(title, prompt, initialvalue=""):
    import tkinter as tk
    from tkinter import simpledialog
    root = tk.Tk(); root.withdraw()
    s = simpledialog.askstring(title, prompt, initialvalue=initialvalue)
    root.destroy()
    return s
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/text_classifier/bert_classifier/float32/latest/bert_classifier.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# テキスト入力
input_str = input_text("テキスト分類", "分類したい英語テキストを入力:",
                       initialvalue="I love this product!")

# 入力前処理(前後空白除去・連続空白の単一化)
normalized_text = re.sub(r"\s+", " ", input_str).strip()

# 推論
options = text.TextClassifierOptions(base_options=python.BaseOptions(model_asset_path=str(model)))
classifier = text.TextClassifier.create_from_options(options)
result = classifier.classify(normalized_text)

# 表示
print(f"入力: {normalized_text}")
print("分類結果:")
for cat in result.classifications[0].categories:
    print(f"  {cat.category_name}: {cat.score:.4f}")
classifier.close()

【MediaPipe版】タスク12:言語検出(Language Detector,画像不要)

機能説明:入力テキストの言語を判定する.110言語に対応する.多言語コンテンツのルーティング(言語ごとに処理経路を振り分けること),自動翻訳の前段処理(翻訳元言語を特定する処理),国際化対応のWebサービス等に利用される.本コードでは入力テキストが20文字未満の場合に注意を表示し,短文での精度低下を利用者に明示する構成とする.

# task_language_detector
import os, urllib.request
from pathlib import Path
from mediapipe.tasks import python
from mediapipe.tasks.python import text

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def input_text(title, prompt, initialvalue=""):
    import tkinter as tk
    from tkinter import simpledialog
    root = tk.Tk(); root.withdraw()
    s = simpledialog.askstring(title, prompt, initialvalue=initialvalue)
    root.destroy()
    return s
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/language_detector/language_detector/float32/1/language_detector.tflite"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# テキスト入力
input_str = input_text("言語検出", "言語を判定したいテキストを入力:",
                       initialvalue="今日は良い天気です.")

# 短文での精度低下に関する注意
if len(input_str) < 20:
    print(f"注意: 入力が短い({len(input_str)}文字)ため判定精度が低下する場合があります")

# 推論
options = text.LanguageDetectorOptions(base_options=python.BaseOptions(model_asset_path=str(model)))
detector = text.LanguageDetector.create_from_options(options)
result = detector.detect(input_str)

# 表示
print(f"入力: {input_str}")
print("言語検出結果(上位):")
for d in result.detections[:5]:
    print(f"  {d.language_code}: {d.probability:.4f}")
detector.close()

【MediaPipe版】タスク13:ホリスティック検出(Holistic Landmarker)

機能説明:単一の人物に対して顔ランドマーク(468点),ポーズランドマーク(33点),左右の手ランドマーク(各21点)を一度の推論で同時検出する.合計543点の人体ランドマークを一括取得できるため,全身ジェスチャー解析,手話認識,スポーツフォーム解析,アバター駆動等の,顔・体幹・手指を統合的に扱う応用に適する.

タスク4・タスク5との関係:

# task_holistic_landmarker
import os, urllib.request
from pathlib import Path
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python.vision.holistic_landmarker import (
    HolisticLandmarker, HolisticLandmarkerOptions)

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path

def show_image(window_name, img):
    cv2.imshow(window_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード(全身が写る画像)
IMG_URL = "https://cdn.pixabay.com/photo/2019/03/12/20/39/girl-4051811_960_720.jpg"
IMG_DIR = get_image_dir("pixabay")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/holistic_landmarker/holistic_landmarker/float16/latest/holistic_landmarker.task"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# BlazePoseの33点接続関係(タスク4と同一定義)
POSE_CONNECTIONS = [
    (0, 1), (1, 2), (2, 3), (3, 7), (0, 4), (4, 5), (5, 6), (6, 8),
    (9, 10), (11, 12), (11, 13), (13, 15), (15, 17), (15, 19), (15, 21),
    (17, 19), (12, 14), (14, 16), (16, 18), (16, 20), (16, 22), (18, 20),
    (11, 23), (12, 24), (23, 24), (23, 25), (24, 26), (25, 27), (26, 28),
    (27, 29), (28, 30), (29, 31), (30, 32), (27, 31), (28, 32)]

# 手の21点接続関係(タスク5と同一定義)
HAND_CONNECTIONS = [
    (0, 1), (1, 2), (2, 3), (3, 4),
    (0, 5), (5, 6), (6, 7), (7, 8),
    (5, 9), (9, 10), (10, 11), (11, 12),
    (9, 13), (13, 14), (14, 15), (15, 16),
    (13, 17), (0, 17), (17, 18), (18, 19), (19, 20)]

# 画像選択
selected = select_file(
    title="ホリスティック検出する画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論
options = HolisticLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    min_pose_detection_confidence=0.7,
    min_pose_landmarks_confidence=0.7,
    min_face_detection_confidence=0.7,
    min_face_landmarks_confidence=0.7,
    min_hand_landmarks_confidence=0.7)
landmarker = HolisticLandmarker.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = landmarker.detect(image)

# 表示
img = cv2.imread(selected)
h, w = img.shape[:2]

# 顔ランドマーク(468点を緑の小点で描画)
if result.face_landmarks:
    for lm in result.face_landmarks:
        cv2.circle(img, (int(lm.x * w), int(lm.y * h)), 1, (0, 255, 0), -1)
    print(f"顔ランドマーク: 468点検出")

# ポーズランドマーク(タスク4と同一の描画形式)
if result.pose_landmarks:
    points = [(int(lm.x * w), int(lm.y * h)) for lm in result.pose_landmarks]
    for s, e in POSE_CONNECTIONS:
        cv2.line(img, points[s], points[e], (0, 255, 0), 2)
    for p in points:
        cv2.circle(img, p, 4, (0, 0, 255), -1)
    print(f"ポーズランドマーク: 33点検出")

# 両手ランドマーク(タスク5と同一の描画形式)
for hand_landmarks, label in [(result.left_hand_landmarks, "Left"),
                              (result.right_hand_landmarks, "Right")]:
    if hand_landmarks:
        points = [(int(lm.x * w), int(lm.y * h)) for lm in hand_landmarks]
        for s, e in HAND_CONNECTIONS:
            cv2.line(img, points[s], points[e], (255, 0, 0), 2)
        for p in points:
            cv2.circle(img, p, 4, (0, 0, 255), -1)
        print(f"{label}手ランドマーク: 21点検出")

show_image("Holistic Landmarker", img)
landmarker.close()

【MediaPipe版】タスク14:手の3D可視化(Hand Landmarker world landmarks)

機能説明:Hand Landmarkerが出力するhand_world_landmarks(手首を原点とするメートル単位の3D座標)を,matplotlibの3D散布図および骨格線として可視化する.画面はマウスドラッグで回転でき,手指の立体構造を任意の視点から観察できる.モーション解析,手形状の3D計測,VR/AR用ジェスチャー設計等に利用される.

タスク5との関係:

# task_hand_landmarker_3d
import os, urllib.request
from pathlib import Path
import matplotlib.pyplot as plt
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード(タスク5と同一)
IMG_URL = "https://storage.googleapis.com/mediapipe-tasks/hand_landmarker/woman_hands.jpg"
IMG_DIR = get_image_dir("mediapipe")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存.タスク5と同一モデル)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 手の21点接続関係(タスク5と同一定義)
HAND_CONNECTIONS = [
    (0, 1), (1, 2), (2, 3), (3, 4),
    (0, 5), (5, 6), (6, 7), (7, 8),
    (5, 9), (9, 10), (10, 11), (11, 12),
    (9, 13), (13, 14), (14, 15), (15, 16),
    (13, 17), (0, 17), (17, 18), (18, 19), (19, 20)]

# 画像選択
selected = select_file(
    title="手の画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論(タスク5と同一形式)
options = vision.HandLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    num_hands=2,
    min_hand_detection_confidence=0.7,
    min_hand_presence_confidence=0.7,
    min_tracking_confidence=0.7)
landmarker = vision.HandLandmarker.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = landmarker.detect(image)

# 3D可視化(world landmarks使用)
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection='3d')
colors = ['r', 'b']
for i, (hand_world, handedness) in enumerate(zip(result.hand_world_landmarks, result.handedness)):
    label = handedness[0].category_name
    xs = [lm.x for lm in hand_world]
    ys = [lm.y for lm in hand_world]
    zs = [lm.z for lm in hand_world]
    c = colors[i % len(colors)]
    ax.scatter(xs, ys, zs, c=c, marker='o', label=label)
    for s, e in HAND_CONNECTIONS:
        ax.plot([xs[s], xs[e]], [ys[s], ys[e]], [zs[s], zs[e]], c=c)
    print(f"{label} hand: 21 world landmarks (meters)")
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('Hand 3D (hand_world_landmarks)')
ax.legend()
plt.show()
landmarker.close()

【MediaPipe版】タスク15:姿勢の3D可視化(Pose Landmarker world landmarks)

機能説明:Pose Landmarkerが出力するpose_world_landmarks(腰中央を原点とするメートル単位の3D座標)を,matplotlibの3D散布図および骨格線として可視化する.画面はマウスドラッグで回転でき,全身姿勢の立体構造を任意の視点から観察できる.フィットネスフォーム解析,リハビリ評価,モーションキャプチャの可視化等に利用される.

タスク4との関係:

# task_pose_landmarker_3d
import os, urllib.request
from pathlib import Path
import matplotlib.pyplot as plt
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# ==== ヘルパ ====
def get_script_dir():
    return Path(os.path.dirname(os.path.abspath(__file__)))

def get_image_dir(subdir):
    d = Path("C:/image") / subdir
    d.mkdir(parents=True, exist_ok=True)
    return d

def select_file(title, initialdir, filetypes):
    import tkinter as tk
    from tkinter import filedialog
    root = tk.Tk(); root.withdraw()
    path = filedialog.askopenfilename(title=title, initialdir=str(initialdir), filetypes=filetypes)
    root.destroy()
    return path
# ================

# User-Agent設定
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)

# 画像ダウンロード(全身が写る単一人物画像)
IMG_URL = "https://cdn.pixabay.com/photo/2019/03/12/20/39/girl-4051811_960_720.jpg"
IMG_DIR = get_image_dir("pixabay")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(スクリプトと同じディレクトリに保存.タスク4と同一モデル)
SCRIPT_DIR = get_script_dir()
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task"
model = SCRIPT_DIR / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# BlazePoseの33点接続関係(タスク4と同一定義)
POSE_CONNECTIONS = [
    (0, 1), (1, 2), (2, 3), (3, 7), (0, 4), (4, 5), (5, 6), (6, 8),
    (9, 10), (11, 12), (11, 13), (13, 15), (15, 17), (15, 19), (15, 21),
    (17, 19), (12, 14), (14, 16), (16, 18), (16, 20), (16, 22), (18, 20),
    (11, 23), (12, 24), (23, 24), (23, 25), (24, 26), (25, 27), (26, 28),
    (27, 29), (28, 30), (29, 31), (30, 32), (27, 31), (28, 32)]

# 画像選択
selected = select_file(
    title="姿勢推定する画像を選択", initialdir=IMG_DIR,
    filetypes=[("画像", "*.jpg *.jpeg *.png *.bmp"), ("すべて", "*.*")])

# 推論(タスク4と同一形式)
options = vision.PoseLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    min_pose_detection_confidence=0.7,
    min_pose_presence_confidence=0.7,
    min_tracking_confidence=0.7)
landmarker = vision.PoseLandmarker.create_from_options(options)
image = mp.Image.create_from_file(selected)
result = landmarker.detect(image)

# 3D可視化(world landmarks使用)
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection='3d')
for pose_world in result.pose_world_landmarks:
    xs = [lm.x for lm in pose_world]
    ys = [lm.y for lm in pose_world]
    zs = [lm.z for lm in pose_world]
    ax.scatter(xs, ys, zs, c='r', marker='o')
    for s, e in POSE_CONNECTIONS:
        ax.plot([xs[s], xs[e]], [ys[s], ys[e]], [zs[s], zs[e]], c='g')
    print("33 world landmarks 検出 (meters)")
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('Pose 3D (pose_world_landmarks)')
plt.show()
landmarker.close()

MediaPipe の用途例

本資料の構成と前提

本資料では,MediaPipe Tasks API のうち Vision 系9タスク(Object Detector,Image Classifier,Image Segmenter,Pose Landmarker,Hand Landmarker,Gesture Recognizer,Face Landmarker,Face Detector,Image Embedder)と Audio 系1タスク(Audio Classifier)を組み合わせた7つの応用例を扱う.Text 系2タスク(Text Classifier,Language Detector)は本資料の対象外であるが,Tasks API の全体像を示す目的で「12タスク」として併記する.

7応用例の内訳は,用途例として概観する5件(WebVTuberスタジオ,ARフォトスタジオ,なんでも画像検索,ジェスチャと指先動作による対話型コントローラ,リアルタイム音響イベント分類)と,Face Landmarker を深掘りする2件(顔交換オーバーレイ,表情・頭部姿勢転写)からなる.

各コード例は学習目的のため簡略化しており,エラーチェック,例外処理,フォールバック処理は省略する(顔・手・音などの検出が空のフレームで実行時例外が生じる場合がある).本番運用ではこれらを別途実装すること.実行環境としては,pip install --no-user および「コマンドプロンプトを管理者として実行」は OS 全体に共通導入する場合の手順であり,venv 等の仮想環境を用いる場合はいずれも不要である.

MediaPipe の用途例を以下に示す.

ここで参照する MediaPipe Tasks API の12タスクは次のとおりである.

Object Detector(1),Image Classifier(2),Image Segmenter(3),Pose Landmarker(4),Hand Landmarker(5),Gesture Recognizer(6),Face Landmarker(7),Face Detector(8),Image Embedder(9),Audio Classifier(10),Text Classifier(11),Language Detector(12).本資料で実装するのはこのうち1〜10の10タスクである.

後続のコード中の t += 33 は VIDEOモード(連続フレームを時系列的に処理する動作モード)に与えるミリ秒単位のタイムスタンプ(フレームごとの時刻情報)であり,約30 FPS相当である(1000ミリ秒 ÷ 30 ≒ 33ミリ秒).cv2.waitKey(1) == 2727 はESCキーのASCIIコード(文字に割り当てられた数値.ESCは27番)を指す.MediaPipe公式配布モデルは Apache License 2.0(再配布や改変を認めるオープンソースライセンス)であるが,個別モデルは学習データ由来の制約等で別条件が付くことがあるため,商用利用および再配布時は各モデルカード(モデルの仕様やライセンスを記載した公式文書)のライセンス条項を確認すること.

各コード例は次のスタイル方針で記述する.モデルファイルの自動ダウンロードはスクリプト先頭の辞書 MODELS によるループで統一する.エラーチェック,例外処理,フォールバック処理は記述しない.関数化は行数削減に寄与する場合のみ行う.同一機能(モデルダウンロード,OpenCV-MediaPipe間の色変換,VIDEOモードのタイムスタンプ更新,ESCキーによる終了処理,リソース解放)は全コードで同一の記述に揃える.Pose Landmarker および Hand Landmarker のランドマーク番号(例:左肩=11,人差し指先=8)は MediaPipe 公式ドキュメントのランドマーク対応図に従う.正規化座標(画像の左上を(0,0),右下を(1,1)とする無次元座標)から画素位置への変換は,画像の幅 W と高さ H を乗じることで行う.

1. WebVTuberスタジオ

本コードは Face Landmarker,Pose Landmarker,Hand Landmarker を VIDEOモードで併用し,ウェブカメラ映像から毎フレーム以下を取得する.

output_face_blendshapes=True で取得したARKit互換ブレンドシェイプ係数(顔の各部位の動きを0から1の数値で表す係数)から,口パク用の jawOpen(顎の開き),mouthSmileLeftmouthSmileRight(左右の口角),瞬き用の eyeBlinkLefteyeBlinkRight(左右の目の閉じ),視線用の eyeLookIn/Out/Up/Down × Left/Right(左右各眼の内・外・上・下方向への視線移動)を抽出する.左右で対をなす項目を1組と数えると,jawOpen 1項目,口角1組,瞬き1組,視線4組の8カテゴリ・計13値となる.さらに output_facial_transformation_matrixes=True で得られる4×4変換行列(3次元空間における回転と並進を表す行列)の回転成分から頭部姿勢のyaw(左右の首振り),pitch(上下のうなずき),roll(左右の首傾げ)を算出する.

補助情報として身体の左肩座標および両手の人差し指先座標も併せて取得する.

Face Landmarker(478個の3Dランドマーク,52個の blendshapes スコア(各表情パラメータの強度),4×4の facial_transformation_matrixes),Pose Landmarker(33個のランドマーク:各点に xyzvisibility(点が可視である確率),presence(点が画像内に存在する確率)),Hand Landmarker(21個のランドマーク × 最大2手,handedness(左右の判定))の出力をマッピング層(Kalidokit 等:MediaPipe出力をVRMボーン回転値に変換するライブラリ)で @pixiv/three-vrm(Three.js上でVRMアバターを表示するライブラリ)のボーン回転と表情パラメータに変換する.

WebAssembly(ブラウザ上で動作するバイナリ実行形式)により単一タスクならCPUで30 FPS級,三タスク並行時は端末性能に応じた間引き(処理フレームを一定間隔で省略する手法)が必要となる.OBS仮想カメラ連携(配信ソフトOBSの仮想カメラ機能と接続する機能)で配信環境にも展開できる.

使用タスク:Face Landmarker(7),Pose Landmarker(4),Hand Landmarker(5)

ユーザ操作

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する(VRMレンダリングは別途Three.js(WebGLベースの3D描画ライブラリ)等での実装を想定し,本コードは抽出値の出力と確認表示までを担当する).ESCキーで終了する

Pythonコード

import os
import urllib.request
import cv2
import numpy as np
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# モデルファイルの自動ダウンロード
MODELS = {
    'face_landmarker.task':
        'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',
    'pose_landmarker_lite.task':
        'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task',
    'hand_landmarker.task':
        'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task',
}
for name, url in MODELS.items():
    if not os.path.exists(name):
        urllib.request.urlretrieve(url, name)

# 取り出すブレンドシェイプ係数(ARKit互換名,8カテゴリ・計13値)
BLENDSHAPES = [
    'jawOpen',
    'mouthSmileLeft', 'mouthSmileRight',
    'eyeBlinkLeft', 'eyeBlinkRight',
    'eyeLookInLeft', 'eyeLookOutLeft', 'eyeLookUpLeft', 'eyeLookDownLeft',
    'eyeLookInRight', 'eyeLookOutRight', 'eyeLookUpRight', 'eyeLookDownRight',
]

# タスクの初期化(VIDEOモード)
face = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path='face_landmarker.task'),
    running_mode=vision.RunningMode.VIDEO,
    output_face_blendshapes=True,
    output_facial_transformation_matrixes=True,
    num_faces=1))
pose = vision.PoseLandmarker.create_from_options(vision.PoseLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path='pose_landmarker_lite.task'),
    running_mode=vision.RunningMode.VIDEO,
    num_poses=1))
hand = vision.HandLandmarker.create_from_options(vision.HandLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path='hand_landmarker.task'),
    running_mode=vision.RunningMode.VIDEO,
    num_hands=2))

cap = cv2.VideoCapture(0)
t = 0  # ミリ秒単位のタイムスタンプ
while True:
    ok, frame = cap.read()
    if not ok:
        break
    H, W = frame.shape[:2]
    img = mp.Image(image_format=mp.ImageFormat.SRGB,
                   data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    f = face.detect_for_video(img, t)
    p = pose.detect_for_video(img, t)
    h = hand.detect_for_video(img, t)
    t += 33  # 約30FPS相当

    # 顔ブレンドシェイプ:口パク・瞬き・視線
    if f.face_blendshapes:
        s = {b.category_name: b.score for b in f.face_blendshapes[0]}
        print('blend:', ' '.join(f'{k}={s[k]:.2f}' for k in BLENDSHAPES))
        for i, k in enumerate(BLENDSHAPES):
            cv2.putText(frame, f'{k}={s[k]:.2f}', (10, 20 + i * 16),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.42, (0, 255, 0), 1)

    # 顔変換行列:頭部姿勢(yaw・pitch・roll)
    if f.facial_transformation_matrixes:
        R = f.facial_transformation_matrixes[0][:3, :3]
        yaw = np.degrees(np.arctan2(R[1, 0], R[0, 0]))
        pitch = np.degrees(np.arctan2(-R[2, 0], np.sqrt(R[2, 1] ** 2 + R[2, 2] ** 2)))
        roll = np.degrees(np.arctan2(R[2, 1], R[2, 2]))
        print(f'head: yaw={yaw:.1f} pitch={pitch:.1f} roll={roll:.1f}')
        cv2.putText(frame,
                    f'head yaw={yaw:.1f} pitch={pitch:.1f} roll={roll:.1f}',
                    (10, 20 + len(BLENDSHAPES) * 16 + 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 200, 255), 1)

    # 左肩(Pose Landmarker のランドマーク番号11)
    if p.pose_landmarks:
        ls = p.pose_landmarks[0][11]
        print(f'left_shoulder=({ls.x:.2f}, {ls.y:.2f})')
        px, py = int(ls.x * W), int(ls.y * H)
        cv2.circle(frame, (px, py), 6, (255, 0, 0), -1)
        cv2.putText(frame, f'L_shoulder ({ls.x:.2f},{ls.y:.2f})',
                    (px + 10, py), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)

    # 両手の人差し指先(Hand Landmarker のランドマーク番号8)
    for hlm in h.hand_landmarks:
        tip = hlm[8]
        print(f'index_tip=({tip.x:.2f}, {tip.y:.2f})')
        px, py = int(tip.x * W), int(tip.y * H)
        cv2.circle(frame, (px, py), 6, (0, 0, 255), -1)
        cv2.putText(frame, f'idx ({tip.x:.2f},{tip.y:.2f})',
                    (px + 10, py), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)

    cv2.imshow('vtuber preview', frame)
    if cv2.waitKey(1) == 27:  # ESCキーによる終了処理
        break

cap.release()
cv2.destroyAllWindows()

演習1.WebVTuberスタジオ

手順

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する
  3. 口を開閉,左右の口角を上げる,瞬き,左右上下への視線移動,首の左右振り・上下うなずき・左右傾げの動作を順に行う
  4. 左肩を画面内で動かし,両手の人差し指を画面内で移動させる
  5. ESCキーで終了する

ヒント

考察ポイント

2. ARフォトスタジオ(背景・髪色・服色・顔フィルタの一括変換)

Image Segmenter で人物,髪,服を画素単位に分離し,output_category_mask=True(画素ごとに最も確率の高いクラス番号を出力する設定)と output_confidence_masks=True(クラスごとの確率マップを出力する設定)で取得したマスクを参照して領域ごとに色を差し替える(ブラウザ実装では同等処理を WebGL シェーダ(GPU上で描画処理を行う小さなプログラム)で行うこともできる).

分離手段の選択肢としては,背景差し替え用の Selfie segmentation(selfie_segmenter.tflite),髪色変換用の Hair segmentation(hair_segmenter.tflite),多領域同時分離用の Multi-class selfie(selfie_multiclass_256x256.tflite,6クラス:0:background,1:hair,2:body-skin,3:face-skin,4:clothes,5:others)の3モデルが利用可能である.本コードでは1つのモデルで背景・髪・服を同時に分離できる Multi-class selfie のみを採用する.

Face Detector の6個のキーポイント(左眼,右眼,鼻先,口中心,左耳珠点,右耳珠点.公式名は left/right eye tragion)で顔向き判定とプライバシーぼかしを行い,Face Landmarker の facial_transformation_matrixes で立体スタンプ(3次元位置に追従して描画する装飾)を位置合わせする.

ビフォーアフター比較による教材演出やSNS投稿向けコンテンツ生成に応用できる.

使用タスク:Image Segmenter(3),Face Detector(8),Face Landmarker(7)

ユーザ操作

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する(モデルファイルは初回起動時のみダウンロードされ,スクリプトと同一ディレクトリに保存される.参照画像はスクリプト起動ごとに上書きダウンロードされる).ESCキーで終了する

Pythonコード

import os
import urllib.request
import cv2
import numpy as np
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# モデルファイルの自動ダウンロード
MODELS = {
    'selfie_multiclass_256x256.tflite':
        'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/latest/selfie_multiclass_256x256.tflite',
    'blaze_face_short_range.tflite':
        'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite',
    'face_landmarker.task':
        'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',
}
for name, url in MODELS.items():
    if not os.path.exists(name):
        urllib.request.urlretrieve(url, name)

# バーチャル背景用の参照画像(OpenCV公式テストデータ:ゴッホ「星月夜」)を上書きダウンロード
urllib.request.urlretrieve(
    'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/starry_night.jpg',
    'reference.jpg')

# タスクの初期化(VIDEOモード)
seg = vision.ImageSegmenter.create_from_options(vision.ImageSegmenterOptions(
    base_options=python.BaseOptions(model_asset_path='selfie_multiclass_256x256.tflite'),
    running_mode=vision.RunningMode.VIDEO,
    output_category_mask=True,
    output_confidence_masks=True))
fd = vision.FaceDetector.create_from_options(vision.FaceDetectorOptions(
    base_options=python.BaseOptions(model_asset_path='blaze_face_short_range.tflite'),
    running_mode=vision.RunningMode.VIDEO))
fl = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path='face_landmarker.task'),
    running_mode=vision.RunningMode.VIDEO,
    output_facial_transformation_matrixes=True))

cap = cv2.VideoCapture(0)
W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
bg = cv2.resize(cv2.imread('reference.jpg'), (W, H))  # 背景合成用に画像サイズを揃える

t = 0  # ミリ秒単位のタイムスタンプ
while True:
    ok, frame = cap.read()
    if not ok:
        break
    img = mp.Image(image_format=mp.ImageFormat.SRGB,
                   data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    s = seg.segment_for_video(img, t)
    d = fd.detect_for_video(img, t)
    l = fl.detect_for_video(img, t)
    t += 33  # 約30FPS相当

    # カテゴリマスクを画素単位のクラス番号に変換し,領域ごとに色を差し替える
    mask = s.category_mask.numpy_view().squeeze()  # 形状 (H, W, 1) を (H, W) に
    out = frame.copy()
    out[mask == 0] = bg[mask == 0]     # 0:background → 参照画像で合成
    out[mask == 1] = (180, 100, 255)   # 1:hair → ピンク
    out[mask == 4] = (50, 200, 50)     # 4:clothes → 緑

    # Face Detector:バウンディングボックス + 6キーポイント
    # (左眼,右眼,鼻先,口中心,左耳珠点,右耳珠点)
    for det in d.detections:
        bb = det.bounding_box
        cv2.rectangle(out, (bb.origin_x, bb.origin_y),
                      (bb.origin_x + bb.width, bb.origin_y + bb.height),
                      (0, 255, 255), 2)
        for kp in det.keypoints:
            cv2.circle(out, (int(kp.x * W), int(kp.y * H)), 4, (255, 0, 255), -1)

    # Face Landmarker:478点ランドマーク + 立体スタンプ(鼻先=ランドマーク番号1 に星)
    for face in l.face_landmarks:
        for lm in face:
            cv2.circle(out, (int(lm.x * W), int(lm.y * H)), 1, (0, 255, 0), -1)
        nose = face[1]
        cv2.putText(out, '*', (int(nose.x * W) - 20, int(nose.y * H) + 20),
                    cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 255), 3)

    if l.facial_transformation_matrixes:
        print('head_pose_matrix:', l.facial_transformation_matrixes[0])

    # ビフォーアフター比較:左=元映像,右=加工後
    cv2.imshow('photo studio', cv2.hconcat([frame, out]))
    if cv2.waitKey(1) == 27:  # ESCキーによる終了処理
        break

cap.release()
cv2.destroyAllWindows()

演習2.ARフォトスタジオ

手順

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する
  3. カメラに向かって正面・横顔・うつむきなど顔向きを変える
  4. 体を左右に動かし,髪と服が領域ごとに色付けされる様子を確認する
  5. ESCキーで終了する

ヒント

考察ポイント

3. 「なんでも画像検索」(類似画像検索)

Object Detector の efficientdet_lite0.tflite(COCO 80クラスで学習された物体検出モデル)で実世界の物体をリアルタイムに検出し,max_results(返す検出結果の最大件数)と score_threshold(採用する検出スコアの下限)で対象を絞り込む.

本コードでは参照画像にノートPCの写真を固定で用いるが,reference.jpg を任意の画像に差し替えれば,地域生物・展示物・備品など別対象に同じ枠組みを適用できる.参照画像(Unsplash公開のノートPC写真)を1枚自動ダウンロードし,これにもObject Detectorを適用して最高スコアの領域を切り出すことで,背景を除いた対象物体のみの特徴を参照とする.参照領域は Image Classifier で細分類し,Image Embedder で埋め込みベクトル(画像内容を表す数値ベクトル)に変換しておく.

ウェブカメラの各フレームでは,検出された全領域について,Object DetectorのCOCOクラス名と検出スコア,Image ClassifierのImageNet細分類名(ImageNet 1000クラス)と分類スコア,参照とのコサイン類似度の3つを枠の上に常時表示する.参照に最も類似する1領域を緑(しきい値以上で MATCH!)またはシアン(best)で強調し,他は薄い灰色で並置する.参照画像のサムネイル,COCOラベル,細分類ラベル,しきい値は画面右上に常時オーバーレイ表示する.

COCOとImageNetの結果を併用する意義は次の3点である.

vision.ImageEmbedder.cosine_similarity はクラスメソッドであり,引数として生のベクトルではなく Embedding オブジェクトを受け取る.COCO・ImageNet いずれも学習データの分布に依存するため,分布外(学習時に存在しないカテゴリや極端な構図)の入力では性能が低下しうる.

使用タスク:Object Detector(1),Image Classifier(2),Image Embedder(9)

ユーザ操作

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する(モデルファイルは初回起動時のみダウンロードされ,スクリプトと同一ディレクトリに保存される.参照画像はスクリプト起動ごとに上書きダウンロードされる).ESCキーで終了する

Pythonコード

import os
import urllib.request
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# モデルファイルの自動ダウンロード
MODELS = {
    'efficientdet_lite0.tflite':
        'https://storage.googleapis.com/mediapipe-models/object_detector/efficientdet_lite0/int8/1/efficientdet_lite0.tflite',
    'efficientnet_lite0.tflite':
        'https://storage.googleapis.com/mediapipe-models/image_classifier/efficientnet_lite0/float32/1/efficientnet_lite0.tflite',
    'mobilenet_v3_small.tflite':
        'https://storage.googleapis.com/mediapipe-models/image_embedder/mobilenet_v3_small/float32/1/mobilenet_v3_small.tflite',
}
for name, url in MODELS.items():
    if not os.path.exists(name):
        urllib.request.urlretrieve(url, name)

# 参照画像(Unsplash公開のノートPC画像)を上書きダウンロード
urllib.request.urlretrieve(
    'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=1280',
    'reference.jpg')

SIM_THRESHOLD = 0.7  # MATCH! 判定のコサイン類似度しきい値

# タスクの初期化(VIDEOモードと IMAGEモードを併用)
det_video = vision.ObjectDetector.create_from_options(vision.ObjectDetectorOptions(
    base_options=python.BaseOptions(model_asset_path='efficientdet_lite0.tflite'),
    running_mode=vision.RunningMode.VIDEO,
    max_results=5,
    score_threshold=0.5))
det_image = vision.ObjectDetector.create_from_options(vision.ObjectDetectorOptions(
    base_options=python.BaseOptions(model_asset_path='efficientdet_lite0.tflite'),
    running_mode=vision.RunningMode.IMAGE,
    max_results=5,
    score_threshold=0.3))
cls = vision.ImageClassifier.create_from_options(vision.ImageClassifierOptions(
    base_options=python.BaseOptions(model_asset_path='efficientnet_lite0.tflite'),
    running_mode=vision.RunningMode.IMAGE,
    max_results=1))
emb = vision.ImageEmbedder.create_from_options(vision.ImageEmbedderOptions(
    base_options=python.BaseOptions(model_asset_path='mobilenet_v3_small.tflite'),
    running_mode=vision.RunningMode.IMAGE))

# 参照画像から対象物体を切り出し,参照ラベルと参照埋め込みベクトルを作成する
ref_bgr = cv2.imread('reference.jpg')
ref_rgb = cv2.cvtColor(ref_bgr, cv2.COLOR_BGR2RGB)
ref_mp = mp.Image(image_format=mp.ImageFormat.SRGB, data=ref_rgb)
ref_best = max(det_image.detect(ref_mp).detections,
               key=lambda d: d.categories[0].score)
bb = ref_best.bounding_box
RH, RW = ref_rgb.shape[:2]
rx1, ry1 = max(0, bb.origin_x), max(0, bb.origin_y)
rx2, ry2 = min(RW, bb.origin_x + bb.width), min(RH, bb.origin_y + bb.height)
ref_crop_rgb = ref_rgb[ry1:ry2, rx1:rx2]
ref_crop_mp = mp.Image(image_format=mp.ImageFormat.SRGB, data=ref_crop_rgb)
ref_coco = ref_best.categories[0]
ref_fine = cls.classify(ref_crop_mp).classifications[0].categories[0]
ref_embedding = emb.embed(ref_crop_mp).embeddings[0]

# 参照画像のサムネイル(高さ140画素に縮小)
THUMB_H = 140
ref_thumb_bgr = cv2.resize(
    cv2.cvtColor(ref_crop_rgb, cv2.COLOR_RGB2BGR),
    (int(THUMB_H * ref_crop_rgb.shape[1] / ref_crop_rgb.shape[0]), THUMB_H))

cap = cv2.VideoCapture(0)
t = 0  # ミリ秒単位のタイムスタンプ
while True:
    ok, frame = cap.read()
    if not ok:
        break
    H, W = frame.shape[:2]
    rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    img = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)

    # 各検出領域について COCOラベル,ImageNetラベル,参照とのコサイン類似度を計算
    items = []
    best_sim = -2.0
    for d in det_video.detect_for_video(img, t).detections:
        bb = d.bounding_box
        x1, y1 = max(0, bb.origin_x), max(0, bb.origin_y)
        x2, y2 = min(W, bb.origin_x + bb.width), min(H, bb.origin_y + bb.height)
        crop_mp = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb[y1:y2, x1:x2])
        coco = d.categories[0]
        fine = cls.classify(crop_mp).classifications[0].categories[0]
        # cosine_similarity はクラスメソッド.Embedding オブジェクトを引数に取る
        sim = vision.ImageEmbedder.cosine_similarity(
            ref_embedding, emb.embed(crop_mp).embeddings[0])
        items.append((sim, x1, y1, x2, y2, coco, fine))
        best_sim = max(best_sim, sim)
    t += 33  # 約30FPS相当

    # 検出領域ごとに枠とラベルを描画
    for sim, x1, y1, x2, y2, coco, fine in items:
        is_best = sim == best_sim
        matched = is_best and sim >= SIM_THRESHOLD
        color = (0, 255, 0) if matched else ((0, 200, 255) if is_best else (180, 180, 180))
        thickness = 3 if matched else (2 if is_best else 1)
        cv2.rectangle(frame, (x1, y1), (x2, y2), color, thickness)
        lines = [
            f'[OD] {coco.category_name} {coco.score:.2f}',
            f'[IC] {fine.category_name} {fine.score:.2f}',
            f'[IE] sim={sim:.2f}' + ('  MATCH!' if matched else ('  best' if is_best else '')),
        ]
        for k, txt in enumerate(lines):
            cv2.putText(frame, txt, (x1, max(y1 - 38, 14) + k * 16),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)

    # 参照画像のサムネイルとラベルを画面右上にオーバーレイ
    th, tw = ref_thumb_bgr.shape[:2]
    x0, y0 = W - tw - 10, 10
    cv2.rectangle(frame, (x0 - 4, y0 - 4), (x0 + tw + 4, y0 + th + 60),
                  (30, 30, 30), -1)
    frame[y0:y0 + th, x0:x0 + tw] = ref_thumb_bgr
    cv2.putText(frame, f'REF [OD] {ref_coco.category_name} {ref_coco.score:.2f}',
                (x0, y0 + th + 18), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
    cv2.putText(frame, f'REF [IC] {ref_fine.category_name} {ref_fine.score:.2f}',
                (x0, y0 + th + 36), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
    cv2.putText(frame, f'threshold: {SIM_THRESHOLD}',
                (x0, y0 + th + 54), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)

    cv2.imshow('encyclopedia', frame)
    if cv2.waitKey(1) == 27:  # ESCキーによる終了処理
        break

cap.release()
cv2.destroyAllWindows()

演習3.「なんでも画像検索」

手順

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する
  3. ノートPC(参照画像と同種の物体)をカメラに写し,COCOラベル,ImageNetラベル,コサイン類似度の表示を確認する
  4. カップ・本・ペットボトル等,参照画像と異なる物体を写し,類似度の値を観察する
  5. reference.jpg を別の画像に差し替えてスクリプトを再起動し,参照対象を変えた挙動を確認する
  6. ESCキーで終了する

ヒント

考察ポイント

4. ジェスチャと指先動作による対話型コントローラ

Hand Landmarker(手のひらと指の21点の位置を検出するMediaPipeのタスク)が出力する指先ランドマーク(ランドマークとは検出対象上の特徴点であり,番号で識別される.INDEX_FINGER_TIP=8MIDDLE_FINGER_TIP=12RING_FINGER_TIP=16PINKY_TIP=20THUMB_TIP=4 が各指の先端を指す)の y 座標(正規化座標における縦方向の値.画面上端を0,下端を1とする)と速度を監視し,閾値交差(指先 y 座標が事前に定めた境界値を上から下へまたは下から上へまたぐ事象)を打点イベントとして検出する.

Gesture Recognizer(手の形状を分類するMediaPipeのタスク)の組み込み7クラス(Closed_Fist:握り拳,Open_Palm:開いた手のひら,Pointing_Up:人差し指で上を指す,Thumb_Up:親指を上に立てる,Thumb_Down:親指を下に向ける,Victory:人差し指と中指でV字,ILoveYou:親指・人差し指・小指を立てる手話サイン)を用いることで,ジェスチャごとに離散的なコマンドを生成する.

Gesture Recognizer は内部で手のランドマーク検出を含むため,その出力 hand_landmarks を経由して指先座標を参照する.本コードでは Hand Landmarker タスクを別途初期化していない(「使用タスク」欄の Hand Landmarker は Gesture Recognizer の付随出力として参照される位置づけである).

検出した打点イベントと離散コマンドは,下流の処理に渡すことで多様な応用に展開できる.たとえば,ジェスチャに音高を割り当てればエアピアノやエアドラムとなり,OSのキーボードショートカットに割り当てればプレゼンテーション操作となり,IoT機器のAPIに接続すればスマート家電制御となり,ゲームの入力イベントに接続すればコマンド入力となる.本コードでは下流処理を分離するため,検出結果を画面オーバーレイと標準出力に送出するまでを担当する.標準出力された NOTE_ON を実際の音に変換するには,別途 MIDI 出力ライブラリやサウンド合成ライブラリへの結線が必要である.

本実装では prev_y[i] を「r.hand_landmarksi 番目」に紐づけているため,フレーム間で手の検出順序が入れ替わった場合に偽の閾値交差が生じうる(教材用途では片手運用または安定した手の出現順を前提とする).

Model Maker(MediaPipeが提供するモデル再学習用ツール)で独自の指型を学習させた gesture_recognizer.task(MediaPipe Tasksが読み込むモデルバンドルファイル形式)を生成すれば,手話の指文字(日本手話・米国手話などで五十音やアルファベットを表す指の形)や独自ジェスチャへ拡張できる.

使用タスク:Hand Landmarker(5),Gesture Recognizer(6)

ユーザ操作

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する(モデルファイルは初回起動時のみダウンロードされ,スクリプトと同一ディレクトリに保存される)
  3. 検出結果は画面上にオーバーレイ表示されると同時に,イベント(NOTE_ONDRUM_HIT)が標準出力に送出される.ESCキーで終了する

Pythonコード

import os
import urllib.request
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# モデルファイルの自動ダウンロード
MODELS = {
    'gesture_recognizer.task':
        'https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task',
}
for name, url in MODELS.items():
    if not os.path.exists(name):
        urllib.request.urlretrieve(url, name)

# ジェスチャ名と音高(科学的音名表記.C4が中央ド,数字は音域)の対応表
NOTES = {'Closed_Fist': 'C4', 'Open_Palm': 'E4', 'Pointing_Up': 'G4',
         'Thumb_Up': 'A4', 'Thumb_Down': 'B4', 'Victory': 'C5', 'ILoveYou': 'D5'}

# タスクの初期化(VIDEOモード).Gesture Recognizer は内部に Hand Landmarker を含む
gr = vision.GestureRecognizer.create_from_options(vision.GestureRecognizerOptions(
    base_options=python.BaseOptions(model_asset_path='gesture_recognizer.task'),
    running_mode=vision.RunningMode.VIDEO,
    num_hands=2))

prev_y = [1.0, 1.0]   # 直前フレームの指先y座標(手2本分).初期値1.0は画面下端
drum_flash = [0, 0]   # ドラムヒット表示の残フレーム数(手2本分)

cap = cv2.VideoCapture(0)
t = 0  # ミリ秒単位のタイムスタンプ
while True:
    ok, frame = cap.read()
    if not ok:
        break
    H, W = frame.shape[:2]
    img = mp.Image(image_format=mp.ImageFormat.SRGB,
                   data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    r = gr.recognize_for_video(img, t)
    t += 33  # 約30FPS相当

    # ジェスチャ認識結果と対応ノートを画面左上に表示する
    for idx, g in enumerate(r.gestures):
        name = g[0].category_name
        score = g[0].score
        note = NOTES.get(name, '-')
        cv2.putText(frame, f'Hand{idx}  {name}  ->  {note}  ({score:.2f})',
                    (10, 30 + 30 * idx),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
        if name in NOTES:
            print(f'NOTE_ON {note}  (gesture: {name})')

    # 各手の人差し指先(ランドマーク番号8)のy座標を取得し,
    # 「上から下へ閾値0.5を越えた瞬間」を打楽器ヒットとして検出する
    # ※ prev_y[i] は r.hand_landmarks の i 番目に紐づくため,
    #   手の検出順序が入れ替わると偽の閾値交差が生じうる(教材用途では片手運用を前提)
    for i, lm in enumerate(r.hand_landmarks[:2]):
        y = lm[8].y
        px, py = int(lm[8].x * W), int(y * H)
        cv2.circle(frame, (px, py), 8, (255, 255, 0), 2)
        if prev_y[i] < 0.5 <= y:  # 上方から下方への閾値交差
            print(f'DRUM_HIT  hand={i}')
            drum_flash[i] = 8  # 8フレーム間ヒット表示を残す
        prev_y[i] = y
        if drum_flash[i] > 0:
            cv2.circle(frame, (px, py), 20, (0, 0, 255), -1)
            drum_flash[i] -= 1

    # 閾値ライン(y=0.5)を水平線として描画し,打点基準を可視化する
    cv2.line(frame, (0, H // 2), (W, H // 2), (200, 200, 200), 1)

    cv2.imshow('gesture instrument', frame)
    if cv2.waitKey(1) == 27:  # ESCキーによる終了処理
        break

cap.release()
cv2.destroyAllWindows()

演習4.ジェスチャと指先動作による対話型コントローラ

手順

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する
  3. 片手で7種類のジェスチャ(Closed_FistOpen_PalmPointing_UpThumb_UpThumb_DownVictoryILoveYou)を順に作る
  4. 人差し指を画面上部から閾値ライン(y=0.5の水平線)を越えて下方向に振り下ろす動作を繰り返す
  5. 標準出力の NOTE_ONDRUM_HIT の出力タイミングを確認する
  6. ESCキーで終了する

ヒント

考察ポイント

5. マイク入力によるリアルタイム音響イベント分類と音声波形・スペクトル表示

Audio Classifier(音声データに含まれる音響イベントを分類するMediaPipeのタスク)に YAMNet(Googleが公開する事前学習済みモデルであり,AudioSetデータセットで学習された521クラスの音響イベント分類器)を読み込ませることで,マイクから取り込んだ音声を分類する.YAMNetは入力として16kHz・モノラル・float32の波形(標本化周波数16kHz・1チャネル・32ビット浮動小数点形式の音声信号)を前提とし,各クラスに対するスコア(0から1の範囲をとる確信度)を出力する.動作モードは AUDIO_CLIPS(独立した固定長の音声クリップを同期的に分類するモード)を採用し,1秒分の音声を1クリップとして逐次分類する.録音と分類が直列に進むため,連続ストリーミング処理ではなく1秒ブロック単位の準リアルタイムである.連続ストリーミングが必要な場合は AUDIO_STREAM モード(コールバックで分類結果を非同期に受け取るモード)を用いる.

時間波形表示(横軸を時刻,縦軸を振幅とする音の大きさの時間変化を示す表示)と周波数スペクトル表示(音に含まれる各周波数成分の強度分布を示す表示)を併用することで,分類結果と入力信号の対応関係を確認できる.スペクトルは numpy.fft.rfft(実数信号に対する高速フーリエ変換.時間波形を周波数成分に分解する処理)で得られた振幅をdB(デシベル,対数スケールでの強度単位)に変換して描画する.サンプリング周波数16kHzに対するナイキスト周波数(標本化定理により再現可能な最高周波数であり,サンプリング周波数の半分)は8kHzであり,スペクトルの右端が8kHzに対応する.コード中の (spec_db + 40) / 80 という正規化は,表示レンジを-40 dBを画面下端,+40 dBを画面上端に対応付けるための便宜的な選択であり,入力ゲインに応じて調整してよい.

分類結果と音声特徴は,下流の処理に渡すことで多様な応用に展開できる.たとえば,特定カテゴリ(Speech:人の発話,Music:音楽,Dog:犬の鳴き声)の検出をトリガとしてOSの通知APIに接続すれば異常音検知の警告システムとなり,録音ファイルの自動タグ付けに用いれば音声アーカイブの索引化となり,IoT機器のAPIに接続すれば音声反応型のスマート家電制御となり,スペクトル形状を特徴量として扱えば楽器音の識別や話者照合の入力となる.本コードでは下流処理を分離するため,検出結果と入力波形・スペクトルを画面表示するまでを担当する.

Model Maker(MediaPipeが提供するモデル再学習用ツール)で独自の音響カテゴリ(特定の機械動作音,動物の鳴き声,楽器音等)を学習させた .tflite(TensorFlow Liteのモデルファイル形式)のモデルを生成すれば,用途に特化した分類器へ拡張できる.

使用タスク:Audio Classifier(10)

ユーザ操作

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe sounddevice opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. マイクを接続してスクリプトを実行する(モデルファイルは初回起動時のみダウンロードされ,スクリプトと同一ディレクトリに保存される)
  3. 検出結果は3つのウィンドウにオーバーレイ表示される(audio:上位5クラスの分類結果とスコアを水平棒グラフで表示,waveform:1秒間の時間波形を表示,spectrum:0-8000Hzの周波数スペクトルを表示).ESCキーで終了する

Pythonコード

import os
import urllib.request
import cv2
import numpy as np
import sounddevice as sd
from mediapipe.tasks import python
from mediapipe.tasks.python import audio
from mediapipe.tasks.python.components.containers.audio_data import AudioData

# モデルファイルの自動ダウンロード
MODELS = {
    'yamnet.tflite':
        'https://storage.googleapis.com/mediapipe-models/audio_classifier/yamnet/float32/1/yamnet.tflite',
}
for name, url in MODELS.items():
    if not os.path.exists(name):
        urllib.request.urlretrieve(url, name)

# タスクの初期化(AUDIO_CLIPSモード:固定長クリップを同期的に分類)
ac = audio.AudioClassifier.create_from_options(audio.AudioClassifierOptions(
    base_options=python.BaseOptions(model_asset_path='yamnet.tflite'),
    running_mode=audio.RunningMode.AUDIO_CLIPS,
    max_results=5))

while True:
    # 1秒分(16000サンプル)の音声をマイクから録音.
    # 録音と分類が直列のため,処理は1秒ブロック単位の準リアルタイム
    buf = sd.rec(16000, samplerate=16000, channels=1, dtype='float32')
    sd.wait()
    wave = buf.flatten()

    # AudioData:MediaPipe Audio Classifier の入力形式(波形配列とサンプリング周波数を保持)
    cats = ac.classify(AudioData.create_from_array(wave, 16000))[0].classifications[0].categories

    # 分類結果パネル:上位5クラスのスコアを水平棒グラフで表示
    img = np.zeros((300, 600, 3), dtype=np.uint8)
    for i, c in enumerate(cats):
        y = 40 + i * 50
        cv2.rectangle(img, (200, y - 20), (200 + int(c.score * 380), y + 10), (0, 200, 0), -1)
        cv2.putText(img, f'{c.category_name}', (10, y),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
        cv2.putText(img, f'{c.score:.2f}', (200 + int(c.score * 380) + 5, y),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
    cv2.imshow('audio', img)

    # 時間波形パネル(横軸:時刻0-1秒,縦軸:振幅-1~+1)
    wave_img = np.zeros((200, 600, 3), dtype=np.uint8)
    cv2.line(wave_img, (0, 100), (600, 100), (80, 80, 80), 1)
    xs = np.linspace(0, 599, len(wave)).astype(np.int32)
    ys = (100 - wave * 95).astype(np.int32)
    cv2.polylines(wave_img, [np.stack([xs, ys], axis=1)], False, (0, 255, 255), 1)
    cv2.putText(wave_img, 'waveform (1s)', (10, 20),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
    cv2.imshow('waveform', wave_img)

    # 周波数スペクトルパネル(rfftの結果をdB変換し,0-8000Hzを表示)
    spec = np.abs(np.fft.rfft(wave))
    spec_db = 20 * np.log10(spec + 1e-6)              # dB変換(log10の発散を避けるため微小値を加算)
    spec_db = np.clip((spec_db + 40) / 80, 0, 1)      # -40dBを画面下端,+40dBを画面上端に対応付け
    spec_img = np.zeros((200, 600, 3), dtype=np.uint8)
    bin_x = np.linspace(0, 599, len(spec_db)).astype(np.int32)
    for i in range(len(spec_db) - 1):
        bar_h = int(spec_db[i] * 180)
        cv2.line(spec_img, (bin_x[i], 195), (bin_x[i], 195 - bar_h), (255, 100, 0), 1)
    cv2.putText(spec_img, 'spectrum (0-8kHz)', (10, 20),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
    cv2.imshow('spectrum', spec_img)

    if cv2.waitKey(1) == 27:  # ESCキーによる終了処理
        break

cv2.destroyAllWindows()

演習5.リアルタイム音響イベント分類

手順

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe sounddevice opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. マイクを接続してスクリプトを実行する
  3. マイクに向かって「発話」「拍手」「指笛または口笛」「机を叩く音」など異なる音響イベントを1秒以上ずつ入力する
  4. 音楽再生(スマートフォン等の音源)をマイクに聞かせる
  5. audiowaveformspectrum の3ウィンドウの表示変化を確認する
  6. ESCキーで終了する

ヒント

考察ポイント

6. 顔交換オーバーレイ

ユーザから見える効果は次のとおりである.画面には自分の背景・体・髪型がそのまま映り,顔だけが別人物の顔に置き換わる(自分が別の顔を持って動いているように見える).

ウェブカメラの各フレームに対し,ユーザの顔位置を別人物の静止画の顔で差し替える.Face Landmarker(顔の478点を検出するMediaPipeタスク)で取得したランドマークのうち顔メッシュ部分(先頭468点,虹彩の10点を除く)をDelaunay三角形分割(点集合をどの三角形の外接円も他の点を含まないように分割する手法)し,三角形ごとに静止画の対応領域をアフィン変換(拡大・縮小・回転・平行移動の組み合わせ)で写像する.

処理の流れ

精度・安定性の工夫

本実装の前提と限界

使用タスク:Face Landmarker(7)

ユーザ操作

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する(モデルファイルは初回起動時のみダウンロードされ,スクリプトと同一ディレクトリに保存される)
  3. 起動時に表示されるダイアログで別人物の正面顔画像を選択する(初期ディレクトリはスクリプトと同じ位置).ESCキーで終了する

Pythonコード

import os
import urllib.request
import tkinter as tk
from tkinter import filedialog
import cv2
import numpy as np
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# スクリプト所在ディレクトリ:モデル保存先と画像選択ダイアログの初期位置
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

# モデルファイルの自動ダウンロード(SCRIPT_DIR配下に保存)
URL = 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task'
MODEL_PATH = os.path.join(SCRIPT_DIR, 'face_landmarker.task')
if not os.path.exists(MODEL_PATH):
    urllib.request.urlretrieve(URL, MODEL_PATH)

# GPU推論の自動選択(GPU利用可能ならGPU,不可ならCPU)
try:
    _t = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
        base_options=python.BaseOptions(model_asset_path=MODEL_PATH,
                                         delegate=python.BaseOptions.Delegate.GPU),
        running_mode=vision.RunningMode.IMAGE))
    _t.close()
    DELEGATE = python.BaseOptions.Delegate.GPU
except Exception:
    DELEGATE = python.BaseOptions.Delegate.CPU
print('delegate:', DELEGATE.name)

# 参照画像の選択(tkinterダイアログ,初期位置はSCRIPT_DIR)
_root = tk.Tk()
_root.withdraw()
SOURCE_PATH = filedialog.askopenfilename(
    initialdir=SCRIPT_DIR,
    title='別人物の正面顔画像を選択',
    filetypes=[('画像ファイル', '*.jpg *.jpeg *.png *.bmp')])
_root.destroy()

# 静止画の読み込みと顔ランドマーク検出(IMAGEモード,1回のみ)
src_bgr = cv2.imread(SOURCE_PATH)
src_h, src_w = src_bgr.shape[:2]
fl_image = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=MODEL_PATH, delegate=DELEGATE),
    running_mode=vision.RunningMode.IMAGE))
src_mp = mp.Image(image_format=mp.ImageFormat.SRGB,
                  data=cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB))
src_result = fl_image.detect(src_mp)

# 顔メッシュ用は先頭468点(虹彩の10点を除く)
N = 468
src_points = np.array([(lm.x * src_w, lm.y * src_h)
                       for lm in src_result.face_landmarks[0][:N]], dtype=np.float32)

# Delaunay三角形分割:ランドマーク番号の三つ組として記録
subdiv = cv2.Subdiv2D((0, 0, src_w, src_h))
for p in src_points:
    subdiv.insert((float(p[0]), float(p[1])))
triangles = []
for tr in subdiv.getTriangleList():
    verts = [(tr[0], tr[1]), (tr[2], tr[3]), (tr[4], tr[5])]
    idx = []
    for v in verts:
        d = np.linalg.norm(src_points - v, axis=1)
        i = int(np.argmin(d))
        if d[i] < 1.0:
            idx.append(i)
    if len(idx) == 3 and len(set(idx)) == 3:
        triangles.append(idx)

# 静止画三角形の符号付き面積(三角形法線のZ成分に等しい)を事前計算
def signed_area(pts):
    a, b, c = pts
    return (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0])
src_signs = [np.sign(signed_area(src_points[t])) for t in triangles]

# ウェブカメラ用 Face Landmarker (VIDEOモード)
fl_video = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=MODEL_PATH, delegate=DELEGATE),
    running_mode=vision.RunningMode.VIDEO))

# ウェブカメラ解像度を1280x720に要求(非対応カメラは実装値で動作)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

# 指数移動平均パラメータ(α=0.5.大きいほど平滑強).※単一顔前提
SMOOTH = 0.5
prev_points = None

t = 0  # ミリ秒単位のタイムスタンプ
while True:
    ok, frame = cap.read()
    if not ok:
        break
    img = mp.Image(image_format=mp.ImageFormat.SRGB,
                   data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    result = fl_video.detect_for_video(img, t)
    t += 33  # 約30FPS相当

    output = frame.copy()
    for face in result.face_landmarks:
        dst_points = np.array([(lm.x * W, lm.y * H)
                               for lm in face[:N]], dtype=np.float32)
        # 指数移動平均(α=0.5)で時間的平滑化
        if prev_points is None:
            prev_points = dst_points
        dst_points = (1 - SMOOTH) * dst_points + SMOOTH * prev_points
        prev_points = dst_points

        # 三角形ごとにアフィン変換,Z座標による裏面カリング適用
        warped_face = np.zeros_like(frame)
        for tri, src_sign in zip(triangles, src_signs):
            # 裏面カリング:符号反転した三角形は描画しない
            if np.sign(signed_area(dst_points[tri])) != src_sign:
                continue
            s = src_points[tri]
            d = dst_points[tri]
            r1 = cv2.boundingRect(s)
            r2 = cv2.boundingRect(d)
            s_local = s - np.array([r1[0], r1[1]], dtype=np.float32)
            d_local = d - np.array([r2[0], r2[1]], dtype=np.float32)
            crop = src_bgr[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
            M = cv2.getAffineTransform(s_local, d_local)
            w_tri = cv2.warpAffine(crop, M, (r2[2], r2[3]),
                                   None, flags=cv2.INTER_LINEAR,
                                   borderMode=cv2.BORDER_REFLECT_101)
            mask = np.zeros((r2[3], r2[2]), dtype=np.uint8)
            cv2.fillConvexPoly(mask, np.int32(d_local), 255)
            roi = warped_face[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]]
            np.copyto(roi, w_tri, where=mask[:, :, None].astype(bool))

        # 凸包マスクへのGaussian Blur(カーネル21×21)で羽根化,seamlessCloneで合成
        hull = cv2.convexHull(dst_points.astype(np.int32))
        face_mask = np.zeros((H, W), dtype=np.uint8)
        cv2.fillConvexPoly(face_mask, hull, 255)
        face_mask = cv2.GaussianBlur(face_mask, (21, 21), 0)
        Mm = cv2.moments(face_mask)
        cx = int(Mm['m10'] / Mm['m00'])
        cy = int(Mm['m01'] / Mm['m00'])
        output = cv2.seamlessClone(warped_face, frame, face_mask,
                                   (cx, cy), cv2.NORMAL_CLONE)

    cv2.imshow('face swap', output)
    if cv2.waitKey(1) == 27:  # ESCキーによる終了処理
        break

cap.release()
cv2.destroyAllWindows()

演習6.顔交換オーバーレイ

手順

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する
  3. 起動時のダイアログで別人物の正面顔画像(jpg/jpeg/png/bmp)を選択する
  4. 正面,左右への首振り,上下のうなずき,左右の首傾げを順に行う
  5. 口の開閉,瞬きなどの表情変化を加える
  6. ESCキーで終了する

ヒント

考察ポイント

7. 表情・頭部姿勢転写

ユーザから見える効果は次のとおりである.画面には別人物の静止画が映り,その顔がユーザの表情と頭の向きに追従して動く(他人の写真が自分の真似をするように見える).

別人物の静止画の顔を,ウェブカメラのユーザの表情と頭部姿勢に追従させる.Face Landmarker の output_face_blendshapes=Trueoutput_facial_transformation_matrixes=True を有効化し,52次元のブレンドシェイプ係数(無表情からの各部位の動きの強度)と4×4の頭部姿勢行列(頭部の3次元位置と向きを表す同次変換行列)を取得する.表情と頭部姿勢はランドマーク座標自体に反映されるため,Delaunay三角形分割で静止画の顔を変形してアバター化する.併せてブレンドシェイプ係数の上位5項目を画面に,頭部姿勢行列を標準出力に提示する.

処理の流れ

精度・安定性の工夫

本実装の前提と限界

使用タスク:Face Landmarker(7)

ユーザ操作

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する(モデルファイルは初回起動時のみダウンロードされ,スクリプトと同一ディレクトリに保存される)
  3. 起動時に表示されるダイアログで別人物の正面顔画像を選択する(初期ディレクトリはスクリプトと同じ位置).ESCキーで終了する

Pythonコード

import os
import urllib.request
import tkinter as tk
from tkinter import filedialog
import cv2
import numpy as np
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# スクリプト所在ディレクトリ:モデル保存先と画像選択ダイアログの初期位置
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

# モデルファイルの自動ダウンロード(SCRIPT_DIR配下に保存)
URL = 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task'
MODEL_PATH = os.path.join(SCRIPT_DIR, 'face_landmarker.task')
if not os.path.exists(MODEL_PATH):
    urllib.request.urlretrieve(URL, MODEL_PATH)

# GPU推論の自動選択(GPU利用可能ならGPU,不可ならCPU)
try:
    _t = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
        base_options=python.BaseOptions(model_asset_path=MODEL_PATH,
                                         delegate=python.BaseOptions.Delegate.GPU),
        running_mode=vision.RunningMode.IMAGE))
    _t.close()
    DELEGATE = python.BaseOptions.Delegate.GPU
except Exception:
    DELEGATE = python.BaseOptions.Delegate.CPU
print('delegate:', DELEGATE.name)

# 参照画像の選択(tkinterダイアログ,初期位置はSCRIPT_DIR)
_root = tk.Tk()
_root.withdraw()
SOURCE_PATH = filedialog.askopenfilename(
    initialdir=SCRIPT_DIR,
    title='別人物の正面顔画像を選択',
    filetypes=[('画像ファイル', '*.jpg *.jpeg *.png *.bmp')])
_root.destroy()

# 静止画の読み込みと顔ランドマーク検出(IMAGEモード,1回のみ)
src_bgr = cv2.imread(SOURCE_PATH)
src_h, src_w = src_bgr.shape[:2]
fl_image = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=MODEL_PATH, delegate=DELEGATE),
    running_mode=vision.RunningMode.IMAGE))
src_mp = mp.Image(image_format=mp.ImageFormat.SRGB,
                  data=cv2.cvtColor(src_bgr, cv2.COLOR_BGR2RGB))
src_result = fl_image.detect(src_mp)

# 顔メッシュ用は先頭468点(虹彩の10点を除く)
N = 468
src_points = np.array([(lm.x * src_w, lm.y * src_h)
                       for lm in src_result.face_landmarks[0][:N]], dtype=np.float32)

# Delaunay三角形分割:ランドマーク番号の三つ組として記録
subdiv = cv2.Subdiv2D((0, 0, src_w, src_h))
for p in src_points:
    subdiv.insert((float(p[0]), float(p[1])))
triangles = []
for tr in subdiv.getTriangleList():
    verts = [(tr[0], tr[1]), (tr[2], tr[3]), (tr[4], tr[5])]
    idx = []
    for v in verts:
        d = np.linalg.norm(src_points - v, axis=1)
        i = int(np.argmin(d))
        if d[i] < 1.0:
            idx.append(i)
    if len(idx) == 3 and len(set(idx)) == 3:
        triangles.append(idx)

# 静止画三角形の符号付き面積(三角形法線のZ成分に等しい)を事前計算
def signed_area(pts):
    a, b, c = pts
    return (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0])
src_signs = [np.sign(signed_area(src_points[t])) for t in triangles]

# 静止画ランドマークの境界矩形(整列基準)
src_bbox_min = src_points.min(axis=0)
src_bbox_max = src_points.max(axis=0)
src_center = (src_bbox_min + src_bbox_max) / 2
src_size = src_bbox_max - src_bbox_min

# ウェブカメラ用 Face Landmarker (VIDEOモード,ブレンドシェイプと頭部姿勢行列を出力)
fl_video = vision.FaceLandmarker.create_from_options(vision.FaceLandmarkerOptions(
    base_options=python.BaseOptions(model_asset_path=MODEL_PATH, delegate=DELEGATE),
    running_mode=vision.RunningMode.VIDEO,
    output_face_blendshapes=True,
    output_facial_transformation_matrixes=True))

# ウェブカメラ解像度を1280x720に要求(非対応カメラは実装値で動作)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

# 指数移動平均パラメータ(α=0.5.大きいほど平滑強).※単一顔前提
SMOOTH = 0.5
prev_points = None

t = 0  # ミリ秒単位のタイムスタンプ
while True:
    ok, frame = cap.read()
    if not ok:
        break
    img = mp.Image(image_format=mp.ImageFormat.SRGB,
                   data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    result = fl_video.detect_for_video(img, t)
    t += 33  # 約30FPS相当

    avatar = src_bgr.copy()
    for i, face in enumerate(result.face_landmarks):
        user_points = np.array([(lm.x * W, lm.y * H)
                                for lm in face[:N]], dtype=np.float32)
        # 指数移動平均(α=0.5)で時間的平滑化
        if prev_points is None:
            prev_points = user_points
        user_points = (1 - SMOOTH) * user_points + SMOOTH * prev_points
        prev_points = user_points

        # ユーザ顔の境界矩形を静止画顔の境界矩形に整列(平行移動 + 均一スケール)
        user_bbox_min = user_points.min(axis=0)
        user_bbox_max = user_points.max(axis=0)
        user_center = (user_bbox_min + user_bbox_max) / 2
        user_size = user_bbox_max - user_bbox_min
        scale = float(np.mean(src_size / user_size))
        mapped = (user_points - user_center) * scale + src_center

        # 三角形ごとにアフィン変換,Z座標による裏面カリング適用
        warped = np.zeros_like(src_bgr)
        for tri, src_sign in zip(triangles, src_signs):
            # 裏面カリング:符号反転した三角形は描画しない
            if np.sign(signed_area(mapped[tri])) != src_sign:
                continue
            s = src_points[tri]
            d = mapped[tri]
            r1 = cv2.boundingRect(s)
            r2 = cv2.boundingRect(d)
            s_local = s - np.array([r1[0], r1[1]], dtype=np.float32)
            d_local = d - np.array([r2[0], r2[1]], dtype=np.float32)
            crop = src_bgr[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
            M = cv2.getAffineTransform(s_local, d_local)
            w_tri = cv2.warpAffine(crop, M, (r2[2], r2[3]),
                                   None, flags=cv2.INTER_LINEAR,
                                   borderMode=cv2.BORDER_REFLECT_101)
            mask = np.zeros((r2[3], r2[2]), dtype=np.uint8)
            cv2.fillConvexPoly(mask, np.int32(d_local), 255)
            roi = warped[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]]
            np.copyto(roi, w_tri, where=mask[:, :, None].astype(bool))

        # 凸包マスクへのGaussian Blur(カーネル21×21)で羽根化,seamlessCloneで合成
        hull = cv2.convexHull(mapped.astype(np.int32))
        face_mask = np.zeros((src_h, src_w), dtype=np.uint8)
        cv2.fillConvexPoly(face_mask, hull, 255)
        face_mask = cv2.GaussianBlur(face_mask, (21, 21), 0)
        Mm = cv2.moments(face_mask)
        cx = int(Mm['m10'] / Mm['m00'])
        cy = int(Mm['m01'] / Mm['m00'])
        avatar = cv2.seamlessClone(warped, src_bgr, face_mask,
                                    (cx, cy), cv2.NORMAL_CLONE)

        # ブレンドシェイプ係数(上位5項目)を画面に重畳
        top = sorted(result.face_blendshapes[i],
                     key=lambda c: c.score, reverse=True)[:5]
        for j, c in enumerate(top):
            cv2.putText(avatar, f'{c.category_name}: {c.score:.2f}',
                        (10, 30 + j * 25),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 1)

        # 頭部姿勢行列を標準出力に出力
        print('head_pose_matrix:', result.facial_transformation_matrixes[i])

    cv2.imshow('expression transfer', avatar)
    if cv2.waitKey(1) == 27:  # ESCキーによる終了処理
        break

cap.release()
cv2.destroyAllWindows()

演習7.表情・頭部姿勢転写

手順

  1. コマンドプロンプトを管理者として実行し,pip install --no-user mediapipe opencv-python numpy を実行する(仮想環境利用時は管理者権限および --no-user は不要)
  2. ウェブカメラを接続してスクリプトを実行する
  3. 起動時のダイアログで別人物の正面顔画像(jpg/jpeg/png/bmp)を選択する
  4. 口の開閉,瞬き,左右の口角を上げる動作を順に行い,画面のブレンドシェイプ係数の上位5項目の変化を確認する
  5. 左右への首振り,上下のうなずき,左右の首傾げを順に行い,標準出力の頭部姿勢行列の変化を確認する
  6. ESCキーで終了する

ヒント

考察ポイント