# mechArm のデジタルツイン（Unity）で、π0.5 の失敗した試行を横から見て、置き場所の候補を確かめる（2026-11-07・依頼107）

計画: `PixelstoMotors/research/research_r2r2r_to_pi05_plan_20261107.md` の段階0-1。
**実機は動かしていない**（Raspberry Pi・daemon・SSH・シリアルには触れていない）。実機の記録（`runs/`・`mecharm_calibration/`・`fk_tables/`）は読むだけ。

Unity は `Robotics_UX_New`（2022.3.62f3）を画面なし（`-batchmode`）で起動し、毎回終わらせた。既存のシーン・スクリプト・プレハブは変えていない。作業用のシーンは `R2R2R-20260224.unity` のファイルの写し（`Assets/TwinCheck/TwinCheck_Base.unity`）を開くだけで、保存していない。

## 先に読むこと（結論）

| | 結果 |
|---|---|
| A 関節角と爪先の対応 | 表から無作為に選んだ20行で、**ずれの最大 139.7mm・中央値 0.3mm**。腕がマット・箱・ブロックに触れない16行では最大 4.2mm・中央値 0.2mm。大きくずれた2行は、角度どおりに置くと腕がマットの下（爪先 y=-121mm）に入る姿勢で、表の値はマットの高さで止まっている。**置き方（角度をそのまま度で置く・符号 +・根元が原点）は合っていて、表の中に「物理で床などに当たって止まった値」が混ざっている**と見ている（200行でも同じ傾向。下の A 節） |
| B 失敗12件の分類の候補 | つかむ前に外れた **0**・つかんだが持ち上げで落ちた **7**・箱に当たった **0**・分からない **5**（5件とも、爪を閉じたあと 15mm 以下しか持ち上げていない） |
| C 置き場所の候補 | 14点（格子9＋教えた位置5）のうち **使えそう 12**・要注意 1（G_x160_z180、つかむ姿勢で腕がマットの下 3.5mm）・使えない 1（G_x120_z180、表の中で選べる行がない） |
| D 実機と Unity の画 | `D/d_side_by_side_384x216.png`（左が実機、右が Unity。学習と同じ 384×216） |

**大事な注意**: Unity の「角度 → 位置」と実機のずれは、この依頼では測っていない（昔の記録で 40〜55mm。肩 88° 超の命令が捨てられていた影響が混ざっていて、本当の大きさは分からない）。A で測ったのは Unity と表のずれだけ。B の分類は、失敗の値を**成功5件の値の幅**と比べて、大きく外れたときだけ決めている。

---

## A. 関節角と爪先の対応を合わせる

### 置き方

`FKScanner_MotionBased.cs` と同じく、j0〜j5 を link1〜link6 の ArticulationBody にそのまま（度）対応させ、爪先は左右の `gripper_{left,right}1/…/Instance-Def-210103_4302_101/Brep` の中点、根元（base）からの差（Unity の m・高さは Y）とした。

ただし表を作ったときの `SetJointPositions`＋0.5 秒の物理は使わず、**ドライブ（stiffness 20,000・damping 2,000・target）には触れず、物理も回さず**、各リンクの Transform を「親のアンカー → X 軸まわりに角度だけ回す → 子のアンカーの逆」で置いた（ArticulationBody の回転関節と同じ式。`TwinArm.SetAngles`）。符号は + と − を試し、+ で合った（− は中央値 336mm）。シーンの20体どれで置いても同じ値（差 0.00mm）。

### 結果

| 行の選び方 | 行数 | ずれの最大 | 中央値 | 腕が触れない行（数・最大・中央値） | 10mm を超えた行（うち腕が触れる） |
|---|---|---|---|---|---|
| 無作為 20行（乱数の種 107） | 20 | **139.7mm** | **0.3mm** | 16行・4.2mm・0.2mm | 2行（2行） |
| 補足：無作為 200行（種 1070） | 200 | 294.2mm | 4.1mm | 137行・97.1mm・0.3mm | 54行（47行） |

