MediaPipe顔検出入門:FaceDetectorの結果を読み、枠とモザイクへつなぐ
FaceDetectorは「人の顔がどこにありそうか」を調べるタスクです。今回はプロジェクト内のAI生成人物画像に緑の枠を描き、JSONに出てくる位置とスコアを読みます。顔を囲む処理を理解すると、サイトの顔モザイクや顔の一括切り出しにつながります。
1. 顔の位置と、顔のパーツは別の出力
FaceDetectorは顔を囲む長方形、検出スコア、目・鼻など少数の特徴点を返します。人を識別したり、唇の細かな輪郭を取ったりする用途とは分けて考えます。リップ試着に必要な輪郭はFaceLandmarkerの顔の点で扱います。
このサンプルはSDK 0.10.34、BlazeFace full-range float16の版1、IMAGEモードを使います。対象は人の顔です。猫の写真を選べるのは、対象外の入力を試して「処理完了」と「認識できた」を区別するためです。猫で枠が出ても、それを猫用の顔認識機能として扱えません。機能とモデルは公式FaceDetector概要で確認できます。
2. サンプルを実行する
顔検出とモザイクのサンプルを開く → 「AI作成の人物」を選ぶ → 実行ボタンを押す、の順に進めます。検出数、モザイク、枠、スコアが出たら、下のJSONも開いて同じ数の要素があるか確かめます。その後「愛猫の写真」へ切り替えてもう一度実行します。Web導入の記事から進んだ方は、検出した位置へ加工を追加する違いを確認します。

