サービスロボット導入シミュレーション Log#28 | 道の線を消すと、配膳ロボットの詰まりは3割しか拾えない

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

Unity で再現したファミレスの録画から、配膳ロボットの30秒以上の止まりを画像の異常検知(PatchCore)で見つける試しで、画面に描かれていたロボットの進む予定の道の線を消すと、拾えた割合は72%から28%に落ちました。ロボットどうしの詰まりは、線があるときは43件中23件を拾えたのに、線を消すと47件中4件しか拾えませんでした。

この試しで分かったことは、店に画像の見張りを入れる前の判断に使えます。

  • どの詰まりが画像で拾えて、どれが拾えないかが分かった。 お客さんとすれ違って止まった場面は、線がなくても17件中13件を拾えました。ロボットどうし・スタッフとの詰まりは、画像1枚ではほとんど拾えませんでした
  • シミュレーションの録画を、実際のカメラに近い見た目で撮れるようになった。 Unity の録画に、道の線を描かない切り替えを足しました。これからの録画は、この切り替えを付けて撮ります
  • かかった時間は、録画の撮り直しが約2時間(Mac が自動で)、切り出しが約9分、PatchCore で覚えさせて確かめるのが1通り約15分(クラウドの CPU 2コア、費用なし)。 店で失敗の映像を集めなくても、同じ手順で「どれが拾えるか」を先に確かめられます

きっかけは、良品の画像だけで覚えさせて、見たことのない傷を見つける異常検知の紹介でした。配膳ロボットの詰まりも、失敗の例が少なく、形もバラバラで集めにくい点が同じです。そこで、これまで比較動画の素材として撮りためていた Unity の録画で試しました。最初の試し(予備実験)では、長い止まりを8割前後拾えました。ところが、モデルが怪しいと見た所を画像に重ねると、ロボットとロボットの間に加えて、画面に描かれた道の線が交わる所が光っていました。実際の店のカメラには、この線は映りません。

画像のAIは、シミュレーションの画面にだけある表示を手がかりにしてしまう。線を消すと、ロボットどうしの詰まりはほとんど拾えなくなった。 シミュレーションで画像のAIを試すときは、実際のカメラに映るものだけで撮る必要があります。

やったことなぜ結果
Unity の録画から、配膳ロボットのまわり3m四方を切り出した
普段の様子だけで覚えさせ、止まりを見つけられるかを試すため
6回の録画から、線あり・線なしそれぞれ約2,200枚
PatchCore に普段の画像だけを覚えさせ、長い止まりを拾えるかを測った(予備実験)
失敗の例を集めなくても見つけられるかを知るため
誤報5%で長い止まりの82%を拾えたが、怪しい所が道の線にも出た
Unity の録画に、道の線を描かない切り替えを足して撮り直した
実際のカメラには線が映らないため
同じ条件の6回を、線なしで撮った
線あり・線なしで同じ手順を回して比べた
線に頼っていたかを確かめるため
拾えた割合が72%から28%に落ちた

下の図で、線あり・線なしを切り替えると、止まりの原因ごとに何件拾えたかが変わります。

店の図面から、シミュレーションの店を自動で起こす試し

店の寸法つきの図面があれば、配膳ロボットのシミュレーションに使う店の形(卓・通路・壁・受け渡し口の位置を数字で書いたデータ)は、人の手直しなしで起こせました。Unity とサイトの試算で使っているファミレス②を寸法つきの平面図に描き直し、そのデータを見ていない別の担当に図面だけから起こさせると、19卓・88席とも同じになり、19卓とも位置のずれは0.0mでした。

  • 店から図面が届けば、シミュレーションの店を作る作業の大半を自動にできる。今の②は、手描きの見取り図から起こしたあと、人の実感で何度も直して作った店です(その手間は測っていません)
  • 店の人に聞くことが減る。寸法つきの図面なら、聞くことはロボットの待機場所と台数の1つまで減りました。手描きの見取り図では、店の幅・受け渡し口・ドリンクバー・待機場所・店の形の5つが要りました
  • 手描きの見取り図でも、店の幅を1か所だけ教えれば、決まりの違反は0件で、18卓すべてにロボットが届く店になりました

手描きの見取り図で一番困ったのは、縮尺が読めないことでした。手で描くと家具が部屋に対して大きめになるので、家具の大きさから縮尺を決めると、店が半分ほどの広さに読まれます。通路が狭くなり、ロボットがふだんの道で届く卓は18卓中3卓だけでした。

寸法が1つでも分かれば、図面から起こした店は、ロボットの届き方まで人が直した店とほぼ同じになった。図面から分からないのは、ロボットの待機場所のように、店がまだ決めていないことだった。

図面店の人に聞かずに起こした結果店の人に聞くこと
手描きの見取り図(寸法なし)
店が半分の広さに読まれ、ふだんの道でロボットが届くのは18卓中3卓。店の幅を1か所教えると、違反0件・18卓すべてに届いた
店の幅・受け渡し口・ドリンクバー・待機場所・店の形の5つ
②を寸法つきで一から描いた平面図
最初に起こした時点で違反0件。19卓・88席、19卓とも位置のずれ0.0m
ロボットの待機場所と台数の1つ
別に手に入れた寸法つきの図面
縮尺の問題は消えたが、斜めに置いた卓・半円のブース・カウンター席が、今の家具の種類では表せなかった
ロボットが料理を受け取る所と待機場所、店員の通路を通ってよいか、ベンチの席数など

別に手に入れた寸法つきの図面は、出典を確かめられていないので、図面も、そこから起こした図も載せていません。

左はファミレス②を寸法つきで描いた平面図。右は、②のデータを見ていない別の担当がこの図面だけから起こした店の形で、オレンジの線は受け渡し口からロボットが卓へ向かう道

配膳ロボットを自分の店に入れたらどうなるかは、このシリーズの試算をもとにした別のページで試せます。

店の立体図(席・キッチン・配膳ロボットの通り道)導入前シミュレーション配膳ロボットを、導入前に自分の店舗でシミュレーション店舗の図面と人数から、何人で回るか・どこで止まるか・月いくらかかるかを出します

作業ログ ― 配膳ロボットの止まりを、画像の異常検知で見つける試し

ここから下は作業ログです。コード・出力・結果の数字を全部載せています。効果の数字は、どれも測った値です。

PatchCore は、普段の画像だけを覚えて、違う所を見つける仕組み

PatchCore は、画像の異常検知(いつもと違う所を見つける方法)の1つです。工場の検査で、傷のない良品の画像だけを覚えさせ、見たことのない傷を見つけるのに使われています。きっかけになったのは、Roboflow(画像のAIを作って動かすサービス)が異常検知に対応し、良品のオレンジの画像237枚だけで覚えさせて腐れや傷を見つけた、という紹介でした。

この記事で使う言葉意味
普段の画像
ロボットがふつうに働いている所の切り出し。PatchCore にはこれだけを覚えさせる
点数
覚えた普段の画像と、どれだけ違うか。大きいほど怪しい
怪しいと見た所
1枚の画像のうち、点数が高かった場所。画像に色で重ねて見る
AUROC
2つの組(例: 長い止まりと普段)を、点数でどれだけ見分けられるか。1なら完全に見分け、0.5ならでたらめと同じ
誤報5%での拾えた割合
普段の画像のうち5%だけが怪しいと出るしきい値にしたとき、長い止まりのうち何割を怪しいと出せたか

中身は単純です。画像を、ImageNet(一般の写真)で学習済みの WideResNet50 という画像のモデルに通し、1枚を28×28の小さな区画に分けて、区画ごとの特徴を取り出します。普段の画像の区画の特徴を、代表だけ残して覚えておきます。新しい画像の区画ごとに、覚えた中で一番近い区画までの距離を測り、その最大を1枚の点数にします。新しく学習させる部分がないので、CPU でも1通り15分ほどで回ります。

このプロジェクトでは、配膳ロボットの止まりに当てました。ファミレスで配膳ロボットが止まる原因は、ロボットどうしの向かい合い、人とのすれ違い、人の立ち止まりなどで、形がバラバラです。失敗の例を集めて教えるのは大変なので、普段の画像だけで覚えさせられるなら、店に入れたあとの見張りに使える見込みがあります。

Unity の録画と、止まりの記録の時刻・位置はそろっていた

素材は、比較動画を作るために撮りためていた Unity の録画です。Unity で再現したファミレス(③ 効率重視・入口が角)を真上から撮り、店の時間0.5秒ごとに1コマ、横2,560画素で、回の最初から最後のお客さんが帰るまでを録っています。

Unity で再現したファミレスを真上から撮った録画の1コマ。白い丸が配膳ロボット、黄色がお客さん、青がスタッフ

録画には、何コマ目が店の何秒目かの表(_frames.csv)が付いています。止まりの記録(stops.csv)には、止まった時刻・位置・秒数・原因が1回ずつ入っています。突き合わせると、次のことが分かりました。

  • 位置と画面:③ は1m=100画素。店の外寸に0.8mずつの余白があるので、記録の位置から画面の場所を計算できる
  • 止まりの記録と録画の時刻はそろっている。20秒以上の止まり27件のうち、その場所にほかのロボットがいなかった17件で、録画にロボットが着いた時刻が記録の0〜1.5秒以内(16件は0.5秒以内)だった
  • 走っている時の道の記録(robot_track.csv、5秒ごと)は、位置が1m前後ずれる。普段(走っている)の画像は、記録の位置から1.5m以内で一番近いロボットを録画の中で探し、その位置を中心に切り出した。見つからない3〜4割は使っていない

切り出しは、普段2種類と止まり2種類

ロボットのまわり3m四方を切り出し、224×224画素にしました。

切り出しの4種類。左から、走っているロボット、待機場所で待つロボット、3〜30秒の止まり、30秒以上の止まり
種類何を切り出したかPatchCore での扱い
普段(走っている)
卓や受け渡し口へ向かう途中で、5秒に0.5m以上動いた時刻
覚えさせる・確かめる
普段(止まっている)
待機場所で待っている時刻と、卓で料理を渡している時刻
覚えさせる・確かめる
短い止まり
3〜30秒の止まりの、始まりから3秒後
確かめるだけ
長い止まり
30秒以上の止まり(このシリーズで立ち往生と数える線)の、始まりから15秒後
確かめるだけ

普段に「止まっている」を入れたのは、止まっているだけで怪しいと出ないようにするためです。長い止まりを始まりから15秒後で切り出したのは、30秒になる前に見つけられるかを見るためです。

