# 画像認識と物体検出を学び直す Log#12 | 画像認識の答えの形を、ロボットアームの写真1枚で見せる

- 記録: LOG459（画像認識と物体検出）
- 公開: 2026/11/7
- 記事のページ: https://pixelstomotors.com/record/arm-photo-six-vision-outputs
- サイト: Pixels to Motors（ADAPT DESIGN / 島津 栄一） https://pixelstomotors.com/
- 要約: 画像認識と物体検出は返す答えの形で6つに分かれ、同じロボットアームの写真でも答えが違う。真上から撮った mechArm 270 の実写に6つを重ね、深度は Depth Anything V2 で推し、追跡は実機の録画から取った。

> AI が読むための記事の全文です（コード・実行ログを含む）。引用するときは上の「記事のページ」の住所を使ってください。

---

**画像認識と物体検出は、返す答えの形で6つに分かれる。同じロボットアーム（mechArm 270）の写真1枚に重ねても、名前を1つ付ける分類、四角い枠の物体検出、形を塗り分けるセグメンテーション、関節に点を打つ姿勢推定、同じ番号を振り続ける追跡、距離を返す深度で、答えはまったく違う絵になる。** 真上のカメラで撮った机の上のアームの実写に6つの答えを重ね、深度は写真1枚から距離を推すモデル Depth Anything V2 で実際に推し、追跡は実機の録画から赤いブロックの動きを取り出して、このシリーズのページ pixelstomotors.com/series7 に載せた。

ページに足したもので、次のことが変わった。

- シリーズのページを開いた人が、記事を1本も読まずに、このシリーズで何をしているか（下から積む5段と、それぞれの回）と、工場・物流・店のロボットなど6つの現場でどの技術が使われているかを見渡せる
- 答えの形を説明する6枚の絵が、灰色の面に四角や丸を置いた記号の絵から、実物の写真と実際の計算の結果に変わった。深度の絵は、モデルが推した値をそのまま白黒にしたもの
- 6枚はスクリプト5本で作り直せる。2コアの CPU で通して約46秒（深度の推定が約16秒、録画1,024コマの追跡が約29秒）。流し直した6枚は、公開中の絵とバイト単位で同じだった

ページは、タイトルと説明文の下にいきなり記録の一覧が並ぶ形だった。初めて来た人には、何を学び直しているのか、それが工場や店のロボットとどうつながるのかが分からない。そこで見出しの下に3つの解説を足した。最初に作った答えの形の6枚は記号の絵で、出してすぐに次の指摘を受けた。

> ここのイラストのクオリティが低いので別の提案をください。

何を主役にするかの違う3案（実写に重ねる・答えのデータを見せる・人の手間を比べる）を作り、実写の案に決めた。作り直しの途中で、追跡が思ったより素直に動かないことも分かった。アームの手に貼った青い印を追うと、手首が回って印が隠れたとたん、指先の別の青い印に飛び移る。赤い物を追うと、アームに這わせた赤い配線までつかまえる。最後は、塊の大きさで絞って赤いブロックだけを追った。

| やったこと | なぜ | 結果 |
|---|---|---|
| series7 の見出しの下に、やっていること・答えの形・現場の技術の3つを足した | 初めて来た人が、記事を読まずにシリーズの中身と現場とのつながりを見渡せるように | 下から積む5段（13本の回へのリンク）、6つの答えの形、6つの現場を載せた |
| 答えの形の絵を、方向の違う3案から選び直した | 記号の絵では、答えの形の違いが伝わらなかった | 実写に重ねる案Aに決めた |
| 真上のカメラの写真1枚から、5つの答えを作った | 同じ写真で答えだけが違うことを見せるため | アームと2つのブロックの形を取り出し、枠・塗り分け・点・距離を重ねた |
| 実機の録画から、赤いブロックに同じ番号を振り続けた | 追跡は動画がないと描けない | 622〜705コマ目（20.8〜23.6秒）の84コマで、ブロックが箱へ運ばれる道を描いた |
| 作り方を GitHub に残し、流し直した | 素材を替えても同じ手順で作り直せるように | 6枚ともバイト単位で同じ絵が出た |


## 作業ログ ― series7 のページの見出しの下に3つの解説を足す

依頼はこうだった。

> 画像認識と物体検出のページを充実させたいです。タイトルと説明文の下にどんなことをおこなっているか、また画像認識と物体検出にはどんな技術が現場で使われているかの解説を追加してください。

このサイトは pixelstomotors.com で、GitHub のリポジトリ PixelstoMotors/p2m-site の main に push すると約1分で公開される。シリーズのページは `src/pages/[series].astro` が1つで8シリーズ分を作り、シリーズごとの設定は `src/lib/records.ts` の `SERIES_PAGES` にある。series7 だけに解説を足すので、部品 `src/components/VisionGuide.astro` を新しく作り、設定に `vision: true` を足して、その印があるシリーズだけに部品を出す形にした。

### 3つのセクションで伝えることを1つずつ決めた

デザインの決まり（1つのセクションで伝えることは1つ、要らない要素は入れない）に合わせ、作る前にセクションごとの伝えることを決めた。

| セクション | 伝えること | 中身 |
|---|---|---|
| このシリーズでやっていること | 画素の数字から新しいモデルまで、下から順に積んでいる | 5段。段ごとに、測ったことを1文と、その回へのリンク |
| 何を出すかで、技術が分かれる | 同じ写真でも、返す答えの形が違う | 分類・物体検出・セグメンテーション・姿勢推定・追跡・深度の6つ |
| 現場で使われている技術 | 技術は新しさではなく、照明と置き場所をどこまで固定できるかと、見落としと見間違いのどちらが困るかで選ばれる | 6つの現場ごとに、見たいもの・使われる技術・関連の回 |

下から積む5段は、公開済みの13本の回をそのまま並べた。

| 段 | 測ったこと | 回 |
|---|---|---|
| 01 画素と色を測る | 同じ赤 (255, 0, 0) が読み込む道具で青と入れ替わる。オレンジの照明で青いブロックが色を失う | LOG299・LOG300・LOG301 |
| 02 人が条件を書く手法を、1つずつ壊してみる | しきい値・モルフォロジー・輪郭の特徴量・特徴点マッチング・射影変換。特徴点マッチングは、対象が写っていない写真とも867件の対応を付けた | LOG302・LOG304・LOG309・LOG310 |
| 03 物差しを先に決める | IoU・適合率・再現率 | LOG298 |
| 04 学習で条件を決めるモデルの言葉を積み直す | モデル・学習・推論・バックボーン・ゼロショット、YOLO26・DINOv3・SAM 3・単眼深度の位置 | LOG313 |
| 05 新しいモデルを、ロボットの作業に当てる | SAM 3 はガラスかプラスチックかを見分けなかった。鏡像も切り出した。Decisions API は真上のカメラでは持ち上がったかを判定できなかった | LOG317・LOG236・LOG325 |

現場の6つは次のとおり。工場の外観検査の「照明が結果を先に決める」は LOG319（AI外観検査は撮り方で決まる）と LOG301 に、店や施設を動くロボットは、配膳ロボットの詰まりを録画の異常検知で拾った LOG418 につないだ。

| 現場 | 見たいもの | 使われる技術（ページに書いたもの） |
|---|---|---|
| 工場の外観検査 | 傷・欠け・汚れ | しきい値、パターンマッチング、良品だけで学ぶ異常検知（PatchCore など） |
| 物流の仕分けとピッキング | 箱や袋を1つずつつかむ | 3Dカメラ、インスタンスセグメンテーション、バーコード・文字の読み取り |
| 店や施設を動くロボット | 人と障害物をよける | LiDAR・深度カメラ、物体検出（YOLO 系）、追跡 |
| ロボットアームの作業 | 物の位置と向き | マーカー・特徴点、射影変換、画像から関節の動きまで出すロボット用の AI（Physical Intelligence の π0.5 など） |
| 人の作業を見る | 姿勢と動きを数える | 姿勢推定、追跡 |
| 学習データづくり | 正解の下書きを作る | 言葉で探すモデル（SAM 3 など）、アノテーション（画像に正解の枠や輪郭を付ける作業） |

- ねらい：シリーズのページを、記録の一覧から、初めて来た人がシリーズの中身と現場とのつながりを1ページで見渡せる入口にする
- 変わったこと：見出しの下に3つのセクションを足し、13本の回へのリンクと6つの現場を本文の文字で載せた（絵だけにせず、AI がページを読んでも中身が取れる形）
- 見込める効果：シリーズのページから記事へ移る人と、AI がこのページを答えに使う回数が増える見込み。サイトの計測（AI の利用者の依頼で読みに来た数）で確かめる。効果は公開から間もないので、まだ測っていない

### records.ts と [series].astro に足した3行

`src/lib/records.ts` の型に `vision?: boolean;` を足し、series7 の設定に `vision: true,` を足した。

