デジタルツイン学習 R2R2R の中身を表紙で伝える ― サムネイル77本を動画と実写に

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

デジタルツイン学習 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本あった。ロボットアームを動かしているシリーズなのに、一覧を開くと黒い画面に白い文字ばかりが目に入る。

替える前のシリーズのページの一部。LOG213 から LOG190 のあたりで、ターミナルの画面写真やエラーの画面がカードの表紙に並んでいる

やったことの全体は次のとおり。

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

替えたあとのページでは、同じあたりにアームとブロックの写真や、図解の図が並ぶ。

替えた後のシリーズのページの同じあたり。LOG204・LOG200・LOG197 などが、実機の写真や図解の図になっている

ターミナルの画面が混ざった表紙は、まだ残っている。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本を作り直した。

サムネイルに使うので、アップで作ったり、引きで作ったりとサムネイルらしい構成にしてください。

作り直した見本。左から LOG20(アームのレンダーを小さく右に寄せた引き)、LOG232(弧の図の下半分に寄せた図の寄り)、LOG272(ブロックをつかんだ爪先に寄せ、右に置き先の白い箱を入れたアップ)
  • ねらい:一覧で並んだときに、カードごとに見る所がはっきりするようにする
  • 変わったこと:素材を画面いっぱいに縮めるのをやめ、爪先・箱・図の中心など、その回で見てほしい所に寄せた
  • 見込める効果:一覧で同じ大きさの絵が続かなくなる。どれだけ記事が開かれるかは、サイトの計測で確かめる

実機の写真は元の大きさが 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 の道具の使い方の回で、写真も図解もない。最初は、大きな文字と太い線のポスターのような絵を描いた。

最初に描いた11本。大きな漢字や太い線を使ったポスターのような絵で、写真と図解でできたほかの表紙から浮いている

なんかサムネイルデザイン全然違いすぎない?

ほかの表紙は写真と記事の図解の一部なので、ポスター調の絵だけが一覧で浮いていた。記事の図解と同じ見た目にそろえて描き直した。地は #f7f7f7 のグレー、白いカード、字は #242424、角丸 8px、小さな英字のラベル。中身は記事に書いてあることだけで組んだ(ノードとトピックの流れ、3段の復旧、Gemini・自分・Cline の役割など)。

色が多い。差し色は1色か2色まで

LOG22 の X・Y・Z の軸(赤・緑・青)、LOG32 のコードの色分け(ピンク・黄・オレンジなど)、LOG36 のゲージ(緑・黄・赤)で、色が多くなっていた。差し色を青1色にし、2色目のオレンジは LOG36 の「警告」だけにした。LOG22 は軸を黒にし、上を向く Z だけを青にした。軸の横の文字も、赤・緑の色の名前から、ROS の決まりの向き(X が前・Y が左・Z が上)に書き換えた。

公開した11本。左上から LOG21・22・23・29・32・36・74・75・77・104・105。グレーの地に白いカードで、差し色は青
  • ねらい:描き起こした表紙を、写真と図解でできたほかの表紙と同じ種類に見せる
  • 変わったこと:ポスター調の絵から、記事の図解を切り出したような見た目にした。差し色を5色以上から青1色(警告だけオレンジ)にした
  • 見込める効果:一覧で、描き起こした11本だけが目立たなくなる。見た目の判定はエイイチが一覧で行った

作業ログ ― HTML で作った表紙の17本も替えた

公開したあと、シリーズのページの上の段が前のままだと言われた。

サムネイルアップした?

上の段の LOG297〜LOG284 は、記事ごとに HTML で作った表紙だった。替える回を選んだとき、画像の表紙だけをダウンロードして並べていて、HTML の表紙は中身を見ずに外していた。頼まれたのは、写真や画像、記事の中の制作物を使った表紙に揃えることで、HTML の表紙も対象に入る。LOG250〜258 と LOG284〜297 の17本を、同じ方針で替えた。

HTML の表紙から替えた回。左上から LOG297・295・294・293・289・288・285・284・258・257・256・255・254・253・252・250。受け渡しのシリーズの回は、7月20日に撮った受け渡しの写真を使った

LOG295 は記事の中のカメラの基板の写真、LOG254 は記事の中の FK テーブル(関節の角度と爪先の位置の対応表)の点の図を使った。LOG253 は、アームの土台を回す関節 j0 を左右に振ったときの写真を2枚並べた。

作業ログ ― 動画がある回は、動画の1コマを優先した

LOG99のサムネイルはYouTubeの画像を使ってサムネイルにして

動画の画像があれば優先的に使ってください。

記事の本文と図解の中から YouTube の埋め込みを探し、動画のある回を12本見つけた。YouTube が動画ごとに用意している画像(i.ytimg.com/vi/{動画の番号}/maxresdefault.jpg、1280×720)を取り、Unity の操作画面の枠・黒い帯・動画を作ったときに入った clideo.com の透かしが入らないように切り出した。

動画の1コマにした10本。上の段の左から LOG37・73・99・120・124、下の段の左から LOG137・149・151・190・251

LOG113 は人の体が写っていたので、LOG71 は動画の画像が Unity とターミナルの画面全体で、アームがとても小さかったので、前の表紙のままにした。

作業ログ ― スマホでシリーズのページがスクロールできなかった

作業の途中で、iPhone で series2 のページが下へ動かないと言われた。

R2のSPページがスクロールできないので確認してください。

原因は、ページの「ドキュメント/事業構想」の欄だった。Google ドキュメントの公開版を iframe(ページの中に別のページをはめ込む枠)ごと埋め込んでいて、その中のドキュメントは幅 800px・高さ 1280px で固定されていた。スマホでは、指でなぞる操作をこの枠が受け取ってしまい、ページ全体が動かなかった。右側が切れて見えたのも、固定の幅のためだった。

ほかのシリーズの要件定義書と同じく、tools/fetch_series_docs.py で Google ドキュメントの中身(見出し・段落・表・太字・リンク)を取ってきて、ページに直接載せる形に替えた。iPhone 13 の画面の大きさで開き、ページが 1,496px から 2,296px まで動くことと、横にはみ出さないことを確かめた。

iPhone の大きさで開いたシリーズのページ。事業構想の文章が、枠ではなくページの中に直接出ている
  • ねらい:スマホでシリーズのページを最後まで読めるようにする
  • 変わったこと: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 を書き出す

01_drawn_eleven_gen.py.txt11,298字・89行・python
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();})();