猫の手ツール

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

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

MediaPipe顔検出入門:FaceDetectorの結果を読み、枠とモザイクへつなぐ

FaceDetectorは「人の顔がどこにありそうか」を調べるタスクです。今回はプロジェクト内のAI生成人物画像に緑の枠を描き、JSONに出てくる位置とスコアを読みます。顔を囲む処理を理解すると、サイトの顔モザイクや顔の一括切り出しにつながります。

1. 顔の位置と、顔のパーツは別の出力

FaceDetectorは顔を囲む長方形、検出スコア、目・鼻など少数の特徴点を返します。人を識別したり、唇の細かな輪郭を取ったりする用途とは分けて考えます。リップ試着に必要な輪郭はFaceLandmarkerの顔の点で扱います。

このサンプルはSDK 0.10.34、BlazeFace full-range float16の版1、IMAGEモードを使います。対象は人の顔です。猫の写真を選べるのは、対象外の入力を試して「処理完了」と「認識できた」を区別するためです。猫で枠が出ても、それを猫用の顔認識機能として扱えません。機能とモデルは公式FaceDetector概要で確認できます。

2. サンプルを実行する

顔検出とモザイクのサンプルを開く → 「AI作成の人物」を選ぶ → 実行ボタンを押す、の順に進めます。検出数、モザイク、枠、スコアが出たら、下のJSONも開いて同じ数の要素があるか確かめます。その後「愛猫の写真」へ切り替えてもう一度実行します。Web導入の記事から進んだ方は、検出した位置へ加工を追加する違いを確認します。

AI生成人物の顔を検出し、矩形より少し広い範囲にモザイクを描いたサンプル実行画面
WindowsのChrome・GPUで実行したモザイクサンプル。緑は検出矩形、その周囲12%を加えた範囲を加工しています。SDK 0.10.34・BlazeFace full-range版1・静止画モードでの例です。

自分の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を読む

顔ごとのJSONにある主な値
値意味単位・注意
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顔検出ガイドへ。唇や目に沿った加工が必要になったら、次の顔ランドマークへ進みます。

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

制作時に確かめたこと

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

顔検出が改善しても、全員を隠せるとは限らなかった

手動追加にも、別の描画確認が必要だった

全自動顔モザイクでは、精度が上がっても完璧ではないと感じ、手動で顔を指定して追加する操作を相談しました。ところが、画像の端をタップするとモザイクが薄くなる、最初のタップで既存のモザイクまで再描画されたように見える、カーソルが消えるという問題も記録しています。

この時点の記録では再描画の問題が残っており、最終的に直ったとは確認できません。AIの見逃しを補う操作を加えても、すでに隠した領域が操作後に変わらないかを別途検証する必要があります。

検出数・スコアの確認に加えて、完成画像の全対象、画像の端、追加後の領域、保存した画像を見比べます。自動候補を作れたことと、公開前の確認を終えたことを区別するのが、顔を隠すツールでは大切です。