Colab を用いたAI実行体験:MediaPipe による15種類のAIタスク実行(カメラ入力対応)

【概要】

  1. MediaPipe Tasks API(Googleが提供する事前学習済みAIタスクを数行のコードで呼び出せるライブラリ)を用いて,物体検出,画像セグメンテーション(画像を領域ごとに分割する処理),姿勢・手・顔・ジェスチャー認識,音声分類,テキスト処理等の15種類のAIタスクを実行する
  2. Pythonプログラムは,処理対象ファイル(画像・音声)の自動ダウンロード,モデル自動ダウンロード,AIによる推論,結果表示の手順で統一し,理解と改変を容易にする。映像を扱うタスクでは,カメラ入力によるリアルタイム処理を行う
  3. Google Colab 上での実行に対応する。繰り返し検証等への発展に適する
  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 の用途例

【サイト内の関連ページ】

プログラム実行手順

本章では,各タスクのプログラムを Colab で実行する手順を示す.Colab は,ブラウザ上で Python を実行できる Google のサービスである(詳細はGoogle Colaboratoryとはを参照).

Colab でのプログラム実行

  1. 新しいセルを追加し,本資料の該当タスクのコードを貼り付けて実行する
  2. 静止画を処理するタスク(タスク9から12,タスク14,タスク15)では,/content 直下にダウンロードされたファイルの一覧が番号付きで表示されるので,処理対象ファイルの番号を入力して選択する。テキストを処理するタスク(タスク11,タスク12)では,input() による標準入力で文字列を入力する
  3. カメラ入力を用いるタスク(タスク1から8,タスク13)では,初回実行時にブラウザがカメラの使用許可を求めるので,これを許可する。カメラ映像に推論結果を重ねた画像がセルの直下に連続表示される。処理を終了するには,セルの停止ボタンを押す
  4. 結果はセルの直下に表示される(画像はインライン表示,テキスト出力は print 関数の出力としてセル出力欄に表示される)

Pythonプログラムの共通構造

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

  1. 処理対象ファイル(画像・音声)の自動ダウンロード(存在チェックを含む).保存先は /content 直下である。これらのファイルを使用できる
  2. 学習済みモデル(AIモデルファイル)の自動ダウンロード(存在チェックを含む).保存先は /content 直下である
  3. 入力:映像を扱うタスクでは,ブラウザのカメラから取得した映像フレームを入力とする。静止画を扱うタスクでは,/content 直下のファイルを番号入力で選択する。テキスト系タスクでは input() による標準入力を用いる
  4. 結果表示:映像を扱うタスクでは,推論結果を重ねた画像をブラウザに連続表示する。静止画を扱うタスクおよびテキストを扱うタスクでは print による出力を用い,3D可視化タスク(タスク14,タスク15)では matplotlib による表示を用いる

処理対象ファイルおよびモデルは,初回実行時にコード内で自動ダウンロードする.静止画を扱うタスクでは,番号入力により,ダウンロード済みファイル以外の任意の /content 直下のファイルも処理対象とできる.なお,MediaPipe 等のライブラリ導入についてはパッケージのインストールを参照のこと.

カメラ入力を用いるタスクでは,各スクリプトの冒頭でブラウザのカメラを起動するJavaScriptを実行し,フレームを1枚ずつ取得して推論・描画し,結果をブラウザに表示する処理を繰り返す。

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

  1. 初回実行時はインターネット接続が必要である(モデルおよびサンプル画像のダウンロードのため)
  2. 初回実行時は処理開始までに数十秒から数分程度かかる場合がある(モデルおよびサンプル画像のダウンロードのため)
  3. 学習済みモデルおよび処理対象ファイルの保存先は /content 直下である(2回目以降の実行ではダウンロードを省略する)
  4. カメラ入力を用いるタスクでは,初回実行時にブラウザがカメラの使用許可を求めるので,これを許可する
  5. カメラ入力を用いるタスクは while True による連続処理であり,処理を終了するにはセルの停止ボタンを押す
  6. タスク14およびタスク15ではmatplotlibを使用する(Colab には標準でインストール済みである)
  7. Colab のセッションが切れると /content 配下のファイルは消去されるため,再実行時には再度ダウンロードが行われる(セッション切断のリスクを参照。保存が必要な場合はGoogle Driveへの定期保存を参照)

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

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

# task_object_detector
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/object_detector/efficientdet_lite2/float16/latest/efficientdet_lite2.tflite"
model = Path("/content") / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 推論器の作成
options = vision.ObjectDetectorOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)),
    score_threshold=0.5)
detector = vision.ObjectDetector.create_from_options(options)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = detector.detect(mp_image)
    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)
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

概要:画像全体に対して単一の分類ラベルを推定する.EfficientNet-Lite2(EfficientNetアーキテクチャによる画像分類モデル)をベースモデルとし,ImageNet(約120万枚・1000クラスの画像分類用大規模データセット)の1000クラスに対応する.画像検索の自動タグ付け,コンテンツモデレーション(不適切コンテンツの選別),アルバム整理等に利用される.本タスクでは,カメラ映像の各フレームに対して推論を行い,上位の分類結果を重ねて連続表示する.

