mechArm 270 PickAndPlace 12回試行。PythonログとUnityデータで「全成功」と判明した
ログを見て「0/12」と書いた。コードを読んで「12/12」だと気づいた。
このプロジェクトについて
iPhoneで撮影した現実空間をUnityでデジタルツイン化し、ロボットアームの動作をシミュレーションで最適化してから実機に展開するパイプラインを個人で構築している。
ターゲットはシンプルだ。ロボットアームがコーヒーカップを掴み、台座の上に置く。その一連の動作を、iPhoneとUnityだけで設計できることを証明する。
この記録は「失敗だと思っていたものが成功だった」話と、「成功の先にある問い」の話だ。
動作モックの大量生成
LOG99で確立したpick and placeの動作をベースに、4アプローチ × 3速度 = 12パターンのCSVを生成して連続再生した。
standard(正面から) × slow / normal / fast
high(上から) × slow / normal / fast
low(低い軌道) × slow / normal / fast
side(横から) × slow / normal / fastPythonスクリプトで5次多項式補間の軌道CSVを自動生成し、TrajectoryFollower.cs が連続再生しながらフレームごとにログを記録する仕組みを作った。
Logs/batch01/run_001/
frames/ ← パターンごとのフレームログ(ee位置・関節角度・カップ位置)
summary.csv ← パターンサマリー(success/fail・失敗原因・誤差値)
reset_log.csv ← カップリセット確認ログ「全失敗」の正体
summary.csvを最初に見たとき、こう書いてあった。
success = False × 12全パターン失敗。原因は GRASP_MISS と JOINT_LAG。
落胆した。しかしログを詳しく読んで気づいた。
standard_slow のセグメント到達状況:
to_grasp(250フレーム)
→ to_lift(249フレーム)
→ to_place_high(249フレーム)
→ lowering(374フレーム)
→ to_home(249フレーム)全セグメントを完走している。
カップの位置も追った。
動作開始時:cup = (-0.20, 0.00, 0.22)
to_lift開始:cup_y = 0.025 ← 持ち上がった
lowering終了:cup = (0.19, 0.025, 0.21) ← 台座付近に移動
to_home終了:cup = (0.20, 0.025, 0.22) ← 台座の上に静止カップはグラスプされ、運ばれ、台座に置かれていた。
12/12、全パターン成功していた。
何が間違っていたか
grasp_offset_err という値がto_graspセグメント中ずっと -1.0000 になっていた。これはFixedJointがまだ確立していない状態を示す番兵値として設計したものだ。
検出ロジックはこの -1.0000 を「グラスプ失敗」として判定していた。しかしFixedJointはto_graspからto_liftへの遷移タイミングで正常に確立しており、その後カップは全フレームで追従している。
設計の意図:
grasp_offset_err = -1 → 「まだグラスプしていない」(正常)
検出ロジックの誤解釈:
grasp_offset_err = -1 → 「グラスプ失敗」(誤検知)fast系4パターンの JOINT_LAG も同様だ。J1が15度の閾値を frame33 でわずかに超えたが、その後収束して全セグメントを完走した。閾値の設定が厳しすぎた。
データで診断した成果
この誤検知を発見できたのは、ログが充実していたからだ。
success=False という1行の結論だけでは「全失敗」にしか見えない。しかしフレームログにはカップのY座標が 0.025 になった瞬間が記録されていた。セグメント遷移のフレーム番号が記録されていた。
「感覚でなく、数値で確認する」ずっと続けてきた診断アプローチが、LOG100でも機能した。
成功の先にある問い
12/12の完走が確認できた。しかし、ここで一つの問いが生まれた。
シミュレーションでカップの位置を追えても、実機では追えない。実機でどう成否を判断するか。
シミュレーションでは cup_x/y/z がUnityの物理エンジンからフレームごとに取れる。実機では取れない。
この問いを今日一日かけて考えた。
実機での成否判定:産業ロボットの現実から考える
最初に思いついたのはロードセル(重量センサー)だった。台座に置くだけで、カップの重さが乗ったかどうかをシンプルに判定できる。
しかし産業ロボットのラインであれば機能するが、人とロボットが協働する環境では成立しないと気づいた。
産業ライン:
環境が固定されている。カップは必ずここにある。
→ ロードセルが機能する
協働環境:
カップをどこに置くかは人間が決める。
台座の位置も変わる。
→ 環境が毎回変動する産業ロボットが「失敗を検出する設計」ではなく「失敗しない環境を作る設計」で動いている理由がここにある。
では変動する環境に対応するには何が必要か。
カメラとAIしかない。
環境を固定できないなら、環境を毎回認識し直すしかない。
iPhoneがロボットの目になる
ここで気づいたことがある。
R2R2RのパイプラインにはすでにiPhoneが存在している。3Dスキャンのためのツールとして。
現在:
iPhone(3Dスキャン・静的)→ Unity(座標)→ ロボット制御
実機展開後:
iPhone(リアルタイム認識・動的)→ カップ位置 → 成否判定同じデバイスが、役割を事前処理から実行時処理に拡張する。
iPhoneにはLiDARスキャナーが搭載されている(12 Pro以降)。ARKitを使えば、リアルタイムの深度マップとカップの3D座標が取れる。追加機材ゼロで。
シミュレーション:
cup_x/y/z = Unity物理エンジンが計算
実機:
cup_x/y/z = iPhone LiDARが実測ログの構造は変わらない。成否判定のロジックも変わらない。シミュレーションと実機のデータが同じフォーマットで並ぶ。
これがReal2Render2Realの名前通りの構造になる。
フェーズ設計
実機展開に向けた現実的な段階をこう整理した。
フェーズ2a(ゼロコスト):
関節完走フラグ + iPhone LiDAR前後スナップ比較
→ 「カップが台座に乗っているか」を目視に代わって確認
フェーズ2b(数百円):
台座ロードセル追加(固定環境デモ限定)
→ 数値でプレース成否を記録
フェーズ3(本格化):
iPhone ARKitリアルタイムストリーミング
→ シミュレーションと同じログ構造で実機データを取得初期段階でmm単位の座標追跡を再現しようとする必要はない。「カップが台座に乗って倒れていない」が証明できれば、最初のデモとしては成立する。
確定したこと
動作モックの大量生成インフラが完成した。ログ構造が確立した。
そしてより重要な問いが見えた——シミュレーションと実機の差分をどう埋めるか。
その答えは追加機材の購入ではなく、すでに手元にあるiPhoneの役割拡張にある。
「精度はあとからお金で解決できる。
まず体験が成立するかどうかを確認する。」iPhoneで始まったパイプラインは、iPhoneで完結する。
実装コード(TrajectoryFollower.cs・抜粋)
// 失敗しても再生を止めず最後まで記録する設計
private void DetectFailure(string segment, int frame)
{
// GRASP_MISS:グラスプ誤差が閾値超過(FixedJoint確立後のみ判定)
if (graspJointEstablished && graspOffsetErr > GRASP_MISS_THRESHOLD)
{
RecordFailure("GRASP_MISS", frame, $"err={graspOffsetErr:F4}");
return;
}
// JOINT_LAG:関節追従遅れが閾値超過
float maxLag = GetMaxJointLag();
if (maxLag > JOINT_LAG_THRESHOLD)
{
RecordFailure("JOINT_LAG", frame, $"lag={maxLag:F2}deg");
}
}
// 失敗検出後も再生継続
private void RecordFailure(string reason, int frame, string detail)
{
if (failState != "") return; // 最初の失敗のみ記録
failState = reason;
failFrame = frame;
failReason = detail;
// ← return しない。再生を継続する。
}