猫の手ツール

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

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

InteractiveSegmenterで指定した猫の領域を選ぶ

画像の中の猫をクリック・タップし、その場所の対象を領域として選んでみましょう。InteractiveSegmenterは、画像とユーザーの指定位置を手掛かりにマスクを返します。Web SDK 1.1.0の新しいAPIを、画像の準備と領域の推定に分けて学びます。

この記事でできるようになること

画面のクリック位置を正規化座標に変換し、setImageとsegmentを順に呼べる。マスクを色付けし、不要になったマスクを解放できる。

HTMLとJavaScriptの変数・関数・イベントの基本が分かる方を対象に、MediaPipeの学習済みモデルを使うところから進めます。自分でモデルを学習させる準備は不要です。

1. 何をしたいときに使うか・モデルを選ぶ

人物用ImageSegmenterは、モデルが用意した人物や髪などのカテゴリを推定します。一方、InteractiveSegmenterでは「ここにある対象」という位置の情報をユーザーが渡します。猫の画像でも、人物の画像でも、指定した点を含む物体の境界を推定する流れを試せます。カテゴリ名を出すObjectDetectorとは、目的と出力が異なります。

この教材は、指定した位置を中心に短い横線を渡す形にしています。新しいAPIでは点の列をストロークとして扱うため、1点だけの旧式のROI指定とは分けます。左側の入力画像をタップすると位置を変更できます。タップしただけでは推論せず、ボタンを押して実行します。キーボードで操作する場合はX・Yのスライダーを使います。初学者が「位置を変えた」「AIを実行した」「色付けした」の各段階を追えるように分けています。

2026年10月12日に確認した新しいSDKでは、画像を一度準備し、指定位置を変えて領域を再推定するAPIになっています。旧版の例にあるsegment(image, ROI)を、そのまま新しいクラスへ渡すことはできません。新APIのモデルはMagicTouch v2のint8バンドルで、本サンプルは固定版1を使います。

SDKとモデルをセットで選びます。人物と背景を分けるモデルのURLだけをMagicTouchへ変え、同じImageSegmenterのコードで動くと考えないようにします。タスクの作り方、入力、結果の受け方も異なります。比較は公式の新旧APIの説明を参照できます。最新と書かれたURLをコードに使わず、動作確認した版を固定することが再現性につながります。

2. まず完成したサンプルを動かす

次のデモを開き、サンプル画像を選んで「AIを読み込んで実行する」を押します。画像を選ぶだけではAIを実行しません。最初のボタン操作でSDK・WASM・モデルを取得し、次からは初期化済みのタスクを再利用します。

InteractiveSegmenterで指定した猫の領域を選ぶ:ブラウザでの実行画面。入力画像と、実際のモデルから得た結果・実行情報を表示。
本プロジェクトの画像を使い、上のサンプルをPCのChromeで実行したキャプチャです。SDK 1.1.0、MagicTouch v2 int8、長辺最大960 px。数値は画面に表示された実行結果で、端末や画像によって変わります。スマホの実機結果を示す画像ではありません。

今回の実行で観察したこと

最初に中心の1点だけを渡した実験では、閾値0.5以上の面積が画像全体の1.0%となり、猫の領域を選べませんでした。同じ画像・モデルのまま、中心(0.35, 0.55)を通る13点の短い横線へ変更すると、茶色い猫を含む領域が色付けされ、選択面積は36.1%、指定中心のマスク値は1になりました。条件はPCのChrome、GPU、SDK 1.1.0、MagicTouch v2 int8版1、入力800×589 pxです。

この結果を踏まえ、完全コードは短い線を渡す形にしています。APIが1点を禁止しているわけではなく、1点なら常に失敗するという評価でもありません。この1枚の写真では入力の手掛かりを増やすと結果が変わった、という実装時の観察です。選択面積は精度や猫の一致率ではありません。色付きの範囲と元画像を確認し、毛先や境界の修正が必要かも判断します。

中心を(0.70, 0.55)へ移して、同じ画像の灰色の猫で実行すると、選択面積は38.8%、指定中心のマスク値は1でした。画像を変更していないのでsetImage()は再利用し、線の位置を変えてsegment()を呼んでいます。2匹の間の境界は画像を見て確認し、希望通りに細部が分かれているかを判断します。

