猫の手ツール

MediaPipe 入門 · 01

MediaPipeとは?
全体像と最新動向から
Web顔検出を始めよう

「AIで顔の位置を探す」「人物と背景を分ける」。猫の手ツールのAI画像ツールにも使っているMediaPipeを、最初の一歩から説明します。全体像と、このサイトの構成・最新の更新を確認した後、実際のコードで人物画像に顔の枠を描いてみましょう。

この記事を読み終えたときにできること

  • 「タスク」「モデル」「推論」の違いを説明できる。
  • やりたい画像処理に合うMediaPipeのタスクを選べる。
  • このサイトが使う版と最新版、SDKとモデルの更新を区別できる。
  • サンプルを自分のPCで起動し、顔検出の結果を確認できる。
  • 「猫が検出されない」理由を、プログラムの不具合と区別できる。

HTMLのタグと、JavaScriptの変数・関数・イベントの基本を知っていれば進められます。機械学習の数式や、モデルを自分で学習させる知識は、この例では必要ありません。

MediaPipe入門 全20記事の一覧と、目的別の読む順番から、続きの記事を選べます。

1. MediaPipeの全体像

MediaPipeは、Googleが提供するオープンソースのAI・機械学習関連のライブラリやツールです。画像に含まれる顔や手を探したり、音声やテキストを分類したりする機能を、自分のアプリへ組み込めます。ここでは、用途ごとのAPIと学習済みモデルを使うMediaPipe Tasksを中心に学びます。全体の構成は公式のMediaPipe Solutionsガイドにまとめられています。

名前に「AI」が付いていても、顔検出は画像を生成する処理ではありません。入力画像を調べて、「この範囲に顔がありそう」という数値データを返します。その情報を使って枠を描いたり、モザイクをかけたりする部分は、アプリ側で作ります。

Web専用の技術ではない

同じMediaPipeでも、実装する環境は異なる
環境使う場面このサイトとの関係
WebJavaScriptでブラウザ上のアプリを作る。PC・スマホのブラウザで使う猫の手ツールはこの形。
AndroidAndroidのネイティブアプリへ組み込む。Android端末でWebサイトを開く場合とは実装方法が違う。
iOSiPhone・iPadのネイティブアプリへ組み込む。iPhoneのSafariでこのサイトを開く場合はWeb版を使う。
PythonPythonのプログラムで画像などを処理する。学習・実験や処理の自動化に使える。この記事のコードはJavaScript。

すべての機能が、すべての環境で使えるとは限りません。使いたいタスクを選び、公式ガイドの対応表と対象プラットフォームを確認します。以降の実装はWeb版・静止画の顔検出に絞ります。

Tasks以外の名前は、今は位置づけだけでよい

  • Models:各タスクで利用する学習済みモデル。
  • Model Maker:対応するタスクで、自分のデータを使ってモデルをカスタマイズするためのツール。
  • Studio:ブラウザで対応するソリューションを試したり、評価したりするためのツール。
  • Framework:処理を組み合わせる仕組みを、より低い層から構築するためのフレームワーク。

最初は「TasksのAPIから、公開済みモデルを使う」という道筋で十分です。古い記事に出てくる@mediapipe/face_detectionと、この記事の@mediapipe/tasks-visionはAPIが異なります。コードを混ぜず、どちらを説明しているか確認しましょう。旧Solutionsと新しいTasksの関係は公式の旧Solutions案内を参照してください。

2. 「タスク」「モデル」「推論」を分けて考える

タスクは「何をするか」、モデルは「その処理に使う学習済みデータ」、推論は「モデルを使って入力の結果を求めること」です。今回なら、タスクはFaceDetector、モデルはBlazeFace full-range、推論は「この人物画像の顔を検出する」という実行です。

Webの顔検出で準備する3種類のファイル
種類役割今回の例
JavaScript SDKプログラムからタスクを呼び出すためのAPI。@mediapipe/tasks-visionのJavaScriptファイル。
WASM関連ファイルブラウザで推論処理を実行するための実行コード。WASMはWebAssemblyの略。SDKと同じバージョンのwasmフォルダにあるファイル。
モデル検出に使う学習済みデータ。SDKだけでは顔検出できない。BlazeFace full-rangeの.tfliteファイル。

モデルのファイル形式はタスクによって異なり、.taskのモデルバンドルを使う場合もあります。ファイルの拡張子だけで判断せず、使うタスクに対応したモデルを公式ページから選ぶことが大切です。

目的からタスクを選ぶ

まず欲しい「出力」を決める
やりたいこと候補となるタスク主に受け取るもの
人の顔を四角で囲むFaceDetector顔の矩形と少数の特徴点。
唇や目の輪郭に合わせて加工FaceLandmarker顔の細かな点の座標。
手や身体の姿勢を調べるHandLandmarker / PoseLandmarker手・身体の点の座標。
顔・手・身体をまとめて調べるHolisticLandmarker顔・手・身体を組み合わせた点の座標。
手のジェスチャーを判定GestureRecognizerジェスチャーの分類結果と手の座標。
人物と背景、髪などを分けるImageSegmenterどの画素がどの領域かを表すマスク。分けられる対象はモデル次第。
指定した場所の対象を切り抜くInteractiveSegmenter指定位置などを手掛かりにした領域マスク。
猫などの物体の場所を探すObjectDetectorカテゴリ名、スコア、矩形。猫に対応するモデルが必要。
画像全体が何かを分類ImageClassifier画像のカテゴリとスコア。画像内の位置は返さない。
似た画像や文章を比べるImageEmbedder / TextEmbedder画像・文章の特徴を表す数値の配列。
音や文章を扱うAudioClassifier / TextClassifier / LanguageDetectorなど音・文章の分類、言語など。画像とは別のSDKや入力を使う。

