mechArm 270 Pi 動的追従の限界を探る Log#26 | 「完璧に届くまで待つ」をやめた ― だいたい合ったら離して、入った/入らないを人が見て決める
LOG220R2R2R2026/6/3

mechArm 270 Pi 動的追従の限界を探る Log#26 | 「完璧に届くまで待つ」をやめた ― だいたい合ったら離して、入った/入らないを人が見て決める

届くのを待ち続けるより、だいたいで離して、結果を貯めるほうが前に進めた。

「完璧に届くまで待つ」をやめた

PLACE 動作の降下で、止まらなくなっていた。狙った高さに 2.5° 以内で届いたら次へ進む、という到達判定が厳しすぎて、何度やっても降下の途中で中断する。位置は目で見れば合っているのに、コードは「まだ着いていない」と判断して先に進まない。

そこで、精度を詰める方向に粘るのをやめた。完璧な到達を待つ関門(HOLD)を外し、だいたいの位置まで降りたら一度止めて、目で見て問題なければ離す。離した結果が「白箱に入ったか」を自分で y/n で答え、そのときの位置と角度を一緒に記録する。狙いは到達の精度を上げることではなく、入った/入らないを測れる形にすることだった。9 回ぶん、データが取れた。変えたのは次の3点になる。

これまで

今回

狙った高さに 2.5° 以内で届くまで待つ。届かなければ中断

だいたい降りたら止める。完璧な到達は待たない

届かないと先(離す工程)に進めない

目視で問題なければ離す。判断は人がする

成否を測る手段がなかった

離した結果を y/n で記録し、位置・角度と一緒に貯める

何が引っかかっていたか

降下は、白箱の真上の決まった高さへ行こうとする。だがアームは任意の高さに行けるわけではなく、実測で集めた到達可能姿勢のテーブルにある姿勢にしか行けない。そのテーブルから「狙いに一番近い姿勢」を選んで送る。問題は、選ばれた姿勢が今の姿勢から関節の構成が大きく違う「別解」のことがある点だった。同じ場所で少し下げるだけのつもりが、複数の関節が十数度から二十度動く姿勢に飛ぼうとして、実機が時間内に届ききらない。あるいは、その位置の真上には目標高さの姿勢がテーブルに無く、より低い姿勢しか近くにない。どちらの場合も、到達判定が「届いていない」と出して中断する。

位置は合って見えるのに止まる、という状態が続いた。ここで、到達の精度を上げる方向に粘るのをやめた。やりたいのは「掴んだブロックを白箱に入れる」ことで、関節角度が狙いにきっかり届くことではない。だいたいの位置で離して、入ったかどうかを直接確かめるほうが、ずっと早く前に進む。

切り替えた方針

降下に「送って数秒待ったら、届かなくても次へ進む」という選択肢を足した。これまでの「届くまで待って、ダメなら中断」は残したまま、離す工程から呼ぶときだけ、待った後に進む。降りる前の安全確認(テーブルに姿勢があるか、距離が離れすぎていないか、低すぎないか、関節の範囲内か)はそのまま効かせる。変えたのは「送った後に、きっかり届かないと中断する」ところだけだ。

そのうえで、離す工程を新しく一つ作った。降下が終わったら、離す前に一度止めて次の4つを表示する。白箱の位置、赤ブロックの位置、アーム6軸の角度、横カメラで見た高さ。これを見て、問題なければ離す。離したあと「白箱に入った?」に y か n で答える。その答えと4つの値を、1試行1行で記録に追記していく。VLM に成否を判定させる構想の、人の目で代える版だ。まずは人が判定して、どの条件なら入るのかのデータを貯める。

横カメラの高さは、表示だけにした。横からはアームが邪魔で白箱を安定して捉えられないので、自動の安全判定には使わない。赤ブロックが画面のどのあたりの高さに写っているかを数値で出して、判断材料にするだけにとどめた。

離す工程(17_place_release.py)

降下が終わった姿勢で①〜④を集め、一度停止し、目視で Enter したら離す。離した後に y/n を聞いて記録する。離す前にアームは動かさない。動くのはグリッパーの開閉だけだ。全文を載せる。

