fullseye

高速ビジョン(1ms 視覚フィードバック)を物理シミュ上でやる — 計画

発端: ユーザー要望(2026-08-25)「fullseye で http://ishikawa-vision.org/movies/index-j.html にあるような事をしたい」「物理シミュレーション環境上でやりたいのです」。


1. 石川グループ研究室の 150 項目を貫く 1 本の軸

ページには 6 大分類・約 150 のデモがある(センサフュージョン / ダイナミック ビジョンシステム / システムビジョンデザイン / アクティブパーセプション / 光コンピューティング)。個々はばらばらに見えるが、通っている軸は 1 本しかない。

フレームレートではなく、遅延。

知覚から行動までの往復を 1ms 級まで縮めると、それまで不可能だった作業が可能になる —— 飛翔体のキャッチ、動く物体への投影、動く台座の上での精密作業。彼らは遅延を 下げるために 専用ハードを作った(ビジョンチップ、列並列ビジョン、DynaFlash 1000fps プロジェクタ、サッカードミラー)。つまり研究の大半は 「遅延を固定値として下げる工学」である。

2. 物理シミュ上でやる意味 — ここが差別化軸

実機では遅延は 設計して固定する量。掃引できない(ハードを作り直すことになる)。 シミュレーションでは 独立変数として自由に掃引できる

したがって我々が出せるのは点ではなく 関数 である:

        追従誤差 / 成功率  =  f(遅延, 対象の速さ, センサ雑音, 制御帯域)

これは実機の論文が原理的に出しにくい形の結果で、しかも実務的に価値がある。 「この作業を成立させるには何 ms 必要か」を、ハードを買う前に言える

Fullseye の使命(Physical AI / evis 視覚のための統一 I/F・即使える視覚アルゴリズム workbench)にそのまま乗る。視覚 op を「静止画に対する関数」ではなく 「閉ループの中の一段」 として評価する軸が増えることになる。

MuJoCo の timestep は既定 2ms、1ms や 0.5ms も普通に回る。1000Hz 制御はネイティブ

3. いま在るもの(2026-08-25 に実コードで確認)

モジュール 役割
sim_source.py MuJoCo が RGB / depth / 点群 / 内部行列 K を供給する F4 契約。物理はシミュ側、視覚は fullseye 側という分業がすでに引かれている
event_camera.py / events.py DVS(log 強度差分の閾値発火)。事象が動く輪郭に集中することを実測で確認済み
stereo_sim.py MuJoCo から矯正済みステレオ対を描画 → 自前ステレオで深度 → 真値で採点
lidar_sim.py / polar_cam.py / focus_stack.py 疑似 LiDAR / 極座標カメラ / 焦点合成
sensor_fusion.py 6 状態 Kalman が投擲物を追跡。位置単独・速度単独に勝つことを実測
evis_fullseye_bridge.py GPU 学習した evis の歩行ロールアウトを RGB / depth / DVS で「見る」
g1_policy_bridge.py GPU 学習した G1 方策を native MuJoCo で回す。疑似 LiDAR 光線が観測に入っており、視覚が閉ループに入っている唯一の既存例
flow.py / sceneflow.py オプティカルフロー、FoE、接触時間(TTC)、looming
odometry.py / motion.py / features.py / matching.py / measure.py 自己位置、動き、特徴、照合、計測

不足しているもの(実測確認): latency / 遅延モデル化しているコードは 1 行も無い (grep のヒットは遅延 import の別語義のみ)。sensor_fusion.py は推定までで制御に戻していない。 つまり 「遅延を持つ閉ループ台」が丸ごと無い。ここが最初の一手になる。

3b. 研究フェーズとしての設計(2026-08-25、ユーザー方針)

「画像処理寄りから行けば成果は得やすいし、記事にもしやすいが、やはり一つの 研究フェーズに数週間は時間をかけたい

画像処理寄りの危険は、成果が出やすいぶん 小さな勝ちが散らばって積み上がらない こと。避け方は一つしかない —— 小さな勝ちが全部、同じ一つの問いに feed する形に する。この節はそのための設計。

研究の問い(数週間かける価値のある形にしたもの)

閉ループにおいて、遅延はどの作業をどこで壊すか。 そしてその境界は、アルゴリズムを安くすることでどこまで動かせるか。

この形にすると、op を 1 つ実装するたびに交換曲線の上に点が 1 つ増える。 画像処理寄りの「得やすい成果」がそのまま research の素材になり、散らばらない。

事前登録した予測(着手前に書く)

段階ごとの関門(通らなければ先へ進まない)

段階 関門 通らなかったら
H1 台 遅延 0 で誤差 ≈ 0、遅延↑で誤差が単調増加 台が壊れている。先へ進まない
H2 追従 Self-Windowing が計算量を実測で減らし、追従が壊れない 窓の更新則を設計し直す
H2b じゃんけん 勝率が遅延 0 で 1.0、大遅延で 1/3 に落ちる ベンチが成立していない
§4b 進化 時間予算つきと精度のみで 選ばれる op が違う 予算が緩すぎる。締める
H3〜H5 各作業の閾値が P2(速さに反比例)に乗る 乗らない理由を突き止めるほうが本題になる

記事について

記事は副産物であって目的ではない。運動会第二弾(案 B 本体 + 案 A 横串)の 「A = 速いほうが強い」の柱がここから供給されるが、記事のために結論を先に 決めない。交換曲線が「一番正確なやつが勝つ」形になったら、そう書く。

4. 段階(H1 〜 H6)

H1. 遅延を測れる閉ループ台 — 他の全部の前提

新規 vloop.py。MuJoCo を step しながら {描画 → 知覚 → 制御} を回す。要点は 遅延を明示的なパイプライン段数として持つこと:

    センサ遅延 n_s ステップ(露光 + 転送)
    計算遅延   n_c ステップ(知覚アルゴリズムの実行時間)
    制御遅延   n_a ステップ(アクチュエータ応答)

n_c は実測値を使うモードと、手で指定して掃引するモードの両方を持つ。

H2. 高速トラッキング + アクティブ注視(サッカードミラー相当)

2 自由度の雲台に載せたカメラで飛翔体を画面中心に保つ。

H2b. じゃんけん — 遅延の効果が二値で出る最良のベンチ

ユーザーは実機を東京大学 山川研究室で見ている(2026-08-25 の証言)。石川研の ページでは「じゃんけんロボット」としてセンサフュージョンの分類に入っている。

これを H2 の直後に置くのは、遅延の効果が最もきれいに出る課題だから:

測るもの: 勝率 = f(遅延)。遅延 0 で勝率 1.0、遅延を上げるとどこかで 1/3(でたらめ)に落ちる。その落ちる位置と急峻さが答えになる。 実機では 1 点しか測れないが、シミュなら曲線全体が出る。

シミュ上の実装: 人間の手は MuJoCo 上の 3 姿勢を持つ多指ハンド(既存の MyoHand か 簡略ハンド)。指定時刻に向けてグー / チョキ / パーへ遷移させる。

判定アルゴリズムは既に答えが分かっている。ユーザーが実機で見た中身(一次観察、 2026-08-25):

手のひらを中心に 円弧状のプロファイル を取り、そこを横切る 指の本数を 数える。ルールベースを GPU で回しているだけ。

これは手抜きではなく 1ms に収めるための必然。円弧プロファイルは O(円周) で 画像全体を触らない。H2 の Self-Windowing とまったく同じ思想 (処理量を対象の大きさに比例させ、画面の大きさに依らせない)。 知能はアルゴリズムの側に無く、全部が遅延の側にある