この一覧は選び始めるための目安です。モデルが学習していない対象を、タスク名だけで認識できるわけではありません。対応タスクは公式の対応一覧、猫などの検出はObjectDetectorの公式ガイドで確認できます。

3. 現在のサイトの構成と、最新のMediaPipe

「このサイトで使っている版」と「今、新しく公開されている版」は分けて確認します。ここでのSDKバージョンは、Webの画像処理向けnpmパッケージ@mediapipe/tasks-visionのものです。Pythonなど、すべての言語のパッケージが同じ番号になるという意味ではありません。

2026年10月12日時点で確認したバージョン
対象Web SDK読み込み方・確認方法
現在の猫の手ツール0.10.34SDK・WASMをサイト内から配信。package-lock.jsonと配布SDKを照合して確認。
この記事の動作サンプル0.10.34CDNのURLを固定。掲載コードとキャプチャの再現用に同じ版を使う。
最新の安定版Web SDK1.1.0npmのdist-tags.latestを確認。npm公開は2026年10月7日(日本時間)。

最新バージョンは公式npmレジストリのパッケージ情報(JSON・英語)、リリース内容は公式GitHubのMediaPipe v1.1.0リリースノート(英語)が一次情報です。サンプルは@latestを使わず、記事の途中で動作条件が変わらないようにしています。

現在のサイトは、SDK・WASM・モデルを自サイトから配信する

既存のAI画像ツールは/lib/tasks-vision/vision_bundle.jsと/lib/tasks-vision/wasmを読み込み、タスクに応じたモデルをサイト内から取得します。モデルの例は次のとおりです。

既存のAI画像ツールで参照している6種類のモデル
用途モデルファイルサイトの例
顔の検出blaze_face_full_range.tflite顔モザイク、顔の切り出し。
顔の細かな座標face_landmarker.taskリップ試着、カラコン。
手の座標hand_landmarker.task手のポーズ線画、手形スタンプ。
人物と背景の分離selfie_segmenter.tflite人物の背景透過、背景交換。
髪の領域hair_segmenter.tfliteヘアカラー。
人物の部位別領域selfie_multiclass_256x256.tflite衣服の色変更。

モデルは主に/models/、顔のランドマーク用は/lib/tasks-vision/models/から読み込みます。ファイルをサイト内で配信する方式も、CDNから配信する今回の方式も、推論はブラウザ内で行います。違いは必要なプログラムやモデルを、どのURLから取得するかです。

最新リリースでは、Webの検索・埋め込み機能も広がっている

v1.1.0のリリースノートには、Web向けの主な変更として、次が記載されています。ここで挙げるのはMediaPipe全体のJavaScript関連の更新であり、すべてが顔検出の設定になるわけではありません。

  • UniversalEmbedder / SemanticRetriever:埋め込みや意味に基づく検索を扱うWeb APIの追加。画像の枠を探すFaceDetectorとは目的が異なる。
  • ランドマークごとのpresenceスコア:各点の存在に関するスコアをWeb Tasks APIで取得できるようにする変更。
  • Web Tasksの統計ログ:実行状態を調べるための統計ログの追加。

この一覧から「1.1.0なら顔を必ずより正確に検出できる」「スマホで必ず速くなる」とは判断できません。使うタスク、モデル、入力画像、端末を揃えて確認します。この記事の実行キャプチャは0.10.34の結果です。

SDKの更新と、モデルの更新は別に確認する

SDKの版を変えることと、モデルのファイルを変えることは別の操作です。例えば、SDKを1.1.0にしても、MODEL_URLが同じなら指定したモデルは変わりません。逆に、SDKを据え置いたまま対応モデルを交換する場合もあります。

最新版を試すときは、まず配布サンプルをコピーした別のフォルダで、SDK_VERSIONだけを変更します。SDKとWASMは同じ変数からURLを作るため、同時に揃います。人物・猫の入力と検出結果を比較し、GPUとCPU、PCと実際のスマホでも確認してから、自分のアプリへ取り込みます。

生成AI関連の機能もありますが、今回の画像検出とはSDKや必要な環境が異なります。確認日時点では、MediaPipe LLM Inference APIは保守モードで、公式はWebプロジェクトのLiteRT-LMへの移行を案内しています。生成AIを始める際は公式の更新案内を確認してください。

4. まず完成サンプルを動かす

サンプルは、プロジェクト内のAI生成の人物画像と運営者の愛猫2匹の写真を使います。人物は既存の顔モザイク用作例public/images/tool-examples/ai-face-mosaic/before.jpg、猫はpublic/images/hero-cats.jpgが元画像です。人物で成功例を確認し、猫ではモデルの対象が異なるとどうなるかを観察します。

顔検出の動作デモを開く 自分のPCで動かす手順へ
  1. デモを開き、「AI作成の人物」を選びます。最初は人物が選択されています。
  2. 「AIを読み込んで顔検出」を押し、モデルの準備が終わるまで待ちます。
  3. 画像の上に描かれた枠と、検出件数・結果のJSONを確認します。
  4. 「愛猫の写真」に切り替え、「選んだ画像で顔検出」を押します。人物の結果と比較してください。

最初の実行では、SDK・WASM・モデルを読み込む時間が含まれます。同じページで2回目を実行したときとは、待ち時間の意味が異なります。初回の待ち時間全体を「1枚の推論にかかった時間」として比較しないようにしましょう。