"""
17_place_release.py

PLACE動作【新】RELEASE。HOLD(tight reach 到達待ち)を廃止し、降下が settle した
だいたいの位置で一度停止 → ①〜④を表示 → 目視で離す → 成否 y/n を記録する。
成否と①〜④を CSV に貯め、どの条件なら白箱に入るかを後で学ぶためのデータ収集工程。

呼ばれた時点で、降下(15 を proceed_on_timeout=True で呼んだ)が settle 済み=
アームは白箱のだいたい上で静止している前提。

絶対ルール:
- send_coords() 永久禁止。
- release_all_servos() 禁止。
- 17 は離す前にアームを動かさない(daemon_send_angles を呼ばない)。
- グリッパー開閉は daemon_set_gripper(state) のみ(state=0:開く / 1:閉じる)。
- ④ サイド center_y は表示のみ。自動 ABORT しない(白箱の検出が横で不安定なため)。
- 退避・HOME 復帰は 14 の責務。

呼び出し:
  run_release(cap_top, cap_side, box_unity_x, box_unity_z, current_angles) -> bool
    True  : 離した(成否は CSV に記録)
    False : 離さず中断(グリッパーは触らない)
"""

import sys
import time
from datetime import datetime
from pathlib import Path

import cv2

SCRIPTS_ROOT = Path(__file__).resolve().parents[3]
MODULE_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPTS_ROOT))
sys.path.insert(0, str(MODULE_DIR))

from visual_servo_pick import (
    daemon_get_angles,
    daemon_set_gripper,
    detect_red_block,
    pixel_to_unity,
)
from side_camera_utils import detect_red_block_side
import place_preview
import place_trial_log

SNAP_DIR = Path.home() / "Downloads"
GET_ANGLES_RETRY = 3
GET_ANGLES_RETRY_SLEEP = 0.2


def _get_angles_retry():
    for _ in range(GET_ANGLES_RETRY):
        m = daemon_get_angles()
        if m is not None and len(m) == 6:
            return m
        time.sleep(GET_ANGLES_RETRY_SLEEP)
    return None


def _detect_block_top(cap_top):
    """上カメラで赤ブロックを検出。(block_px, unity_x, unity_z) を返す。
    検出失敗・例外は (None, None, None) で続行(離す判断はエイイチに委ねる)。"""
    try:
        for _ in range(5):
            cap_top.read()
        ret, frame = cap_top.read()
        if not ret or frame is None:
            return None, None, None
        px = detect_red_block(frame)
        if px is None:
            return None, None, None
        ux, uz = pixel_to_unity(px[0], px[1])
        return px, ux, uz
    except Exception as e:
        print(f"[RELEASE] [WARN] 上カメラ赤検出で例外: {e}")
        return None, None, None


def _side_height(cap_side):
    """横カメラで赤ブロックの center_y(pixel) を取り、side フレームを保存する。
    表示のみ。自動 ABORT しない。
    戻り値: (center_y or None, snapshot_path or "")。
    center_y の取り方は 16 の _check_side_height と同じく det["center_y"]。"""
    if cap_side is None:
        return None, ""
    try:
        for _ in range(5):
            cap_side.read()
        ret, frame = cap_side.read()
        if not ret or frame is None:
            return None, ""
        snap = ""
        try:
            SNAP_DIR.mkdir(parents=True, exist_ok=True)
            ts = datetime.now().strftime("%Y%m%d_%H%M%S")
            snap_path = SNAP_DIR / f"place_release_side_{ts}.png"
            if cv2.imwrite(str(snap_path), frame):
                snap = str(snap_path)
        except Exception as e:
            print(f"[RELEASE] [WARN] サイド画像保存で例外: {e}")
            snap = ""
        cy = None
        det = detect_red_block_side(frame)
        if det is not None:
            cy = det["center_y"]
        return cy, snap
    except Exception as e:
        print(f"[RELEASE] [WARN] 横カメラ高さ取得で例外: {e}")
        return None, ""