正直に開示すべき点: このロボットが勝つのは「速いから」であって 「読んでいる」からではない。人間から見れば ズルであり、そこを隠さないことが このデモの誠実さになる(evis 物理動画で確立した方針と同じ)。 勝率曲線を出すことは、そのズルの大きさを定量することでもある。

4b. 横断軸 — 時間予算つきの適応度(この計画が Fullseye 本体に返すもの)

上の一次観察から、高速ビジョンは Fullseye の適応度関数を変える ことが分かる。

いまの進化は 精度だけ で選抜している(evolve.py の docstring: “Fitness is the TRAIN score only”。実行時間の項は無い —— grep で確認済み)。 それでは「重くて正確なもの」が必ず勝ち、円弧プロファイルのような 安いが十分なもの は選ばれない。

    従来:  適応度 = holdout 精度
    高速:  適応度 = 時間予算 T 以内に返せた場合の精度。超えたら失格(0 点)

時間のペナルティを足すのではなく、硬い制約にすることが要点。閉ループでは 遅延はコストを連続的に増やすのではなく、ある点から 破綻 させるため。 適応度の形は現実の形に合わせる。

★ここが一番面白い実験になる

時間予算を与えた Fullseye は、円弧プロファイルを自力で再発見するか。

これがあるので、H6(帯域・計算量の制約)は「余力があれば最後に」ではなく H2b と同時に走る横断軸 に格上げする。じゃんけんは「遅延の効果を測るベンチ」で あると同時に 「時間予算つき進化の検定台」 になる。

H3. キャッチング(視覚フィードバックで飛翔体を捕る)

既存の Panda gripper(ctrlrange [0,255]、6DoF IK は world フレーム)か MyoHand を使う。

H4. 動的補償(Dynamic Compensation)

台座が動いていても工具先端を world 座標で静止させる。視覚で台座の動きを測り、 腕で打ち消す。

H5. 動的プロジェクションマッピング

動く物体の表面にテクスチャを貼り続ける。シミュなので投影機の光学モデルが要らない —— 「投影」は物体表面 UV への書き込みとして実装できる。

H6. ビジョンチップの制約を模した設計(§4b の横断軸として H2b と同時に走らせる)

「全画素を毎フレーム読み出せない」という帯域制約(列並列、限られた転送量)を 明示的に課したとき、どのアルゴリズムが生き残るか。

5. やらないこと(honest)

6. 最初の一手

H1 の vloop.py。これが無いと H2 以降のどれも測れない。

成立確認は 1 つだけ: 遅延 0 で誤差がほぼ 0、遅延を上げると誤差が単調増加。 これが出なければ先へ進まない。

7. 来歴

石川グループ研究室のデモ一覧は上記 URL(2026-08-25 取得)。個々の手法名 (サッカードミラー、Self-Windowing、DynaFlash、VarioLight、Lumipen、列並列ビジョン、 ビジュアルショックアブソーバ、動的補償)はそのページの記載から取った。 一次文献(論文)にはまだ当たっていないので、実装に入る段階で確認すること。

じゃんけんロボットの実機を見た経験(東京大学 山川研究室)と、その画像処理が 「手のひら中心の円弧プロファイル + 指の本数カウント」であったことは ユーザーの 一次観察(2026-08-25)。論文では確認していない。実装前に当たること。


H1 の結果(2026-08-25)— 台は成立した

実装 vloop.py。上から見た 1 次元の追従。赤い球が勝手に動き、緑の板が 画像だけを頼りに 追う。知覚 = 赤画素の重心(0 次と 1 次のモーメント)。 timestep = 1 ms なので 遅延のステップ数 = ミリ秒

板の動特性: 質量 1.08 kg / kp 8000 / damping 150 → 固有 13.7 Hz・減衰比 0.81。 目標の最高成分 1.9 Hz に対し 7 倍の余裕。

error(latency)

遅延 ms RMSE m 最大 m
0 0.0341 0.0641
1 0.0358 0.0673
2 0.0375 0.0705
4 0.0409 0.0768
8 0.0477 0.0895
16 0.0613 0.1148
32 0.0883 0.1647
64 0.1403 0.2609
128 0.2319 0.4318

何もしない場合(板を動かさない)の誤差 = 0.2303 m。対象の平均速度 = 1.434 m/s。

関門(全て合格)

  判定 実測
G1 遅延 0 で閉ループが効く 合格 0.0341 / 0.2303 = 0.15(< 0.25)
G2 誤差が遅延に単調増加 合格 Spearman 1.000
G3 大遅延で壊れる 合格 128 ms で 0.2319 ≈ 何もしない 0.2303
G4 傾き ≈ 対象の平均速度 合格 傾き 1.694 m/s 対 平均速度 1.434 m/s(比 1.18)

G4 が重要。「誤差 ≈ 速度 × 遅延」という素朴な予測が実測にそのまま乗った。 台の物理が正しいことの強い証拠で、H4(動的補償)と H5(投影)で立てる同じ形の 予測も、この台の上でなら検証できることを意味する。

検算 — 遅延はどの段に載せても同じ(ばらつき 0.0%)

32 ms をどこに載せるか RMSE
n_sensor(露光・転送) 0.0883
n_compute(知覚の実行時間) 0.0883
n_act(駆動の遅れ) 0.0883
三等分 0.0883

完全に一致。 効くのは合計遅延だけで、内訳は効かない。 以後 3 段を 1 つの数 に畳んで扱ってよい。docstring に書いていた 「どの段でも同じはず」は推論だったので、測って確定させた。

早くも出た最初の知見 — 予測補償の効く範囲

実効遅延ぶんを線形外挿で先読みすると:

遅延 ms 補償なし 補償あり
0 0.0341 0.0341
8 0.0477 0.0341(完全に打ち消す)
16 0.0613 0.0345
32 0.0883 0.0378
64 0.1403 0.0616
128 0.2319 0.1660(28% しか戻らない)

32 ms までは遅延をほぼ完全に打ち消し、64 ms を超えると効かなくなる。 理由は明快で、対象の運動が 0.7 Hz と 1.9 Hz の和なので、1.9 Hz の周期 526 ms に対し 128 ms は 4 分の 1 周期。線形外挿が破綻する。

つまり 遅延の予算は絶対値では決まらない。「対象をどれだけ先まで予測できるか」で 決まる。これは §3b の問いに対する最初の部分的な答えで、H3(キャッチング)以降で 対象の予測可能性を変えながら確かめる。

途中で直した実装の誤り(記録)

  1. カメラの画角不足 — fovy 既定 45°、z=1.4 では半幅 0.555 m しか写らず、 較正点 x=±0.6 が視野外。fovy 70° に。
  2. 共振 — 板の初期設定 kp 300 / damping 4 は固有 2.65 Hz・減衰比 0.11 で、 目標の 1.9 Hz 成分と共振していた。遅延 0 でも追従できず G1 不合格 (0.1451、比 0.63)。kp 8000 / damping 150 に。
  3. 隠れた +1 msqpos を書き換えた後 mj_forward を呼ばずに描画していたので、 画像が 1 歩前の運動学を映していた。修正で「見失い」も 3 → 0 になり、 診断の裏も取れた。

H2(モーメント追跡 + Self-Windowing + アクティブ注視)。台はそのまま使える。


§4b の測定(2026-08-25)— 精度は「足りていれば」それ以上効かない

実装 vloop_algos.py。H1 の台の上で 同じ課題を 5 つの違う値段の検出器で解き、 各検出器の実行時間をそのまま閉ループの遅延として食わせる。 5 ms かかるアルゴリズムは 5 ms の遅延そのものである。