- 「腕が触れる」= 角度どおりに置くと、link1 から先の見た目のどこかがマットの下に入るか、シーンの白箱・赤ブロックにめり込む姿勢
- 20行で外れた2行は、どちらも肩 j1=110°台で、角度どおりなら爪先が y=−121mm・−63mm（マットの下）。表の ee_y は 10mm 前後（マットの上で止まった値）
- 200行で、触れないのに大きくずれた行（最大 97mm）は j2 が 40〜62° と大きいものに集まっている。自分の腕どうしの当たりか、表を作った当時のシーンの物に当たった可能性があるが、確かめていない
- 4mm 前後の小さなずれの行（j1 が整数の古い系統の行）は、表を作った当時のアンカーの値が今の写しと少し違う可能性がある

→ 置き方は合っていると判断し、B 以降に進んだ。**B・C は表ではなくこの置き方で直接計算**している。C で表から姿勢を選ぶときは、表と Unity の差が 5mm 以内の行だけを使った。

ファイル: `A/a_sample_rows.csv`（20行）・`A/a_unity_tip_20.csv`（Unity の値・20体分）・`A/a_result_20rows.csv`（1行ごとのずれと接触）・`A/a_*_200*.csv`（200行）

---

## B. 失敗した試行を再生して横から見る

### 対象

`runs/pi05_policy/` のうち `{run_stem}.json` の `judgments` に pick と place の人の判定が両方あるもの **31件（失敗12・成功19）**（Codex の数えと同じ）。失敗12件全部と、比べる成功5件（補正なしの成功2件全部＋補正ありの成功を日ごとに古い日から1件ずつ）。一覧は `B/selection.json`。

### 使った記録

- 関節角: `recording/states.csv` の `event_type=actual`（j0〜j5）。前に残した行から全関節の差が 0.3° 未満で 2 秒以内の行は間引いた（17件で 2,085 行）
- 爪: 同じファイルの `gripper_cmd`（0=開・1=閉）。閉じた瞬間は「最初の 1」の時刻
- 補正の区間: `stage=correct` の始めから次の stage までの区間（表の「補正の区間」列。秒は記録の始めから）
- ブロック: 最初のコマでは HOME の爪の真下に半分隠れているため、**爪を閉じる命令より前のコマを1つおきに全部見て、赤がいちばん大きく見えたコマ**で位置を取った（本線と同じ HSV のしきい値、根元から 120mm 以内・細長い赤（配線）は外す）
- 白箱: 本線の 06f（テンプレート＋輪郭）では、どの試行でも輪郭が1つに絞れずテンプレートの中心（箱の角に寄る）に頼っていたため、**白い四角の輪郭を自前で取り直し**、テンプレートの中心にいちばん近い四角を採った（`twin_prep.detect_box_white`）。向きは輪郭の長方形から求めた（どの試行も 87〜90°＝ほぼまっすぐ）
- 画像の点 → マットの上の点: 各試行の最初のコマでマットの四隅を検出し、本線と同じ対応（`visual_servo_pick` の (±310, −89)・(±310, 361) mm）でホモグラフィを作った
- **箱の高さは仮に 43mm**（本線のコメントの値。シーンの白箱もこの高さ）

### 検出で怪しいもの

- `pi05_20261031_185246`（成功）: 上カメラの動画がはじめから終わりまでピンぼけ（鮮明さ 1〜2）で、ブロック・箱を検出しなかった。**ほかの試行の中央値で仮に置いた**（`trials.csv` の `*_pos_source` 列）。この試行は爪の命令の記録もない
- 全試行で、ブロックは x≈−48〜−62mm・z≈240〜256mm、箱は x≈−194〜−210mm・z≈233〜240mm（同じ場所に置いていた）。教えた位置5（Unity で x=−70・z=253mm）のあたり
- D で分かったこと: 画像の上の端はマットの z≈−55mm に当たり、本線の決め（−89mm）と約 34mm 違う。ブロックのあたりでは本線の座標が **約 9mm 手前**にずれる計算。また、ブロックの上面（高さ 24mm）を床の点として直しているので、さらに数 mm ずれる。B はすべての試行で同じずれ方なので、成功と比べる分類には効かない
- Unity では、運んでいるブロックは描かない（ブロックは最初の位置のまま）

### 場面の決め方

- 閉じた瞬間: 爪を閉じる命令（gripper_cmd=1 の最初）にいちばん近い記録の行
- 持ち上げたあと: 閉じたあと、j0 が閉じたときから 8° 以上回る（箱へ運び始める）か、次の開く命令か、記録の終わりまでのうち、爪先がいちばん高かった行
- 箱にいちばん近い: 閉じたあと、爪先・爪・前腕（link3〜6）のどれかが箱の4つの壁（底板は除く）にいちばん近づいた行。距離は見た目のメッシュの頂点（間引き）と壁の直方体の距離、めり込むと負