def run_release(cap_top, cap_side, box_unity_x, box_unity_z, current_angles):
    """HOLD 廃止 RELEASE。降下 settle 後に①〜④収集→目視 Enter→離す→y/n→CSV。

    戻り値:
        True  : 離した(y/n は CSV へ記録済み)
        False : 離さず中断(グリッパーは触らない)
    """
    print("[RELEASE] 開始(HOLD 廃止・目視判定)")

    # ② 赤ブロック(上カメラ)
    block_px, block_x, block_z = _detect_block_top(cap_top)
    # ③ 6軸角度(実測)
    angles = _get_angles_retry()
    if angles is None:
        print("[RELEASE] [WARN] 実測角取得失敗。current_angles を代用")
        angles = list(current_angles) if current_angles else [None] * 6
    # ④ 横カメラ高さ(表示のみ・スナップショット保存)
    side_cy, side_snap = _side_height(cap_side)

    # ターミナル表示(①〜④)
    print(f"[RELEASE] ① 白箱 xz = ({box_unity_x:+.3f}, {box_unity_z:+.3f}) m")
    if block_x is not None:
        print(
            f"[RELEASE] ② 赤ブロック xz = ({block_x:+.3f}, {block_z:+.3f}) m  "
            f"pixel={block_px}"
        )
    else:
        print("[RELEASE] ② 赤ブロック検出できず(None)")
    angles_str = (
        "[" + ", ".join(
            f"{a:.2f}" if isinstance(a, (int, float)) else "None" for a in angles
        ) + "]"
    )
    print(f"[RELEASE] ③ 6軸角度(実測) = {angles_str}")
    if side_cy is not None:
        print(
            f"[RELEASE] ④ 横カメラ center_y = {side_cy}px(表示のみ・判定なし)  "
            f"画像: {side_snap}"
        )
    else:
        print(
            f"[RELEASE] ④ 横カメラ高さ取得できず(表示のみ)  画像: {side_snap}"
        )

    # プレビュー更新(失敗しても本処理は止めない)
    try:
        lines = [
            f"box=({box_unity_x:+.3f},{box_unity_z:+.3f})",
            (
                f"block=({block_x:+.3f},{block_z:+.3f})"
                if block_x is not None else "block=None"
            ),
            (f"side_cy={side_cy}" if side_cy is not None else "side_cy=None"),
        ]
        place_preview.update(
            phase="RELEASE 確認",
            block_px=block_px,
            lines=lines,
        )
        place_preview.tick()
    except Exception:
        pass

    # 一度停止して目視 → Enter で離す / 何か入力で中断
    ans = input(
        "[RELEASE] ①〜④を確認。離すなら Enter / "
        "離さず中断は n+Enter または Ctrl+C: "
    ).strip().lower()
    if ans != "":
        print("[RELEASE] 中断。グリッパーは触らずに終了(離さない)")
        return False

    # 離す(開く)
    print("[RELEASE] グリッパーを開いて離します(daemon_set_gripper(0))")
    try:
        daemon_set_gripper(0)
    except Exception as e:
        print(f"[RELEASE] [ERROR] グリッパー開放で例外: {e}")
        print("[RELEASE] それでも目視判定と記録は続行します")

    # ⑤ 成否(エイイチ入力、y/n に正規化。空入力は再入力)
    success = ""
    while success not in ("y", "n"):
        raw = input("[RELEASE] 白箱に入った? y/n: ").strip().lower()
        if raw == "":
            print("[RELEASE] y か n を入力してください")
            continue
        success = "y" if raw == "y" else "n"

    # 共通モジュールで CSV 追記(失敗しても log_trial 側で握りつぶす)
    place_trial_log.log_trial(
        "released", box_unity_x, box_unity_z,
        stage="release",
        block_x=block_x, block_z=block_z,
        angles=angles,
        side_center_y=side_cy,
        side_snapshot=side_snap,
        success=success,
    )

    print(f"[RELEASE] 完了(success={success})。退避/HOME は 14 が行います")
    return True


if __name__ == "__main__":
    print(
        "17_place_release.py は run_release(cap_top, cap_side, "
        "box_unity_x, box_unity_z, current_angles) を\n"
        "エクスポートするライブラリです。14 から --release で呼ばれます。"
    )