推論はブラウザ内で行います。「自分の画像でも試す」で選んだ画像を、サンプルのコードから外部サーバーへ送信する処理はありません。一方で、SDKやモデルの取得には通信を使うため、初回から完全にオフラインで動くサンプルではありません。

AI生成の6人の集合画像を顔検出デモで実行し、6個の顔の枠と検出結果を表示した画面
人物画像で実際にサンプルを動かした画面。Windows上のChromeでGPU実行し、6個の顔を検出しました。元画像1024×559pxを処理用に960×524pxへ縮小しています。画像を押すと原寸で確認できます。

5. 自分のPCで起動する

まず、動作確認に使ったサンプル一式を使って起動します。その後でコードを変更すると、「環境が原因なのか、変更したコードが原因なのか」を切り分けやすくなります。

手順1:必要なものを確認する

  • ブラウザ:PCのChromeなど。最初は開発者ツールでエラーを確認しやすいPCで進めます。
  • エディタ:VS Codeなど、HTML・JavaScriptを編集できるもの。
  • Node.jsとnpm:ローカルHTTPサーバーを起動するために使います。顔検出はNode.jsではなくブラウザで実行します。
  • インターネット接続:必要なソフトの入手、初回のnpx、SDK・WASM・モデルの取得に使います。

Node.jsが入っているか分からない場合は、ターミナルで次を1行ずつ実行します。これはWindowsのPowerShell向けの表記です。バージョン番号が返れば準備できています。コマンドが見つからない場合は、Node.js公式サイトからLTS版を導入し、ターミナルを開き直してください。

node --version
npm.cmd --version

手順2:サンプル一式を保存・展開する

サンプルにはHTML・JavaScript・人物と猫の画像を含めます。SDK・WASM・モデルは初回の実行時にインターネットから取得するため、実行には通信できる環境が必要です。次の構成で、フォルダ全体を同じ場所に置いてください。

mediapipe-face-detection/
├─ index.html          … 画面と操作ボタン
├─ app.js              … モデルの準備・顔検出・描画
├─ person.jpg          … AI生成の人物サンプル
├─ cat.jpg             … 運営者の愛猫の写真
└─ README.txt          … 起動方法と使用ファイルの説明
サンプル一式をダウンロード

ZIPを展開した後、index.htmlとapp.jsが見えるフォルダをVS Codeで開きます。「ターミナル」→「新しいターミナル」で開いた場所が、そのフォルダになっていることを確認してください。ZIPを開いたまま実行せず、先に展開します。

手順3:HTTPサーバーを起動する

npx.cmd --yes http-server . -p 8080

npxは必要なコマンドを実行する仕組み、http-serverは静的ファイルを配信する小さなサーバーです。.は今いるフォルダ、-p 8080は8080番ポートを使う指定です。このサーバーがAI処理をするわけではありません。

Windowsではnpx.cmdを使うと、PowerShellのスクリプト実行ポリシーに依存せず実行できます。macOS・Linuxでは、npm.cmdをnpm、npx.cmdをnpxに置き換えてください。

初回はコマンドの取得に時間がかかることがあります。ターミナルにサーバーのURLが出たら、そのまま起動した状態にして、ブラウザでhttp://localhost:8080/を開きます。localhostは自分のPCを指します。

ここまでの成功条件:デモの画面と人物画像が表示され、実行ボタンを押すと検出結果が表示されること。終了するときは、サーバーのターミナルでCtrl + Cを押します。

index.htmlをダブルクリックして開く方法は使いません。アドレスがfile://だと、JavaScriptモジュールや関連ファイルの読み込みに制約があります。MDNのモジュール解説でも、ローカルサーバーを使って確認する方法が案内されています。

手順4:自分でファイルを作る場合

下の完全コードを見ながら、空のフォルダにindex.htmlとapp.jsを作っても構いません。その場合も、ダウンロードした一式からperson.jpgとcat.jpgをコピーしてください。ファイル名を変えず、手順3のHTTPサーバーで開きます。index.html.txtのような別の拡張子になっていないかも確認します。

今回の導入はCDN方式なので、MediaPipe用のnpm installやビルドツールは不要です。SDKとWASMは0.10.34へ固定し、モデルもURLのバージョン1を指定します。別の導入方法へ進むときは公式のWeb導入ガイドを確認し、SDKとWASMのバージョンを揃えましょう。

6. 完全なサンプルコード

動作デモのソースを、そのまま掲載します。先に起動を確認できていれば、すべてを一度に理解する必要はありません。次の節で「準備 → 入力 → 推論 → 描画」の順に読む場所を説明します。

