猫の手ツール

作成・資料確認日:2026年10月12日

公式資料と、このプロジェクトの実装・サンプルの実行をもとに説明します。人物の作例はAI生成、猫の写真は運営者の愛猫です。実行条件や確認できた範囲は各記事に記載しています。

MediaPipe Web導入:最初のJavaScriptサンプルを自分のPCで動かす

この記事のゴールは、AI生成の人物画像をブラウザに読み込み、MediaPipeで顔を検出するプログラムを自分のPCで起動することです。HTML・JavaScriptを少し読める方を対象に、フォルダーの開き方から結果の確認まで順番に進めます。Web開発のフレームワークは使いません。

1. 先に完成サンプルを試す

顔検出のサンプルを別タブで開くと、人物の画像が表示されます。「AIを読み込んで顔検出」を押し、緑色の枠が顔の位置に出ることを確かめてください。初回はAI関連ファイルを取得するため、結果が出るまで待ちます。ページを開いただけではAIの取得と推論を開始しません。

プロジェクト内のAI生成人物画像を実行した顔検出サンプルの画面
導入サンプルの実行画面。AIが顔の位置を返し、サンプルのJavaScriptが枠を描いています。人物はAI生成の既存素材です。

用意するものは、PC、ChromeまたはSafari、コードエディター、Node.js、インターネット接続です。WindowsではVS CodeとPowerShellを使う手順を示します。Node.jsは、この例では開発中のHTTPサーバーを動かすために使います。顔の推論を実行するのはブラウザです。スマホの閲覧者にNode.jsを入れてもらう必要はありません。

Node.jsをまだ入れていない場合はNode.js公式のダウンロードページでLTS版を選び、インストールします。公式Web導入ガイドはChrome・Safariと、npmまたはCDN経由の導入を案内しています。この記事の対象環境はMediaPipeのWeb導入ガイドも参照してください。

2. VS Codeを準備し、編集・保存の操作を覚える

VS Code(Visual Studio Code)は、HTMLやJavaScriptのファイルを開いて編集するためのコードエディターです。サンプルのapp.jsを読む画面と、コマンドを実行するターミナルを同じアプリ内で使えます。初めて使う方は、この節で準備してから次のZIP起動手順へ進んでください。

今回使う3つの道具の役割
道具今回の役割
VS Codeindex.html・app.jsを読み、コードを編集して保存する。
Node.jsとnpm開発用のHTTPサーバーを取得・起動する。VS Codeとは別に導入する。
ブラウザ配信されたサンプルを表示し、MediaPipeで推論する。

WindowsにVS Codeをインストールする

  1. VS Code公式:Windowsのインストール手順(英語)を開きます。
  2. 通常は自分のWindowsアカウントへ入れるUser setupを選び、端末に対応するインストーラーを保存します。公式ページにダウンロードへのリンクがあります。
  3. 保存したVSCodeUserSetup-…exeを起動し、画面の案内に従ってインストールします。完了したらスタートメニューからVisual Studio Codeを開きます。

ファイルを編集するVS Codeと、サーバーを起動するNode.jsは別の道具です。VS Codeを入れた後も、前の節のNode.jsの準備とバージョン確認を行います。すでに使っているエディターがある方は、そのままでもサンプルを編集できます。

日本語の表示にする

  1. VS CodeでCtrl+Shift+Pを押し、コマンドパレットを開きます。
  2. Configure Display Languageと入力し、その項目を選びます。
  3. 日本語(ja)を選びます。必要なLanguage Pack(表示言語の拡張機能)のインストールを案内されたら導入し、再起動の案内に従います。
  4. メニューが「ファイル」「編集」などの日本語になったことを確認します。

表示言語の変更はVS Code公式:表示言語とLanguage Pack(英語)で確認できます。日本語表示はメニューを読みやすくする設定で、JavaScriptの文法やモデルの認識対象を変えるものではありません。

フォルダーを開き、ターミナルを表示する

次の節でZIPを展開したら、VS Codeの「ファイル → フォルダーを開く」(英語表示ならFile → Open Folder)で、index.htmlとapp.jsが並ぶフォルダーを選びます。左側のエクスプローラーにファイル一覧が出れば、プロジェクトを開けています。app.jsをクリックすると中央にコードが表示されます。

「ターミナル → 新しいターミナル」(Terminal → New Terminal)を選ぶと、下部にコマンドを入力する画面が開きます。表示中のフォルダーがサンプルの場所か確認し、次の節のnode --versionなどを一行ずつ実行します。コードの本文へコマンドを書くのではなく、ターミナルへ入力します。

編集したら保存し、ブラウザを更新する

最初の起動に成功した後、app.jsのMAX_EDGEを一つだけ変更してみます。Ctrl+Sで保存し、サンプルを開いたブラウザを更新してから、実行ボタンを押します。推論サイズ欄の変化を確認し、元の値へ戻してもう一度保存します。この教材のHTTPサーバーは、保存しただけでブラウザを自動更新する仕組みではありません。