作動点 1024x1024。理由は下の「最初の試行は失敗した」を参照。

1. 静止画での精度と値段

検出器 誤差 mm 実測 ms 画素演算 ms(宣言)
全画面重心 0.12 8.29 5.24
窓重心(2 次元 Self-Windowing) 0.12 0.18 0.08
間引き重心(1/4) 0.47 0.29 0.33
行走査(1/4) 0.15 1.86 1.31
テンプレート照合(NCC + サブピクセル) 0.72 43.84 629.1

2. 閉ループ(実行時間 = 遅延)

検出器 遅延 ms 閉ループ RMSE m
間引き重心(1/4) 0 0.0341
窓重心(2 次元) 0 0.0341
行走査(1/4) 2 0.0376
全画面重心 8 0.0479
テンプレート照合 44 0.1087

静止画 1 位の全画面重心が閉ループでは 4 位。 P4 は支持された。 (1 位は間引きと窓重心の同点。どちらも遅延 0 に丸まるため。順位の細部ではなく 下の対照のほうが本質。)

3. 対照 — 全部を遅延 0 で回す(= 精度だけの評価)

検出器 RMSE m
間引き重心 / 行走査 / 全画面重心 / 窓重心 0.0341
テンプレート照合 0.0342

5 種すべてが並ぶ。 静止画の精度差 6 倍(0.12〜0.72 mm)が、閉ループ性能に 一切現れない。どの検出器の誤差も、閉ループ自身の追従誤差 34 mm より 2 桁小さいため。

  開き 閉ループへの影響
静止画の精度 6 倍(0.12 〜 0.72 mm) ゼロ
実行時間 240 倍(0.18 〜 43.8 ms) 3.2 倍

ここから出た設計則

知覚の精度は、閉ループ自身の追従誤差を下回っていれば十分。 それ以上の精度は閉ループ性能に一切効かない。余った予算は全部速度に回すべき。

これは石川研のじゃんけんロボットが「手のひら中心の円弧プロファイル + 指の本数」で 済ませていた理由そのもの。手抜きではなく、精度をそれ以上上げても意味がない領域 だった。ユーザーの一次観察が、測定で裏付けられた形になる。

最初の試行は失敗した(記録)

解像度 128 で始めたところ、5 種すべての実行時間が 0.01〜0.21 ms に収まり、 1 ms 刻みに対して全部 0 ms に丸まった。遅延軸がまったく効かず、実験が成立しない。 あわせて 128 px では球が 6 px しかなく、間引き重心は 60/60 見失い(安い検出器ではなく 壊れた検出器)、テンプレート径 13 px も球と合わず誤差 12.5 mm だった。

これ自体が知見: Self-Windowing の価値は「費用をセンサの大きさから切り離すこと」 なので、センサが計算予算より大きいときにしか現れない。実測 res 128/512/1024 で 全画面重心 0.02/0.43/2.89 ms。1024 で初めて 1 ms 予算に入らなくなる。

また、最初の窓重心は列だけを切っていて(img[:, lo:hi])行は全走査だった。 それでは画面の高さに比例したままで Self-Windowing になっていない。2 次元に直した。

費用モデルの食い違い(honest)

実装非依存の「画素演算数」モデルは、NCC を 629 ms と見積もったが実測は 43.8 ms (14 倍の過大評価)。FFT の定数を 6 N log2 N と置いたのが粗すぎる。 逆に全画面重心は 5.24 対 8.29 で妥当。2 つの費用モデルは NCC で大きく食い違う ので、片方だけを載せない。GPU / FPGA / ビジョンチップに移せば定数は変わる —— 石川研がハードで解いたのはこの定数の部分。

次に要ること

いまの課題では 精度が「余っている」ので費用が全て になった。交換曲線を 本当に描くには、精度が効く領域まで課題を難しくする必要がある:

そこで初めて「安いが粗い」対「高いが正確」の 交換の前線 が見える。 いまの結果はその前線の 片側の端 を測ったことになる。


交換の前線(2026-08-25)— 交点が出た

前節で「精度が余っているので費用が全て」になった。雑音を入れて精度が効く領域まで 難しくする。偽の赤画素を割合 p で撒く(現実の対応物 = ホットピクセル、反射、赤い別物)。

静止画の誤差 mm

p 全画面重心 窓重心 間引き 行走査 テンプレート
0 0.11 0.11 0.49 0.15 0.74
0.0002 18.36 1.06 18.71 20.57 0.72
0.0010 70.27 4.33 75.88 70.91 0.73
0.0050 151.47 255.14 158.63 154.73 0.73

閉ループ RMSE m(実行時間を遅延として食わせたまま)

括弧内は各検出器の遅延。

p 全画面(9ms) 窓(1ms) 間引き(0ms) 行走査(2ms) テンプレート(43ms)
0 0.0440 0.0298 0.0297 0.0326 0.0875
0.0002 0.0413 0.0298 0.0291 0.0320 0.0874
0.0010 0.0676 0.0298 0.0669 0.0665 0.0874
0.0050 0.1422 0.6166 0.1430 0.1426 0.0874

低雑音では費用が全て、高雑音では精度が全て。交点は p=0.001 と 0.005 の間。 これが §3b で成果物と宣言した交換曲線の最初の 1 本。

読み取れる 3 つのこと

(1) 窓重心は雑音に対して完全に平坦(0.0298 が 3 水準で不変)。小窓が外れ値を 構造的に見ないので 安さと頑健さを同時に持つ。Self-Windowing が石川研で 使われた理由の、費用以外の側面。

(2) しかし優雅に劣化せず、崖から落ちる。 p=0.005 で 0.6166 は 何もしない場合(0.23)より悪い。頑健さの源が「見ないこと」なので、 一度間違えると戻れない(全画面への復帰先も汚染されており雑音にロックオンする)。

壊れた知覚は、知覚が無いより悪い。

実機の石川研がこれを回避できるのは、1000 fps では対象がフレーム間でほとんど 動かず窓が外れないから。フレームレートは精度のためではなく、窓が外れないために要る。 これは「速さ」の 3 番目の理由(1. 遅延を減らす 2. 予測を要らなくする 3. 追跡の連続性を保つ)。

(3) テンプレート照合は全水準で 0.0874 の一定値。 塩胡椒雑音に完全免疫 (53 px の円盤形状と相関を取るので孤立画素は一致しない)だが、43 ms の費用を 永久に払う。だから高雑音でしか勝てない。

表をまたいだ比較はできない(honest)

この節の閉ループ数値(0.0297 など)は前節(0.0341 など)と条件が違う (800 歩・位相 1 通り 対 1200 歩・位相 2 通り。雑音掃引の時間を抑えるため)。 節の中での比較だけが有効。


§4b の進化(2026-08-25)— 雑音なしでは検定にならなかった

実装 vloop_evolve.py。遺伝子 = {窓を使うか / 窓の一辺 / 間引き / しきい値 / 重心かテンプレートか / テンプレート径}。個体 8 x 世代 8、適応度を 2 通り。

適応度 選ばれた検出器 静止画 mm 費用 ms
A(精度だけ) 窓 350 / 間引き 1-5 / しきい 114 / 重心 0.28 0.04
B(時間予算) 窓 332 / 間引き 1-5 / しきい 132 / 重心 0.71 0.04

P3 は支持されなかった。 「違う」と出たのはしきい値の差だけで、実質同じもの。 費用は同一。

理由は明快で、この探索空間には支配解がある。窓 + 間引き 5 + 重心が精度でも 費用でも十分良いので、交換すべきものが存在しない。適応度を変えても選択が変わらない。