### 分類の候補の決め方

Unity と実機は数十 mm ずれうるので、**失敗の値を成功5件の幅と比べ、大きく外れたときだけ**決めた。

| 分類 | 条件 |
|---|---|
| つかむ前に外れた | 閉じた瞬間の爪先の高さか、ブロックとの水平のずれが、成功の最大（高さ 25.3mm・ずれ 39.4mm）より 15mm 以上大きい |
| 箱に当たった | 閉じたあとに、爪先・爪・前腕が箱の壁に 10mm より深くめり込む。±10mm 以内は「縁すれすれ」と書くだけ |
| つかんだが持ち上げで落ちた | 上の2つに当たらず、閉じたあとに爪先が 30mm 以上持ち上がっている（人の判定はどれも pick=no） |
| 分からない | 上のどれでもない（爪の命令がない・持ち上げていない など） |

### 表（A で測ったずれ: 表との差は中央値 0.3mm・腕が触れない行で最大 4.2mm。実機とのずれは未測定で数十 mm ありうる）

| 試行 | 結果 | 分類の候補 | 閉じた瞬間の爪先の高さ mm | ブロックとの水平のずれ mm | 持ち上げたあとの高さ mm（上がった分） | 箱の壁までの最短 mm（部位） | 箱の中に入った | 補正の区間（秒） |
|---|---|---|---|---|---|---|---|---|
| pi05_20261030_185133 | 失敗 | 分からない | 23.0 | 11.4 | 30.7（+7.7） | 1.1（爪） | 入った（深さ 5.1mm） | 5回 119-191;252-290;364-470;490-521;567-664 |
| pi05_20261030_190659 | 失敗 | つかんだが持ち上げで落ちた | 28.0 | 19.9 | 132.1（+104.1） | 49.5（爪） | いいえ | 4回 50-72;115-126;131-179;202-318 |
| pi05_20261031_180023 | 失敗 | つかんだが持ち上げで落ちた | 19.5 | 15.7 | 58.7（+39.2） | 45.3（爪） | いいえ | 3回 46-70;86-146;167-210 |
| pi05_20261031_183219 | 失敗 | 分からない | 16.8 | 6.8 | 19.1（+2.3） | 79.0（爪） | いいえ | 1回 35-80 |
| pi05_20261031_183426 | 失敗 | 分からない | 21.4 | 14.9 | 31.0（+9.6） | 19.0（爪） | いいえ | 5回 18-97;114-161;190-232;251-272;286-319 |
| pi05_20261101_191547 | 失敗 | 分からない | 22.4 | 4.7 | 24.9（+2.5） | 69.2（爪） | いいえ | なし |
| pi05_20261101_192758 | 失敗 | つかんだが持ち上げで落ちた | 18.3 | 10.5 | 77.2（+58.9） | 61.5（爪） | いいえ | 2回 25-45;76-132 |
| pi05_20261101_194042 | 失敗 | つかんだが持ち上げで落ちた | 21.5 | 15.3 | 136.3（+114.8） | 33.6（爪） | いいえ | 1回 52-120 |
| pi05_20261106_184312 | 失敗 | 分からない | 21.2 | 26.5 | 36.1（+14.9） | −1.0（爪） | 入った（深さ 4.9mm） | 2回 19-32;51-63 |
| pi05_20261106_185050 | 失敗 | つかんだが持ち上げで落ちた | 21.3 | 21.7 | 206.3（+185.0） | 61.4（爪） | いいえ | 2回 18-31;49-65 |
| pi05_20261106_185225 | 失敗 | つかんだが持ち上げで落ちた | 17.5 | 30.0 | 170.9（+153.4） | 63.8（爪） | いいえ | 3回 16-25;42-54;66-76 |
| pi05_20261106_185415 | 失敗 | つかんだが持ち上げで落ちた | 22.9 | 22.5 | 180.5（+157.6） | 54.4（爪） | いいえ | 2回 17-27;46-64 |
| pi05_20261030_184245 | 成功 | （比べる用） | 25.3 | 24.5 | 51.6（+26.3） | 10.4（爪） | いいえ | なし |
| pi05_20261031_185246 | 成功 | （比べる用） | — | — | — | 213.4（爪） | いいえ | なし（爪の命令の記録なし） |
| pi05_20261030_210954 | 成功 | （比べる用） | 20.5 | 26.4 | 52.2（+31.7） | 10.2（爪） | いいえ | 1回 70-136 |
| pi05_20261031_175527 | 成功 | （比べる用） | 20.3 | 39.4 | 56.7（+36.4） | 1.3（爪） | いいえ | 2回 92-120;180-221 |
| pi05_20261101_190347 | 成功 | （比べる用） | 23.3 | 10.0 | 58.2（+34.9） | 17.8（爪） | いいえ | 2回 109-137;180-232 |