覚えさせる回と確かめる回は、回ごとに分けました。隣のコマどうしはほぼ同じ絵なので、同じ回を両方に入れると、答えを覚えているのと変わらなくなるためです。

分け方
③ の回(条件・ロボットの台数)
覚えさせる3回
普通・真ん中の人数・1台/空いている・2台/スタッフが多い・2台
確かめる3回
普通・真ん中の人数・2台/スタッフが少ない・2台/混んでいる・2台

予備実験では長い止まりの82%を拾えたが、怪しい所が道の線にも出た

最初の試しは、前の素材(2026-10-30 の夜に撮った cs:49 の録画)で行いました。1コマの切り出し(A)と、10秒前・5秒前・その時刻の3コマを白黒にして重ねた切り出し(B)の2通りです。B は、動いていない所が灰色、動いている所に色がにじむ画像で、止まっていることを画像に入れるために試しました。

A:1コマB:3コマ重ね
確かめる枚数(普段・短い止まり・長い止まり)
785・339・77785・339・77
長い止まりと普段の見分け(AUROC)
0.920.87
長い止まりと 待機・卓で止まる の見分け
0.950.89
長い止まりと短い止まりの見分け
0.700.62
誤報5%で長い止まりを拾えた割合
82%(63/77)60%(46/77)

A の原因ごと(拾えた数)は、もう1台のロボット 26/36、人が立ち止まっていた 13/15、お客さんとすれ違った 14/14、スタッフとすれ違った 10/12 でした。動きを入れた B の方が悪く、以降は A だけで比べています。

予備実験のまま進めずに止まったのは、怪しいと見た所を画像に重ねたときです。点数の高い長い止まりの多くで、ロボットとロボットの間に加えて、画面に描かれた道の線(ロボットが進む予定の道を、だいだいと水色の細い線で描いたもの)が交わる所が光っていました。この線は、Unity の画面で動きを確かめるために描いていたもので、実際の店のカメラには映りません。

予備実験の切り出し・コード・結果は KIOXIA の vision/measurements/Log12/ に置いています(画像認識シリーズの置き場所に置いてしまったもので、元はそのまま残し、比べの材料は service_sim/vision_LOG418/ に置きました)。

作業ログ ― 道の線を描かない録画を撮り、同じ手順で比べた

Unity の録画に、道の線を描かない切り替え -ssRecHidePaths を足した

Unity の作業は Claude Code の担当なので、依頼箱(Claude Code への依頼を Mac が自動で渡す仕組み)に依頼を置きました。比較動画の素材の撮り直し(依頼43)が終わったあとに、続けて動く順番です。

依頼51:ロボットの道の線を消す録画の切り替えを足し、③の6回を線なしで撮る(全文)

---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 180
---
# 依頼50:ロボットの道の線を消す録画の切り替えを足し、③の6回を線なしで撮る(夜・依頼43のあと)

エイイチと決めたこと:画像認識シリーズの予備実験(KIOXIA の vision/measurements/Log12/README.md)で、比較動画の素材(20261030_rec)から配膳ロボットのまわりを切り出し、普段の画像だけで覚えさせた PatchCore で長い止まりを見つけられるかを試した。怪しいと見た所が、ロボットの進む予定の道の線(青・オレンジの細い線)が交わる所にも出ていた。実際の店のカメラには線がないので、線を消した録画で同じ比べをする。
**エイイチは寝ている。人に聞かないと進めない所に当たったら、その作業は止めて報告に書き、ほかの作業は続ける。**

## 始める前に確かめること(違えば回さずに報告して終わる)

1. 依頼43(比較動画の素材の撮り直し・night/20261031_rec)が終わっている。終わっていなければ、待たずに報告して終わる
2. Unity エディタが起動していない・ほかの night_runner/rec_runner が動いていない
3. /Volumes/KIOXIA がマウントされている。unity_ledger_v1.lock がない
4. ServiceSim の保留が 0

## 1. 録画だけの切り替えを足す

- 録画の引数に、ロボットの進む予定の道の線を描かない切り替えを足す(名前は任せる。例:`-ssRecHidePaths`)。付けなければ今までどおり描く
- 消すのは画面に描く線だけ。道の引き方・人とロボットの動き・速さ・物理の設定・記録の中身は変えない
- 人やスタッフの道の線など、ほかにも動きに合わせて描かれる線があれば一緒に消す。床の塗り・通路の印・ロボットが入れない所の印など、動かない表示は変えない。消したもの・残したものを報告に書く
- 確かめ:③(fam_p1)・普通・真ん中の人数・ロボット2台・種7 を、店の 0〜300 秒だけ、切り替えなし・ありで1回ずつ撮る。店の 150 秒のコマを1枚ずつ png で `/Volumes/KIOXIA/r2r2r-data/vision/generated/Log12/preview/` に `nopath_check_off.png`・`nopath_check_on.png` として置く
- check-in する。録画用の実行ファイルを作り直し、build_info の pending_changes が空であることを確かめる

## 2. ③の6回を線なしで撮る

依頼43 の ③ のロボットありの6回と**同じ条件・同じ種(7)・同じ映し方**で、線を消す切り替えだけを付けて撮る。

| 条件 | ロボット |
|---|---|
| 普通・真ん中の人数 | 1台・2台 |
| スタッフが少ない | 2台 |
| 混んでいる | 2台 |
| スタッフが多い | 2台 |
| 空いている | 2台 |

- 映し方:真上・全体(外寸に0.8mの余白)・幅2560・0.5秒ごと・30コマ/秒・回の最初から全員が帰るまで。1コマごとの店の時刻の表(_frames.csv)も今までどおり書く
- 回し方:依頼43 と同じ rec_runner のやり方。名札は `rec_nopath`(混み具合・スタッフの人数の付け方は今までどおり。例 `rec_nopath_st3`)、夜の名前は 20261101。記録は night/20261101_rec_nopath/
- 同時に2本。時刻で止めず全部終わるまで回す。途中で止まっても回し直さない
- Mac をスリープさせない。回し終わったら、スリープの設定を戻す前に報告と一覧を書き終える

## 変えないもの

- 店の回し方・人とロボットの動き・速さ・物理の設定
- 台帳の列と今までの行。20261030_rec・20261031_rec とその動画

## 報告

依頼箱の結果に加えて、claude_code_reports に 20261101_rec_nopath.txt として書く。
- 始める前の確かめ4つ
- 足した切り替えの名前と、消したもの・残したもの。cs 番号・build_id
- 確かめの png 2枚の置き場所
- 6回の結果(completed の数・失敗・止まった回・最長の実時間)と、night/20261101_rec_nopath/rec_index.csv の全文(列は依頼43 の rec_index.csv と同じ)
- 依頼43 の ③ の同じ条件の回との比べ(入店待ち・席は空いているのに待った・料理の待ち・会計待ち・動けなくなった)。6組を表で。線を消すのは見た目だけなので、大きく違えば原因を調べずにそのまま書く

依頼の中の番号は「依頼50」のままですが、依頼箱でアーム側の依頼と番号が重なったため、依頼箱が自動で51に付け替えて回しました。

Claude Code の報告:消したのはロボットの道の線だけ

依頼51の結果は115分で返ってきました。Claude Code の報告をそのまま載せます。

依頼51の結果(依頼箱の結果ファイル・Claude Code の報告の部分)

## Claude Code の報告

**やったこと**
- 始める前の確かめ4つは、すべて合格しました。
  - 依頼43は 11-01 06:39 に終わっていました。
  - Unity エディタ・night_runner・rec_runner は動いていませんでした。
  - KIOXIA はマウントされていて、unity_ledger_v1.lock もありませんでした。
  - 保留は 0(cs:52)でした。
- 録画の引数に `-ssRecHidePaths` を足しました。check-in は cs:53 です。録画用の実行ファイルを Builds/rec7_nopath53 に作り直しました。build_id は cs53_20261101_0715、pending_changes は空です。
  - 消したもの:ロボットの進む予定の道の線(だいだい・水色)だけです。
  - 残したもの:床の塗り・通路の印・入口の印・ロボットが入れない所の印・人とロボットの体です。
  - 人やスタッフの道の線は、もともと描いていませんでした。
- 確かめの2回(③・普通・真ん中の人数・2台・種7・店の 0〜300 秒)を撮り、店の 150 秒のコマを png にしました。置き場所は `/Volumes/KIOXIA/r2r2r-data/vision/generated/Log12/preview/` の `nopath_check_off.png`・`nopath_check_on.png` です。
- ③の6回を、依頼43と同じ条件・同じ種・同じ映し方で、線を消す切り替えだけを付けて撮りました。記録は night/20261101_rec_nopath/ です。

**結果**
- 確かめの png 2枚は、まったく同じ絵になりました。この回のロボットの最初の往復は店の 856 秒からで、0〜300 秒には線がもともと出ていないためです。
  - 代わりに店の 870 秒の切り抜きで比べました。依頼43の回にはだいだいの線があり、今回は無いことを目で確かめました。
  - 切り抜きの置き場所は night/20261101_rec_nopath_check/ です。
- 6回とも completed でした。失敗 0、止まった回 0、やり直し 0、全員が帰っています。
  - 最長の実時間は 29.1 分(rec_nopath_a54 の回)、平均は 24.3 分でした。依頼43の平均 14.0 分より長く、理由は調べていません。
  - 動画6本の合計は 265,888,344 バイトです。コマ数と _frames.csv は合っています。
- 台帳は 1526 → 1534 行です(確かめ2回+本番6回を追記)。前の行の md5 は控えと同じです。
- 依頼43との比べ(依頼43 → 今回):

| 条件 | ロボット | 入店待ち(分) | 席は空いているのに待った(分) | 料理の待ち(分) | 会計待ち(分) | 動けなくなった(回) |
|---|---|---|---|---|---|---|
| 普通・真ん中の人数 | 1台 | 3.16→4.04 | 0.89→1.75 | 2.36→2.50 | 10.23→8.81 | 0→7 |
| 普通・真ん中の人数 | 2台 | 3.42→2.99 | 1.35→0.81 | 1.84→1.94 | 8.79→11.85 | 20→35 |
| スタッフが少ない | 2台 | 12.41→13.23 | 9.90→10.85 | 2.92→2.05 | 3.65→4.50 | 34→31 |
| 混んでいる | 2台 | 14.11→14.02 | 3.82→2.51 | 2.17→3.63 | 8.59→8.88 | 11→6 |
| スタッフが多い | 2台 | 2.98→2.72 | 0.90→0.69 | 1.93→1.79 | 11.42→10.87 | 23→19 |
| 空いている | 2台 | 1.30→1.35 | 0.08→0.17 | 1.38→2.56 | 3.99→5.05 | 3→6 |

  線を消すのは見た目だけのはずですが、数字は回ごとに違いました(動けなくなった回数が 0→7、20→35 など)。原因は調べていません。