今日 2 つ目の、同じ形の失敗

  Afterman 実験 NB Fullseye §4b 進化
問い 進化した構造は手設計を上回るか 時間予算つき適応度は違うものを選ぶか
結果 上回らない 同じものを選ぶ
原因 空間に有用ビットが 1 つしかない 空間に支配解がある

どちらも「探索の価値」を問う実験で、空間に支配解があると検定にならないという 同じ構造で失敗した。偶然ではなく、この種の実験の共通の落とし穴。

探索の価値を測る実験は、まず「空間に支配解が無いこと」を確かめてから走らせる。

これは事前に確かめられる。全遺伝子を総当たりする必要はなく、 両端(最も安い / 最も正確)を手で作って、片方が両軸で勝っていないかを見るだけでよい。

前向きな面

Fullseye の進化は、人間の設計者と同じ答えに独立に到達した。 「小窓だけ見る + まばらに標本化 + モーメント」は、じゃんけんロボットの円弧プロファイルと同じ論理 (対象の居る所だけを、粗く、0 次と 1 次のモーメントで)。 ただし そこへ行くのに時間予算は要らなかった

直し方 — 前線が作動点を教えている

交換の前線の測定によれば、p=0.001 で静止画 1 位 = テンプレート照合、 閉ループ 1 位 = 窓重心 と割れている。そこで進化させれば適応度は分岐するはず。 vloop_evolve.py 0.001 で走行中。


§4b の進化・雑音下(2026-08-25)— P3 も P4 も確認。予測 3 件が全て当たった

割れている作動点(p=0.001)で同じ進化を回す。vloop_evolve.py 0.001

適応度 選ばれた検出器 静止画 mm 費用 ms
A(精度だけ) 全画面 / 間引き 1-1 / テンプレート(53) 0.87 48.59
B(時間予算) 窓 317 / 間引き 1-8 / テンプレート(48) 3.70 0.07

交差評価 — これが P4 の直接の証拠

  A が選んだもの B が選んだもの
静止画の誤差 0.87 mm(4.3 倍正確) 3.70 mm
閉ループ RMSE 0.0791 0.0219(3.6 倍良い)
費用 48.59 ms 0.07 ms(694 倍安い)

精度だけの適応度は、静止画で 4.3 倍正確だが閉ループで 3.6 倍悪い検出器を選ぶ。

事前登録した予測の成績 — 3/3

予測 結果
A は use_window=False・stride=1 を選ぶ 当たり(全画面・間引き 1-1)
B は use_window=True・stride > 1 を選ぶ 当たり(窓 317・間引き 1-8)
2 つの適応度が選ぶ個体は違う(P3) 当たり

予想していなかった、もっと重要なこと

両方ともテンプレート照合を選んだ。 雑音下では重心が脆いので、 アルゴリズムの選択は一致した。違ったのは どこを、どれだけ細かく見るか だけ。

B の答え = 「頑健なアルゴリズムを、小窓に、粗く適用する」

これは「安物のアルゴリズムに替える」のとは違う。アルゴリズムの選択と、 適用範囲・密度の選択は 分離できる量 であり、時間予算が変えるのは後者だけ だった。

じゃんけんロボットの円弧プロファイルも、この読み方のほうが正確だと思う —— 「粗いアルゴリズムを使った」のではなく「手のひらという既知の位置の周りだけを、 円弧という 1 次元の標本で見た」。適用範囲と密度を絞ったのであって、 アルゴリズムを妥協したのではない。

workbench としての意味

Fullseye の進化は、時間予算を与えられただけで、人間の設計者が実機で使った設計原理に 独立に到達した。§4b で「一番面白い実験」と書いた問いへの答えは 肯定 である。

ただし条件がある。雑音なし(支配解のある空間)では到達しても意味が無い —— 前節のとおり、そこでは適応度を変えても選択が変わらないので、 「到達した」ことが workbench の能力の証拠にならない。 支配解が無い空間で、かつ適応度が分岐する作動点で測って、初めて証拠になる。


予算地図(2026-08-25)— 前節の主張に条件が付いた

budget_map.py。(対象の速さ x 雑音)の 4 作動点で、2 つの適応度が何を選ぶかを見る。

速さ 雑音 適応度 A が選ぶ A 閉ループ 適応度 B が選ぶ B 閉ループ A/B
1x 0.0010 全画面 / 1-1 / テンプレート(53) 0.0743 窓 317 / 1-8 / テンプレート(48) 0.0219 3.39
2x 0.0010 全画面 / 1-1 / テンプレート(53) 0.1888 窓 332 / 1-6 / 重心 0.0670 2.82
1x 0.0050 全画面 / 1-1 / テンプレート(56) 0.0670 窓 260 / 1-8 / テンプレート(48) 0.0220 3.05
2x 0.0050 全画面 / 1-1 / テンプレート(56) 0.1801 窓 375 / 1-5 / 重心 0.0670 2.69

前節の主張を訂正する

前節で「時間予算が変えるのは適用範囲と密度だけで、アルゴリズムの選択は変えない」と 書いた。それは速さが一定のときだけ成り立つ話だった。

速さが 2 倍になると、B は テンプレート照合から重心へ、アルゴリズムそのものを替えた。 正しくは:

時間予算はまず「どこをどれだけ細かく見るか」を変える。 時間圧が上がると、次に「どのアルゴリズムか」まで変える。

理由は H1 で測った関係から出る。誤差 ≈ 速度 x 遅延 なので、速さが 2 倍になると 1 ms あたりの罰が 2 倍になる。窓に絞ったテンプレート照合(0.07 ms)ですら高すぎて、 頑健性を売って速さを買う ことになる。二段構えの応答で、一行の主張より面白い。

構造的な予測が実証された

適応度 A は速さが変わっても選択がまったく同じ(1x と 2x で完全に同一)。 静止画評価に対象の速さが入らないので、原理的に作動点に反応できない。 雑音にだけは反応する(しきい値 121 -> 154、テンプレート径 53 -> 56)。

これは「静止画ベンチマークで選んだアルゴリズムを閉ループに持ち込む」という 一般的な実務が、作動点が変わっても同じ答えを出し続ける ことを意味する。 B は 4 作動点すべてで A に 2.7〜3.4 倍勝っている。

予算地図としての読み方

作動点 時間予算が要求する設計
遅い・低雑音 何でもよい(§4b 測定のとおり精度は余っている)
遅い・高雑音 頑健なアルゴリズムを、小窓に、粗く
速い 頑健性を捨てて安いアルゴリズムへ。窓と間引きは維持

「この作業に何 ms 必要か」だけでなく「この作動点はどの設計を要求するか」まで 言えるようになった。これが §3b で成果物と宣言した交換曲線の完成形に近い。


H2b じゃんけん(2026-08-25)— 円弧プロファイルを実装して測った

実装 janken.py。人の手(5 本指、手のひらは円盤)が「握った待機」から reveal_ms = 200 かけて目標の形へ開く。ロボットは画像だけを見て勝つ形を作る。

円弧プロファイルはちゃんと動く

ユーザーの一次観察どおりに実装した —— 手のひら中心の円周を 64 点で標本化し、 手の画素が連続する区間の数を数える。較正済みの半径 1 つで、完成形を完全に分離した

円弧の半径(握り拳の半幅に対する比) グー チョキ パー
1.2 0 2 6
1.4 0 2 5
1.6 0 2 2
1.8 0 0 0

k=1.4 でちょうど 0 / 2 / 5。ただし 窓が狭い —— 安いが較正に脆い。

手のひらが丸くないと成立しない。 最初は手のひらを四角い箱にしていて、円弧が角を 横切り グー 2 / チョキ 4 / パー 7 と出た。手の形そのものがアルゴリズムの前提