どの数字から決めたかは `B/b_table.csv` の `category_basis` 列。補正が閉じる前にあったか（`correct_before_close`）、爪を閉じる命令の回数（`n_grip_close_cmds`）もそこにある。

### 読み取れること

- **閉じた瞬間の爪先の位置は、失敗も成功もほぼ同じ幅**（高さ 17〜28mm 対 20〜25mm、水平のずれ 5〜30mm 対 10〜39mm）。Unity の解像度（実機とのずれが未測定）では、失敗を「爪を閉じた位置の違い」として見分けられなかった
- 失敗の7件は、閉じたあと 39〜185mm 持ち上げている。人の判定はどれも pick=no なので、ブロックが爪から抜けたか、そもそも爪の間に入っていなかった。どちらかは Unity では分けられない（爪の開き具合の実測がない）
- 失敗の5件（分からない）は、閉じたあと **2〜15mm しか持ち上げていない**。成功は 26〜36mm 持ち上げてから j0 を回している。「閉じたあと持ち上げない」はこの依頼の分類にない形なので「分からない」に入れた
- 箱に 10mm より深く当たった試行はない。2件（185133・184312）は爪が箱の中に 5mm ほど入る（縁から ±1mm）

### 画像（試行ごと `B/images/{run}/`）

| ファイル | 中身 |
|---|---|
| `1_close_side_note.png` | 爪を閉じる命令の瞬間、横から（正射影。マット 0mm・ブロック上面 24mm・箱の縁 43mm（仮）の線と、50mm ごとの目盛り） |
| `1_close_front_note.png` | 同じ瞬間、ブロックの先から根元を見る向き |
| `2_lift_side_note.png` | 持ち上げたあと、横から |
| `3_box_side_note.png`・`3_box_front_note.png` | 箱にいちばん近づいたとき |
| `*.png`（`_note` なし） | 字を書き込む前の元の画 |

「横」は根元から見た向き（ブロックまたは箱へ）に直角で、手前（+Z 側）から見ている。B の画だけ、腕が暗くならないよう補助の光を1つ足している（D は足していない）。`B/first_frames/{run}.jpg` は検出の確認用（黄＝マット四隅、赤丸＝ブロック、青＝白箱）。

---

## C. 次に実機で試す置き場所の候補を確かめる

### 候補

- 格子の9点: Unity の X = 120・160・200mm、Z = 180・220・260mm（`G_x{X}_z{Z}`）
- 教えた5か所（`T1`〜`T5`）: `runs/run_pick_place/{試行}/teach/drag_v2_*.json` の爪を閉じた瞬間の角度（`C/teach_pick_angles.csv`）を A の置き方で置いた爪先の位置（`C/teach_pick_fk.csv`）

### 姿勢の選び方（どれも肩 j1 ≤ 88°・表と Unity の差 5mm 以内の行だけ）

| 姿勢 | 条件 |
|---|---|
| つかむ | 爪先の高さ 10〜40mm。水平のずれ ＋ 0.2×|高さ−20mm| が小さい順（20mm は B の成功の閉じた瞬間の高さ 20〜25mm に合わせた目安） |
| ブロックの真上 | 高さ 60〜100mm・水平のずれ 15mm 以内のうち、つかむ姿勢との関節の差（j0〜j4 の絶対値の和）が小さい順 |
| 持ち上げ | 高さ 80〜130mm・同じく |
| 白箱の上 | 白箱の中心（B の検出の中央値 x=−203.8・z=235.9mm）の上、高さ 80〜130mm・水平 15mm 以内のうち、持ち上げ姿勢との関節の差が小さい順 |

道すじ: HOME [0,0,0,0,0,−30] → 真上 → つかむ（開）→ 持ち上げ（閉）→ 白箱の上 を、関節角の直線補間（区間ごとに 40 刻み）で動かして、各刻みで箱の壁・マット・ブロックとの近さを測った。