ファイル名のタブに未保存の印がある時は、変更がまだサーバーから配信されるファイルへ反映されていません。エラーが出たら、まず直前の変更を戻して保存し、ブラウザのConsoleを確認します。「編集 → 保存 → ブラウザ更新 → 実行」の順を揃えると、どのコードを試したか追いやすくなります。

3. ZIPを展開してHTTPサーバーを起動する

  1. 顔検出サンプル一式のZIPを保存します。ZIPファイルを右クリックし「すべて展開」を選びます。
  2. 展開したフォルダーの中にindex.html、app.js、person.jpg、cat.jpg、README.txtがあることを確認します。ZIPの中身を直接開かず、展開済みのファイルを使います。
  3. VS Codeの「ファイル → フォルダーを開く」で、その5ファイルが入ったフォルダーを選びます。
  4. 「ターミナル → 新しいターミナル」を開き、次のコマンドを一行ずつ実行します。Node.jsを入れた直後で認識されない場合は、VS Codeを開き直します。
node --version
npm.cmd --version
npx.cmd --yes http-server . -p 8080 -c-1

最初の2行でバージョン番号が表示されれば準備完了です。3行目は現在のフォルダーを配信します。--yesは初回に必要なhttp-serverパッケージを取得し、-p 8080はポート番号、-c-1は開発中のキャッシュを無効にする指定です。Windowsでnpm.cmd・npx.cmdと書くのは、PowerShellのスクリプト実行設定に影響されにくい入口を使うためです。macOS・Linuxはnpm・npxに読み替えます。

  1. ターミナルは閉じず、ブラウザのアドレス欄にhttp://127.0.0.1:8080/を入力します。
  2. 人物画像が出たら実行ボタンを押します。枠、検出数、実行先、JSONが表示されれば起動成功です。
  3. 終了するときはターミナルでCtrl+Cを押します。再開するときは3行目をもう一度実行します。

index.htmlのダブルクリックで開くと、アドレスがfile://になります。モジュールやWASMの取得でブラウザの制約に触れやすいため、この教材はHTTPサーバーで開きます。記事を公開したサーバーと自分のPCのサーバーは別物です。127.0.0.1は「いま操作している端末自身」を指します。

4. SDK・WASM・モデルを分けて理解する

初回のボタン操作で読み込む3種類
種類役割確認する場所
SDKFaceDetectorなどのAPIをJavaScriptへ提供する。vision_bundle.mjs
WASM推論の実行コード。SDKと同じ版のファイルを使う。FilesetResolver.forVisionTasks()のURL
モデル学習済みデータ。目的に対応するものを選ぶ。modelAssetPath

SDKを導入しただけでは、モデルはまだ用意できていません。逆に.tfliteを保存しても、そのファイルを呼び出すAPIと実行環境が必要です。FaceLandmarkerやHandLandmarkerでは.taskというモデルバンドルを使います。拡張子を交換するだけで別のタスクにはできません。

サンプルはSDKとWASMを0.10.34に固定し、モデルも公式配布URLの版/1/を指定しています。教材を読んでいる間に内容が変わるのを避け、サイトの採用版と揃えるためです。CDNは外部のファイル配信サービスです。通信が切れていると、初回の取得は完了しません。入力画像をサーバーで解析するという意味ではありません。

5. 完全コードと読み始める場所

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);

最初はHTMLの全CSSを読む必要はありません。input-canvasとresult-canvas、実行ボタン、<script type="module" src="./app.js">を探します。type="module"はJavaScriptをモジュールとして扱う指定です。AI取得のawaitが出てくるので、処理の完了を待つ考え方が必要になります。

const base = "https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]";
const { FaceDetector, FilesetResolver } = await import(base + "/vision_bundle.mjs");
const vision = await FilesetResolver.forVisionTasks(base + "/wasm");
const detector = await FaceDetector.createFromOptions(vision, {
  baseOptions: { modelAssetPath: MODEL_URL, delegate: "CPU" },
  runningMode: "IMAGE"
});

上の抜粋は準備の関係を見やすくしたものです。MODEL_URLには完全コードのモデルURLを使います。await import()でSDKを読み込み、WASMを準備し、createFromOptions()でタスクを作ります。完全コードではGPUを先に試し、初期化できない場合にCPUで作り直します。GPUなら常に速いという意味ではなく、どちらも推論するための実行先です。

次にselectImage()を読みます。画像のdecode()完了を待ち、長辺を960 px以内に縮小してCanvasへ描きます。そのCanvasをdetect()へ渡し、result.detectionsを描画します。ファイルを選び直した時の古い結果を表示しない工夫や、作ったObject URLを解放する処理も含めています。最初の改造ではMAX_EDGEや枠の色を一つずつ変え、結果との対応を確かめてください。