予想外 — 円弧は判別が いちばん遅い

検出器 グー チョキ パー 費用(256px)
円弧 0 130 51 0.192 ms
追跡円弧(Self-Windowing) 0 130 51 0.144 ms
面積(充実度) 0 25 37 0.224 ms
テンプレート照合 0 57 55 0.316 ms

数字は「初めて正解し、以後ずっと正解であり続けた時刻 [ms]」。reveal は 200 ms。

円弧はしきい値型なので、指が円周を越えるまで何も起きない。面積(充実度)は 指が動き始めた瞬間から連続的に反応する。

費用の安さと判別の早さは別の量で、閉ループで効くのは合計。 円弧プロファイルは前者に最適化して後者を犠牲にしている。

自分の仮説を 1 つ反証した

「私のモデルは指が短いから円弧を越えるのが遅いのでは」と考えて指の長さを振った。 逆だった:

指の長さ 円弧 チョキ 円弧 パー
0.080 130 51
0.110 130 102
0.140 134 127

半径を握り拳の幅から較正しているので、指が長いと拳も大きくなり半径も伸びる。 判別の遅さは手の形の問題ではなく、しきい値型であることに内在する。

実装で踏んだ誤り(記録)

  1. アクチュエータに名前を付けていなかった —— model.actuator("human_j0") が KeyError。<position name=... joint=...> が要る。
  2. 度 / ラジアンの罠を 2 回。MuJoCo の既定は angle="degree":
    • カメラの euler="1.5708 0 0"1.57 度しか回らず真っ暗な画になった
    • 関節の range="-0.1 1.6"-0.1 度 〜 1.6 度になり、可動域の拘束力 (qfrc_con = -6.3)がアクチュエータと綱引きして指が 1.24 rad で止まった。 較正した姿(qpos 直接指定)と試合中の姿がずれ、面積検出器が遅延 0 でも 勝率 0.33(あいこ 0.67)になっていた → <compiler angle="radian"/> を明示して解決。診断は qfrc_actuatorqfrc_constraint を並べて見たら一発だった。
  3. 「円弧は O(標本点数)」は実装として嘘だった —— 手のひら重心を出すために 全画面のマスクを作っていた(実測 0.192 ms、全画面型の面積 0.224 ms とほぼ同じ)。 手のひらを追跡する版(detect_arc_win)を足して 0.144 ms に。 これは §4b で測った Self-Windowing そのもの で、2 つの実験が同じ機構に合流した。
  4. テンプレート照合の正規化忘れ —— 生の相関だと画素数の多いパーが必ず勝つ。 外接矩形を切り出して 48x48 に正規化してから相関を取る。

勝率 = f(遅延) と、遅延の予算

reveal_ms = 200、保持 120 ms、解像度 256:

遅延 ms 円弧 追跡円弧 面積 テンプレート
0 〜 120 1.00 1.00 1.00 1.00
160 0.67 0.67 1.00 1.00
200 0.67 0.67 1.00 1.00

円弧が最初に崩れる。 判別が 130 ms と遅いぶん、遅延に使える余裕が少ない。

遅延の予算(勝率 1.00 を保てる最大遅延)を直接測る:

人が出す時間 円弧 追跡円弧 面積 テンプレート
150 130 130 200 190
200 150 150 240 220
300 180 180 320 290

判別時刻(最悪の姿勢):

人が出す時間 円弧 追跡円弧 面積 テンプレート
150 98 98 28 43
200 130 130 37 57
300 195 195 56 85

台が閉じた形で説明できた

予算 + 判別時刻 は、検出器によらず一定:

人が出す時間 予算 + 判別(4 検出器) (reveal + hold) 差 = 形成時間
150 228 / 228 / 228 / 233 270 42
200 280 / 280 / 277 / 277 320 42
300 375 / 375 / 376 / 375 420 45

遅延の予算 = (reveal + hold) − 判別時刻 − 形成時間

判別時刻と遅延は 1 対 1 で交換できる。 1 ms 早く判別できれば、遅延の予算が ちょうど 1 ms 増える。前節で「費用の安さと判別の早さは 別の量」と書いたが、 正確には 加算的で交換可能な同じ通貨 だった。

これは H1 の「遅延はどの段に載せても同じ、合計だけが効く」の自然な拡張で、 判別時刻も同じ和の一項である。

判別時刻は reveal の一定割合(円弧 0.65、面積 0.19、テンプレート 0.28)なので、 予算は reveal の一次式になる:

    予算 = reveal x (1 - 判別率) + hold - 形成時間

    円弧 (0.65): 200 x 0.35 + 120 - 43 = 147    実測 150
    面積 (0.19): 200 x 0.81 + 120 - 43 = 239    実測 240

事前登録した「崩れる点は reveal_ms に比例する」は 当たり。さらに一次式の 係数まで決まった(比例ではなく、切片 hold - 形成時間 を持つ一次式)。

じゃんけんロボットの読み方が 3 回変わった

実測を経て、ユーザーの一次観察の解釈がこう変わった:

  1. 最初 —— 「粗いアルゴリズムで済ませている」
  2. §4b 後 —— 「粗いのではなく、適用範囲と密度を絞っている」
  3. H2b 後 —— 「絞る代わりに判別が遅くなる。それでも成立するのは、 センサが大きくて費用が支配的だから

256 px では全検出器が 0.3 ms 未満なので費用が効かず、判別の早い面積検出器が勝つ。 円弧の存在理由は 1024 px 級のセンサで全画面処理が予算に入らないとき に現れる (§4b の測定と同じ構造)。これは円弧プロファイルの否定ではなく、 成立条件の特定 である。

円弧に最良の半径を与えても遅い(自分の結論を検算した)

「判別が遅いのは半径を拳から一律 1.4 倍で較正したせいでは」と疑い、 アルゴリズムに最良の設定を与えて 測り直した。

作動する半径帯(完成形を 0/2/5 に正しく分ける範囲)は k = 1.2 〜 1.5:

k R px 判別時刻 グー / チョキ / パー 最悪
1.2 58.8 0 / 131 / 113 131
1.3 63.7 0 / 130 / 111 130
1.4 68.6 0 / 130 / 51 130
1.5 73.5 0 / 128 / 71 128
(面積) 0 / 25 / 37 37

帯の全域で 128 〜 131 ms。半径をどう選んでも変わらない。 結論は維持される。

なぜ構造的に遅いのか — 2 段の説明(この節は二値版にのみ当てはまる。下の訂正を参照)

(1) しきい値の共有による矛盾

グーを 0 本と読むには、半径は 握った指の到達点より外 に無ければならない。 ところがチョキの指も 同じ点を通過 しないと数えられない。 「グーを 0 と読む」ことと「チョキを早く読む」ことは同じしきい値を共有していて、 両立しない

これは TRIZ でいう物理的矛盾そのもの(同じ量に相反する要求)。実際の手で成立して いるのは、指の長さが手のひらに対して大きく、握った位置と伸びた位置の隔たりが 広いからだと思われる(私のモデルは 手のひら半径 0.075 に対し指 0.080)。

(2) 回転の投影は cos なので、動き始めは伸びない

指は根元のヒンジで回る。半径方向の到達点は R_palm + L cos(θ) で、 θ が 1.45 rad から動き始めても cos の平坦部 なのでほとんど伸びない。 しきい値型の検出器は その平坦区間を丸ごと捨てている。 面積(充実度)は 2 次元の積分なので、回転の最初から連続的に反応する。

