フィジカルAI研究室を作る Log#29 | π0.5 が位置2 を外した理由として、上カメラの傾きを疑う

この記事の素材(コード・ログ・データ 7ファイル・まとめてダウンロードあり)
AIで読む ChatGPT・Claude などで、この記事に質問できます

Physical Intelligence の π0.5(ロボット用の AI のモデル)が、実物の小型ロボットアーム mechArm 270 で位置2 のブロックまで下りなかった理由として、いちばん疑わしいのは、上から撮るカメラ(スマホ)が、学習データを撮ったときより約1.7°回っていたことです。 カメラの傾きを学習データにそろえ直すと、π0.5 は位置2 のつかむ姿勢(肩 j1 が約88°、ひじ j2 が約−34°)へ向かう答えを3回続けて出しました。ただ、付け直したときにカメラの上下左右の位置も数px変わっていて、比べた回数も午前4回・夜3回と少なく、傾きだけが原因だとはまだ確かめていません。最後の数mmで爪がブロックの奥側にずれ、つかむところまでも届いていません。

この回でやったことで、次のことが変わりました。

  • カメラの傾きが数字で見えるようになった:位置合わせの画面と実機推論の始めに、学習データとの傾きの差が出る。これまでの確かめは上下左右のずれ(±20px)だけで、1.7°の回転は OK のまま通っていた
  • お手本集めの落とし穴の候補が1つ見つかった:位置ごとに撮る時間を分けると、その間にカメラが少し動き、カメラの向きそのものが「どの位置か」の手がかりとして学習されるおそれがある。確かめる前でも、撮る前に傾きをそろえておけば避けられる
  • 時間:傾きを数字で出す道具は、Claude Code への依頼から25分で使える形になった(17:16〜17:41)。原因を探すのにかかった時間は測っていない
  • 失敗:位置2 を手で直そうとゲームパッドの補正を押すと、推論のプロセスが落ちて固まり、Mac の再起動が要る状態で中断した

前の回(LOG484)で、位置ごとに10本ずつそろえたお手本50本で学習した π0.5 は、位置1 のブロックは3回とも自分でつかめたのに、位置2 では4回とも、つかむ姿勢まで下りずに途中で止まりました。ブロックの置き場所はお手本と同じなのに、なぜ位置2 だけ届かないのか。午前の記録を洗い直すと、11:20ごろから、カメラの写り方が少しだけ回っていました。その向きは、学習データ50本のうち、位置1 を撮った9本とだけ同じでした。照明の違いは画像の明るさを比べて否定できましたが、傾きが原因かどうかの決着は、録画の画像だけを回して π0.5 に渡す試しでつける予定です。

やったことなぜ結果
午前の記録から問題点を洗い出す
位置2 だけ届かない理由を探す
8つの問題点。いちばん大きいのは位置2 に届かないこと
位置2 のお手本と午前の画像を並べる
ブロックの置き方を疑う
ブロックの位置は同じ。違ったのはカメラの傾き
マットの角から傾きを測る
学習データとの違いを数字で見る
右下がりに写っているのは位置1 の9本だけ
傾きを数字で出す道具を作る(依頼142)
手で合わせられるようにする
位置合わせの画面に傾きと差が出る
スマホを回して傾きを合わせ、位置2 を試し直す
傾きが原因かを見る
π0.5 が位置2 のつかむ姿勢へ向かった。爪は数mm奥にずれた。カメラの位置も少し変わったので、傾きだけの影響とは言い切れない
ゲームパッドで直そうとする直した動きを学習データにする
推論のプロセスが落ちて固まり、中断
傾きをソフトでそろえる(依頼144)・固まるのを直す(依頼145)を出す
手で合わせる手間をなくす・補正を使えるようにする
依頼箱で順番待ち(この回の時点では未実行)

実機と学習の記録は、午前(LOG484)と同じ KIOXIA の lab/Log28/ に入れています。

GPU を買うか借りるかの判断は、Runpod で実際に借りて学習させた費用をもとに、別のページにまとめています。

導入比較ガイドロボットAIの学習用GPU、買うか借りるか実際に借りて学習させた費用と、損益分岐の計算機で決められます

作業ログ ― 位置2 に届かない理由を探し、カメラの傾きを合わせ直して試すまで

ここから下は作業の記録です。コマンド・出力・コードは全量で載せます。効果を書いた所のうち、数字で書いたものは測った値、「見込み」と書いたものはまだ確かめていない値です。

この回で何度も出てくる言葉を先に言い換えておきます。

  • 位置1〜位置5:赤いブロックを置く5つの場所。お手本はこの5か所で10本ずつ撮った(LOG479 で決めた形)
  • j0〜j5:アームの6つの関節。j0 は根元の回転、j1 は肩、j2 はひじ、j4 は手首
  • 上カメラ:マットを真上から撮るスマホ。π0.5 はこの画像と関節の角度を見て、次の動きを決める
  • 傾き:上カメラの画像の中で、マットの手前の辺が水平からどれだけ回っているか。プラスは右下がり、マイナスは右上がり
  • 依頼箱:Claude Code への依頼を、Mac の見張り係が順番に渡して報告を書き戻す仕組み。この回の依頼142・144・145 はここに入れた

午前の記録(lab/Log28)から洗い出した8つの問題点

午前に deep1(位置ごとに10本そろえた50本で、π0.5 を一から学習したモデル)を8回動かした記録を読み、問題を並べました。結果は、位置1 は3回中2回が人の直しなしで成功し、位置2 は4回とも自力ではつかめていません。

#問題記録から分かったこと
1位置2 の真上まで行けない
π0.5 が止まる姿勢は j1 約68°・j2 約−8°。位置2 のお手本(j1 87.9°・j2 −33.9°)より、6関節で比べると位置1 に近い(約20° 対 約35°)
2
補正したあとも、つかむ所で失敗する
113600 は補正が足りず浅い所で閉じた。113741 は j1 を 88.8° まで寄せて閉じたが失敗
3箱の手前で補正が要る
111034・112639 で箱の手前の直し。112639 は中断
4位置3・位置4 を試していない
位置1 が3回、位置2 が4回、位置5 付近が1回
5角度の読み取りがよく失敗する
get_angles returned empty/invalid after 12 retries がほぼ毎回の試行で出る。ゲームパッドの長押しの起点が読み違いの値になる
6ゲームパッドでの補正が長い
位置2 で補正1回に 88〜92秒、送った目標は 27〜36回
7
補正中に Ctrl-C で抜けると成否が残らない
113433 が中断扱い。114106 も結果を入れていない
8立ち上げの待ち
スクリプト送りが毎回75秒、モデルの取得が133秒。進み具合が見えない

位置2 のお手本は j1 が 87.9° で、肩の上限 J1_SAFE_MAX(88°)の手前 0.1° しかありません。5か所のうち位置2 だけが、ほかの位置から46〜80°離れた姿勢です。はじめは、学習データの端にある姿勢なのでモデルが出しにくいのでは、と考えました。ところが位置2 は、前のモデルでは特に問題がありませんでした。そこで、学習データの中身ではなく、午前に何が違ったのかを調べることにしました。

位置2 のブロックは、お手本と同じ所に置かれていた

まず、ブロックの置き方を疑いました。位置2 のお手本(10/19 と 11/09)と午前の試行の、HOME のときの1枚目と閉じる直前の1枚を並べて見ます。上カメラの画像から、マットの上の mm でブロックの位置を出すスクリプト(count_by_position.py の見つけ方を使い回し)も回しました。

tilt_pos2_cmp.py(位置1・位置2 のお手本と午前の試行で、ブロックの位置を mm で出す)

import sys, json
sys.path.insert(0, '.')
from pathlib import Path
import count_by_position as cb
root = Path(sys.argv[1]); runs = root/'runs'
ds = root/'datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes'
rows=[]
for ep in sorted(p for p in ds.iterdir() if p.is_dir() and not p.name.startswith('.')):
    meta = {}
    for m in ('meta.json',):
        if (ep/m).exists(): meta=json.load(open(ep/m))
    r = cb.classify(ep.name, meta, runs)
    if r['position'] in (1,2):
        rd = cb.find_run_dir(ep.name, runs)
        b,why = cb.block_mm(rd) if rd else (None,'no dir')
        print('train', r['position'], ep.name, [round(v,1) for v in b['mm']] if b else why)
for s in ['pi05_20261110_112346','pi05_20261110_112639','pi05_20261110_112924','pi05_20261110_113433','pi05_20261110_113600','pi05_20261110_113741','pi05_20261110_114106']:
    b,why = cb.block_mm(runs/'pi05_policy'/s)
    print('today', s, [round(v,1) for v in b['mm']] if b else why, cb.nearest_position(b['mm']) if b else '')

tilt_pos2_cmp.py の出力

train 2 run_0001_20261019_191257 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261019_191541 [213.0, 239.2]
train 2 run_0001_20261019_191635 [215.8, 242.5]
train 1 run_0001_20261109_094839 [189.6, 189.0]
train 1 run_0001_20261109_095308 閉じる前のコマでブロックが見つからない
train 1 run_0001_20261109_095810 [183.7, 195.7]
train 1 run_0001_20261109_095948 [191.0, 179.8]
train 1 run_0001_20261109_100126 閉じる前のコマでブロックが見つからない
train 1 run_0001_20261109_100307 [187.4, 194.0]
train 1 run_0001_20261109_100444 閉じる前のコマでブロックが見つからない
train 1 run_0001_20261109_100716 [192.7, 190.2]
train 1 run_0001_20261109_101157 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261109_102109 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261109_102248 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261109_102431 閉じる前のコマでブロックが見つからない
train 1 run_0001_20261109_103049 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261109_103804 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261109_103943 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261109_104110 閉じる前のコマでブロックが見つからない
train 2 run_0001_20261109_104517 閉じる前のコマでブロックが見つからない
today pi05_20261110_112346 [191.8, 191.6] (1, 1, 9.5)
today pi05_20261110_112639 [190.0, 188.8] (1, 1, 7.3)
today pi05_20261110_112924 閉じる前のコマでブロックが見つからない 
today pi05_20261110_113433 閉じる前のコマでブロックが見つからない 
today pi05_20261110_113600 閉じる前のコマでブロックが見つからない 
today pi05_20261110_113741 閉じる前のコマでブロックが見つからない 
today pi05_20261110_114106 [195.9, 230.2] (2, 2, 23.0)

位置2 は、ブロックが爪の陰に入ってほとんど見つかりませんでした。見つかった 114106 は位置2 の印から23mmと出ましたが、この測り方は HOME の1枚目でブロックが半分隠れるので当てにならず、画像を直接見比べることにしました。

tilt_pos2_frames.py(HOME の1枚目と、閉じる直前の1枚を横に並べた画像を作る)