同じ愛猫写真の灰色の猫へ短い線の中心を移し、対象領域を実際に色付けした結果。選択面積38.8%。
同じ入力画像のまま指定中心をX70%・Y55%へ変更した実行キャプチャ。SDK・モデル・閾値は最初の実行と同じです。画像の準備を再利用し、指定位置によって対象が変わる様子を確認しました。

デモに同梱する人物はAI生成画像、猫は運営者の愛猫の写真です。任意のJPEG・PNG・WebPも読み込めますが、入力画像を推論用サーバーへ送信する処理はありません。SDKとモデルの取得は外部への通信を行います。時刻欄は初期化を除く推論・結果加工・描画の計測で、速度を保証する数字ではありません。

3. ZIPを展開して自分のPCで動かす

VS Codeでフォルダーを開く方法や、app.jsの編集・保存が初めてなら、導入記事のエディター準備と「保存 → ブラウザ更新 → 実行」の手順を先に確認してください。下では、このサンプルの起動に必要な操作を示します。

この教材はHTMLとJavaScriptだけで動く独立サンプルです。サイト全体のAstroプロジェクトをインストールする必要はありません。SDKをCDNから取得するため、起動時のインターネット接続は必要です。画像は推論用サーバーへ送信せず、ブラウザ内で処理します。

  1. 上のZIPをダウンロードします。WindowsではZIPを右クリックして「すべて展開」を選びます。
  2. 展開したフォルダーを開き、index.htmlとapp.js、person.jpg、cat.jpg、README.txtが並んでいることを確認します。さらに同じ名前のフォルダーがある場合は、その中へ進みます。
  3. エクスプローラーのアドレス欄へpowershellと入力してEnterを押します。このフォルダーを作業場所にした端末が開きます。
  4. 次のコマンドでNode.jsとnpmがあるか確認します。
node --version
npm.cmd --version

バージョンの数字が出れば準備できます。「認識されません」と出る場合はNode.js公式のダウンロードページからLTS版を導入し、端末を開き直します。Node.jsはこの例ではAIを実行するためではなく、HTTPサーバーを起動するために使います。

npx.cmd --yes http-server . -p 8080

初回はHTTPサーバーのツールを取得することがあります。端末にアドレスが表示され、そのまま入力を待つ状態になればサーバーが動いています。ブラウザでhttp://localhost:8080を開きます。画像の選択画面が出たら成功です。端末は実行中のまま残します。

Mac・Linuxはnpm --version、npx --yes http-server . -p 8080に読み替えます。Windowsの.cmdは、PowerShellのスクリプト実行制限に当たりにくい呼び出し方です。HTMLをダブルクリックしてfile://で開くとモジュールの読み込みが制限されることがあるため、HTTPで開きます。

8080が使用中ならコマンドのポートを8081へ変え、開くURLもhttp://localhost:8081に変えます。終了するときは端末でCtrl+Cを押します。コードを修正したら保存し、ブラウザを再読み込みしてから実行します。古い結果が残る場合は、開発者ツールのNetworkでキャッシュを無効にして再読み込みします。

4. AIの出力を自分のJavaScriptへつなぐ

最初にsetImage(inputCanvas)を呼びます。この時点で画像の特徴を準備するため、処理時間が掛かります。次にsegment(strokes)へ指定点の情報を渡します。画像を変えずに指定点だけ変える場合、サンプルはsetImageを繰り返さず、画像変更の番号を比較して必要なときだけ実行します。

strokesには、brushMode、point、isCompletedを入れます。この最小例は公式の型定義のBrushMode.POSITIVE = 1に対応する数値定数で「この線は対象に含めたい」と示し、pointに13点のx・yを渡します。線の横幅は画像幅の12%で、クリック・タップやスライダーはその中心を決めます。黄色い線が対象の内側に収まる場所へ合わせます。除外側の指示や複数の線へ広げる前に、この短い線でマスクが変わる様子を確認します。1.1.0のESモジュールはBrushModeを実行時の値として公開していないため、この完全コードはconst BRUSH_MODE_POSITIVE = 1を定義して使います。型定義にある名前と、ブラウザで読み込める値の違いにも注意します。

