ループを証明するとはどういうことか。R2R2Rの設計を問い直した
LOG125R2R2R2026/4/3

ループを証明するとはどういうことか。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 True

Stage 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 False

Stage 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")
        break

Iriun 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に繋がった瞬間は、ちょっとテンション上がったーーー。