import sys, cv2, numpy as np
sys.path.insert(0,'.')
from pathlib import Path
import grasp_depth as gd, count_by_position as cb
root=Path(sys.argv[1]); out=root/'lab/Log28/analysis/pos2_frames'; out.mkdir(parents=True, exist_ok=True)
runs=root/'runs'
stems=['run_0001_20261019_191541','run_0001_20261109_102109','run_0001_20261109_104517','pi05_20261110_112346','pi05_20261110_113433','pi05_20261110_113600','pi05_20261110_113741','pi05_20261110_114106']
for s in stems:
    rd=cb.find_run_dir(s,runs); rec=rd/'recording'
    gt,gs=gd.load_gripper_cmds(rec/'states.csv'); tc=gd.close_time(gt,gs)
    _,fts=gd.load_frame_log(rec/'top_frame_log.csv')
    kc = next((i for i,t in enumerate(fts) if tc is not None and t>tc), len(fts)-1)
    cap=cv2.VideoCapture(str(rec/'top.mp4')); imgs={}
    for want,name in ((0,'home'),(max(kc-1,0),'close')):
        cap.set(cv2.CAP_PROP_POS_FRAMES,want); ok,f=cap.read()
        if ok: imgs[name]=f
    cap.release()
    row=np.hstack([imgs[k] for k in ('home','close') if k in imgs])
    h,w=row.shape[:2]; row=cv2.resize(row,(w*800//h if False else 1600, int(h*1600/w)))
    cv2.putText(row,s,(10,40),cv2.FONT_HERSHEY_SIMPLEX,1.2,(0,255,255),3)
    cv2.imwrite(str(out/f'{s}.jpg'),row)
    print(s, rd, 'close frame', kc, imgs['home'].shape)

tilt_pos2_frames.py の出力

run_0001_20261019_191541 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261019_191541 close frame 460 (1080, 1920, 3)
run_0001_20261109_102109 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261109_102109 close frame 576 (1080, 1920, 3)
run_0001_20261109_104517 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261109_104517 close frame 564 (1080, 1920, 3)
pi05_20261110_112346 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261110_112346 close frame 339 (1080, 1920, 3)
pi05_20261110_113433 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261110_113433 close frame 399 (1080, 1920, 3)
pi05_20261110_113600 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261110_113600 close frame 372 (1080, 1920, 3)
pi05_20261110_113741 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261110_113741 close frame 994 (1080, 1920, 3)
pi05_20261110_114106 /sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261110_114106 close frame 938 (1080, 1920, 3)
位置2 のお手本(10/19)。左は HOME の1枚目、右は爪を閉じる直前。ブロックはマットの左寄りの下にある
位置2 のお手本(11/09)。ブロックの位置は10/19とほぼ同じ
午前の試行(113600)。ブロックの位置はお手本と同じだが、閉じる直前の爪はブロックの手前で止まっている

ブロックの写る位置は、お手本と午前でほぼ同じでした。違ったのは画面全体で、午前の画像はアームの爪とマットの下の縁が、お手本より少し下に写っています。白い箱はほとんど動いていないのに、高い所にある爪だけが大きくずれているので、カメラが少し動いたと見ました(カメラに近い物ほど、カメラが動いたときに大きくずれて写る)。

マットの角で測ると、午前の11:20ごろから上カメラの傾きが変わっていた

画面の動きを数字で見るため、マットの四隅を取る関数(mat_corner_detection.detect_corners)で、各試行の1枚目の角の位置を出しました。上の2つの角は画面の上で切れるので(y=0)、下の2つ(右下 BR・左下 BL)を見ます。

tilt_corners.py(お手本と午前の試行の1枚目で、マットの四隅の位置を出す)

import sys, cv2
sys.path.insert(0,'.')
from pathlib import Path
import count_by_position as cb
runs=Path(sys.argv[1])/'runs'
stems=['run_0001_20261019_191541','run_0001_20261019_191635','run_0001_20261109_102109','run_0001_20261109_104517','run_0001_20261109_094839','pi05_20261110_111034','pi05_20261110_112346','pi05_20261110_113433','pi05_20261110_113600','pi05_20261110_113741','pi05_20261110_114106']
for s in stems:
    rd=cb.find_run_dir(s,runs); cap=cv2.VideoCapture(str(rd/'recording/top.mp4')); ok,f=cap.read(); cap.release()
    c=cb.detect_corners(f)
    print(s, None if c is None else ' '.join(f'({x:.0f},{y:.0f})' for x,y in c))

tilt_corners.py の出力(左上・右上・右下・左下の順。1920×1080 の画像の画素)

run_0001_20261019_191541 (321,0) (1691,0) (1718,924) (314,937)
run_0001_20261019_191635 (320,0) (1691,0) (1717,925) (315,938)
run_0001_20261109_102109 (303,0) (1679,0) (1706,923) (308,948)
run_0001_20261109_104517 (300,0) (1674,0) (1703,915) (306,944)
run_0001_20261109_094839 (318,0) (1691,0) (1695,953) (296,941)
pi05_20261110_111034 (308,0) (1683,0) (1712,919) (311,944)
pi05_20261110_112346 (323,0) (1702,0) (1705,964) (299,947)
pi05_20261110_113433 (324,0) (1701,0) (1705,964) (299,947)
pi05_20261110_113600 (324,0) (1701,0) (1704,964) (300,948)
pi05_20261110_113741 (324,0) (1701,0) (1705,964) (299,947)
pi05_20261110_114106 (324,0) (1701,0) (1704,964) (300,948)

右下の角と左下の角の高さの差を見ると、10/19 と 11/09 の位置2 のお手本、それに午前1回目の 111034(成功した回)は、右下の方が上にあります(右上がり)。112346 から最後までは、右下の方が 17px ほど下にあります(右下がり)。111034 と 112346 の間、lab_start.sh を立ち上げ直した 11:17〜11:20 のあいだに、カメラの向きが変わっています。

11/09 の位置1 のお手本(094839)も、右下の方が下(1695,953 と 296,941)でした。そこで、学習データ50本の全部で同じものを測りました。

学習データ50本のうち、右下がりに写っているのは位置1 の9本だけだった

tilt_corners_all.py(deep_even10_v1 の50本の1枚目で、下の2つの角と高さの差を出す)

import sys, cv2, json
sys.path.insert(0,'.')
from pathlib import Path
import count_by_position as cb
root=Path(sys.argv[1]); runs=root/'runs'
ds=root/'datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes'
for ep in sorted(p for p in ds.iterdir() if p.is_dir() and not p.name.startswith('.')):
    meta=json.load(open(ep/'meta.json')) if (ep/'meta.json').exists() else {}
    r=cb.classify(ep.name,meta,runs)
    f=cv2.imread(str(ep/'frames/000000.jpg')) if (ep/'frames/000000.jpg').exists() else None
    c=cb.detect_corners(f) if f is not None else None
    s=f'BR=({c[2][0]:.0f},{c[2][1]:.0f}) BL=({c[3][0]:.0f},{c[3][1]:.0f}) tilt={c[2][1]-c[3][1]:+.0f}' if c is not None else str(None if f is None else f.shape)
    print(r['position'], ep.name, s)

tilt_corners_all.py の出力(学習データの画像は 384×216。tilt は右下 − 左下の高さの差 px)

1 run_0001_20261109_094839 BR=(338,190) BL=(59,187) tilt=+3
1 run_0001_20261109_095308 BR=(338,190) BL=(59,187) tilt=+3
1 run_0001_20261109_095810 BR=(338,190) BL=(59,187) tilt=+3
1 run_0001_20261109_095948 BR=(338,190) BL=(59,187) tilt=+3
1 run_0001_20261109_100126 BR=(338,190) BL=(59,188) tilt=+2
1 run_0001_20261109_100307 BR=(338,190) BL=(59,188) tilt=+2
1 run_0001_20261109_100444 BR=(338,190) BL=(59,188) tilt=+2
1 run_0001_20261109_100716 BR=(338,190) BL=(59,188) tilt=+2
1 run_0001_20261109_101157 BR=(338,190) BL=(59,188) tilt=+2
1 run_0001_20261109_103049 BR=(338,183) BL=(58,190) tilt=-7
2 run_0001_20261019_191257 BR=(342,185) BL=(63,187) tilt=-2
2 run_0001_20261019_191541 BR=(343,184) BL=(63,187) tilt=-3
2 run_0001_20261019_191635 BR=(342,185) BL=(63,187) tilt=-2
2 run_0001_20261109_102109 BR=(341,184) BL=(61,188) tilt=-4
2 run_0001_20261109_102248 BR=(341,184) BL=(61,188) tilt=-4
2 run_0001_20261109_102431 BR=(341,184) BL=(61,188) tilt=-4
2 run_0001_20261109_103804 BR=(339,183) BL=(61,188) tilt=-5
2 run_0001_20261109_103943 BR=(339,183) BL=(61,188) tilt=-5
2 run_0001_20261109_104110 BR=(339,183) BL=(61,188) tilt=-5
2 run_0001_20261109_104517 BR=(339,183) BL=(61,188) tilt=-5
3 run_0001_20261019_192438 BR=(343,184) BL=(63,187) tilt=-3
3 run_0001_20261019_192539 BR=(343,184) BL=(63,187) tilt=-3
3 run_0001_20261109_123848 BR=(340,184) BL=(61,189) tilt=-5
3 run_0001_20261109_124032 BR=(340,184) BL=(61,189) tilt=-5
3 run_0001_20261109_124117 BR=(340,184) BL=(61,189) tilt=-5
3 run_0001_20261109_124207 BR=(340,184) BL=(61,189) tilt=-5
3 run_0001_20261109_124256 BR=(340,184) BL=(61,189) tilt=-5
3 run_0001_20261109_124438 BR=(340,184) BL=(61,189) tilt=-5
3 run_0001_20261109_124817 BR=(340,184) BL=(61,189) tilt=-5
3 run_0001_20261109_124920 BR=(340,184) BL=(61,189) tilt=-5
4 run_0001_20261019_193404 BR=(343,184) BL=(62,187) tilt=-3
4 run_0001_20261019_193459 BR=(343,184) BL=(62,186) tilt=-2
4 run_0001_20261019_193617 BR=(343,184) BL=(62,186) tilt=-2
4 run_0001_20261019_193825 BR=(342,185) BL=(62,187) tilt=-2
4 run_0001_20261019_193932 BR=(342,185) BL=(62,187) tilt=-2
4 run_0001_20261109_130542 BR=(341,181) BL=(62,186) tilt=-5
4 run_0001_20261109_130723 BR=(341,181) BL=(62,186) tilt=-5
4 run_0001_20261109_130824 BR=(341,181) BL=(62,186) tilt=-5
4 run_0001_20261109_130929 BR=(341,181) BL=(62,186) tilt=-5
4 run_0001_20261109_131103 BR=(341,181) BL=(62,186) tilt=-5
5 pi05_20261031_175527 BR=(342,184) BL=(62,184) tilt=+0
5 pi05_20261031_180435 BR=(341,185) BL=(62,185) tilt=+0
5 pi05_20261031_184016 BR=(341,185) BL=(62,185) tilt=+0
5 pi05_20261101_190347 BR=(339,186) BL=(60,185) tilt=+1
5 pi05_20261106_183926 BR=(343,182) BL=(64,190) tilt=-8
5 pi05_20261106_184650 BR=(343,182) BL=(64,190) tilt=-8
5 run_0001_20261019_194818 BR=(342,185) BL=(62,187) tilt=-2
5 run_0001_20261019_194913 BR=(342,185) BL=(62,187) tilt=-2
5 run_0001_20261019_195023 BR=(342,185) BL=(62,187) tilt=-2
5 run_0001_20261019_195120 BR=(342,185) BL=(63,188) tilt=-3

50本のうち、高さの差がプラス(右下がり)なのは、11/09 の午前に撮った位置1 の9本だけでした。 位置2〜位置5 はすべてマイナス(右上がり)か0です。11/09 は位置ごとに撮る時間を分けていて、位置1 を撮り終えた 10:12 と、位置2 を撮り始めた 10:21 の間に、カメラが少し動いています。

午前の 112346 以降の画像を学習データと同じ大きさに縮めると、右下 (341,193)・左下 (60,189) になり、位置1 の9本(338,190・59,187)とほぼ同じ写り方でした。

ここから、π0.5 がブロックの場所だけでなく、画面全体の傾きも手がかりにして位置を見分けている可能性がある、と考えました。午前の結果とは食い違いません。

  • 位置1 は3回中2回成功した
  • 位置2 では4回とも、位置2 のつかむ姿勢まで下りなかった(止まった姿勢は、6関節で比べると位置1 のつかむ姿勢の方に近いが、位置1 の姿勢そのものでもない)
  • 前のモデル(11/09 のお手本を足す前)は、位置2 で問題がなかった

この時点ではまだ推定で、カメラを戻して位置2 で試すまでは確かめられていません。

写り方の確かめ(phaseCorrelate・±20px)は平行移動しか見ていなかった

実機推論の始めと位置合わせの画面には、写り方の確かめがあります。午前もずっと OK でした。なぜ見逃したのかを camera_align.py で確かめました。

読んだ所(pi05/camera_align.py の measure_framing と framing_verdict。依頼142 より前の版):

def measure_framing(ref_bgr, cur_bgr):
    """写り方のずれを測る. 戻り値: (dx, dy, response). cv2.phaseCorrelate + ハニング窓.

    2026-10-21 実測 (基準 = teach_v1 の run_0001_20261019_183454 の 1 枚目):
      お手本 191257   dy= +9.1  / お手本 195120 dy= -6.7   ← ブロックの位置で揺れる範囲
      ドライラン 082512 dy= -7.8                            ← お手本と同じ写り方. 通す
      実機 082645       dy=+37.5                            ← 下にずれた写り方. 止める
    dx はブロックの位置で ±6 ほど動く (お手本どうしで +5.0 / +5.8) ので判定には使わず, 表示だけ.
    """
    a = cv2.cvtColor(ref_bgr, cv2.COLOR_BGR2GRAY).astype(np.float32)
    b = cv2.cvtColor(cur_bgr, cv2.COLOR_BGR2GRAY).astype(np.float32)
    if a.shape != b.shape:
        b = cv2.resize(b, (a.shape[1], a.shape[0]), interpolation=cv2.INTER_AREA)
    win = cv2.createHanningWindow((a.shape[1], a.shape[0]), cv2.CV_32F)
    (dx, dy), response = cv2.phaseCorrelate(a, b, win)
    return float(dx), float(dy), float(response)


def framing_verdict(dx, dy, response):
    """写り方の判定. 戻り値: (verdict, 理由). verdict は "OK" / "ずれ" / "測れない".

    相関が弱い (response が低い) ときは dx/dy 自体が当てにならないので,
    「ずれ」ではなく「測れない」として扱う (既定ではどちらも止める).
    """
    if not all(np.isfinite(v) for v in (dx, dy, response)):
        return "測れない", "dx / dy / response に非有限値"
    if response < FRAMING_MIN_RESPONSE:
        return "測れない", (f"相関が弱く dx/dy が当てにならない "
                            f"(response={response:.3f} < {FRAMING_MIN_RESPONSE:.2f})")
    bad = []
    if abs(dy) > FRAMING_MAX_DY:
        bad.append(f"|dy|={abs(dy):.1f} > {FRAMING_MAX_DY:.0f}")
    if abs(dx) > FRAMING_MAX_DX:
        bad.append(f"|dx|={abs(dx):.1f} > {FRAMING_MAX_DX:.0f}")
    if bad:
        return "ずれ", " / ".join(bad)
    return "OK", (f"|dy|<={FRAMING_MAX_DY:.0f} / |dx|<={FRAMING_MAX_DX:.0f} / "
                  f"response>={FRAMING_MIN_RESPONSE:.2f}")

phaseCorrelate(OpenCV の関数。2枚の画像を重ねて、上下左右に何 px ずらすと一番よく重なるかを出す)は、平行移動だけを測ります。±20px の線は、10/21 にカメラが下に37px ずれた失敗を止めるために決めた値で、わずかな回転は最初から対象に入っていません。

回すと画面の中心はほとんど動かず端だけが動くので、平行移動として測るとほぼ 0px になります。午前も dy +2.0・dx +4.2 で、OK の範囲に十分収まっていました。重ねた画像(透かし)を目で見比べても、1.7°の回転はまず気づけない大きさです。人の目やこの確かめでは誤差に見える違いが、学習データの中では位置を見分ける手がかりになっていた可能性があります。

依頼142:上カメラの傾きを数字で出す camera_tilt.py を作った

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(7本)

01_sec07.txt ― Claude Code の報告(claude_code_reports/20261110_142.txt)・4,629字

02_sec07.txt ― commit 0531717 の差分(camera_tilt.py・test_camera_tilt.py は全文。camera_align.py・run_policy_client.py・lab_start.sh・test_lab_start.sh は変えた所)・27,985字

03_sec16.txt ― lab_start_20261110_190916.txt(新しい Pod を借り、1回目 191618 で p を押して落ち、2回目 191905 を Ctrl-C で止めた)・48,069字

04_sec16.txt ― lab_start_20261110_191918.txt(同じ Pod につなぎ直し、192201 で推論4 のあと p を押して固まった)・19,460字

05_sec16.txt ― lab_start_20261110_192719.txt(daemon が ping に答えず、HOME に戻せずに止まった)・3,859字

06_sec16.txt ― lab_start_20261110_193100.txt(193431 で位置2 のつかむ姿勢へ向かい h で戻した、194015 で p を押して固まった)・27,176字

07_sec16.log.txt ― serve_20261110_194719.log(Pod の推論サーバーの記録)・3,188字

傾きを数字で見ながら合わせられるように、Claude Code に依頼しました。カメラを読むだけで、実機は動かしません。

依頼文(依頼箱 20261110_142)

---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 60
---
# 依頼142:上カメラの傾き(回転)を数字で出し、位置合わせの画面に足す

実機は動かさない(カメラを読むだけ)。エイイチは見ていない。Log28。

## なぜ
2026-11-10 の deep1 の実機推論(Log28)で、位置2が4回ともつかめず、AI は位置1寄りの姿勢へ行った。
上カメラのマットの下の辺の傾きを測ると、

- 学習データ deep_even10_v1 の位置1(11/09 午前の9本)だけが 右下がり 約 +0.5°
- 位置2〜5 と 11/10 の1回目(pi05_20261110_111034・成功)は 右上がり 約 −0.5〜−1.6°
- 11/10 の 112346 以降は 右下がり 約 +0.7°(iPhone を付け直したあと)

だった。camera_align.py の写り方の確かめ(phaseCorrelate の dx/dy・±20px)は平行移動しか見ないので、1.7° の回転は OK のまま通った。
カメラの傾きを数字で見ながら合わせられるようにしたい。

測り方の下書き(Claude が KIOXIA の lab/Log28/analysis で試したもの。/tmp/corners_all.py 相当):
pi05/count_by_position.py の detect_corners() でマットの四隅を取り、上の2つは画面の上で切れる(y=0)ので使わず、
下の2つ(左下 BL・右下 BR)を結ぶ線の角度を傾きとした。1920×1080 の生の画像でも、学習データの小さい画像でも四隅は取れた。
傾きの向き:度で、プラス=右下がり(画像の y は下向きなので atan2(BR.y − BL.y, BR.x − BL.x))。

## やること
1. 傾きを測る関数を1つ作る(置き場所は pi05/ の中で、camera_align.py から使える所。detect_corners を使い回す)。四隅が取れないときは「測れない」を返す
2. 学習データの傾きを一覧にする小さな道具 pi05/camera_tilt.py:
   - `--dataset <名前>`:各エピソードの1枚目の傾きを、位置(count_by_position.classify)ごとに 中央値・最小〜最大 で表にする。全体の中央値も出す
   - `--live`:上カメラ(camera_resolver)から1枚取り、今の傾きと、--dataset の全体の中央値との差を出す
   - 何も書き出さない(datasets/ には書かない)
3. camera_align.py の位置合わせの画面(透かし)と [ALIGN ] の行に、今の傾き・お手本の中央値・差 を出す。お手本は今の基準画像と同じく lab_start.sh から渡る raw_episodes(deep1 なら deep_even10_v1 を見るのがよい。どちらの raw_episodes を見るか迷ったら、安全範囲の --raw-dir の方を使い、報告に書く)
   - 差が ±0.5° を超えたら、画面の数字を目立つ色にして「傾きがお手本と違う」と出す。止めはしない(エイイチは止める条件が増えるのを嫌う)
4. run_policy_client.py の [CAMERA] の行にも傾きと差を1行足す(表示だけ。判定・停止は変えない)
5. 2 を deep_even10_v1 で回し、表を報告に貼る(上の数字と合うか)。Log28 の 11/10 の試行(pi05_20261110_111034・112346・113600・114106)の top.mp4 の1コマ目の傾きも出して報告に貼る
6. テストを足して通す(傾きの向きと値:四隅を回した合成画像で ±1° が正しく出る・測れないとき落ちない)。既存のテストも通す
7. codex で差分を見てもらう。指摘を直して通るまで
8. 変えたファイルだけを指定して git add・commit(git add . は使わない)

## やらないこと
- 写り方の OK/ずれ の判定の線(±20px・response)は変えない
- lab_train.sh・学習の設定・datasets/ は触らない

## 報告
claude_code_reports/20261110_142.txt に:変えた所、5 の表、テストの結果、codex の回数と指摘、commit の番号、`python3 pi05/camera_tilt.py --live --dataset mecharm_redblock_box_deep_even10_v1` の打ち方(エイイチが試す前に使う)、決めてほしいこと。
**報告の最後の1行は必ず次のどれか**:「判定:合格」/「判定:止めた(決めてほしいことあり)」/「判定:失敗」

Claude Code の報告(claude_code_reports/20261110_142.txt)

01_sec07.txt4,629字・77行・text
依頼 142:上カメラの傾き(回転)を数字で出し、位置合わせの画面に足す(Log28・実機は動かしていない)

■ 1. 変えた所
- pi05/camera_tilt.py(新規)
  - tilt_deg(frame):count_by_position.detect_corners でマットの四隅を取り、下の 2 つ(左下 BL・右下 BR)を結ぶ線の角度を返す。
    度で、プラス=右下がり(atan2(BR.y − BL.y, BR.x − BL.x))。四隅が取れない・見つけ方が落ちる・左右が逆のときは None(=測れない)。
  - reference_tilt(raw_dir):各エピソードの 1 枚目の傾きの中央値(測れた本数・全本数も返す)。50 本で 0.1 秒。
  - tilt_text(今, お手本):端末用の 1 行。差が ±0.5°(TILT_WARN_DEG)を超えたら「傾きがお手本と違う」。
別のタブで開く保存

報告の表は、私が先に測った値と合いました。位置1 の11/09午前の9本は +0.41〜+0.62°、残りの1本(103049)だけが −1.43° です。午前の試行の1コマ目は、成功した 111034 が −1.02°、112346・113600・114106 は +0.65〜+0.69° でした。

  • ねらい:1.7°の回転のような、目では気づけない写り方の違いを、合わせる段階で数字で見えるようにする
  • 変わったこと:位置合わせの画面と [ALIGN ] の行に、今の傾き・お手本の中央値(−0.61°)・差が出る。差が ±0.5° を超えると色が変わり TILT DIFFERS と出る(止めない)。実機推論の始めの [CAMERA] にも1行出る
  • 見込める効果:位置2 のような、カメラの向きで行き先が変わる失敗を、動かす前に気づける。合成画像の試験では +1° を +0.98° と測れた(報告の値)。実機で何回の失敗を防げるかは、これから数える

commit 0531717 の差分(camera_tilt.py・test_camera_tilt.py は全文。camera_align.py・run_policy_client.py・lab_start.sh・test_lab_start.sh は変えた所)

02_sec07.txt27,985字・576行・diff
commit 05317174b9bd7eec825671d72ebf99b6f90e5231
Author: Eiichi Shimazu <studio@adaptdesign.jp>
Date:   Tue Nov 10 17:40:03 2026 +0900

    上カメラの傾き (マットの下の辺 左下→右下 の角度. プラス=右下がり) を数字で出す pi05/camera_tilt.py を足す (tilt_deg は count_by_position.detect_corners の下の 2 つの角を使い, 取れなければ None. --dataset で各エピソードの 1 枚目を位置ごとに 中央値・最小〜最大 の表に, --live で上カメラ 1 枚の傾きとお手本の全体の中央値との差, --video で動画の 1 コマ目. 何も書き出さない). camera_align.py の画面と [ALIGN ] の行に 今の傾き・お手本の中央値・差 を出し, 差が ±0.5° を超えたら色を変えて TILT DIFFERS と出す (止めない. お手本は --tilt-raw-dir, 未指定なら --raw-dir). lab_start.sh は --tilt-raw-dir に安全範囲と同じ raw_episodes (deep1 なら deep_even10_v1) を渡す. run_policy_client.py の [CAMERA] に傾きと差を 1 行 (表示だけ・判定と停止は変えない). テストを足す (test_camera_tilt 8・lab_start 178) (依頼 142. codex 2 回)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
別のタブで開く保存

Claude Code からは決めてほしいことが3つ来ていて、次のように答えました(依頼144 の頭に書いて渡した)。

  1. 傾きのお手本は、安全範囲と同じデータ(deep_even10_v1 の −0.61°)のままでよい。今回問題になったのは deep_even10_v1 の中の偏りなので、そことそろえる
  2. 合わせる先は全体の中央値。位置ごとの中央値に合わせると、カメラの向きで位置が分かる状態が続く
  3. ±0.5°の線と、表示が 0.2°きざみで揺れることは、止めないのでこのままでよい

スマホを手で回して傾きを合わせるのは難しかった

道具ができたので、スマホを回して合わせました。最初の打ち方は、r2r2r-scripts のフォルダに入らずに打ったので、ファイルが見つかりませんでした。

端末:camera_tilt.py —live で今の傾きを測る

Last login: Tue Nov 10 14:42:07 on ttys005
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % python3 pi05/camera_tilt.py --live --dataset mecharm_redblock_box_deep_even10_v1
python3: can't open file '/Users/shimazueiichi/pi05/camera_tilt.py': [Errno 2] No such file or directory
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % cd ~/r2r2r-scripts && python3 pi05/camera_tilt.py --live --dataset mecharm_redblock_box_deep_even10_v1
[REF ] /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes  全体の中央値 -0.61°  (測れた 50/50 本)
[LIVE] 上カメラ (1920×1080) 傾き +1.69° (プラス=右下がり) / お手本の中央値 -0.61° / 差 +2.30° → 傾きがお手本と違う (±0.5° を超える)
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % ccd ~/r2r2r-scripts && python3 pi05/camera_tilt.py --live --dataset mecharm_redblock_box_deep_even10_v1
zsh: command not found: ccd
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % cd ~/r2r2r-scripts && python3 pi05/camera_tilt.py --live --dataset mecharm_redblock_box_deep_even10_v1
[REF ] /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes  全体の中央値 -0.61°  (測れた 50/50 本)
[LIVE] 上カメラ (1920×1080) 傾き +1.77° (プラス=右下がり) / お手本の中央値 -0.61° / 差 +2.38° → 傾きがお手本と違う (±0.5° を超える)
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % cd ~/r2r2r-scripts && python3 pi05/camera_tilt.py --live --dataset mecharm_redblock_box_deep_even10_v1
[REF ] /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes  全体の中央値 -0.61°  (測れた 50/50 本)
[LIVE] 上カメラ (1920×1080) 傾き +1.65° (プラス=右下がり) / お手本の中央値 -0.61° / 差 +2.26° → 傾きがお手本と違う (±0.5° を超える)
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts %

3回とも +1.65〜+1.77° で、午前(+0.7°)からさらに回っていました。1回ずつ打つのは手間なので、位置合わせの画面(透かし)を単独で開き、傾きを見ながらスマホを回しました。画面は、基準の画像(お手本の1枚目)と今のカメラを重ねて表示し、上に平行移動のずれと傾きを出します。

位置合わせの画面。上下に約31、左右に約17ずれて NO MATCH、傾きは +1.24°。マット・白い箱・爪が2重に写っている
平行移動は OK になったが、傾きは −1.23°で、逆向きに回しすぎた
重なりはほぼぴったり(dy −1.0・dx +1.3・相関 0.64)なのに、傾きは +0.82°

3枚目の画面は、重なりがほぼぴったりなのに傾きが合っていません。原因は2つありました。

1つ目は、重ねている基準の画像と、傾きの比べる相手が違うことです。重ねる画像(trim2 の 183454・10/19 の位置1 のお手本)は、それ自体が +0.2〜+0.4°傾いています。傾きの数字は学習データ50本の中央値 −0.61° と比べるので、重なりをぴったり合わせるほど傾きの数字は合わなくなります。測る画像の大きさで値が変わっていないかも、同時に確かめました。

tilt_res_cmp.py(同じ1枚を、学習データの大きさ 384×216 と 1920×1080 で測り比べる)

import sys, cv2
sys.path.insert(0,'pi05'); sys.path.insert(0,'.')
from pathlib import Path
import camera_tilt as ct, count_by_position as cb
root=Path(sys.argv[1])
for ds,stem in [('mecharm_redblock_box_teach_v1_trim2','run_0001_20261019_183454'),('mecharm_redblock_box_deep_even10_v1','run_0001_20261019_191541'),('mecharm_redblock_box_deep_even10_v1','run_0001_20261109_094839'),('mecharm_redblock_box_deep_even10_v1','run_0001_20261109_102109')]:
    small=cv2.imread(str(root/'datasets'/ds/'raw_episodes'/stem/'frames/000000.jpg'))
    rd=cb.find_run_dir(stem, root/'runs'); cap=cv2.VideoCapture(str(rd/'recording/top.mp4')); ok,big=cap.read(); cap.release()
    f=[n for n in dir(ct) if 'tilt' in n.lower()]
    print(stem, small.shape, big.shape, f)
    print('   small', round(ct.tilt_deg(small),2) if ct.tilt_deg(small) is not None else None, ' big', ct.tilt_deg(big), ' big->small', ct.tilt_deg(cv2.resize(big,(384,216),interpolation=cv2.INTER_AREA)))

tilt_res_cmp.py の出力

run_0001_20261019_183454 (216, 384, 3) (1080, 1920, 3) ['TILT_WARN_DEG', 'episode_tilts', 'live_tilt', 'reference_tilt', 'tilt_deg', 'tilt_text']
   small 0.2  big 0.36675318121020006  big->small 0.2046269139658639
run_0001_20261019_191541 (216, 384, 3) (1080, 1920, 3) ['TILT_WARN_DEG', 'episode_tilts', 'live_tilt', 'reference_tilt', 'tilt_deg', 'tilt_text']
   small -0.61  big -0.5305013166738104  big->small -0.613859863109328
run_0001_20261109_094839 (216, 384, 3) (1080, 1920, 3) ['TILT_WARN_DEG', 'episode_tilts', 'live_tilt', 'reference_tilt', 'tilt_deg', 'tilt_text']
   small 0.62  big 0.4914456703916715  big->small 0.6160599083992302
run_0001_20261109_102109 (216, 384, 3) (1080, 1920, 3) ['TILT_WARN_DEG', 'episode_tilts', 'live_tilt', 'reference_tilt', 'tilt_deg', 'tilt_text']
   small -0.82  big -1.0244934394563543  big->small -0.8184554616886145

大きさによる違いは 0.2°ほどで、183454 は小さい画像で +0.20°、大きい画像で +0.37° でした。

2つ目は、±0.5°が手で回すには細かすぎることです。0.5°はスマホの端がほんの1mmほど動く差で、数字も1回ごとに約0.2°揺れます。

位置2 のお手本は −1.03〜−0.41°の範囲で撮られているので、この試しには −0.4〜−1.6°のどこかに入れば足りる、と線をゆるめて合わせ直しました。

傾き −1.02°・差 −0.41°で、すべて OK。薄く写る左のブロックが今の位置2、爪の右下に隠れているのが基準の画像(位置5 のお手本)のブロック

重ねている基準の画像は位置5 のお手本(194818)なので、その赤いブロックは爪の右下に隠れていて、位置2 に置いたブロックは重なりません。ここではカメラを合わせるためだけに使っています。

依頼144:上カメラの傾きをソフトでそろえてから π0.5 に渡す

手で ±0.5°に入れるのは毎回の手間になるので、カメラの画像を π0.5 に渡す前にソフトの側で回し、学習データの傾きにそろえる形を依頼しました。スマホはだいたいの位置に置くだけで済む見込みです。この回の時点では依頼箱で順番待ちで、まだ動いていません。

依頼文(依頼箱 20261110_144)

---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 90
---
# 依頼144:上カメラの傾きをソフトで直してから AI に渡す

実機は動かさない(実装・テスト・レビューまで)。エイイチは見ていない。Log28。

## 依頼142 の「決めてほしいこと」への答え(エイイチ 2026-11-10)
1. 傾きのお手本は安全範囲と同じデータ(deep 系は deep_even10_v1 の −0.61°)のままでよい
2. 合わせる先は全体の中央値。位置ごとの中央値は使わない
3. ±0.5° の線と表示のちらつきはこのままでよい

## なぜ
依頼142 の道具で iPhone を手で回して合わせてみたが、±0.5° に入れるのが難しい(0.5° は iPhone の端で約1mm、測るたびに約0.2°揺れる)。
さらに、位置合わせで重ねる基準画像 trim2 の run_0001_20261019_183454 は、それ自体の傾きが +0.2〜+0.4° あり、
重なりをぴったり合わせると傾きの数字が合わない(重なり OK・傾き +0.82° になった)。
iPhone はだいたいの位置に置くだけにして、傾きはソフトでそろえたい。

## やること
1. run_policy_client.py:実機推論の始め(HOME にいる間)に上カメラの傾きを数コマ測り(中央値)、
   お手本の中央値(--tilt-raw-dir。deep 系は deep_even10_v1)との差だけ画像を回してから policy.infer の observation/image に渡す。
   - 回すのは画像の中心まわり(cv2.warpAffine。縁は BORDER_REPLICATE などで黒い三角が出ないように。どれにしたかを報告に)
   - 角度は始めに1回決めて、その試行の間は同じ角度を使う(腕でマットの角が隠れるので、動いている間は測り直さない)
   - 測れなかったときは回さずに続け、そう1行出す(止めない)
   - 差が大きくても止めない(3° を超えたら目立つ表示だけ)
   - [CAMERA] の行に「傾き +0.82° → −0.61° にそろえて渡す(−1.43° 回す)」のように出す
   - log.jsonl の頭に、測った傾き・お手本の値・回した角度を残す
   - `--no-tilt-fix` で今までどおり回さない形にできるようにする。lab_start.sh は回す方を既定にする
2. 写り方の確かめ(phaseCorrelate・±20px)は、回したあとの画像で測る(判定の線は変えない)。どちらで測ったかを [CAMERA] に出す
3. camera_align.py の位置合わせの画面:ライブの画像を 1 と同じ角度で回して重ねる(AI に渡る画像と同じ見え方にする)。
   傾きの行に「ソフトでそろえる」と出し、TILT DIFFERS は 3° を超えたときだけにする
4. 記録(recording/top.mp4 など)と学習データづくり(build_correction_episodes.py など):
   - 録画は今までどおり回していない元の画像のまま残す。回した角度は記録に残す(1 の log.jsonl)
   - 補正入りの試行から学習データを作るとき、どの画像をデータに入れるべきか(元の画像か、回した画像か)を調べ、案と理由を報告に書く。今回は build_* は変えない
5. テスト:合成画像でマットを ±1°・±2° 回したものを直すと、直したあとの傾きがお手本の値から 0.2° 以内になること。測れないときに落ちないこと。--no-tilt-fix で今までどおりになること。既存のテスト(test_lab_start.sh ほか pi05/ のもの)も通す
6. 実データでの確かめ:Log28 の pi05_20261110_112346・113600 の top.mp4 の1コマ目を 1 と同じ処理で回し、回したあとの傾きと、
   trim2 の 183454 の1枚目との phaseCorrelate(dx・dy・response)を、回す前と後で並べて報告に貼る。回す前と後の画像を
   KIOXIA の lab/Log28/analysis/tilt_fix/ に jpg で置く(ここだけに書く)
7. codex で差分を見てもらう。指摘を直して通るまで
8. 変えたファイルだけを指定して git add・commit(git add . は使わない)

## やらないこと
- 関節・安全範囲・送る速さ・爪の扱いは変えない
- lab_train.sh・学習の設定・datasets/ は触らない
- 平行移動(dx・dy)はソフトで直さない(必要そうなら報告の「決めてほしいこと」へ)

## 報告
claude_code_reports/20261110_144.txt に:変えた所、5・6 の結果、4 の案、codex の回数と指摘、commit の番号、エイイチが次に実機で試すときの打ち方(lab_start.sh の打ち方が変わらないならそう書く)、決めてほしいこと。
**報告の最後の1行は必ず次のどれか**:「判定:合格」/「判定:止めた(決めてほしいことあり)」/「判定:失敗」
  • ねらい:スマホを付け直すたびに変わるカメラの向きを、学習データの向きに自動でそろえる
  • 変わること:実機推論の始め(HOME の間)に傾きを測り、学習データの中央値との差だけ画像を回してから π0.5 に渡す。録画は元の画像のまま残し、回した角度を記録する
  • 見込める効果:位置合わせで傾きを手で合わせる時間がなくなる見込み。ただし、回して渡した画像で π0.5 が位置2 まで届くかは、まだ確かめていない

傾きを直すと、π0.5 は位置2 のつかむ姿勢まで向かった

カメラを合わせ直し(傾き −0.82〜−1.03°)、新しい Pod を借りて、位置2 にブロックを置いて試しました。夜の試行は4回で、どれも途中で止めています。

試行始めの傾きどこまで進めたか終わり方
191618−1.03°
推論5(j1 が上限 88° で切られる所)のあと p
ゲームパッドの補正で推論のプロセスが落ちた(Segmentation fault)
191905−1.03°推論の前Ctrl-C で止めた
192201−0.82°
推論4(j1 88°・j2 約−31°)のあと p
ゲームパッドの補正で固まった
193431−0.82°
推論3 を送ったあと、推論4(閉じる指令)の前に h
爪がブロックの奥側にずれていたので HOME へ戻した
194015
+4.59°(測り違いと見ている)
推論3 のあと pゲームパッドの補正で固まった

π0.5 がどこへ行こうとしたかを、午前と夜で比べました。log.jsonl に残る、π0.5 が出した10ステップ先までの行き先を、最初に人が操作する手前までつなぎ、肩 j1 の最大とひじ j2 の最小を出しています(最後の1チャンクは送らなかった分も含む)。

extract_LOG486.py(位置2 の試行で π0.5 が出した行き先を、午前と夜で書き出す)

"""LOG486 のサムネイル用: 2026-11-10 の位置2 の試行で、π0.5 が出した行き先(log.jsonl の actions を、最初の人の操作の手前まで順につなぐ。
最後の1チャンクは送らなかったものも含める)。before = 午前(カメラが右下がり約 +0.7°)、after = 夜(右上がり約 -0.8〜-1.0°)。点は [j0, j1, j2, 閉じる指令か, 'm']"""
import json, sys
R = lambda v: round(float(v), 1)
def aims(s):
    pts = []
    for l in open(f'runs/pi05_policy/pi05_20261110_{s}/log.jsonl'):
        x = json.loads(l)
        if 'actions' in x:
            pts += [[R(a[0]), R(a[1]), R(a[2]), 1 if a[6] > 0.5 else 0, 'm'] for a in x['actions']]
        elif x.get('type') not in ('model', 'framing'):
            break
    return pts
o = {'LOG486': {'before': [aims(s) for s in ('113433', '113600', '113741', '114106')],
                'after': [aims(s) for s in ('191618', '192201', '193431', '194015')]}}
json.dump(o, open(sys.argv[1], 'w'), separators=(',', ':'))
for k in ('before', 'after'):
    for r in o['LOG486'][k]:
        print(k, len(r), 'j1 max', max(p[1] for p in r), 'j2 min', min(p[2] for p in r))

extract_LOG486.py の出力(上の4行が午前の 113433・113600・113741・114106、下の4行が夜の 191618・192201・193431・194015)

before 50 j1 max 79.7 j2 min -18.5
before 40 j1 max 72.4 j2 min -11.4
before 40 j1 max 70.6 j2 min -9.9
before 40 j1 max 73.3 j2 min -13.7
after 60 j1 max 93.7 j2 min -34.8
after 50 j1 max 88.4 j2 min -30.4
after 50 j1 max 88.5 j2 min -33.6
after 40 j1 max 76.2 j2 min -17.4

午前の4回は j1 が 70〜80°、j2 が −10〜−18°までしか行こうとしていません。夜は、途中で止めた 194015 を除く3回が、j1 88〜94°、j2 −30〜−35°まで向かっています。位置2 のお手本のつかむ姿勢は j1 87.9°・j2 −33.9°です。同じ位置2 のブロックで、変えたつもりだったのはカメラの傾きです。ただ、付け直したので、上下左右の位置も数px変わっています(実機推論の始めの写り方の確かめの値は、午前の位置1・位置2 の7回が dy +1.8〜+2.7・dx +3.7〜+4.9、夜の5回が dy −2.0〜−2.7・dx +6.2〜+7.2)。

上カメラの傾きを学習データにそろえ直すと、π0.5 は位置2 のつかむ姿勢へ向かう答えを出すようになりました。 ただし、これで傾きが原因だと確定したわけではありません。何が確かめられていないかは、下の「傾きだけが原因とは、まだ言い切れない」の節にまとめました。

191618 では、π0.5 が j1 を 93.7°まで出していて、肩の上限 88°で切られています。

[005] 推論 871ms  10ステップ先  Δ= +1.5  +9.7 -16.7  -0.4  +8.2  -1.6
       [LIMIT] j1 安全範囲でクランプ +93.7→+88.0
       [VIA   ] 中継点 4 点: step 2, 5, 8, 9
       [LIMIT] step 5: j1 安全範囲でクランプ +88.2→+88.0
       [LIMIT] step 8: j1 安全範囲でクランプ +92.8→+88.0
       [LIMIT] step 9: j1 安全範囲でクランプ +93.7→+88.0

位置2 のお手本の j1 は 87.9°で、上限の 88°とほぼ同じです。π0.5 の答えが少し上へぶれると、上限で切られて届かなくなる位置でもあります。

傾きだけが原因とは、まだ言い切れない

午前と夜の違いは、カメラの傾きのほかにもあり得ます。照明は画像の明るさと色味で比べ、ほぼ同じでした。

tilt_brightness.py(学習データ・午前・夜の1枚目で、明るさと青と赤の比を比べる)

# 学習データ・午前・夜の1枚目で、画像全体とマットの明るさ、青と赤の比を比べる(照明の違いを疑ったため。2026-11-10 夜)
# cd /Volumes/KIOXIA/r2r2r-data && python3 <このファイル>
import cv2, numpy as np, glob
def first(d):
    fr=sorted(glob.glob(d+'/frames/*.jpg'))
    if fr: return cv2.imread(fr[0])
    cap=cv2.VideoCapture(d+'/recording/top.mp4'); ok,f=cap.read(); return f
for d in ['runs/run_pick_place/run_0001_20261019_191541','runs/run_pick_place/run_0001_20261109_094839','runs/run_pick_place/run_0001_20261109_102109',
          'runs/pi05_policy/pi05_20261110_111034','runs/pi05_policy/pi05_20261110_113600','runs/pi05_policy/pi05_20261110_191618','runs/pi05_policy/pi05_20261110_193431']:
    f=first(d); 
    if f is None: print(d,'none'); continue
    f=cv2.resize(f,(384,216)); g=cv2.cvtColor(f,cv2.COLOR_BGR2GRAY); hsv=cv2.cvtColor(f,cv2.COLOR_BGR2HSV)
    mat=g[60:200,80:300]
    print(d.split('/')[-1], 'mean',round(g.mean(),1),'mat',round(mat.mean(),1),'B/R',round(f[...,0].mean()/f[...,2].mean(),3))

tilt_brightness.py の出力(mean は画像全体、mat はマットのあたりの明るさ。B/R は青と赤の比)

run_0001_20261019_191541 mean 130.0 mat 102.3 B/R 0.965
run_0001_20261109_094839 mean 129.2 mat 101.8 B/R 0.975
run_0001_20261109_102109 mean 129.4 mat 102.4 B/R 0.962
pi05_20261110_111034 mean 130.2 mat 105.6 B/R 0.968
pi05_20261110_113600 mean 128.4 mat 102.7 B/R 0.968
pi05_20261110_191618 mean 129.6 mat 103.4 B/R 0.96
pi05_20261110_193431 mean 129.4 mat 103.7 B/R 0.96

明るさは 128〜130、マットのあたりは 102〜106、青と赤の比は 0.96〜0.98 で、学習データ・午前・夜でほぼ同じでした。照明が違ったから、という説明は弱いと見ています。

まだ消せていない疑いは次の4つです。

疑い今わかっていること
変えたのは傾きだけではない
付け直したので、上下左右の位置も数px変わった。傾きと位置のどちらが効いたかは分けられていない
回数が少ない
比べたのは午前4回と夜3回。π0.5 は同じ画像でも毎回少しずつ違う答えを出すので、偶然の範囲かもしれない
逆の向きを試していない
夜に、わざと右下がりへ戻して、また届かなくなるかは確かめていない
「位置1 だと思った」とまでは言えない
午前に止まった姿勢は、位置1 のつかむ姿勢(ひじ j2 約 0°)そのものではない。言えるのは「位置2 まで下りなかった」まで

決着は、実機を動かさずに、録画済みの画像だけでつけられます。

  1. 午前に届かなかった 113600 の1枚目を、ソフトで約1.5°右上がりに回して π0.5 に渡し、行き先が位置2 へ変わるかを見る
  2. 逆に、夜の 193431 の1枚目を右下がりに回して、位置2 へ行かなくなるかを見る
  3. 関節の角度は元のまま、回す角度だけを変えて、それぞれ10回ずつ答えを出させ、ばらつきも見る

画像の回転以外は何も変えないので、傾きが原因なら行き先がはっきり入れ替わり、変わらなければ別の原因を探すことになります。

最後の数mmで、爪はブロックの奥側にずれた

193431 は、推論2 のあとの姿勢を、位置2 のお手本が通る途中の姿勢と比べました。

demo_traj.py(位置2 のお手本2本の関節角を、閉じる前まで8コマおきに出す)

# 位置2 のお手本2本(deep_even10_v1)の関節角を、閉じる前まで8コマおきに出す(2026-11-10 夜に Mac 連携の shell で実行)
# cd /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes; python3 -c "<下>"
import numpy as np
for ep in ['run_0001_20261109_102109','run_0001_20261019_191541']:
    s=np.load(ep+'/episode.npz')['state']
    ch=np.flatnonzero(s[1:,6]>s[:-1,6])+1
    print(ep,'close idx',ch[:1],len(s))
    for i in range(0,(ch[0] if len(ch) else len(s))+1,8): print(i, np.round(s[i,:7],1))

demo_traj.py の出力(j0〜j5 と爪。最後の列が 0 の間は開いている)

run_0001_20261109_102109 close idx [47] 141
0 [ -0.4   1.6   1.9   0.1  -0.3 -30.2   0. ]
8 [ -8.   16.2  -2.2   1.7 -12.6 -31.6   0. ]
16 [-20.9  39.   -8.6   5.  -32.1 -32.9   0. ]
24 [-32.9  62.6 -13.9   6.2 -52.3 -33.    0. ]
32 [-35.8  68.  -14.1   6.9 -57.  -33.1   0. ]
40 [-35.2  81.  -26.9   6.8 -50.3 -34.    0. ]
run_0001_20261019_191541 close idx [45] 139
0 [ -0.4   1.5   0.8  -0.2  -0.3 -30.2   0. ]
8 [ -8.   16.4  -2.6   1.5 -12.9 -31.4   0. ]
16 [-21.1  39.2  -8.1   4.7 -32.6 -32.8   0. ]
24 [-32.9  62.9 -13.7   6.5 -52.7 -32.7   0. ]
32 [-35.6  70.  -16.3   6.6 -56.1 -33.1   0. ]
40 [-35.7  85.1 -31.2   6.4 -47.7 -34.3   0. ]
j0j1j2j4
193431 の推論2 のあと−37.4+67.4−12.7−58.2
お手本 102109(つかむ15コマ前)
−35.8+68.0−14.1−57.0
お手本 191541(同じ所)−35.6+70.0−16.3−56.1
お手本でつかむ直前−35.2〜−35.7+81〜+85−27〜−31−48〜−50

位置2 のお手本は、いったんこのブロックの上の姿勢を通り、そこから j1 を足し j2 を引きながら手前へ下りてつかみます。193431 はこの姿勢で、j0 が 1.6〜1.8°多い(横に約6mm)ほかは、お手本とほぼ同じ所にいました。

推論3 を送ったあと、π0.5 は推論4 で j1 を 88°(上限で切った値)、j2 を約 −34°まで下げて爪を閉じようとしました。上から見ると爪はブロックの奥側にずれていて、このまま下りると爪の先がブロックに当たると判断して、h で HOME へ戻しました。

ブロックの置き場所がお手本とずれていないかも、画像の赤の位置で測りました。

tilt_red.py(HOME の1枚目で、ブロックの赤の中心を画素で出す)

import sys,cv2,numpy as np
from pathlib import Path
def first(p):
    p=Path(p)
    fr=sorted((p/'frames').glob('*.jpg')) if (p/'frames').is_dir() else []
    if fr: return cv2.imread(str(fr[0]))
    cap=cv2.VideoCapture(str(p/'recording/top.mp4')); ok,f=cap.read(); cap.release(); return f if ok else None
for p in sys.argv[1:]:
    f=first(p)
    if f is None: print(p,'none'); continue
    f=cv2.resize(f,(1920,1080))
    hsv=cv2.cvtColor(f,cv2.COLOR_BGR2HSV)
    m=((hsv[...,0]<10)|(hsv[...,0]>170))&(hsv[...,1]>90)&(hsv[...,2]>70)
    m[:, :300]=0; m[:, 900:]=0; m[:400]=0; m[800:]=0
    ys,xs=np.nonzero(m)
    print(Path(p).name, (int(xs.mean()),int(ys.mean()),len(xs)) if len(xs) else 'no red')

tilt_red.py の出力

pi05_policy/pi05_20261110_193431/:
frames
log.jsonl
recording

pi05_policy/pi05_20261110_193431/frames:
0000.jpg
0001.jpg
0002.jpg
0003.jpg
run_0001_20261019_191541 (552, 673, 4002)
run_0001_20261019_191635 (546, 672, 4266)
run_0001_20261109_102109 (598, 649, 1762)
run_0001_20261109_104517 (611, 640, 1481)
pi05_policy/pi05_20261110_113600 none
pi05_policy/pi05_20261110_114106 none
pi05_policy/pi05_20261110_192201 none
pi05_policy/pi05_20261110_193431 none

夜の試行の画像では、赤を見つける範囲に入らず、測れませんでした(範囲と色の線をお手本の画像に合わせて決めたため)。置いた本人の目では、ブロックは位置2 の印に合っていて、ずれていませんでした。爪が奥側にずれた理由は、まだ分かっていません。π0.5 の答えの j0 が 1.6〜1.8°多いことと、j1 が上限で切られて手前へ届ききらないことの、どちらか(または両方)を疑っています。

ここで直す方針を決めました。π0.5 がずれる所をゲームパッドで手で直し、その直した動きを補正つきのお手本として集め、学習し直します(前から使っている、巻き戻しと補正の流れ)。

ゲームパッドの補正 p で、推論のプロセスが落ち、固まった

直そうとして p(ゲームパッドで補正)を押すと、3回とも止まりました。

試行p を押したあと
191618
「コントローラーを待っています」のあと Segmentation fault: 11 で推論のプロセスが落ちた(終了コード 139)
192201
「[PAD ] 補正 1 …」の行のあと何も出ず、Ctrl-C も効かない
194015192201 と同じ

192201 のプロセスは、pkill・kill -9 でも消えず、「終わりかけ(E)」の状態で残りました。

端末:192201 の推論のプロセス(24051)を止める

Last login: Tue Nov 10 14:47:32 on ttys002
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % pkill -f run_policy_client.py
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % pkill -9 -f run_policy_client.py
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % pgrep -fl python3
1426 python3 /Users/shimazueiichi/r2r2r-scripts/inbox/inbox_runner.py
24051 python3 pi05/run_policy_client.py --raw-dir /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute --ignore-framing --model deep1 (PixelstoMotors/pi05-mecharm-deep-even10-lora-v1) --framing-raw-dir /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % kill -9 24051; sleep 1; ps -o pid,stat,command -p 24051
  PID STAT COMMAND
24051 ?E+  (python3.12)
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % ps -o pid,stat,command -p 24051
  PID STAT COMMAND
24051 ?E+  (python3.12)
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ %

E は、プロセスが終わろうとしていて、OS の中の後片付けが済んでいない状態です。ゲームパッドかカメラを Mac が手放すのを待って止まっていると見ています。このプロセスが残っている間は、Pi のアームを動かす係(daemon。一度に1つのつながりしか受けない)も返事をしなくなり、立ち上げ直しが HOME の所で止まりました(記録の全量の lab_start_20261110_192719.txt)。daemon を起動し直して先へ進めましたが、194015 で p を押すと、また同じ所で固まりました。

端末:19:39 に 24051 がまだ残っているか確かめた

Last login: Tue Nov 10 19:26:57 on ttys002
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ %    ps -p 24051
  PID TTY           TIME CMD
24051 ??         0:00.00 (python3.12)
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ %

pygame と SDL2 は、ゲームパッドを読むための部品

p を押すと、推論のプロセスはゲームパッドを読むために pygame(Python でゲームの入力や画面を扱う部品)を読み込みます。pygame は中で SDL2(画面・音・コントローラーを扱う C の部品)を使います。カメラの画像を扱う OpenCV も、別の SDL2 を抱えています。

言葉ここでの意味
pygame
ゲームパッドのボタンとスティックを読むのに使っている部品(tools/gamepad_teleop.py)
SDL2
pygame の下でコントローラーを扱う部品。OpenCV も同じ名前の部品を別に持っている
objc の警告
Mac で、同じ名前の部品が2つ読み込まれたときに出る警告。「動作がおかしくなったり、謎の落ち方をすることがある」と書かれている

p を押すたびに、次の警告が出ていました(全文は記録の全量にある)。

objc[24773]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x120ffc890) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1328592c8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.

午前は同じ警告が出ても p が何度も使えていたので、この警告だけが原因とは言い切れません。夜は1回目(191618)で落ちたあと、2回目からは固まっています。1回目で落ちたときに何かが残ったのか、残った 24051 がゲームパッドを握ったままなのか、まだ分かっていません。24051 を消せるのは Mac の再起動だけで、この場では再起動できなかったため、中断しました。

固まらない形に直すことを、依頼145 として依頼箱に入れました。

依頼文(依頼箱 20261110_145)

---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 90
---
# 依頼145:実機推論の p(ゲームパッドで補正)で固まるのを直す

実機は動かさない(実装・テスト・レビューまで)。エイイチは見ていない。Log28。

## 何が起きたか(2026-11-10 夜・KIOXIA lab/Log28/lab_start_20261110_193100.txt ほか)
- 11/10 午前は p が何度も使えた(「コントローラーを待っています」→「[接続] PS4 Controller」が出た)
- 19:22 の試行(pi05_20261110_192201)で p を押すと「[PAD   ] 補正 1 (推論 4 のあと)…」の行のあと何も出ず、Ctrl-C も効かず固まった。
  pkill・kill -9 のあとも、プロセス(24051)が ps で STAT「?E+」のまま消えない(終わりかけで止まっている)。Mac の再起動まで残る見込み
- そのため Pi の daemon(1本ずつしか受けない)が ping に答えなくなり、tools/daemon_ctl.sh restart が要った
- 19:40 の試行(pi05_20261110_194015)で p を押すと、また同じ所で固まった(24051 が残ったまま)
- p のたびに objc の警告が出る:cv2 と pygame が libSDL2 をそれぞれ別に持っていて「Class SDL… is implemented in both … This may cause spurious casting failures and mysterious crashes」

## やること
1. 原因を調べて報告に書く(SDL2 の二重読み込み・すでに死にかけのプロセスがパッドを握っている・pygame の初期化がパッド待ちで止まる、など。分かる範囲で)
2. 固まっても推論ごと止まらない形にする。案:ゲームパッドの読み取りを別のプロセスに分ける(cv2 と同じプロセスで pygame を読まない)。
   別プロセスが一定時間(例 5 秒)で接続の返事をしなければ、「ゲームパッドがつながりません。c (キー) で補正してください」と出して、その補正を抜け、推論の入力待ちに戻る。
   daemon のつながりと鍵の扱い(1本ずつ)は今のまま守る
3. 補正中に Ctrl-C を押したら、補正を抜けて推論の入力待ちに戻れるようにする(今は効かない)
4. tools/gamepad_teleop.py を単独で動かす形も、今までどおり使えるようにする
5. 操作と送り方(1関節ずつ・長押し・L2+R2 で止める・□ で戻る)は変えない
6. テストを足して通す(パッドがつながらない・返事がないときに固まらず戻る、を偽物のパッドで)。既存のテスト(test_gamepad_teleop.py・pi05/ のもの・test_lab_start.sh)も通す
7. codex で差分を見てもらう。指摘を直して通るまで
8. 変えたファイルだけを指定して git add・commit(git add . は使わない)

## やらないこと
- 関節・安全範囲・送る速さ・爪の扱いは変えない
- 依頼144(傾きをソフトで直す)と同じファイル(run_policy_client.py)を触るので、144 の commit の上に重ねる。食い違ったら止めて報告

## 報告
claude_code_reports/20261110_145.txt に:原因の見立て、変えた所、テストの結果、codex の回数と指摘、commit の番号、Mac の再起動のあとエイイチが確かめる手順(p を押して何が出れば直っているか)、決めてほしいこと。
**報告の最後の1行は必ず次のどれか**:「判定:合格」/「判定:止めた(決めてほしいことあり)」/「判定:失敗」
  • ねらい:補正(直した動きを学習データにするための入り口)を、固まらずに使えるようにする
  • 変わること:ゲームパッドの読み取りを推論とは別のプロセスに分け、5秒つながらなければ補正を抜けて推論の入力待ちに戻る。補正中の Ctrl-C でも戻れる
  • 見込める効果:p で固まって Mac の再起動まで止まる状態がなくなる見込み。直ったかは、Mac を再起動したあと p を押して「[接続] PS4 Controller」が出るかで確かめる

アームを HOME に戻し、Pod を消した(38分・約59円)

推論のプロセスが固まったまま、アームは位置2 のブロックの上で止まっていました。daemon を起動し直してから HOME に戻し、Pod を消しました。

端末:daemon を起動し直して HOME に戻し、Pod を消す

(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % cd ~/r2r2r-scripts && tools/daemon_ctl.sh restart && sleep 3 && python3 dynamic_tracking_limits/place/place_modules/08_home_from_anywhere.py
--- process
21910 /usr/bin/python3 -u /home/ubuntu/mecharm_daemon.py
--- 5555
LISTEN0      1                                 0.0.0.0:5555        0.0.0.0:*    
--- systemd
enabled: enabled
active:  active
[INIT] daemon 接続中...
  [Pi] fresh_mode=1
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-37.35, 70.48, -10.01, 6.85, -54.14, -32.69]
[SEND] HOME へ移動指令
[WAIT] t=0.6s  max_err=57.39°
[WAIT] t=1.1s  max_err=41.74°
[WAIT] t=1.6s  max_err=25.83°
[WAIT] t=2.1s  max_err=9.93°
[WAIT] t=2.6s  max_err=1.40°
[DONE] HOME 到達 max_err=1.40°
[POST] 最終姿勢: [-0.35, 1.40, 0.70, -0.61, 0.79, -30.84]
  [Pi] fresh_mode=0
[EXIT] fresh_mode=0, socket close
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % ./lab_stop.sh --log Log28 
[STOP  ] Log28 の Pod: id 9qvy252fd9qgb4  GPU=NVIDIA A40  作った時刻 2026-11-10 19:09:20
[LOG   ] serve.log を持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log28/serve_20261110_194719.log (33 行)
[STOP  ] Terminate を送った (19:47:27)
[STOP  ] Pod (id 9qvy252fd9qgb4) が消えたことを API で確かめた
[COST  ] 借りていた時間 38 分 7 秒 (0.64 時間) × 1時間 0.59 ドル = 約 0.37 ドル (約 59 円. 1ドル=158円)
[TUNNEL] 8000 番のトンネルは残っていません
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts %

夜に借りた Pod は NVIDIA A40 で、38分7秒・約0.37ドル(約59円)でした。午前の分(51分・約80円)は LOG484 にあります。

位置2 の補正つきのお手本を集め、deep1b で学習し直す

この回で分かったことと、次に進める順番です。

  • 位置2 に届かなかった理由として、いちばん疑わしいのは上カメラの傾き。学習データの中で、位置1 の9本だけがほかと違う向きで写っていた。ただし確定はしていない
  • 傾きをそろえ直した夜は、π0.5 が位置2 のつかむ姿勢へ向かう答えを3回続けて出した。ただし最後の数mmで、爪がブロックの奥側にずれる
  • お手本を撮るときは、撮る前にカメラの傾きをそろえる(位置ごとに時間を分けて撮ると、向きの違いが位置の手がかりになるおそれがある)

次は、まず録画の画像だけを回して π0.5 に渡す試し(上の節の3つ)で、傾きが原因かを確かめます。そのうえで、Mac を再起動して 24051 を消し、依頼145 の報告で p が固まらなくなったことを確かめてから、位置2 で π0.5 がずれる所をゲームパッドで直し、補正つきのお手本を5本ほど集めます。それを足して、学習率の下げきりをステップ数に合わせた deep1b(LOG480 のあとに用意した設定)で学習し直します。位置1 の9本だけ写り方が違う件は、そのあとで、撮り直すか、依頼144 のソフトでそろえる形で吸収できるかを見て決めます。

記録の全量 ― 端末の出力・各回の記録・Pod の記録

端末の出力は、lab_start.sh が KIOXIA の lab/Log28/ に残したものをそのまま載せます。serve_20261110_194719.log の Pod の中の IP アドレス(192.168 で始まる、Pod の中だけの番地)は <POD_INTERNAL_IP> に置き換えました。色の付いた文字の制御コード(ESC)は、表示が崩れるので外しています。

lab_start_20261110_190916.txt(新しい Pod を借り、1回目 191618 で p を押して落ち、2回目 191905 を Ctrl-C で止めた)

03_sec16.txt48,069字・775行・text
[RANGE ] 安全範囲 (学習データ ±10°) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (--model deep1)
[FRAME ] 写り方の確かめの基準画像は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes の最初のエピソードから (位置合わせと同じ trim2. 依頼 137)
[TILT  ] 上カメラの傾きのお手本 (各エピソードの 1 枚目の中央値) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (安全範囲と同じ. 表示だけ. 依頼 142)
[LAB   ] 2026-11-10 19:09:16  GPU=NVIDIA A40  episode=run_0001_20261019_194818  log=/Volumes/KIOXIA/r2r2r-data/lab/Log28  
[MODEL ] --model deep1: π0.5 base から deep_even10_v1 で新しく学習した 2999 (依頼 133. decay_steps 5000)
         PixelstoMotors/pi05-mecharm-deep-even10-lora-v1 → /workspace/ckpt/deep_v1_2999  設定 pi05_mecharm_deep_even10_lora  assets local/mecharm_redblock_box_deep_even10_v1

=== 1. Pod を作る
別のタブで開く保存

lab_start_20261110_191918.txt(同じ Pod につなぎ直し、192201 で推論4 のあと p を押して固まった)

04_sec16.txt19,460字・187行・text
[RANGE ] 安全範囲 (学習データ ±10°) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (--model deep1)
[FRAME ] 写り方の確かめの基準画像は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes の最初のエピソードから (位置合わせと同じ trim2. 依頼 137)
[TILT  ] 上カメラの傾きのお手本 (各エピソードの 1 枚目の中央値) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (安全範囲と同じ. 表示だけ. 依頼 142)
[LAB   ] 2026-11-10 19:19:18  GPU=NVIDIA A40  episode=run_0001_20261019_191541  log=/Volumes/KIOXIA/r2r2r-data/lab/Log28  
[MODEL ] --model deep1: π0.5 base から deep_even10_v1 で新しく学習した 2999 (依頼 133. decay_steps 5000)
         PixelstoMotors/pi05-mecharm-deep-even10-lora-v1 → /workspace/ckpt/deep_v1_2999  設定 pi05_mecharm_deep_even10_lora  assets local/mecharm_redblock_box_deep_even10_v1

=== 1. Pod を作る
別のタブで開く保存

lab_start_20261110_192618.txt(古い lab_start.sh が残っていて、二重に起動しないよう止まった)

[RANGE ] 安全範囲 (学習データ ±10°) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (--model deep1)
[FRAME ] 写り方の確かめの基準画像は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes の最初のエピソードから (位置合わせと同じ trim2. 依頼 137)
[TILT  ] 上カメラの傾きのお手本 (各エピソードの 1 枚目の中央値) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (安全範囲と同じ. 表示だけ. 依頼 142)
[ERROR] Log28 の lab_start.sh がもう動いています (pid 23845). 二重に借りないよう止まります

lab_start_20261110_192719.txt(daemon が ping に答えず、HOME に戻せずに止まった)

05_sec16.txt3,859字・86行・text
[RANGE ] 安全範囲 (学習データ ±10°) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (--model deep1)
[FRAME ] 写り方の確かめの基準画像は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes の最初のエピソードから (位置合わせと同じ trim2. 依頼 137)
[TILT  ] 上カメラの傾きのお手本 (各エピソードの 1 枚目の中央値) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (安全範囲と同じ. 表示だけ. 依頼 142)
[LAB   ] 2026-11-10 19:27:19  GPU=NVIDIA A40  episode=run_0001_20261019_194818  log=/Volumes/KIOXIA/r2r2r-data/lab/Log28  
[MODEL ] --model deep1: π0.5 base から deep_even10_v1 で新しく学習した 2999 (依頼 133. decay_steps 5000)
         PixelstoMotors/pi05-mecharm-deep-even10-lora-v1 → /workspace/ckpt/deep_v1_2999  設定 pi05_mecharm_deep_even10_lora  assets local/mecharm_redblock_box_deep_even10_v1

=== 1. Pod を作る
別のタブで開く保存

端末:daemon を起動し直してから lab_start.sh を打った所

(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % tools/daemon_ctl.sh restart && sleep 3 && ./lab_start.sh --model deep1 --log Log28
--- process
9155 /usr/bin/python3 -u /home/ubuntu/mecharm_daemon.py
--- 5555
LISTEN0      1                                 0.0.0.0:5555        0.0.0.0:*    
--- systemd
enabled: enabled
active:  active
(このあとは lab_start_20261110_193100.txt)

lab_start_20261110_193100.txt(193431 で位置2 のつかむ姿勢へ向かい h で戻した、194015 で p を押して固まった)

06_sec16.txt27,176字・292行・text
[RANGE ] 安全範囲 (学習データ ±10°) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (--model deep1)
[FRAME ] 写り方の確かめの基準画像は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes の最初のエピソードから (位置合わせと同じ trim2. 依頼 137)
[TILT  ] 上カメラの傾きのお手本 (各エピソードの 1 枚目の中央値) は /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes から (安全範囲と同じ. 表示だけ. 依頼 142)
[LAB   ] 2026-11-10 19:31:00  GPU=NVIDIA A40  episode=run_0001_20261019_194818  log=/Volumes/KIOXIA/r2r2r-data/lab/Log28  
[MODEL ] --model deep1: π0.5 base から deep_even10_v1 で新しく学習した 2999 (依頼 133. decay_steps 5000)
         PixelstoMotors/pi05-mecharm-deep-even10-lora-v1 → /workspace/ckpt/deep_v1_2999  設定 pi05_mecharm_deep_even10_lora  assets local/mecharm_redblock_box_deep_even10_v1

=== 1. Pod を作る
別のタブで開く保存

lab_stop_20261110_194717.txt

[STOP  ] Log28 の Pod: id 9qvy252fd9qgb4  GPU=NVIDIA A40  作った時刻 2026-11-10 19:09:20
[LOG   ] serve.log を持ち帰った → /Volumes/KIOXIA/r2r2r-data/lab/Log28/serve_20261110_194719.log (33 行)
[STOP  ] Terminate を送った (19:47:27)
[STOP  ] Pod (id 9qvy252fd9qgb4) が消えたことを API で確かめた
[COST  ] 借りていた時間 38 分 7 秒 (0.64 時間) × 1時間 0.59 ドル = 約 0.37 ドル (約 59 円. 1ドル=158円)
[TUNNEL] 8000 番のトンネルは残っていません

serve_20261110_194719.log(Pod の推論サーバーの記録)

07_sec16.log.txt3,188字・33行・text
INFO:root:Loading model...
INFO:2026-11-10 10:13:57,743:jax._src.xla_bridge:925: Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:jax._src.xla_bridge:Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:2026-11-10 10:13:57,747:jax._src.xla_bridge:925: Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:jax._src.xla_bridge:Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:absl:orbax-checkpoint version: 0.11.13
INFO:absl:Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x7c9647275390>
INFO:absl:Restoring checkpoint from /workspace/ckpt/deep_v1_2999/params.
別のタブで開く保存

pi05_20261110_191905.json(2回目。推論の前に Ctrl-C)

{
 "script": "run_policy_client.py",
 "source": "pi05_policy",
 "run_stem": "pi05_20261110_191905",
 "run_dir": "/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261110_191905",
 "recording_enabled": true,
 "end_reason": "ctrl_c",
 "result": "interrupted",
 "exit_code": 0,
 "outcome": null,
 "rewinds": 0,
 "corrections": 0,
 "history_left": [],
 "args": {
  "host": "localhost",
  "port": 8000,
  "execute": true,
  "auto": false,
  "steps_per_chunk": 10,
  "max_chunks": 60,
  "max_move_deg": 20.0,
  "raw_dir": "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes",
  "framing_raw_dir": "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes",
  "prompt": "put the red block in the white box",
  "model": "deep1 (PixelstoMotors/pi05-mecharm-deep-even10-lora-v1)",
  "record": false,
  "ignore_framing": true,
  "state_from_target": true,
  "via_stride": 3,
  "settle_tol": 1.0,
  "settle_retries": 0,
  "dry_keys": false,
  "send_wait": 0.0,
  "grip_wait": 0.8,
  "grip_open_wait": 0.0,
  "settle_wait": 0.8
 },
 "ended_at": "2026-11-10T19:19:08.564102"
}

pi05_20261110_193431.json(爪が奥側にずれていたので h で HOME へ。失敗として記録)

{
 "script": "run_policy_client.py",
 "source": "pi05_policy",
 "run_stem": "pi05_20261110_193431",
 "run_dir": "/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261110_193431",
 "recording_enabled": true,
 "end_reason": "h",
 "result": "completed",
 "exit_code": 0,
 "outcome": "failure",
 "rewinds": 0,
 "corrections": 0,
 "history_left": [
  {
   "kind": "chunk",
   "chunk": 0,
   "correction_id": null,
   "grip_changed": false
  },
  {
   "kind": "chunk",
   "chunk": 1,
   "correction_id": null,
   "grip_changed": false
  },
  {
   "kind": "chunk",
   "chunk": 2,
   "correction_id": null,
   "grip_changed": false
  },
  {
   "kind": "chunk",
   "chunk": 3,
   "correction_id": null,
   "grip_changed": false
  }
 ],
 "args": {
  "host": "localhost",
  "port": 8000,
  "execute": true,
  "auto": false,
  "steps_per_chunk": 10,
  "max_chunks": 60,
  "max_move_deg": 20.0,
  "raw_dir": "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_deep_even10_v1/raw_episodes",
  "framing_raw_dir": "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes",
  "prompt": "put the red block in the white box",
  "model": "deep1 (PixelstoMotors/pi05-mecharm-deep-even10-lora-v1)",
  "record": false,
  "ignore_framing": true,
  "state_from_target": true,
  "via_stride": 3,
  "settle_tol": 1.0,
  "settle_retries": 0,
  "dry_keys": false,
  "send_wait": 0.0,
  "grip_wait": 0.8,
  "grip_open_wait": 0.0,
  "settle_wait": 0.8
 },
 "ended_at": "2026-11-10T19:39:57.652312",
 "judgments": {
  "pick": {
   "human_verdict": "no",
   "verdict": "no",
   "source": "human"
  },
  "place": {
   "human_verdict": "no",
   "verdict": "no",
   "source": "human"
  }
 }
}

191618・192201・194015 は、推論のプロセスが落ちたか固まったため、各回の json が書かれていません(log.jsonl と recording/ は残っている)。

調べるのに使ったスクリプト(KIOXIA の lab/Log28/analysis/)

上の節で使ったもののほか、ブロックの位置を mm で出そうとして、夜の画像では赤が見つからなかったスクリプトも残しています。

tilt_bm.py(マットの上の mm で赤の候補を出す。お手本・夜の試行とも候補が見つからなかった)

import sys, cv2
sys.path.insert(0,'.')
from pathlib import Path
import count_by_position as cb
runs=Path(sys.argv[1])/'runs'
for s in sys.argv[2:]:
    rd=cb.find_run_dir(s,runs)
    cap=cv2.VideoCapture(str(rd/'recording/top.mp4')); ok,f=cap.read(); cap.release()
    c=cb.detect_corners(f)
    import numpy as np
    co=[[float(x),float(y)] for x,y in c]
    H=cv2.getPerspectiveTransform(np.array(co,np.float32),np.array(cb.MAT_CORNERS_MM,np.float32))
    bbox=(int(min(co[0][0],co[3][0])),int(max(co[1][0],co[2][0])),int(min(co[0][1],co[1][1])),int(max(co[2][1],co[3][1])))
    cs=cb.red_candidates(f,bbox,H)
    print(s, [ (round(x['mm'][0],1),round(x['mm'][1],1),x['area']) for x in cs])

tilt_bm.py の出力(193431 は、この時点では録画のファイルがまだ書き終わっておらず、読めなかった):

[mov,mp4,m4a,3gp,3g2,mj2 @ 0xc93e4ceff5f0] moov atom not found
run_0001_20261019_191541 []
run_0001_20261019_191635 []
run_0001_20261109_102109 []
run_0001_20261109_104517 []
pi05_20261110_113600 []
pi05_20261110_114106 []
Traceback (most recent call last):
  File "/tmp/bm.py", line 9, in <module>
    c=cb.detect_corners(f)
  File "/sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/r2r2r-scripts/pi05/./count_by_position.py", line 141, in detect_corners
    return mcd.detect_corners(frame)
  File "/sessions/rcw-01levexubzsy9ycdbwdhrthd/mnt/r2r2r-scripts/mat_corner_detection.py", line 47, in detect_corners
    h, w = frame.shape[:2]
AttributeError: 'NoneType' object has no attribute 'shape'

サムネイルのために足したコード(p2m-site の tools/lab_thumbs/)

表紙は、位置2 で π0.5 が出した行き先のひじ j2 を、午前の4回(薄い線)と夜の4回(青)で重ねたものです。夜の線だけが、位置2 のつかむ姿勢の −34°まで下りていきます。

lab_thumb.py に足した LOG486 の分(git diff)

diff --git a/tools/lab_thumbs/lab_thumb.py b/tools/lab_thumbs/lab_thumb.py
index 9c20cd5..ce418e8 100644
--- a/tools/lab_thumbs/lab_thumb.py
+++ b/tools/lab_thumbs/lab_thumb.py
@@ -339,7 +339,7 @@ def both(*fs):
 # ---------- 記事ごとの指定 ----------
 # 一覧の並び(LOG番号の大きい順)と構図。隣どうしで同じ構図にしない
 ORDER = """
-484 split   480 dark    479 split   463 dark    461 paper   460 split   450 dark    446 paper   436 dark    422 photo_l 419 photo_r 417 dark
+486 paper   484 split   480 dark    479 split   463 dark    461 paper   460 split   450 dark    446 paper   436 dark    422 photo_l 419 photo_r 417 dark
 407 split   403 dark    401 paper   363 split   361 paper   358 dark    355 paper   352 split
 349 dark    348 paper   345 split   341 dark    337 split   327 paper   326 dark    277 paper
 234 dark    225 paper
@@ -349,6 +349,14 @@ ORDER = """
 def build(log, d, img_dir):
     n = int(log[3:])
     # GPU ラックの写真(public/site/gbr/reverse.webp)はここでは使わない(2026-11-08 エイイチ「GPUラックの写真は作っちゃダメ」)
+    if n == 486:
+        # 位置2 で π0.5 が出した行き先(log.jsonl の actions を、最初の人の操作の手前までつないだもの)のひじ j2。
+        # 薄い線は午前の4回(上カメラが右下がり約 +0.7°。位置1 寄りの -10° 前後で平らになる)、青は夜の4回(右上がり約 -0.8〜-1.0° に直したあと。
+        # 位置2 のつかむ姿勢の -34° まで下りていく)。赤は閉じる指令の所
+        d6 = d['LOG486']
+        runs = d6['before'] + d6['after']
+        role = lambda ri, p: 'q' if ri < len(d6['before']) else None
+        return paper('CAMERA TILT', 'POSITION 2', runs_draw(runs, 2, w=6, kind_of=role, scale=(-38, 4)))
     if n == 484:
         # 上は位置1 の3回、下は位置2 の4回。肩 j1 の動き(横は1回の中の順番、縦は同じ目盛り)。青=π0.5 が動かした所、赤=つかんでいる間、
         # 墨・紙色=ゲームパッドで直した所。位置1 は3回とも下りきってつかみ、位置2 は途中で人の直しに替わる