クリック位置は画面に表示されたCanvasのサイズで割ります。getBoundingClientRect()の左上を引いてから、表示幅・表示高さで割ると、CSSで縮小していても0〜1の位置へ変換できます。Canvasの内部ピクセル幅で、画面上のクリック値を直接割るとスマホでずれやすくなります。

結果はMPMaskです。getAsFloat32Array()で画素ごとの値を読み、この例は0.5以上の画素だけへ半透明の青緑を重ねます。0.5という境界は表示ルールです。輪郭のぎりぎりの部分を自然に透過させる場合は、連続した値をアルファへ変換するなど、別の加工が必要です。

マスクの幅・高さはmask.width、mask.heightを使います。出力の画素数を入力の幅だと決めつけず、マスクサイズのCanvasで色を作り、入力サイズへ描画します。データを読んで描画した後はmask.close()で解放します。JSONでは数十万の画素をすべて表示せず、サイズ・選択割合・指定中心の値・先頭20値を要約しています。処理が完了しただけで正しい領域が選べたとは判断せず、指定中心が閾値以上かと、色付けした範囲が対象に合うかも確認します。

次は処理の中心だけを抜き出した例です。inputCanvasやタスクの変数は、すでに初期化済みとします。この数行だけをHTMLへ貼ると動くという意味ではありません。実際に動かすコードは次の完全版を使い、中心となるAPIをこの短い例で確認します。

// SDK 1.1.0の型定義でPOSITIVEは1。実行時のenum exportはないため定数を使う
const BRUSH_MODE_POSITIVE = 1;
segmenter.setImage(inputCanvas);
// 正規化座標で、猫の顔の内側を通る短い横線を作る
const points = Array.from({ length: 13 }, (_, index) => ({
  x: 0.29 + index * 0.01,
  y: 0.55,
}));
const mask = segmenter.segment([{
  brushMode: BRUSH_MODE_POSITIVE,
  point: points,
  isCompleted: true,
}]);
try {
  const values = mask.getAsFloat32Array();
  // mask.width・mask.heightを使い、valuesを色や透明度へ変換する
} finally {
  mask.close();
}

5. 完全なコードを確認する

index.htmlは操作画面とCanvasを用意し、app.jsは画像選択・モデル初期化・推論・描画を担当します。画像を読み込むawait image.decode()の後にCanvasへ描き、読み込み前の画像を推論へ渡さないようにしています。GPUの初期化が失敗した場合はCPUで再試行し、両方失敗した場合は理由を画面とConsoleへ表示します。

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>InteractiveSegmenterで指定した猫の領域を選ぶ | 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; } }
    