降下に「待ったら次へ」を足した

降下側には、フラグを一つ足しただけにした。デフォルトは従来どおり「届くまで待って、ダメなら一度再送、それでもダメなら中断」。離す工程から呼ぶときだけ、このフラグを立てて「待ったら、届かなくても次へ進む」に切り替える。降りる前の安全確認は、フラグの値に関係なく常に効く。変更が及ぶのは送信後の挙動だけだ。今回の切り替えの本体である判定部分を抜き出す。

def descend_step(target_angles, label, proceed_on_timeout=False):
    """1段の降下送信。
    手順:
      1) send_angles(target, speed=SPEED, wait=0) を1回送る
      2) Mac 側で sleep + get_angles で到達確認(REACH_TIMEOUT_SEC まで)
      3) 未到達なら同一 target を「1回だけ」再送(14 の 3 回再送は持ち込まない)
      4) 再送後も未到達なら追加送信せず未到達で返す(張り付き・押し付け回避)

    proceed_on_timeout=True(HOLD 廃止経路)の場合:
      2) の待機後、tight reach 未達でも 3)4) を行わず settle 姿勢で
      reach_ok=True を返す。再送・ABORT しない(17 の RELEASE 経路用)。
      JOINT_LIMITS チェックと最初の send + 待機は通常どおり実施する。

    戻り値: (reach_ok: bool, resent: bool, last_measured: list or None)
    """
    violations = check_joint_limits(target_angles)
    if violations:
        for i, v, lo, hi in violations:
            print(f"[{label}] [LIMITS] NG: j{i}={v:.2f} 範囲 [{lo}, {hi}] 外")
        return False, False, None

    print(f"[{label}] send_angles {fmt(target_angles)}")
    daemon_send_angles(target_angles, speed=SPEED, wait=0)
    ok, err, measured = _wait_for_reach(target_angles, label)
    if ok:
        return True, False, measured

    if proceed_on_timeout:
        err_str = f"{err:.2f}" if err is not None else "None"
        print(
            f"[{label}] tight reach 未達 max_err={err_str}°だが "
            "proceed_on_timeout=True のため settle 姿勢で次へ進む(再送しない)"
        )
        return True, False, measured

    err_str = f"{err:.2f}" if err is not None else "None"
    print(f"[{label}] 未到達 max_err={err_str}°。1回だけ再送する")
    daemon_send_angles(target_angles, speed=SPEED, wait=0)
    ok2, err2, measured2 = _wait_for_reach(target_angles, label)
    if ok2:
        return True, True, measured2

    err2_str = f"{err2:.2f}" if err2 is not None else "None"
    print(f"[{label}] 再送後も未到達 max_err={err2_str}°。これ以上送信しない")
    return False, True, measured2 if measured2 is not None else measured

降下が中断しても、人が離すかを決められるようにした

降下が安全確認で中断したとき、これまでは自動で HOME に戻っていた。だが中断の多くは「その位置の真上に目標高さの姿勢がテーブルに無く、少し低い姿勢が最も近かった」というもので、アームは安全に止まっている。目で見れば離せる位置のこともある。だから中断のあと、HOME に戻る前に「離す工程へ進む(r)」か「HOME に戻る(h)」かを選べるようにした。r を選んでも、離す工程は必ず①〜④を表示して目視で止まるので、いきなり離すことはない。