自分のPCで動かす場合はモザイクサンプルのZIP一式を展開し、index.htmlのあるフォルダーをVS Codeで開きます。ターミナルでnpx.cmd --yes http-server . -p 8080 -c-1を実行し、http://127.0.0.1:8080/へアクセスします。Node.jsの準備やmacOS向けの読み替えはWeb導入手順を参照してください。終了はCtrl+Cです。
3. 完全コードと処理の境界
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>顔の矩形を検出し、12%の余白を加えてモザイクを描きます。緑の枠は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;
// 顔の矩形を少し広げ、画像範囲内に収めてからモザイクにする。
const padding = Math.max(box.width, box.height) * 0.12;
const left = Math.max(0, Math.floor(box.originX - padding));
const top = Math.max(0, Math.floor(box.originY - padding));
const right = Math.min(inputCanvas.width, Math.ceil(box.originX + box.width + padding));
const bottom = Math.min(inputCanvas.height, Math.ceil(box.originY + box.height + padding));
const width = right - left, height = bottom - top;
if (width > 0 && height > 0) {
const small = document.createElement("canvas");
small.width = Math.max(1, Math.round(width / 12));
small.height = Math.max(1, Math.round(height / 12));
small.getContext("2d").drawImage(inputCanvas, left, top, width, height, 0, 0, small.width, small.height);
resultContext.imageSmoothingEnabled = false;
resultContext.drawImage(small, left, top, width, height);
resultContext.imageSmoothingEnabled = true;
}
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);
getDetector()は初回だけSDK・WASM・モデルを取得し、作った検出器を次の画像でも使い回します。selectImage()は画像をCanvasへ用意する処理、runDetection()は推論、drawDetections()は結果の描画です。この3つを分けて読むと、AIの仕事と自分の加工コードの境目が分かります。
const result = detector.detect(inputCanvas);
for (const detection of result.detections) {
const box = detection.boundingBox;
if (!box) continue;
resultContext.strokeRect(box.originX, box.originY, box.width, box.height);
console.log(detection.categories[0]?.score);
} 静止画はdetect()へ入力Canvasを渡します。結果の配列を一つずつ読み、strokeRect()で枠を描きます。detect()は結果を返す処理で、元画像に枠を書き込む処理ではありません。画像を上書きせず入力用と結果用のCanvasを分けると、再実行や加工の取り消しがしやすくなります。
4. boundingBoxとscoreを読む
| 値 | 意味 | 単位・注意 |
|---|---|---|
originX / originY | 顔の枠の左上 | 推論した画像のピクセル座標 |
width / height | 顔の枠の幅と高さ | ピクセル数 |
categories[0].score | 顔検出のスコア | 正解率や本人確認の確率ではない |
keypoints | 少数の顔特徴点 | 矩形とは異なり、x・yは正規化座標 |
入力画像の長辺を960 px以内に縮めてから推論するので、枠も縮小後のサイズに対応します。例えば横幅1024 pxの画像を960 pxにしたなら、取得したx座標を1024 pxの画像へ直接描くとずれます。元画像へ戻すには元幅÷推論幅、元高さ÷推論高さをそれぞれ掛けます。表示上のCSS幅だけを変える場合はCanvas内部の座標まで縮めないようにします。
スコアの高低は、モデルがその候補をどれだけ顔として扱ったかの参考です。「98%なら98%の個人情報保護ができる」という解釈はできません。複数の顔は配列として返り、順番を人物のIDに使えません。どれが同じ人物かを追跡する機能を作ったわけではないからです。
5. モデルと検出閾値を選ぶ
公式には近距離向けのshort-rangeと、より広い距離向けのfull-rangeがあります。この教材は複数人が写る既存画像を例にfull-rangeを使います。カメラの至近距離など別の条件なら、同じ画像を両方で試して比較します。顔の見える大きさや角度が変わるため、名前だけで最適なモデルを決めません。
minDetectionConfidence: 0.5は候補を採用する下限です。0.3へ下げると候補が増えることがありますが、誤検出も増え得ます。0.7へ上げると候補を絞れる一方、隠したい顔を見逃すかもしれません。まず初期値で実行し、同じ入力に対して一つの設定だけ変更します。変更前後の検出数と枠の位置を記録すると、閾値の効果を把握できます。
6. サイトの顔モザイクへつなぐ
顔モザイクツールでは検出した範囲を加工へ渡します。サンプルの枠を描くループで、boxを受け取った後に次の処理を加えると、同じ矩形へ簡単なモザイクを描けます。これは完成コードの描画処理を置き換えて試す追加例です。
const pixelated = document.createElement("canvas");
pixelated.width = Math.max(1, Math.round(box.width / 12));
pixelated.height = Math.max(1, Math.round(box.height / 12));
pixelated.getContext("2d").drawImage(inputCanvas,
box.originX, box.originY, box.width, box.height,
0, 0, pixelated.width, pixelated.height);
resultContext.imageSmoothingEnabled = false;
resultContext.drawImage(pixelated, box.originX, box.originY, box.width, box.height);
resultContext.imageSmoothingEnabled = true; 小さなCanvasへ顔を縮小し、補間を無効にして拡大すると四角い粒になります。AIが粒を作るのではなく、顔の範囲を教えています。検出された枠だけでは髪や額が十分に隠れない場合があるため、実用品では余白を足して画像の範囲内へ収めます。端にある枠を切り出すときはx・yを0以上にし、幅・高さが元画像を越えないように確認します。
顔の一括切り出しも同じ座標を使えます。ただし用途は逆で、周囲を少し含めたいか、顔だけを残したいかで余白の設計が変わります。AI検出を初期候補にして、人が枠を確認・補正できる流れにすると使いやすくなります。
自分で改造して仕組みを確かめる
完全コードのpaddingを0へ変えて実行し、元の検出矩形だけを加工した結果と比べます。次に余白を0.2へ増やし、隠す範囲が広がることを確認します。顔の検出数とスコアが同じまま見た目だけ変われば、余白はモデルの設定ではなくアプリの加工設定だと分かります。さらに縮小Canvasの除数12を6・20へ変えると、モザイクの粒だけが変化します。一つずつ変更し、元の値へ戻せるように記録します。
最後に猫の画像へ切り替え、候補がなければモザイクも描かれないことを確かめます。0件で加工を呼ばない分岐も、利用者へ意味を伝えるために必要です。手動の追加枠や画像全体の加工をどう用意するかは、実用品へ進む時の次の課題です。
7. 見逃し・誤検出と、動かない場合を分ける
小さな顔、横顔、手や物で隠れた顔、強い逆光では見逃すことがあります。AI生成画像にも、現実には不自然な目や輪郭が含まれることがあります。一枚成功しても、あらゆる写真での性能を確認したことにはなりません。愛猫で0件ならモデルの対象を再確認し、人の画像で0件なら画像解像度・顔の大きさ・閾値を順に見ます。
「0個」とJSONが表示されるのは、正常に推論して候補がなかった状態です。SDK取得失敗や初期化エラーの場合はエラーメッセージが出ます。GPU初期化に失敗するとCPUを試し、どちらも失敗した場合はConsoleに詳細が残ります。検出器はページ離脱時にclose()で解放します。
モザイクを公開する前は完成画像を目で見て、すべての対象を隠せたか確認します。サンプルは枠の理解を目的とし、匿名化の保証はしません。APIの詳細は公式Web顔検出ガイドへ。唇や目に沿った加工が必要になったら、次の顔ランドマークへ進みます。