- rec_index.csv の全文は報告ファイルに載せました。
- Mac のスリープの設定は、報告と一覧を書き終えてから戻しました。

**変えたファイル**
- ServiceSim(cs:53):
  - Assets/ServiceSim/Recording/SimTimeRecorder.cs
  - Assets/ServiceSim/Robot/RobotPathLine.cs
- 実行ファイル:
  - Builds/rec7_nopath53(本番用)
  - Builds/rec7_nopath_pre(check-in 前のコンパイル確かめ用。使っておらず、消していません)
- KIOXIA:
  - 記録:night/20261101_rec_nopath/、night/20261101_rec_nopath_check(_off・_on も)
  - 各回のフォルダ8つ
  - 台帳への追記8行
  - 台帳の控え:archive/unity_ledger_v1_prerec_20261101/
  - 確かめの png 2枚
- 報告:claude_code_reports/20261101_rec_nopath.txt

**決めてほしいこと**
- 確かめの png を、線が出る時刻(店の 856 秒より後)で撮り直すかどうか。依頼どおりの 150 秒では、切り替えなし・ありで同じ絵になります。
- Builds/rec7_nopath_pre を消してよいか。

確かめの画像2枚が同じ絵になった件は、報告のとおり、店の0〜300秒にはロボットがまだ動いておらず、線がもともと描かれない場面だったためです。撮り終わった本番の録画で、走っているロボットのまわりに線が出ていないことを、店の1,325秒と2,315秒のコマで目で確かめました。

同じ通路を走る配膳ロボットの切り出し。上の線ありの録画では、ロボットの後ろに水色やだいだいの道の線が出ている。下の線なしの録画には出ていない
  • ねらい:Unity の録画を、実際の店のカメラに映るものだけで撮れるようにする
  • 変わったこと:録画の引数に -ssRecHidePaths を付けると、ロボットの進む予定の道の線(だいだい・水色)を描かない。付けなければ今までどおり描く。人とロボットの動き・記録の中身は変えていない
  • 見込める効果:これから録画で画像のAIを試すとき、Unity にしかない表示を手がかりにされずに済む。この記事の比べでは、線を消すと長い止まりを拾えた割合が72%から28%に変わり、線がどれだけ効いていたかを測れた

同じ条件・同じ種でも、Unity の回は毎回少し違った

報告の表のとおり、線を消すのは見た目だけなのに、6回とも数字が少しずつ違いました(③ 普通・2台で、ロボットが動けなくなった回数が20回から35回など)。録画の撮り直しの前と後でも同じことが起きており、Unity の回は同じ条件・同じ乱数の種でも毎回同じにはならないと見ています。原因はまだ調べていません。

そのため、この比べでは同じ場面どうしを並べず、線あり6回と線なし6回を、それぞれ同じ切り出し方・同じ分け方で数として比べました。確かめる3回の長い止まりの数は、線あり93件・線なし94件で、ほぼ同じでした。

切り出した枚数
覚えさせる3回(普段・短い・長い)
確かめる3回(普段・短い・長い)
線あり(依頼43の撮り直し、cs:52)
778・260・37747・317・93
線なし(依頼51、cs:53)
794・233・40765・295・94

覚えさせたのは普段の画像だけです(短い・長い止まりは覚えさせていません)。

線を消すと、長い止まりを拾えた割合は72%から28%に落ちた

線あり線なし
長い止まりと普段の見分け(AUROC)
0.920.77
長い止まりと、走っているロボットの見分け
0.890.73
長い止まりと、待機・卓で止まるロボットの見分け
0.950.81
長い止まりと短い止まりの見分け
0.640.51
誤報5%で長い止まりを拾えた割合
72%(67/93)28%(26/94)
同じしきい値で短い止まりを拾った割合
49%19%
普段(走っている)の誤報7.5%4.9%
普段(待機・卓で止まる)の誤報
2.5%5.3%

長い止まりと短い止まりの見分けは、線なしで0.51でした。止まりが30秒以上続くかどうかは、画像1枚からはまったく分からないということです。

線がないと、ロボットどうしの詰まりとスタッフとのすれ違いは拾えない

長い止まりの原因線あり(拾えた数)線なし(拾えた数)
もう1台のロボット23/434/47
スタッフとすれ違った20/213/18
お客さんとすれ違った12/1413/17
人が立ち止まっていた12/146/11
分からない0/10/1

お客さんとすれ違った止まりだけは、線がなくてもほぼ同じだけ拾えました。白いロボットのすぐそばに黄色いお客さんがいる絵は、普段の画像に少ないためと考えています。ロボットどうしが向かい合っている絵は、白い丸が2つ並ぶだけで、待機場所で2台が並んでいる普段の絵と区別がつきにくくなります。線があるときは、2台の線が交わる形がその区別を担っていました。

長い止まりのうち、ロボットどうしが原因で点数の高かった3つずつ。上の線ありでは道の線の上が光り、下の線なしではロボットのまわりの人の所が光っている

怪しいと見た所を重ねると、違いがはっきり見えます。線ありでは、だいだい・水色の線の上が光っています。線なしでは、ロボットの近くにいるスタッフ(青)の所が光っています。線なしで点数が高かったロボットどうしの詰まりも、2台の間ではなく、近くの人を見て高くなっていた可能性があります。

作業ログ ― 切り出し・PatchCore・集計のコードと出力(全文)

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

ファイルの一覧(12本)

01_make_crops_v3.py.txt ― 切り出しのスクリプト make_crops_v3.py(全文)・4,584字

02_patchcore.py.txt ― PatchCore で覚えさせて点数を付ける patchcore.py(全文)・4,641字

03_TASK.md ― 手描きの見取り図で別の担当に渡した指示 TASK.md(全文)・3,129字

04_build.py.txt ― 手描きの見取り図から店の形を起こすスクリプト build.py(全文)・5,583字

05_notes.md ― 手描きの見取り図を起こした別の担当の記録 notes.md(全文)・5,556字

06_draw_plan.py.txt ― 寸法つきの平面図を描く draw_plan.py(全文)・5,530字

07_TASK.md ― 寸法つきの図面で別の担当に渡した指示 TASK.md(全文)・3,456字

08_work_build_v0.py.txt ― 寸法つきの図面から店の形を起こす work/build_v0.py(全文)・3,989字

09_notes.md ― 寸法つきの図面を起こした別の担当の記録 notes.md(全文)・4,895字

10_auto_final.json ― 手描きの見取り図から起こした店の形 auto_final.json(全文)・4,766字

11_auto_scaled_w24.json ― 店の幅24mだけ教えて広げた店の形 auto_scaled_w24.json(全文)・4,936字

12_auto_final.json ― 寸法つきの図面から起こした店の形 auto_final.json(全文)・5,622字

回した環境と順番

段どこで使ったものかかった時間
切り出し
Mac 連携の作業環境(Linux、4コア)
Python 3・OpenCV・NumPy
3回ずつ並べて 1分47秒・2分21秒・2分04秒・2分48秒(計約9分)
PatchCore
クラウドの作業環境(CPU 2コア・メモリ7GB・GPU なし)
PyTorch 2.15(CPU 版)・torchvision 0.29.2・WideResNet50(ImageNet の重み)・scikit-learn
線あり約18分・線なし約13分
集計・図クラウドの作業環境
analyze.py・heat2.py
数秒

費用はかかっていません。乱数の種はすべて0です。

# Mac:切り出し(線ありの覚えさせる3回の例。確かめる3回と線なしも同じ形で回した)
cd ~/work
cat > jobs_on.txt <<'E'
20261031_rec_cs52_fam_p1_r1_s7_passon_sbret_ek_post train
20261031_rec_a30_cs52_fam_p1_r2_s7_passon_sbret_ek_post train
20261031_rec_st5_cs52_fam_p1_r2_s7_passon_sbret_ek_post train
E
cat jobs_on.txt | xargs -P 3 -L 1 sh -c 'python3 make_crops_v3.py $0 $1 crops_on'

# クラウド:PatchCore と集計
python3 patchcore.py crops_on A res_on > run_on.log 2>&1
python3 patchcore.py crops_off A res_off > run_off.log 2>&1
python3 analyze.py A crops_on res_on
python3 analyze.py A crops_off res_off
python3 heat2.py LOG418_02_heatmap_on_off.png

切り出しの出力(Mac)

rec_a30_r2 train Counter({'normal_move': 138, 'normal_idle': 120, 'short': 88, 'long': 5}) move dropped 112
rec_st5_r2 train Counter({'normal_move': 145, 'short': 120, 'normal_idle': 120, 'long': 28}) move dropped 105
rec_r1 train Counter({'normal_move': 135, 'normal_idle': 120, 'short': 52, 'long': 4}) move dropped 115

real	1m47.486s
user	6m40.377s
sys	0m11.920s

rec_r2 test Counter({'normal_move': 136, 'normal_idle': 120, 'short': 112, 'long': 28}) move dropped 114
rec_st3_r2 test Counter({'normal_idle': 120, 'normal_move': 107, 'short': 85, 'long': 44}) move dropped 143
rec_a54_r2 test Counter({'normal_move': 144, 'short': 120, 'normal_idle': 120, 'long': 21}) move dropped 106

real	2m21.712s
user	8m40.483s
sys	0m14.973s

rec_nopath_a30_r2 train Counter({'normal_move': 151, 'normal_idle': 120, 'short': 71, 'long': 7}) move dropped 99
rec_nopath_r1 train Counter({'normal_move': 136, 'normal_idle': 120, 'short': 42, 'long': 9}) move dropped 114
rec_nopath_st5_r2 train Counter({'normal_move': 147, 'short': 120, 'normal_idle': 120, 'long': 24}) move dropped 103

real	2m4.600s
user	7m36.261s
sys	0m12.544s