しきい値型は「動きの最後」しか見ない。積分型は「動きの最初」から見る。 時間圧が高い課題では、この差が費用の差より大きくなりうる。

これは今日 §4b で得た「安いほうが勝つ」を、もう一段限定する。 安さは判別の早さと交換される場合があり、閉ループで効くのは和のほう。


H2b の訂正(2026-08-25)— 私の実装が間違っていた

上で「円弧プロファイルは構造的に判別が遅い」と結論し、2 段の説明まで付けた。 それは円弧プロファイル法の性質ではなく、私の二値実装の性質だった。

ユーザーの説明を読み直した:

手のひらを中心に 円弧状のプロファイル取得後 の指の本数カウント

プロファイルは信号であって二値ではない。 私が実装したのは 「固定半径の円周を横切るか」という最も過酷な二値版だった。 各方向への 手の広がり(半径方向プロファイル) を測って山を数える版を実装し直す (detect_arc_profile)。突出のしきい値は他の検出器と同じ較正予算で決める (3 姿勢の完成形から中点。実測 グー 0.21 / チョキ 0.48 / パー 0.46 x 半径 -> しきい 0.331)。

判別時刻が 2.7 倍早くなった

検出器 グー チョキ パー 最悪
円弧(二値) 0 130 51 130
円弧(プロファイル) 0 49 46 49
面積 0 25 37 37
テンプレート 0 57 55 57

そして解像度を上げると逆転する — 交点を実測で捉えた

janken_scale.py。予算 = (reveal + hold) − 判別 − 形成 − 費用。

解像度 円弧プロファイル 費用 / 予算 面積 費用 / 予算 勝者
256 0.15 / 227.9 0.21 / 239.8 面積 (+11.9)
1024 1.22 / 229.8 6.18 / 232.8 面積 (+3.0)
2048 6.38 / 223.6 26.44 / 213.6 円弧プロファイル (+10.0)

円弧プロファイルの予算は解像度によらずほぼ一定(228 / 230 / 224)。 面積は落ちていく(240 / 233 / 214)。交点は 1024 と 2048 の間 —— まさに高速ビジョンが扱うセンサ規模。

ちなみに二値版の円弧は 2048 で予算 127、追跡つきでも 134 で、 プロファイル版(224)に遠く及ばない。同じ「円弧」でも実装で 100 ms 違う。

何を間違えたか(方法論の教訓)

  1. 一次観察の言葉を精読しなかった。 「プロファイル取得後の本数カウント」の 「プロファイル」を落として二値に読んだ。
  2. アルゴリズムに最良の実装を与える前に結論を出した。 半径の最適化は やったが(それは正しかった)、実装の形そのものを疑わなかった
  3. しかも「構造的に遅い」という もっともらしい 2 段の説明まで作った。 説明が付いてしまうと検算しなくなる。説明の説得力は正しさの証拠ではない。

他人の一次観察を実装で再現するときは、うまくいかなかったら 「相手の観察が不正確」より先に「自分の実装が相手の言葉どおりでない」を疑う。

結論(訂正後)

ユーザーが実機で見た円弧プロファイルは、その規模のセンサに対して正しい選択だった。 費用が画面の大きさにほぼ依らないので、センサが大きいほど有利になる。 1024 px 級で全画面型と並び、2048 px で追い越す。

そして今日の主題ともきれいに繋がる: 「頑健なアルゴリズムを、小窓に、粗く適用する」(§4b)の、 分類課題における具体形が円弧プロファイルだった。


H5 動的プロジェクションマッピング(2026-08-25)— 予測 2/2 的中

実装 projmap.py。板が滑りながら 回る。系は画像モーメント(0 次 = 面積 / 1 次 = 重心 / 2 次 = 傾き)で姿勢を推定し、板の局所座標の模様を画像へ写す = 投影する。

シミュレーションなので投影機の光学モデルが要らない。 実機の VarioLight / Lumipen が解いた光学・機械の問題を飛ばして、遅延の効果だけを測れる。

並進 — ズレ ≈ 速度 x 遅延

並進 0.710 m/s、模様半径 75 mm:

遅延 ms ズレ mm 予測 v x L
0 0.65 0.00
8 5.76 5.68
16 10.85 11.35
32 20.98 22.71
64 41.02 45.42

H1 で追従誤差について確認した関係が、投影でもそのまま成り立つ。

回転 — 中心はゼロ、外周は半径に比例

並進を止めて回転だけ(2.892 rad/s)、遅延 16 ms:

模様半径 mm ズレ 中心 外周 予測 w x L x r
20 1.01 0.04 1.08 0.93
50 2.53 0.04 2.69 2.31
75 3.80 0.04 4.03 3.47
100 5.07 0.04 5.37 4.63
140 7.09 0.04 7.52 6.48

中心のズレは 0.04 mm で一定(回転は重心を動かさないので当然)。 外周は 半径に完全に比例(外周/半径 が全て 0.054)。

設計則

投影のズレ = 並進速度 x 遅延(模様全体で一定) ** + 角速度 x 遅延 x 半径(中心からの距離に比例)**

大きな模様ほど回転に弱い。

動的プロジェクションマッピングの遅延予算の基本式。「この投影を何 mm の精度で やりたいなら遅延は何 ms 以下」が模様の大きさごとに言える。

途中で直した設計の誤り(記録)

真値を「世界座標から別式で」作っていたため、回転の符号規約が投影側と食い違い、 遅延 0 でもズレ 77.85 mm と出た。真値も投影も同じ関数 place() を通す 形に直した(真値 = 遅延 0 の計測、投影 = 遅延ありの計測)。 こうすると規約の食い違いが原理的に起きず、遅延だけの効果が残る。

「同じ量を 2 通りの式で計算して比べる」設計は、規約の食い違いを必ず呼ぶ。 片方を他方の特殊ケースにできるなら、そうする。


ピラミッドの関門(2026-08-25)— 「ピラミッドになるもの」は対象に依る

「マッチングアルゴリズムは大体ピラミッドサーチだけど、輝度相関値や形状一致スコアで やってる。ピラミッドになるものが何か ってところを考えないといけない」 (ユーザー、2026-08-25)

実装 pyramid_gate.py。ピラミッドの正体は解像度ではなく 証拠を担う量の階層。 使えるかどうかを測る関門を作った。

判定基準を途中で変えた(道具が自分の誤りを暴いた)

最初は Spearman ρ(粗↔細の順位相関)で判定した。走らせたら 3 軸とも ρ 0.57〜0.67 と冴えないのに 上位 k の残存率は全て 1.00 だった。

理由は明白で、候補の大半は雑音の位置なのでそこの順位は最初から意味が無い。 ρ はその雑音に支配される。枝刈りに効くのは「良い候補が残るか」だけ。

ピラミッドになれる条件は「順位を保存すること」ではなく「上位が上位に残ること」。 全体の順位は保存しなくてよい。

実際のピラミッドサーチが粗い階層の雑さに耐えるのはこれが理由。判定を 上位 k の残存率 に変え、候補も「良い候補どうしの区別」を問う配置に直した。

3 つの軸を測った

輝度相関の探索。細かい階層 = 原寸・全点の正規化相関。

太い棒(4 px)をモデルに 細い線(1 px)をモデルに
解像度 1/2 使える(ρ 0.625) 使えない(ρ 0.076)
ビット幅を粗く 使える(ρ 0.959) 使える(ρ 0.956)
モデル点数 1/16 使える(ρ 0.729) 使えない(ρ 0.080)

設計上の含意

既存の照合器は 常に解像度 でピラミッドを作る(HALCON も。細い部品で NumLevels を手で下げないといけないのはそのため)。 モデルごとにピラミッド軸を選ぶ照合器 が作れるはずで、この関門がその選択器になる。