上から見えるか: D で合わせた上カメラから、つかむ直前（爪は開）を 960×540 で撮り、腕ありと腕なしで赤の画素を数えた比（50% 以上で「見える」）。

### 表（1点1行。すべての姿勢の角度は `C/c_table.csv`）

| 点 | 種類 | x, z mm | つかむ姿勢 [j0〜j5] | 肩 j1（つかむ／最大）° | 爪先の狙いからのずれ mm | 表と Unity の差 mm | 箱の壁までの最短 mm | マットまでの最短 mm | 上から見えるブロック | 判定 | 理由 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| G_x120_z180 | 格子 | 120, 180 | — | — | — | — | — | — | — | 使えない | 表の中で、つかむ・真上・持ち上げを選べなかった（候補の行は、角度を置き直すと腕がマットの下 10mm に入る＝表を作ったときに床に当たって止まった値） |
| G_x120_z220 | 格子 | 120, 220 | [−38.6, 77.4, 15.0, −14.9, −84.2, −29.4] | 77.4／77.4 | 1.2 | 0.3 | 56.2 | 7.1 | 100% | 使えそう | |
| G_x120_z260 | 格子 | 120, 260 | [−24.5, 85.5, −10.0, 1.4, −70.5, −32.9] | 85.5／85.5 | 2.9 | 0.5 | 31.2 | 7.1 | 100% | 使えそう | |
| G_x160_z180 | 格子 | 160, 180 | [−42.2, 69.9, 25.0, 0.1, −79.2, −29.4] | 69.9／69.9 | 0.5 | 0.3 | 52.2 | −3.5 | 100% | 要注意 | つかむ姿勢で腕がマットの下 3.5mm（±10mm 以内なので当たるとは決めない） |
| G_x160_z220 | 格子 | 160, 220 | [−31.1, 77.4, 5.0, 10.1, −74.2, −29.4] | 77.4／77.4 | 0.5 | 0.3 | 58.4 | 8.1 | 100% | 使えそう | |
| G_x160_z260 | 格子 | 160, 260 | [−30.0, 87.7, −19.5, 3.6, −70.1, −30.0] | 87.7／87.7 | 2.6 | 0.6 | 26.6 | 19.7 | 99% | 使えそう | |
| G_x200_z180 | 格子 | 200, 180 | [−56.6, 77.4, 5.0, −14.9, −74.2, −29.4] | 77.4／77.4 | 1.8 | 0.3 | 56.9 | 7.6 | 100% | 使えそう | |
| G_x200_z220 | 格子 | 200, 220 | [−42.5, 88.0, −15.0, 1.4, −70.5, −32.9] | 88.0／88.0 | 3.4 | 4.2 | 20.8 | 12.1 | 100% | 使えそう | |
| G_x200_z260 | 格子 | 200, 260 | [−31.9, 87.7, −38.3, 18.6, −45.1, −30.0] | 87.7／87.7 | 2.4 | 4.4 | 20.5 | 22.2 | 97% | 使えそう | |
| T1 | 教えた位置 | 161.6, 212.8 | [−46.1, 72.4, 10.0, −14.9, −74.2, −29.4] | 72.4／72.4 | 0.9 | 0.3 | 56.5 | 16.1 | 100% | 使えそう | |
| T2 | 教えた位置 | 195.0, 260.5 | [−38.0, 87.0, −33.0, −4.0, −47.0, −32.0] | 87.0／87.7 | 3.9 | 4.2 | 34.3 | 16.4 | 100% | 使えそう | |
| T3 | 教えた位置 | 104.5, 247.4 | [−31.6, 77.4, 5.0, −14.9, −74.2, −29.4] | 77.4／77.4 | 1.9 | 0.3 | 56.1 | 7.6 | 100% | 使えそう | |
| T4 | 教えた位置 | 238.7, 127.2 | [−54.2, 77.4, 5.0, 15.1, −74.2, −29.4] | 77.4／77.4 | 1.0 | 0.3 | 39.7 | 7.5 | 100% | 使えそう | |
| T5 | 教えた位置 | −70.0, 253.0 | [14.4, 76.7, 10.3, −1.4, −78.0, −29.5] | 76.7／76.7 | 2.1 | 0.4 | 36.5 | 6.4 | 93% | 使えそう | |