rec_nopath_r2 test Counter({'normal_move': 140, 'short': 120, 'normal_idle': 120, 'long': 45}) move dropped 110
rec_nopath_a54_r2 test Counter({'normal_move': 151, 'normal_idle': 120, 'short': 60, 'long': 13}) move dropped 99
rec_nopath_st3_r2 test Counter({'normal_idle': 120, 'short': 115, 'normal_move': 114, 'long': 36}) move dropped 136

real	2m48.552s
user	8m15.489s
sys	0m16.364s

move dropped は、走っている所の候補250のうち、録画の中でロボットが見つからずに使わなかった数です。

切り出しのスクリプト make_crops_v3.py(全文)

01_make_crops_v3.py.txt4,584字・82行・python
# LOG418 線あり・線なしの比べ(v3:回・分け方・出力先を引数で渡す。中身は v2 と同じ)
# 使い方: python3 make_crops_v3.py <回の名前> <train|test> <出力フォルダ>
# 元: Log12 予備実験 v2:Unity の録画から、配膳ロボットのまわり3m四方を切り出す
# 普段(走っている) = 往復の途中で5秒に0.5m以上動いた時刻。道の記録は走っている時に1m前後ずれるので、
#   記録の位置から1.5m以内で一番近いロボットを録画の中で探し、その位置を中心にする
# 普段(止まっている) = 待機場所で待っている・卓で料理を渡している時刻(ふつうの止まり)
# 短い止まり = 3〜30秒の止まりの、始まりから3秒後/長い止まり = 30秒以上の止まりの、始まりから15秒後
# A = その時刻の1コマ/B = 10秒前・5秒前・その時刻の3コマを白黒にして重ねた画像
別のタブで開く保存

PatchCore で覚えさせて点数を付ける patchcore.py(全文)

02_patchcore.py.txt4,641字・100行・python
# Log12 予備実験:PatchCore(WideResNet50)で配膳ロボットの長い止まりを見つけられるか
# 覚えさせるのは「普段」(往復の途中で走っている所)の切り出しだけ。
# 確かめは、別の回の 普段・短い止まり(3〜30秒)・長い止まり(30秒以上、始まりから15秒後)。
# 使い方: python3 patchcore.py <crops のフォルダ> <A か B> <結果のフォルダ>
import sys, os, glob, json, csv, random
import numpy as np, torch, torch.nn.functional as F
from torchvision.models import wide_resnet50_2, Wide_ResNet50_2_Weights
from PIL import Image
別のタブで開く保存

普段の種類・原因ごとに分けて数える analyze.py(全文)

import csv,glob,json,sys,numpy as np
from sklearn.metrics import roc_auc_score
kind=sys.argv[1]
idx={}
for f in glob.glob(sys.argv[2]+'/index/*.csv'):
    for r in csv.DictReader(open(f)):
        if r['split']=='test': idx[r['file']]=r
sc=[(r['label'],r['file'],float(r['score'])) for r in csv.DictReader(open(sys.argv[3]+f'/scores_{kind}.csv'))]
g={}
for lab,f,s in sc:
    sub=idx[f]['label'] if lab=='normal' else lab
    g.setdefault(sub,[]).append((s,idx[f]))
thr=json.load(open(sys.argv[3]+f'/summary_{kind}.json'))['threshold_p95_normal']
def auc(a,b): return roc_auc_score([0]*len(a)+[1]*len(b),list(a)+list(b))
mv=[s for s,_ in g['normal_move']]; idl=[s for s,_ in g['normal_idle']]; lg=[s for s,_ in g['long']]; sh=[s for s,_ in g['short']]
out={'kind':kind,'n':{k:len(v) for k,v in g.items()},
 'auroc_long_vs_move':auc(mv,lg),'auroc_long_vs_idle':auc(idl,lg),'auroc_short_vs_idle':auc(idl,sh),
 'false_alarm_move':float(np.mean(np.array(mv)>thr)),'false_alarm_idle':float(np.mean(np.array(idl)>thr)),
 'idle_by_phase':{}, 'long_hit_by_cause':{}, 'short_hit_by_cause':{}}
for s,r in g['normal_idle']: out['idle_by_phase'].setdefault(r['cause'],[]).append(s>thr)
out['idle_by_phase']={k:f'{sum(v)}/{len(v)}' for k,v in out['idle_by_phase'].items()}
for lab in ('long','short'):
    d={}
    for s,r in g[lab]: d.setdefault(r['cause'],[]).append(s>thr)
    out[f'{lab}_hit_by_cause']={k:f'{sum(v)}/{len(v)}' for k,v in sorted(d.items(),key=lambda a:-len(a[1]))}
print(json.dumps(out,ensure_ascii=False,indent=1))
json.dump(out,open(sys.argv[3]+f'/breakdown_{kind}.json','w'),ensure_ascii=False,indent=1)

怪しいと見た所を画像に重ねる heat2.py(全文)

# LOG418:線あり・線なしで、長い止まり(もう1台のロボットが原因)の点数が高かった例に、怪しいと見た所を重ねる
import numpy as np, csv, glob, sys
from PIL import Image, ImageDraw, ImageFont
import matplotlib; matplotlib.use('Agg'); import matplotlib.cm as cm
f=ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',22)
rows=[]
for kind,crops,res in (('線あり','crops_on','res_on'),('線なし','crops_off','res_off')):
    m=np.load(f'{res}/maps_A.npz'); idx={}
    for g in glob.glob(f'{crops}/index/*.csv'):
        for r in csv.DictReader(open(g)): idx[r['file']]=r
    sc=[(float(r['score']),r['file']) for r in csv.DictReader(open(f'{res}/scores_A.csv')) if r['label']=='long' and idx[r['file']]['cause']=='もう1台のロボット']
    sc.sort(reverse=True); picks=sc[:3]
    vmin=float(np.quantile(m['normal'],0.5)); vmax=float(np.quantile(m['long'],0.995))
    files=list(m['long_files']); tiles=[]
    for s,fn in picks:
        i=files.index(fn); im=Image.open(f'{crops}/A/test/long/{fn}').convert('RGB')
        h=np.clip((m['long'][i]-vmin)/(vmax-vmin),0,1)
        hm=Image.fromarray((cm.inferno(h)[:,:,:3]*255).astype(np.uint8)).resize((224,224),Image.BILINEAR)
        tiles.append((im,Image.blend(im,hm,0.45),s))
    rows.append((kind,tiles))
W=Image.new('RGB',(224*6+70,224*2+90),'white'); d=ImageDraw.Draw(W)
for r,(kind,tiles) in enumerate(rows):
    y=40+r*(224+45); d.text((8,y-34),kind+':ロボットどうしの詰まり(点数の高い順に3つ。左が切り出し、右が怪しいと見た所)',fill='black',font=f)
    for j,(a,b,s) in enumerate(tiles):
        x=j*(448+35); W.paste(a,(x,y)); W.paste(b,(x+224,y))
W.save(sys.argv[1]); print(W.size)

線ありの PatchCore の出力 run_on.log(全文)

A train 778 {'normal': 747, 'short': 317, 'long': 93}
bank (6099, 1536)
{
 "kind": "A",
 "n_train": 778,
 "n_test": {
  "normal": 747,
  "short": 317,
  "long": 93
 },
 "bank": [
  6099,
  1536
 ],
 "auroc_long_vs_normal": 0.9186423111802047,
 "auroc_short_vs_normal": 0.9230191005874179,
 "auroc_long_vs_short": 0.6364777314202367,
 "threshold_p95_normal": 2.761341619491577,
 "hit_rate_long_at_thr": 0.7204301075268817,
 "hit_rate_short_at_thr": 0.4921135646687697,
 "false_alarm_normal_at_thr": 0.050870147255689425,
 "score_median": {
  "normal": 2.0329103469848633,
  "short": 2.7581443786621094,
  "long": 2.8881263732910156
 }
}

線なしの PatchCore の出力 run_off.log(全文)

A train 794 {'normal': 765, 'short': 295, 'long': 94}
bank (6224, 1536)
{
 "kind": "A",
 "n_train": 794,
 "n_test": {
  "normal": 765,
  "short": 295,
  "long": 94
 },
 "bank": [
  6224,
  1536
 ],
 "auroc_long_vs_normal": 0.7684744819913782,
 "auroc_short_vs_normal": 0.834531959676526,
 "auroc_long_vs_short": 0.5065993508835196,
 "threshold_p95_normal": 2.77754807472229,
 "hit_rate_long_at_thr": 0.2765957446808511,
 "hit_rate_short_at_thr": 0.19322033898305085,
 "false_alarm_normal_at_thr": 0.050980392156862744,
 "score_median": {
  "normal": 1.9876863956451416,
  "short": 2.583054780960083,
  "long": 2.629531502723694
 }
}

線ありの分けた数え breakdown_on.json(全文)

{
 "kind": "A",
 "n": {
  "normal_idle": 360,
  "normal_move": 387,
  "short": 317,
  "long": 93
 },
 "auroc_long_vs_move": 0.8851240587924758,
 "auroc_long_vs_idle": 0.9546893667861409,
 "auroc_short_vs_idle": 0.9605371538731161,
 "false_alarm_move": 0.07493540051679587,
 "false_alarm_idle": 0.025,
 "idle_by_phase": {
  "往復おわり": "2/203",
  "待機": "0/49",
  "卓で止まる": "7/108"
 },
 "long_hit_by_cause": {
  "もう1台のロボット": "23/43",
  "スタッフとすれ違った": "20/21",
  "人が立ち止まっていた": "12/14",
  "お客さんとすれ違った": "12/14",
  "分からない": "0/1"
 },
 "short_hit_by_cause": {
  "お客さんとすれ違った": "69/124",
  "もう1台のロボット": "36/89",
  "人が立ち止まっていた": "24/45",
  "スタッフとすれ違った": "20/39",
  "分からない": "0/12",
  "椅子・荷物・ベビーカー": "7/8"
 }
}

線なしの分けた数え breakdown_off.json(全文)

