デジタルツイン学習 R2R2R の中身を表紙で伝える ― サムネイル77本を動画と実写に
この記事の素材(コード・ログ・データ 1ファイル・まとめてダウンロードあり)arrow_downwardAIで読む ChatGPT・Claude などで、この記事に質問できます expand_more
デジタルツイン学習 R2R2R のシリーズの記事サムネイル77本を、ターミナルやエラー画面の画面写真から、その回の動画の1コマ・机の上のロボットアーム(mechArm 270)の写真・その回の図解に替えた。 記事一覧のカードを見ただけで、各回でアームが何をしたのか、何を測ったのかが分かるようにするためだ。素材の選び方は、動画があれば動画、なければその日の実機の写真、それもなければ図解の図の部分、という順に決めた。
替えたことで、次のことが変わった。
- シリーズのページ(pixelstomotors.com/series2 )を開いた人が、記事を開く前に、その回のアームの姿勢やブロックの位置を写真で見られる。前は、文字だらけの黒い画面のカードが何本も続いていた
- 77本のうち、10本は YouTube に上げた動画の1コマ、24本は実機の写真、3本は記事の中の画像、29本は記事の図解の一部、11本は記事の図解と同じ見た目で描き起こした図にした
- 作業はひとつのチャットで、最初の依頼から公開まで約1時間半だった(チャットの時刻で数えた。1本あたりの時間は測っていない)。途中で、スマホでシリーズのページがスクロールできない不具合も見つけて直した
サムネイルは、記事一覧のカードの上に出る絵のこと。R2R2R は、iPhone で物を3Dスキャンし、Unity(ゲームを作る道具で、ここではロボットの動きを試す画面)の中で動きを試してから、机の上の mechArm 270 で実際に動かす記録だ。記事の多くは Studio という前のサイトの道具で書いていて、その頃は表紙にその日の作業画面をそのまま貼っていた。そのため、プログラムの実行結果が並んだターミナルの画面が表紙の回が、約50本あった。ロボットアームを動かしているシリーズなのに、一覧を開くと黒い画面に白い文字ばかりが目に入る。

やったことの全体は次のとおり。
| やったこと | 本数 | 素材 |
|---|---|---|
ターミナルの画面写真だった表紙を替えた | 42 | 実機の写真・図解の図の部分・記事の中の画像 |
写真も図もない回の表紙を描き起こした | 11 | 記事に書いてあることを、図解と同じ見た目の図に |
HTML で作った表紙の回も、同じ方針で替えた | 17 | 実機の写真・図解の図の部分・記事の中の画像 |
YouTube の動画がある回は、動画の1コマに替えた | 10 | 動画の1コマ(そのうち3本は上の回と重なる) |
スマホでシリーズのページが動かない不具合を直した | 1ページ | 埋め込んだ Google ドキュメントを、ページに直接載せる形に |
替えたあとのページでは、同じあたりにアームとブロックの写真や、図解の図が並ぶ。