#summary{white-space:pre-line;overflow-wrap:anywhere}select{font:inherit;max-width:100%;padding:8px;margin:10px}input[type="range"]{display:block;width:100%;max-width:400px}#input-canvas{touch-action:manipulation}</style></head><body><main><header><p class="eyebrow">MEDIAPIPE · JAVASCRIPT · IMAGE</p><h1>InteractiveSegmenterで指定した猫の領域を選ぶ</h1><p>画像をクリック・タップして対象の位置を指定し、実行ボタンを押します。指定位置を中心とした短い線を、対象に含める手掛かりとして渡します。新しいAPIのsetImageとsegmentで領域マスクを得て、青緑の色で重ねます。</p><p class="note">人物はプロジェクト内のAI生成画像、猫は運営者の愛猫です。画像はブラウザ内で処理し、推論用サーバーへ送信しません。初回は外部CDNとGoogleからSDK・モデルを取得します。</p></header><section class="panel"><h2>1. 画像を選んで実行する</h2><fieldset><legend>サンプル画像</legend><div class="choices"><label class="choice"><input type="radio" name="sample" value="person" >AI作成の人物</label><label class="choice"><input type="radio" name="sample" value="cat" checked>愛猫の写真</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まで。長辺960 pxへ縮小します。</p><p>入力画像をクリック・タップして指定する短い線の中心を移動できます。黄色い線を対象の内側へ合わせます。キーボードでは次のスライダーを使います。</p><label>指定点のX(%) <input id="point-x" type="range" min="0" max="100" value="35" /></label><label>指定点のY(%) <input id="point-y" type="range" min="0" max="100" value="55" /></label><div class="actions"><button id="run-button" disabled>AIを読み込んで実行する</button></div><p id="status" role="status" aria-live="polite" aria-atomic="true">画像を読み込んでいます。</p><noscript>JavaScriptを有効にしてください。</noscript></section><section class="panel"><h2>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><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">時間はモデル初期化を除き、推論と結果の加工・描画を含みます。端末によって変わり、性能保証ではありません。</p><p id="summary">まだ実行していません。</p></section><section class="panel"><h2>3. 出力JSONを読む</h2><details open><summary>結果のデータ(小数は表示用に丸めています)</summary><pre id="json-output">まだ実行していません。</pre></details></section><footer><p>SDK:@mediapipe/tasks-vision 1.1.0 / モデル:MagicTouch v2 int8</p><p><a href="./README.txt">起動方法・画像出典</a></p></footer></main><script type="module" src="./app.js"></script></body></html>
app.jsの全文を開く
// SDK・WASMは同じ固定版にそろえます。モデル版は別に管理します。
const CONFIG = {
  "slug": "mediapipe-interactive-segmentation",
  "title": "InteractiveSegmenterで指定した猫の領域を選ぶ",
  "task": "InteractiveSegmenter",
  "version": "1.1.0",
  "model": "https://storage.googleapis.com/mediapipe-models/interactive_segmenter_v2/magic_touch/int8/1/interactive_segmentation.task",
  "modelName": "MagicTouch v2 int8",
  "type": "interactive",
  "method": "segment",
  "options": {},
  "default": "cat",
  "result": "指定点を含む領域の色付け",
  "intro": "画像をクリック・タップして対象の位置を指定し、実行ボタンを押します。新しいAPIのsetImageとsegmentで領域マスクを得て、青緑の色で重ねます。"
};

// 1.1.0の型定義: BrushMode.POSITIVE = 1。ESモジュールにはBrushMode値のexportがないため数値定数を使います。
const BRUSH_MODE_POSITIVE = 1;
const $ = selector => document.querySelector(selector);
const input = $('#input-canvas');
const output = $('#result-canvas');
const ctx = output.getContext('2d');
const inputCtx = input.getContext('2d');
const runButton = $('#run-button');
let task = null,
  sdk = null,
  delegate = '',
  imageReady = false,
  busy = false,
  selection = 0,
  encodedSelection = -1;