なお shapematch.pyfind_shape_modelピラミッドサーチをしていない (step=2 の平坦な全走査)。Gauss ピラミッドの op(f2_gauss_pyramid)はあるのに 照合が使っていない。実在する HALCON parity の穴

今日の afterman の失敗も、この関門の不在だった

実験 NB。AR2 の「内側 800 ステップ訓練」は、最終性能という細かい量に対する 粗い階層 だった。粗い階層で出した順位を細かい階層で検算しなかったので、 6.4 倍という差の内訳を誤って解釈していた。

粗い評価を使うときは、必ず「上位が上位に残るか」を先に測る。 これは画像照合でも、機械学習の予算配分でも、同じ 1 つの関門である。


ピラミッドの正体を一次ソースで確認した(2026-08-25)

「HALCON は find_shape 系も一度 Region によるカクカクした輪郭にしてるように見える。 ピラミッド化するために縮小構造を画像に近い扱いができるものにしてるんじゃなかろうか」 (ユーザーの観察)

WebSearch の予算が尽きたのでユーザーの助言に従い外部 AI(Perplexity)経由で 一次資料を確認した。ユーザーの記憶も仮説も裏付けられた。 ただし機構は少し違う。

確認できたこと(出典つき)

出典: shape-based matching solution guide / create_shape_model / inspect_shape_model / find_shape_model

ピラミッドを作っているのは画像であって、モデルではない。モデルは各階層で作り直す。

領域が見えるのは「その解像度で抽出し直したモデルの姿」だから。縮小に耐えるように 領域化しているのではない。結果としての効果は同じだが、機構は「作り直す」ほう。

私の測定の訂正 —— 2 回した

1 回目: 「細い構造では解像度ピラミッドは使えない」と書いた。 これは img[::2, ::2]単純な間引き(平滑化なし)を使った産物だった。 面積平均で縮小すれば 1 px の線も「半分の濃さの線」として残る。

2 回目(1 回目の過剰訂正): 「面積平均なら細線でも成立する」と書いた。 私は 1 階層しか試していなかった。 1/2 なら残るが、1/4、1/8 と重ねれば消える。 一次資料が言っているのはそちらで、それが NumLevels を下げる理由。 両方正しく、私の測定が浅かった。

エッジモデルの比較は無効だった(記録)

pyramid_edge.py で「モデル点を間引く」対「各階層で作り直す」を比べたところ、 後者が細線で上位残存 0.00 と出た。これはテスト設計の欠陥で、候補位置を rc[0] // 2 で粗い格子に落としていたため 1 px 違いの候補が同じセルに潰れていた。 粗い階層で 2 px より細かく位置決めできないのは当たり前の性質(だから次の階層で 精密化する)。この比較は無効。

産物 —— NumLevels を目視でなく実測で決める(pyramid_levels.py)

MVTec の推奨手順は「inspect_shape_model で各階層を 目視 して、細部が消えて いたら NumLevels を下げる」。これを実測に置き換えた。各階層について (a) 抽出できたモデル点数 (b) 原寸スコアの上位を残すか を測り、両方が保たれる 最大の階層を返す。

モデル 壊れる階層 推奨 NumLevels
太い棒 12 px 3 3
細い線 1 px 3 3
縦棒 4 px(テンプレート 50x10) 2 2

もう一段細かいことが出た: 細線が太い棒と同じ深さまで耐えている。テンプレートが 10x80 なので、線が細くても 長さ方向にモデル点が大量にある から。壊れたのは 縦棒で、理由も違って モデル点数が 9 点まで落ちた(上位残存は 1.00 のまま)。

効いているのは「細さ」そのものではなく、粗い階層で残るモデル点の数。

「細い構造では NumLevels を下げよ」という指針は、実体としてこれを言っていた。

正直な限界: 候補の間隔を最粗階層に合わせて 32 px にしたので、細かい階層では 上位残存が簡単に 1.00 になる。階層ごとに間隔を変えるのが本来。


find_shape_model にピラミッドサーチを実装した(2026-08-25)

一次資料で確認した HALCON の流儀に合わせて実装。画像をピラミッド化し、モデルは 各階層で作り直す(モデル点を間引くのではない)。最粗階層を全走査して候補を 12 個残し、階層を下りながら近傍 ±2 px だけ精密化する。

結果 — 同じ答えで 22〜92 倍速

画像 テンプレート 平坦走査 ピラミッド 速度比 一致
256x256 40 1088 ms 49 ms 22 倍 位置・スコアとも完全一致
512x512 48 5915 ms 64 ms 92 倍 同上

速度差は画像が大きいほど開く。8 種の場面で平坦走査と 8/8 一致(位置 ±1 px、 スコア ±0.02)。

階層数は自動で決まる

打ち切りの基準は 粗い階層で残るモデル点数(既定 12 点)。実測で 「細さ」ではなくこの点数が効いていたため。num_levels を明示すれば固定もできる。 num_levels=0 で従来どおりの平坦走査。

見つけた副作用と防御

find_scaled_shape_modeldict(model) で複製して pts/shape だけ差し替える。 そのため template が古いまま残り、shape と食い違う。作り直すと誤った結果に なるので、build_model_pyramid に食い違いの検査を入れて平坦探索へ落とす。 回帰テスト test_stale_template_falls_back_to_flat で固定。

検証

tests/test_shapematch_pyramid.py 9 件追加、全通過。関連する既存テスト 310 件も通過。 test_codegen.py の 2 件は失敗するが、変更前から失敗していることを stash で確認 (回帰ではない)。


形状マッチング一族の点検(2026-08-25)

find_shape_model にピラミッドサーチを入れた後、同じ一族の他の op も無事かを 点検した。7 件の欠陥が出た。全て「先に実測で再現し、直してから再実測」で確認している。

見つかったもの

# 対象 症状(実測) 原因
1 find_shape_models 中心 (170,170) の物体を検出できず、代わりに (8,118) を返す 走査が range(0, H-mh)左上規約なのに _score_at中心規約。右下に幅 mh/2 の死角ができていた
2 同上 端で model が半分はみ出た位置が高得点 見えている点だけの平均を取っていた
3 find_scaled_shape_models 真の scale 1.0 に対し 0.8 を選び、スコアも 0.999→0.80 に落ちる scale を一致件数で選んでいた。#2 の偽陽性が 1 件多いだけで逆転する
4 find_local_deformable_model column常に None、変形場は画像の左端を切り出して計算 find_shape_model"col" を返すのに rigid.get("column", 0) を読んでいた。既定値 0 に落ちて気付けない
5 find_aniso_shape_model / find_shape_model_3d 未検出時に {"found": False} だけ返す 呼び出し側の res["row"]KeyError
6 find_ncc_model / find_ncc_models / best_match 中心 (60,150) の物体に対し (40,130) を返す NCC 一族が左上規約、形状一族が中心規約。テンプレート半分ずれる。HALCON が返すのはモデル原点(既定=中心)
7 一族全体 純雑音の画像で最良スコア 0.73–0.75 MinContrast が無く、勾配が雑音だけの点も mag で割ると単位ベクトルに化ける。\|cos\| の期待値は 2/π = 0.637

#7 が一番深い

スコアは |cos| の平均なので、方向がでたらめでも 0.637 に張り付く。 つまりこの一族の既定 min_score=0.5構造的に何も棄却できなかったfound が常に真になるので、誤りが表に出ない。

HALCON に MinContrast があるのはこのため。しかも本ファイルの determine_shape_model_params は最初から min_contrast返していた ——誰も使っていなかっただけだった。