```diff
-export const SERIES_PAGES: Record<string, { series: string; title: string; pageTitle: string; limit: number; intro?: string; points?: { h: string; items: string[] }[]; guide?: boolean; thinking?: boolean; serving?: boolean; sim?: ...
+export const SERIES_PAGES: Record<string, { series: string; title: string; pageTitle: string; limit: number; intro?: string; points?: { h: string; items: string[] }[]; guide?: boolean; thinking?: boolean; serving?: boolean; vision?: boolean; sim?: ...
-  series7: { series: '画像認識・物体検出', title: 'Image recognition and Object detection | 画像認識と物体検出', pageTitle: 'Series7 | 画像認識と物体検出', limit: 12,
+  series7: { series: '画像認識・物体検出', title: 'Image recognition and Object detection | 画像認識と物体検出', pageTitle: 'Series7 | 画像認識と物体検出', limit: 12, vision: true,
```

`src/pages/[series].astro` には、部品の読み込みと、見出しのセクションの直後に出す1行を足した。

```diff
 import SeriesDoc from '../components/SeriesDoc.astro';
+import VisionGuide from '../components/VisionGuide.astro';
 ...
   </div></section>
+  {conf.vision && <VisionGuide />}
   {conf.sim && (
```

### VisionGuide.astro（全文・最初の記号の絵の版・commit 11d8b14）

最初の版では、答えの形の6枚を SVG の記号で描いた。灰色の面に、分類は黒い四角と「良品」の札、検出は2つの四角と0.92・0.87の数字、深度は3段の灰色の帯。