const status = (message, error = false) => {
  $('#status').textContent = message;
  $('#status').dataset.error = String(error);
};
function controls() {
  runButton.disabled = busy || !imageReady;
  for (const element of document.querySelectorAll('input, select')) element.disabled = busy;
  runButton.textContent = busy ? '処理しています…' : 'AIを読み込んで実行する';
}
async function loadImage(source, name) {
  const request = ++selection;
  imageReady = false;
  controls();
  $('#count').textContent = '未実行';
  $('#elapsed').textContent = '未実行';
  $('#summary').textContent = 'まだ実行していません。';
  $('#json-output').textContent = 'まだ実行していません。';
  const url = source instanceof File ? URL.createObjectURL(source) : null;
  try {
    const image = new Image();
    image.src = url ?? source;
    await image.decode();
    if (request !== selection) return;
    const scale = Math.min(1, 960 / Math.max(image.naturalWidth, image.naturalHeight));
    input.width = output.width = Math.max(1, Math.round(image.naturalWidth * scale));
    input.height = output.height = Math.max(1, Math.round(image.naturalHeight * scale));
    inputCtx.drawImage(image, 0, 0, input.width, input.height);
    ctx.drawImage(input, 0, 0);
    $('#dimensions').textContent = image.naturalWidth + ' × ' + image.naturalHeight + ' → ' + input.width + ' × ' + input.height + ' px';
    input.setAttribute('aria-label', name + 'の入力画像');
    imageReady = true;
    drawPoint();
    status(name + 'を選びました。実行ボタンを押してください。');
  } catch (error) {
    console.error(error);
    status('画像を読み込めませんでした。JPEG・PNG・WebPを選び直してください。', true);
  } finally {
    if (url) URL.revokeObjectURL(url);
    if (request === selection) controls();
  }
}
async function initialize() {
  if (task) return task;
  status('初回の準備中:SDK・WASM・モデルをインターネットから取得しています。');
  const base = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@' + CONFIG.version;
  sdk = await import(base + '/vision_bundle.mjs');
  const vision = await sdk.FilesetResolver.forVisionTasks(base + '/wasm');
  const create = hardware => sdk[CONFIG.task].createFromOptions(vision, {
    ...CONFIG.options,
    baseOptions: {
      modelAssetPath: CONFIG.model,
      delegate: hardware
    }
  });
  try {
    task = await create('GPU');
    delegate = 'GPU';
  } catch (error) {
    console.warn('GPUの初期化失敗。CPUで再試行します。', error);
    status('GPUを初期化できませんでした。CPUで再試行しています。');
    task = await create('CPU');
    delegate = 'CPU';
  }
  $('#delegate').textContent = delegate;
  return task;
}
function point() {
  return {
    x: Number($('#point-x').value) / 100,
    y: Number($('#point-y').value) / 100
  };
}
function strokePoints(p = point()) {
  return Array.from({
    length: 13
  }, (_, index) => ({
    x: Math.max(0.001, Math.min(0.999, p.x - 0.06 + index * 0.01)),
    y: p.y
  }));
}
function drawHint(p = point()) {
  const points = strokePoints(p);
  ctx.strokeStyle = '#ffcf44';
  ctx.lineWidth = Math.max(4, output.width / 160);
  ctx.beginPath();
  points.forEach((item, index) => {
    if (index === 0) ctx.moveTo(item.x * output.width, item.y * output.height);else ctx.lineTo(item.x * output.width, item.y * output.height);
  });
  ctx.stroke();
  ctx.fillStyle = '#ffcf44';
  ctx.strokeStyle = '#102a29';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.arc(p.x * output.width, p.y * output.height, 7, 0, Math.PI * 2);
  ctx.fill();
  ctx.stroke();
}
function drawPoint() {
  inputCtx.save();
  // 入力Canvasを書き換えると推論対象にも印が入るため、印は結果側だけに描きます。
  ctx.drawImage(input, 0, 0);
  const p = point();
  ctx.fillStyle = '#ffcf44';
  ctx.strokeStyle = '#102a29';
  ctx.lineWidth = 3;
  drawHint(p);
  inputCtx.restore();
}
async function processImage() {
  const engine = await initialize();
  ctx.drawImage(input, 0, 0);
  let result, count, summary;
  const started = performance.now();
  if (encodedSelection !== selection) {
    engine.setImage(input);
    encodedSelection = selection;
  }
  const p = point();
  const mask = engine.segment([{
    brushMode: BRUSH_MODE_POSITIVE,
    point: strokePoints(p),
    isCompleted: true
  }]);
  try {
    const values = mask.getAsFloat32Array();
    const maskCanvas = document.createElement('canvas');
    maskCanvas.width = mask.width;
    maskCanvas.height = mask.height;
    const maskCtx = maskCanvas.getContext('2d');
    const pixels = maskCtx.createImageData(mask.width, mask.height);
    let selected = 0;
    values.forEach((value, i) => {
      pixels.data[i * 4] = 0;
      pixels.data[i * 4 + 1] = 206;
      pixels.data[i * 4 + 2] = 184;
      pixels.data[i * 4 + 3] = value >= .5 ? 125 : 0;
      if (value >= .5) selected++;
    });
    maskCtx.putImageData(pixels, 0, 0);
    ctx.drawImage(maskCanvas, 0, 0, output.width, output.height);
    count = (selected / values.length * 100).toFixed(1) + '%';
    summary = '閾値0.5以上の領域を色付け。選択した割合:' + count;
    const centerX = Math.min(mask.width - 1, Math.round(p.x * (mask.width - 1)));
    const centerY = Math.min(mask.height - 1, Math.round(p.y * (mask.height - 1)));
    result = {
      point: p,
      strokePoints: strokePoints(p),
      pointConfidence: values[centerY * mask.width + centerX],
      maskWidth: mask.width,
      maskHeight: mask.height,
      threshold: 0.5,
      selectedPixels: selected,
      totalPixels: values.length,
      first20ConfidenceValues: Array.from(values.slice(0, 20))
    };
  } finally {
    mask.close();
  }
  drawHint(p);
  $('#elapsed').textContent = (performance.now() - started).toFixed(1) + ' ms';
  $('#count').textContent = String(count);
  $('#summary').textContent = summary;
  $('#json-output').textContent = JSON.stringify(result, (_key, value) => ArrayBuffer.isView(value) ? Array.from(value) : typeof value === 'number' ? Number(value.toFixed(5)) : value, 2);
  output.setAttribute('aria-label', CONFIG.result + '。' + summary);
  status(result.pointConfidence < .5 ? '処理は完了しましたが、指定した中心は閾値0.5の領域に含まれていません。別の位置を選んで再実行し、色付き領域が対象と合うか確認してください。' : '実行が完了しました。画像・数値・JSONを確認してください。');
}
runButton.addEventListener('click', async () => {
  if (busy || !imageReady) return;
  busy = true;
  controls();
  try {
    await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
    await processImage();
  } catch (error) {
    console.error(error);
    status('実行に失敗しました:' + error.message + '。通信とConsoleを確認し、SDK・WASM・モデルの取得状況を調べてください。', true);
  } finally {
    busy = false;
    controls();
  }
});
for (const radio of document.querySelectorAll('input[name="sample"]')) radio.addEventListener('change', () => {
  $('#image-file').value = '';
  void loadImage('./' + radio.value + '.jpg', radio.value === 'cat' ? '愛猫の写真' : 'AI作成の人物');
});
$('#image-file').addEventListener('change', () => {
  const file = $('#image-file').files[0];
  if (!file) return;
  if (file.size > 20 * 1024 * 1024 || !['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
    status('JPEG・PNG・WebP、20 MB以下を選んでください。', true);
    return;
  }
  for (const radio of document.querySelectorAll('input[name="sample"]')) radio.checked = false;
  void loadImage(file, file.name);
});
for (const slider of document.querySelectorAll('input[type="range"]')) slider.addEventListener('input', () => {
  drawPoint();
  status('指定点を変更しました。実行ボタンを押して領域を更新してください。');
});
input.addEventListener('pointerdown', event => {
  if (busy || !imageReady) return;
  const rect = input.getBoundingClientRect();
  $('#point-x').value = Math.max(0, Math.min(100, (event.clientX - rect.left) / rect.width * 100));
  $('#point-y').value = Math.max(0, Math.min(100, (event.clientY - rect.top) / rect.height * 100));
  drawPoint();
  status('指定点を変更しました。実行ボタンを押してください。');
});
addEventListener('pagehide', () => {
  task?.close();
  task = null;
  encodedSelection = -1;
});
void loadImage('./' + CONFIG.default + '.jpg', CONFIG.default === 'cat' ? '愛猫の写真' : 'AI作成の人物');

最初は全文を暗記せず、CONFIGでモデルと設定を確認し、initialize()でタスクを作り、processImage()で結果を読む順番で追います。UIの状態管理は、AIの推論と別の役割です。実行中に画像が変わらないようにする処理も、公開するアプリには必要になります。

6. 1つずつ条件を変えて理解する

  1. 愛猫の写真の初期位置で実行し、どの範囲が青緑になるか見ます。
  2. 別の猫の中心へ指定点を移し、実行ボタンを押して結果を比較します。同じ猫写真のため画像の準備は再利用します。
  3. 猫の輪郭ぎりぎり、背景、2匹が重なる部分へ点を移し、選択がどう変わるか調べます。
  4. スライダーでも同じ位置を指定し、クリック操作と座標の意味が一致するか確認します。

2匹が接している写真では、1匹だけに完全に分かれるとは限りません。点を変えても希望通りにならない部分は、マスクの描画バグと推定の限界を分けて調べます。選択領域の色付けが正しければ、次は複数の線や除外指示、手動修正のUIを検討する順番です。

7. このサイトのツールへどうつながるか

サイトのヘアカラー試着はURLの分類名にint-segを含みますが、実装はImageSegmenterと髪用モデルです。この記事のInteractiveSegmenterの実装例として、その既存ツールを扱うことはできません。初心者がURL名だけでAPIを選ばないように、実際のクラスとモデルを区別します。

この新しいサンプルは、愛猫の領域を手掛かりから選ぶ処理を学ぶためのものです。背景交換や色変更へつなげる場合は、返ったマスクをCanvasの合成やアルファへ変換します。AIが切り抜き済みPNGを直接返すわけではなく、保存形式や境界の処理はアプリで作ります。

出力を使う前に確認する限界

毛先、ひげ、重なる猫などの細部を完璧に分けるとは限りません。モデルは約30.5 MBあり、初回の通信待ちが長くなる場合があります。新しいAPIの画像準備も同期処理なので、重い入力ではUIが一時的に止まることがあります。

8. 動かない・結果が違うときの調べ方

このタスクの出力と、確認する場所
状態先に確認すること
指定しても小さな領域しか選ばれない今回の観察では1点と13点の短い線で結果が変わりました。入力の手掛かりとマスクを見比べ、対象の内側を指定します。
タップ位置と指定点がずれるclientX・clientYから表示Canvasの左上を引き、表示幅・高さで割って正規化します。内部ピクセル幅とは区別します。
点を変えても前の色が残る位置変更だけでは推論しません。実行ボタンを押してsegment()を呼び、同じ画像ではsetImage()の準備を再利用します。
モデルとAPIの組み合わせでエラーSDK 1.1.0とMagicTouch v2の固定モデルを使います。旧segment(image, ROI)の呼び出しを混ぜないようにします。
画面・入力画像を開けないHTTPサーバーのフォルダーとURLを確認し、Networkで同梱画像が404になっていないか見ます。
SDK・モデルを読み込めないSDK・WASM・モデルの固定URLをNetworkで確認します。GPU初期化失敗時はCPUで再試行し、両方失敗した場合はConsoleを読みます。

ChromeやEdgeではF12で開発者ツールを開けます。Consoleに出た最初のエラーと、Networkの失敗したURLを確認します。モデルの読み込み失敗なのにスコアの閾値を変更しても直りません。画像読み込み、モデル初期化、推論、描画の順に分けて調べます。

サンプルでは実行中のボタンと画像変更を無効にし、同時に処理が走らないようにしています。画像ファイルのObject URLは読み込み後に解放し、ページから離れるときはタスクのclose()を呼びます。スマホの画面幅では結果を縦に並べます。PCの表示幅を狭める確認は実機検証の代わりにはならないので、端末・OS・ブラウザを変える際は別途試します。

9. サイトの採用版と最新のMediaPipe事情

現在の猫の手ツールのTasks Visionは0.10.34です。SDKは推論のAPIと実行環境、モデルは学習済みのデータなので、バージョンを別々に管理します。この記事のモデルは固定版1で、コードに/latest/は使っていません。公式の例にlatestがあっても、教材を再現する場合は確認したURLを固定します。

この独立サンプルだけは1.1.0を固定して使います。新しいInteractiveSegmenterとMagicTouch v2モデルの組み合わせを学ぶためです。旧0.10.xのsegment(image, ROI)の例は、新APIと区別します。既存サイトの依存パッケージ・WASM・モデルを更新する変更ではありません。

SDKのファイルとWASMだけを別の版にすると、APIが読み込めても実行時に問題が出る場合があります。また、旧MediaPipe SolutionsとTasksでは使うパッケージ・クラス・設定が異なります。検索で見つけたコードをコピーする前に、記事がどの版のどのタスクを扱うか確認します。

参考資料

確認日:2026年10月12日。本文は公式API・プロジェクトの実装と、本教材のデモに基づいて説明しています。モデルの推定結果を人の判断の代わりに確定した情報として扱わず、画像とJSONを見比べて検証します。

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