- 「判定」の線（この依頼で置いた目安。エイイチが決め直してよい）: 使えない＝表で選べない・箱に 10mm より深く当たる・マットの下に 10mm より深く入る・上から 50% 以上隠れる・つかむ姿勢の爪先が狙いから 10mm 超離れる。要注意＝箱やマットから ±10mm 以内
- マットまでの最短が 6〜8mm の点は、つかむ姿勢で爪先がマットに近づいているだけ（24mm のブロックをつかむので当たり前）
- 箱に当たる点はなかった。どの点も箱にいちばん近いのは最後の「白箱の上」の姿勢で、運んでいるブロックは描いていない
- **T5（教えた位置5）は、つかむ直前には上から 93% 見える**。計画書の「位置5は上カメラから見てブロックが腕の陰にほぼ隠れる」は、HOME の最初のコマのこと（B の最初のコマでもブロックは爪の真下に半分隠れていた）と思われる
- 肩が 88° ちょうど（G_x200_z220）や 87.7° の点は、実機で命令が捨てられる境目（約 88°）に近い

画像（1点1枚）: `C/c_{点}.png`（左: 上カメラから見たつかむ直前、中: 横から見たつかむ姿勢、右: 横から見た箱にいちばん近いとき）。元の画は `C/images/`。

---

## D. Unity と実機の画を並べる

### 合わせ方

1. 実機の1コマ: `pi05_20261101_191547`（補正なしの失敗）の `top.mp4` の最初のコマ（アームが HOME、ブロックは爪の真下・白箱は右）
2. マットの四隅を本線の `mat_corner_detection.detect_corners` で検出（(326,0)・(1698,0)・(1704,935)・(305,922) px）
3. 画素は正方形・光軸は画像の中心・レンズのゆがみなしと仮定。**カメラがほぼ真下を向いているため、ホモグラフィ（四隅）だけでは焦点距離が決まらない**（ホモグラフィから解くと 22,534px という外れた値になった）
4. **上の2隅は画像の上の端で切れていて本当の角ではない**（v=0）。そこで次の 10 個の式で 9 個（焦点距離・位置3・向き3・上の2点の奥行き2）を最小二乗で解いた（`twin_camera_d.py`）
   - 下の2隅（本当の角）を (±310, 361) mm に
   - 上の2点を「マットの左右の縁 x=±310mm の上のどこか」に
   - HOME の爪先の青い印2つ（実機の画像で検出）を、Unity の HOME の左右の爪先（tip Brep の位置、高さ 235mm・左右の間 59mm）に
5. Unity のカメラにその位置・向き・縦の画角を入れ、シーンの光のまま（色・光・模様は調整しない）1920×1080 で撮り、学習と同じ 384×216 に縮めて並べた

### 結果

| 項目 | 値 |
|---|---|
| 焦点距離 | 1,457px（縦の画角 40.7°・横 66.7°。iPhone の広角として自然な値） |
| カメラの位置（Unity の m） | x=−0.003・y=0.652・z=0.205（根元のほぼ真上から 20cm 手前、高さ 65cm） |
| 向き | ほぼ真下（前向きの方向 (0.030, −0.999, −0.026)） |
| 合わせた6点の再投影の誤差 | 最大 3.2px（下の2隅 3.1・3.2px、上の2点 2.8・1.0px、爪先の印 1.9・0.9px） |
| 画像の上の端が当たるマットの位置 | z≈−58・−54mm（本線は −89mm と決めている） |
| 残ったずれ（白箱の中心） | Unity が実機より右に 21.6px・上に 14.5px（およそ 12mm）。箱は B の位置（本線のやり方のホモグラフィ）に置いたので、上の z のずれが入っている |

- 爪先の印の誤差が小さいのは、焦点距離をそこに合わせたから。独立した確かめは白箱だけ
- 焦点距離は、Unity の爪の開き（左右の間 59mm）が実機と同じという前提に乗っている

### 画像

| ファイル | 中身 |
|---|---|
| **`D/d_side_by_side_384x216.png`** | 左: 実機・右: Unity（どちらも 384×216） |
| `D/d_side_by_side_960x540.png` | 同じものを大きく |
| `D/d_overlay_960x540.png` | 実機と Unity を半分ずつ重ねたもの（橙＝マット四隅、青＝爪先の印） |
| `D/real_home_*.{jpg,png}`・`D/unity_home_*.png` | 元の画 |
| `D/camera_fit.json` | 合わせたカメラの値すべて |
| `D/residuals.json` | 残ったずれ |