{
 "kind": "A",
 "n": {
  "normal_move": 405,
  "normal_idle": 360,
  "short": 295,
  "long": 94
 },
 "auroc_long_vs_move": 0.7325847123719464,
 "auroc_long_vs_idle": 0.8088947990543736,
 "auroc_short_vs_idle": 0.8584792843691149,
 "false_alarm_move": 0.04938271604938271,
 "false_alarm_idle": 0.05277777777777778,
 "idle_by_phase": {
  "卓で止まる": "2/110",
  "往復おわり": "17/201",
  "待機": "0/49"
 },
 "long_hit_by_cause": {
  "もう1台のロボット": "4/47",
  "スタッフとすれ違った": "3/18",
  "お客さんとすれ違った": "13/17",
  "人が立ち止まっていた": "6/11",
  "分からない": "0/1"
 },
 "short_hit_by_cause": {
  "お客さんとすれ違った": "22/105",
  "もう1台のロボット": "16/98",
  "人が立ち止まっていた": "11/45",
  "スタッフとすれ違った": "6/32",
  "分からない": "1/14",
  "椅子・荷物・ベビーカー": "1/1"
 }
}

データの置き場所

何場所
線ありの録画(③ の6回)
KIOXIA service_sim/unity/20261031_rec_*_cs52_fam_p1_*(night/20261031_rec)
線なしの録画(③ の6回)
KIOXIA service_sim/unity/20261101_rec_nopath_*_cs53_fam_p1_*(night/20261101_rec_nopath)
切り出し画像・コード・結果
KIOXIA service_sim/vision_LOG418/(crops/crops_on_off.tar・code/・results_on/・results_off/・README.md)
予備実験(cs:49 の素材)
KIOXIA vision/measurements/Log12/・vision/generated/Log12/

画像で見張るなら、時間の情報と実際のカメラの角度が要る

線を消したあとの結果から、次に確かめることが3つ見えています。

見えたこと次に確かめること
止まりが続くかどうかは、画像1枚では分からない(0.51)
止まりの時間は画像ではなくロボットの記録から取り、画像は原因の場所を示す役にする形で、組み合わせを試す
ロボットどうしの詰まりは、白い丸が並ぶだけで普段の絵と区別がつかない
待機場所の2台と、通路で向かい合う2台を、場所の情報を足して分けられるかを見る
真上から撮った Unity の絵で試している
実際の店のカメラに近い、天井の斜めからの映し方で撮り、人の陰にロボットが隠れる絵でも同じ傾向かを見る

Unity の回が同じ条件でも毎回少し違う件は、比べの数を増やすときに効いてくるので、別に調べます。

作業ログ ― 図面から、シミュレーションの店の形を自動で起こす試し

店の形のデータは、卓・壁・受け渡し口の位置を数字で書いたもの

サイトの試算と Unity のシミュレーションは、どちらも同じ店の形のデータ(famires-layout/0.3 の JSON)を読んで店を組み立てます。中身は、店の外形、入口、部屋(キッチン・トイレ・レジ・ドリンクバー)、壁、受け渡し口(キッチンから料理が出てくる所)、ロボットの待機場所、卓の種類・位置・席数です。単位はmで、店の左下の角を原点にしています。

このデータを、p2m-site の tools/sim_layouts/famires_v3.py で2つの面から確かめています。

確かめ中身
決まり(check_rules)
ロボット(BellaBot)が通れる幅0.8m・ふだん走らせる幅1.0m・人が通れる幅0.5m。入口から1.5m・受け渡し口の前1.2m・トイレの入口の前1.2mには何も置かない。卓の重なり、座る側がふさがっていないか、レジの長さなど
ロボットの届き方(analyze)
受け渡し口の前から卓ごとに、ふだんの道で届くか・狭い道(0.8m)でだけ届くか・人が運ぶか。届くなら、止まる位置までの距離

比べる相手にしたのは、サイトで公開している②(定番の郊外型、19卓・88席)です。②は、昔働いていたファミレスを記憶から手で描いた見取り図(LOG360)から起こし、そのあと人の実感で何度も直した店です。公開している店の形のデータは読んだだけで、変えていません(版 9b8dae7ebf8c)。基準のデータが変わると、これまでの結果と比べられなくなるためです。

起こす作業は、②のデータを見ていない別の担当に任せた

図面から起こす作業は、Claude の中で別に立ち上げた担当(サブエージェント。渡された物と指示だけを知っていて、この会話の中身は知らない)に任せました。②のデータを知っている者が起こすと、図面から読めない所を答えで埋めてしまい、試しにならないためです。

渡したのは、図面・別の店(③)の書き方の見本・標準の家具の大きさ・決まりで確かめる道具(evaluate_layout.py)の4つだけです。②のデータと、②を以前に起こしたものは見てはいけないと指示に書きました(指示の全文は下の作業ログ)。図面に描かれていないことは、ファミレスとしてふつうの形で仮に置き、仮に置いたことを記録するように頼みました。決まりの違反が出たら、図面の意図からなるべく離れない最小の動かし方で直すところまでを、自動の範囲にしました。

確かめる道具 evaluate_layout.py は、サイトと同じ famires_v3.py の決まりと計算をそのまま呼びます。公開済みの③に使うと、③の数字(42卓・160席・卓までの距離の平均10.6m)をそのまま出すことを確かめてから使いました。

手描きの見取り図からは、店が半分の広さに読まれた

1回目は、②の元になった手描きの見取り図です。寸法は書かれていません。

別の担当は、標準の大きさが決まっている家具(4人の角テーブル1.2m四方・丸テーブル1.1m四方・6人の長テーブル3.2m×1.94m)が図の上で何画素あるかを測り、横89画素/m・縦79画素/mと決めました。3種類の家具で値がそろっていたので、読み方としては筋が通っています。ところが、この縮尺だと店は16.4m×12.1mになり、今の②(24.0m×19.2m)の半分ほどの広さでした。手で描くと、家具は部屋に対して大きめに描かれるためです。

最初に起こした時点の決まりの違反は7件でした(卓の重なり2件、レジが入口から1.5m以内、トイレの横の卓に目隠しの壁がない、レジが短い、座る側がふさがった卓2件)。別の担当が自分で直して0件にしましたが、店が狭いので通路も狭く、ふだんの道でロボットが届くのは18卓中3卓、狭い道でだけ届くのが8卓、人が運ぶのが7卓になりました。

左から、手描きの見取り図、そこから自動で起こした店、店の幅24mだけ教えて広げた店、今の②。自動で起こした店は狭く、紫の点線(人が運ぶ道)が多い

店の幅を1か所だけ教えると、18卓すべてにロボットが届いた

次に、店の幅(24m)を1つだけ教えた場合を試しました(scale_one_dim.py)。図面から読んだ位置を同じ倍率(1.463倍)で広げ、家具は標準の大きさのまま、それぞれの中心に置き直しました。

自動(家具の大きさで縮尺)自動+店の幅だけ教える今の②(人が直した)
店の大きさ16.4m × 12.1m24.0m × 17.7m
24.0m × 19.2m(出っ張りあり)
卓・席18卓・86席18卓・86席19卓・88席
決まりの違反7件 → 自動で直して0件0件0件
ふだんの道でロボットが届く卓3卓18卓(全部)19卓(全部)
狭い道でだけ届く卓・人が運ぶ卓
8卓・7卓0卓・0卓0卓・0卓
卓までの距離の平均・最長6.6m・11.1m10.0m・17.4m8.5m・13.8m

店の幅を教えた版と今の②の卓を、位置と種類が近いものどうしで1つずつ対応させると(match.py)、18組のうち17組は家具の種類が同じでした。違ったのは右下の8人席で、②では L字の6人席と4人席に分かれています。位置のずれは中央値0.8m・最大2.4mでした。

卓までの距離は、左側の卓で自動の方が1〜4m長く、右下の3卓は2〜3m短くなりました。受け渡し口の位置が、自動では約2m右にあったためです。それでも合わなかった所は、どれも図面に描かれていないか、人が決めた所でした。

合わなかった所今の②(人が決めた)自動で起こした店
ドリンクバー
図面にない。受け渡し口の下に置くと決めた
トイレの横の箱をドリンクバーと読んだ
ロボットの待機場所受け渡し口の横
置ける余地がないとして右下の隅
受け渡し口の位置と数上と下の2つ下の1つで、位置が約2m右
トイレとバックヤードの出っ張り
あり
図面から読めず、四角の店にした
カウンター席(5席)置いていない
家具の種類にないので置いていない

キッチンまわりは粗くてよいのですが、受け渡し口の位置だけは、卓までの距離が大きく変わるので正確に要ります。

②を、寸法つきの平面図として一から描いた

次は寸法つきの図面で試したいところです。最初は別に手に入れた寸法つきの図面で試しましたが(あとの節)、出典を確かめられていないので、図面も、起こした図も載せられません。そこで、載せてよい寸法つきの図面を自分で用意しました。

公開している②のデータから、CAD の平面図のような図面を描きました(draw_plan.py)。寸法は、通り芯(柱や壁の中心を通る基準の線。点線で描く)の間と全体だけです。家具は縮尺どおりに描き、実際の店の図面にないもの(ロボットの待機場所・道・止まる位置・会計の列)は描いていません。②のデータは読むだけで、変えていません。

寸法つきの図面からは、人の手直しなしで今の②とほぼ同じ店になった

同じ手順で、②のデータを見ていない別の担当に、この図面だけから起こさせました。

手描きの見取り図手描き+店の幅だけ教える寸法つきの図面今の②(人が直した)
店の大きさ16.4m × 12.1m24.0m × 17.7m
24.0m × 19.2m(出っ張りあり)
24.0m × 19.2m(出っ張りあり)
卓・席18卓・86席18卓・86席19卓・88席19卓・88席
決まりの違反(最初に起こした時点)
7件0件0件0件
ふだんの道でロボットが届く卓3卓18卓19卓19卓
卓までの距離の平均・最長6.6m・11.1m10.0m・17.4m8.5m・13.76m8.5m・13.77m
卓の位置のずれ(②と比べて)-中央値0.8m・最大2.4m19卓とも0.0m-

縮尺は、寸法線の端の目盛りを画素で拾って、横16.925mm/画素・縦16.931mm/画素と決めていました。縦と横の差は0.04%で、途中の寸法とのずれは±0.02m以内です。左上の出っ張り(建物の外)も、上の寸法と左の寸法が途中で止まることから読み取っていました。

家具の種類は19卓とも②と同じで、受け渡し口2つ・ドリンクバー・待合・レジ・洗い場・パントリー・キッチンも、②と同じ位置(ずれ0.03m以内)に入りました。卓までの距離の差は、どの卓も0.08m以内です。仮に置いたのは、図面に描いていないものだけでした。ロボットの待機場所は受け渡し口の右に2台分(②は右に3台分)、レジの店員が立つ所はカウンターの奥としていました。