# task_image_classifier
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/image_classifier/efficientnet_lite2/float32/latest/efficientnet_lite2.tflite"
model = Path("/content") / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 推論器の作成
options = vision.ImageClassifierOptions(
    base_options=python.BaseOptions(model_asset_path=str(model)), max_results=5)
classifier = vision.ImageClassifier.create_from_options(options)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = classifier.classify(mp_image)
    y_offset = 30
    for cat in result.classifications[0].categories:
        cv2.putText(img, f"{cat.category_name}: {cat.score:.2f}",
                    (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
        y_offset += 25
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

概要:画像を画素単位で領域分割する(各画素がどのクラスに属するかを判定する処理).DeepLab V3(Googleが開発したセマンティックセグメンテーション用モデル)をベースモデルとし,Pascal VOC 2012(物体認識評価のための代表的データセット)の21クラス(背景,人,車,犬,猫,自転車,椅子,植木鉢等)に対応する.背景除去,写真合成,自動運転の領域認識等に利用される.本タスクでは,カメラ映像の各フレームに対してセグメンテーションを行い,領域を色分けして重ねて連続表示する.

# task_image_segmenter
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/image_segmenter/deeplab_v3/float32/latest/deeplab_v3.tflite"
model = Path("/content") / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 推論器の作成(カテゴリマスクと確信度マスクの両方を出力)
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)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
confidence_threshold = 0.5
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = segmenter.segment(mp_image)
    mask = result.category_mask.numpy_view()
    # 各クラスの確信度マスクから低確信度領域を背景化
    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(img, (colored.shape[1], colored.shape[0]))
    overlay = cv2.addWeighted(img, 0.5, colored, 0.5, 0)
    _, buf = cv2.imencode('.jpg', overlay)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

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

# task_pose_landmarker
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task"
model = Path("/content") / 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")]

# 推論器の作成
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)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = landmarker.detect(mp_image)
    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)
        # 代表点の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)
            y_offset += 22
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

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

# task_hand_landmarker
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task"
model = Path("/content") / 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")]

# 推論器の作成
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)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = landmarker.detect(mp_image)
    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
        # 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)
            y_offset += 22
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

概要:Hand Landmarkerを基盤として手の形状を分類するタスクである.標準7種類のジェスチャー(Closed_Fist:グー,Open_Palm:パー,Pointing_Up:指差し,Thumb_Down:サムズダウン,Thumb_Up:サムズアップ,Victory:ピース,ILoveYou:アイラブユー)に対応する.非接触UI(手を触れずに操作するインタフェース),プレゼンテーション制御,アクセシビリティ用途(身体的制約のあるユーザの操作支援)等に利用される.本タスクでは,カメラ映像の各フレームに対して認識を行い,結果を重ねて連続表示する.

# task_gesture_recognizer
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from mediapipe.tasks.python.components import processors
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task"
model = Path("/content") / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 推論器の作成
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)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = recognizer.recognize(mp_image)
    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)
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

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

# task_face_landmarker
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task"
model = Path("/content") / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 推論器の作成
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)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = landmarker.detect(mp_image)
    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:
        y_offset = 25
        for bs in sorted(result.face_blendshapes[0], key=lambda x: x.score, reverse=True)[:5]:
            text = f"{bs.category_name}: {bs.score:.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)
            y_offset += 22
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

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

# task_face_detector
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_full_range/float16/latest/blaze_face_full_range.tflite"
model = Path("/content") / Path(MODEL_URL).name
if not model.exists():
    urllib.request.urlretrieve(MODEL_URL, model)

# 推論器の作成
options = vision.FaceDetectorOptions(base_options=python.BaseOptions(model_asset_path=str(model)))
detector = vision.FaceDetector.create_from_options(options)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = detector.detect(mp_image)
    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)
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

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

画像を2枚選択するため,/content 直下のファイルの番号入力を2回行う.

# task_image_embedder
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

def select_file(title, initialdir, filetypes):
    # /content 直下のファイルを拡張子で絞り込み番号入力で選択
    exts = filetypes[0][1].lower().replace("*", "").split()
    candidates = sorted(p for p in Path(initialdir).iterdir()
                        if p.is_file() and p.suffix.lower() in exts)
    print(f"[{title}]")
    for i, p in enumerate(candidates):
        print(f"  {i}: {p.name}")
    return str(candidates[int(input(f"番号を入力 (0-{len(candidates)-1}): "))])

# 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 = Path("/content")
for url in IMG_URLS:
    p = IMG_DIR / Path(url).name
    if not p.exists():
        urllib.request.urlretrieve(url, p)

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/image_embedder/mobilenet_v3_large/float32/latest/mobilenet_v3_large.tflite"
model = Path("/content") / 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
!pip install -q -U mediapipe
import 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 select_file(title, initialdir, filetypes):
    # /content 直下のファイルを拡張子で絞り込み番号入力で選択
    exts = filetypes[0][1].lower().replace("*", "").split()
    candidates = sorted(p for p in Path(initialdir).iterdir()
                        if p.is_file() and p.suffix.lower() in exts)
    print(f"[{title}]")
    for i, p in enumerate(candidates):
        print(f"  {i}: {p.name}")
    return str(candidates[int(input(f"番号を入力 (0-{len(candidates)-1}): "))])

