フィジカルAI研究室の中身を一目で伝える ― 構成・実験・技術の解説を /series6 に

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

pixelstomotors.com のフィジカルAI研究室のページ(/series6)に、研究室の構成・いま回している実験・いま調べていること・使っている技術の4つの解説を足し、ページの HTML から読める研究室の説明に2,298字が加わりました。 研究室は、小型のロボットアーム mechArm 270 と Mac に、Runpod で時間単位で借りる GPU(NVIDIA A40)を足しただけの構成で、Physical Intelligence の π0.5(ロボット用の AI のモデル)に、赤いブロックを白い箱へ入れる作業を覚えさせています。

足したことで、次のことが変わりました。

  • 初めて来た人は、研究室の記録(Log#1〜#22)を1本も開かずに、何を使い、何を試し、いまどこまで来たかを1ページでつかめる。解説の数字はすべて記録の値で、元の記録へのリンクを18本付けた
  • AI と検索エンジンは、図ではなく HTML の文字として説明を読める。見出しの下の説明は、導入比較ガイドのバナーを含めて295字だった
  • 時間:依頼(11月7日 16:10)から最初の公開(16:20)まで10分。画面写真に一言ずつ付いた直しと足しを5回入れ、最後の公開は18:13。前に同じ量の解説を作ったときの時間は測っていない
  • 引き換え:研究室の構成に実機の動画(0.77MB)を入れたので、ページは重くなった

始まりは、フィジカルAI研究室のページが、見出しと短い紹介文、GPU の導入比較ガイドのバナー、記録のカードの並び、だけだったことです。記録のカードを開けば全部書いてありますが、記録は作業ログなので、1本ずつ読んで全体をつかむのは、初めての人にはかなりきつい。

フィジカルAI研究所にページの解説セクションを追加してください。どんな構成で、どんな実験を行なっていて、どんな技術的なことなのかを一目でわかるようにしたです。

最初の版は10分で出せましたが、矢印の図と、意味の分からない黒いカード、見出しの長さで段々になったカード、斜めに写った写真が残っていました。そこから、画面写真に一言の指示 → 直して公開 → 実際の画面で確かめる、を5回回しています(5回目は爪の電流の図を足す依頼)。

やったことなぜ結果
研究室の記録22本から、解説に載せる数字と道具を拾った
解説の数字を、記録で確かめられる値だけにするため
19本・4,000ステップ・15回中9回・13秒・11本など。どれも元の LOG へリンク
構成・実験・技術の3つの節を作った(commit c7caad9)
依頼の3つ(構成・実験・技術)に1節ずつ答えるため
10分で最初の公開
実験のカードの段をそろえた(commit 09c80f5)
見出しが2行のカードだけ数字が下がり、段々に見えた
CSS の subgrid で、5枚の数字の高さが同じになった。上の線も外した
矢印の図と黒いカードをやめた(commit c556a04・43cf04c)
矢印はごちゃつき、黒いカードは意図が伝わらなかった
実機の動画と、手元に置くもの・時間で借りるものの2つの一覧
上のカメラの写真の傾きを直した(commit 965efdb)
マットが斜めに写っていた
元の録画(1920×1080)から、マットの縁で射影変換して切り直した
爪の電流の図を足した(commit 3a73764・43f5322)
別のチャットで、爪の電流でつかめたかが分かると出た
空で閉じた15前後・つかんだ870前後の図。LOG461 の公開後に数字とリンクをそろえた
解説を足す前のフィジカルAI研究室のページ。見出しと紹介文、GPU の導入比較ガイドのバナーの下に、すぐ記録のカードが並んでいた

作業ログ ― 解説に載せる数字を、研究室の記録22本から拾う

載せた数字は、記録で確かめられる値だけにした

解説の数字は、あとで変わってもたどれるように、すべて記録(LOG)の値から取り、その LOG へのリンクを付けました。数字は 2026年11月6日(LOG450)までの記録の値で、ページの実験の節の下にもそう書いています。

載せた所数字出どころ
構成:Runpod の A40
GPU のメモリ 48GB、1時間あたり約98円
LOG401・LOG450
構成:Hugging Face
学習済みのモデル 6.3GB、借りた GPU が13秒で取ってくる
LOG401・LOG450
構成:ゲームパッド3,198円の互換品LOG419
実験 01 お手本を集める19本(置き場所5か所)LOG349・LOG352
実験 02 π0.5 に覚えさせる
4,000ステップ、学習の誤差(loss)0.0858 → 0.0037
実験 03 実機で動かす
15回中9回(腕を伸ばしきる位置は5回中1回)
LOG361・LOG401
実験 04 人が直す
1回あたり中央の値で13秒(キーで直していた頃は41〜51秒)
LOG403・LOG446・LOG450
実験 05 直した成功を学習に足す
11本、前のモデルから1,500ステップ続けて学ぶ
LOG417・LOG422
技術:推論サーバー
π0.5 の計算は1回平均97ミリ秒、Mac から送って返るまでの往復は391ミリ秒(A40)
LOG326
いま調べていること
爪の値は、空で閉じると15前後、つかむと870前後
LOG461

研究室の記録の一覧(作業を始めた時点で Log#1〜#22)

Studio から移した記事(src/data/cards.json)と、新しい記事(src/content/records/)の両方から、シリーズがフィジカルAI研究室の記事を拾いました。下の一覧は記事化のときに出し直したもので、作業のあとに公開された Log#23(LOG461)と Log#24(LOG460)も入っています。

# フィジカルAI研究室の記録の一覧を、p2m-site の中から作る(cards.json は Studio から移した記事、records は新しい記事)
import json,glob,re
c=json.load(open('src/data/cards.json'))
rows={}
num=lambda s:int(re.sub(r'\D','',str(s)))
for x in c:
    if x['series']=='フィジカルAI研究室': rows[num(x['day'])]=(x['title'],x['slug'])
for f in glob.glob('src/content/records/*.md'):
    s=open(f).read(3000)
    if 'series: "フィジカルAI研究室"' in s:
        t=re.search(r'^title: "(.*)"',s,re.M).group(1); sl=re.search(r'^slug: (.*)',s,re.M).group(1); d=num(re.search(r'^day: (\S+)',s,re.M).group(1))
        rows[d]=(t,sl)
for d in sorted(rows): print(d,'|',rows[d][0])
print(len(rows))
225 | SO-101 失敗から学ぶアームを作る Log#1 | 精度を上げることと、失敗から学ぶことは違う
234 | SO-101 失敗から学ぶアームを作る Log#2 | 2TBの外付けSSDを、学習データの定位置にする
277 | フィジカルAI研究室を作る Log#1 | 自宅にゼロから、誰でも再現できる形で
326 | フィジカルAI研究室を作る Log#2 | Physical Intelligence の π0.5 を借りたGPUで動かした
327 | フィジカルAI研究室を作る Log#3 | Physical Intelligence の π0.5 に要るGPUメモリを測った
337 | フィジカルAI研究室を作る Log#4 | Physical Intelligence の π0.5 を mechArm 270 で追加学習
341 | フィジカルAI研究室を作る Log#5 | Physical Intelligence の π0.5 を学習。予測はお手本とほぼ一致
345 | フィジカルAI研究室を作る Log#6 | Physical Intelligence の π0.5 で実機を動かすと、爪がブロックに乗った
348 | フィジカルAI研究室を作る Log#7 | Physical Intelligence の π0.5 を倍学習しても、爪は奥へずれた
349 | フィジカルAI研究室を作る Log#8 | 手で教えた動きを再生し、4回続けて箱に入れた
352 | フィジカルAI研究室を作る Log#9 | 手で教えてお手本19本。肩88°を超えた姿勢は動かなかった
355 | フィジカルAI研究室を作る Log#10 | 位置を変えたお手本で、Physical Intelligence の π0.5 を学習し直した
358 | フィジカルAI研究室を作る Log#11 | Physical Intelligence の π0.5 は「待ち時間」まで覚えた
361 | フィジカルAI研究室を作る Log#12 | Physical Intelligence の π0.5 が初めてブロックを箱に入れた
363 | フィジカルAI研究室を作る Log#13 | Physical Intelligence の π0.5 がお手本の3か所すべてで箱に入れた
401 | フィジカルAI研究室を作る Log#14 | π0.5 を回数で測ったら、苦手な置き場所が見えた
403 | フィジカルAI研究室を作る Log#15 | mechArm 270 の失敗を巻き戻し、直して覚えさせる
407 | フィジカルAI研究室を作る Log#16 | 直した mechArm 270 の動きを π0.5 に覚えさせる手前まで
417 | フィジカルAI研究室を作る Log#17 | 人が直した mechArm 270 の5回で π0.5 を学び直す
419 | フィジカルAI研究室を作る Log#19 | ゲームパッドで mechArm 270 を直す準備ができた
422 | フィジカルAI研究室を作る Log#18 | 学び直した π0.5 でも mechArm 270 には人の直しが要った
436 | フィジカルAI研究室を作る Log#20 | ゲームパッドで動いた mechArm 270、遅さは pymycobot の版
446 | フィジカルAI研究室を作る Log#21 | ゲームパッドで mechArm 270 を直す形が固まった
450 | フィジカルAI研究室を作る Log#22 | mechArm 270 の AI の直しが、ゲームパッドで1回13秒に
460 | フィジカルAI研究室を作る Log#24 | π0.5 の失敗の原因を探ると、浅いお手本だった
461 | フィジカルAI研究室を作る Log#23 | 手応えを学習に足すため、mechArm 270 の電流を読んだ
26

先に決めたこと:1つの節で伝えることは1つ

デザインの進め方の決まり(LOG369 で整理したもの)に沿って、節ごとに伝えることを先に1つ決めました。研究室の構成は、机の上と借りる GPU の何で動いているか。実験は、お手本 → 学習 → 実機 → 人の直し → 学び直し のループ。技術は、使っている5つの技術の名前と中身です。1つの節に3つ以上のことを入れない、要らない飾りは入れない、ボタン・矢印・角丸はサイトのほかのページ(画像認識の /series7 の解説)にそろえる、も最初から守るつもりで作りました。

作業ログ ― 最初の版:構成・実験・技術の3つの節(commit c7caad9・16:20)

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

ファイルの一覧(8本)

01_sec02.txt ― 最初の版の差分(commit c7caad9)・17,004字

02_sec03.txt ― 段をそろえた差分(commit 09c80f5)・3,256字

03_sec04.txt ― 1回目:机の写真と、2つに分けた一覧(commit c556a04・17:29)・7,419字

04_sec04.txt ― 2回目:写真を動画に、2つの一覧を面で分ける(commit 43cf04c・17:46)・3,319字

05_sec06.json ― 爪の値のデータ(src/data/lab/gripper_current_20261107.json)・3,705字

06_sec06.txt ― 最初は16と960、LOG461 の公開後に15前後と870前後へそろえた(commit 43f5322・11月8日 15:37)・9,713字

07_sec06.txt ― 最初は16と960、LOG461 の公開後に15前後と870前後へそろえた(commit 43f5322・11月8日 15:37)・3,232字

08_LabGuide.astro.txt ― 作業ログ ― 最後の LabGuide.astro(commit 43f5322 の時点の全文)・16,105字

部品 LabGuide.astro を作り、/series6 だけに出す

画像認識のページ(/series7)には、同じ日の朝に入れた解説の部品 VisionGuide.astro があります。同じ置き方で、研究室用の部品 LabGuide.astro を作り、シリーズの設定(src/lib/records.ts)に lab: true を足したページだけに出すようにしました。中身の図はすべて HTML の文字と CSS で組み、画像の中に説明の文字を焼き込まないようにしています(AI が読めるように)。

写真の素材:横からの録画のコマと、π0.5 に渡したお手本のコマ

研究室の構成の写真には、10月29日に π0.5 で実機を回したときの横からの録画(LOG401 の15回の試し)からコマを抜きました。技術の節の π0.5 の行には、π0.5 に実際に渡したお手本のコマ(384×216)を置きました。

# 横からの録画(2026-10-29・IMG_4215.MOV・3840×2160)から5コマを抜く(Mac 連携の shell)
cd "$HOME/mnt/KIOXIA/r2r2r-data/recordings/20261029_iphone"
mkdir -p $HOME/frames
for t in 5 40 80 120 160; do ffmpeg -v error -y -ss $t -i IMG_4215.MOV -frames:v 1 -vf scale=1600:-2 -q:v 3 "$HOME/frames/a_$t.jpg"; done
D="$HOME/mnt/PixelstoMotors/デジタルツイン学習R2R2R/フィジカルAI研究室を作る/series6_guide_images"
mkdir -p "$D" && cp $HOME/frames/*.jpg "$D/"

# π0.5 に渡したお手本のコマ(corr_v5 の pi05_20261106_183926・384×216)
cd "$HOME/mnt/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5/raw_episodes/pi05_20261106_183926"
cp frames/000000.jpg "$D/top_000.jpg"; n=$(ls frames | wc -l); cp frames/$(printf %06d $((n/2))).jpg "$D/top_mid.jpg"
# 最初の版の写真2枚(commit c7caad9)。desk.webp はのちに動画へ替えて消した
from PIL import Image
U='/mnt/user-data/uploads/PixelstoMotors/デジタルツイン学習R2R2R/フィジカルAI研究室を作る/series6_guide_images/'
im=Image.open(U+'a_80.jpg').convert('RGB'); im=im.crop((60,0,1600-60,900-60)); im=im.resize((1200,round(1200*im.size[1]/im.size[0])),Image.LANCZOS); print(im.size); im.save('public/site/lab/desk.webp',quality=84)
t=Image.open(U+'top_mid.jpg').convert('RGB'); print(t.size); t.save('public/site/lab/top-camera.webp',quality=92)
(1200, 681)
(384, 216)

最初の版の画面

研究室の構成は、机の上・手元の Mac・時間で借りる GPU の3つの箱を横に並べ、間を矢印でつなぎました。借りる GPU の箱だけを黒く塗っています。実験は5枚のカードを横に並べ、カードの上に黒い線を引きました。

最初の版の研究室の構成。机の上・手元の Mac・時間で借りる GPU の3つの箱が矢印でつながり、右の GPU の箱だけが黒い

最初の版の差分(commit c7caad9)

01_sec02.txt17,004字・286行・diff
diff --git a/src/components/LabGuide.astro b/src/components/LabGuide.astro
new file mode 100644
index 0000000..51bc453
--- /dev/null
+++ b/src/components/LabGuide.astro
@@ -0,0 +1,238 @@
+---
+// /series6(フィジカルAI研究室)の見出しの下に置く解説(2026-11-07)
別のタブで開く保存

作業ログ ― 実験のカードが段々になった:CSS の subgrid で段をそろえる(commit 09c80f5・17:21)

カードの見出しが2行になると、そのカードだけ数字が下がる

この段々になっているのを修正。カードの上にライン入っているのは無しで削除

5枚のカードは、1枚ずつ上から番号・見出し・数字・文・リンクを積んでいました。04(失敗しかけたら人が直す)と05(直した成功を学習に足す)のように見出しが2行になるカードだけ、数字が1行分下がります。5枚が横に並ぶと、数字の高さがそろわず段々に見えました。

直す前の実験のカード。見出しが2行の04と05だけ数字が下がり、カードの上に黒い線がある

subgrid は、子の要素の段を親の格子とそろえる CSS の仕組み

CSS の subgrid(サブグリッド)は、格子(grid)の中に置いたカードの中身を、親の格子の行に合わせて並べる仕組みです。カード1枚を親の格子の5行にまたがせ(grid-row: span 5)、カードの中の5つの段(番号・見出し・数字・文・リンク)を親の行に乗せる(grid-template-rows: subgrid)と、5枚のどれかの見出しが2行になったとき、その行の高さが5枚とも同じに伸びます。上の図解で、前と後を切り替えて確かめられます。

  • ねらい:5枚の数字を同じ高さに置き、横に読み比べられるようにする
  • 変わったこと:数字の上の位置の差が、図解の幅(カード1枚約125px)で測って21px から 0px になった。カードの上の黒い線は外した
  • 見込める効果:LOG が進んで見出しや数字を書き換えても、段がくずれない

ページの幅を 1240px にしたとき、4,000ステップの単位が折り返して数字の段だけ高くなったので、数字に white-space: nowrap を付け、字の大きさを画面の幅に合わせて 32〜40px で変えるようにしました。

直したあとの実験のカード。5枚の見出し・数字・文・リンクが同じ高さに並び、カードの上の線はない

段をそろえた差分(commit 09c80f5)

02_sec03.txt3,256字・40行・diff
diff --git a/src/components/LabGuide.astro b/src/components/LabGuide.astro
index 51bc453..e33cf3f 100644
--- a/src/components/LabGuide.astro
+++ b/src/components/LabGuide.astro
@@ -168,6 +168,7 @@ const tech: { name: string; sub: string; p: string; fig?: boolean; links: Link[]
   .lg-zone-gpu .lg-items dt { color: #fff; }
   .lg-zone-gpu .lg-items dd { color: #c8c8c2; }
   .lg-flow { display: grid; align-content: start; gap: 28px; padding: 72px 8px 0; }
別のタブで開く保存

作業ログ ― 矢印の図と黒いカードをやめ、手元に置くもの・時間で借りるもの に分ける(commit c556a04・43cf04c)

黒いカードの意図は、ページのどこにも書いていなかった

矢印やデザイン性の低い構成を考え直してください。右のカードがなぜ黒いかの意図もわからない

借りる GPU の箱を黒くしたのは、AI の計算はここで動いていると示すためでした。ただ、ページのどこにもそう書いておらず、ほかの2つが灰色の中で1つだけ黒い理由が見えません。矢印も、何が行き来するかを見せるつもりで置いたものの、ラベルの長さで上下の位置がずれ、図をごちゃつかせただけでした。

そこで、伝えることを1つに絞り直しました。机の上のアームとカメラと Mac に、時間で借りる GPU を足しただけ、ということです。矢印で見せたかった流れ(Mac がカメラの画像と関節の角度を送り、π0.5 が次の1秒の関節の角度を返す)は、導入の1文に入れています。

1回目:机の写真と、2つに分けた一覧(commit c556a04・17:29)

左に机の写真、右に手元に置くもの(mechArm 270・上のカメラ・ゲームパッド・Mac)と時間で借りるもの(Runpod の A40・Hugging Face)の2つの一覧を置きました。写真は、腕・ブロック・箱が入るように、ブロックから箱までを約1.75:1で切り直しています。

1回目の直し。左に机の写真、右に手元に置くもの・時間で借りるものの一覧。2つの分け目は小さな灰色の見出しだけ
03_sec04.txt7,419字・133行・diff
diff --git a/src/components/LabGuide.astro b/src/components/LabGuide.astro
index e33cf3f..e507d91 100644
--- a/src/components/LabGuide.astro
+++ b/src/components/LabGuide.astro
@@ -1,6 +1,6 @@
 ---
 // /series6(フィジカルAI研究室)の見出しの下に置く解説(2026-11-07)
-// 1 研究室の構成:机の上・Mac・借りた GPU の3か所と、その間を行き来するもの(図は HTML で組む。AI も読めるように)
別のタブで開く保存

2回目:写真を動画に、2つの一覧を面で分ける(commit 43cf04c・17:46)

手元に置くもの・時間で借りるもので分けられていた方がいいね(よくわかんない)。左の写真は動画がいいね(画角もう少し見直して)

2つの分け目が小さな灰色の文字だけだったので、それぞれを灰色の面で囲み、見出しを大きくしました。写真は、π0.5 が人の手を入れずに成功した1回(10月29日、LOG401 の試しの1つ)を横から撮った録画に替えました。録画の左側の何も起きていないマットを外し、右寄りに4:3で切っています。元の録画は、腕を立てた姿勢だと腕の上が画面の外に出るので、腕が下りてから箱に入れ終わるまで(12〜140秒)を6倍速にしました。

  • ねらい:π0.5 が何をしているのかを、文を読む前に見て分かるようにする
  • 変わったこと:止まった写真が、ブロックをつかんで箱に入れるまでの約21秒の動画(音なし・繰り返し)になった。手元に置くものと時間で借りるものが、別々の面になった
  • 見込める効果:初めて来た人が、研究室の作業の中身(何を・どこへ)を動画1本でつかめる。代わりにページは 0.77MB 重くなった(読み込みの速さへの影響は測っていない)
# 研究室の構成の動画(commit 43cf04c)。2026-10-29 の横からの録画 IMG_4222.MOV(3840×2160)の12〜140秒を、
# 右寄りに4:3で切り出して6倍速にする。元の録画は腕を立てた姿勢で上が画面の外に出るので、腕が下りてからを使う
cd "$HOME/mnt/KIOXIA/r2r2r-data/recordings/20261029_iphone"
D="$HOME/mnt/PixelstoMotors/デジタルツイン学習R2R2R/フィジカルAI研究室を作る/series6_guide_images"
mkdir -p "$D/upload"
ffmpeg -v error -y -ss 12 -t 128 -i IMG_4222.MOV -an -vf "crop=2700:2025:1140:0,setpts=PTS/6,fps=30,scale=1200:900" -c:v libx264 -preset veryfast -crf 26 -pix_fmt yuv420p -movflags +faststart "$D/upload/lab-setup-pi05-run-v2.mp4"
ffmpeg -v error -y -ss 12 -i IMG_4222.MOV -frames:v 1 -vf "crop=2700:2025:1140:0,scale=1200:900" -q:v 3 "$D/lab-setup-pi05-run-v2-poster.jpg"
ls -la "$D/upload"
total 1824
-rw-------  1 … 772542 Nov  7 08:45 lab-setup-pi05-run-v2.mp4
-rw-------  1 … 1090633 Nov  7 08:41 lab-setup-pi05-run.mp4

最初は0〜165秒(27.5秒・1.09MB)で作って R2 に入れましたが、腕を立てた最初の姿勢で上が切れていたので、12〜140秒の v2 を作り直してページはそちらを読むようにしました。R2 へは、同じ名前がすでにあれば入れない形で1本ずつ入れています(Cloudflare の鍵は KIOXIA のファイルから読むだけで、記事には載せていない)。

# 動画1本を R2(p2m-media)へ入れる。同じ名前がすでにあれば入れない(上書きしない)
# Mac 連携の shell で: python3 - <動画のパス> <<'PYEOF' … PYEOF の形で動かした。鍵は KIOXIA のファイルから読むだけ
import json, sys, time, urllib.parse, urllib.request
from pathlib import Path
API='https://api.cloudflare.com/client/v4/accounts/21d7aed1b193796b89287df10913f2dc/r2/buckets/p2m-media/objects'
tok=(Path.home()/'mnt/KIOXIA/p2m_cloudflare_token.txt').read_text().strip().split()[0]
p=Path(sys.argv[1]); name=p.name
def req(m,u,data=None,ct=None):
    r=urllib.request.Request(u,data=data,method=m); r.add_header('Authorization','Bearer '+tok)
    if ct: r.add_header('Content-Type',ct)
    with urllib.request.urlopen(r,timeout=300) as res: return json.loads(res.read() or b'{}')
try:
    req('GET',API+'/'+urllib.parse.quote(name)); print('すでにある・入れない')
except Exception:
    d=req('PUT',API+'/'+urllib.parse.quote(name),p.read_bytes(),'video/mp4'); print('入れた',d.get('success'))
入れた True
$ curl -s -o /dev/null -w "%{http_code} %{size_download} %{content_type}\n" https://pixelstomotors.com/media/lab-setup-pi05-run.mp4
200 1090633 video/mp4
2回目の直しのあとの研究室の構成。左に実機の動画、右に手元に置くものと時間で借りるものが別々の灰色の面に分かれている
04_sec04.txt3,319字・53行・diff
diff --git a/src/components/LabGuide.astro b/src/components/LabGuide.astro
index e507d91..1175b66 100644
--- a/src/components/LabGuide.astro
+++ b/src/components/LabGuide.astro
@@ -1,9 +1,11 @@
 ---
 // /series6(フィジカルAI研究室)の見出しの下に置く解説(2026-11-07)
-// 1 研究室の構成:机の写真と、手元に置くもの・時間で借りるものの一覧(矢印の図は 2026-11-07 にやめた)
別のタブで開く保存

作業ログ ― 上のカメラの写真の傾きを、マットの縁から射影変換で直す(commit 965efdb・17:56)

384×216 のまま直すと、さらに粗くなる

写真をトリミングと補正をかけてください。斜めになっています

技術の節の π0.5 の行の写真は、π0.5 に渡したお手本のコマそのもの(384×216)で、マットが少し傾いて写っていました。この大きさのまま傾きを直して切ると、さらに粗くなります。そこで、同じ上のカメラの元の録画(11月6日の実機の回 pi05_20261106_183926、1920×1080)から、爪がブロックをつかみ、横に箱がある125秒目のコマを取り直しました。

直す前の π0.5 の行。上のカメラの写真のマットが斜めに写っている

射影変換は、写真の中の四角を長方形に写し直す計算

射影変換(OpenCV の getPerspectiveTransform と warpPerspective)は、写真の中で斜めに写った四角の4つの角を、きっちりした長方形の4つの角へ写すように、写真全体をゆがめ直す計算です。マットは白い机の上の暗い面なので、明るさが白から暗へ変わる所を左・右・下の縁として拾い、それぞれに直線を当てました。上の縁は画面の外なので、左右の直線が画面の上端と交わる点を上の角にしています。

# 上のカメラの1コマ(1920×1080)から、マットの左・右・下の縁を拾って直線を当てる(commit 965efdb の前)
import numpy as np, cv2
src='/mnt/user-data/uploads/PixelstoMotors/デジタルツイン学習R2R2R/フィジカルAI研究室を作る/series6_guide_images/top_full_125.jpg'
im=cv2.imread(src); g=cv2.cvtColor(im,cv2.COLOR_BGR2GRAY).astype(int)
L=[];R=[];B=[]
for y in range(10,880,10):
    r=g[y]
    for x in range(250,500):
        if r[x-3]>180 and r[x+3]<150: L.append((x,y)); break   # 白い机 → 暗いマット
    for x in range(1800,1550,-1):
        if r[x+3]>180 and r[x-3]<150: R.append((x,y)); break
for x in range(350,1700,10):
    c=g[:,x]
    for y in range(1060,800,-1):
        if c[y+3]>180 and c[y-3]<150: B.append((x,y)); break
def fit(pts,vert):
    p=np.array(pts,float)
    return np.polyfit(p[:,1],p[:,0],1) if vert else np.polyfit(p[:,0],p[:,1],1)
la,lb=fit(L,1); ra,rb=fit(R,1); ba,bb=fit(B,0)
print(len(L),la,lb,len(R),ra,rb,len(B),ba,bb)
def xl(y): return la*y+lb
def xr(y): return ra*y+rb
yl=(ba*lb+bb)/(1-ba*la); xbl=xl(yl)
yr=(ba*rb+bb)/(1-ba*ra); xbr=xr(yr)
tl=(xl(0),0); tr=(xr(0),0)
print('tl',tl,'tr',tr,'bl',(xbl,yl),'br',(xbr,yr))
87 0.015934606692425454 304.9427960438384 87 0.05026791572501142 1680.3303929430633 135 -0.03310896497902642 970.7933665008289
tl (np.float64(304.9427960438384), 0) tr (np.float64(1680.3303929430633), 0) bl (np.float64(320.24305322185177), np.float64(960.1904504669302)) br (np.float64(1726.257110154082), np.float64(913.638780295942))

左の縁は下へ行くほど右へ(1,000px 下がって約16px)、下の縁は右へ行くほど上へ(1,400px 進んで約46px)ずれていました。この4つの角を 1391×937 の長方形へ写し、アーム・ブロック・箱が入る右寄りを4:3で切りました。明るさと色は少しだけ上げ、マス目がきつく見えないよう、コントラストを強める処理(CLAHE)は試したうえで使っていません。

# 上のカメラの写真を作り直す(commit 965efdb)。拾ったマットの4隅を長方形へ写し(射影変換)、右寄りに4:3で切り、明るさを少し上げる
import numpy as np, cv2
src='/mnt/user-data/uploads/PixelstoMotors/デジタルツイン学習R2R2R/フィジカルAI研究室を作る/series6_guide_images/top_full_125.jpg'
im=cv2.imread(src)
tl=(304.94,0); tr=(1680.33,0); bl=(320.24,960.19); br=(1726.26,913.64)
Wd,Hd=1391,937
M=cv2.getPerspectiveTransform(np.float32([tl,tr,br,bl]),np.float32([(0,0),(Wd,0),(Wd,Hd),(0,Hd)]))
out=cv2.warpPerspective(im,M,(Wd,Hd),flags=cv2.INTER_CUBIC)
x1=Wd-14; x0=x1-1110; y1=Hd-14; y0=y1-832
c=out[y0:y1,x0:x1].astype(np.float32)
c=np.clip(c*1.06+4,0,255).astype(np.uint8)
hsv=cv2.cvtColor(c,cv2.COLOR_BGR2HSV).astype(np.float32); hsv[...,1]*=1.08; c=cv2.cvtColor(np.clip(hsv,0,255).astype(np.uint8),cv2.COLOR_HSV2BGR)
c=cv2.resize(c,(800,600),interpolation=cv2.INTER_AREA)
cv2.imwrite('public/site/lab/top-camera.png',c)
from PIL import Image
Image.open('public/site/lab/top-camera.png').save('public/site/lab/top-camera.webp',quality=86)
import os; os.remove('public/site/lab/top-camera.png'); print('ok')
ok

写真の下の説明も、「π0.5 に渡している、上のカメラの画像(384×216)」から「上のカメラの画像。傾きを直して切り出しています。π0.5 には、この画像を 384×216 に縮めて渡します」に替えました。いまの写真は、π0.5 に渡したそのものではないためです。

直したあとの使っている技術の節。π0.5 の行の写真はマットの縁がまっすぐになり、その下に追加学習・時間で借りる GPU・推論サーバー・巻き戻しと人の直しが並ぶ
diff --git a/src/components/LabGuide.astro b/src/components/LabGuide.astro
index 1175b66..9089371 100644
--- a/src/components/LabGuide.astro
+++ b/src/components/LabGuide.astro
@@ -6,7 +6,8 @@
 // 2 いま回している実験:お手本 → 学習 → 実機 → 人の直し → 直した成功を学習に足す、のループと各段の数字
 // 3 使っている技術:π0.5・追加学習(LoRA)・借りる GPU・推論サーバー・巻き戻しと人の直し
 // 絵は public/site/lab/(run-poster.webp は動画の最初のコマ、
-// top-camera.webp は corr_v5 のお手本 pi05_20261106_183926 の真ん中のコマ。元は iCloud の
+// top-camera.webp は pi05_20261106_183926 の上のカメラの録画(KIOXIA runs/pi05_policy/…/recording/top.mp4、1920×1080)の125秒目。
+// マットの左・右・下の縁から傾きを直して右寄りに4:3で切り出し、明るさを少し上げた。元は iCloud の
 // フィジカルAI研究室を作る/series6_guide_images/)
 // 数字はどれも LOG の記録の値。新しい LOG で数字が変わったら、ここも書き換える
 type Link = { log: string; slug: string; t: string };
@@ -136,8 +137,8 @@ const tech: { name: string; sub: string; p: string; fig?: boolean; links: Link[]
             </div>
             {t.fig && (
               <figure class="lg-fig">
-                <img src="/site/lab/top-camera.webp" alt="真上のカメラの画像。mechArm 270 の爪が赤いブロックをつかみ、右に白い箱がある" width="384" height="216" loading="lazy" />
-                <figcaption>π0.5 に渡している、上のカメラの画像(384×216)</figcaption>
+                <img src="/site/lab/top-camera.webp" alt="真上のカメラの画像。mechArm 270 の爪が赤いブロックをつかみ、右に白い箱がある" width="800" height="600" loading="lazy" />
+                <figcaption>上のカメラの画像。傾きを直して切り出しています。π0.5 には、この画像を 384×216 に縮めて渡します</figcaption>
               </figure>
             )}
           </div>
@@ -214,7 +215,7 @@ const tech: { name: string; sub: string; p: string; fig?: boolean; links: Link[]
     .lg-loop { grid-template-columns: repeat(2, minmax(0, 1fr)); }
     .lg-tech-row { grid-template-columns: minmax(0, 1fr); gap: 8px; }
     .lg-tech-body.has-fig { grid-template-columns: minmax(0, 1fr); gap: 20px; }
-    .lg-fig { max-width: 384px; }
+    .lg-fig { max-width: 480px; }
   }
   @media (max-width: 599px) {
     .lg-lead { margin: -16px 0 24px; font-size: 15px; line-height: 26px; }

作業ログ ― 爪の電流の図を、いま調べていることの節として足す(commit 3a73764・18:13)

別のチャットで、爪の電流でつかめたかが分かると出た

同じ日に別のチャット(のちの LOG461)で、mechArm 270 のサーボの電流を読んでいました。爪のサーボの値は、空で閉じたときとブロックをつかんだときで大きく違い、つかめたかどうかがカメラなしで分かります。

それとこの電流値の図解をこのページのセクションに入れ込めますか? 別のチャットで進めているものです。

伝えることは、つかめたかは爪の電流でカメラなしに分かる、の1つにし、別のチャットで描いた肩の図は入れませんでした(押されたことは分かっても向きが分からず、この節の1つのことから外れるため)。左に2つの大きな数字、右に爪の値の線だけの図を置き、空で閉じた区間とつかんだ区間を灰色の面で示しています。並びは、実験の節のあと、技術の節の前です。

図の値は、KIOXIA の実機の記録から読み直した

図は別のチャットの画面写真ではなく、KIOXIA の diag/servo_current_20261107/180051_pad/samples.csv から読み直して描きました。爪(サーボ7番)の値は、番地69(下位)と70(上位)の2つに分かれていて、70の一番上のビットが向き(マイナス)を表します。

$ cat marks.csv
t_mac,key,name,sample_i
1794042097.898721,1,static,449
1794042118.407143,2,close_empty,663
1794042140.794489,3,grasp,876
1794042154.007000,4,pull,1013
1794042183.396141,6,push_arm,1298
1492 (0.0, -5, 11) (144.36, -47, 3)
0 40 435 2 -7 8
55 62 75 16 7 21
66 78 113 -48 -63 -3
80 82 20 946.0 923 977
82 88 61 885 854 926
100 106 61 802 793 809
112 140 301 -45 -66 -24

上は、区間ごとの始めの秒・終わりの秒・数・中央値・最小・最大です。読めた数は144.36秒で1,492回、1秒に10.34回でした。値が変わった所を拾うと、空で閉じたのは51〜65秒、つかんだのは79.6〜109.4秒でした。

n 1492 dur 144.36 hz 10.34
0.0 -5 z
50.95 39 pos
53.04 8 z
53.14 16 pos
55.49 7 z
55.58 10 pos
60.77 8 z
60.88 16 pos
67.39 -3 z
67.47 -43 neg
79.0 1 z
79.1 39 pos
79.6 627 hi
109.4 349 pos
109.5 -49 neg
# 爪(サーボ7番)の電流の値を、0.5秒ごとの中央値にして JSON に書き出す(commit 3a73764)
# Mac 連携の shell で KIOXIA の diag/servo_current_20261107/180051_pad の中で動かし、出力を iCloud に置いた
import csv,json,statistics
rows=list(csv.DictReader(open('samples.csv')))
t0=float(rows[0]['t_mac_recv']); b={}
for r in rows:
    try: regs=json.loads(r['regs'])
    except: continue
    v={(a,c):d for a,c,d,*_ in regs}
    lo,hi=v.get((7,69)),v.get((7,70))
    if lo is None or hi is None: continue
    t=float(r['t_mac_recv'])-t0
    b.setdefault(int(t*2),[]).append((lo+(hi&0x7f)*256)*(-1 if hi&0x80 else 1))
print(json.dumps({"source":"KIOXIA r2r2r-data/diag/servo_current_20261107/180051_pad/samples.csv(爪=サーボ7の番地69・70。70の最上位ビットを符号として読む)","step_sec":0.5,"stat":"0.5秒ごとの中央値","points":[[k/2,round(statistics.median(x))] for k,x in sorted(b.items())]},ensure_ascii=False))

爪の値のデータ(src/data/lab/gripper_current_20261107.json)

05_sec06.json3,705字・1行・json
{"source": "KIOXIA r2r2r-data/diag/servo_current_20261107/180051_pad/samples.csv(爪=サーボ7の番地69・70。70の最上位ビットを符号として読む)", "step_sec": 0.5, "stat": "0.5秒ごとの中央値", "points": [[0.0, -3], [0.5, -3], [1.0, -1], [1.5, 1], [2.0, 0], [2.5, -4], [3.0, -4], [3.5, -3], [4.0, 2], [4.5, 0], [5.0, 2], [5.5, 4], [6.0, -2], [6.5, 1], [7.0, 2], [7.5, -1], [8.0, 0], [8.5, 4], [9.0, 7], [9.5, 5], [10.0, -1], [10.5, 6], [11.0, 4], [11.5, 5], [12.0, 6], [12.5, 5], [13.0, 3], [13.5, 2], [14.0, 4], [14.5, 0], [15.0, -4], [15.5, 4], [16.0, 4], [16.5, 4], [17.0, 5], [17.5, 2], [18.0, -4], [18.5, 4], [19.0, -3], [19.5, -2], [20.0, 5], [20.5, 0], [21.0, 0], [21.5, -4], [22.0, -3], [22.5, 4], [23.0, 3], [23.5, 2], [24.0, 5], [24.5, 2], [25.0, 4], [25.5, -1], [26.0, 2], [26.5, 6], [27.0, -2], [27.5, 0], [28.0, 2], [28.5, 0], [29.0, -1], [29.5, 5], [30.0, 3], [30.5, 4], [31.0, 2], [31.5, -2], [32.0, -3], [32.5, -1], [33.0, 3], [33.5, -1], [34.0, 2], [34.5, 2], [35.0, 2], [35.5, 4], [36.0, 1], [36.5, 5], [37.0, 3], [37.5, 4], [38.0, 1], [38.5, 1], [39.0, -2], [39.5, 4], [40.0, 0], [40.5, 2], [41.0, 4], [41.5, 0], [42.0, 7], [42.5, 0], [43.0, 3], [43.5, 2], [44.0, 5], [44.5, 4], [45.0, 2], [45.5, -1], [46.0, 6], [46.5, -2], [47.0, 4], [47.5, 0], [48.0, 0], [48.5, 4], [49.0, -2], [49.5, 1], [50.0, 3], [50.5, 2], [51.0, 79], [51.5, 23], [52.0, 15], [52.5, 16], [53.0, 19], [53.5, 15], [54.0, 13], [54.5, 17], [55.0, 16], [55.5, 14], [56.0, 18], [56.5, 16], [57.0, 16], [57.5, 12], [58.0, 18], [58.5, 17], [59.0, 11], [59.5, 14], [60.0, 17], [60.5, 16], [61.0, 17], [61.5, 14], [62.0, 16], [62.5, 15], [63.0, 12], [63.5, 14], [64.0, 20], [64.5, 16], [65.0, 15], [67.0, -23], [67.5, -47], [68.0, -52], [68.5, -46], [69.0, -51], [69.5, -46], [70.0, -47], [70.5, -52], [71.0, -47], [71.5, -48], [72.0, -48], [72.5, -47], [73.0, -48], [73.5, -44], [74.0, -51], [74.5, -49], [75.0, -48], [75.5, -47], [76.0, -47], [76.5, -49], [77.0, -53], [77.5, -49], [78.0, -50], [78.5, -47], [79.0, 116], [79.5, 896], [80.0, 966], [80.5, 951], [81.0, 940], [81.5, 930], [82.0, 921], [82.5, 910], [83.0, 905], [83.5, 897], [84.0, 890], [84.5, 887], [85.0, 877], [85.5, 876], [86.0, 870], [86.5, 865], [87.0, 860], [87.5, 860], [88.0, 856], [88.5, 851], [89.0, 848], [89.5, 848], [90.0, 845], [90.5, 843], [91.0, 840], [91.5, 837], [92.0, 835], [92.5, 834], [93.0, 831], [93.5, 832], [94.0, 824], [94.5, 823], [95.0, 820], [95.5, 819], [96.0, 816], [96.5, 816], [97.0, 814], [97.5, 813], [98.0, 812], [98.5, 808], [99.0, 809], [99.5, 810], [100.0, 806], [100.5, 806], [101.0, 806], [101.5, 803], [102.0, 805], [102.5, 800], [103.0, 800], [103.5, 799], [104.0, 798], [104.5, 796], [105.0, 797], [105.5, 800], [106.0, 800], [106.5, 793], [107.0, 800], [109.0, 558], [109.5, -36], [110.0, -57], [110.5, -52], [111.0, -50], [111.5, -53], [112.0, -49], [112.5, -58], [113.0, -50], [113.5, -54], [114.0, -54], [114.5, -62], [115.0, -60], [115.5, -61], [116.0, -58], [116.5, -60], [117.0, -58], [117.5, -58], [118.0, -60], [118.5, -54], [119.0, -57], [119.5, -58], [120.0, -57], [120.5, -53], [121.0, -57], [121.5, -58], [122.0, -59], [122.5, -58], [123.0, -44], [123.5, -45], [124.0, -38], [124.5, -44], [125.0, -47], [125.5, -45], [126.0, -39], [126.5, -46], [127.0, -48], [127.5, -39], [128.0, -34], [128.5, -39], [129.0, -32], [129.5, -38], [130.0, -36], [130.5, -40], [131.0, -42], [131.5, -30], [132.0, -38], [132.5, -33], [133.0, -36], [133.5, -45], [134.0, -27], [134.5, -39], [135.0, -45], [135.5, -38], [136.0, -30], [136.5, -36], [137.0, -35], [137.5, -33], [138.0, -37], [138.5, -34], [139.0, -36], [139.5, -45], [140.0, -36], [140.5, -37], [141.0, -30], [141.5, -39], [142.0, -42], [142.5, -31], [143.0, -38], [143.5, -43], [144.0, -46]]}
別のタブで開く保存

最初は16と960、LOG461 の公開後に15前後と870前後へそろえた(commit 43f5322・11月8日 15:37)

最初に公開したときは、0.5秒ごとの中央値から、空で閉じた値を16、つかんだ値を960(つかんだ直後の高い所)と書き、およそ60倍としていました。そのあと公開された LOG461 は、同じ記録を区間の中央値で、空で閉じると15前後、つかむと870前後、差は50倍以上と書いています。ページの数字が記録と食い違わないよう、記事化の前に、ページを LOG461 の書き方にそろえ、LOG461 へのリンクを付けました。図の線はそのままです。

いま調べていることの節。左に空で閉じた15前後とつかんだ870前後の数字と LOG461 へのリンク、右に爪の値の線の図
06_sec06.txt9,713字・110行・diff
diff --git a/src/components/LabGuide.astro b/src/components/LabGuide.astro
index 9089371..4c55dd0 100644
--- a/src/components/LabGuide.astro
+++ b/src/components/LabGuide.astro
@@ -4,7 +4,8 @@
 //   動画は R2 の lab-setup-pi05-run-v2.mp4(2026-10-29 の横からの録画 IMG_4222.MOV の12〜140秒を、右寄りに4:3で切り出し6倍速。
 //   元の録画は腕を立てた姿勢で上が画面の外に出るので、腕が下りてから箱に入れ終わるまでを使う)
 // 2 いま回している実験:お手本 → 学習 → 実機 → 人の直し → 直した成功を学習に足す、のループと各段の数字
別のタブで開く保存
07_sec06.txt3,232字・49行・diff
diff --git a/src/components/LabGuide.astro b/src/components/LabGuide.astro
index 4c55dd0..e93c331 100644
--- a/src/components/LabGuide.astro
+++ b/src/components/LabGuide.astro
@@ -81,7 +81,8 @@ const tech: { name: string; sub: string; p: string; fig?: boolean; links: Link[]
 ];
 
 // 4 いま調べていること:爪のサーボの電流の値(2026-11-07 の実機。0.5秒ごとの中央値。元は KIOXIA の diag/servo_current_20261107/180051_pad)
別のタブで開く保存

作業ログ ― 公開した7回と、確かめたこと

commit の一覧

commit時刻中身
c7caad911/7 16:20
構成・実験・技術の3つの節を足した
09c80f511/7 17:21
実験のカードの段を subgrid でそろえ、上の線を外した
c556a0411/7 17:29
矢印の図と黒いカードをやめ、机の写真と2つの一覧にした
43cf04c11/7 17:46
写真を実機の動画に替え、2つの一覧を面で分けた
965efdb11/7 17:56
上のカメラの写真の傾きを射影変換で直した
3a7376411/7 18:13
いま調べていること(爪の電流)を足した
43f532211/8 15:37
爪の数字を LOG461 にそろえ、LOG461 へのリンクを足した

公開のたびに、ビルドと実際の画面で確かめた

公開の前に npm run build を通し、手元で astro preview を開いて、幅 1440px・1240px・390px の画面写真を撮って見ました。公開したあとは、ページの HTML に新しい文が入ったことを curl で待ってから、公開されたページで同じように確かめています。

const { chromium } = require('playwright');
(async () => {
  const [w, out] = process.argv.slice(2);
  const b = await chromium.launch(); const p = await b.newPage({ viewport: { width: +w, height: 900 } });
  await p.goto('http://localhost:4321/series6', { waitUntil: 'networkidle' });
  await p.addStyleTag({content:'[class*=toast]{display:none!important}'});
  const el = (await p.$$('.lg'))[2]; await el.scrollIntoViewIfNeeded(); await p.waitForTimeout(500);
  await el.screenshot({ path: out });
  await b.close();
})();

HTML から読める文字の数を数えた

公開されたページで、解説の4つの節の文字(空白を除く)を数えました。見出しの下の説明(導入比較ガイドのバナーを含む)は295字、解説の4つの節は403字・745字・288字・862字で、合わせて2,298字です。節の中のリンクは17本で、LOG461 へのリンクを足して18本になりました。

const { chromium } = require('playwright');
(async () => {
  const b = await chromium.launch(); const p = await b.newPage();
  await p.goto('https://pixelstomotors.com/series6', { waitUntil: 'networkidle' });
  const r = await p.evaluate(() => {
    const lg=[...document.querySelectorAll('.lg')].map(e=>e.innerText.replace(/\s+/g,'').length);
    const hero=document.querySelector('.panel')?.innerText.replace(/\s+/g,'').length;
    const links=[...document.querySelectorAll('.lg a')].length;
    return {lg, hero, links};
  });
  console.log(JSON.stringify(r));
  await b.close();
})();
{"lg":[403,745,288,862],"hero":295,"links":17}
69456

2行目は、そのときのページの HTML の大きさ(バイト)です。

記事の画面写真を撮ったスクリプト

const { chromium } = require('playwright');
(async () => {
  const b = await chromium.launch(); const p = await b.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
  await p.goto('https://pixelstomotors.com/series6', { waitUntil: 'networkidle' });
  await p.addStyleTag({content:'[class*=toast],[class*=parade],[class*=hakobu-walk]{display:none!important} header,.site-header,nav{position:static!important}'});
  const secs = await p.$$('.lg');
  const names = ['05_setup_video','08_experiment_aligned','09_gripper_current','07_tech_rows'];
  for (let i=0;i<secs.length;i++){ await secs[i].scrollIntoViewIfNeeded(); await p.waitForTimeout(i===0?6000:800); await secs[i].screenshot({path:`LOG462_images/LOG462_${names[i]}.png`}); }
  await b.close();
})();

作業ログ ― 最後の LabGuide.astro(commit 43f5322 の時点の全文)

08_LabGuide.astro.txt16,105字・292行・astro
---
// /series6(フィジカルAI研究室)の見出しの下に置く解説(2026-11-07)
// 1 研究室の構成:実機の動画と、手元に置くもの・時間で借りるものの一覧(矢印の図は 2026-11-07 にやめた)
//   動画は R2 の lab-setup-pi05-run-v2.mp4(2026-10-29 の横からの録画 IMG_4222.MOV の12〜140秒を、右寄りに4:3で切り出し6倍速。
//   元の録画は腕を立てた姿勢で上が画面の外に出るので、腕が下りてから箱に入れ終わるまでを使う)
// 2 いま回している実験:お手本 → 学習 → 実機 → 人の直し → 直した成功を学習に足す、のループと各段の数字
// 3 いま調べていること:爪のサーボの電流(2026-11-07 追加。並びは実験のあと・技術の前)
// 4 使っている技術:π0.5・追加学習(LoRA)・借りる GPU・推論サーバー・巻き戻しと人の直し
別のタブで開く保存