ただし、この図面は店の形のデータから描いたもので、線がきれいで、家具も今の種類と決まりどおりにそろっています。実際の店の図面では、次の節のように別の課題が出ます。

別に手に入れた寸法つきの図面では、家具の種類と決まりが足りなかった

別に手に入れた寸法つきの図面(ファミレスの平面図)でも、同じ手順で試しました。出典を確かめられていないので、図面も、そこから起こした図も、細かい数字も載せず、分かったことだけを書きます。

寸法があるので、縮尺の問題は消えました。縦と横の縮尺はそろっていて、切り欠きのある店の形も、卓の位置も図面どおりに入りました。代わりに目立ったのは、次の2つです。

  1. 家具の種類が足りない。斜め45°に置いた4人席、半円のブース、L字のカウンターのスツール席、椅子が向かい合う2人掛けのテーブルは、今の家具の種類(ボックス席・2人席・L字ソファの6人席・角テーブル・丸テーブル・長テーブル)では表せず、近い種類で仮に置くしかありませんでした。ベンチには椅子が描かれていないので、席数はベンチの長さから推し量るしかありませんでした
  2. 決まりが日本のファミレス向けになっている。ドリンクバーがあることを決まりにしていたので、ドリンクバーのない店は違反が残り、ロボットの届き方の計算まで進めませんでした。会計の列(ロボットを通さない所)が入口とレジの間の通路をふさぎ、奥の客席へはレジの裏の店員の通路を狭い道として通る計算になりました。卓の間の通路が、ふだん走らせる幅1.0mに届かない所もありました

ドリンクバーの決まりは、店にあるときだけ確かめる形に変えました。

  • ねらい:ドリンクバーのない店の図面でも、決まりの確かめで止まらずに、ロボットの届き方まで計算できるようにする
  • 変わったこと:famires_v3.py の check_rules から「ドリンクバーがない」の違反を外した。ドリンクバーがある店では、ほかの部屋と同じ決まりで確かめる。テストを1つ足し、141件すべて通った(p2m-site の commit 16dc593)
  • 見込める効果:別に手に入れた図面から起こした店は、ドリンクバーの1件を外すと違反0件になり、届き方まで計算できた。公開している6店は、出力が版の番号のほかは同じだった(25ファイルで確かめた)。公開している店の形のデータは作り直さず、版 9b8dae7ebf8c のまま残した

店の人に聞くことは、図面の詳しさで決まった

3つの図面で、別の担当が挙げた質問のうち、店の形を確かにするのに最低限要るものを並べました。

図面店の人に聞くこと(最低限)
手描きの見取り図
店の幅(1か所の実寸)、受け渡し口の位置と数、ドリンクバーの位置、ロボットの待機・充電の場所、店の形が四角か(出っ張りがあるか)。別の担当が挙げた質問は全部で12
②を寸法つきで描いた平面図
ロボットの待機・充電の場所と台数
別に手に入れた寸法つきの図面
ロボットが料理を受け取る所と待機・充電の場所、レジの裏の店員の通路をロボットが通ってよいか、ベンチの席数、半円のブースは何人掛けか

寸法つきの図面で最後まで残るロボットの待機場所は、図面の読み取りの問題ではなく、店がロボットを入れるときに決めることです。図面からシミュレーションの店を起こす所は、寸法さえあればほぼ自動にでき、人が手を入れるのは、店の人に聞いて決める所と、今の家具の種類にない形の扱いになります。

図面から起こす試しで、次に確かめること

見えたこと次に確かめること
②の寸法つきの図面は、データから描いたので線がきれいで、家具も今の種類どおり
出典がはっきりしていて載せてよい、実際の店の寸法つきの図面で同じ手順を試す
斜めの卓・半円のブース・カウンター席が、今の家具の種類では表せない
今の6店のデータを変えずに、家具の種類にない形をどう扱うかを決める
卓までの距離は、受け渡し口の位置で大きく変わる
店の人への質問の最初に、受け渡し口の位置とロボットの待機場所を置く

作業ログ ― 図面から店の形を起こしたときの指示・コード・出力(全文)

回した環境

段どこで使ったもの
図面を描く・図面から起こす・確かめる
クラウドの作業環境(CPU 2コア・メモリ7GB・GPU なし)
Python 3・matplotlib・NumPy・SciPy・Pillow。決まりと計算は p2m-site の tools/sim_layouts/famires_v3.py(版 0969920。ドリンクバーの決まりを変える前)

かかった時間は測っていません。費用はかかっていません。下の evaluate_layout.py と match.py の出力は、記事にするときに同じ版の famires_v3.py で回し直して写したもので、元の check.json のまとめと同じ結果になることを確かめています。

指示の中の /home/claude/blind/・/home/claude/blind3/ は、別の担当の作業フォルダです(中身は iCloud の 図面から店の形_20261031/ に写してあります)。

手描きの見取り図で別の担当に渡した指示 TASK.md(全文)

03_TASK.md3,129字・66行・markdown
# 作業:手描きの見取り図から、ファミレスの店の形のデータを起こす

## 目的
店の人から図面が届いたとき、店の形のデータをどこまで自動で起こせるかを試す。
人の手直しや店の人への質問なしで、図面と標準の家具の大きさだけから起こす。

## 使ってよいもの(このフォルダの中だけ)
- `sketch.jpg`:昔働いていたファミレスを記憶から手で描いた見取り図。寸法は書かれていない
別のタブで開く保存

手描きの見取り図から店の形を起こすスクリプト build.py(全文)

04_build.py.txt5,583字・117行・python
# 手描きの見取り図(sketch.jpg)の読み取り値(画素)から、店の形(famires-layout/0.3 の layout)を起こす
# 縮尺:標準の家具の大きさから、横 89px/m・縦 79px/m(notes.md 参照)
# 原点:外壁の内側の左下の角 = 画素 (32, 1070)
import json, sys, copy
from pathlib import Path

SX, SY = 89.0, 79.0
PX0, PY0 = 32, 1070
別のタブで開く保存

決まりとロボットの届き方で確かめる道具 evaluate_layout.py(全文)

# 図面から起こした店の形(famires-layout/0.3 の "layout" 部分)を、サイトの店の形と同じ決まり・同じ計算で確かめる
# 使い方: python3 evaluate_layout.py <店の形の JSON> <出力フォルダ>
#   JSON は {"layout": {...}} でも {...}(layout の中身だけ)でもよい
# 出力: check.json(決まりの違反・卓ごとの結果・まとめ)と plan.png(図)
import json, sys, copy
from pathlib import Path
sys.path.insert(0, "/home/claude/p2m-site/tools/sim_layouts")
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
import famires_v3 as v3

src, out = Path(sys.argv[1]), Path(sys.argv[2])
out.mkdir(parents=True, exist_ok=True)
d = json.load(open(src))
L = copy.deepcopy(d.get("layout", d))
L.setdefault("walls", []); L.setdefault("toilet_doors", [])
L = v3.prepare(L)            # 卓の番号を振り直し、出発点(受け渡し口の前)を決める
v = v3.check_rules(L)
res, summ, conv = None, None, []
if not v:
    try:
        res, err = v3.analyze(L, v3.SETTINGS["res"])
        res_b, err_b = v3.analyze(L, v3.SETTINGS["check_res"])
        v += err + [e for e in err_b if e not in err]
        conv = [a["id"] for a, b in zip(res, res_b) if a["category"] != b["category"]]
        summ = v3.summarize(L, res)
    except ValueError as ex:
        v.append(("出発点", str(ex)))
fig, ax = plt.subplots(figsize=(14, 11), dpi=110)
if res is not None:
    v3.draw(ax, L, res, summ, conv)
else:   # 違反があっても形は描く
    v3.draw(ax, L, [dict(id=t["id"], seats=t["seats"], category="human", open_sides=[]) for t in L["tables"]],
            dict(name=L.get("name", ""), seats=sum(t["seats"] for t in L["tables"]), tables=len(L["tables"]),
                 model="BellaBot", pass_width=0.8, normal_width=1.0, robot_tables=0, robot_seats=0, narrow_tables=0,
                 narrow_seats=0, human_tables=len(L["tables"]), human_seats=0, robot_dist_mean=None, robot_dist_max=None,
                 human_walk_total=0, human_walk_max=None), [])
fig.savefig(out / "plan.png", bbox_inches="tight", facecolor="white"); plt.close(fig)
strip = lambda r: {k: x for k, x in r.items() if k != "path"}
json.dump(dict(violations=[f"{k}: {m}" for k, m in v], summary=summ,
               tables=[strip(r) for r in res] if res else None, layout=L),
          open(out / "check.json", "w"), ensure_ascii=False, indent=1)
print("違反", len(v)); [print(" -", k, m) for k, m in v]
print("まとめ", json.dumps(summ, ensure_ascii=False))

店の幅を1つだけ教えて広げる scale_one_dim.py(全文)

# 店の幅を1つだけ教えてもらった場合:図面の位置を同じ倍率で広げ、家具は標準の大きさのまま中心に置き直す
import json,sys,copy
A=json.load(open('/home/claude/blind/out/auto_final.json')); A=copy.deepcopy(A.get('layout',A))
W=float(sys.argv[1]); s=W/A['outer']['w']
R=lambda r:[round(v*s,3) for v in r]
A['outer']={'w':round(A['outer']['w']*s,3),'d':round(A['outer']['d']*s,3)}
e=A['entrance']; e['a0']=round(e['a0']*s,3); e['a1']=round(e['a1']*s,3)
for k in ('rooms','walls','doors','dish_up'):
    for r in A.get(k,[]): r['rect']=R(r['rect'])
A['toilet_doors']=[R(r) for r in A.get('toilet_doors',[])]
sb=A['robot_standby']; cx,cy=(sb['rect'][0]+sb['rect'][2])/2*s,(sb['rect'][1]+sb['rect'][3])/2*s; w,h=sb['rect'][2]-sb['rect'][0],sb['rect'][3]-sb['rect'][1]
sb['rect']=[round(cx-w/2,3),round(cy-h/2,3),round(cx+w/2,3),round(cy+h/2,3)]
for t in A['tables']:
    cx,cy=(t['x']+t['w']/2)*s,(t['y']+t['h']/2)*s; t['x']=round(cx-t['w']/2,3); t['y']=round(cy-t['h']/2,3)
A.pop('start',None)
json.dump({'layout':A},open(f'auto_scaled_w{W:g}.json','w'),ensure_ascii=False,indent=1); print('scale',round(s,3),A['outer'])