一点だけ分けた。降下が安全確認で止まったとき(想定内の静止)は r/h を選べる。だが降下の処理中に予期しない例外が出たとき(何が起きたか分からない状態)は、r を出さず HOME だけに倒す。状態が読めないときに離す判断材料は揃わないからだ。この分岐を、14 の降下後に入れた。

        if descended:
            print("[DESCEND] 最終段に到達し HOLD 中")
        else:
            print("[DESCEND] 降下が中断されました(HOLD なし)")

        # 降下後(HOLD or 中断)の状態を目視
        print("[DESCEND] 降下後の状態を確認してください")

        # 後段(release/align/較正)へ進むかどうか。HOLD 到達なら True。
        # release モードの ABORT 時のみ、目視で「離す(r) / HOME(h)」を選ばせる。
        proceed_to_post = descended

        if not descended and args.release and not descend_error:
            print(
                "[DESCEND] 降下は ABORT しましたが、アームは静止しています。"
                "目視で問題なければ離す工程へ進めます(17 で①〜④確認→目視 Enter→離す)。"
            )
            while True:
                choice = input(
                    "[DESCEND] 離す工程へ進む=r / HOME へ戻る=h を入力: "
                ).strip().lower()
                if choice in ("r", "h"):
                    break
                print("[DESCEND] r か h を入力してください")
            if choice == "r":
                proceed_to_post = True
                print("[DESCEND] r 選択。離す工程(17)へ進みます")
            else:
                proceed_to_post = False
                print("[DESCEND] h 選択。HOME へ戻ります")
        else:
            # HOLD 到達時、または release 以外(align/較正)の ABORT 時は従来どおり。
            print(
                "[DESCEND] 確認が済んだら Enter で ALIGN/較正(降下成功時)または "
                "HOME 復帰(降下中断時)に進みます"
            )
            input()

試行を1本の記録に集約した

離した試行も、降下や接近で止まった試行も、同じ1本の記録に追記するようにした。離せた回は白箱位置・赤ブロック位置・6軸角度・横カメラ高さ・成否を、止まった回は白箱位置と止まった理由を残す。どこに置けば届くか、どこなら入るかの地図を、後で一望できるようにするためだ。記録は専用の小さなモジュールにまとめ、接近・降下・離すの各工程から呼ぶ。記録の追記でアームの動きは一切変えていない。全文を載せる。

"""
place_trial_log.py

PLACE 試行を 1 本の CSV に集約する共通の追記モジュール。
14(approach abort)/ 15(descend abort)/ 17(released)から log_trial() を呼ぶ。
記録だけ。アームは動かさない。

CSV: ~/r2r2r-scripts/logs/place_trials.csv
列: timestamp, stage, result, box_x, box_z, block_x, block_z,
    j0..j5, side_center_y, side_snapshot, success, abort_reason
"""

import csv
from datetime import datetime
from pathlib import Path

LOG_DIR = Path.home() / "r2r2r-scripts" / "logs"
TRIALS_CSV = LOG_DIR / "place_trials.csv"

HEADER = [
    "timestamp", "stage", "result", "box_x", "box_z", "block_x", "block_z",
    "j0", "j1", "j2", "j3", "j4", "j5",
    "side_center_y", "side_snapshot", "success", "abort_reason",
]


def _num(v, fmt="{:.4f}"):
    if v is None:
        return ""
    try:
        return fmt.format(v)
    except Exception:
        return str(v)


def log_trial(result, box_x, box_z, *, stage="", block_x=None, block_z=None,
              angles=None, side_center_y=None, side_snapshot="",
              success="", abort_reason=""):
    """1 試行を place_trials.csv に追記する。失敗しても例外を投げない(本処理を止めない)。

    引数:
        result: "approach_abort" / "descend_abort" / "released"
        box_x, box_z: 白箱中心 Unity 座標(m)。常に渡す。
        stage: "approach" / "descend" / "release"
        block_x, block_z: 赤ブロック Unity 座標(release のみ。無ければ None)
        angles: 6 要素の実測関節角 list(release のみ。無ければ None)
        side_center_y: 横カメラ center_y(release のみ。無ければ None)
        side_snapshot: 横画像パス(release のみ)
        success: "y"/"n"(release のみ)
        abort_reason: ABORT 理由テキスト(abort のみ)
    """
    try:
        LOG_DIR.mkdir(parents=True, exist_ok=True)
        new = not TRIALS_CSV.exists()
        ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        if angles is None:
            ang_cols = ["", "", "", "", "", ""]
        else:
            ang_cols = [_num(a, "{:.2f}") for a in list(angles)[:6]]
            while len(ang_cols) < 6:
                ang_cols.append("")

        row = [
            ts, stage, result,
            _num(box_x), _num(box_z),
            _num(block_x), _num(block_z),
            *ang_cols,
            ("" if side_center_y is None else side_center_y),
            side_snapshot,
            success,
            abort_reason,
        ]
        with open(TRIALS_CSV, "a", newline="") as f:
            w = csv.writer(f)
            if new:
                w.writerow(HEADER)
            w.writerow(row)
        print(f"[TRIAL_LOG] {result} を記録: {TRIALS_CSV}")
    except Exception as e:
        print(f"[TRIAL_LOG] [WARN] 記録に失敗: {e}")