素の見え方の違い: Unity のマット（0.8m 角の板）は実機のマットより広く、方眼の模様がない。光は上からの1灯で、影が濃い。机・配線・爪の青い印はない。

---

## 使ったコード

Unity（`Robotics_UX_New/Assets/TwinCheck/Editor/`、このフォルダの `code/` に写し）

| ファイル | 役目 |
|---|---|
| `TwinArm.cs` | 1体のアームに関節角を直接置く（ドライブ・物理は使わない）・爪の開閉・爪先の位置 |
| `TwinGeom.cs` | 腕のメッシュと箱の壁・ブロックの距離、箱の中に入ったか、腕のいちばん低い所 |
| `TwinScene.cs` | シーンの写しを開き、1体目のアーム・ブロック・箱・床・光だけを見せる。RenderTexture で撮って PNG に書く |
| `TwinCsv.cs` | CSV を読む |
| `TwinCheckA.cs` | A（表の行の角度を置いて爪先を書き出す） |
| `TwinCheckB.cs` | B の測定（`Metrics`）と、B・C・D の撮影（`Render`） |
| `TwinCheckC.cs` | C の道すじの確かめと撮る場面 |
| `TwinCheckD.cs` | D の手がかり（HOME の爪の点の 3D 位置） |
| `TwinCheckDump.cs` | 下調べ（プレハブ・シーンの中身を書き出す） |

Python（`Robotics_UX_New/Assets/TwinCheck/Python~/`、同じく `code/` に写し。`~` で終わるフォルダは Unity が読み込まない）

| ファイル | 役目 |
|---|---|
| `twin_a.py` | A のまとめ |
| `twin_prep.py` | B の下ごしらえ（試行の選び出し・時刻の並び・ブロックと箱の検出）。本線の検出の関数を読み取り専用で借りる |
| `twin_table_b.py` | B の表・分類の候補・撮る場面 |
| `twin_overlay.py` | B の画に字と目盛りを書く |
| `twin_c.py` | C の姿勢の選び出し・表・画像 |
| `twin_camera_d.py` | D のカメラ合わせ |
| `twin_d.py` | D の撮る場面と並べた画 |

### シーンの扱い（2026-11-07 変更・依頼111）

- 作業用のシーンの写しは記録（git・Unity Version Control）に入れない形にした。元のシーン `Assets/Scenes/R2R2R-20260224.unity` は読むだけで、開かない・保存しない
- 依頼107 で使った写しは、このフォルダの **`code/R2R2R-20260224_cs110.unity`** に残した。Unity Version Control の **cs:110**（2026-07-17）の元のシーンと同じ中身（MD5 `b1613505dfd781275874030ce3da23d6`）
- 動かすたびに `TwinCheckA.OpenWorkScene` が元のシーンを `Assets/TwinCheck/_work/TwinCheck_Base.unity` に写して開く（Unity は Assets の外のシーンを開けないため）。`TwinScene.Open`・`TwinCheckA.Run`・`TwinCheckDump.RunScene` はみなこれを通る
- 写すたびに、出力フォルダ（`-twinOut`、なければ `-twinShots` のあるフォルダ）に `scene_source.json`（元のパス・MD5・changeset）を書く。changeset は Unity Version Control で読み込んでいる版の中身と MD5 が一致したときだけ書き、元のシーンにチェックインしていない変更があれば `null` になる
- `.gitignore`・`ignore.conf` に `_work/`・`_work.meta`・`Python~/__pycache__/` を足した
- やり直し方のコマンドは変わらない。D（`TwinCheckD.Points` → `twin_camera_d.py`）を一時フォルダでやり直し、`D/home_points_unity.json`・`D/camera_fit.json` と差の最大 0 を確かめた（このフォルダの結果は上書きしていない）
- `code/` の `.cs` は依頼107 の時点のもの。この変更のあとのコードは git `736b3d3`・Unity Version Control cs:146

## やり直し方

Unity の Editor でこのプロジェクトを開いていないことを確かめてから、`Robotics_UX_New` で:

```bash
U=/Applications/Unity/Hub/Editor/2022.3.62f3/Unity.app/Contents/MacOS/Unity
O="/Volumes/KIOXIA/r2r2r-data/lab/twin/20261107_失敗の再生と置き場所"
P="Assets/TwinCheck/Python~"
run() { $U -batchmode -quit -projectPath "$PWD" -logFile /tmp/twin.log -executeMethod "$@"; }

# A
run TwinCheckA.Run -twinIn "$O/A/a_sample_rows.csv" -twinOut "$O/A/a_unity_tip_20.csv" -twinSign 1
python3 $P/twin_a.py "$O/A/a_sample_rows.csv" "$O/A/a_unity_tip_20.csv" "$O/A/a_result_20rows.csv"

# B
python3 $P/twin_prep.py
run TwinCheckB.Metrics -twinTrials "$O/B/trials.csv" -twinTimeline "$O/B/timeline.csv" -twinOut "$O/B/timeline_metrics.csv"   # 約3分
python3 $P/twin_table_b.py
run TwinCheckB.Render -twinShots "$O/B/shots_b.csv"
python3 $P/twin_overlay.py

# D（C の上カメラに使うので C より先）
run TwinCheckD.Points -twinOut "$O/D/home_points_unity.json"
python3 $P/twin_camera_d.py
python3 $P/twin_d.py shots
run TwinCheckB.Render -twinShots "$O/D/shots_d.csv"
python3 $P/twin_d.py compose

# C
run TwinCheckA.Run -twinIn "$O/C/teach_pick_angles.csv" -twinOut "$O/C/teach_pick_fk.csv"
python3 $P/twin_c.py select
run TwinCheckA.Run -twinIn "$O/C/c_candidates.csv" -twinOut "$O/C/c_candidates_fk.csv"
python3 $P/twin_c.py finalize
python3 -c "import json;d=json.load(open('$O/D/camera_fit.json'));p=d['cam_pos_unity_m'];q=d['cam_quat_unity_xyzw'];open('$O/C/cam_for_c.csv','w').write('cam_x,cam_y,cam_z,cam_qx,cam_qy,cam_qz,cam_qw,cam_fov\n'+','.join(f'{v:.6f}' for v in p+q+[d['fov_vertical_deg']])+'\n')"
run TwinCheckC.Path -twinPoses "$O/C/c_poses.csv" -twinOut "$O/C/c_path.csv" -twinShots "$O/C/c_shots.csv" -twinCam "$O/C/cam_for_c.csv" -twinImgDir "$O/C/images"   # 約3分
run TwinCheckB.Render -twinShots "$O/C/c_shots.csv"
python3 $P/twin_c.py table
```

`teach_pick_angles.csv` は README の C 節の5つの教えた記録から、`gripper_events` の close の idx の角度を取り出したもの。

作業用のシーン `Assets/TwinCheck/TwinCheck_Base.unity` は `Assets/Scenes/R2R2R-20260224.unity`（2026-07-17 の書きかけの状態）のファイルの写し。開くだけで保存しない。

## 気をつけること・分かっていないこと

- Unity と実機の「角度 → 位置」のずれは測っていない。B の数 mm〜十数 mm の差では決めていない
- 白箱は 80mm 角・高さ 43mm（仮）・まっすぐ置いた箱としている。実物の寸法は測っていない
- ブロックの位置は本線のホモグラフィ（上の端を −89mm とする決め）で出しているので、D の結果からは z が約 9mm 手前にずれている見込み
- 爪の開き具合は Unity の `GripperController` と同じ 0°（開）／40°（閉）で描いている。実機の開き具合の記録は使っていない
- 表（`fk_scan_motion_based_v2.csv`）には、物理で何かに当たって止まった値が混ざっている疑いがある（200行のうち、角度どおりなら腕が何かに触れる行が 63 行・約 3 割、10mm 超ずれる行が 54 行）。表だけで姿勢を選ぶと、角度どおりなら床の下に入る姿勢を拾うおそれがある

## 決めたこと（エイイチ 2026-11-07）

- A の大きなずれ：表（`fk_scan_motion_based_v2.csv`）には物理で床などに当たって止まった値が混ざっている、という見方でよい。表だけで姿勢を選ぶときは、Unity で角度を置き直して確かめた行だけを使う
- C の「使えそう」の目安（上から 50% 以上見える・箱やマットから ±10mm 以内は要注意・つかむ姿勢の爪先が狙いから 10mm 以内）は、このまま使う
- 上カメラの上の端のずれ（本線は −89mm、D では −55mm 前後）は調べる（依頼110 の F）