scale_one_dim.py の出力(全文)

scale 1.463 {'w': 24.0, 'd': 17.707}

手描き+店の幅と今の②の卓を1つずつ対応させる match.py(全文)

import json,numpy as np
from scipy.optimize import linear_sum_assignment
P=json.load(open('check_p0/check.json')); A=json.load(open('check_scaled_w24/check.json')); V=json.load(open('/home/claude/blind/out/check_final/check.json'))
def tabs(C):
    L=C['layout']; out=[]
    for t,r in zip(L['tables'],C['tables']):
        out.append(dict(id=t['id'],type=t['type'],seats=t['seats'],cx=t['x']+t['w']/2,cy=t['y']+t['h']/2,dist=r.get('dist'),cat=r['category']))
    return out
p,a=tabs(P),tabs(A)
C=np.array([[np.hypot(x['cx']-y['cx'],x['cy']-y['cy'])+(0 if x['type']==y['type'] else 3) for y in p] for x in a])
ri,ci=linear_sum_assignment(C)
pos=[];dd=[];same=0
for i,j in zip(ri,ci):
    x,y=a[i],p[j]; d=np.hypot(x['cx']-y['cx'],x['cy']-y['cy']); pos.append(d); same+=x['type']==y['type']
    if x['dist'] and y['dist']: dd.append(x['dist']-y['dist'])
    print(f"{x['id']} {x['type']:8s} -> {y['id']} {y['type']:8s} ずれ {d:4.1f}m  距離 自動{x['dist']} / ②{y['dist']}")
pos=np.array(pos);dd=np.array(dd)
print('対応',len(ri),'種類が同じ',same,'位置のずれ 中央値 %.1fm 最大 %.1fm'%(np.median(pos),pos.max()),'距離の差 中央値 %+.1fm 平均の差 %+.1fm'%(np.median(dd),dd.mean()))
print('②の卓で対応なし',[p[j]['id']+':'+p[j]['type'] for j in set(range(len(p)))-set(ci)])

match.py の出力(全文)

T01 booth6L  -> T01 booth6L  ずれ  0.9m  距離 自動17.39 / ②13.77
T02 t4       -> T03 t4       ずれ  1.1m  距離 自動15.94 / ②12.12
T03 t4       -> T04 t4       ずれ  1.7m  距離 自動14.21 / ②11.57
T04 r4       -> T05 r4       ずれ  0.5m  距離 自動14.26 / ②10.62
T05 r4       -> T06 r4       ずれ  0.5m  距離 自動11.86 / ②8.63
T06 booth6L  -> T02 booth6L  ずれ  0.1m  距離 自動16.22 / ②12.13
T07 t4       -> T07 t4       ずれ  0.5m  距離 自動13.4 / ②10.71
T08 t4       -> T08 t4       ずれ  1.7m  距離 自動8.54 / ②6.63
T09 booth6L  -> T09 booth6L  ずれ  0.5m  距離 自動9.35 / ②7.57
T10 t4       -> T11 t4       ずれ  1.3m  距離 自動6.79 / ②5.92
T11 t4       -> T12 t4       ずれ  1.7m  距離 自動4.66 / ②3.52
T12 long6    -> T13 long6    ずれ  1.3m  距離 自動3.61 / ②2.55
T13 booth6L  -> T10 booth6L  ずれ  0.5m  距離 自動10.19 / ②8.29
T14 t4       -> T14 t4       ずれ  1.1m  距離 自動7.3 / ②6.61
T15 t4       -> T15 t4       ずれ  2.4m  距離 自動6.5 / ②4.45
T16 t4       -> T16 t4       ずれ  0.8m  距離 自動3.53 / ②5.35
T17 t4       -> T17 t4       ずれ  0.2m  距離 自動6.27 / ②8.77
T18 booth    -> T19 t4       ずれ  0.4m  距離 自動9.26 / ②12.39
対応 18 種類が同じ 17 位置のずれ 中央値 0.8m 最大 2.4m 距離の差 中央値 +1.9m 平均の差 +1.5m
②の卓で対応なし ['T18:booth6L']

手描きの見取り図を起こした別の担当の記録 notes.md(全文)

05_notes.md5,556字・124行・markdown
# 手描きの見取り図から店の形を自動で起こした記録

入力は `sketch.jpg`(寸法なし)、標準の家具の大きさ、図から読めるキッチンまわりの点だけ。店の人への質問や人の手直しはしていない。
起こし方のコードは `/home/claude/blind/work/build.py`(画素で読んだ値をメートルに直し、v0 と final を書き出す)。

- `auto_v0.json`:図を読んで最初に起こしたもの。違反7件(`check_v0/`)
- `auto_final.json`:違反を直したもの。違反0件(`check_final/`)
別のタブで開く保存

寸法つきの平面図を描く draw_plan.py(全文)

06_draw_plan.py.txt5,530字・129行・python
# ②(記憶の店)を、寸法つきの平面図(CAD 風)として描く
# 元:公開している店の形のデータ(public/sim/layouts/unity/p0_memory.json。版 9b8dae7ebf8c)。読むだけで変えない
# 実際の店の図面にないもの(ロボットの待機場所・道・止まる位置・会計の列など)は描かない
# 使い方: python3 draw_plan.py <出力png>
import json, sys, math
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Circle, Arc, Polygon
別のタブで開く保存

寸法つきの図面で別の担当に渡した指示 TASK.md(全文)

07_TASK.md3,456字・71行・markdown
# 作業:寸法つきの図面から、ファミレスの店の形のデータを起こす

## 目的
店から寸法つきの図面が届いたとき、店の形のデータをどこまで自動で起こせるかを試す。
人の手直しや店の人への質問なしで、図面と標準の家具の大きさだけから起こす。
前回は寸法のない手描きの見取り図で試し、縮尺が読めないことが一番の問題だった。今回は寸法がある。

## 使ってよいもの(このフォルダの中だけ)
別のタブで開く保存

寸法つきの図面の線のつながりを mm で出す work/comps.py(全文)

# 図面の黒い線のつながり(連結成分)を拾い、外接矩形を mm で出す
from PIL import Image; import numpy as np
from scipy import ndimage
im=np.array(Image.open('/home/claude/blind3/drawing.png').convert('L'))
X0,X1,Y0,Y1=192,1610,1291,157
sx=24000/(X1-X0); sy=19200/(Y0-Y1)
def mx(px): return (px-X0)*sx/1000
def my(py): return (Y0-py)*sy/1000
dark=im<140
lab,n=ndimage.label(dark,structure=np.ones((3,3)))
objs=ndimage.find_objects(lab)
rows=[]
for i,s in enumerate(objs):
    ys,xs=s; h=ys.stop-ys.start; w=xs.stop-xs.start
    if w*h<30: continue
    rows.append((xs.start,ys.start,xs.stop-1,ys.stop-1,int((lab[s]==i+1).sum())))
rows.sort(key=lambda r:(r[1],r[0]))
for r in rows:
    x0,y0,x1,y1,c=r
    print(f"px x{x0}-{x1} y{y0}-{y1} ({x1-x0}x{y1-y0}) n={c} | m x{mx(x0):.2f}-{mx(x1):.2f} y{my(y1):.2f}-{my(y0):.2f} size {(x1-x0)*sx/1000:.2f}x{(y1-y0)*sy/1000:.2f}")

寸法つきの図面の行・列ごとの黒い区間を出す work/runs.py(全文)

# 指定した行・列の上で、黒い区間(runs)を画素と mm で出す
import sys
from PIL import Image; import numpy as np
im=np.array(Image.open('/home/claude/blind3/drawing.png').convert('L'))
X0,X1,Y0,Y1=192,1610,1291,157
sx=24000/(X1-X0); sy=19200/(Y0-Y1)
mx=lambda p:(p-X0)*sx/1000; my=lambda p:(Y0-p)*sy/1000
def runs(a):
    d=np.r_[0,(a<128).astype(int),0]; e=np.diff(d); s=np.where(e==1)[0]; t=np.where(e==-1)[0]
    return list(zip(s,t))
for arg in sys.argv[1:]:
    k,v=arg[0],int(arg[1:])
    if k=='r':
        print('row',v,f'(y={my(v):.3f})',[(a,b,f'{mx(a):.3f}-{mx(b):.3f}') for a,b in runs(im[v])])
    else:
        print('col',v,f'(x={mx(v):.3f})',[(a,b,f'{my(b):.3f}-{my(a):.3f}') for a,b in runs(im[:,v])])

寸法つきの図面の太い線を拾う work/thick.py(全文)

# 太い線(壁)だけを残して、壁の矩形を mm で出す
from PIL import Image; import numpy as np
from scipy import ndimage
im=np.array(Image.open('/home/claude/blind3/drawing.png').convert('L'))
X0,X1,Y0,Y1=192,1610,1291,157
sx=24000/(X1-X0); sy=19200/(Y0-Y1)
mx=lambda p:(p-X0)*sx/1000; my=lambda p:(Y0-p)*sy/1000
dark=im<100
th=ndimage.binary_opening(dark,structure=np.ones((9,9)))
lab,n=ndimage.label(th)
for i,s in enumerate(ndimage.find_objects(lab)):
    ys,xs=s
    print(f"px x{xs.start}-{xs.stop} y{ys.start}-{ys.stop} | m x{mx(xs.start):.3f}-{mx(xs.stop):.3f} y{my(ys.stop):.3f}-{my(ys.start):.3f}")

寸法つきの図面から店の形を起こす work/build_v0.py(全文)

08_work_build_v0.py.txt3,989字・76行・python
# 図面(drawing.png)から読んだ値で、店の形(famires-layout/0.3 の layout 部分)を起こす。単位 m
# 縮尺:横 24000mm / 1418px = 16.925 mm/px、縦 19200mm / 1134px = 16.931 mm/px(差 0.04%)
# 原点:外壁の内側の左下(画素 x=192, y=1291)。読んだ画素は comps.py・runs.py の出力
import json, sys
out = sys.argv[1] if len(sys.argv) > 1 else "/home/claude/blind3/out/auto_v0.json"
R = lambda v: round(v, 3)
def T(typ, x0, y0, x1, y1, seats, **kw):
    return dict(type=typ, x=R(x0), y=R(y0), w=R(x1 - x0), h=R(y1 - y0), seats=seats, **kw)
別のタブで開く保存

寸法つきの図面の final を書き出す work/make_final.py(全文)