ターミナルの画面が混ざった表紙は、まだ残っている。LOG209・LOG208・LOG199 のように、写真と画面が半々の表紙と、白黒に加工した画面は、元の表紙のままにした。どこまで替えるかは、次に一覧を見直すときに決める。
作業ログ ― 替える回と、使える素材を数えた
最初に、サイトのデータ(GitHub の p2m-site の src/data/cards.json と、記事ごとの src/data/records/*.json)から、R2R2R のシリーズの記事を数えた。185本あり、そのうち168本は画像の表紙、17本は HTML で作った表紙だった。画像の表紙を全部ダウンロードして並べ、ターミナルやエラーの画面が表紙になっている回を目で拾った。
拾った回について、本文に何が入っているかを数えた。
| 本文にあるもの | 本数 |
|---|---|
| 記事の中に画像がある | 5 |
画像はなく、図解(記事に入れた HTML の図)だけがある | 40 |
| 画像も図解もない | 5 |
記事の中の画像も、多くはターミナルや GitHub の画面写真だった。そのまま使える写真があったのは LOG20 と LOG36 の2本だけだった。そこで、Mac の外付けの SSD(KIOXIA)にある実機の記録と、r2r2r-scripts のフォルダにある撮影の画像を、記事の日付ごとに探した。
runs/run_pick_place/{回の名前}/に、1回の試行ごとに上のカメラで撮った PICK(つかむ)と PLACE(置く)の写真がある。6月19日から7月15日の回で使えたr2r2r-scripts/logs/とdynamic_tracking_limits/logs/に、4月から6月の校正やカメラの確認の画像がある- 3月より前の回には、実機の写真がない。この頃はまだ Unity の中だけで動かしていた
見本の3本を、アップと引きで作り直した
最初の見本は、素材をそのまま 16:9 に収めただけだった。サムネイルらしく、アップと引きを使い分けるように言われ、3本を作り直した。
サムネイルに使うので、アップで作ったり、引きで作ったりとサムネイルらしい構成にしてください。

- ねらい:一覧で並んだときに、カードごとに見る所がはっきりするようにする
- 変わったこと:素材を画面いっぱいに縮めるのをやめ、爪先・箱・図の中心など、その回で見てほしい所に寄せた
- 見込める効果:一覧で同じ大きさの絵が続かなくなる。どれだけ記事が開かれるかは、サイトの計測で確かめる
実機の写真は元の大きさが 1920×1080 なので、爪先だけまで寄せると画質が落ちる。アップは LOG272 くらいの寄り方を上限にした。
本人が写った写真は、表紙に使わない
素材を集めている途中で、本人が写った写真は掲載しないように言われた。
俺の写真は掲載してはダメ!(もし記事内にあったら削除。)
候補の写真から、人の手が写ったもの(マウスを持つ手)と、カメラの確認で人が写り込んだものを外した。サイトに載っている記事の画像1,026枚と、サイトの中の画像326枚、記事の動画32本から3コマずつを、顔を見つける仕組み(OpenCV の YuNet)で調べた。色の付いた写真で顔が見つかったのは9枚で、人の写真はトップのページの動画の止め絵の1枚だった。これは記事ではなく、トップのページの自己紹介の欄のもので、記事の中には見つからなかった。4コママンガのキャラクターの顔も多く引っかかったが、絵なので外していない。
動画の1コマを表紙にする回でも、LOG113 は動画の画像にアームを持っている人の体が写っていたので、使わなかった。
図解の図の部分だけを、高い解像度で切り出した
図解は、記事の中に入れた HTML の図で、ブラウザで開くと動く。丸ごと撮ると、見出しや説明の文が多く、ターミナルの画面とあまり変わらない見た目になる。そこで、Playwright(ブラウザを自動で動かす道具)で図解を開き、図の部分(グラフ・弧・タイムライン・数字)だけを、切り出す幅に合わせて拡大して撮った。幅 560px の範囲を切るときは、1920px になるように約3.5倍で描かせている。画像を拡大するのではなく、図を大きく描かせてから撮るので、線や文字がにじまない。
切り出す位置は、図解を全体で撮って 100px ごとの目盛りを重ね、目で決めた(dclips.json)。図の中に絵がなく、文字のカードだけの図解は、その日の実機の写真を使った。
似た表紙が並ばないように、隣の回と見比べた
残りを作ってください。ただし同じようなデザインが並ばないように工夫してください。
シリーズのページの並び順(LOG の番号の大きい順)に、新しい表紙と、替えない回の表紙を並べた一覧の画像を作り、隣り合う3枚が同じ種類にならないかを見た。写真は、アップ・引き・横から撮った写真・2〜3枚を並べたもの、の4つの構図を回した。図解は、数字の寄り・グラフの寄り・全体の引きを混ぜ、明るい地の図と暗い地の図が続かないようにした。LOG283 は PICK の写真と横から撮った写真を左右に、LOG228 は手で教えた動きの3つのコマを縦の帯で並べた。
作業ログ ― 写真も図もない11本は、図解と同じ見た目で描き起こした
LOG21・22・23・29・32・36・74・75・77・104・105 は、ROS 2 の入門や、AI の道具の使い方の回で、写真も図解もない。最初は、大きな文字と太い線のポスターのような絵を描いた。

なんかサムネイルデザイン全然違いすぎない?
ほかの表紙は写真と記事の図解の一部なので、ポスター調の絵だけが一覧で浮いていた。記事の図解と同じ見た目にそろえて描き直した。地は #f7f7f7 のグレー、白いカード、字は #242424、角丸 8px、小さな英字のラベル。中身は記事に書いてあることだけで組んだ(ノードとトピックの流れ、3段の復旧、Gemini・自分・Cline の役割など)。
色が多い。差し色は1色か2色まで
LOG22 の X・Y・Z の軸(赤・緑・青)、LOG32 のコードの色分け(ピンク・黄・オレンジなど)、LOG36 のゲージ(緑・黄・赤)で、色が多くなっていた。差し色を青1色にし、2色目のオレンジは LOG36 の「警告」だけにした。LOG22 は軸を黒にし、上を向く Z だけを青にした。軸の横の文字も、赤・緑の色の名前から、ROS の決まりの向き(X が前・Y が左・Z が上)に書き換えた。

- ねらい:描き起こした表紙を、写真と図解でできたほかの表紙と同じ種類に見せる
- 変わったこと:ポスター調の絵から、記事の図解を切り出したような見た目にした。差し色を5色以上から青1色(警告だけオレンジ)にした
- 見込める効果:一覧で、描き起こした11本だけが目立たなくなる。見た目の判定はエイイチが一覧で行った
作業ログ ― HTML で作った表紙の17本も替えた
公開したあと、シリーズのページの上の段が前のままだと言われた。
サムネイルアップした?
上の段の LOG297〜LOG284 は、記事ごとに HTML で作った表紙だった。替える回を選んだとき、画像の表紙だけをダウンロードして並べていて、HTML の表紙は中身を見ずに外していた。頼まれたのは、写真や画像、記事の中の制作物を使った表紙に揃えることで、HTML の表紙も対象に入る。LOG250〜258 と LOG284〜297 の17本を、同じ方針で替えた。

LOG295 は記事の中のカメラの基板の写真、LOG254 は記事の中の FK テーブル(関節の角度と爪先の位置の対応表)の点の図を使った。LOG253 は、アームの土台を回す関節 j0 を左右に振ったときの写真を2枚並べた。
作業ログ ― 動画がある回は、動画の1コマを優先した
LOG99のサムネイルはYouTubeの画像を使ってサムネイルにして
動画の画像があれば優先的に使ってください。
記事の本文と図解の中から YouTube の埋め込みを探し、動画のある回を12本見つけた。YouTube が動画ごとに用意している画像(i.ytimg.com/vi/{動画の番号}/maxresdefault.jpg、1280×720)を取り、Unity の操作画面の枠・黒い帯・動画を作ったときに入った clideo.com の透かしが入らないように切り出した。

LOG113 は人の体が写っていたので、LOG71 は動画の画像が Unity とターミナルの画面全体で、アームがとても小さかったので、前の表紙のままにした。
作業ログ ― スマホでシリーズのページがスクロールできなかった
作業の途中で、iPhone で series2 のページが下へ動かないと言われた。
R2のSPページがスクロールできないので確認してください。
原因は、ページの「ドキュメント/事業構想」の欄だった。Google ドキュメントの公開版を iframe(ページの中に別のページをはめ込む枠)ごと埋め込んでいて、その中のドキュメントは幅 800px・高さ 1280px で固定されていた。スマホでは、指でなぞる操作をこの枠が受け取ってしまい、ページ全体が動かなかった。右側が切れて見えたのも、固定の幅のためだった。
ほかのシリーズの要件定義書と同じく、tools/fetch_series_docs.py で Google ドキュメントの中身(見出し・段落・表・太字・リンク)を取ってきて、ページに直接載せる形に替えた。iPhone 13 の画面の大きさで開き、ページが 1,496px から 2,296px まで動くことと、横にはみ出さないことを確かめた。

- ねらい:スマホでシリーズのページを最後まで読めるようにする
- 変わったこと:Google ドキュメントの埋め込み(幅 800px で固定)を、ページの文章として載せる形にした
- 見込める効果:スマホで開いた人が、記事の一覧まで下りられる。スマホからの記事の開かれ方は、サイトの計測で確かめる
作業ログ ― 素材を選ぶ順番
素材を選ぶ順番を、図にまとめた。回の種類を選ぶと、どの素材が選ばれるかと、その本数が出る。
作業ログ ― 画像をサイトに入れた手順
表紙の画像は、作業の環境で作り、Mac の iCloud の デジタルツイン学習R2R2R/R2R2R_サムネイル_20261108/ に置いてから、Mac の upload_media.py で Cloudflare R2(サイトの画像と動画を置く所)に入れた。サイトのデータの表紙の欄(cards.json と記事ごとの json の cover)を書き換え、ビルドして公開した。最初に描いた11枚は、同じフォルダの _不採用/ に移した。
画像の名前は、写真と図解が r2r2r-thumb-LOG{番号}.webp、描き起こした11本が r2r2r-thumb-v2-LOG{番号}.webp、動画の1コマが r2r2r-thumb-yt-LOG{番号}.webp。
R2 に入れたときの出力
入れる 42・入っている 0・名前の重なり 0・合計 4.8MB
42/42
おわり
入れる 11・入っている 42・名前の重なり 0・合計 0.3MB
11/11
おわり
入れる 11・入っている 53・名前の重なり 0・合計 0.4MB
11/11
おわり
入れる 17・入っている 64・名前の重なり 0・合計 2.3MB
おわり
入れる 1・入っている 81・名前の重なり 0・合計 0.1MB
おわり
入れる 9・入っている 82・名前の重なり 0・合計 0.9MB
おわり
公開した変更(p2m-site)
53668bc R2R2R(series2)のサムネイル42本を、写真・図解・制作物に差し替え
5661b93 R2R2R(series2)の素材のない11本に、図解と同じ見た目のサムネイルを付ける
358f63b R2R2R(series2)の HTML 表紙だった17本を、写真・図解のサムネイルに差し替え
66cc3b6 LOG99 のサムネイルを YouTube の動画の画像に差し替え
51b4aba series2 の事業構想を、Google ドキュメントの埋め込みからページに直接載せる形へ
37a3e37 R2R2R の YouTube 動画がある9本のサムネイルを、動画の画像に差し替え
公開したあとの確かめ
公開サイトのシリーズのページの HTML から、新しい表紙の画像の名前を数えた。
$ curl -s https://pixelstomotors.com/series2 | grep -o 'r2r2r-thumb-[a-z0-9-]*LOG[0-9]*' | sort -u | wc -l
70
$ curl -s https://pixelstomotors.com/series2 | grep -o 'thumb-yt-LOG[0-9]*' | sort -u | tr '\n' ' '
thumb-yt-LOG120 thumb-yt-LOG124 thumb-yt-LOG137 thumb-yt-LOG149 thumb-yt-LOG151 thumb-yt-LOG190 thumb-yt-LOG251 thumb-yt-LOG37 thumb-yt-LOG73 thumb-yt-LOG99
$ curl -s https://pixelstomotors.com/series2 | grep -c 'series2-1.html'
0
70本は、動画の1コマに替える前の数(42+11+17)。そのあと動画の10本のうち7本は前の表紙から替えたので、替えた回は全部で77本になる。
表紙の素材の一覧
| 素材 | 回 |
|---|---|
| 動画の1コマ(10) | LOG37・73・99・120・124・137・149・151・190・251 |
| 実機の写真(24) | LOG153・170・176・180・182・184・197・200・204・213・217・227・228・230・253・255・258・261・271・272・283・284・285・289 |
| 記事の中の画像(3) | LOG20・254・295 |
| 図解の一部(29) | LOG96・97・100・101・106・129・132・136・144・148・158・172・183・186・188・191・203・226・232・238・246・250・252・256・257・288・293・294・297 |
| 描き起こした図(11) | LOG21・22・23・29・32・36・74・75・77・104・105 |
| 前のまま | LOG71・113 と、画面写真が半々の回・もともと実機の写真だった回 |
作業ログ ― 表紙を作ったコード
この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。
ファイルの一覧(1本)
01_drawn_eleven_gen.py.txt ― drawn_eleven_gen.py(全文)― 描き起こした11本の HTML を書き出す・11,298字
実機の写真と動画の1コマは、Python の Pillow で切り出した(切り出す位置は、上の「表紙の素材の一覧」の回ごとに、写真に100px ごとの目盛りを重ねて目で決めた)。図解と描き起こした図は、次のファイルで作った。
survey.js(全文)― 図解を全体で撮り、図の要素の大きさを書き出す
const { chromium } = require('playwright'); const fs=require('fs');
const T=JSON.parse(fs.readFileSync('targets.json'));
(async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1100,height:900},deviceScaleFactor:1});const res={};
for(const [t,v] of Object.entries(T)){for(const [i,e] of v.emb.entries()){
try{await p.goto('file:///home/claude/p2m-site/public'+e,{timeout:15000});await p.waitForTimeout(700);
const els=await p.$$eval('svg,canvas,img',ns=>ns.map(n=>{const r=n.getBoundingClientRect();return {tag:n.tagName,x:r.x+scrollX,y:r.y+scrollY,w:r.width,h:r.height}}).filter(o=>o.w>200&&o.h>120));
const H=await p.evaluate(()=>document.documentElement.scrollHeight);
res[`${t}_${i}`]={e,els,H};
await p.screenshot({path:`emb/${t}_${i}.png`,fullPage:true});}catch(err){res[`${t}_${i}`]={e,err:String(err)}}}}
fs.writeFileSync('survey.json',JSON.stringify(res,null,1));await b.close();})();
dclips.json(全文)― 図解から切り出す位置(42本の回のうち図解の22本)
{"96":["96_0",200,150,420],"97":["97_0",214,90,450],"99":["99_0",330,150,440],"100":["100_0",200,190,700],
"101":["101_0",214,580,480],"106":["106_0",214,80,420],"129":["129_0",214,440,300],"132":["132_0",214,350,560],
"136":["136_0",214,95,672],"144":["144_0",214,330,560],"148":["148_0",214,430,672],"151":["151_0",214,820,672],
"158":["158_0",214,380,480],"172":["172_0",214,330,600],"183":["183_0",200,125,560],"186":["186_0",190,60,720],
"188":["188_0",214,640,600],"191":["191_1",250,290,620],"203":["203_0",150,240,760],"226":["226_0",214,200,672],
"238":["238_0",230,230,380],"246":["246_0",160,90,800]}
dclips_fix.json(全文)― 切り出す位置を直した4本
{"129":["129_0",214,290,560]}
dshot.js(全文)― 図解の図の部分を、切り出す幅に合わせて拡大して撮る
const { chromium } = require('playwright'); const fs=require('fs');
const C=JSON.parse(fs.readFileSync('dclips.json')); const S=JSON.parse(fs.readFileSync('survey.json'));
(async()=>{const b=await chromium.launch();
for(const [log,[key,x,y,w]] of Object.entries(C)){const dsf=Math.ceil(1920/w*10)/10;
const p=await b.newPage({viewport:{width:1100,height:1500},deviceScaleFactor:dsf});
await p.goto('file:///home/claude/p2m-site/public'+S[key].e);await p.waitForTimeout(900);
await p.screenshot({path:`out/raw_${log}.png`,clip:{x,y,width:w,height:w*9/16}});await p.close();}
await b.close();})();
c17.json(全文)― HTML の表紙だった回の切り出す位置
{"297": ["/embeds/shoot-one-frame-before-building-detection-AAopSn_m.html", "x", 214, 215, 560], "294": ["/embeds/handover-lost-the-incremental-loop-O8EFaTxo.html", "x", 214, 160, 560], "293": ["/embeds/handover-control-code-complete-cP7omSIK.html", "x", 214, 130, 560], "288": ["/embeds/handover-step1-design-Zl3l789a.html", "x", 214, 125, 560], "257": ["/embeds/why-z-correction-failed-twin-poses-EoiUe6St.html", "x", 230, 140, 620], "256": ["/embeds/data-map-before-real-run-IpfCyyU4.html", "x", 200, 165, 580], "252": ["/embeds/which-file-the-live-pick-reads-uusGw3qC.html", "x", 214, 170, 560], "250": ["/embeds/real-data-first-digital-twin-6tlwNejn.html", "x", 214, 75, 560], "251": ["/embeds/arcs-without-area-Sn9SA1es.html", "x", 0, 0, 1100]}
c17b.json(全文)― LOG251 の切り出す位置(点の雲の図)
{"251": ["/embeds/arcs-without-area-Sn9SA1es.html", "x", 200, 170, 640]}
c17c.json(全文)― LOG250・LOG252 の切り出す位置を直したもの
{"250": ["/embeds/real-data-first-digital-twin-6tlwNejn.html", "x", 200, 85, 700], "252": ["/embeds/which-file-the-live-pick-reads-uusGw3qC.html", "x", 214, 230, 560]}
s17.js(全文)― HTML の表紙だった回の図解を撮る
const { chromium } = require('playwright'); const fs=require('fs');
const C=JSON.parse(fs.readFileSync('c17.json'));
(async()=>{const b=await chromium.launch();
for(const [k,[e,_,x,y,w]] of Object.entries(C)){const p=await b.newPage({viewport:{width:1100,height:1400},deviceScaleFactor:Math.ceil(1920/w*10)/10});
await p.goto('file:///home/claude/p2m-site/public'+e);await p.waitForTimeout(900);
await p.screenshot({path:`out17_raw_${k}.png`,clip:{x,y,width:w,height:w*9/16}});await p.close();}
await b.close();})();
drawn_eleven_gen.py(全文)― 描き起こした11本の HTML を書き出す
H='''<!doctype html><html><head><meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
<style>*{margin:0;padding:0;box-sizing:border-box}
html,body{width:640px;height:360px;overflow:hidden;background:#f7f7f7}
body{font-family:'Inter','Noto Sans JP',sans-serif;color:#242424;-webkit-font-smoothing:antialiased}
.w{width:640px;height:360px;padding:28px 34px;position:relative;display:flex;flex-direction:column;justify-content:center}
.lab{font-family:'Inter',monospace;font-size:10px;letter-spacing:.14em;color:#888;text-transform:uppercase}
.h{font-size:17px;font-weight:700;margin:6px 0 16px}drawn_eleven_shot.js(全文)― 描き起こした11本を 1920×1080 で撮る
const { chromium } = require('playwright');
(async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:640,height:360},deviceScaleFactor:3});
for(const k of ['LOG21','LOG22','LOG23','LOG29','LOG32','LOG36','LOG74','LOG75','LOG77','LOG104','LOG105']){
await p.goto('file:///home/claude/work/t11b/'+k+'.html',{waitUntil:'networkidle'});await p.waitForTimeout(400);await p.screenshot({path:k+'.png'});}
await b.close();})();