index.htmlを保存 app.jsを保存
index.htmlの完全コードを開く
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex,follow" />
    <title>顔検出を一枚の画像で試す | MediaPipe入門サンプル</title>
    <style>
      :root { color-scheme: light; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #253235; background: #f3f7f6; line-height: 1.8; }
      * { box-sizing: border-box; }
      body { margin: 0; }
      main { width: min(100% - 32px, 1080px); margin: 32px auto 56px; }
      h1 { font-size: clamp(1.45rem, 4vw, 2.2rem); line-height: 1.5; margin: 8px 0 16px; }
      h2 { font-size: 1.15rem; margin: 0 0 12px; }
      p { margin: 8px 0; }
      a { color: #006453; text-underline-offset: 3px; }
      .eyebrow { color: #006453; font-size: .85rem; font-weight: 700; letter-spacing: .07em; }
      .intro { max-width: 780px; }
      .panel { margin-top: 24px; padding: 24px; border: 1px solid #d2e1dc; border-radius: 16px; background: white; }
      fieldset { border: 0; padding: 0; margin: 0 0 18px; }
      legend { padding: 0; margin-bottom: 8px; font-weight: 700; }
      .choices { display: flex; flex-wrap: wrap; gap: 12px; }
      .choice { display: flex; align-items: center; gap: 8px; border: 1px solid #bdcec7; border-radius: 8px; padding: 10px 16px; cursor: pointer; }
      .choice:has(input:checked) { background: #e8f7ee; border-color: #00715b; }
      input[type="radio"] { width: 18px; height: 18px; accent-color: #00715b; }
      input[type="file"] { display: block; max-width: 100%; margin-top: 8px; font: inherit; font-size: .9rem; }
      label[for="image-file"] { font-weight: 700; }
      button { font: inherit; padding: 12px 22px; border-radius: 9px; border: 0; background: #006b55; color: white; font-weight: 700; cursor: pointer; min-height: 48px; }
      button:disabled { opacity: .5; cursor: wait; }
      :focus-visible { outline: 3px solid #e3aa28; outline-offset: 3px; }
      .actions { margin-top: 20px; }
      .note { font-size: .88rem; color: #566561; }
      #status { min-height: 1.8em; margin-top: 16px; font-weight: 600; overflow-wrap: anywhere; }
      #status[data-error="true"] { color: #a42b24; }
      .images { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
      figure { margin: 0; min-width: 0; }
      figcaption { font-weight: 700; margin-bottom: 8px; }
      .canvas-wrap { display: grid; place-items: center; min-height: 180px; overflow: hidden; border: 1px solid #d2e1dc; border-radius: 10px; background: #f0f3f1; }
      canvas { display: block; width: 100%; height: auto; }
      dl { display: flex; flex-wrap: wrap; gap: 12px 36px; margin: 20px 0 0; }
      dl > div { min-width: 130px; }
      dt { font-size: .85rem; color: #566561; }
      dd { margin: 0; font-weight: 700; }
      pre { margin: 0; padding: 18px; background: #f0f4f2; border: 1px solid #d2e1dc; border-radius: 8px; max-height: 420px; overflow: auto; font-size: .85rem; line-height: 1.7; }
      details summary { cursor: pointer; font-weight: 700; padding: 4px 0; }
      details pre { margin-top: 12px; }
      footer { font-size: .85rem; color: #566561; margin-top: 24px; }
      @media (max-width: 640px) { main { margin-top: 20px; } .panel { padding: 18px; } .images { grid-template-columns: 1fr; } button { width: 100%; } dl { gap: 12px 24px; } }
    </style>
  </head>
  <body>
    <main>
      <header class="intro">
        <p class="eyebrow">MEDIAPIPE · JAVASCRIPT · IMAGE</p>
        <h1>顔検出を一枚の画像で試す</h1>
        <p>画像を選んでボタンを押すと、人の顔を検出して枠を描きます。AIが返す座標とスコアも確認できます。</p>
        <p class="note">人物画像はAI作成、猫の写真は運営者の愛猫です。FaceDetectorは人の顔を対象とするため、猫での検出は保証されません。</p>
      </header>

      <section class="panel" aria-labelledby="choose-heading">
        <h2 id="choose-heading">1. 画像を選ぶ</h2>
        <fieldset id="sample-choices">
          <legend>プロジェクト内のサンプル画像</legend>
          <div class="choices">
            <label class="choice"><input type="radio" name="sample" value="person" checked /> AI作成の人物</label>
            <label class="choice"><input type="radio" name="sample" value="cat" /> 愛猫の写真</label>
          </div>
        </fieldset>
        <label for="image-file">自分の画像でも試す(任意)</label>
        <input id="image-file" type="file" accept="image/jpeg,image/png,image/webp" />
        <p class="note">JPEG・PNG・WebP、20 MBまで。選んだ画像はブラウザ内で処理します。</p>
        <div class="actions"><button id="run-button" type="button" disabled>AIを読み込んで顔検出</button></div>
        <p class="note">初回はSDK・WASM・モデルを読み込むため時間がかかります。推論時間には読み込みと描画の時間を含みません。</p>
        <p id="status" role="status" aria-live="polite" aria-atomic="true">画像を読み込んでいます。</p>
        <noscript><p>JavaScriptを有効にしてから、ページを開き直してください。</p></noscript>
      </section>

      <section class="panel" aria-labelledby="result-heading">
        <h2 id="result-heading">2. 入力と検出結果を見比べる</h2>
        <div class="images">
          <figure>
            <figcaption>入力画像</figcaption>
            <div class="canvas-wrap"><canvas id="input-canvas" width="480" height="320" role="img" aria-label="選択した入力画像"></canvas></div>
          </figure>
          <figure>
            <figcaption>検出結果(緑の枠=顔)</figcaption>
            <div class="canvas-wrap"><canvas id="result-canvas" width="480" height="320" role="img" aria-label="顔検出の実行前です"></canvas></div>
          </figure>
        </div>
        <dl aria-label="実行情報">
          <div><dt>顔の検出数</dt><dd id="count">未実行</dd></div>
          <div><dt>入力 → 推論サイズ</dt><dd id="dimensions">読み込み中</dd></div>
          <div><dt>推論時間</dt><dd id="elapsed">未実行</dd></div>
          <div><dt>実行先</dt><dd id="delegate">未初期化</dd></div>
        </dl>
        <p class="note">画像の長辺は最大960 pxに縮小してから推論します。枠の座標は、この縮小後の画像に対するピクセル値です。</p>
      </section>

      <section class="panel" aria-labelledby="data-heading">
        <h2 id="data-heading">3. AIから返ったデータを見る</h2>
        <p class="note"><code>boundingBox</code>は顔を囲む長方形、<code>categories[].score</code>は検出スコアです。表示用に小数を丸めています。</p>
        <details open>
          <summary>検出結果のJSON</summary>
          <pre id="json-output">まだ実行していません。</pre>
        </details>
      </section>
      <footer>
        <p>SDK:@mediapipe/tasks-vision 0.10.34 / モデル:BlazeFace full-range float16 / 静止画モード</p>
        <p><a href="./README.txt">起動手順・使用画像の出典を読む(README)</a></p>
      </footer>
    </main>
    <script type="module" src="./app.js"></script>
  </body>
</html>
app.jsの完全コードを開く
// 1. SDK・WASM・モデルは、それぞれ別のファイルです。
// このサンプルはnpmのインストール不要。HTTPサーバー経由で開いてください。
const SDK_VERSION = "0.10.34";
const SDK_BASE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${SDK_VERSION}`;
const MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_full_range/float16/1/blaze_face_full_range.tflite";
const MAX_EDGE = 960;
const MAX_FILE_BYTES = 20 * 1024 * 1024;
const SAMPLE_IMAGES = {
  person: { url: "./person.jpg", name: "AI作成の人物" },
  cat: { url: "./cat.jpg", name: "愛猫の写真" },
};

const runButton = document.querySelector("#run-button");
const fileInput = document.querySelector("#image-file");
const sampleInputs = [...document.querySelectorAll('input[name="sample"]')];
const status = document.querySelector("#status");
const inputCanvas = document.querySelector("#input-canvas");
const resultCanvas = document.querySelector("#result-canvas");
const inputContext = inputCanvas.getContext("2d");
const resultContext = resultCanvas.getContext("2d");
const count = document.querySelector("#count");
const dimensions = document.querySelector("#dimensions");
const elapsed = document.querySelector("#elapsed");
const delegateLabel = document.querySelector("#delegate");
const jsonOutput = document.querySelector("#json-output");

let detector = null;
let currentDelegate = "";
let selectedImage = null;
let selectionVersion = 0;
let loadingImage = false;
let running = false;

function setStatus(message, isError = false) {
  status.textContent = message;
  status.dataset.error = String(isError);
}

function updateControls() {
  runButton.disabled = loadingImage || running || !selectedImage;
  runButton.textContent = running
    ? "処理しています…"
    : detector ? "選んだ画像で顔検出" : "AIを読み込んで顔検出";
  fileInput.disabled = running;
  for (const radio of sampleInputs) radio.disabled = running;
}

// 2. 画像の読み込みを待ち、サイズを揃えてCanvasに描きます。
// image.decode()完了前にdetect()を呼ぶと、画像を推論できません。
async function selectImage(source, name) {
  const version = ++selectionVersion;
  loadingImage = true;
  selectedImage = null;
  count.textContent = "未実行";
  elapsed.textContent = "未実行";
  dimensions.textContent = "読み込み中";
  jsonOutput.textContent = "まだ実行していません。";
  resultCanvas.setAttribute("aria-label", "顔検出の実行前です");
  inputContext.clearRect(0, 0, inputCanvas.width, inputCanvas.height);
  resultContext.clearRect(0, 0, resultCanvas.width, resultCanvas.height);
  setStatus(`${name}を読み込んでいます。`);
  updateControls();

  const objectUrl = source instanceof File ? URL.createObjectURL(source) : null;
  try {
    const image = new Image();
    image.src = objectUrl ?? source;
    await image.decode();
    // 読み込み中に別の画像を選んだ場合、古い結果を表示しません。
    if (version !== selectionVersion) return;

    const scale = Math.min(1, MAX_EDGE / Math.max(image.naturalWidth, image.naturalHeight));
    const width = Math.max(1, Math.round(image.naturalWidth * scale));
    const height = Math.max(1, Math.round(image.naturalHeight * scale));
    inputCanvas.width = resultCanvas.width = width;
    inputCanvas.height = resultCanvas.height = height;
    inputContext.drawImage(image, 0, 0, width, height);
    resultContext.drawImage(inputCanvas, 0, 0);
    selectedImage = { name, width, height };
    inputCanvas.setAttribute("aria-label", `${name}の入力画像`);
    dimensions.textContent = `${image.naturalWidth} × ${image.naturalHeight} → ${width} × ${height} px`;
    setStatus(`${name}を選びました。ボタンを押して顔検出を実行してください。`);
  } catch (error) {
    if (version !== selectionVersion) return;
    console.error(error);
    dimensions.textContent = "読み込み失敗";
    setStatus("画像を読み込めませんでした。サンプルを選び直すか、JPEG・PNG・WebPの画像を選んでください。", true);
  } finally {
    if (objectUrl) URL.revokeObjectURL(objectUrl);
    if (version === selectionVersion) {
      loadingImage = false;
      updateControls();
    }
  }
}

// 3. 初回のボタン操作時にAIを初期化し、次の推論でも使い回します。
async function getDetector() {
  if (detector) return detector;
  setStatus("初回の準備中:インターネットからSDK・WASM・モデルを読み込んでいます。しばらくお待ちください。");
  const { FaceDetector, FilesetResolver } = await import(/* @vite-ignore */ `${SDK_BASE}/vision_bundle.mjs`);
  const vision = await FilesetResolver.forVisionTasks(`${SDK_BASE}/wasm`);

  const create = (delegate) => FaceDetector.createFromOptions(vision, {
    baseOptions: { modelAssetPath: MODEL_URL, delegate },
    runningMode: "IMAGE",
    minDetectionConfidence: 0.5,
  });

  try {
    detector = await create("GPU");
    currentDelegate = "GPU";
  } catch (gpuError) {
    // GPUの初期化ができない環境では、CPUで再試行します。
    console.warn("GPUを初期化できなかったためCPUを試します。", gpuError);
    setStatus("GPUを初期化できませんでした。CPUでAIの読み込みを再試行しています。");
    detector = await create("CPU");
    currentDelegate = "CPU";
  }
  delegateLabel.textContent = currentDelegate;
  return detector;
}

// 4. AIが返したピクセル座標で、入力と同じサイズのCanvasに枠を描きます。
function drawDetections(detections) {
  resultContext.clearRect(0, 0, resultCanvas.width, resultCanvas.height);
  resultContext.drawImage(inputCanvas, 0, 0);
  const fontSize = Math.max(16, Math.round(resultCanvas.width / 32));
  resultContext.font = `bold ${fontSize}px system-ui`;
  resultContext.lineWidth = Math.max(3, resultCanvas.width / 200);
  resultContext.textBaseline = "top";

  detections.forEach((detection, index) => {
    const box = detection.boundingBox;
    if (!box) return;
    resultContext.strokeStyle = "#00e68b";
    resultContext.strokeRect(box.originX, box.originY, box.width, box.height);
    const score = detection.categories[0]?.score ?? 0;
    const label = `顔 ${index + 1} · ${(score * 100).toFixed(1)}%`;
    const labelWidth = resultContext.measureText(label).width + 16;
    const labelHeight = fontSize + 12;
    const x = Math.max(0, Math.min(box.originX, resultCanvas.width - labelWidth));
    const y = Math.max(0, Math.min(box.originY - labelHeight, resultCanvas.height - labelHeight));
    resultContext.fillStyle = "#064737";
    resultContext.fillRect(x, y, labelWidth, labelHeight);
    resultContext.fillStyle = "#ffffff";
    resultContext.fillText(label, x + 8, y + 5);
  });
}

// 5. 静止画はdetect()で一度だけ推論します(動画用APIとは異なります)。
async function runDetection() {
  if (running || loadingImage || !selectedImage) return;
  const version = selectionVersion;
  running = true;
  updateControls();
  try {
    const faceDetector = await getDetector();
    if (version !== selectionVersion) return;
    setStatus(`${selectedImage.name}の顔を検出しています。`);
    // ステータスとボタンを描画してから同期的な推論を開始します。
    await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
    const start = performance.now();
    const result = faceDetector.detect(inputCanvas);
    const inferenceTime = performance.now() - start;
    drawDetections(result.detections);

    count.textContent = `${result.detections.length} 個`;
    elapsed.textContent = `${inferenceTime.toFixed(1)} ms`;
    resultCanvas.setAttribute("aria-label", `${selectedImage.name}の顔検出結果。${result.detections.length}個の顔を検出。`);
    jsonOutput.textContent = JSON.stringify(result, (_key, value) =>
      typeof value === "number" ? Number(value.toFixed(4)) : value, 2);
    setStatus(result.detections.length
      ? `${result.detections.length}個の顔を検出しました。枠・スコア・JSONを確認してください。`
      : "顔の検出は0個でした。処理は完了しています。人の顔が写る画像でも、大きさや向きなどによって検出されないことがあります。");
  } catch (error) {
    console.error(error);
    setStatus("顔検出に失敗しました。通信を確認して再試行してください。SDK・WASM・モデルの取得やGPU/CPUの初期化が失敗している可能性があります。詳細は開発者ツールのConsoleで確認できます。", true);
  } finally {
    running = false;
    updateControls();
  }
}

for (const radio of sampleInputs) {
  radio.addEventListener("change", () => {
    if (!radio.checked) return;
    fileInput.value = "";
    const sample = SAMPLE_IMAGES[radio.value];
    void selectImage(sample.url, sample.name);
  });
}

fileInput.addEventListener("change", () => {
  const file = fileInput.files?.[0];
  if (!file) return;
  if (file.size > MAX_FILE_BYTES) {
    fileInput.value = "";
    setStatus("20 MB以下の画像を選んでください。現在の画像を引き続き使えます。", true);
    return;
  }
  for (const radio of sampleInputs) radio.checked = false;
  void selectImage(file, file.name);
});

runButton.addEventListener("click", () => void runDetection());

// 通常のページ離脱時にWASMやGPUのリソースを解放します。
window.addEventListener("pagehide", (event) => {
  if (event.persisted) return; // 戻る操作用のページキャッシュでは保持します。
  detector?.close();
  detector = null;
});

// 初期表示では画像だけを読み込みます。AIの取得・推論はボタンで開始します。
void selectImage(SAMPLE_IMAGES.person.url, SAMPLE_IMAGES.person.name);

7. コードの処理を順に理解する

画像を読み込む
    ↓
MediaPipeで推論する
    ↓
顔の位置やスコアを受け取る
    ↓
自分のJavaScriptでCanvasに枠を描く

HTMLはボタンやCanvasを用意し、JavaScriptは操作を受け取って処理を進めます。type="module"を付けたscriptからapp.jsを読み込むことで、JavaScript内のimportを使えるようにしています。

① APIとWASMを準備する

FaceDetectorが顔検出を行うAPI、FilesetResolverがWASMの関連ファイルを解決するAPIです。FilesetResolver.forVisionTasks()には、WASMファイルを置いたフォルダのURLを渡します。ここで読み込む実行ファイルと、JavaScript SDKのバージョンを揃えます。

const SDK_VERSION = "0.10.34";
const SDK_BASE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${SDK_VERSION}`;
const { FaceDetector, FilesetResolver } = await import(`${SDK_BASE}/vision_bundle.mjs`);
const vision = await FilesetResolver.forVisionTasks(`${SDK_BASE}/wasm`);

SDKとWASMのURLには同じSDK_BASEを使うため、バージョンが揃います。人物と猫の画像は./person.jpgと./cat.jpgという相対パスです。./は今のサンプルページがある場所を基準にします。

② モデルを指定してFaceDetectorを作る

FaceDetector.createFromOptions()で、modelAssetPathにモデルのURL、runningModeに"IMAGE"を指定します。この時点で検出器を準備し、画像を選ぶたびにモデルを作り直さないようにします。

  • IMAGE:今回のように、1枚の静止画を処理するモード。
  • VIDEO:動画やカメラのフレームを、時刻付きで処理するWebのモード。
  • minDetectionConfidence:どのスコア以上を検出として採用するか。今回の基準は0.5。

このサンプルはGPUでの初期化を試し、初期化に失敗した場合にCPUでも試す流れです。GPUを使えば必ず速いという意味ではなく、利用できる実行方法が端末によって違うためです。設定とWeb APIは公式のFaceDetector Webガイドを参照してください。

人物サンプルは集合画像なので、今回はfull-rangeを選びました。自撮りなど近距離の顔向けにはshort-rangeという選択肢もあります。どのモデルが合うかは、顔の大きさ・向き・隠れ方を含む実際の入力で確認します。モデルごとの用途は公式のモデル説明を参照してください。

③ 画像の読み込み完了を待つ

画像のURLを設定した直後には、まだ画像データを使えません。サンプルでは、読み込み・デコードが完了してから推論とCanvas描画へ進みます。非同期の準備が終わるまで待つための構文がawaitです。

重要なのは、モデルの準備完了と、画像の準備完了の両方を待つことです。「画像が見えているからモデルも準備できている」とは限りません。async・awaitの基本は非同期処理の技術メモで補えます。

④ detect()で結果を受け取る

// faceDetectorとinputCanvasは準備済みのものを使う
const result = faceDetector.detect(inputCanvas);
console.log(result.detections);

detect()の戻り値にはdetectionsという配列があり、検出した顔ごとの情報が入ります。顔が見つからなければ空の配列です。まず配列の長さで検出件数を知り、その後で一つずつ描画します。

ここでのdetect()は同期処理です。awaitを付ければ別のスレッドで動く、というわけではありません。今回のボタンで1枚ずつ処理する例から、連続する動画へ発展させる際には、UIを止めないためのWeb Workerなども検討します。

⑤ Canvasに元画像と枠を描く

検出結果は数値なので、それだけでは画像の上に何も出ません。サンプルでは、長辺が960pxを超える画像を比率を保って縮小し、inputCanvasへ描きます。そのCanvasを推論に渡し、同じサイズのresultCanvasに元画像と枠を描きます。推論した画像と、結果を描く画用紙のサイズを揃えることで、座標の換算を単純にしています。

// 準備済みのinputCanvasとresultContextを使う
resultContext.drawImage(inputCanvas, 0, 0);

for (const detection of detections) {
  const box = detection.boundingBox;
  if (!box) continue;
  resultContext.strokeRect(box.originX, box.originY, box.width, box.height);
}

これは描画部分を抜き出した説明用コードです。実行には完全版を使ってください。表示はCSSで画面幅に収めます。canvas.widthの画素数と、スマホ画面上の表示幅は別物です。両者の違いを意識すると、枠がずれる原因を探しやすくなります。Canvas自体の基本はCanvas APIの技術メモを参照してください。

⑥ エラーと後片付けも用意する

モデルの読み込み失敗、画像の読み込み失敗、利用できないGPUなどは、成功したときとは別の経路です。配布コードのtry・catchで、画面に失敗したことが分かるメッセージを表示しています。ボタンの連打を防ぐ処理も、複数の初期化が同時に走らないために必要です。

検出器を使い終わったらclose()で関連リソースを解放します。別のモデルへ切り替えるアプリでは、古い検出器を残したまま新しい検出器を増やさないようにします。

8. 結果の数値と猫の結果を読み取る

detectionsに入る主な情報
プロパティ読み方
boundingBox.originX / originY顔の矩形の左上の位置。今回の入力Canvas上の画素単位。原点は画像の左上。
boundingBox.width / height矩形の幅・高さ。Canvasの枠を描く寸法に使う。
categories[0].scoreモデルの検出スコア。「この結果は顔らしい」という判断の強さ。実際の正解率をそのまま表す数値ではない。
keypoints目・鼻などの少数の特徴点。正規化された座標なので、Canvasに描くにはxに入力Canvasの幅、yに入力Canvasの高さを掛ける。

矩形は画素、特徴点は正規化座標、と単位が違います。すべての値に画像の幅を掛けたり、すべてを同じ単位だと思ったりしないことが大切です。デモのJSON表示を開いて、実際のフィールドと数値を確認してみてください。

猫はFaceDetectorの対象ではない

運営者の愛猫2匹の写真を同じ顔検出デモへ入力し、入力画像・検出結果・検出0個を表示した実行画面
愛猫の写真を同じFaceDetectorで処理した実行キャプチャ。Windows上のChrome・GPU実行で検出は0個、JSONは{ "detections": [] }でした。800×589pxの写真は縮小せず処理しています。この画像・条件での観察結果です。

FaceDetectorで使うBlazeFaceは、人の顔を検出するモデルです。猫の顔を探すためのモデルではありません。猫の写真で検出件数が0でも、それだけで導入失敗とは判断できません。反対に、猫の模様などに枠が出た場合も、猫を正しく認識できる証拠にはなりません。

猫が画像のどこにいるかを知りたいなら、猫のカテゴリに対応したモデルを使うObjectDetectorが候補になります。画像全体が猫らしいかを分類したいならImageClassifierが候補です。物体の位置と、画像全体の分類は異なる処理です。ImageClassifierの公式ガイドも比較してください。

次に試す小さな変更

  1. まず、人物と猫の両方で処理が完了することを確認します。
  2. app.jsのminDetectionConfidence: 0.5を0.8へ変更します。
  3. 保存後、ブラウザを再読み込みして、同じ人物画像を処理します。
  4. 採用する結果の基準を厳しくしたとき、検出件数が変わるか観察します。変わらない場合もあります。
  5. 確認が終わったら0.5へ戻します。

しきい値を下げると、弱い候補も拾う一方、誤検出が増えることがあります。しきい値を変えても、そのモデルが猫検出用になるわけではありません。まず対象に合うタスクとモデルを選び、その後で設定を調整します。

9. スマホで動かすときの考え方

このサンプルはブラウザで動くWeb版です。スマホでアクセスするときも、Android用・iOS用のネイティブSDKを別途組み込む必要はありません。ただし、対応状況、GPU、メモリ、処理速度は端末とブラウザによって異なります。

  • まず公開デモで確認する:スマホでは記事の動作デモへのリンクを開きます。スマホのlocalhostはスマホ自身を指すため、PCで起動したlocalhost:8080をそのまま入力してもPCへ接続できません。
  • 画像と表示のサイズを分ける:Canvas内部の画素数は保ち、CSSのmax-width: 100%などで表示を小さくします。
  • GPUだけに依存しない:この例のように、GPUの初期化失敗時にCPUを試す経路を作ります。それでも全端末での動作を保証するものではありません。
  • 大きな画像を何枚も保持しない:実用アプリでは処理用画像の縮小や、不要になったモデル・画像の解放を考えます。
  • カメラは次の段階:今回は静止画だけを使い、カメラ権限は要求しません。カメラ版ではHTTPSなどの条件、権限、動画の連続処理と停止処理も必要になります。

PCのブラウザを狭く表示して確認できるのは、主にレイアウトです。実際のスマホでの推論速度やメモリ使用は実機で確認します。Webの前提条件は公式のWebセットアップ案内も参照してください。

10. このサイトのAI画像ツールへどうつながるか

今回の「枠を描く」が理解できれば、FaceDetectorの役割と、画像加工の役割を分けて考えられます。例えば全自動顔モザイクでは、顔の位置を推定した後、その範囲へモザイクなどを描画します。顔だけ一括切り出し保存では、その座標を使って切り抜く範囲を求めます。

最小サンプルから実用ツールへ進むと、追加するもの

  • ファイル選択と画像の読み込み。
  • 検出範囲の余白や、画像の端を越える場合の補正。
  • プレビューと保存画像の解像度の扱い。
  • 複数画像を扱うときの順番・進捗・メモリ管理。
  • 見逃しや誤検出を利用者が確認・修正する操作。

唇に色を塗るなら、顔の矩形だけでは位置が粗すぎます。リップ試着のような用途ではFaceLandmarkerの細かな点を使います。人物の背景透過では、顔の点ではなく領域マスクを返すImageSegmenterが出発点になります。

「顔検出を導入すれば、画像のあらゆる加工ができる」という理解より、必要な出力に合うタスクを選び、その後の加工を自分で作ると考えると、次の技術を選びやすくなります。

11. うまく動かないときの確認表

PCではF12などでブラウザの開発者ツールを開き、ConsoleとNetworkを確認します。Networkの404は「そのURLにファイルがない」という意味です。エラーメッセージだけでなく、読み込もうとしたURLも確認してください。

症状から、次に確認する場所を決める
症状まず確認すること
ページは開くがボタンが動かないURLがhttp://localhost:8080/か。Consoleのモジュール読み込みエラー、app.jsの404を確認。
画像が表示されないperson.jpgとcat.jpgを同じフォルダに置いたか。ZIP内の構成を変えていないか。画像URLの404を確認。
WASM・モデルを読めないインターネットへ接続できるか。NetworkでCDN・モデルのURLを確認。端末やネットワークがそれらの取得をブロックしていないか。
サーバーを起動できない8080番が使用中なら-p 8081で起動し、ブラウザもhttp://localhost:8081/を開く。
人物の顔が0件になる人物サンプルでも同じか。顔が小さい・横向き・隠れている場合や、しきい値が高い場合を確認。
猫に枠が出ないFaceDetectorの対象は人の顔。処理が完了して空の結果なら、対象とモデルの組み合わせを確認。
枠がずれる検出に使った画像と描画画像が同じか。元画像の画素数とCSSの表示幅を混同していないか。
コードを変えても結果が同じファイルを保存したか。ブラウザを再読み込みしたか。開発者ツールのNetworkでキャッシュを無効にして確認。

公式資料と、次に読む技術メモ

APIや対応環境は更新されます。記事のサンプルを起点にし、別のタスクやモデルへ進む際は、そのタスクの公式ガイドとモデルの説明を確認してください。

制作時に確かめたこと