# v0 に違反がなかったので、final は v0 と同じ形(名前だけ final に)。直した所なし
import json
d = json.load(open("/home/claude/blind3/out/auto_v0.json"))
d["layout"]["name"] = "寸法つき図面のファミレス(自動で起こした版・final)"
json.dump(d, open("/home/claude/blind3/out/auto_final.json", "w"), ensure_ascii=False, indent=1)

寸法つきの図面から起こした店と今の②の卓を対応させる work_match.py(全文)

import json,numpy as np
from scipy.optimize import linear_sum_assignment
P=json.load(open('/home/claude/blind/compare/check_p0/check.json')); A=json.load(open('/home/claude/blind3/out/check_final/check.json')); V=json.load(open('/home/claude/blind/out/check_final/check.json'))
def tabs(C):
    L=C['layout']; out=[]
    for t,r in zip(L['tables'],C['tables']):
        out.append(dict(id=t['id'],type=t['type'],seats=t['seats'],cx=t['x']+t['w']/2,cy=t['y']+t['h']/2,dist=r.get('dist'),cat=r['category']))
    return out
p,a=tabs(P),tabs(A)
C=np.array([[np.hypot(x['cx']-y['cx'],x['cy']-y['cy'])+(0 if x['type']==y['type'] else 3) for y in p] for x in a])
ri,ci=linear_sum_assignment(C)
pos=[];dd=[];same=0
for i,j in zip(ri,ci):
    x,y=a[i],p[j]; d=np.hypot(x['cx']-y['cx'],x['cy']-y['cy']); pos.append(d); same+=x['type']==y['type']
    if x['dist'] and y['dist']: dd.append(x['dist']-y['dist'])
    print(f"{x['id']} {x['type']:8s} -> {y['id']} {y['type']:8s} ずれ {d:4.1f}m  距離 自動{x['dist']} / ②{y['dist']}")
pos=np.array(pos);dd=np.array(dd)
print('対応',len(ri),'種類が同じ',same,'位置のずれ 中央値 %.1fm 最大 %.1fm'%(np.median(pos),pos.max()),'距離の差 中央値 %+.1fm 平均の差 %+.1fm'%(np.median(dd),dd.mean()))
print('②の卓で対応なし',[p[j]['id']+':'+p[j]['type'] for j in set(range(len(p)))-set(ci)])

work_match.py の出力(全文)

T01 booth6L  -> T01 booth6L  ずれ  0.0m  距離 自動13.76 / ②13.77
T02 booth6L  -> T02 booth6L  ずれ  0.0m  距離 自動12.11 / ②12.13
T03 t4       -> T03 t4       ずれ  0.0m  距離 自動12.17 / ②12.12
T04 t4       -> T04 t4       ずれ  0.0m  距離 自動11.62 / ②11.57
T05 r4       -> T05 r4       ずれ  0.0m  距離 自動10.6 / ②10.62
T06 r4       -> T06 r4       ずれ  0.0m  距離 自動8.63 / ②8.63
T07 t4       -> T07 t4       ずれ  0.0m  距離 自動10.64 / ②10.71
T08 t4       -> T08 t4       ずれ  0.0m  距離 自動6.68 / ②6.63
T09 booth6L  -> T09 booth6L  ずれ  0.0m  距離 自動7.58 / ②7.57
T10 booth6L  -> T10 booth6L  ずれ  0.0m  距離 自動8.29 / ②8.29
T11 t4       -> T11 t4       ずれ  0.0m  距離 自動5.93 / ②5.92
T12 t4       -> T12 t4       ずれ  0.0m  距離 自動3.55 / ②3.52
T13 long6    -> T13 long6    ずれ  0.0m  距離 自動2.55 / ②2.55
T14 t4       -> T14 t4       ずれ  0.0m  距離 自動6.64 / ②6.61
T15 t4       -> T15 t4       ずれ  0.0m  距離 自動4.51 / ②4.45
T16 t4       -> T16 t4       ずれ  0.0m  距離 自動5.43 / ②5.35
T17 t4       -> T17 t4       ずれ  0.0m  距離 自動8.82 / ②8.77
T18 t4       -> T19 t4       ずれ  0.0m  距離 自動12.43 / ②12.39
T19 booth6L  -> T18 booth6L  ずれ  0.0m  距離 自動9.81 / ②9.85
対応 19 種類が同じ 19 位置のずれ 中央値 0.0m 最大 0.0m 距離の差 中央値 +0.0m 平均の差 +0.0m
②の卓で対応なし []

寸法つきの図面を起こした別の担当の記録 notes.md(全文)

09_notes.md4,895字・80行・markdown
# 寸法つき図面から店の形を起こした記録(blind3)

入力は `drawing.png`・標準の家具の大きさ・図面から読めるキッチンまわりの点だけ。人の手直し・店の人への質問なし。
途中のスクリプトは `work/`(`comps.py`:線のつながりの外接矩形を mm で出す、`runs.py`:行・列ごとの黒い区間、`thick.py`:太い線、`build_v0.py`:JSON を起こす、`make_final.py`)。

## 1. 縮尺の決め方
- 寸法線の端の目盛りを、画素の列・行で拾った(上の寸法線の目盛り x=192/641/771/901/1314/1610、下 192/759/925/1610、右 y=157/286/936/1291、左 286/783/1291)
- 横:24000mm ÷ (1610−192)=1418px → **16.925 mm/px**
別のタブで開く保存

手描き v0・final・店の幅24m、寸法つき v0・final を確かめた evaluate_layout.py の出力(全文)

##### hand_v0
違反 7
 - 重なり T09 と T12
 - 重なり T09 と T13
 - 入口の前 部屋:レジ が入口から1.5m以内にある
 - トイレ T03 がトイレから0.21m(1.0m以内に置くなら、席の両端からトイレの入口の両端への4本の線が全部壁に当たること)
 - 入口まわり レジが短い(1.80m)
 - 座れない卓 T09 の座る側がすべてふさがっている
 - 座れない卓 T13 の座る側がすべてふさがっている
まとめ null
##### hand_final
違反 0
まとめ {"name": "⓪ 手描きの見取り図から自動で起こした店(記憶のファミレス)", "seats": 86, "tables": 18, "model": "BellaBot", "pass_width": 0.8, "normal_width": 1.0, "robot_tables": 3, "robot_seats": 16, "narrow_tables": 8, "narrow_seats": 36, "human_tables": 7, "human_seats": 34, "robot_dist_mean": 6.6, "robot_dist_max": 11.08, "human_walk_total": 47.7, "human_walk_max": 11.22}
##### hand_w24
違反 0
まとめ {"name": "⓪ 手描きの見取り図から自動で起こした店(記憶のファミレス)", "seats": 86, "tables": 18, "model": "BellaBot", "pass_width": 0.8, "normal_width": 1.0, "robot_tables": 18, "robot_seats": 86, "narrow_tables": 0, "narrow_seats": 0, "human_tables": 0, "human_seats": 0, "robot_dist_mean": 10.0, "robot_dist_max": 17.39, "human_walk_total": 0.0, "human_walk_max": null}
##### d2_v0
違反 0
まとめ {"name": "寸法つき図面のファミレス(自動で起こした版)", "seats": 88, "tables": 19, "model": "BellaBot", "pass_width": 0.8, "normal_width": 1.0, "robot_tables": 19, "robot_seats": 88, "narrow_tables": 0, "narrow_seats": 0, "human_tables": 0, "human_seats": 0, "robot_dist_mean": 8.5, "robot_dist_max": 13.76, "human_walk_total": 0.0, "human_walk_max": null}
##### d2_final
違反 0
まとめ {"name": "寸法つき図面のファミレス(自動で起こした版・final)", "seats": 88, "tables": 19, "model": "BellaBot", "pass_width": 0.8, "normal_width": 1.0, "robot_tables": 19, "robot_seats": 88, "narrow_tables": 0, "narrow_seats": 0, "human_tables": 0, "human_seats": 0, "robot_dist_mean": 8.5, "robot_dist_max": 13.76, "human_walk_total": 0.0, "human_walk_max": null}

今の②を確かめた evaluate_layout.py の出力(全文)

$ python3 evaluate_layout.py public/sim/layouts/p0_memory.json check_p0
違反 0
まとめ {"name": "② 定番の郊外型", "seats": 88, "tables": 19, "model": "BellaBot", "pass_width": 0.8, "normal_width": 1.0, "robot_tables": 19, "robot_seats": 88, "narrow_tables": 0, "narrow_seats": 0, "human_tables": 0, "human_seats": 0, "robot_dist_mean": 8.5, "robot_dist_max": 13.77, "human_walk_total": 0.0, "human_walk_max": null}

手描きの見取り図から起こした店の形 auto_final.json(全文)

10_auto_final.json4,766字・392行・json
{
 "layout": {
  "name": "⓪ 手描きの見取り図から自動で起こした店(記憶のファミレス)",
  "style": "効率重視",
  "outer": {
   "w": 16.4,
   "d": 12.1
  },
別のタブで開く保存

店の幅24mだけ教えて広げた店の形 auto_scaled_w24.json(全文)

11_auto_scaled_w24.json4,936字・392行・json
{
 "layout": {
  "name": "⓪ 手描きの見取り図から自動で起こした店(記憶のファミレス)",
  "style": "効率重視",
  "outer": {
   "w": 24.0,
   "d": 17.707
  },
別のタブで開く保存

寸法つきの図面から起こした店の形 auto_final.json(全文)

12_auto_final.json5,622字・466行・json
{
 "layout": {
  "name": "寸法つき図面のファミレス(自動で起こした版・final)",
  "style": "効率重視",
  "outer": {
   "w": 24.0,
   "d": 19.2
  },
別のタブで開く保存

図面から起こす試しのデータの置き場所

何場所
手描きの見取り図・起こした店の形・確かめの結果・道具
iCloud PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/図面から店の形_20261031/(入口は README.md)
②の寸法つきの平面図と、そこから起こした店の形
同じフォルダの 寸法つきの図面_②/
別に手に入れた寸法つきの図面での試し
同じフォルダに置いているが、出典を確かめられていないので公開しない
比べる相手の②(読んだだけで変えていない)
GitHub PixelstoMotors/p2m-site の public/sim/layouts/p0_memory.json(版 9b8dae7ebf8c)
ドリンクバーの決まりを変えた commit
p2m-site の 16dc593(tools/sim_layouts/famires_v3.py・test_famires_v3.py)