掴み直しのための開閉スクリプト

繰り返し試すには、離したあと毎回ブロックを掴み直す必要がある。手で爪に挟む前後に、グリッパーを開閉する小さなスクリプトを用意した。アームは動かさず、開閉だけする。全文を載せる。

"""
gripper.py

グリッパーを開く/閉じるだけの最小スクリプト。アームは一切動かさない。
手で赤ブロックを挟む前後の開閉に使う(PLACE データ収集の掴み直し用)。

使い方:
    python3 gripper.py open     # 開く  (daemon_set_gripper(0))
    python3 gripper.py close    # 閉じる (daemon_set_gripper(1))

絶対ルール:
- daemon_send_angles / send_coords は呼ばない(関節を動かさない)。
- release_all_servos() 禁止。
- グリッパーは daemon_set_gripper(state) のみ。state=0:開く / 1:閉じる。
"""

import sys
from pathlib import Path

SCRIPTS_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(SCRIPTS_ROOT))

from visual_servo_pick import (
    get_daemon_socket,
    close_daemon_socket,
    daemon_set_gripper,
)

USAGE = "使い方: python3 gripper.py [open|close]"


def main():
    if len(sys.argv) != 2 or sys.argv[1] not in ("open", "close"):
        print(USAGE)
        sys.exit(1)

    action = sys.argv[1]
    state = 0 if action == "open" else 1   # 0:開く / 1:閉じる

    print("[GRIPPER] daemon 接続中...")
    get_daemon_socket()
    try:
        print(f"[GRIPPER] {action}(daemon_set_gripper({state}))")
        daemon_set_gripper(state)
        print(f"[GRIPPER] 完了: {action}")
    except Exception as e:
        print(f"[GRIPPER] [ERROR] {e}")
        raise
    finally:
        try:
            close_daemon_socket()
        except Exception:
            pass
        print("[GRIPPER] socket close")


if __name__ == "__main__":
    main()

9 回ぶんのデータが取れた

白箱の位置を変えながら回した結果が、1本の記録に貯まった。まず内訳だけ先に出す。

release       9回   (入った y=6 / 入らなかった n=3)
approach_abort 1回
descend_abort  5回

そのうえで、記録の中身をそのまま載せる。

timestamp,stage,result,box_x,box_z,block_x,block_z,j0,j1,j2,j3,j4,j5,side_center_y,side_snapshot,success,abort_reason
2026-06-03 17:20:48,descend,descend_abort,-0.1648,0.2498,,,,,,,,,,,,高さ下振れ ee_y_n=+0.100<+0.105
2026-06-03 17:21:42,descend,descend_abort,-0.1656,0.2413,,,,,,,,,,,,高さ下振れ ee_y_n=+0.100<+0.105
2026-06-03 18:16:22,descend,descend_abort,-0.1648,0.2399,,,,,,,,,,,,高さ下振れ ee_y_n=+0.100<+0.105
2026-06-03 18:16:51,release,released,-0.1648,0.2399,-0.1975,0.2914,23.02,67.06,-12.65,-24.25,-66.88,-30.14,620,(side),y,
2026-06-03 18:18:34,descend,descend_abort,-0.2085,0.2179,,,,,,,,,,,,高さ下振れ ee_y_n=+0.104<+0.105
2026-06-03 18:18:51,release,released,-0.2085,0.2179,-0.2523,0.2561,33.13,67.06,-12.65,-23.99,-67.23,-29.70,639,(side),y,
2026-06-03 18:23:15,release,released,0.1612,0.2054,0.2260,0.2515,-30.05,86.22,-28.65,16.87,-80.59,-29.97,575,(side),y,
2026-06-03 18:24:42,release,released,0.1868,0.1427,0.2850,0.1871,-44.47,85.60,-29.17,16.78,-79.62,-30.23,580,(side),n,
2026-06-03 18:25:56,release,released,0.2570,0.1613,0.3033,0.1535,-48.25,87.97,-31.99,21.44,-78.04,-30.23,538,(side),n,
2026-06-03 18:27:12,release,released,0.2507,0.2585,0.2785,0.2599,-43.33,89.20,-52.20,1.40,-46.58,-30.14,588,(side),n,
2026-06-03 18:28:35,release,released,0.1639,0.2214,0.2209,0.2710,-29.53,86.22,-28.56,12.12,-81.29,-30.14,559,(side),y,
2026-06-03 18:29:52,release,released,-0.2673,0.2927,-0.2694,0.2893,43.85,88.06,-53.96,2.98,-43.94,-29.79,670,(side),y,
2026-06-03 18:30:59,approach,approach_abort,-0.0209,0.1010,,,,,,,,,,,,3D距離超過 124.3mm>100mm
2026-06-03 18:31:31,descend,descend_abort,-0.2298,0.1667,,,,,,,,,,,,高さ下振れ ee_y_n=+0.102<+0.105
2026-06-03 18:31:45,release,released,-0.2298,0.1667,-0.2954,0.1953,44.91,67.06,-12.65,-24.43,-66.09,-29.79,635,(side),y,