# 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 = Path("/content")
wav_path = WAV_DIR / Path(WAV_URL).name
if not wav_path.exists():
    urllib.request.urlretrieve(WAV_URL, wav_path)

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/audio_classifier/yamnet/float32/latest/yamnet.tflite"
model = Path("/content") / 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投稿のセンチメント解析(文章から書き手の感情の肯定・否定度を推定する分析),カスタマーサポートのトリアージ(問い合わせの重要度・種別による振り分け)等に利用される.

テキスト入力には標準入力(input())を用いる.本コードでは入力前処理(前後の空白除去,連続空白の単一化)を行う.

# task_text_classifier
!pip install -q -U mediapipe
import re, urllib.request
from pathlib import Path
from mediapipe.tasks import python
from mediapipe.tasks.python import text

def input_text(title, prompt, initialvalue=""):
    print(f"[{title}]")
    s = input(f"{prompt} (既定値: {initialvalue}): ")
    return s if s else initialvalue

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/text_classifier/bert_classifier/float32/latest/bert_classifier.tflite"
model = Path("/content") / 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
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
from mediapipe.tasks import python
from mediapipe.tasks.python import text

def input_text(title, prompt, initialvalue=""):
    print(f"[{title}]")
    s = input(f"{prompt} (既定値: {initialvalue}): ")
    return s if s else initialvalue

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/language_detector/language_detector/float32/1/language_detector.tflite"
model = Path("/content") / 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
!pip install -q -U mediapipe
import urllib.request
from pathlib import Path
import numpy as np, cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python.vision.holistic_landmarker import (
    HolisticLandmarker, HolisticLandmarkerOptions)
from IPython.display import display, Javascript, Image, update_display
from google.colab.output import eval_js
from base64 import b64decode

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

# モデルダウンロード(/content 直下に保存)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/holistic_landmarker/holistic_landmarker/float16/latest/holistic_landmarker.task"
model = Path("/content") / 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)]

# 推論器の作成
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)

# カメラ起動(getUserMedia)
display(Javascript('''
async function startCamera() {
  try {
    window._video = document.createElement('video');
    window._video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
    await window._video.play();
  } catch (e) {
    window._video = null;
  }
}

function takeFrame() {
  if (!window._video || !window._video.videoWidth || !window._video.videoHeight) return null;
  const c = document.createElement('canvas');
  c.width = window._video.videoWidth;
  c.height = window._video.videoHeight;
  c.getContext('2d').drawImage(window._video, 0, 0);
  return c.toDataURL('image/jpeg', 0.8);
}
'''))
eval_js('startCamera()')

# カメラループ
display(Image(data=b''), display_id='cam')
while True:
    data = eval_js('takeFrame()')
    if data is None:
        continue
    img = cv2.imdecode(np.frombuffer(b64decode(data.split(',')[1]), np.uint8), cv2.IMREAD_COLOR)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    result = landmarker.detect(mp_image)
    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)
    # ポーズランドマーク(タスク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)
    # 両手ランドマーク(タスク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)
    _, buf = cv2.imencode('.jpg', img)
    update_display(Image(data=buf.tobytes()), display_id='cam')

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

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

タスク5との関係:

# task_hand_landmarker_3d
!pip install -q -U mediapipe
import 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 select_file(title, initialdir, filetypes):
    # /content 直下のファイルを拡張子で絞り込み番号入力で選択
    exts = filetypes[0][1].lower().replace("*", "").split()
    candidates = sorted(p for p in Path(initialdir).iterdir()
                        if p.is_file() and p.suffix.lower() in exts)
    print(f"[{title}]")
    for i, p in enumerate(candidates):
        print(f"  {i}: {p.name}")
    return str(candidates[int(input(f"番号を入力 (0-{len(candidates)-1}): "))])

# 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 = Path("/content")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(/content 直下に保存.タスク5と同一モデル)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task"
model = Path("/content") / 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
!pip install -q -U mediapipe
import 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 select_file(title, initialdir, filetypes):
    # /content 直下のファイルを拡張子で絞り込み番号入力で選択
    exts = filetypes[0][1].lower().replace("*", "").split()
    candidates = sorted(p for p in Path(initialdir).iterdir()
                        if p.is_file() and p.suffix.lower() in exts)
    print(f"[{title}]")
    for i, p in enumerate(candidates):
        print(f"  {i}: {p.name}")
    return str(candidates[int(input(f"番号を入力 (0-{len(candidates)-1}): "))])

# 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 = Path("/content")
img_path = IMG_DIR / Path(IMG_URL).name
if not img_path.exists():
    urllib.request.urlretrieve(IMG_URL, img_path)

# モデルダウンロード(/content 直下に保存.タスク4と同一モデル)
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task"
model = Path("/content") / 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()