ループを証明するとはどういうことか。R2R2Rの設計を問い直した
ピック&プレイスが成立した。ブロックが箱に入った。
次はR2R2Rループだ。Real→Simulation→Real。でも「ループが回った」と言うために、何を証明しなければいけないのか。それを問い直すところから今日は始まった。
💡 Key Takeaway
「動いたデータを取る」だけではループではない。タスク結果をフィードバックして動作が改善する——その一周を証明することがR2R2Rの本題だ。その設計を固め、カメラ判定の基盤としてIriun WebcamによるiPhone→OpenCV接続を確立した。
最初に考えていたことは間違っていた
最初に考えたのは2つのルートだった。
ルートA:実機の関節角度ログ取得 → シミュレーション比較
ピック&プレイス中の関節角度を記録して、Unityシミュレーションの予測値と差分を取る。数値として誤差が可視化される。実装量が少なく、既存資産(pymycobot + Python CSV)だけで動く。
ルートB:iPhone再スキャン → 物体位置ずれ検出
ロボットがブロックを置いた後、iPhoneで再スキャンして「設計上の置き場所」と「実際の置き場所」のずれをXYZで計測する。座標変換が必要で実装量は多いが、空間で誤差が見える。
一見するとルートAの方が現実的に見えた。でももう一度考え直した。
ルートAの本質的な問題
mechArm 270のサーボは指令角度に対して自己収束する内部制御を持っている。send_angles() を送れば、内部のPID制御がその角度に到達させる。
つまり get_angles() で取得できるのは「指令通りに動いたかどうか」の確認でしかない。正常動作していれば指令値とほぼ一致するし、しなければハードウェアの故障だ。
Unityと比較しても「Unityのシミュレーションが正確だった」という事実確認にしかならない。
そして最大の問題がある。ループとして閉じない。
R2R2Rループの本質は「タスク結果をフィードバックして動作を改善する」ことだ。ルートAで得られるのはプロセスの記録であって、タスクの結果ではない。プロセスを見ても「次の動作をどう変えるか」が出てこない。
ルートA(角度ログ) | ルートB(再スキャン) | |
|---|---|---|
実装量 | 少 | 多 |
新規ツール | 不要 | LiDARアプリ必要 |
最大の壁 | ほぼなし | 座標系アライメント |
ループとして成立するか | 弱い | 成立する |
将来性 | 低い | 高い |
記事メッセージ | 「検証できた」 | 「ループが回った」 |
詰まるリスク | 低 | 高 |
ルートAは「記録として有用だが、R2R2Rループの証明にはならない」という結論になった。おまけとして角度ログ取得は実装するが、ループの証明にはルートBの考え方を使う。
座標系アライメントの難しさは軽量版(B-Lite)で回避する。PLACEの目標位置にマスキングテープで枠を貼り、置いた後にiPhoneで真上から撮影、ブロックが枠内に収まっているか判定する。完全自動でなくても「ループが回った事実」は証明できる。
R2R2Rで証明すべきことを再定義した
「ブロックを移動できた」ではなく、
「身近なツールで、ロボットの動作体験を計測・改善・回復できるサイクルが個人で回せる」
これを証明することがゴールだ。
3フェーズ設計
Phase 1 — ループが回る(事実の証明)
シミュレーション→実機のデータが繋がることを確認する。ここで終わると「動いた」止まりになる。
[Unityで軌跡生成]
↓
[Pi→mechArm実行]
↓
[PLACE後の配置結果を確認(カメラ or 目視)]
↓
[誤差があれば補正値を算出]
↓
[補正した軌跡で再実行]
↓
[1回目と2回目の誤差を比較]Phase 2 — 最適化データの生成(改善の証明)
「何を変えたら体験が良くなったか」を数値で示す。
計測項目 | 内容 | 取得方法 |
|---|---|---|
動作時間 | 各フェーズの所要時間 | Python timestamp |
配置精度 | 目標位置からのXYZずれ | iPhoneカメラ画像 |
軌跡のなめらかさ | 関節速度の変化率 | get_angles()ログ |
Unityで複数の軌跡パターンを生成→実機で実行→結果を比較する比較実験ができる。「iPhoneとUnityだけで最適化データを生成できる」の証明になる。
Phase 3 — リカバリー設計(体験設計の証明)
失敗を検知して自律的に回復する動作を実装する。
リカバリーこそがロボットUXの本質だ。掴めなかった、落とした、ずれた——そこで止まるのか、回復するのかが体験の差になる。これを個人がiPhone+Unityで設計できると証明できれば、「ロボット開発はエンジニアだけのものではない」というコアメッセージが完成する。
リカバリーシナリオ | トリガー | 対応動作 |
|---|---|---|
掴み失敗 | gripper_value が閾値以下 | 一度引いて再アプローチ |
置きずれ | カメラ判定でROI外 | PLACE座標を補正して再実行 |
落下 | 移動中にカメラで未検出 | 停止してログ記録 |
フェーズ全体の位置づけ:
フェーズ | 証明すること | 状態 |
|---|---|---|
Phase 1 | ループが一周回る | 現在進行中 |
Phase 2 | 改善データが生成できる | 次フェーズ |
Phase 3 | 失敗から回復できる | 将来フェーズ |
カメラ判定のStage設計
センサー追加はしない方針にした。接触センサーや圧力センサーを追加すると体験設計の複雑さが増す。カメラだけで判定する。
カメラで検知できる失敗パターン:
失敗タイプ | 発生タイミング | 検知に必要な情報 |
|---|---|---|
掴み失敗 | PICK直後 | グリッパーに物があるか |
落下 | 移動中 | 軌跡上に物があるか |
置き失敗 | PLACE直後 | 目標位置に物があるか |
Stage 1:gripper_value による掴み判定
追加センサー不要。pymycobotから直接取得できる。
# pick_and_place.py に追加するリトライロジック
def pick_with_check(mc):
mc.set_gripper_state(0, 70) # グリッパーを開く
time.sleep(1.0)
mc.send_angles(APPROACH_PICK, SPEED)
time.sleep(2.5)
mc.send_angles(PICK, SPEED)
time.sleep(2.5)
mc.set_gripper_state(1, 70) # グリッパーを閉じる
time.sleep(1.5)
gripper_val = mc.get_gripper_value()
# 0=完全クローズ、100=完全オープン
# 物体を掴んでいれば途中で止まる(30〜60程度)
# 空振りなら限界まで閉じる(5以下)
if gripper_val < 10:
print(f"掴み失敗 gripper_val={gripper_val} → リトライ")
mc.send_angles(HOME, SPEED)
time.sleep(2.0)
return pick_with_check(mc) # 1回リトライ
else:
print(f"掴み成功 gripper_val={gripper_val}")
return TrueStage 2:iPhoneカメラ + PLACE後の画像判定
import cv2
import numpy as np
IRIUN_INDEX = 1 # iPhone背面カメラ
# ROI: PLACEの目標位置に対応する画像領域(実測で調整)
ROI = (320, 200, 520, 400) # (x1, y1, x2, y2)
THRESHOLD = 80 # 輝度閾値(暗い物体 = ブロックあり)
def check_place_result():
cap = cv2.VideoCapture(IRIUN_INDEX)
time.sleep(0.5) # カメラ安定待ち
ret, frame = cap.read()
cap.release()
if not ret:
print("カメラ取得失敗")
return False
x1, y1, x2, y2 = ROI
roi = frame[y1:y2, x1:x2]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
mean_val = gray.mean()
# デバッグ用に画像を保存
cv2.imwrite("place_check.jpg", frame)
print(f"ROI mean brightness: {mean_val:.1f}")
if mean_val < THRESHOLD:
print("ブロック検出 → PLACE成功")
return True
else:
print("ブロック未検出 → PLACE失敗 → リトライ")
return FalseStage 3:リアルタイム動画監視(Phase 3以降)
# 将来実装:動作中の落下検知
cap = cv2.VideoCapture(IRIUN_INDEX)
while robot_is_moving:
ret, frame = cap.read()
if detect_drop(frame):
robot.stop()
log_failure("drop_detected")
breakIriun WebcamでiPhoneをOpenCVに繋いだ
Webcamアプリ比較
アプリ | OpenCV相性 | macOS Sonoma安定性 | コスト | 判定 |
|---|---|---|---|---|
Continuity Camera | 要注意 | 非推奨警告あり | 無料 | ✗ |
EpocCam(Elgato) | 仮想カメラ経由でOK | 比較的安定 | 無料〜1,500円 | △ |
Iriun Webcam | 仮想カメラ経由でOK | 安定(2026/3更新) | 無料 | ✓ |
USB Webカメラ | 最も安定 | 高い | 2,000〜5,000円 | △(別途購入必要) |
Continuity CameraはmacOS Sonoma上でOpenCVからアクセスするとAVFoundationの非推奨警告が発生する。AVCaptureDeviceTypeExternal is deprecated for Continuity Cameras というエラーで、Info.plistの編集が必要になるケースがあり、環境によっては解決しない。
IriunはAVFoundationを経由せず仮想カメラデバイスとして認識させるため、この問題を完全に回避できる。
セットアップ手順
Step 1: iPhone → App Store → 「Iriun Webcam for PC and Mac」をインストール
Step 2: Mac → iriun.com → Mac用ドライバーをダウンロード・インストール
Step 3: システム設定 → プライバシーとセキュリティ → セキュリティ
→「Iriun Extensionを許可」をクリック
Step 4: iPhoneとMacを同じWiFiに接続
Step 5: iPhoneのIriunアプリを起動
Step 6: MacのIriun Webcamアプリを起動 → 自動認識OpenCVインストール
pip3 install opencv-python
# Collecting opencv-python
# Downloading opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl (46.2 MB)
# Successfully installed numpy-2.4.4 opencv-python-4.13.0.92カメラインデックス確認
import cv2
for i in range(5):
cap = cv2.VideoCapture(i)
if cap.isOpened():
print(f"Camera index {i}: OK")
cap.release()
else:
print(f"Camera index {i}: NG")実行結果:
WARNING: AVCaptureDeviceTypeExternal is deprecated... ← 警告のみ、動作には影響なし
Camera index 0: OK ← Mac内蔵カメラ(FaceTime HD)
Camera index 1: OK ← Iriun = iPhone背面カメラ ★
Camera index 2: OK ← Mac内蔵FaceTime(別認識)
Camera index 3: NG
Camera index 4: NG静止画保存で実機確認
import cv2
for idx in [1, 2]:
cap = cv2.VideoCapture(idx)
ret, frame = cap.read()
if ret:
cv2.imwrite(f"camera_{idx}.jpg", frame)
print(f"camera_{idx}.jpg saved")
cap.release()camera_1.jpg → 明るく高画質の天井画像 = iPhone背面カメラ(Iriun) camera_2.jpg → 暗い天井画像 = Mac内蔵FaceTime
index 1 = Iriun(iPhoneの背面カメラ) で確定。
確立されたシステム構成
[iPhone on 三脚]
Iriun Webcam(WiFi経由)
↓
[Mac / OpenCV]
cv2.VideoCapture(1)
Python 判定スクリプト
↓ SSH(10.42.0.1)
[Pi / Ubuntu 18.04]
pymycobot
↓
[mechArm 270-Pi]iPhone→Mac→Pi→mechArmの全経路が繋がった。
確立したもの
項目 | 状態 |
|---|---|
R2R2Rループの設計(3フェーズ) | ✅ 確定 |
カメラ判定方針(センサーなし) | ✅ 確定 |
Stage設計(Stage1→2→3) | ✅ 確定 |
Iriun Webcamインストール | ✅ 完了 |
iPhone↔Mac接続 | ✅ 確認 |
OpenCV camera index確認 | ✅ index 1 = iPhone背面カメラ |
Pythonからフレーム取得 | ✅ 動作確認済み |
次はStage 1の gripper_check.py とStage 2の camera_check.py の実装に入る。
わかったこと
「データを取る」と「ループが回る」は別のことだ。
プロセスを記録することはできる。でもそれはログであってフィードバックではない。フィードバックとは、結果が次の動作を変えることを指す。
R2R2Rはそのサイクルを証明するプロジェクトだ。だから「動いたこと」より「結果が次に繋がったこと」を証明しなければいけない。
その設計が固まった。
最初に考えていた「角度ログを比較する」方法、自分でもなんとなく物足りないと思ってたんだよね。でも「ループの証明にならない」という言葉でやっと腑に落ちた。iPhoneがOpenCVに繋がった瞬間は、ちょっとテンション上がったーーー。