離せた 9 回について、赤ブロックが白箱の中心からどれだけ離れた所に落ちたかを並べる。差は block から box を引いた値(mm)で、横(X)と奥行き(Z)の2方向。

白箱 (x, z) m

赤ブロック (x, z) m

差 X (mm)

差 Z (mm)

入った

(-0.165, +0.240)

(-0.198, +0.291)

-33

+51

y

(-0.209, +0.218)

(-0.252, +0.256)

-44

+38

y

(+0.161, +0.205)

(+0.226, +0.252)

+65

+47

y

(+0.187, +0.143)

(+0.285, +0.187)

+98

+44

n

(+0.257, +0.161)

(+0.303, +0.154)

+46

-7

n

(+0.251, +0.258)

(+0.279, +0.260)

+28

+2

n

(+0.164, +0.221)

(+0.221, +0.271)

+57

+50

y

(-0.267, +0.293)

(-0.269, +0.289)

-2

-4

y

(-0.230, +0.167)

(-0.295, +0.195)

-65

+28

y

9回のデータから構造の片鱗は見えているが、まだ式で言い切れる段階ではない。入った回の多くは、赤ブロックが白箱より奥(Z が +40 前後)に落ちている。入らなかった 3 回は、Z 方向の差が小さい(+44 / -7 / +2)か、横の差が大きい。掴んだブロックは、爪で挟んだ位置のぶん、狙いから一定方向にずれて落ちる。そのずれが白箱の置き場所(=アームの姿勢)によって変わる。差の大きさだけでは入る/入らないを割り切れていないのは、白箱の位置を毎回変えていて、同じ条件の繰り返しが無いからだ。

次にやること

今わかったのは、「掴んだブロックが狙いから一定方向にずれて落ちる」という構造があること、そしてそのずれが姿勢によって変わることだ。これを掴むには、白箱を同じ位置に固定して何度も繰り返し、ずれが安定して同じ方向に出るかを見るのがいい。安定していれば、そのぶんを降下の狙いに足して命中率を上げられる。ばらつくなら、手で挟む位置をそろえる工夫が先になる。次は、白箱を1か所に固定した状態で連続して離し、この「奥にずれる」傾向が姿勢ごとに決まった定常のずれなのか、それとも掴むたびの物理的なばらつきなのかを切り分ける。

完璧な到達を待っていたときは、データは0件だった。だいたいで離したら、9件取れた。精度は上がっていない。でも、学習は始まった。

※ プロジェクト名「R2R2R」は、UC Berkeley・Toyota Research Instituteの研究「Real2Render2Real」(Yu, Fu et al., 2025)の概念に触発されています。現実空間をデジタルツイン化し、仮想空間での試行から得た知見を実世界のモーター駆動へ還元する——その設計と実装過程を毎日記録しています。技術アプローチおよび目的は独自のものです。