```astro
---
// /series7（画像認識と物体検出）の見出しの下に置く解説（2026-11-07）
// 1 このシリーズでやっていること（下から積む5段と、その回）
// 2 何を出すかで分かれる（分類・検出・セグメンテーション・姿勢推定・追跡・深度）
// 3 現場で使われている技術（現場ごとに、何を見たいか・何が使われるか・関連の回）
// 文字は AI も読むので、本文の文字で書く（絵だけにしない）
type Link = { log: string; slug: string; t: string };
const steps: { h: string; p: string; links: Link[] }[] = [
  { h: '画素と色を測る',
    p: '同じ赤 (255, 0, 0) が、読み込む道具によって青と入れ替わる。オレンジの照明の下では、青いブロックが色を失う。検出が揺れたとき、原因がモデルより手前の画素の段階にあることを先に確かめています。',
    links: [
      { log: 'LOG299', slug: 'where-pixel-values-change', t: '画素の数字は、読むと・保存すると・撮ると変わる' },
      { log: 'LOG300', slug: 'hue-holds-color-breaks', t: 'オレンジの光の下で青いブロックが色を失う' },
      { log: 'LOG301', slug: 'rgb-hsv-threshold-lighting-failure', t: '光が変わると、赤検出が青を拾う' },
    ] },
  { h: '人が条件を書く手法を、1つずつ壊してみる',
    p: 'しきい値で白と黒に分け、モルフォロジーで小さな粒を消し、輪郭から面積や円形度を出し、特徴点で2枚の写真を対応させ、射影変換で斜めの紙を真上から見た形に直す。寸法の分かっている印刷物で、どの段がどう壊れるかを測りました。特徴点マッチングは、対象が写っていない写真とも867件の対応を付けています。',
    links: [
      { log: 'LOG302', slug: 'morphology-kernel-fits-or-vanishes', t: 'モルフォロジー演算' },
      { log: 'LOG304', slug: 'what-circularity-actually-measures', t: '重心・面積・外接矩形・円形度' },
      { log: 'LOG309', slug: 'same-place-different-object', t: '特徴点マッチング' },
      { log: 'LOG310', slug: 'tilted-plane-to-flat', t: '射影変換' },
    ] },
  { h: '物差しを先に決める',
    p: 'IoU は、出した枠と正解の枠がどれだけ重なっているか。1.0 で完全に一致します。適合率は、見つけたもののうち正しかった割合で、低いと見間違いが多い。再現率は、あるべきもののうち見つけられた割合で、低いと見落としが多い。物差しがないままモデルを替えても、良くなった気がする、までしか言えません。',
    links: [{ log: 'LOG298', slug: 'image-recognition-object-detection-overview', t: '9つの層と、評価指標を先に学ぶ理由' }] },
  { h: '学習で条件を決めるモデルの言葉を積み直す',
    p: 'モデル、学習、推論、バックボーン、ゼロショットといった言葉を4段に分けて定義し直し、YOLO26、DINOv3、SAM 3、写真1枚から距離を推す単眼深度が、それぞれどこに立つ技術なのかを整理しました。',
    links: [{ log: 'LOG313', slug: 'vocabulary-of-modern-vision', t: '用語を下から積み直す' }] },
  { h: '新しいモデルを、ロボットの作業に当てる',
    p: 'SAM 3 は物の名前と色では絞り込めたのに、ガラスかプラスチックかは見分けなかった。ステンレスに映った像まで物として切り出した。OpenAI の Decisions API は、爪の間にブロックがあるかははっきり見分けたが、真上のカメラでは持ち上がったかを判定できなかった。',
    links: [
      { log: 'LOG317', slug: 'sam3-first-look-sorting-line', t: 'SAM 3 はガラス瓶とペットボトルを同じ物として切り出した' },
      { log: 'LOG236', slug: 'sam3-mirror-reflection-grasp-problem', t: 'ロボットの目は、鏡像もあると見る' },
      { log: 'LOG325', slug: 'decisions-api-grasp-check', t: 'Decisions API は、持ち上がったかを判定できなかった' },
    ] },
];

// 出力の形。pic は小さな絵の種類
const outputs: { name: string; en: string; p: string; pic: string }[] = [
  { name: '分類', en: 'Classification', pic: 'cls', p: '写真1枚に、名前を1つ返す。この写真は良品、この写真は不良品、のように。' },
  { name: '物体検出', en: 'Detection', pic: 'det', p: '物ごとに、四角い枠と名前と確からしさを返す。何がいくつ、どこにあるかが分かる。' },
  { name: 'セグメンテーション', en: 'Segmentation', pic: 'seg', p: '画素ごとに、どの物かを塗り分ける。枠ではなく形が分かるので、つかむ場所を決められる。' },
  { name: '姿勢推定', en: 'Pose estimation', pic: 'pose', p: '関節や角など、決まった点の位置を返す。人の動きや、物の向きが分かる。' },
  { name: '追跡', en: 'Tracking', pic: 'trk', p: '動画の中で、同じ物に同じ番号を振り続ける。どこから来てどこへ行ったかが分かる。' },
  { name: '深度', en: 'Depth', pic: 'dep', p: 'カメラから物までの距離を返す。アームの手をどの高さまで下ろせばいいかが分かる。' },
];

const fields: { place: string; h: string; p: string; tags: string[]; links: { href: string; t: string }[] }[] = [
  { place: '工場の外観検査', h: '傷・欠け・汚れを見つける',
    p: '照明とカメラの位置を固定できるラインでは、しきい値やパターンマッチングのような人が条件を書く手法がいまも主力です。速く、なぜ不良としたかを説明できるからです。傷の形が毎回違い、不良品の写真がなかなか集まらない場合は、良品の写真だけで学習して良品と違う所を拾う異常検知（PatchCore など）が使われます。どちらの方式でも、結果を先に決めるのは照明の当て方です。',
    tags: ['しきい値', 'パターンマッチング', '異常検知'],
    links: [{ href: '/record/ai-visual-inspection-lighting', t: 'LOG319 AI外観検査は撮り方で決まる' }, { href: '/record/rgb-hsv-threshold-lighting-failure', t: 'LOG301 光が変わると、赤検出が青を拾う' }] },
  { place: '物流の仕分けとピッキング', h: '箱や袋を1つずつつかむ',
    p: 'ばら積みの箱や袋をロボットがつかむ現場では、距離が分かる3Dカメラと、物を1つずつ塗り分けるインスタンスセグメンテーションを組み合わせて、つかむ点を決めます。品物の種類が多く入れ替わりも激しいので、品物ごとに学習し直さなくても形を切り出せるモデルが求められています。荷札のバーコードや文字の読み取りは、昔からの画像処理が担っています。',
    tags: ['3Dカメラ', 'インスタンスセグメンテーション', 'バーコード・文字の読み取り'],
    links: [{ href: '/record/sam3-first-look-sorting-line', t: 'LOG317 分別ラインを想定して SAM 3 を試した' }] },
  { place: '店や施設を動くロボット', h: '人と障害物をよける',
    p: '配膳や搬送のロボットは、LiDAR や深度カメラで距離を測って障害物をよけ、カメラの物体検出で人を見分けます。動きながら判断するので、1段階で枠を出す速い検出モデル（YOLO 系など）がよく使われます。人を見落とす方が危ないので、見間違いが少し増えても再現率を優先して調整します。',
    tags: ['LiDAR・深度カメラ', '物体検出（YOLO 系）', '追跡'],
    links: [{ href: '/record/serving-robot-jam-detection-without-path-lines', t: 'LOG418 配膳ロボットの詰まりを、録画の異常検知で拾う' }] },
  { place: 'ロボットアームの作業', h: '物の位置と向きを知る',
    p: '決まった部品を決まった場所に置くアームでは、マーカーや特徴点で位置を合わせ、射影変換で座標をそろえる方式が長く使われてきました。近年は、カメラの画像を直接見て関節の動きまで出すロボット用の AI（Physical Intelligence の π0.5 など）が出てきて、物を見つける処理と動きを決める処理の境目が薄れつつあります。',
    tags: ['マーカー・特徴点', '射影変換', 'ロボット用の AI'],
    links: [{ href: '/record/tilted-plane-to-flat', t: 'LOG310 斜めに撮った紙を真上から見た形に直す' }, { href: '/series6', t: 'フィジカルAI研究室（π0.5 を学習させて動かす記録）' }] },
  { place: '人の作業を見る', h: '姿勢と動きを数える',
    p: '工場の作業分析や介護の見守りでは、人の関節の位置を出す姿勢推定が使われます。映像そのものを残さず、関節の点の動きだけを記録して、どの姿勢で何秒かかったかを数える使い方もできます。',
    tags: ['姿勢推定', '追跡'],
    links: [] },
  { place: '学習データづくり', h: '正解の下書きを作る',
    p: 'SAM 3 のように言葉で探すモデルは、学習データを用意しなくても動きますが、専用に学習したモデルには精度で劣ります。そこで、言葉で探すモデルに正解の枠や輪郭の下書きを作らせ、人が直して学習データにする使い方が現実的です。画像に正解を付ける作業は、学習でいちばん時間を食う所だからです。',
    tags: ['言葉で探すモデル（SAM 3 など）', 'アノテーション'],
    links: [{ href: '/record/vocabulary-of-modern-vision', t: 'LOG313 用語を下から積み直す' }] },
];
---
<section class="sec vg"><div class="panel">
  <h2 class="sec-h">このシリーズでやっていること | What we do</h2>
  <p class="vg-lead">答えが分かっている生成画像で予測を立て、机の上のアームを撮った写真で確かめる。この往復を、画素の数字から言葉で物を探すモデルまで、下から順に積んでいます。</p>
  <ol class="vg-steps">
    {steps.map((s, i) => (
      <li class="vg-step">
        <span class="vg-num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
        <div class="vg-step-body">
          <h3 class="vg-step-h">{s.h}</h3>
          <p class="vg-p">{s.p}</p>
          <ul class="vg-links">
            {s.links.map((l) => <li><a href={`/record/${l.slug}`}><b>{l.log}</b>{l.t}<i class="ms">arrow_forward</i></a></li>)}
          </ul>
        </div>
      </li>
    ))}
  </ol>
</div></section>

<section class="sec vg"><div class="panel">
  <h2 class="sec-h">何を出すかで、技術が分かれる | Outputs</h2>
  <p class="vg-lead">画像認識と物体検出は、同じ写真を見ても返す答えの形が違います。答えの形が違うと、人が用意する正解データの手間も桁で変わります。名前を1つ付けるだけなのか、枠を描くのか、画素を1つずつ塗るのか。</p>
  <ul class="vg-outs">
    {outputs.map((o) => (
      <li class="vg-out">
        <svg class="vg-pic" viewBox="0 0 120 80" aria-hidden="true">
          {o.pic === 'cls' && (<>
            <rect x="38" y="20" width="44" height="34" rx="3" fill="#242424" />
            <rect x="34" y="62" width="52" height="12" rx="2" fill="#fff" stroke="#242424" stroke-width="2" />
            <text x="60" y="71.5" text-anchor="middle" font-size="9" font-weight="700" fill="#242424">良品</text>
          </>)}
          {o.pic === 'det' && (<>
            <rect x="16" y="26" width="26" height="22" rx="2" fill="#242424" />
            <rect x="66" y="34" width="34" height="26" rx="2" fill="#242424" />
            <rect x="11" y="21" width="36" height="32" fill="none" stroke="#242424" stroke-width="2" />
            <rect x="61" y="29" width="44" height="36" fill="none" stroke="#242424" stroke-width="2" />
            <rect x="11" y="12" width="30" height="9" fill="#242424" /><text x="14" y="19" font-size="7" fill="#fff">0.92</text>
            <rect x="61" y="20" width="30" height="9" fill="#242424" /><text x="64" y="27" font-size="7" fill="#fff">0.87</text>
          </>)}
          {o.pic === 'seg' && (<>
            <path d="M18 54 C14 34 30 18 46 24 C58 28 56 50 44 58 C34 64 20 64 18 54 Z" fill="#242424" />
            <path d="M66 30 L100 22 L106 50 L74 62 Z" fill="#888" />
          </>)}
          {o.pic === 'pose' && (<>
            <polyline points="24,64 40,40 64,32 88,44 96,24" fill="none" stroke="#242424" stroke-width="3" stroke-linejoin="round" />
            <circle cx="24" cy="64" r="5" fill="#242424" /><circle cx="40" cy="40" r="5" fill="#242424" /><circle cx="64" cy="32" r="5" fill="#242424" /><circle cx="88" cy="44" r="5" fill="#242424" /><circle cx="96" cy="24" r="5" fill="#242424" />
          </>)}
          {o.pic === 'trk' && (<>
            <rect x="12" y="44" width="20" height="20" rx="2" fill="#cfcfcf" />
            <rect x="44" y="34" width="20" height="20" rx="2" fill="#999" />
            <rect x="78" y="22" width="20" height="20" rx="2" fill="#242424" />
            <path d="M22 40 C36 24 60 22 74 26" fill="none" stroke="#242424" stroke-width="2" stroke-dasharray="4 3" />
            <text x="88" y="35.5" text-anchor="middle" font-size="9" font-weight="700" fill="#fff">7</text>
            <text x="54" y="47.5" text-anchor="middle" font-size="9" font-weight="700" fill="#fff">7</text>
            <text x="22" y="57.5" text-anchor="middle" font-size="9" font-weight="700" fill="#555">7</text>
          </>)}
          {o.pic === 'dep' && (<>
            <rect x="1" y="1" width="118" height="26" fill="#cfcfcf" />
            <rect x="1" y="27" width="118" height="26" fill="#888" />
            <rect x="1" y="53" width="118" height="26" fill="#242424" />
            <text x="110" y="18" text-anchor="end" font-size="9" font-weight="700" fill="#242424">遠い</text>
            <text x="110" y="70" text-anchor="end" font-size="9" font-weight="700" fill="#fff">近い</text>
          </>)}
        </svg>
        <h3 class="vg-out-h">{o.name}<span>{o.en}</span></h3>
        <p class="vg-p">{o.p}</p>
      </li>
    ))}
  </ul>
</div></section>

<section class="sec vg"><div class="panel">
  <h2 class="sec-h">現場で使われている技術 | In the field</h2>
  <p class="vg-lead">現場で選ばれる技術は、新しさで決まりません。照明と物の置き場所をどこまで固定できるか、そして見落としと見間違いのどちらが困るかで決まります。</p>
  <div class="vg-fields">
    {fields.map((f) => (
      <article class="vg-field">
        <p class="vg-place">{f.place}</p>
        <h3 class="vg-field-h">{f.h}</h3>
        <p class="vg-p">{f.p}</p>
        <ul class="vg-tags">{f.tags.map((t) => <li>{t}</li>)}</ul>
        {f.links.length > 0 && (
          <ul class="vg-links vg-links-sm">
            {f.links.map((l) => <li><a href={l.href}>{l.t}<i class="ms">arrow_forward</i></a></li>)}
          </ul>
        )}
      </article>
    ))}
  </div>
</div></section>

<style>
  .vg { padding-bottom: 0; }
  .vg-lead { margin: -16px 0 32px; max-width: 46em; font: 400 16px/28px var(--sans); color: var(--ink-2); }
  .vg-p { margin: 0; font: 400 15px/27px var(--sans); color: var(--ink-2); word-break: auto-phrase; text-wrap: pretty; }

  /* 1 下から積む5段 */
  .vg-steps { list-style: none; margin: 0; padding: 0; display: grid; }
  .vg-step { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 24px; padding: 28px 0; border-top: 1px solid #e6e6e6; }
  .vg-step:last-child { padding-bottom: 0; }
  .vg-num { display: grid; place-items: center; width: 56px; height: 56px; border-radius: 8px; background: var(--dark); color: #fff;
    font: 700 20px/1 var(--sans); font-variant-numeric: tabular-nums; }
  .vg-step-h { margin: 0 0 8px; font: 700 18px/28px var(--sans); color: var(--ink); }
  .vg-step-body { max-width: 52em; }
  .vg-links { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
  .vg-links a { display: inline-flex; align-items: center; gap: 8px; padding: 7px 10px 7px 12px; border-radius: 4px; background: #f2f2f2;
    color: var(--ink); font: 500 13px/20px var(--sans); text-decoration: none; transition: background .15s; }
  .vg-links a b { font-weight: 700; font-variant-numeric: tabular-nums; }
  .vg-links a .ms { font-size: 15px; color: var(--ink-3); transition: transform .2s; }
  .vg-links a:hover { background: #e6e6e6; }
  .vg-links a:hover .ms { transform: translateX(2px); }

  /* 2 出力の形 */
  .vg-outs { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px 28px; }
  .vg-pic { display: block; box-sizing: border-box; width: 100%; height: 136px; padding: 18px; margin-bottom: 14px; border-radius: 8px; background: #f2f2f2; }
  .vg-pic text { font-family: var(--sans); }
  .vg-out-h { margin: 0 0 6px; font: 700 17px/26px var(--sans); color: var(--ink); }
  .vg-out-h span { margin-left: 8px; font: 400 12px/18px var(--sans); color: var(--ink-3); }

  /* 3 現場 */
  .vg-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
  .vg-field { padding: 28px; border-radius: 8px; background: var(--bg); }
  .vg-place { margin: 0 0 4px; font: 600 13px/20px var(--sans); color: var(--ink-3); }
  .vg-field-h { margin: 0 0 12px; font: 700 20px/30px var(--sans); color: var(--ink); }
  .vg-tags { list-style: none; margin: 16px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
  .vg-tags li { padding: 3px 8px; border-radius: 4px; background: var(--dark); color: #fff; font: 500 12px/18px var(--sans); }
  .vg-links-sm { margin-top: 16px; }
  .vg-links-sm a { background: #fff; }
  .vg-links-sm a:hover { background: #e6e6e6; }

  @media (max-width: 1023px) {
    .vg-outs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .vg-fields { grid-template-columns: minmax(0, 1fr); }
  }
  @media (max-width: 599px) {
    .vg-lead { margin: -16px 0 24px; font-size: 15px; line-height: 26px; }
    .vg-step { grid-template-columns: 40px minmax(0, 1fr); gap: 14px; padding: 22px 0; }
    .vg-num { width: 40px; height: 40px; font-size: 15px; }
    .vg-outs { grid-template-columns: minmax(0, 1fr); gap: 28px; }
    .vg-pic { height: 120px; }
    .vg-field { padding: 20px 18px; }
    .vg-field-h { font-size: 18px; line-height: 28px; }
  }
</style>
```


