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専用の技術ではない
| 環境 | 使う場面 | このサイトとの関係 |
|---|---|---|
| Web | JavaScriptでブラウザ上のアプリを作る。 | PC・スマホのブラウザで使う猫の手ツールはこの形。 |
| Android | Androidのネイティブアプリへ組み込む。 | Android端末でWebサイトを開く場合とは実装方法が違う。 |
| iOS | iPhone・iPadのネイティブアプリへ組み込む。 | iPhoneのSafariでこのサイトを開く場合はWeb版を使う。 |
| Python | Pythonのプログラムで画像などを処理する。 | 学習・実験や処理の自動化に使える。この記事のコードは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、推論は「この人物画像の顔を検出する」という実行です。
| 種類 | 役割 | 今回の例 |
|---|---|---|
| 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など、すべての言語のパッケージが同じ番号になるという意味ではありません。
| 対象 | Web SDK | 読み込み方・確認方法 |
|---|---|---|
| 現在の猫の手ツール | 0.10.34 | SDK・WASMをサイト内から配信。package-lock.jsonと配布SDKを照合して確認。 |
| この記事の動作サンプル | 0.10.34 | CDNのURLを固定。掲載コードとキャプチャの再現用に同じ版を使う。 |
| 最新の安定版Web SDK | 1.1.0 | npmの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を読み込み、タスクに応じたモデルをサイト内から取得します。モデルの例は次のとおりです。
| 用途 | モデルファイル | サイトの例 |
|---|---|---|
| 顔の検出 | 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が元画像です。人物で成功例を確認し、猫ではモデルの対象が異なるとどうなるかを観察します。
- デモを開き、「AI作成の人物」を選びます。最初は人物が選択されています。
- 「AIを読み込んで顔検出」を押し、モデルの準備が終わるまで待ちます。
- 画像の上に描かれた枠と、検出件数・結果のJSONを確認します。
- 「愛猫の写真」に切り替え、「選んだ画像で顔検出」を押します。人物の結果と比較してください。
最初の実行では、SDK・WASM・モデルを読み込む時間が含まれます。同じページで2回目を実行したときとは、待ち時間の意味が異なります。初回の待ち時間全体を「1枚の推論にかかった時間」として比較しないようにしましょう。
推論はブラウザ内で行います。「自分の画像でも試す」で選んだ画像を、サンプルのコードから外部サーバーへ送信する処理はありません。一方で、SDKやモデルの取得には通信を使うため、初回から完全にオフラインで動くサンプルではありません。
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の完全コードを開く
<!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. 結果の数値と猫の結果を読み取る
| プロパティ | 読み方 |
|---|---|
boundingBox.originX / originY | 顔の矩形の左上の位置。今回の入力Canvas上の画素単位。原点は画像の左上。 |
boundingBox.width / height | 矩形の幅・高さ。Canvasの枠を描く寸法に使う。 |
categories[0].score | モデルの検出スコア。「この結果は顔らしい」という判断の強さ。実際の正解率をそのまま表す数値ではない。 |
keypoints | 目・鼻などの少数の特徴点。正規化された座標なので、Canvasに描くにはxに入力Canvasの幅、yに入力Canvasの高さを掛ける。 |
矩形は画素、特徴点は正規化座標、と単位が違います。すべての値に画像の幅を掛けたり、すべてを同じ単位だと思ったりしないことが大切です。デモのJSON表示を開いて、実際のフィールドと数値を確認してみてください。
猫はFaceDetectorの対象ではない
{ "detections": [] }でした。800×589pxの写真は縮小せず処理しています。この画像・条件での観察結果です。FaceDetectorで使うBlazeFaceは、人の顔を検出するモデルです。猫の顔を探すためのモデルではありません。猫の写真で検出件数が0でも、それだけで導入失敗とは判断できません。反対に、猫の模様などに枠が出た場合も、猫を正しく認識できる証拠にはなりません。
猫が画像のどこにいるかを知りたいなら、猫のカテゴリに対応したモデルを使うObjectDetectorが候補になります。画像全体が猫らしいかを分類したいならImageClassifierが候補です。物体の位置と、画像全体の分類は異なる処理です。ImageClassifierの公式ガイドも比較してください。
次に試す小さな変更
- まず、人物と猫の両方で処理が完了することを確認します。
app.jsのminDetectionConfidence: 0.5を0.8へ変更します。- 保存後、ブラウザを再読み込みして、同じ人物画像を処理します。
- 採用する結果の基準を厳しくしたとき、検出件数が変わるか観察します。変わらない場合もあります。
- 確認が終わったら
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や対応環境は更新されます。記事のサンプルを起点にし、別のタスクやモデルへ進む際は、そのタスクの公式ガイドとモデルの説明を確認してください。
- MediaPipe Solutionsガイド:全体像、対応環境、旧APIとの関係。
- Webセットアップガイド:Web版の導入と前提条件。
- FaceDetectorの概要とモデル:入力・出力、モデルの用途。
- FaceDetectorのWeb実装ガイド:JavaScriptの設定と呼び出し。
- File APIとObject URL:利用者が選んだ画像を読み込むための基礎。
- Canvas API:読み込み済みの画像を描く・切り抜くための基礎。
- 全自動顔モザイクの制作解説:今回の顔の位置から実用的な加工へ進む例。