モデル生成時の閾値をそのまま min_contrast として持たせ、 それ未満の画素は 0 点として全点数で割るようにした(#2 もこれで同時に消える)。

純雑音の最良スコア   0.73–0.75  ->  0.028–0.037
物体あり             0.999      ->  0.999

コントラストを落としても本物は残る(HALCON と同じ性質):

物体の振幅 1.00 0.60 0.40 0.25 0.15
score 0.999 0.998 0.980 0.947 0.614

教訓

速度の残課題(正直に)

correctness は直したが、ピラミッドが効いているのは find_shape_model だけ。 256×256 / テンプレート 40 での実測:

op 時間
find_shape_model(ピラミッド) 51 ms
find_shape_model(平坦) 186 ms
find_shape_models 184 ms
find_scaled_shape_model 413 ms
find_scaled_shape_models 554 ms
find_aniso_shape_model 1674 ms

find_aniso / find_scaled_* は自前で pts を伸縮した dict を組むため template を持たず、ピラミッドに乗れない(乗せない側に落ちる防御が働いている)。 テンプレートを拡大縮小してから各 scale のモデルを作り直せば、階層も張れて HALCON の作り方にも一致する。これは次の作業。


スケール系もピラミッドに乗せた(2026-08-25、続き)

前節の最後に「これは次の作業」と書いた分。テンプレートを zoom してから、 その解像度でモデルを作り直す(zoom_model)ようにして、scale 系を全部 ピラミッドに乗せた。ついでに 3 つの欠陥が出てきた。

なぜ点だけ伸縮してはいけなかったか

以前は model["pts"] * s と座標だけ伸縮した dict を組んでいた。これで 3 つが 同時に起きていた:

  1. template を持たないのでピラミッドに乗れない(乗せない側へ落ちる防御が 働き、必ず平坦走査になっていた)
  2. 勾配の向きが元の解像度のまま。異方 scale では法線の向きは実際に変わる —— 座標を A で写すと法線は A^-T で写る。細長く潰した円の法線は、元の円の 法線と違う向きを向く。点だけ伸縮するのは近似ですらない
  3. 縮めても点数が減らないので、小さい scale でも速くならない

テンプレート側を zoom すれば、HALCON の作り方(scale ごとにモデルを作る)と 一致して 3 つとも消える。

出てきた欠陥

# op 症状(実測)
8 find_shape_models 複数インスタンス版は平坦走査のままだった。粗い階層で NMS して候補を複数残し、各候補を独立に下ろす方式にした(単一版のように上位 1 個へ絞ると 2 個目以降が消える)
9 find_aniso_shape_models scale を一切見ずに find_shape_models を素通ししていた。名前が嘘をついていた
10 一族全体 スコアが点ごとに |cos| を取っていた(下記)

#10 —— 前回の MinContrast は「弱い雑音」にしか効いていなかった

前節で純雑音のスコアが 0.73 → 0.03 に落ちたと書いた。あれは雑音が弱い場合の 数字だった。 雑音を強くして測り直すと下駄が戻ってくる:

雑音 sd 0.02 0.05 0.10 0.15 0.30
雑音の |grad| が閾値を超える画素 0% 33% 76% 89% 97%
純雑音の最良スコア 0.000 0.267 0.509 0.570 0.650

MinContrast はコントラストの低い点しか落とせない。雑音が強ければ雑音の勾配が 閾値を超えてしまい、E[|cos|] = 2/pi = 0.637 の下駄がそのまま戻る。

下駄の正体は閾値ではなく、点ごとに絶対値を取っていたことだった。これは HALCON の Metric = 'ignore_local_polarity' に当たる —— HALCON の説明も 「偽陽性が増える」と警告している、一番緩い metric である。既定にしていいものでは なかった。metric を HALCON と同じ 3 択にして、既定を use_polarity(符号つき)に 戻した:

metric 中身 雑音 sd 0.15 雑音 sd 0.30
use_polarity(既定) 符号つき cos の平均 0.104 0.106
ignore_global_polarity 符号つきで足してから絶対値 0.144 0.091
ignore_local_polarity(旧既定) 点ごとに絶対値 0.570 0.650

符号つきなら向きが乱数の点は打ち消し合うので、下駄は原理的に生じない (散らばりは 1/sqrt(n) 程度)。本物との差はこれで開く:

雑音 sd 0.02 0.10 0.30
本物のスコア 0.992 0.952 0.729
同じ雑音での下駄 0.000 0.077 0.106

失うのは「明暗が反転した物体も勝手に一致する」性質だが、それは HALCON の既定でも 失われている。必要なら metric で選ぶ —— 反転した物体は既定で 0.202(不一致)、 ignore_global_polarity で 0.996(一致)。選択にした。

雑音 sd 0.15 の場面での効き方:

複数インスタンス 4 個   検出 4 件(旧: 4 件 + 偽陽性)
純雑音                  0 件(旧: 10 件)

速度

同一実行内で平坦経路(template を外したモデル)と比べた。 256×256 / テンプレート 40 / 物体 2 個。絶対値は機械の状態に依るので比を見る。

op 平坦(旧) ピラミッド 倍率
find_shape_model 288 ms 54 ms 5.4x
find_shape_models 278 ms 63 ms 4.5x
find_scaled_shape_model 847 ms 140 ms 6.0x
find_scaled_shape_models 864 ms 167 ms 5.2x
find_aniso_shape_model 2568 ms 466 ms 5.5x
find_aniso_shape_models 2604 ms 557 ms 4.7x

scale を掃引する op では、画像階層を scale ごとに作り直さないのが効く (_search_scales が必要な深さぶん 1 回だけ作って使い回す)。9 通り試す aniso では、作り直すと探索本体より縮小のほうが高くつく。

教訓(前節に足すもの)


op の GPU 化 = E2E の本丸(2026-08-26、RTX 5090 で初実測)

ユーザー方針「op の GPU 化は E2E の本丸。他プロジェクトの土台としても効く」。imgevolve は 既に accel.py(GPU バッチ backend + parity difftest)と backends_kornia.py(torch GPU)を 持つが、この環境が CPU-only だったので一度も実 GPU で回っていなかった。loco venv (torch 2.11.0+cu128 / RTX 5090)で初めて実測した。

単発 op(run_batch)は転送律速 —— per-op GPU 化は必ずしも勝たない

bench.py --device cuda(500枚 @512²):batch スループットが op によらず ~750 img/s で 一定 = host↔device 転送(500MB)が律速で演算ではない。だから:

教訓: 「op を1つずつ GPU に投げる」だけでは E2E で勝てない。転送が本体を超える。

E2E の本丸 = 常駐パイプライン(run_pipeline、転送1回で op 連鎖)

現実の検査は op の。転送を1回に償却し、間の op は GPU 常駐で回す(accel.run_pipeline)。 5-op 検査チェーン(gauss→sobel→dilate→erode→threshold)、500枚 @512²:

方式 時間 スループット 対 CPU
CPU 逐次(per-image×per-op) 9,096 ms 55 img/s 1.0x
GPU per-op(転送5回) 3,420 ms 146 img/s 2.7x
GPU 常駐(転送1回) 719 ms 695 img/s 12.6x

honest な parity: 単発は忠実、チェーンはドリフトする

他プロジェクトの土台

この _to_batch → 常駐 op 連鎖 → _from_batch の形は、evis 視覚・Afterman の集団評価・ 物理 AI の画像前処理など バッチ画像を GPU 常駐で流す全用途の土台。E2E の骨格を accel の常駐パイプラインに統一していく。