6. npmで導入する場合

AstroやViteなど、すでにnpmを使うプロジェクトならnpm.cmd install @mediapipe/[email protected]でSDKを追加し、import { FaceDetector, FilesetResolver } from "@mediapipe/tasks-vision"と書けます。これはCDN版の代わりに使う入口です。今回のサンプルに追加でインストールする必要はありません。

npmを使っても、WASMとモデルの配信場所は決める必要があります。ブラウザからアクセス可能な公開フォルダーへコピーし、Networkタブで200応答を確認します。サーバー側だけに存在するパスやWindowsのドライブ名をmodelAssetPathへ書いても、閲覧者のブラウザは取得できません。

7. このサイトではローカル配信する

猫の手ツールの画像AI機能は、/lib/tasks-vision/からSDKとWASMを配信し、/models/などからモデルを配信します。教材は持ち運びやすいCDN版ですが、処理の流れは共通です。npm依存に旧@mediapipe/face_detection・@mediapipe/selfie_segmentationが残っていても、実際にページが読み込むSDKをソースで確認することが大切です。

採用版と公開最新は同じとは限りません。最新動向は概要ページの版と動向で整理しています。更新時はJavaScriptだけを差し替えず、WASMも同じ版に揃え、既存画像で出力の形・描画・解放を再確認します。サンプルの成功を確かめてから、顔検出の座標とスコアへ進んでください。

8. 起動できないときの確認表

エラーの見える場所から切り分ける
症状確認と対処
nodeが認識されないインストール後にVS Codeを再起動。Node.jsの公式インストーラーとPATHを確認。
ポート8080が使えないサーバーの-p 8081へ変更し、ブラウザも8081へ変更。
ファイル一覧だけが出るindex.htmlがあるフォルダーでコマンドを実行したか確認。
画像が表示されないperson.jpgとcat.jpgが同じ階層にあるか、ファイル名の大文字小文字を確認。
初回読み込みで止まる開発者ツールのNetworkでCDN・Googleのモデル取得を確認。企業ネットワーク等の制限も確認。
0件と表示される読み込み失敗と区別。人向けモデルなので、愛猫で0件でもコードが失敗したとは限らない。

Consoleのエラー文、ブラウザ、実行先、入力画像を記録してから修正すると、原因を絞れます。スマホの127.0.0.1はPCにはつながりません。まずPCで動作確認し、スマホでの配信と座標の扱いはスマホ対応の記事で扱います。

入力データの推論は端末内で行います。SDK・WASM・モデルの取得には通信が発生します。MediaPipe公式ではAPIの性能・利用状況メトリクスの送信も案内されています。SDKの版・実行条件と、プライバシー方針を確認してください。

制作時に確かめたこと

ツールを作る途中で実際に確認した症状と、変更を選んだ経緯です。日付は当時の記録日です。現在の実装や、このページの学習サンプルの検証とは分けて読んでください。

MediaPipeは、SDK・WASM・モデルを別々に確かめた

手順にあるファイル名が、実物になかった

導入を始めた時、案内されたvision_bundle.jsが見当たらず、実際には.cjsと.mjsがあると報告しました。CDNでも/vision_bundle.jsを取得できないエラーが出ています。ファイル名だけを想像で補うより、採用版の配布物と公式の導入方法を照合する必要がありました。

4月8日には@mediapipe/facelandmarker、14日には@mediapipe/image_segmentationという独立パッケージ名でnpm E404になりました。現在の公式Webガイドでは、FaceLandmarkerとImageSegmenterは@mediapipe/tasks-visionから使います。タスクのクラス名と、npmへ指定するパッケージ名は一致するとは限りません。

取得できた小さいファイルを、モデル成功とは扱わない

背景透過の導入では、selfie_segmenter.taskなど複数の取得先で404が続きました。curlのログにも248バイトや88バイトの取得結果がありましたが、それだけで有効な学習モデルを入手したとは確認できません。

途中で旧SelfieSegmentationを使う案も試し、その後はnative code called abort()と、処理中表示が終わらない状態になりました。さらにselfie_segmentation.tfliteの404を実行ログで確認しています。AIの説明が変わる中でも、実際にブラウザが要求したURLと応答を先に見る必要がありました。

同日13時40分には「これでばっちり動作しました」と報告しています。これは一連の変更後の背景透過ツールの確認で、最初の.taskのURLが正しくなったことや、abortの原因が一つに特定できたことを示しません。現在の記事はTasks版の教材なので、当時の旧APIのファイル一式をそのまま配置する手順ではありません。

仕様の確認:MediaPipe公式:Web環境の準備(別タブ) / MediaPipe公式:ImageSegmenterのWeb導入(別タブ) / MediaPipe公式:FaceLandmarkerのWeb導入(別タブ)