MediaPipe Web導入:最初のJavaScriptサンプルを自分のPCで動かす
この記事のゴールは、AI生成の人物画像をブラウザに読み込み、MediaPipeで顔を検出するプログラムを自分のPCで起動することです。HTML・JavaScriptを少し読める方を対象に、フォルダーの開き方から結果の確認まで順番に進めます。Web開発のフレームワークは使いません。
1. 先に完成サンプルを試す
顔検出のサンプルを別タブで開くと、人物の画像が表示されます。「AIを読み込んで顔検出」を押し、緑色の枠が顔の位置に出ることを確かめてください。初回はAI関連ファイルを取得するため、結果が出るまで待ちます。ページを開いただけでは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起動手順へ進んでください。
| 道具 | 今回の役割 |
|---|---|
| VS Code | index.html・app.jsを読み、コードを編集して保存する。 |
| Node.jsとnpm | 開発用のHTTPサーバーを取得・起動する。VS Codeとは別に導入する。 |
| ブラウザ | 配信されたサンプルを表示し、MediaPipeで推論する。 |
WindowsにVS Codeをインストールする
- VS Code公式:Windowsのインストール手順(英語)を開きます。
- 通常は自分のWindowsアカウントへ入れる
User setupを選び、端末に対応するインストーラーを保存します。公式ページにダウンロードへのリンクがあります。 - 保存した
VSCodeUserSetup-…exeを起動し、画面の案内に従ってインストールします。完了したらスタートメニューからVisual Studio Codeを開きます。
ファイルを編集するVS Codeと、サーバーを起動するNode.jsは別の道具です。VS Codeを入れた後も、前の節のNode.jsの準備とバージョン確認を行います。すでに使っているエディターがある方は、そのままでもサンプルを編集できます。
日本語の表示にする
- VS CodeでCtrl+Shift+Pを押し、コマンドパレットを開きます。
Configure Display Languageと入力し、その項目を選びます。- 日本語(
ja)を選びます。必要なLanguage Pack(表示言語の拡張機能)のインストールを案内されたら導入し、再起動の案内に従います。 - メニューが「ファイル」「編集」などの日本語になったことを確認します。
表示言語の変更は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サーバーを起動する
- 顔検出サンプル一式のZIPを保存します。ZIPファイルを右クリックし「すべて展開」を選びます。
- 展開したフォルダーの中に
index.html、app.js、person.jpg、cat.jpg、README.txtがあることを確認します。ZIPの中身を直接開かず、展開済みのファイルを使います。 - VS Codeの「ファイル → フォルダーを開く」で、その5ファイルが入ったフォルダーを選びます。
- 「ターミナル → 新しいターミナル」を開き、次のコマンドを一行ずつ実行します。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に読み替えます。
- ターミナルは閉じず、ブラウザのアドレス欄に
http://127.0.0.1:8080/を入力します。 - 人物画像が出たら実行ボタンを押します。枠、検出数、実行先、JSONが表示されれば起動成功です。
- 終了するときはターミナルでCtrl+Cを押します。再開するときは3行目をもう一度実行します。
index.htmlのダブルクリックで開くと、アドレスがfile://になります。モジュールやWASMの取得でブラウザの制約に触れやすいため、この教材はHTTPサーバーで開きます。記事を公開したサーバーと自分のPCのサーバーは別物です。127.0.0.1は「いま操作している端末自身」を指します。
4. SDK・WASM・モデルを分けて理解する
| 種類 | 役割 | 確認する場所 |
|---|---|---|
| SDK | FaceDetectorなどの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で動作確認し、スマホでの配信と座標の扱いはスマホ対応の記事で扱います。