## 作業ログ ― 記号の絵を、方向の違う3案から選び直す

公開した記号の絵を見て、次の指摘が来た。

> ここのイラストのクオリティが低いので別の提案をください。

![最初の版の答えの形の6枚。灰色の面に黒い四角や丸、灰色の帯を置いた記号の絵](https://pixelstomotors.com/media/LOG459_01_svg_icons.png)

記号の絵では、四角が何の四角なのか、0.92 が何の数字なのかが分からない。6枚が別々の物を描いているので、同じ写真でも答えの形が違う、というセクションの言いたいことも伝わらない。

### 3案は、何を主役にするかで分けた

案は、並べ方の違いではなく、何を主役にするかの違いで3つ出した。3案とも、同じ1枚の実写から作った。

| 案 | 主役 | 中身 |
|---|---|---|
| A 実写の1枚に重ねる | 実物の写真 | 同じ写真に枠・塗り分け・点・距離を重ねる。深度は Depth Anything V2 で実際に推した結果 |
| B 答えのデータを見せる | 数字と格子 | 枠の座標、塗り分けを24×16の格子にしたもの、点の座標、距離の値を黒い面に並べる |
| C 人の手間を比べる | 手間の数 | 正解を付けるのに人が何回操作するか。1回、3枠、69点、6点、3×全フレーム、0 |

![案A。同じ真上の写真に、6つの答えを重ねた](https://pixelstomotors.com/media/LOG459_02_proposal_a_photo.png)

![案B。枠の座標、塗り分けの格子、点の座標、距離の値を黒い面に並べた](https://pixelstomotors.com/media/LOG459_02_proposal_b_data.png)

![案C。答えの形ごとに、人が正解を付ける操作の数を大きく出した](https://pixelstomotors.com/media/LOG459_02_proposal_c_effort.png)

案Cの69点は、写真から取り出したアーム・赤・青の輪郭を、ずれ2画素までで少ない点に減らしたときの点の数（60＋4＋5）。案の段階では、3案とも追跡の絵だけは手で描いた仮の道だった。

> 案A（実写の1枚に重ねる）がわかりやすいかな

案Aで進め、追跡は実機の録画から本物の動きを取り出して差し替えることにした。


## 作業ログ ― 真上のカメラの写真1枚から、5つの答えを作る

### Depth Anything V2 は、写真1枚から距離を推すモデル

[Depth Anything V2](https://github.com/DepthAnything/Depth-Anything-V2) は、普通のカメラの写真1枚から、画素ごとにカメラからの近さを推すモデル。2台のカメラや距離のセンサーを使わずに、1枚から推す（単眼深度推定と呼ぶ）。大きさの違う版があり、この回ではいちばん小さい [Small 版（Hugging Face の depth-anything/Depth-Anything-V2-Small-hf）](https://huggingface.co/depth-anything/Depth-Anything-V2-Small-hf) を使った。

| 言葉 | この回での意味 |
|---|---|
| 深度 | カメラから物までの距離。この絵では、近いほど白、遠いほど黒 |
| 単眼深度推定 | カメラ1台の写真1枚から、深度を推すこと |
| 相対的な深度 | どちらが近いかは分かるが、何 mm かは分からない値。Depth Anything V2 の出力はこれで、この回の値は 0.31〜5.01 だった |

このプロジェクトでは、真上のカメラの写真の深度を推すのに使った。出てくるのは何 mm 先かではなく、近い・遠いの並びなので、ページの絵でも距離の数字は出していない。モデルの重み（model.safetensors、99,173,660 バイト）は Hugging Face から読み込み、クラウドの作業環境の CPU で動かした。

### 使った写真は、真上のカメラで撮ったマットの上のアームと2つのブロック

素材は、KIOXIA の `r2r2r-data/vision/captured/Log05/mat_orange_20260729_212044_f0.png`（1920×1080）。真上のカメラで、カッティングマットの上の mechArm 270 と、赤いブロック・青いブロックを撮った1枚。ほかの実写（斜めから撮ったブロックだけの写真）も見たが、アームと物の両方が写っているのはこの1枚だった。

これを横 480〜1440・縦 180〜820 で切り出し、960×640（3:2）にした。アームの手と2つのブロックが下寄りに入る位置。

### make.py（全文）― 切り出しと、アーム・赤・青の形の取り出し

形は、色と明るさのしきい値で取り出した。赤は色相が10未満か168より大きく、彩度75より大きく、明るさ90より大きい画素。青は色相95〜125・彩度120より大きい画素。アームは、彩度45未満（色が薄い）で明るさ120より大きい画素。どれも小さな粒を消し（オープニング）、すき間を埋め（クロージング）、いちばん大きな塊を残した。

赤は、最初に彩度110より大きくで取ったら571画素しか残らなかった。ブロックの上の面の彩度を8画素おきに見ると、92〜130くらいの値が並んでいたので、75まで下げて2,705画素にした。

```python
import cv2, numpy as np
src=cv2.imread('/mnt/user-data/uploads/KIOXIA/r2r2r-data/vision/captured/Log05/mat_orange_20260729_212044_f0.png')
c=src[180:820,480:1440].copy(); cv2.imwrite('crop.png',c)
hsv=cv2.cvtColor(c,cv2.COLOR_BGR2HSV); H,S,V=cv2.split(hsv)
def clean(m,k=5):
    m=cv2.morphologyEx(m.astype(np.uint8)*255,cv2.MORPH_OPEN,np.ones((k,k),np.uint8))
    m=cv2.morphologyEx(m,cv2.MORPH_CLOSE,np.ones((k*3,k*3),np.uint8))
    n,lab,st,_=cv2.connectedComponentsWithStats(m)
    i=1+np.argmax(st[1:,4]); return (lab==i)
red=clean(((H<10)|(H>168))&(S>75)&(V>90))
blue=clean((H>95)&(H<125)&(S>120)&(V>80))
arm=clean((S<45)&(V>120),7)
for n,m in [('red',red),('blue',blue),('arm',arm)]:
    np.save(f'm_{n}.npy',m); ys,xs=np.where(m); print(n,m.sum(),xs.min(),ys.min(),xs.max(),ys.max())
```

出力（画素の数と、x と y の最小・最大）:

```
red 2705 147 441 199 493
blue 3043 739 440 799 490
arm 79325 355 0 586 582
```

### depth.py（全文）― 深度を推す

```python
# crop.png の深度を Depth Anything V2（Small）で推して depth.npy に保存する（pip: torch transformers）
from transformers import pipeline
from PIL import Image
import numpy as np, torch
p = pipeline('depth-estimation', model='depth-anything/Depth-Anything-V2-Small-hf')
im = Image.open('crop.png').convert('RGB')
d = p(im)['predicted_depth']
d = torch.nn.functional.interpolate(d[None] if d.dim() == 3 else d[None, None], size=(im.height, im.width), mode='bicubic')[0, 0].numpy()
np.save('depth.npy', d)
```

出力の値の範囲と、かかった時間（モデルの読み込みを含む）:

```
depth (640, 960) 0.3063 5.015
real	0m15.662s
user	0m7.403s
sys	0m0.898s
```

### tiles.py（全文）― 分類・検出・セグメンテーション・姿勢推定・深度の5枚

分類は、写真全体に名前の札を1つだけ付けた。検出は、取り出した形の外側に四角を引き、名前の札を付けた。確からしさの数字は、実際に検出モデルを回していないので載せていない。セグメンテーションは、写真を暗くして、形の画素だけを白く塗った。姿勢推定の6点（関節3つ・手・指先2つ）は、写真を見て手で置いた位置で、モデルの出力ではない。指先と手の点は、実物に貼ってある青い印の位置に合わせた。深度は、Depth Anything V2 の値を最小0・最大1にそろえて白黒にした。

```python
import cv2, numpy as np, json
from PIL import Image, ImageDraw, ImageFont
F='/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'
def font(s): return ImageFont.truetype(F,s,index=0)  # JP
base=Image.open('crop.png').convert('RGB'); W,H=base.size
M={n:np.load(f'm_{n}.npy') for n in ['arm','red','blue']}
# arm: fill holes
a=(M['arm']*255).astype(np.uint8); cs,_=cv2.findContours(a,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
a2=np.zeros_like(a); cv2.drawContours(a2,cs,-1,255,-1); M['arm']=a2>0
def bbox(m): ys,xs=np.where(m); return [int(xs.min()),int(ys.min()),int(xs.max()),int(ys.max())]
B={n:bbox(m) for n,m in M.items()}
NAME={'arm':'アーム','red':'赤いブロック','blue':'青いブロック'}
KP=[(470,95),(470,265),(462,345),(458,405),(395,568),(508,568)]
def chip(d,xy,text,size=22,fg='white',bg=(20,20,20)):
    f=font(size); x,y=xy; tw=d.textlength(text,font=f); d.rectangle([x,y,x+tw+16,y+size+12],fill=bg); d.text((x+8,y+3),text,font=f,fill=fg)
def dim(im,k=0.55): return Image.blend(im,Image.new('RGB',im.size,(0,0,0)),1-k)
out={}
# 分類
im=base.copy(); d=ImageDraw.Draw(im); chip(d,(24,24),'机の上のロボットアーム',38); out['cls']=im
# 検出
im=base.copy(); d=ImageDraw.Draw(im)
for n,(x0,y0,x1,y1) in B.items():
    p=8; d.rectangle([x0-p,y0-p,x1+p,y1+p],outline='white',width=5); chip(d,(x0-p,y0-p-52),NAME[n],30)
out['det']=im
# セグメンテーション：暗くして、物の形だけ白く塗る
im=np.array(dim(base,0.45)).astype(float)
for n,v in [('arm',255),('red',255),('blue',255)]:
    m=M[n]; im[m]=im[m]*0.25+np.array([v,v,v])*0.75
im=Image.fromarray(im.astype(np.uint8)); d=ImageDraw.Draw(im)
for n in M:
    cs,_=cv2.findContours((M[n]*255).astype(np.uint8),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
    for c in cs: d.line([tuple(p[0]) for p in c]+[tuple(c[0][0])],fill='white',width=3)
out['seg']=im
# 姿勢推定
im=dim(base,0.6); d=ImageDraw.Draw(im)
d.line(KP[:4],fill='white',width=6); d.line([KP[3],KP[4]],fill='white',width=6); d.line([KP[3],KP[5]],fill='white',width=6)
for x,y in KP: d.ellipse([x-13,y-13,x+13,y+13],fill='white',outline=(20,20,20),width=4)
out['pose']=im
# 追跡（説明用：手の位置の通り道）
im=dim(base,0.6); d=ImageDraw.Draw(im)
trail=[(150,250),(230,300),(320,350),(400,390),(458,405)]
for i in range(len(trail)-1): d.line([trail[i],trail[i+1]],fill='white',width=5)
for x,y in trail[:-1]: d.ellipse([x-7,y-7,x+7,y+7],fill='white')
x0,y0,x1,y1=B['arm']; chip(d,(trail[-1][0]+20,trail[-1][1]-60),'ID 1  アームの手',22)
for n in ['red','blue']:
    x0,y0,x1,y1=B[n]; d.rectangle([x0-8,y0-8,x1+8,y1+8],outline='white',width=4); chip(d,(x0-8,y0-48),'ID 2' if n=='red' else 'ID 3',22)
out['trk']=im
# 深度（Depth Anything V2 Small の出力。近いほど白）
dp=np.load('depth.npy'); dn=(dp-dp.min())/(dp.max()-dp.min())
im=Image.fromarray((dn*255).astype(np.uint8)).convert('RGB'); out['dep']=im
for k,v in out.items(): v.save(f'A_{k}.png')
json.dump({'bbox':B,'kp':KP,'area':{n:int(M[n].sum()) for n in M}},open('meta.json','w'),ensure_ascii=False)
# データ案用：セグの粗い格子（24x16）
g=np.zeros((16,24),int)
for n,v in [('arm',1),('red',2),('blue',3)]:
    s=cv2.resize(M[n].astype(np.uint8),(24,16),interpolation=cv2.INTER_AREA); g[s>0]=v
json.dump(g.tolist(),open('grid.json','w'))
dd=cv2.resize(dn,(8,5),interpolation=cv2.INTER_AREA); json.dump(np.round(dd,2).tolist(),open('dgrid.json','w'))
# 輪郭の点の数（手で点を打つ数の目安）
pts={}
for n in M:
    cs,_=cv2.findContours((M[n]*255).astype(np.uint8),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
    c=max(cs,key=cv2.contourArea); pts[n]=len(cv2.approxPolyDP(c,2.0,True))
print(B,pts)
json.dump(pts,open('pts.json','w'))
```

出力（3つの形の四角と、輪郭を少ない点に減らしたときの点の数）:

```
{'arm': [355, 0, 586, 582], 'red': [147, 441, 199, 493], 'blue': [739, 440, 799, 490]} {'arm': 60, 'red': 4, 'blue': 5}
```

アームの形は明るさのしきい値で取ったので、影になった縁や、明るい手の印の周りが少し欠ける。物の形を正確に取ることがこの回の目的ではないので、そのまま使った。


## 作業ログ ― 実機の録画から、赤いブロックに同じ番号を振り続ける

追跡は、動いている物に同じ番号を振り続ける答えなので、写真1枚では描けない。素材は、KIOXIA の `r2r2r-data/runs/run_pick_place/run_0001_20261019_195120/recording/top.mp4`。真上のカメラで、アームが赤いブロックをつかんで白い箱へ運んだ1回の録画（1920×1080、1,024コマ、34.2秒、毎秒約29.9コマ）。同じ真上のカメラだが、写真とは別の日の、箱がある場面になる。

### アームの手の青い印を追うと、指先の別の青い印に飛び移った

最初は、アームの手に貼った青い印を追った。1コマごとに青い塊を探し、前のコマの位置から60画素以内でいちばん近い塊を選ぶ。赤い塊も、いちばん大きいものを毎コマ記録した。

```python
import cv2, numpy as np, json
cap=cv2.VideoCapture('/mnt/user-data/uploads/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261019_195120/recording/top.mp4')
def blobs(f, kind):
    hsv=cv2.cvtColor(f,cv2.COLOR_BGR2HSV); H,S,V=cv2.split(hsv)
    m=((H>95)&(H<125)&(S>130)&(V>80)) if kind=='blue' else (((H<10)|(H>168))&(S>120)&(V>90))
    m=cv2.morphologyEx(m.astype(np.uint8)*255,cv2.MORPH_OPEN,np.ones((3,3),np.uint8))
    n,lab,st,cen=cv2.connectedComponentsWithStats(m)
    return [(cen[i],st[i]) for i in range(1,n) if st[i,4]>40]
hand=np.array([1020.,512.]); H=[];R=[]
i=0
while True:
    ok,f=cap.read()
    if not ok: break
    b=blobs(f,'blue')
    if b:
        d=[np.linalg.norm(c-hand) for c,_ in b]; k=int(np.argmin(d))
        if d[k]<60: hand=b[k][0]
    H.append([float(hand[0]),float(hand[1])])
    r=blobs(f,'red')
    if r:
        c,st=max(r,key=lambda x:x[1][4]); R.append([float(c[0]),float(c[1]),[int(v) for v in st[:4]]])
    else: R.append(None)
    i+=1
json.dump({'hand':H,'red':R},open('trk.json','w'))
print(i, H[0], H[-1], [H[j] for j in range(0,i,128)])
print([R[j][:2] if R[j] else None for j in range(0,i,128)])
```

出力（1行目は総コマ数と、手の位置の最初と最後。2行目は128コマおきの手の位置。3行目は128コマおきの赤い塊の位置）:

```
1024 [1021.0889570552147, 511.68865030674846] [1030.2897473997027, 512.0118870728083] [[1021.0889570552147, 511.68865030674846], [1021.4795144157815, 512.1608497723824], [1021.4348484848484, 512.2060606060606], [1032.3432055749129, 540.7473867595819], [1109.2842105263157, 518.2736842105263], [1107.7378640776699, 488.99676375404533], [1373.9108910891089, 494.3894389438944], [1373.921568627451, 494.70588235294116]]
[[906.302995391705, 90.76382488479263], [906.6041214750543, 90.1409978308026], [906.4380733944954, 90.07224770642202], [913.4343839541548, 208.47908309455588], [1128.4813432835822, 691.7529317697229], [1139.7642487046633, 678.2235751295336], [1472.370923574807, 669.546925566343], [1465.0101716465354, 652.4212756940029]]
real	0m28.992s
user	0m48.913s
sys	0m1.867s
```

手の道を880コマ目に重ねると、線がジグザグに折れ、最後は手の印ではない所で止まっていた。手首が回って印が隠れたコマで、60画素以内にあった指先の青い印に飛び移っていた。赤い塊のほうも、最初の3つは (906, 90) 付近で、これはブロックではなく、アームに這わせた赤い配線だった。

![手の青い印を追った道を880コマ目に重ねた絵。線がジグザグに折れ、手の印ではない所で止まっている](https://pixelstomotors.com/media/LOG459_03_hand_dot_tracking_failed.png)

**色の塊を前のコマの近さだけでつなぐ追跡は、印が隠れた瞬間に、近くの同じ色の別の物へ乗り移る。** 番号が同じでも、同じ物を追っているとは限らない。追跡の答えを信じる前に、番号が付いた物を目で確かめる必要がある。

### 大きさで赤いブロックだけを残した trk_tile2.py（全文）

赤い塊のうち、幅と高さが150画素未満で、面積（幅×高さ）が2,500より大きいものだけを残した。配線は幅16・高さ348なので外れる。残ったコマのうち、ブロックが持ち上がって箱へ運ばれる 622〜705コマ目の84コマを使い、前後7コマの平均で道をなめらかにした。最初のコマの四角は灰色、最後のコマの四角は白で描き、どちらにも ID 2 と、その時点の秒を付けた。

箱の周りの動きが絵の中で小さくならないよう、横780画素の範囲を切り出して 960×640 に広げてから描いている。

```python
import cv2, numpy as np, json, sys
from PIL import Image, ImageDraw, ImageFont
F='/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'
R=json.load(open('trk.json'))['red']; FPS=1024/34.243
A,Bf=int(sys.argv[1]),int(sys.argv[2]); X0,Y0=int(sys.argv[3]),int(sys.argv[4]); CW=int(sys.argv[5]) if len(sys.argv)>5 else 960; SC=960/CW
good={i:r for i,r in enumerate(R) if r and r[2][2]<150 and r[2][3]<150 and r[2][2]*r[2][3]>2500}
cap=cv2.VideoCapture('/mnt/user-data/uploads/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261019_195120/recording/top.mp4')
cap.set(cv2.CAP_PROP_POS_FRAMES,Bf); ok,f=cap.read()
im=Image.fromarray(cv2.cvtColor(f,cv2.COLOR_BGR2RGB)).crop((X0,Y0,X0+CW,Y0+CW*2//3)).resize((960,640),Image.LANCZOS)
im=Image.blend(im,Image.new('RGB',im.size,(0,0,0)),0.4); d=ImageDraw.Draw(im)
def chip(xy,text,size=22,bg=(20,20,20)):
    fo=ImageFont.truetype(F,size); x,y=xy; tw=d.textlength(text,font=fo); d.rectangle([x,y,x+tw+16,y+size+12],fill=bg); d.text((x+8,y+3),text,font=fo,fill='white')
idx=[i for i in sorted(good) if A<=i<=Bf]
c=np.array([good[i][:2] for i in idx]); k=7
cs=np.array([c[max(0,j-k):j+k+1].mean(0) for j in range(len(c))])
pts=[((x-X0)*SC,(y-Y0)*SC) for x,y in cs]
d.line(pts,fill='white',width=5,joint='curve')
for p in pts[::6]: d.ellipse([p[0]-6,p[1]-6,p[0]+6,p[1]+6],fill='white')
for fr,dash in [(idx[0],True),(idx[-1],False)]:
    x,y,w,h=good[fr][2]; x=(x-X0)*SC; y=(y-Y0)*SC; w*=SC; h*=SC; p=10
    if dash:
        for s in range(0,int(2*(w+h+4*p)),14):
            pass
        d.rectangle([x-p,y-p,x+w+p,y+h+p],outline=(170,170,170),width=3)
    else: d.rectangle([x-p,y-p,x+w+p,y+h+p],outline='white',width=5)
    chip((x-p,y-p-52),f'ID 2   {fr/FPS:.1f}秒',30,(90,90,90) if dash else (20,20,20))
im.save('A_trk.png'); print(idx[0],idx[-1],len(idx))
```

出力（使った最初と最後のコマ、コマ数）と、かかった時間:

```
622 705 84
real	0m1.022s
user	0m1.430s
sys	0m0.109s
```


## 作業ログ ― 6枚を webp にしてページに組み込み、作り直せるかを確かめる

6枚は 960×640 の webp（画質82）にして `public/site/vision/` に置いた。ページでは3列で1枚が約390px なので、画面の細かいスマホでも粗く見えない大きさ。絵の中の札の文字は、最初の22px では表示が約11px になり読みにくかったので、30〜38px に上げた。分類の説明文は、絵の札に合わせて「この写真は良品、のように」から「この写真は机の上のロボットアーム、のように。どこに写っているかは返さない。」に変えた。6枚の下には、何から作った絵かを1行で書いた。

### 通しで流し直して、公開中の絵と同じになるかを確かめた

`tools/vision_guide_tiles/` のスクリプトを新しいフォルダに写し、make.py から順に流し直して、出てきた webp を公開中のファイルと比べた。

```
Linux-6.18.44-fc-v300-x86_64-with-glibc2.39
python 3.13.18 opencv 5.0.0 torch 2.15.0+cpu transformers 5.22.2 Pillow 12.3.0 numpy 2.5.3
2
red 2705 147 441 199 493
blue 3043 739 440 799 490
arm 79325 355 0 586 582
real	0m0.346s
user	0m0.199s
sys	0m0.037s
real	0m15.662s
user	0m7.403s
sys	0m0.898s
depth (640, 960) 0.3063 5.015
{'arm': [355, 0, 586, 582], 'red': [147, 441, 199, 493], 'blue': [739, 440, 799, 490]} {'arm': 60, 'red': 4, 'blue': 5}
real	0m0.440s
user	0m0.349s
sys	0m0.064s
1024 [1021.0889570552147, 511.68865030674846] [1030.2897473997027, 512.0118870728083] [[1021.0889570552147, 511.68865030674846], [1021.4795144157815, 512.1608497723824], [1021.4348484848484, 512.2060606060606], [1032.3432055749129, 540.7473867595819], [1109.2842105263157, 518.2736842105263], [1107.7378640776699, 488.99676375404533], [1373.9108910891089, 494.3894389438944], [1373.921568627451, 494.70588235294116]]
[[906.302995391705, 90.76382488479263], [906.6041214750543, 90.1409978308026], [906.4380733944954, 90.07224770642202], [913.4343839541548, 208.47908309455588], [1128.4813432835822, 691.7529317697229], [1139.7642487046633, 678.2235751295336], [1472.370923574807, 669.546925566343], [1465.0101716465354, 652.4212756940029]]
real	0m28.992s
user	0m48.913s
sys	0m1.867s
622 705 84
real	0m1.022s
user	0m1.430s
sys	0m0.109s
cls 67662
det 68234
seg 31966
pose 44860
trk 44434
dep 4978
cls same
det same
seg same
pose same
trk same
dep same
```

3行目の 2 は CPU のコア数。最後の6行が、流し直した webp と公開中の webp の比べた結果で、6枚とも同じだった。6枚の合計は 262,134 バイト。

### README.md（全文）― tools/vision_guide_tiles/ の作り方の手順

```markdown
# /series7 の「何を出すかで、技術が分かれる」の絵の作り方（2026-11-07）

出力は public/site/vision/{cls,det,seg,pose,trk,dep}.webp（960×640）。

元の素材（KIOXIA の r2r2r-data）
- 写真：vision/captured/Log05/mat_orange_20260729_212044_f0.png（真上のカメラ。mechArm 270 と赤・青のブロック）
- 録画：runs/run_pick_place/run_0001_20261019_195120/recording/top.mp4（赤いブロックを箱へ運ぶ1回）

順番（同じ作業フォルダで。スクリプト中のパスは素材を置いた場所に合わせる）
1. `python3 make.py` 写真を 960×640 に切り出し（x 480〜1440・y 180〜820）、色と明るさのしきい値でアーム・赤・青の形を取る
2. `python3 depth.py` 深度を Depth Anything V2 Small で推す
3. `python3 tiles.py` 分類・検出・セグメンテーション・姿勢推定・深度の絵を作る（姿勢推定の6点は写真を見て手で置いた位置）
4. `python3 trk.py` 録画の全フレームで赤い塊の位置を取る
5. `python3 trk_tile2.py 622 705 880 380 780`（最後の数字は切り出す幅。960×640 に拡大して描く） 622〜705フレーム目の赤いブロックの道を、705フレーム目に重ねる（A_trk.png）
6. A_*.png を webp（quality 82）にして public/site/vision/ へ
```

### VisionGuide.astro（全文・実写の版・commit 0bad94e）

```astro
---
// /series7（画像認識と物体検出）の見出しの下に置く解説（2026-11-07）
// 1 このシリーズでやっていること（下から積む5段と、その回）
// 2 何を出すかで分かれる（分類・検出・セグメンテーション・姿勢推定・追跡・深度）。絵は public/site/vision/（作り方は tools/vision_guide_tiles/）
// 3 現場で使われている技術（現場ごとに、何を見たいか・何が使われるか・関連の回）
// 文字は AI も読むので、本文の文字で書く（絵だけにしない）
type Link = { log: string; slug: string; t: string };
const steps: { h: string; p: string; links: Link[] }[] = [
  { h: '画素と色を測る',
    p: '同じ赤 (255, 0, 0) が、読み込む道具によって青と入れ替わる。オレンジの照明の下では、青いブロックが色を失う。検出が揺れたとき、原因がモデルより手前の画素の段階にあることを先に確かめています。',
    links: [
      { log: 'LOG299', slug: 'where-pixel-values-change', t: '画素の数字は、読むと・保存すると・撮ると変わる' },
      { log: 'LOG300', slug: 'hue-holds-color-breaks', t: 'オレンジの光の下で青いブロックが色を失う' },
      { log: 'LOG301', slug: 'rgb-hsv-threshold-lighting-failure', t: '光が変わると、赤検出が青を拾う' },
    ] },
  { h: '人が条件を書く手法を、1つずつ壊してみる',
    p: 'しきい値で白と黒に分け、モルフォロジーで小さな粒を消し、輪郭から面積や円形度を出し、特徴点で2枚の写真を対応させ、射影変換で斜めの紙を真上から見た形に直す。寸法の分かっている印刷物で、どの段がどう壊れるかを測りました。特徴点マッチングは、対象が写っていない写真とも867件の対応を付けています。',
    links: [
      { log: 'LOG302', slug: 'morphology-kernel-fits-or-vanishes', t: 'モルフォロジー演算' },
      { log: 'LOG304', slug: 'what-circularity-actually-measures', t: '重心・面積・外接矩形・円形度' },
      { log: 'LOG309', slug: 'same-place-different-object', t: '特徴点マッチング' },
      { log: 'LOG310', slug: 'tilted-plane-to-flat', t: '射影変換' },
    ] },
  { h: '物差しを先に決める',
    p: 'IoU は、出した枠と正解の枠がどれだけ重なっているか。1.0 で完全に一致します。適合率は、見つけたもののうち正しかった割合で、低いと見間違いが多い。再現率は、あるべきもののうち見つけられた割合で、低いと見落としが多い。物差しがないままモデルを替えても、良くなった気がする、までしか言えません。',
    links: [{ log: 'LOG298', slug: 'image-recognition-object-detection-overview', t: '9つの層と、評価指標を先に学ぶ理由' }] },
  { h: '学習で条件を決めるモデルの言葉を積み直す',
    p: 'モデル、学習、推論、バックボーン、ゼロショットといった言葉を4段に分けて定義し直し、YOLO26、DINOv3、SAM 3、写真1枚から距離を推す単眼深度が、それぞれどこに立つ技術なのかを整理しました。',
    links: [{ log: 'LOG313', slug: 'vocabulary-of-modern-vision', t: '用語を下から積み直す' }] },
  { h: '新しいモデルを、ロボットの作業に当てる',
    p: 'SAM 3 は物の名前と色では絞り込めたのに、ガラスかプラスチックかは見分けなかった。ステンレスに映った像まで物として切り出した。OpenAI の Decisions API は、爪の間にブロックがあるかははっきり見分けたが、真上のカメラでは持ち上がったかを判定できなかった。',
    links: [
      { log: 'LOG317', slug: 'sam3-first-look-sorting-line', t: 'SAM 3 はガラス瓶とペットボトルを同じ物として切り出した' },
      { log: 'LOG236', slug: 'sam3-mirror-reflection-grasp-problem', t: 'ロボットの目は、鏡像もあると見る' },
      { log: 'LOG325', slug: 'decisions-api-grasp-check', t: 'Decisions API は、持ち上がったかを判定できなかった' },
    ] },
];

// 出力の形。pic は小さな絵の種類
const outputs: { name: string; en: string; p: string; pic: string; alt: string }[] = [
  { name: '分類', en: 'Classification', pic: 'cls', alt: '真上から撮ったアームの写真に、机の上のロボットアームという名前が1つだけ付いている', p: '写真1枚に、名前を1つ返す。この写真は机の上のロボットアーム、のように。どこに写っているかは返さない。' },
  { name: '物体検出', en: 'Detection', pic: 'det', alt: 'アーム・赤いブロック・青いブロックが、それぞれ四角い枠で囲まれている', p: '物ごとに、四角い枠と名前と確からしさを返す。何がいくつ、どこにあるかが分かる。' },
  { name: 'セグメンテーション', en: 'Segmentation', pic: 'seg', alt: 'アームと2つのブロックの形だけが、画素単位で白く塗られている', p: '画素ごとに、どの物かを塗り分ける。枠ではなく形が分かるので、つかむ場所を決められる。' },
  { name: '姿勢推定', en: 'Pose estimation', pic: 'pose', alt: 'アームの関節と指先に6つの点が打たれ、線でつながっている', p: '関節や角など、決まった点の位置を返す。人の動きや、物の向きが分かる。' },
  { name: '追跡', en: 'Tracking', pic: 'trk', alt: '実機の録画で、赤いブロックが箱へ運ばれるあいだ、同じ ID 2 が付き続け、通った道が線で残っている', p: '動画の中で、同じ物に同じ番号を振り続ける。どこから来てどこへ行ったかが分かる。' },
  { name: '深度', en: 'Depth', pic: 'dep', alt: 'カメラに近いアームが白く、遠いマットが黒く写る距離の画像', p: 'カメラから物までの距離を返す。アームの手をどの高さまで下ろせばいいかが分かる。' },
];

const fields: { place: string; h: string; p: string; tags: string[]; links: { href: string; t: string }[] }[] = [
  { place: '工場の外観検査', h: '傷・欠け・汚れを見つける',
    p: '照明とカメラの位置を固定できるラインでは、しきい値やパターンマッチングのような人が条件を書く手法がいまも主力です。速く、なぜ不良としたかを説明できるからです。傷の形が毎回違い、不良品の写真がなかなか集まらない場合は、良品の写真だけで学習して良品と違う所を拾う異常検知（PatchCore など）が使われます。どちらの方式でも、結果を先に決めるのは照明の当て方です。',
    tags: ['しきい値', 'パターンマッチング', '異常検知'],
    links: [{ href: '/record/ai-visual-inspection-lighting', t: 'LOG319 AI外観検査は撮り方で決まる' }, { href: '/record/rgb-hsv-threshold-lighting-failure', t: 'LOG301 光が変わると、赤検出が青を拾う' }] },
  { place: '物流の仕分けとピッキング', h: '箱や袋を1つずつつかむ',
    p: 'ばら積みの箱や袋をロボットがつかむ現場では、距離が分かる3Dカメラと、物を1つずつ塗り分けるインスタンスセグメンテーションを組み合わせて、つかむ点を決めます。品物の種類が多く入れ替わりも激しいので、品物ごとに学習し直さなくても形を切り出せるモデルが求められています。荷札のバーコードや文字の読み取りは、昔からの画像処理が担っています。',
    tags: ['3Dカメラ', 'インスタンスセグメンテーション', 'バーコード・文字の読み取り'],
    links: [{ href: '/record/sam3-first-look-sorting-line', t: 'LOG317 分別ラインを想定して SAM 3 を試した' }] },
  { place: '店や施設を動くロボット', h: '人と障害物をよける',
    p: '配膳や搬送のロボットは、LiDAR や深度カメラで距離を測って障害物をよけ、カメラの物体検出で人を見分けます。動きながら判断するので、1段階で枠を出す速い検出モデル（YOLO 系など）がよく使われます。人を見落とす方が危ないので、見間違いが少し増えても再現率を優先して調整します。',
    tags: ['LiDAR・深度カメラ', '物体検出（YOLO 系）', '追跡'],
    links: [{ href: '/record/serving-robot-jam-detection-without-path-lines', t: 'LOG418 配膳ロボットの詰まりを、録画の異常検知で拾う' }] },
  { place: 'ロボットアームの作業', h: '物の位置と向きを知る',
    p: '決まった部品を決まった場所に置くアームでは、マーカーや特徴点で位置を合わせ、射影変換で座標をそろえる方式が長く使われてきました。近年は、カメラの画像を直接見て関節の動きまで出すロボット用の AI（Physical Intelligence の π0.5 など）が出てきて、物を見つける処理と動きを決める処理の境目が薄れつつあります。',
    tags: ['マーカー・特徴点', '射影変換', 'ロボット用の AI'],
    links: [{ href: '/record/tilted-plane-to-flat', t: 'LOG310 斜めに撮った紙を真上から見た形に直す' }, { href: '/series6', t: 'フィジカルAI研究室（π0.5 を学習させて動かす記録）' }] },
  { place: '人の作業を見る', h: '姿勢と動きを数える',
    p: '工場の作業分析や介護の見守りでは、人の関節の位置を出す姿勢推定が使われます。映像そのものを残さず、関節の点の動きだけを記録して、どの姿勢で何秒かかったかを数える使い方もできます。',
    tags: ['姿勢推定', '追跡'],
    links: [] },
  { place: '学習データづくり', h: '正解の下書きを作る',
    p: 'SAM 3 のように言葉で探すモデルは、学習データを用意しなくても動きますが、専用に学習したモデルには精度で劣ります。そこで、言葉で探すモデルに正解の枠や輪郭の下書きを作らせ、人が直して学習データにする使い方が現実的です。画像に正解を付ける作業は、学習でいちばん時間を食う所だからです。',
    tags: ['言葉で探すモデル（SAM 3 など）', 'アノテーション'],
    links: [{ href: '/record/vocabulary-of-modern-vision', t: 'LOG313 用語を下から積み直す' }] },
];
---
<section class="sec vg"><div class="panel">
  <h2 class="sec-h">このシリーズでやっていること | What we do</h2>
  <p class="vg-lead">答えが分かっている生成画像で予測を立て、机の上のアームを撮った写真で確かめる。この往復を、画素の数字から言葉で物を探すモデルまで、下から順に積んでいます。</p>
  <ol class="vg-steps">
    {steps.map((s, i) => (
      <li class="vg-step">
        <span class="vg-num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
        <div class="vg-step-body">
          <h3 class="vg-step-h">{s.h}</h3>
          <p class="vg-p">{s.p}</p>
          <ul class="vg-links">
            {s.links.map((l) => <li><a href={`/record/${l.slug}`}><b>{l.log}</b>{l.t}<i class="ms">arrow_forward</i></a></li>)}
          </ul>
        </div>
      </li>
    ))}
  </ol>
</div></section>

<section class="sec vg"><div class="panel">
  <h2 class="sec-h">何を出すかで、技術が分かれる | Outputs</h2>
  <p class="vg-lead">画像認識と物体検出は、同じ写真を見ても返す答えの形が違います。答えの形が違うと、人が用意する正解データの手間も桁で変わります。名前を1つ付けるだけなのか、枠を描くのか、画素を1つずつ塗るのか。</p>
  <ul class="vg-outs">
    {outputs.map((o) => (
      <li class="vg-out">
        <img class="vg-pic" src={`/site/vision/${o.pic}.webp`} alt={o.alt} width="960" height="640" loading="lazy" />
        <h3 class="vg-out-h">{o.name}<span>{o.en}</span></h3>
        <p class="vg-p">{o.p}</p>
      </li>
    ))}
  </ul>
  <p class="vg-note">5枚は、真上のカメラで撮ったマットの上の mechArm 270 と2つのブロックの同じ写真です。深度は Depth Anything V2（Small）で推した結果、追跡は赤いブロックを箱へ運んだ実機の録画から取り出した動きです。</p>
</div></section>

<section class="sec vg"><div class="panel">
  <h2 class="sec-h">現場で使われている技術 | In the field</h2>
  <p class="vg-lead">現場で選ばれる技術は、新しさで決まりません。照明と物の置き場所をどこまで固定できるか、そして見落としと見間違いのどちらが困るかで決まります。</p>
  <div class="vg-fields">
    {fields.map((f) => (
      <article class="vg-field">
        <p class="vg-place">{f.place}</p>
        <h3 class="vg-field-h">{f.h}</h3>
        <p class="vg-p">{f.p}</p>
        <ul class="vg-tags">{f.tags.map((t) => <li>{t}</li>)}</ul>
        {f.links.length > 0 && (
          <ul class="vg-links vg-links-sm">
            {f.links.map((l) => <li><a href={l.href}>{l.t}<i class="ms">arrow_forward</i></a></li>)}
          </ul>
        )}
      </article>
    ))}
  </div>
</div></section>

<style>
  .vg { padding-bottom: 0; }
  .vg-lead { margin: -16px 0 32px; max-width: 46em; font: 400 16px/28px var(--sans); color: var(--ink-2); }
  .vg-p { margin: 0; font: 400 15px/27px var(--sans); color: var(--ink-2); word-break: auto-phrase; text-wrap: pretty; }

  /* 1 下から積む5段 */
  .vg-steps { list-style: none; margin: 0; padding: 0; display: grid; }
  .vg-step { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 24px; padding: 28px 0; border-top: 1px solid #e6e6e6; }
  .vg-step:last-child { padding-bottom: 0; }
  .vg-num { display: grid; place-items: center; width: 56px; height: 56px; border-radius: 8px; background: var(--dark); color: #fff;
    font: 700 20px/1 var(--sans); font-variant-numeric: tabular-nums; }
  .vg-step-h { margin: 0 0 8px; font: 700 18px/28px var(--sans); color: var(--ink); }
  .vg-step-body { max-width: 52em; }
  .vg-links { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
  .vg-links a { display: inline-flex; align-items: center; gap: 8px; padding: 7px 10px 7px 12px; border-radius: 4px; background: #f2f2f2;
    color: var(--ink); font: 500 13px/20px var(--sans); text-decoration: none; transition: background .15s; }
  .vg-links a b { font-weight: 700; font-variant-numeric: tabular-nums; }
  .vg-links a .ms { font-size: 15px; color: var(--ink-3); transition: transform .2s; }
  .vg-links a:hover { background: #e6e6e6; }
  .vg-links a:hover .ms { transform: translateX(2px); }

  /* 2 出力の形 */
  .vg-outs { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px 28px; }
  .vg-pic { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; margin-bottom: 14px; border-radius: 8px; background: #242424; }
  .vg-note { margin: 28px 0 0; font: 400 13px/22px var(--sans); color: var(--ink-3); }
  .vg-out-h { margin: 0 0 6px; font: 700 17px/26px var(--sans); color: var(--ink); }
  .vg-out-h span { margin-left: 8px; font: 400 12px/18px var(--sans); color: var(--ink-3); }

  /* 3 現場 */
  .vg-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
  .vg-field { padding: 28px; border-radius: 8px; background: var(--bg); }
  .vg-place { margin: 0 0 4px; font: 600 13px/20px var(--sans); color: var(--ink-3); }
  .vg-field-h { margin: 0 0 12px; font: 700 20px/30px var(--sans); color: var(--ink); }
  .vg-tags { list-style: none; margin: 16px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
  .vg-tags li { padding: 3px 8px; border-radius: 4px; background: var(--dark); color: #fff; font: 500 12px/18px var(--sans); }
  .vg-links-sm { margin-top: 16px; }
  .vg-links-sm a { background: #fff; }
  .vg-links-sm a:hover { background: #e6e6e6; }

  @media (max-width: 1023px) {
    .vg-outs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .vg-fields { grid-template-columns: minmax(0, 1fr); }
  }
  @media (max-width: 599px) {
    .vg-lead { margin: -16px 0 24px; font-size: 15px; line-height: 26px; }
    .vg-step { grid-template-columns: 40px minmax(0, 1fr); gap: 14px; padding: 22px 0; }
    .vg-num { width: 40px; height: 40px; font-size: 15px; }
    .vg-outs { grid-template-columns: minmax(0, 1fr); gap: 28px; }
    .vg-field { padding: 20px 18px; }
    .vg-field-h { font-size: 18px; line-height: 28px; }
  }
</style>
```

### 公開したページ

![シリーズのページの、このシリーズでやっていることのセクション。5段の番号と、段ごとの回へのリンクが並ぶ](https://pixelstomotors.com/media/LOG459_05_what_we_do.png)

![答えの形のセクション。同じ真上の写真に、分類・物体検出・セグメンテーション・姿勢推定・深度を重ね、追跡だけは録画から作った6枚](https://pixelstomotors.com/media/LOG459_04_outputs_photo.png)

![現場で使われている技術のセクション。6つの現場のカードに、使われる技術の札と関連の回へのリンク](https://pixelstomotors.com/media/LOG459_06_in_the_field.png)

[図解: LOG459_six_outputs.html](https://pixelstomotors.com/embeds/LOG459_six_outputs.html)

変更は2回の commit で公開した。

| commit | 中身 |
|---|---|
| 11d8b14 | 3つのセクションを足した（答えの形は記号の絵） |
| 0bad94e | 答えの形の絵を実写の版に差し替え、作り方を tools/vision_guide_tiles/ に残した |
