発端: ユーザー要望(2026-08-25)「fullseye で http://ishikawa-vision.org/movies/index-j.html にあるような事をしたい」「物理シミュレーション環境上でやりたいのです」。
ページには 6 大分類・約 150 のデモがある(センサフュージョン / ダイナミック ビジョンシステム / システムビジョンデザイン / アクティブパーセプション / 光コンピューティング)。個々はばらばらに見えるが、通っている軸は 1 本しかない。
フレームレートではなく、遅延。
知覚から行動までの往復を 1ms 級まで縮めると、それまで不可能だった作業が可能になる —— 飛翔体のキャッチ、動く物体への投影、動く台座の上での精密作業。彼らは遅延を 下げるために 専用ハードを作った(ビジョンチップ、列並列ビジョン、DynaFlash 1000fps プロジェクタ、サッカードミラー)。つまり研究の大半は 「遅延を固定値として下げる工学」である。
実機では遅延は 設計して固定する量。掃引できない(ハードを作り直すことになる)。 シミュレーションでは 独立変数として自由に掃引できる。
したがって我々が出せるのは点ではなく 関数 である:
追従誤差 / 成功率 = f(遅延, 対象の速さ, センサ雑音, 制御帯域)
これは実機の論文が原理的に出しにくい形の結果で、しかも実務的に価値がある。 「この作業を成立させるには何 ms 必要か」を、ハードを買う前に言える。
Fullseye の使命(Physical AI / evis 視覚のための統一 I/F・即使える視覚アルゴリズム workbench)にそのまま乗る。視覚 op を「静止画に対する関数」ではなく 「閉ループの中の一段」 として評価する軸が増えることになる。
MuJoCo の timestep は既定 2ms、1ms や 0.5ms も普通に回る。1000Hz 制御はネイティブ。
| モジュール | 役割 |
|---|---|
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 は推定までで制御に戻していない。
つまり 「遅延を持つ閉ループ台」が丸ごと無い。ここが最初の一手になる。
「画像処理寄りから行けば成果は得やすいし、記事にもしやすいが、やはり一つの 研究フェーズに数週間は時間をかけたい」
画像処理寄りの危険は、成果が出やすいぶん 小さな勝ちが散らばって積み上がらない こと。避け方は一つしかない —— 小さな勝ちが全部、同じ一つの問いに 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 = 速いほうが強い」の柱がここから供給されるが、記事のために結論を先に 決めない。交換曲線が「一番正確なやつが勝つ」形になったら、そう書く。
新規 vloop.py。MuJoCo を step しながら {描画 → 知覚 → 制御} を回す。要点は
遅延を明示的なパイプライン段数として持つこと:
センサ遅延 n_s ステップ(露光 + 転送)
計算遅延 n_c ステップ(知覚アルゴリズムの実行時間)
制御遅延 n_a ステップ(アクチュエータ応答)
n_c は実測値を使うモードと、手で指定して掃引するモードの両方を持つ。
2 自由度の雲台に載せたカメラで飛翔体を画面中心に保つ。
measure.py に既存の
モーメント op があれば流用、無ければ足す。ユーザーは実機を東京大学 山川研究室で見ている(2026-08-25 の証言)。石川研の ページでは「じゃんけんロボット」としてセンサフュージョンの分類に入っている。
これを H2 の直後に置くのは、遅延の効果が最もきれいに出る課題だから:
測るもの: 勝率 = f(遅延)。遅延 0 で勝率 1.0、遅延を上げるとどこかで 1/3(でたらめ)に落ちる。その落ちる位置と急峻さが答えになる。 実機では 1 点しか測れないが、シミュなら曲線全体が出る。
シミュ上の実装: 人間の手は MuJoCo 上の 3 姿勢を持つ多指ハンド(既存の MyoHand か 簡略ハンド)。指定時刻に向けてグー / チョキ / パーへ遷移させる。
判定アルゴリズムは既に答えが分かっている。ユーザーが実機で見た中身(一次観察、 2026-08-25):
手のひらを中心に 円弧状のプロファイル を取り、そこを横切る 指の本数を 数える。ルールベースを GPU で回しているだけ。
これは手抜きではなく 1ms に収めるための必然。円弧プロファイルは O(円周) で 画像全体を触らない。H2 の Self-Windowing とまったく同じ思想 (処理量を対象の大きさに比例させ、画面の大きさに依らせない)。 知能はアルゴリズムの側に無く、全部が遅延の側にある。
正直に開示すべき点: このロボットが勝つのは「速いから」であって 「読んでいる」からではない。人間から見れば ズルであり、そこを隠さないことが このデモの誠実さになる(evis 物理動画で確立した方針と同じ)。 勝率曲線を出すことは、そのズルの大きさを定量することでもある。
上の一次観察から、高速ビジョンは Fullseye の適応度関数を変える ことが分かる。
いまの進化は 精度だけ で選抜している(evolve.py の docstring:
“Fitness is the TRAIN score only”。実行時間の項は無い —— grep で確認済み)。
それでは「重くて正確なもの」が必ず勝ち、円弧プロファイルのような
安いが十分なもの は選ばれない。
従来: 適応度 = holdout 精度
高速: 適応度 = 時間予算 T 以内に返せた場合の精度。超えたら失格(0 点)
時間のペナルティを足すのではなく、硬い制約にすることが要点。閉ループでは 遅延はコストを連続的に増やすのではなく、ある点から 破綻 させるため。 適応度の形は現実の形に合わせる。
時間予算を与えた Fullseye は、円弧プロファイルを自力で再発見するか。
これがあるので、H6(帯域・計算量の制約)は「余力があれば最後に」ではなく H2b と同時に走る横断軸 に格上げする。じゃんけんは「遅延の効果を測るベンチ」で あると同時に 「時間予算つき進化の検定台」 になる。
既存の Panda gripper(ctrlrange [0,255]、6DoF IK は world フレーム)か
MyoHand を使う。
sceneflow.time_to_contact + sensor_fusion.py の 6 状態 Kalman を再利用。台座が動いていても工具先端を world 座標で静止させる。視覚で台座の動きを測り、 腕で打ち消す。
動く物体の表面にテクスチャを貼り続ける。シミュなので投影機の光学モデルが要らない —— 「投影」は物体表面 UV への書き込みとして実装できる。
ズレ = 速度 × 遅延 を先に予測する。focus_stack.py が使える。「全画素を毎フレーム読み出せない」という帯域制約(列並列、限られた転送量)を 明示的に課したとき、どのアルゴリズムが生き残るか。
event_camera.py / events.py の既存資産と合流する。H1 の vloop.py。これが無いと H2 以降のどれも測れない。
成立確認は 1 つだけ: 遅延 0 で誤差がほぼ 0、遅延を上げると誤差が単調増加。 これが出なければ先へ進まない。
石川グループ研究室のデモ一覧は上記 URL(2026-08-25 取得)。個々の手法名 (サッカードミラー、Self-Windowing、DynaFlash、VarioLight、Lumipen、列並列ビジョン、 ビジュアルショックアブソーバ、動的補償)はそのページの記載から取った。 一次文献(論文)にはまだ当たっていないので、実装に入る段階で確認すること。
じゃんけんロボットの実機を見た経験(東京大学 山川研究室)と、その画像処理が 「手のひら中心の円弧プロファイル + 指の本数カウント」であったことは ユーザーの 一次観察(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 倍の余裕。
| 遅延 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(投影)で立てる同じ形の 予測も、この台の上でなら検証できることを意味する。
| 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(キャッチング)以降で 対象の予測可能性を変えながら確かめる。
qpos を書き換えた後 mj_forward を呼ばずに描画していたので、
画像が 1 歩前の運動学を映していた。修正で「見失い」も 3 → 0 になり、
診断の裏も取れた。H2(モーメント追跡 + Self-Windowing + アクティブ注視)。台はそのまま使える。
実装 vloop_algos.py。H1 の台の上で 同じ課題を 5 つの違う値段の検出器で解き、
各検出器の実行時間をそのまま閉ループの遅延として食わせる。
5 ms かかるアルゴリズムは 5 ms の遅延そのものである。
作動点 1024x1024。理由は下の「最初の試行は失敗した」を参照。
| 検出器 | 誤差 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 |
| 検出器 | 遅延 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 に丸まるため。順位の細部ではなく 下の対照のほうが本質。)
| 検出器 | 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 次元に直した。
実装非依存の「画素演算数」モデルは、NCC を 629 ms と見積もったが実測は 43.8 ms
(14 倍の過大評価)。FFT の定数を 6 N log2 N と置いたのが粗すぎる。
逆に全画面重心は 5.24 対 8.29 で妥当。2 つの費用モデルは NCC で大きく食い違う
ので、片方だけを載せない。GPU / FPGA / ビジョンチップに移せば定数は変わる
—— 石川研がハードで解いたのはこの定数の部分。
いまの課題では 精度が「余っている」ので費用が全て になった。交換曲線を 本当に描くには、精度が効く領域まで課題を難しくする必要がある:
そこで初めて「安いが粗い」対「高いが正確」の 交換の前線 が見える。 いまの結果はその前線の 片側の端 を測ったことになる。
前節で「精度が余っているので費用が全て」になった。雑音を入れて精度が効く領域まで 難しくする。偽の赤画素を割合 p で撒く(現実の対応物 = ホットピクセル、反射、赤い別物)。
| 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 |
括弧内は各検出器の遅延。
| 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 本。
(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 の費用を 永久に払う。だから高雑音でしか勝てない。
この節の閉ループ数値(0.0297 など)は前節(0.0341 など)と条件が違う (800 歩・位相 1 通り 対 1200 歩・位相 2 通り。雑音掃引の時間を抑えるため)。 節の中での比較だけが有効。
実装 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 + 重心が精度でも 費用でも十分良いので、交換すべきものが存在しない。適応度を変えても選択が変わらない。
| Afterman 実験 NB | Fullseye §4b 進化 | |
|---|---|---|
| 問い | 進化した構造は手設計を上回るか | 時間予算つき適応度は違うものを選ぶか |
| 結果 | 上回らない | 同じものを選ぶ |
| 原因 | 空間に有用ビットが 1 つしかない | 空間に支配解がある |
どちらも「探索の価値」を問う実験で、空間に支配解があると検定にならないという 同じ構造で失敗した。偶然ではなく、この種の実験の共通の落とし穴。
探索の価値を測る実験は、まず「空間に支配解が無いこと」を確かめてから走らせる。
これは事前に確かめられる。全遺伝子を総当たりする必要はなく、 両端(最も安い / 最も正確)を手で作って、片方が両軸で勝っていないかを見るだけでよい。
Fullseye の進化は、人間の設計者と同じ答えに独立に到達した。 「小窓だけ見る + まばらに標本化 + モーメント」は、じゃんけんロボットの円弧プロファイルと同じ論理 (対象の居る所だけを、粗く、0 次と 1 次のモーメントで)。 ただし そこへ行くのに時間予算は要らなかった。
交換の前線の測定によれば、p=0.001 で静止画 1 位 = テンプレート照合、
閉ループ 1 位 = 窓重心 と割れている。そこで進化させれば適応度は分岐するはず。
vloop_evolve.py 0.001 で走行中。
割れている作動点(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 |
| 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 倍悪い検出器を選ぶ。
| 予測 | 結果 |
|---|---|
| A は use_window=False・stride=1 を選ぶ | 当たり(全画面・間引き 1-1) |
| B は use_window=True・stride > 1 を選ぶ | 当たり(窓 317・間引き 1-8) |
| 2 つの適応度が選ぶ個体は違う(P3) | 当たり |
両方ともテンプレート照合を選んだ。 雑音下では重心が脆いので、 アルゴリズムの選択は一致した。違ったのは どこを、どれだけ細かく見るか だけ。
B の答え = 「頑健なアルゴリズムを、小窓に、粗く適用する」
これは「安物のアルゴリズムに替える」のとは違う。アルゴリズムの選択と、 適用範囲・密度の選択は 分離できる量 であり、時間予算が変えるのは後者だけ だった。
じゃんけんロボットの円弧プロファイルも、この読み方のほうが正確だと思う —— 「粗いアルゴリズムを使った」のではなく「手のひらという既知の位置の周りだけを、 円弧という 1 次元の標本で見た」。適用範囲と密度を絞ったのであって、 アルゴリズムを妥協したのではない。
Fullseye の進化は、時間予算を与えられただけで、人間の設計者が実機で使った設計原理に 独立に到達した。§4b で「一番面白い実験」と書いた問いへの答えは 肯定 である。
ただし条件がある。雑音なし(支配解のある空間)では到達しても意味が無い —— 前節のとおり、そこでは適応度を変えても選択が変わらないので、 「到達した」ことが workbench の能力の証拠にならない。 支配解が無い空間で、かつ適応度が分岐する作動点で測って、初めて証拠になる。
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 で成果物と宣言した交換曲線の完成形に近い。
実装 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。
円弧はしきい値型なので、指が円周を越えるまで何も起きない。面積(充実度)は 指が動き始めた瞬間から連続的に反応する。
費用の安さと判別の早さは別の量で、閉ループで効くのは合計。 円弧プロファイルは前者に最適化して後者を犠牲にしている。
「私のモデルは指が短いから円弧を越えるのが遅いのでは」と考えて指の長さを振った。 逆だった:
| 指の長さ | 円弧 チョキ | 円弧 パー |
|---|---|---|
| 0.080 | 130 | 51 |
| 0.110 | 130 | 102 |
| 0.140 | 134 | 127 |
半径を握り拳の幅から較正しているので、指が長いと拳も大きくなり半径も伸びる。 判別の遅さは手の形の問題ではなく、しきい値型であることに内在する。
model.actuator("human_j0") が
KeyError。<position name=... joint=...> が要る。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_actuator と
qfrc_constraint を並べて見たら一発だった。detect_arc_win)を足して 0.144 ms に。
これは §4b で測った Self-Windowing そのもの で、2 つの実験が同じ機構に合流した。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 - 形成時間 を持つ一次式)。
実測を経て、ユーザーの一次観察の解釈がこう変わった:
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。半径をどう選んでも変わらない。 結論は維持される。
(1) しきい値の共有による矛盾
グーを 0 本と読むには、半径は 握った指の到達点より外 に無ければならない。 ところがチョキの指も 同じ点を通過 しないと数えられない。 「グーを 0 と読む」ことと「チョキを早く読む」ことは同じしきい値を共有していて、 両立しない。
これは TRIZ でいう物理的矛盾そのもの(同じ量に相反する要求)。実際の手で成立して いるのは、指の長さが手のひらに対して大きく、握った位置と伸びた位置の隔たりが 広いからだと思われる(私のモデルは 手のひら半径 0.075 に対し指 0.080)。
(2) 回転の投影は cos なので、動き始めは伸びない
指は根元のヒンジで回る。半径方向の到達点は R_palm + L cos(θ) で、
θ が 1.45 rad から動き始めても cos の平坦部 なのでほとんど伸びない。
しきい値型の検出器は その平坦区間を丸ごと捨てている。
面積(充実度)は 2 次元の積分なので、回転の最初から連続的に反応する。
しきい値型は「動きの最後」しか見ない。積分型は「動きの最初」から見る。 時間圧が高い課題では、この差が費用の差より大きくなりうる。
これは今日 §4b で得た「安いほうが勝つ」を、もう一段限定する。 安さは判別の早さと交換される場合があり、閉ループで効くのは和のほう。
上で「円弧プロファイルは構造的に判別が遅い」と結論し、2 段の説明まで付けた。 それは円弧プロファイル法の性質ではなく、私の二値実装の性質だった。
ユーザーの説明を読み直した:
手のひらを中心に 円弧状のプロファイル取得後 の指の本数カウント
プロファイルは信号であって二値ではない。 私が実装したのは
「固定半径の円周を横切るか」という最も過酷な二値版だった。
各方向への 手の広がり(半径方向プロファイル) を測って山を数える版を実装し直す
(detect_arc_profile)。突出のしきい値は他の検出器と同じ較正予算で決める
(3 姿勢の完成形から中点。実測 グー 0.21 / チョキ 0.48 / パー 0.46 x 半径 -> しきい 0.331)。
| 検出器 | グー | チョキ | パー | 最悪 |
|---|---|---|---|---|
| 円弧(二値) | 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 違う。
他人の一次観察を実装で再現するときは、うまくいかなかったら 「相手の観察が不正確」より先に「自分の実装が相手の言葉どおりでない」を疑う。
ユーザーが実機で見た円弧プロファイルは、その規模のセンサに対して正しい選択だった。 費用が画面の大きさにほぼ依らないので、センサが大きいほど有利になる。 1024 px 級で全画面型と並び、2048 px で追い越す。
そして今日の主題ともきれいに繋がる: 「頑健なアルゴリズムを、小窓に、粗く適用する」(§4b)の、 分類課題における具体形が円弧プロファイルだった。
実装 projmap.py。板が滑りながら 回る。系は画像モーメント(0 次 = 面積 /
1 次 = 重心 / 2 次 = 傾き)で姿勢を推定し、板の局所座標の模様を画像へ写す = 投影する。
シミュレーションなので投影機の光学モデルが要らない。 実機の VarioLight / Lumipen が解いた光学・機械の問題を飛ばして、遅延の効果だけを測れる。
並進 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)
実装 pyramid_gate.py。ピラミッドの正体は解像度ではなく 証拠を担う量の階層。
使えるかどうかを測る関門を作った。
最初は Spearman ρ(粗↔細の順位相関)で判定した。走らせたら 3 軸とも ρ 0.57〜0.67 と冴えないのに 上位 k の残存率は全て 1.00 だった。
理由は明白で、候補の大半は雑音の位置なのでそこの順位は最初から意味が無い。 ρ はその雑音に支配される。枝刈りに効くのは「良い候補が残るか」だけ。
ピラミッドになれる条件は「順位を保存すること」ではなく「上位が上位に残ること」。 全体の順位は保存しなくてよい。
実際のピラミッドサーチが粗い階層の雑さに耐えるのはこれが理由。判定を 上位 k の残存率 に変え、候補も「良い候補どうしの区別」を問う配置に直した。
輝度相関の探索。細かい階層 = 原寸・全点の正規化相関。
| 軸 | 太い棒(4 px)をモデルに | 細い線(1 px)をモデルに |
|---|---|---|
| 解像度 1/2 | 使える(ρ 0.625) | 使えない(ρ 0.076) |
| ビット幅を粗く | 使える(ρ 0.959) | 使える(ρ 0.956) |
| モデル点数 1/16 | 使える(ρ 0.729) | 使えない(ρ 0.080) |
既存の照合器は 常に解像度 でピラミッドを作る(HALCON も。細い部品で
NumLevels を手で下げないといけないのはそのため)。
モデルごとにピラミッド軸を選ぶ照合器 が作れるはずで、この関門がその選択器になる。
なお shapematch.py の find_shape_model は ピラミッドサーチをしていない
(step=2 の平坦な全走査)。Gauss ピラミッドの op(f2_gauss_pyramid)はあるのに
照合が使っていない。実在する HALCON parity の穴。
実験 NB。AR2 の「内側 800 ステップ訓練」は、最終性能という細かい量に対する 粗い階層 だった。粗い階層で出した順位を細かい階層で検算しなかったので、 6.4 倍という差の内訳を誤って解釈していた。
粗い評価を使うときは、必ず「上位が上位に残るか」を先に測る。 これは画像照合でも、機械学習の予算配分でも、同じ 1 つの関門である。
「HALCON は find_shape 系も一度 Region によるカクカクした輪郭にしてるように見える。 ピラミッド化するために縮小構造を画像に近い扱いができるものにしてるんじゃなかろうか」 (ユーザーの観察)
WebSearch の予算が尽きたのでユーザーの助言に従い外部 AI(Perplexity)経由で 一次資料を確認した。ユーザーの記憶も仮説も裏付けられた。 ただし機構は少し違う。
inspect_shape_model は ModelImages(入力画像のイメージピラミッド)と
ModelRegions(各階層でモデルを表す領域)を返す。モデルの抽出は
「ヒステリシス閾値法に似た方法」で 各階層ごとに 行う出典: shape-based matching solution guide / create_shape_model / inspect_shape_model / find_shape_model
ピラミッドを作っているのは画像であって、モデルではない。モデルは各階層で作り直す。
領域が見えるのは「その解像度で抽出し直したモデルの姿」だから。縮小に耐えるように 領域化しているのではない。結果としての効果は同じだが、機構は「作り直す」ほう。
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 より細かく位置決めできないのは当たり前の性質(だから次の階層で
精密化する)。この比較は無効。
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 になる。階層ごとに間隔を変えるのが本来。
一次資料で確認した HALCON の流儀に合わせて実装。画像をピラミッド化し、モデルは 各階層で作り直す(モデル点を間引くのではない)。最粗階層を全走査して候補を 12 個残し、階層を下りながら近傍 ±2 px だけ精密化する。
| 画像 | テンプレート | 平坦走査 | ピラミッド | 速度比 | 一致 |
|---|---|---|---|---|---|
| 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_model は dict(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 で確認
(回帰ではない)。
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 |
スコアは |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 |
min_score は
棄却の役目を一度も果たしていなかったが、返り値は常に found: True で綺麗だった。
閾値を入れたらまず雑音だけを食わせて、ちゃんと落ちるかを見る。col と column の
併存が #4 を生んだ。.get(key, 0) は綴り間違いを 0 に化けさせる。両方返すようにした。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 の作り方にも一致する。これは次の作業。
前節の最後に「これは次の作業」と書いた分。テンプレートを zoom してから、
その解像度でモデルを作り直す(zoom_model)ようにして、scale 系を全部
ピラミッドに乗せた。ついでに 3 つの欠陥が出てきた。
以前は model["pts"] * s と座標だけ伸縮した dict を組んでいた。これで 3 つが
同時に起きていた:
template を持たないのでピラミッドに乗れない(乗せない側へ落ちる防御が
働き、必ず平坦走査になっていた)テンプレート側を zoom すれば、HALCON の作り方(scale ごとにモデルを作る)と 一致して 3 つとも消える。
| # | op | 症状(実測) |
|---|---|---|
| 8 | find_shape_models |
複数インスタンス版は平坦走査のままだった。粗い階層で NMS して候補を複数残し、各候補を独立に下ろす方式にした(単一版のように上位 1 個へ絞ると 2 個目以降が消える) |
| 9 | find_aniso_shape_models |
scale を一切見ずに find_shape_models を素通ししていた。名前が嘘をついていた |
| 10 | 一族全体 | スコアが点ごとに |cos| を取っていた(下記) |
前節で純雑音のスコアが 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 では、作り直すと探索本体より縮小のほうが高くつく。
ignore_local_polarity は HALCON でも
「偽陽性が増える」と注意書きのある選択肢だった。一族の既定がそこに座っていた。ユーザー方針「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)で初めて実測した。
run_batch)は転送律速 —— per-op GPU 化は必ずしも勝たないbench.py --device cuda(500枚 @512²):batch スループットが op によらず ~750 img/s で
一定 = host↔device 転送(500MB)が律速で演算ではない。だから:
教訓: 「op を1つずつ GPU に投げる」だけでは 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 |
run_pipeline は run_batch を逐次
適用したのと ビット一致(test_accel_pipeline.py、CPU torch でも成立)。_norm_b が 端の reflect 規約差(scipy reflect ≠ torch reflect)を全体スケール
に広げる (2) 末尾のハード threshold がドリフトを二値反転に増幅(mean 差 1.0)。この _to_batch → 常駐 op 連鎖 → _from_batch の形は、evis 視覚・Afterman の集団評価・
物理 AI の画像前処理など バッチ画像を GPU 常駐で流す全用途の土台。E2E の骨格を
accel の常駐パイプラインに統一していく。