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

- 記録: LOG418（サービスロボット導入シミュレーション）
- 公開: 2026/11/1
- 記事のページ: https://pixelstomotors.com/record/serving-robot-jam-detection-without-path-lines
- サイト: Pixels to Motors（ADAPT DESIGN / 島津 栄一） https://pixelstomotors.com/
- 要約: Unity で再現したファミレスの録画から配膳ロボットの長い止まりを PatchCore で見つける試しで、画面に描かれたロボットの道の線を消すと、拾えた割合は72%から28%に落ちた。ロボットどうしの詰まりはほとんど拾えなかった。

> AI が読むための記事の全文です（コード・実行ログを含む）。引用するときは上の「記事のページ」の住所を使ってください。

---

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%に落ちた |

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

[図解: LOG418_path_line_effect.html](https://pixelstomotors.com/embeds/LOG418_path_line_effect.html)


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

店の寸法つきの図面があれば、配膳ロボットのシミュレーションに使う店の形（卓・通路・壁・受け渡し口の位置を数字で書いたデータ）は、人の手直しなしで起こせました。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つ |
| 別に手に入れた寸法つきの図面 | 縮尺の問題は消えたが、斜めに置いた卓・半円のブース・カウンター席が、今の家具の種類では表せなかった | ロボットが料理を受け取る所と待機場所、店員の通路を通ってよいか、ベンチの席数など |

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

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


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

[配膳ロボットの導入前シミュレーション](https://pixelstomotors.com/serving-robot-one-less-staff)


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

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

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

[PatchCore](https://arxiv.org/abs/2106.08265) は、画像の異常検知（いつもと違う所を見つける方法）の1つです。工場の検査で、傷のない良品の画像だけを覚えさせ、見たことのない傷を見つけるのに使われています。きっかけになったのは、[Roboflow](https://roboflow.com/)（画像の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コマ。白い丸が配膳ロボット、黄色がお客さん、青がスタッフ](https://pixelstomotors.com/media/LOG418_04_store_overview.png)

録画には、何コマ目が店の何秒目かの表（_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秒以上の止まり](https://pixelstomotors.com/media/LOG418_03_crop_examples.png)

| 種類 | 何を切り出したか | 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・77 | 785・339・77 |
| 長い止まりと普段の見分け（AUROC） | 0.92 | 0.87 |
| 長い止まりと 待機・卓で止まる の見分け | 0.95 | 0.89 |
| 長い止まりと短い止まりの見分け | 0.70 | 0.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回を線なしで撮る（全文）

```markdown
---
作業フォルダ: /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 の報告の部分）

````markdown
## 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秒のコマで目で確かめました。

![同じ通路を走る配膳ロボットの切り出し。上の線ありの録画では、ロボットの後ろに水色やだいだいの道の線が出ている。下の線なしの録画には出ていない](https://pixelstomotors.com/media/LOG418_05_path_line_examples.png)

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

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

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

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

| 切り出した枚数 | 覚えさせる3回（普段・短い・長い） | 確かめる3回（普段・短い・長い） |
|---|---|---|
| 線あり（依頼43の撮り直し、cs:52） | 778・260・37 | 747・317・93 |
| 線なし（依頼51、cs:53） | 794・233・40 | 765・295・94 |

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

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

| | 線あり | 線なし |
|---|---|---|
| 長い止まりと普段の見分け（AUROC） | 0.92 | 0.77 |
| 長い止まりと、走っているロボットの見分け | 0.89 | 0.73 |
| 長い止まりと、待機・卓で止まるロボットの見分け | 0.95 | 0.81 |
| 長い止まりと短い止まりの見分け | 0.64 | 0.51 |
| 誤報5%で長い止まりを拾えた割合 | 72%（67/93） | 28%（26/94） |
| 同じしきい値で短い止まりを拾った割合 | 49% | 19% |
| 普段（走っている）の誤報 | 7.5% | 4.9% |
| 普段（待機・卓で止まる）の誤報 | 2.5% | 5.3% |

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

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

| 長い止まりの原因 | 線あり（拾えた数） | 線なし（拾えた数） |
|---|---|---|
| もう1台のロボット | 23/43 | 4/47 |
| スタッフとすれ違った | 20/21 | 3/18 |
| お客さんとすれ違った | 12/14 | 13/17 |
| 人が立ち止まっていた | 12/14 | 6/11 |
| 分からない | 0/1 | 0/1 |

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

![長い止まりのうち、ロボットどうしが原因で点数の高かった3つずつ。上の線ありでは道の線の上が光り、下の線なしではロボットのまわりの人の所が光っている](https://pixelstomotors.com/media/LOG418_02_heatmap_on_off.png)

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


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

### 回した環境と順番

| 段 | どこで | 使ったもの | かかった時間 |
|---|---|---|---|
| 切り出し | 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です。

```bash
# 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）

```text
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（全文）

```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コマを白黒にして重ねた画像
import csv,json,os,sys,random,bisect,glob
import numpy as np,cv2
U=os.path.expanduser('~/mnt/KIOXIA/r2r2r-data/service_sim/unity/')
OUT=sys.argv[3].rstrip('/')+'/'
CROP_M=3.0; SIZE=224; random.seed(0)
run=sys.argv[1]; split=sys.argv[2]; d=U+run
der=json.load(open(d+'/conditions.json'))['derived']; OX,OZ=der['outer_size_x_m'],der['outer_size_z_m']
mp4=glob.glob(d+'/recordings/*_whole.mp4')[0]
W,H=(lambda c:(int(c.get(3)),int(c.get(4))))(cv2.VideoCapture(mp4)); s=W/(OX+1.6)
assert abs(H-s*(OZ+1.6))<3,(W,H,s)
tr={}
for r in csv.DictReader(open(d+'/robot_track.csv')): tr.setdefault(r['robot_id'],[]).append((float(r['t_s']),float(r['x']),float(r['z']),r['phase']))
for k in tr: tr[k].sort()
stops=list(csv.DictReader(open(d+'/stops.csv')))
busy={k:[] for k in tr}
for r in stops: busy[r['robot_id']].append((float(r['t_s']),float(r['t_s'])+float(r['duration_s'])))
def inbusy(rid,t,pre=12,post=2): return any(a-pre<=t<=b+post for a,b in busy[rid])
S=[]
for r in stops:
    dur=float(r['duration_s']); t0=float(r['t_s'])
    if dur>=30: S.append(['long',r['robot_id'],t0+15,float(r['x']),float(r['z']),dur,r['cause'],False])
    elif dur>=3: S.append(['short',r['robot_id'],t0+3,float(r['x']),float(r['z']),dur,r['cause'],False])
sh=[x for x in S if x[0]=='short']; random.shuffle(sh); S=[x for x in S if x[0]=='long']+sh[:120]
mv,idle=[],[]
for rid,L in tr.items():
    for (t1,x1,z1,p1),(t2,x2,z2,p2) in zip(L,L[1:]):
        if t2<15 or inbusy(rid,t2): continue
        if p2 in('卓へ','受け渡し口へ') and np.hypot(x2-x1,z2-z1)>=0.5: mv.append(['normal_move',rid,t2,x2,z2,0,'',True])
        if p1==p2 and p2 in('待機','往復おわり','卓で止まる') and np.hypot(x2-x1,z2-z1)<0.05: idle.append(['normal_idle',rid,t2,x2,z2,0,p2,False])
random.shuffle(mv); random.shuffle(idle); S+=mv[:250]+idle[:120]
def blobs(img):
    w=(img>235).all(2).astype(np.uint8); n,lab,st,cen=cv2.connectedComponentsWithStats(w)
    return [cen[i] for i in range(1,n) if 0.06*s*s<st[i,4]<0.4*s*s]
# 1回目：走っている所は、その時刻のコマでロボットを探して中心を決める
cen={}; want={}
for i,x in enumerate(S):
    if x[7]: want.setdefault(int(round(x[2]/0.5)),[]).append(i)
    else: cen[i]=(s*(x[3]+0.8),s*(OZ+0.8-x[4]))
cap=cv2.VideoCapture(mp4); f=0
while want and f<=max(want) and cap.grab():
    if f in want:
        ok,img=cap.retrieve(); bl=blobs(img)
        for i in want[f]:
            px,py=s*(S[i][3]+0.8),s*(OZ+0.8-S[i][4])
            dd=[(np.hypot(b[0]-px,b[1]-py),b) for b in bl]
            if dd and min(dd,key=lambda a:a[0])[0]<1.5*s: cen[i]=tuple(min(dd,key=lambda a:a[0])[1])
    f+=1
# 2回目：切り出す
need={}
for i in cen:
    fi=int(round(S[i][2]/0.5))
    for k,ff in enumerate((fi-20,fi-10,fi)): need.setdefault(ff,[]).append((i,k))
def crop(img,c):
    h=CROP_M*s/2; x0,y0=int(c[0]-h),int(c[1]-h); x1,y1=int(c[0]+h),int(c[1]+h)
    pad=cv2.copyMakeBorder(img,400,400,400,400,cv2.BORDER_CONSTANT,value=(100,100,100))
    return cv2.resize(pad[y0+400:y1+400,x0+400:x1+400],(SIZE,SIZE),interpolation=cv2.INTER_AREA)
parts={}; cap=cv2.VideoCapture(mp4); f=0
while f<=max(need) and cap.grab():
    if f in need:
        ok,img=cap.retrieve()
        for i,k in need[f]: parts[(i,k)]=crop(img,cen[i])
    f+=1
rows=[]; import re; tag=re.sub(r'_cs\d+_fam_p1_','_',run[9:]).split('_s7')[0]
for i,(lab,rid,t,x,z,dur,c,_) in enumerate(S):
    if any((i,k) not in parts for k in range(3)): continue
    a=parts[(i,2)]; b=np.dstack([cv2.cvtColor(parts[(i,k)],cv2.COLOR_BGR2GRAY) for k in range(3)])
    name=f'{tag}_r{rid}_t{t:.1f}.png'; grp='normal' if lab.startswith('normal') else lab
    for kind,im in (('A',a),('B',b)):
        p=f'{OUT}{kind}/{split}/{grp}/'; os.makedirs(p,exist_ok=True); cv2.imwrite(p+name,im)
    rows.append(dict(run=run,split=split,label=lab,robot=rid,t_s=round(t,2),x=x,z=z,cx_px=round(cen[i][0],1),cy_px=round(cen[i][1],1),stop_s=dur,cause=c,file=name))
os.makedirs(OUT+'index',exist_ok=True)
with open(OUT+f'index/index_{run}.csv','w',newline='') as fo:
    w=csv.DictWriter(fo,fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
from collections import Counter; print(tag,split,Counter(r['label'] for r in rows),'move dropped',len(mv[:250])-sum(r['label']=='normal_move' for r in rows))
```

### PatchCore で覚えさせて点数を付ける patchcore.py（全文）

```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
from sklearn.metrics import roc_auc_score

root, kind, out = sys.argv[1], sys.argv[2], sys.argv[3]
os.makedirs(out, exist_ok=True)
torch.manual_seed(0); random.seed(0); np.random.seed(0)
torch.set_num_threads(os.cpu_count())

model = wide_resnet50_2(weights=Wide_ResNet50_2_Weights.IMAGENET1K_V1).eval()
feats = {}
model.layer2.register_forward_hook(lambda m, i, o: feats.__setitem__('l2', o))
model.layer3.register_forward_hook(lambda m, i, o: feats.__setitem__('l3', o))
MEAN = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
STD = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)

def load(paths):
    x = np.stack([np.asarray(Image.open(p).convert('RGB'), dtype=np.float32) / 255 for p in paths])
    return (torch.from_numpy(x).permute(0, 3, 1, 2) - MEAN) / STD

@torch.no_grad()
def embed(paths, bs=16):
    """1枚を 28x28 の区画に分け、区画ごとの特徴（layer2+layer3、3x3でならす）を返す"""
    outs = []
    for i in range(0, len(paths), bs):
        model(load(paths[i:i + bs]))
        l2 = F.avg_pool2d(feats['l2'], 3, 1, 1)
        l3 = F.interpolate(F.avg_pool2d(feats['l3'], 3, 1, 1), size=l2.shape[-2:], mode='bilinear', align_corners=False)
        outs.append(torch.cat([l2, l3], 1))   # (b, 1536, 28, 28)
    return torch.cat(outs)

def coreset(X, n, proj_dim=128):
    """貪欲法で代表の区画だけ残す（PatchCore の記憶を小さくする）"""
    P = X @ torch.randn(X.shape[1], proj_dim) / proj_dim ** 0.5
    idx = [random.randrange(len(X))]
    d = torch.cdist(P, P[idx]).squeeze(1)
    for _ in range(n - 1):
        j = int(torch.argmax(d)); idx.append(j)
        d = torch.minimum(d, torch.cdist(P, P[j:j + 1]).squeeze(1))
    return X[idx]

def score(paths, bank, bs=8):
    """区画ごとに、記憶の中で一番近い区画までの距離。1枚の点数は最大値。地図も返す"""
    sc, maps = [], []
    for i in range(0, len(paths), bs):
        E = embed(paths[i:i + bs]); b, c, h, w = E.shape
        Q = E.permute(0, 2, 3, 1).reshape(-1, c)
        dmin = torch.cat([torch.cdist(Q[k:k + 4096], bank).min(1).values for k in range(0, len(Q), 4096)])
        m = dmin.view(b, h, w)
        sc += m.flatten(1).max(1).values.tolist(); maps.append(m)
    return np.array(sc), torch.cat(maps).numpy()

d = f'{root}/{kind}'
train = sorted(glob.glob(f'{d}/train/normal/*.png'))
test = {lab: sorted(glob.glob(f'{d}/test/{lab}/*.png')) for lab in ('normal', 'short', 'long')}
print(kind, 'train', len(train), {k: len(v) for k, v in test.items()}, flush=True)

# 区画を全部持つとメモリが足りないので、1枚あたり200区画を無作為に残してから代表を選ぶ
Xs = []
for i in range(0, len(train), 16):
    E = embed(train[i:i + 16]); c = E.shape[1]
    Q = E.permute(0, 2, 3, 1).reshape(E.shape[0], -1, c)
    for q in Q: Xs.append(q[torch.randperm(q.shape[0])[:200]])
X = torch.cat(Xs); n_all = len(train) * 28 * 28
bank = coreset(X, int(n_all * 0.01))
print('bank', tuple(bank.shape), flush=True)

res, maps = {}, {}
for lab, ps in test.items():
    res[lab], maps[lab] = score(ps, bank)

def auc(a, b):
    return float(roc_auc_score([0] * len(a) + [1] * len(b), np.r_[a, b]))
# しきい値は 普段 の確かめ画像の 95% 点（普段のうち5%を誤報にする置き方）
thr = float(np.quantile(res['normal'], 0.95))
summary = {
    'kind': kind, 'n_train': len(train), 'n_test': {k: len(v) for k, v in test.items()}, 'bank': list(bank.shape),
    'auroc_long_vs_normal': auc(res['normal'], res['long']),
    'auroc_short_vs_normal': auc(res['normal'], res['short']),
    'auroc_long_vs_short': auc(res['short'], res['long']),
    'threshold_p95_normal': thr,
    'hit_rate_long_at_thr': float((res['long'] > thr).mean()),
    'hit_rate_short_at_thr': float((res['short'] > thr).mean()),
    'false_alarm_normal_at_thr': float((res['normal'] > thr).mean()),
    'score_median': {k: float(np.median(v)) for k, v in res.items()},
}
print(json.dumps(summary, ensure_ascii=False, indent=1), flush=True)
json.dump(summary, open(f'{out}/summary_{kind}.json', 'w'), ensure_ascii=False, indent=1)
with open(f'{out}/scores_{kind}.csv', 'w', newline='') as fo:
    w = csv.writer(fo); w.writerow(['label', 'file', 'score'])
    for lab, ps in test.items():
        for p, s in zip(ps, res[lab]): w.writerow([lab, os.path.basename(p), round(float(s), 4)])
np.savez_compressed(f'{out}/maps_{kind}.npz', **{lab: maps[lab] for lab in maps},
                    **{f'{lab}_files': np.array([os.path.basename(p) for p in ps]) for lab, ps in test.items()})
```

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

```python
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（全文）

```python
# 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（全文）

```text
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（全文）

```text
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（全文）

```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（全文）

```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だけ教えて広げた店、今の②。自動で起こした店は狭く、紫の点線（人が運ぶ道）が多い](https://pixelstomotors.com/media/LOG418_06_sketch_to_layout.png)

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

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

| | 自動（家具の大きさで縮尺） | 自動＋店の幅だけ教える | 今の②（人が直した） |
|---|---|---|---|
| 店の大きさ | 16.4m × 12.1m | 24.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.1m | 10.0m・17.4m | 8.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.1m | 24.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.1m | 10.0m・17.4m | 8.5m・13.76m | 8.5m・13.77m |
| 卓の位置のずれ（②と比べて） | - | 中央値0.8m・最大2.4m | 19卓とも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（全文）

````markdown
# 作業：手描きの見取り図から、ファミレスの店の形のデータを起こす

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

## 使ってよいもの（このフォルダの中だけ）
- `sketch.jpg`：昔働いていたファミレスを記憶から手で描いた見取り図。寸法は書かれていない
- `example_p1_efficient_corner.json`：別の店（③）の店の形のデータ。書き方の見本。中身（卓の並び・寸法）はこの店とは関係ない
- `evaluate_layout.py`：起こした店の形を、サイトと同じ決まり・同じ計算で確かめる道具。`python3 evaluate_layout.py <JSON> <出力フォルダ>`。決まりの違反と、卓ごとのロボットの届き方、図（plan.png）を出す
- 形の解釈を確かめるために `/home/claude/p2m-site/tools/sim_layouts/famires_v3.py` の関数（fp・lsofa_sides・t2_sides・table_sides・check_rules など）は読んでよい

## 見てはいけないもの（比べる相手なので、見ると試しにならない）
- `/home/claude/p2m-site/public/sim/layouts/` と `/home/claude/p2m-site/public/sim/layouts/unity/` の p0 で始まるファイル
- `/home/claude/p2m-site/tools/sim_layouts/` の `layout_v11.json`・`famires_p0.py`・`README.md`
- `/mnt/user-data/uploads/` の中の LOG360 の記事・ほかの画像（sketch.jpg と同じ写真以外）
- そのほか、この店を以前に起こしたもの

## 起こし方の前提
- 入力は3つだけ：(1) この図面、(2) 標準の家具の大きさ（下の家具の種類）、(3) 図面から読めるキッチンまわりの点（受け渡し口・店員の出入り口・洗い場）
- 寸法がないので、縮尺は自分で決める。標準の家具の大きさ（4人のボックス席は1.2m×1.85mなど）を手がかりにする。決め方を記録する
- 図面に描かれていないことは、ファミレスとしてふつうの形で仮に置き、仮に置いたことを記録する（例：受け渡し口の位置、ロボットの待機場所、扉の有無）
- 卓の数・席数は図面に書かれた数字のとおりにする。図面の形と家具の種類が合わないときは、近い種類を選び、そのことを記録する
- 決まりの違反が出たら、図面の意図からなるべく離れない最小の動かし方で直してよい（これも自動の範囲）。直した所はすべて記録する

## データの形（famires-layout/0.3 の layout 部分）
単位はm。原点は店の外壁の内側の左下の角。x は右、y は奥（上）。rect は [x0, y0, x1, y1]。
- `name`・`style`（"効率重視" か "ゆったり"）
- `outer`：{w, d} 店の外形
- `outline`（なくてよい）：建物が四角でないとき、輪郭の点の並び（縦と横の辺だけ）。使うなら、輪郭の外を `rooms` に kind="outside" の部屋として入れる（両方が同じ所を表していることを確かめる決まりがある）
- `entrance`：{side: bottom/top/left/right, a0, a1} 入口がその辺のどこからどこまでか
- `rooms`：{label, rect, kind}。kind は back（厨房側・レジ内など客が入らない）、guest（トイレ・レジ・待合）、drink（ドリンクバー）、outside（建物の外）。レジは guest の「レジ」と back の「レジ内」の2つ
- `walls`：{label, rect} 壁・仕切り・柱
- `doors`（なくてよい）：店員の出入り口 {label, rect, kind:"staff", door: true/false}。壁の切れ目
- `toilet_doors`：トイレの入口 [x0,y0,x1,y1] の並び
- `dish_up`：受け渡し口 [{label, rect}]（1つ目を使う）
- `robot_standby`：{label, rect, slots} ロボットの待機・充電の場所。1台あたり0.8m×0.8m以上
- `tables`：{type, x, y, w, h, seats, id, …}。x, y は外形の左下。type ごとの追加：booth は axis（v/h）、t2 は side（壁がある側 bottom/top/left/right）、booth6L は flip・mirror（向き。famires_v3.py の lsofa_sides を読むこと）
- `start` は道具が受け渡し口から自動で決めるので書かなくてよい

## 家具の種類（標準の大きさ）
- booth：ボックス席。向かい合うベンチ2つと間のテーブル。4人は1.2m×1.85m、6人は1.8m×1.85m（長い辺が座る側）。axis v はベンチが上下（座る側が左右）、h はベンチが左右
- t2：2人席0.8m×1.4m。壁側がベンチ、通路側が椅子
- booth6L：L字ソファの6人席。内側の角にテーブル
- t4：4人の角テーブル（外形1.2m四方）。椅子は2脚ずつ向かい合わせ
- r4：4人の丸テーブル（外形1.1m四方・天板の直径0.8m）
- long6：6人の長テーブル（外形3.2m×1.94m、天板3.2m×0.7m、上下にスツール3つずつ）
- 上にない形（カウンター席・8人席など）は、近い種類で置くか、置けなければ置かずに記録する

## 決まり（evaluate_layout.py が確かめるもの）
- ロボット（BellaBot）が通れる幅0.8m、ふだん走らせる幅1.0m、人が通れる幅0.5m
- 入口から1.5m、受け渡し口の前1.2m、トイレの入口の前1.2mには何も置かない
- トイレから1m以内に席を置くなら、席とトイレの入口の間に壁
- レジは2.5m以上。会計に立つ点・並ぶ点はロボットの道から0.75m以上
- 待機場所の各台が、ほかの台が止まったままで受け渡し口の前に出られること

## 出すもの（`/home/claude/blind/out/` に）
1. `auto_v0.json`：図面を読んで最初に起こしたもの（決まりで直す前）と、その確かめの結果 `check_v0/`
2. `auto_final.json`：決まりの違反を直したもの（違反0）と、その確かめの結果 `check_final/`
3. `notes.md`：
   - 縮尺の決め方
   - 図面から読めたこと（卓の数・席数・部屋・入口など）
   - 図面に描かれておらず、仮に置いたこと
   - 決まりの違反と、どう直したか（v0 → final）
   - 図面と家具の種類が合わなかった所
   - 店の人に聞けば確かになること（質問の形で）
````

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

```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
fx = lambda px: round((px - PX0) / SX, 2)
fy = lambda py: round((PY0 - py) / SY, 2)
W, D = 16.4, 12.1          # 右の壁 1495px → 16.44m、上の壁 115px → 12.09m

def centered(px0, px1, py0, py1, w, h):
    """画素の箱の真ん中に、標準の大きさ w×h（m）の家具を置く"""
    cx, cy = (fx(px0) + fx(px1)) / 2, (fy(py0) + fy(py1)) / 2
    return round(cx - w / 2, 2), round(cy - h / 2, 2)

def t4(px0, px1, py0, py1, bottom=False):
    x, y = centered(px0, px1, py0, py1, 1.2, 1.2)
    if bottom:
        y = 0.0
    return dict(type="t4", x=x, y=y, w=1.2, h=1.2, seats=4)

def r4(px0, px1, py0, py1):
    x, y = centered(px0, px1, py0, py1, 1.1, 1.1)
    return dict(type="r4", x=x, y=y, w=1.1, h=1.1, seats=4)

def lsofa(px1, py0, py1, flip, top=False, bottom=False):
    """L字席：種類に標準の大きさがないので、図の外形（左の壁に付けて描かれている）の大きさをそのまま使う（0.05m に丸める）"""
    r = lambda v: round(round(v / 0.05) * 0.05, 2)
    ytop, ybot = fy(min(py0, py1)), fy(max(py0, py1))
    w, h = r(fx(px1)), r(ytop - ybot)
    y = D - h if top else (0.0 if bottom else r(ybot))
    return dict(type="booth6L", x=0.0, y=round(y, 2), w=w, h=h, seats=6, flip=flip, mirror=False)

def v0():
    tables = [
        lsofa(235, 323, 115, True, top=True),       # 左上の6（帯が上・列が左 → 開いた角は右下）
        t4(300, 410, 135, 225), t4(463, 573, 133, 222),
        r4(252, 350, 288, 373), r4(427, 535, 288, 370),
        lsofa(195, 560, 357, False),                # 左の真ん中の6（列が左・帯が下 → 開いた角は右上）
        t4(263, 373, 460, 555), t4(470, 585, 463, 562),
        lsofa(300, 867, 633, True),                 # 仕切りの下の6（帯が上）
        t4(345, 463, 633, 740), t4(500, 603, 639, 730),
        dict(type="long6", x=round((fx(285) + fx(570)) / 2 - 1.6, 2), y=round((fy(793) + fy(947)) / 2 - 0.97, 2),
             w=3.2, h=1.94, seats=6),
        lsofa(268, 1070, 810, False, bottom=True),  # 左下の6（列が左・帯が下）
        t4(340, 447, 990, 1070, bottom=True), t4(575, 665, 997, 1070, bottom=True),
        t4(938, 1042, 1000, 1070, bottom=True), t4(1142, 1238, 1003, 1070, bottom=True),
        # 右下の8（U字・左が開いている）→ ボックス席（ベンチが上下・右は壁）の6人の大きさで、席数は8
        dict(type="booth", axis="v", x=round(W - 1.8, 2), y=0.0, w=1.8, h=1.85, seats=8),
    ]
    toilet = [6.27, 11.33, 7.71, 12.1]      # 590–718px, 175px〜上の壁
    rooms = [
        dict(label="パントリー", rect=[8.07, 3.5, 9.35, 10.1], kind="back"),
        dict(label="キッチン", rect=[9.35, 3.52, 15.12, 10.1], kind="back"),
        dict(label="冷凍庫・保存庫", rect=[15.12, 3.52, 16.4, 10.1], kind="back"),
        dict(label="洗い場", rect=[10.09, 10.1, 15.26, 12.1], kind="back"),
        dict(label="倉庫", rect=[15.26, 10.1, 16.4, 12.1], kind="back"),
        dict(label="トイレ", rect=toilet, kind="guest"),
        dict(label="ドリンクバー", rect=[7.8, 11.52, 9.84, 12.1], kind="drink"),
        dict(label="待合", rect=[9.55, 0.0, 10.1, 1.2], kind="guest"),
        # レジ：図の箱（710–870px, 928–980px）がカウンター。店員はパントリー側（上）に立つ
        dict(label="レジ", rect=[fx(710), fy(980), fx(870), fy(928)], kind="guest"),
        dict(label="レジ内", rect=[fx(710), fy(928), fx(870), round(fy(928) + 0.5, 2)], kind="back"),
    ]
    walls = [
        dict(label="仕切り", rect=[0.0, fy(607), fx(615), fy(577)]),
        dict(label="パントリーの壁", rect=[fx(702), fy(824), fx(750), 10.1]),
        dict(label="キッチンの壁", rect=[fx(914), fy(840), W, fy(792)]),
        dict(label="トイレの壁", rect=[6.27, 11.33, 6.57, 12.1]),
        dict(label="トイレの壁", rect=[7.41, 11.33, 7.71, 12.1]),
    ]
    doors = [
        dict(label="パントリーの下の出入り口", rect=[8.07, 3.4, 9.35, 3.6], kind="staff", door=False),
        dict(label="パントリーの上の出入り口", rect=[8.07, 10.0, 9.35, 10.2], kind="staff", door=False),
        dict(label="洗い場の出入り口", rect=[9.99, 10.66, 10.19, 11.5], kind="staff", door=True),
    ]
    return dict(
        name="⓪ 手描きの見取り図から自動で起こした店（記憶のファミレス）",
        style="効率重視",
        outer=dict(w=W, d=D),
        entrance=dict(side="bottom", a0=fx(710), a1=fx(875)),
        rooms=rooms, walls=walls, doors=doors,
        toilet_doors=[[6.57, 10.13, 7.41, 11.33]],
        dish_up=[dict(label="受け渡し口", rect=[8.07, 3.5, 9.35, 3.9])],
        robot_standby=dict(label="ロボット待機・充電", rect=[15.6, 2.0, 16.4, 2.8], slots=1),
        tables=tables,
    )

def final():
    L = copy.deepcopy(v0())
    T = L["tables"]
    # 1) 仕切りの下の6（T09）と左下の6（T13）が重なる・T09 と長テーブル（T12）が重なる・T09/T13 の座る側がない
    #    → 2つのL字席を背中合わせに接して、仕切りの下に収める（T13 の奥行き 3.3→2.9m、T09 を y=2.9 へ）。
    #      T09 の幅を T13 と同じ 2.65m に（図では T09 の上の帯が長テーブルの上にかぶさって描かれている）。
    #      長テーブルを右へ 0.31m（L字席との間を人が通れる 0.5m に）
    T[12]["h"] = 2.9
    T[8].update(y=2.9, w=2.65)
    T[11]["x"] = round(T[8]["x"] + T[8]["w"] + 0.5, 2)
    # 2) トイレの入口から1m以内の T03（上の段の右の4人席）→ 入口の左に目隠しの壁を立てる
    L["walls"].append(dict(label="トイレの目隠しの壁", rect=[6.27, 10.13, 6.57, 11.33]))
    # 3) レジ：入口から1.5m以内・長さ2.5m未満・受け渡し口の前 → 入口の1.5m先へ上げ、2.5mに伸ばす。
    #    パントリーの壁の下の通路と、キッチンの壁の下の通路をふさがないよう、カウンター0.4m・店員の立つ所0.4mにし、
    #    右の端をキッチンの壁の角から1m離す（9.1m）
    for r in L["rooms"]:
        if r["label"] == "レジ":
            r["rect"] = [6.6, 1.5, 9.1, 1.9]
        if r["label"] == "レジ内":
            r["rect"] = [6.6, 1.9, 9.1, 2.3]
    return L

if __name__ == "__main__":
    out = Path("/home/claude/blind/out")
    for nm, L in (("auto_v0.json", v0()), ("auto_final.json", final())):
        json.dump({"layout": L}, open(out / nm, "w"), ensure_ascii=False, indent=1)
        print(nm, len(L["tables"]), "卓", sum(t["seats"] for t in L["tables"]), "席")
```

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

```python
# 図面から起こした店の形（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（全文）

```python
# 店の幅を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 の出力（全文）

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

### 手描き＋店の幅と今の②の卓を1つずつ対応させる match.py（全文）

```python
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 の出力（全文）

```text
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（全文）

````markdown
# 手描きの見取り図から店の形を自動で起こした記録

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

- `auto_v0.json`：図を読んで最初に起こしたもの。違反7件（`check_v0/`）
- `auto_final.json`：違反を直したもの。違反0件（`check_final/`）

## 1. 縮尺の決め方

- 写真の上で、標準の大きさが決まっている家具の、描かれた大きさを画素で測った
  - 4人の角テーブル（1.2m四方）：横の平均 106px、縦の平均 95px（下の段の4つは壁で切れているので縦は除いた）
  - 4人の丸テーブル（1.1m四方）：横 103px、縦 84px
  - 6人の長テーブル（外形 3.2m×1.94m）：横 285px、縦 154px
- 3種類とも、横は約 89px/m、縦は約 79px/m でそろった。図は横に1割ほど引き伸ばされて描かれている（手描きのくせか、写真の傾き）ので、横と縦で別の縮尺を使った
  - 横 89px/m、縦 79px/m。原点（外壁の内側の左下）は画素 (32, 1070)
- 外壁は 横 1463px × 縦 955px → **16.4m × 12.1m**
- 確かめ：入口の幅 1.85m、カウンターの丸椅子の間隔 0.72m、長テーブルの丸椅子の間隔 1.18m。どれもファミレスとしてふつうの値
- 家具は、図の箱の真ん中に標準の大きさで置いた。壁に付けて描かれている家具（左の壁のL字席、下の壁の4人席、右の壁の8人席）は壁に付けた
- L字席は種類に標準の大きさがないので、4つとも図の外形の大きさをそのまま使った（0.05m に丸めた）

## 2. 図面から読めたこと

### 卓と席（図に書かれた数字のとおり）

| 図の数字・形 | 数 | 種類 | 席 |
|---|---|---|---|
| 「6」のL字（左の壁ぞい） | 4 | booth6L | 24 |
| 「4」の四角 | 10 | t4 | 40 |
| 「4」の丸 | 2 | r4 | 8 |
| 丸椅子が上下に3つずつの長いテーブル（数字なし） | 1 | long6 | 6 |
| 「8」のU字（右下・左が開いている） | 1 | booth（ベンチが上下・右は壁）、席数8 | 8 |
| **合計** | **18卓** | | **86席** |
| 「カウンタ」と丸椅子5つ | 置いていない（5章） | | （5席） |

L字席の向き：左上と仕切りの下の2つは「帯が上・縦の列が左」なので、開いた角（テーブルのある所）は右下（flip=true）。左の真ん中と左下の2つは「縦の列が左・帯が下」なので、開いた角は右上（flip なし）。

### 部屋・壁・入口

- 入口：下の辺の点線、x=7.6〜9.5m
- 仕切り：斜線の帯。左の壁から x=6.55m まで、y=5.86〜6.24m。仕切りの右端からパントリーの壁までの 0.98m は、上の線だけで斜線がないので通り道（上と下の客席をつなぐ）と読んだ
- パントリーの壁：斜線の縦の帯、x=7.53〜8.07m、y=3.11〜10.1m
- パントリー：パントリーの壁とキッチンの受け渡し台の間。下の端（y≒3.5m）が客席に開いている
- キッチン（冷蔵庫・こおり・斜線の調理台を含む）、冷凍庫・保存庫（右の壁ぞい）、洗い場（上の壁ぞい）、倉庫（右上）は、すべて客が入らない裏の部屋にまとめた
- キッチンの下の壁：斜線の横の帯、x=9.91m〜右の壁、y=2.91〜3.52m
- トイレ：上の壁ぞい、x=6.27〜7.71m、y=11.33〜12.1m（図の箱のとおり）
- レジ：入口のすぐ内側の箱（x=7.62〜9.42m、y=1.14〜1.80m、長さ1.8m）
- 右の壁の外の縦書き（矢印つき）は読み取れなかった。「ウォークイン」に近く見えるので、指している斜線の所（保存庫の下）はウォークインの冷蔵庫と読み、裏の部屋に含めた

### キッチンまわりの点

- 受け渡し口：パントリーの下の端（x=8.07〜9.35m、y=3.5〜3.9m）。キッチンから受け渡し台ごしに料理がパントリーへ出て、パントリーの下の口から客席へ出る形と読んだ。ロボットの出発点は道具が決めて (8.71, 2.95)
- 店員の出入り口：パントリーの下の口（扉なし）、パントリーの上の端（上の小部屋に開いている・扉なし）、上の小部屋から洗い場へ入る点線（扉あり）
- 洗い場：上の壁ぞい（図の「洗い場」）

## 3. 図面に描かれておらず、仮に置いたこと

- **ドリンクバー**：トイレの右の、上の壁に付いた細長い箱（x=7.8〜9.84m、y=11.52〜12.1m）をドリンクバーと読んだ。箱の前の小部屋は、左の点線（開いた境目）から客が入れる所とした
- **トイレの大きさと入口**：大きさは図の箱のまま（1.44m×0.77m・1室）。入口は下の辺（客席の側）の真ん中、幅0.84m。左右の辺に厚さ0.3mの壁を置いた
- **レジの店員が立つ所（レジ内）**：パントリーの側（上）とした。パントリーから出てすぐの所で、客は入口の側（下）から会計する形
- **待合**：入口の右、下の壁ぞいに長椅子（x=9.55〜10.1m、y=0〜1.2m）
- **ロボットの待機・充電の場所**：1台分（0.8m×0.8m）を、8人席の上のすき間（x=15.6〜16.4m、y=2.0〜2.8m）に置いた。受け渡し口の近くはパントリーの壁・レジ・キッチンの壁の間が狭く、0.8m四方を置くとロボットの通り道がふさがるため。台数は1台
- **扉**：入口は扉ありとして扱い（点線）、店員の出入り口の扉の有無は上のとおり。洗い場への点線は扉ありにした
- **8人席**：U字のソファは種類にないので、ボックス席（ベンチが上下、右は壁）の6人の大きさ 1.8m×1.85m で置き、席数は図の8にした（5章）
- 店の形は四角とした（トイレの箱が上の壁の線から少しはみ出して描かれているが、描き方のずれとみなした）

## 4. 決まりの違反と、直したこと（v0 → final）

v0 の違反7件：

1. 仕切りの下のL字席（T09）と左下のL字席（T13）が重なる（図の上でも重なって描かれている）
2. T09 と長テーブル（T12）が重なる（T09 の上の帯が長テーブルの上にかぶさって描かれている）
3. レジが入口から1.5m以内にある
4. 上の段の右の4人席（T03）がトイレから0.21m で、目隠しの壁がない
5. レジが2.5mより短い（1.8m）
6. T09 の座る側がすべてふさがっている
7. T13 の座る側がすべてふさがっている

直したこと（図の意図からなるべく離れない最小の動かし方。ほかの卓・部屋は v0 のまま）：

| 直した所 | v0 | final | 理由 |
|---|---|---|---|
| T13（左下のL字席）の奥行き | 3.3m | 2.9m | 1・7。下の2つのL字席を背中合わせに接して、仕切りの下（y=5.86m）に収めるため。T13 は4つのL字席の中で一番大きく描かれていた |
| T09（仕切りの下のL字席） | y=2.55m・幅3.0m | y=2.9m・幅2.65m | 1・2・6。T13 の上に接して置き、幅を T13 と同じにした |
| T12（長テーブル） | x=2.84m | x=3.15m（右へ0.31m） | 2・6・7。L字席の右の辺との間を、人が通れる 0.5m にした |
| トイレの目隠しの壁 | なし | x=6.27〜6.57m、y=10.13〜11.33m（入口の前の空ける所の左に接して） | 4。T03 の右の辺の両端から入口の両端への4本の線が、すべてこの壁かトイレの左の壁に当たる |
| レジ | x=7.62〜9.42m、y=1.14〜1.80m（長さ1.8m） | x=6.6〜9.1m、y=1.5〜1.9m（長さ2.5m） | 3・5。入口から1.5m先へ上げ、2.5mに伸ばした |
| レジ内（店員の立つ所） | y=1.80〜2.30m（0.5m） | y=1.9〜2.3m（0.4m） | 3・5。下の2つを守るために、カウンター0.4m・店員の立つ所0.4m に薄くした |

レジの置き方の考え方（ここが一番、図の形と決まりがぶつかった所）：

- 入口から1.5mは空ける決まりなので、レジは y=1.5m より上にしか置けない。上には受け渡し口の前（1.2m）とパントリーの壁の下の端（y=3.11m）がある
- レジの上（店員の立つ所の上）を、ロボットがパントリーから左の客席へ抜ける。ここを 0.8m 以上残すために、カウンターと店員の立つ所を合わせて 0.8m にした（残りは 0.81m で、ロボットは「狭い道なら通れる」）
- 右の端は、キッチンの壁の左下の角から 1m 離した（x=9.1m）。これでキッチンの壁の下を右の客席へ行く道は、ふだん走らせる幅 1.0m のまま残った。左へは 2.5m に足りるまで伸ばした（長テーブルとの間 0.25m）

## 5. 図面と家具の種類が合わなかった所

- **カウンター席（丸椅子5つ）**：種類にない。近い長テーブル（3.2m×1.94m・両側に丸椅子）は、キッチンの壁（y=2.91m）と下の段の4人席（上の端 y=1.2m）の間の 1.71m に入らないので、置かなかった。席数86 にカウンターの5席は入っていない。カウンターの場所は通路として空いている
- **8人のU字席**：ボックス席（6人の大きさ）で代わりにした。右の壁ぞいのベンチ（U字の底）は形に入っていない。座る側は左だけで、図と同じ
- **L字席の大きさ**：種類に標準の大きさがないので、図の外形から決めた（final で 左上 2.3×2.65、左の真ん中 1.85×2.55、仕切りの下 2.65×2.95、左下 2.65×2.9m）。左上と左の真ん中の間は 0.45m しかないが、左の真ん中の幅が 1.85m と狭いので、左上の座る側（下）の右の端には人が立てる。L字席の形は外形の四角で扱うので、図で長テーブルの上にかぶさっていた T09 の上の帯は表せず、幅を縮めた
- **下の段の4人席**：図では壁に押されて縦 0.9m ほどに描かれているが、標準の 1.2m 四方で置いた。そのぶん、その上の長テーブル・カウンターとの間が図より狭い

## 6. 結果（final）

- 違反 0、18卓・86席、マス目 2.5cm と 1.25cm で卓の分け方が同じ
- まとめ：ロボットが届く 3卓・16席／狭い道なら届く 8卓・36席／人が運ぶ 7卓・34席。受け渡し口の前から止まる位置まで 平均6.6m・最長11.08m。人が運ぶ分の歩く距離 合計47.7m・最長11.22m
- ふだんの幅（1.0m）で届くのは、キッチンの壁の下を右へ行った3卓（右下の4人席2つと8人席）だけ
- 左半分（上の客席と仕切りの下の客席）は、レジの店員の立つ所とパントリーの壁の間（0.81m）を通るので、届いても「狭い道」になる。どの道も一番狭い所が 0.78〜0.79m
- 人が運ぶ7卓：L字席4つのうち3つ（T01・T09・T13）、T03（トイレの横）、T10、下の段の左の4人席2つ（T14・T15）

## 7. 店の人に聞けば確かになること

1. 店の外形の実際の幅と奥行きは何mですか（いまは家具の大きさから 16.4m×12.1m と決めた）
2. レジの店員はカウンターのどちら側に立ちますか。パントリーから左の客席へ料理を運ぶとき、レジの裏を通りますか
3. パントリーの壁の下の端と、レジの間は何mありますか（ここがロボットの左の客席への通り道で、いまは 0.81m）
4. 仕切りの右端とパントリーの壁の間は通れますか（いまは通れる幅 0.98m と読んだ）。上の客席へは、ほかに道がありますか
5. トイレの右の細長い箱はドリンクバーですか。違うなら、ドリンクバーはどこにありますか
6. トイレの入口はどちら向きですか。トイレは男女別に2室ありますか、大きさはどのくらいですか
7. 右の壁の外に書かれた言葉（矢印で保存庫の下の斜線の所を指している）は何ですか
8. カウンターは何席ですか。カウンターとキッチンの壁の間は、店員が入る所ですか
9. 8人席はU字のソファですか。右の壁ぞいにもベンチがありますか
10. 待合の長椅子はどこにありますか
11. 下の2つのL字席は、背中合わせに付いていますか
12. 配膳ロボットを入れるなら、待機・充電はどこに置けますか（受け渡し口の近くには 0.8m 四方を置く余地がない）
````

### 寸法つきの平面図を描く draw_plan.py（全文）

```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

SRC = "/home/claude/p2m-site/public/sim/layouts/unity/p0_memory.json"
U = json.load(open(SRC))
plt.rcParams["font.family"] = "Noto Sans CJK JP"
K = "#111111"
fig, ax = plt.subplots(figsize=(16, 13.5), dpi=150)
ax.set_aspect("equal"); ax.axis("off")
W, D = U["outer"]["w"], U["outer"]["d"]

def rect(r, lw=0.8, fc="none", ec=K, z=2, hatch=None):
    ax.add_patch(Rectangle((r[0], r[1]), r[2] - r[0], r[3] - r[1], fc=fc, ec=ec, lw=lw, zorder=z, hatch=hatch))

def label(r, s, fs=9, rot=0):
    ax.text((r[0] + r[2]) / 2, (r[1] + r[3]) / 2, s, ha="center", va="center", fontsize=fs, rotation=rot, zorder=6,
            bbox=dict(fc="white", ec=K, lw=0.6, pad=2))

# 外壁（入口は開ける）
for r in U["outer_walls"]:
    rect(r, lw=0.6, fc=K, z=3)
# 内壁・仕切り
for w in U["walls"]:
    tall = w.get("height", 2.7) > 2.0
    rect(w["rect"], lw=0.6, fc=K if tall else "white", z=3, hatch=None if tall else "////")
# 部屋（厨房側は輪郭と名前だけ。建物の外は描かない）
for r in U["rooms"]:
    if r["kind"] == "outside":
        continue
    lab = r["label"]
    if lab in ("レジ", "ドリンクバー", "待合"):
        rect(r["rect"], lw=1.0, z=4)
        label(r["rect"], {"レジ": "レジ", "ドリンクバー": "ドリンクバー", "待合": "待合"}[lab], fs=8)
    elif lab == "レジ内":
        continue
    else:
        rect(r["rect"], lw=1.2, z=2)
        x0, y0, x1, y1 = r["rect"]
        label(r["rect"], lab, fs=10 if (x1 - x0) > 1.5 else 8, rot=90 if (y1 - y0) > 2.5 * (x1 - x0) else 0)
# 料理の受け渡し口（台）
for p in U.get("places", []):
    pass
for d in json.load(open("/home/claude/p2m-site/public/sim/layouts/p0_memory.json"))["layout"]["dish_up"]:
    rect(d["rect"], lw=1.2, fc="white", z=5, hatch="xxxx")
    x0, y0, x1, y1 = d["rect"]
    ax.text((x0 + x1) / 2, y0 - 0.35 if "下" in d["label"] else y1 + 0.35, "受け渡し口", ha="center", va="center", fontsize=8, zorder=6)
# 店員の出入り口（扉ありは開く向き、なしは開口）
for d in U["doors"]:
    x0, y0, x1, y1 = d["rect"]
    rect(d["rect"], lw=0, fc="white", ec="white", z=4)
    if d["door"]:
        w = x1 - x0
        ax.add_patch(Arc((x0, y0), 2 * w, 2 * w, theta1=270, theta2=360, lw=0.7, ec=K, zorder=5))
        ax.plot([x0, x0], [y0, y0 - w], color=K, lw=0.9, zorder=5)
# トイレの入口
L0 = json.load(open("/home/claude/p2m-site/public/sim/layouts/p0_memory.json"))["layout"]
for t in L0["toilet_doors"]:
    x0, y0, x1, y1 = t
    rect(t, lw=0, fc="white", ec="white", z=4)
    w = x1 - x0 if (x1 - x0) > (y1 - y0) else y1 - y0
    if (x1 - x0) >= (y1 - y0):
        ax.add_patch(Arc((x0, y0), 2 * w, 2 * w, theta1=270, theta2=360, lw=0.7, ec=K, zorder=5))
    else:
        ax.add_patch(Arc((x0, y0), 2 * w, 2 * w, theta1=0, theta2=90, lw=0.7, ec=K, zorder=5))
# 入口の扉（両開き）
e = U["entrance"]
a0, a1 = e["a0"], e["a1"]; h = (a1 - a0) / 2
ax.add_patch(Arc((a0, 0), 2 * h, 2 * h, theta1=270, theta2=360, lw=0.7, ec=K, zorder=5))
ax.add_patch(Arc((a1, 0), 2 * h, 2 * h, theta1=180, theta2=270, lw=0.7, ec=K, zorder=5))
ax.plot([a0, a0], [0, -h], color=K, lw=0.9); ax.plot([a1, a1], [0, -h], color=K, lw=0.9)
ax.text((a0 + a1) / 2, -1.6, "入口", ha="center", va="center", fontsize=11,
        bbox=dict(fc="white", ec=K, lw=0.6, pad=2))
# 卓：天板・椅子・ソファ
for t in U["tables"]:
    P = t["parts"]
    for b in P.get("benches", []):
        rect(b, lw=0.8, z=4)
    top = P["top"]
    if P.get("top_shape") == "circle":
        ax.add_patch(Circle(((top[0] + top[2]) / 2, (top[1] + top[3]) / 2), (top[2] - top[0]) / 2, fc="white", ec=K, lw=0.9, zorder=5))
    else:
        rect(top, lw=0.9, fc="white", z=5)
    for c in P.get("chairs", []):
        r = c["rect"]
        if c.get("stool"):
            ax.add_patch(Circle(((r[0] + r[2]) / 2, (r[1] + r[3]) / 2), (r[2] - r[0]) / 2, fc="white", ec=K, lw=0.7, zorder=4))
        else:
            rect(r, lw=0.7, fc="white", z=4)

# 寸法線（mm）
def dim_h(xs, y, tick=0.25, fs=9):
    ax.plot([xs[0], xs[-1]], [y, y], color=K, lw=0.6)
    for x in xs:
        ax.plot([x, x], [y - tick, y + tick], color=K, lw=0.6)
    for a, b in zip(xs, xs[1:]):
        ax.text((a + b) / 2, y + 0.3, f"{round((b - a) * 1000)}", ha="center", va="bottom", fontsize=fs)
def dim_v(ys, x, tick=0.25, fs=9):
    ax.plot([x, x], [ys[0], ys[-1]], color=K, lw=0.6)
    for y in ys:
        ax.plot([x - tick, x + tick], [y, y], color=K, lw=0.6)
    for a, b in zip(ys, ys[1:]):
        ax.text(x - 0.3, (a + b) / 2, f"{round((b - a) * 1000)}", ha="right", va="center", fontsize=fs, rotation=90)
def guide(x0, y0, x1, y1):
    ax.plot([x0, x1], [y0, y1], color=K, lw=0.4, ls=(0, (6, 4)), zorder=1)

xs = [0.0, 7.6, 9.8, 12.0, 19.0, 24.0]          # 通り芯（建物の角・パントリー・キッチンの境）
ys_l = [0.0, 8.6, 17.0]                          # 左：仕切り・建物の上の端
ys_r = [0.0, 6.0, 17.0, 19.2]                    # 右：厨房の下の端・洗い場の下の端
for x in xs:
    guide(x, -0.6, x, D + 1.6)
for y in sorted(set(ys_l + ys_r)):
    guide(-1.6, y, W + 1.6, y)
dim_h(xs, D + 1.2)
dim_v(ys_l, -1.2)
dim_v(ys_r, W + 1.6)
ax.text(W + 2.3, D / 2, f"{round(D * 1000)}", rotation=90, ha="left", va="center", fontsize=10)
dim_h([0.0, a0, a1, W], -2.6)
dim_h([0.0, W], -3.5)
ax.text(W / 2, -4.3, "単位 mm　点線は通り芯", ha="center", fontsize=9)

ax.set_xlim(-3.0, W + 3.4); ax.set_ylim(-4.8, D + 2.4)
fig.savefig(sys.argv[1], bbox_inches="tight", facecolor="white")
print("ok", sys.argv[1])
```

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

````markdown
# 作業：寸法つきの図面から、ファミレスの店の形のデータを起こす

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

## 使ってよいもの（このフォルダの中だけ）
- `drawing.png`：ファミレスの平面図。寸法は mm。上・左・右・下の辺に寸法線がある。点線は通り芯。家具（天板・椅子・ソファ）は縮尺どおりに描かれている
- `example_p1_efficient_corner.json`：別の店の店の形のデータ。書き方の見本。中身はこの店とは関係ない
- `evaluate_layout.py`：起こした店の形を、決まり・ロボットの届き方の計算で確かめる道具。`python3 evaluate_layout.py <JSON> <出力フォルダ>`
- 形の解釈を確かめるために `/home/claude/p2m-site/tools/sim_layouts/famires_v3.py` の関数（fp・lsofa_sides・t2_sides・table_sides・check_rules・check_outline など）は読んでよい（書き換えない）

## 見てはいけないもの（比べる相手なので、見ると試しにならない）
- `/home/claude/p2m-site/public/sim/layouts/` と `/home/claude/p2m-site/public/sim/layouts/unity/` の p0 で始まるファイル
- `/home/claude/p2m-site/tools/sim_layouts/` の `layout_v11.json`・`famires_p0.py`・`README.md`
- `/home/claude/blind/`・`/home/claude/blind3/`・`/home/claude/drawn2/` の中のもの
- `/mnt/user-data/` の中のもの

## 起こし方の前提
- 入力は3つだけ：(1) この図面、(2) 標準の家具の大きさ（下の家具の種類）、(3) 図面から読めるキッチンまわりの点
- 寸法は図面の数字を正とする。まず数字から画素あたりの mm を決め、縦と横で縮尺が違わないかを確かめて記録する
- 卓の位置・大きさ・席数は、図面に描かれたとおりに読む（椅子の数で席数を決める）。家具の種類に合わない形は、近い種類で置き、元の形と大きさ・置いた種類・ずれを記録する
- 図面に描かれていないことは、ふつうの形で仮に置き、仮に置いたことを記録する。特に：
  - ロボットが料理を受け取る所（dish_up）：図面の「受け渡し口」を使う。複数あれば、ロボットが客席へ出やすい方を1つ目にし、理由を記録する
  - ロボットの待機場所：受け渡し口の近くで、ロボットの道をふさがない所
  - ドリンクバー：図面にあれば rooms に kind="drink" で入れる
- 決まりの違反が出たら、図面の意図からなるべく離れない最小の動かし方で直してよい。直した所はすべて記録する
- 図面にある通路の幅（卓と卓、卓と壁の間）は、図面から読んだ値を何か所か記録する（ロボットが通れるかに効くため）

## データの形（famires-layout/0.3 の layout 部分）
単位はm。原点は店の外形（outer）の左下の角。x は右、y は奥（上）。rect は [x0, y0, x1, y1]。
- `name`・`style`（"効率重視" か "ゆったり"）
- `outer`：{w, d} 店をすっぽり囲む四角
- `outline`：建物が四角でないとき、輪郭の点の並び（縦と横の辺だけ）。使うなら、輪郭の外を `rooms` に kind="outside" の部屋として入れる（両方が同じ所を表していることを確かめる決まりがある）
- `entrance`：{side: bottom/top/left/right, a0, a1} 入口がその辺のどこからどこまでか。outline を使う店では、入口がある辺の位置に注意（check_rules と entrance_zone を読むこと）
- `rooms`：{label, rect, kind}。kind は back（厨房側など客が入らない）、guest（トイレ・レジ・待合）、drink（ドリンクバー）、outside（建物の外）。レジは guest の「レジ」と back の「レジ内」の2つ
- `walls`：{label, rect} 壁・仕切り・柱・カウンターの台など、通れない物
- `doors`：店員の出入り口 {label, rect, kind:"staff", door: true/false}
- `toilet_doors`：トイレの入口 [x0,y0,x1,y1] の並び
- `dish_up`：受け渡し口 [{label, rect}]（1つ目を使う。道具はその下の辺の前を出発点にするので、向きに注意。prepare を読むこと）
- `robot_standby`：{label, rect, slots} ロボットの待機・充電の場所。1台あたり0.8m×0.8m以上
- `tables`：{type, x, y, w, h, seats, id, …}。type ごとの追加：booth は axis（v/h）、t2 は side、booth6L は flip・mirror

## 家具の種類（標準の大きさ。図面に描かれた大きさが違えば、図面を正として w・h を変えてよい）
- booth：ボックス席。向かい合うベンチ2つと間のテーブル。4人は1.2m×1.85m、6人は1.8m×1.85m。axis v はベンチが上下（座る側が左右）、h はベンチが左右
- t2：2人席0.8m×1.4m。壁側がベンチ、通路側が椅子
- booth6L：L字ソファの6人席
- t4：4人の角テーブル（外形1.2m四方）。椅子は2脚ずつ向かい合わせ
- r4：4人の丸テーブル（外形1.1m四方）
- long6：6人の長テーブル（外形3.2m×1.94m）
- 上にない形（斜めに置いた卓・半円のブース・カウンター席など）は、近い種類で置くか、置けなければ通れない物（walls）として置いて席は数えない。どちらにしたかを記録する

## 決まり（evaluate_layout.py が確かめるもの）
- ロボット（BellaBot）が通れる幅0.8m、ふだん走らせる幅1.0m、人が通れる幅0.5m
- 入口から1.5m、受け渡し口の前1.2m、トイレの入口の前1.2mには何も置かない
- トイレから1m以内に席を置くなら、席とトイレの入口の間に壁
- レジは2.5m以上。会計に立つ点・並ぶ点はロボットの道から0.75m以上
- 待機場所の各台が、ほかの台が止まったままで受け渡し口の前に出られること

## 出すもの（`/home/claude/blind3/out/` に）
1. `auto_v0.json` と `check_v0/`：図面を読んで最初に起こしたもの（直す前）
2. `auto_final.json` と `check_final/`：決まりの違反を直したもの（違反0）
3. `notes.md`（日本語）：
   - 縮尺の決め方（縦横の mm/画素、全体の寸法が合っているか）
   - 図面から読めたこと（卓の数・種類・席数、部屋、入口、受け渡し口、洗い場、トイレ）
   - 図面から読んだ通路の幅（何か所か）
   - 家具の種類に合わなかった形（元の形・置いた種類・ずれ）
   - 図面に描かれておらず、仮に置いたこと
   - 決まりの違反と、どう直したか（v0 → final）。日本のファミレス向けの決まりと合わなかった所
   - 店の人に聞けば確かになること（質問の形で）
````

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

```python
# 図面の黒い線のつながり（連結成分）を拾い、外接矩形を 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（全文）

```python
# 指定した行・列の上で、黒い区間（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（全文）

```python
# 太い線（壁）だけを残して、壁の矩形を 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（全文）

```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)

L = dict(
    name="寸法つき図面のファミレス（自動で起こした版）",
    style="効率重視",
    outer=dict(w=24.0, d=19.2),
    # 建物は四角でない：左上（x 0〜7.6、y 17.0〜19.2）は建物の外
    outline=[[0, 0], [24.0, 0], [24.0, 19.2], [7.6, 19.2], [7.6, 17.0], [0, 17.0]],
    entrance=dict(side="bottom", a0=9.6, a1=12.4),   # 下の寸法 9600 / 2800 / 11600
    rooms=[
        dict(label="建物の外", rect=[0.0, 17.0, 7.6, 19.2], kind="outside"),
        dict(label="パントリー", rect=[9.8, 6.0, 12.0, 14.0], kind="back"),
        dict(label="キッチン", rect=[12.0, 6.0, 19.0, 16.0], kind="back"),
        dict(label="冷蔵庫・こおり・冷凍庫・保存庫・社員トイレ・更衣室", rect=[19.0, 6.0, 24.0, 16.0], kind="back"),
        dict(label="裏の通路", rect=[12.0, 16.0, 24.0, 17.0], kind="back"),
        dict(label="洗い場", rect=[12.0, 17.0, 21.0, 19.2], kind="back"),
        dict(label="裏の通路（洗い場の横）", rect=[21.0, 17.0, 21.6, 19.2], kind="back"),
        dict(label="物置", rect=[21.6, 17.0, 24.0, 19.2], kind="back"),
        dict(label="トイレ", rect=[7.6, 17.0, 9.2, 19.2], kind="guest"),
        dict(label="待合", rect=[12.7, 0.0, 13.9, 0.5], kind="guest"),
        dict(label="レジ", rect=[9.78, 2.2, 12.3, 3.0], kind="guest"),
        dict(label="レジ内", rect=[9.78, 3.0, 12.3, 3.8], kind="back"),        # 仮：図面にない。カウンターの奥（パントリー側）に0.8m
        dict(label="ドリンクバー", rect=[13.4, 3.2, 18.6, 3.93], kind="drink"),
    ],
    walls=[
        dict(label="パントリーの壁", rect=[9.5, 6.0, 9.8, 14.0]),
        dict(label="トイレ前の通路の壁", rect=[9.5, 15.0, 9.8, 17.0]),
        dict(label="トイレの壁", rect=[9.2, 17.0, 9.5, 19.2]),
        dict(label="トイレの壁", rect=[7.6, 17.0, 8.3, 17.3]),
        dict(label="トイレ前の目隠しの壁", rect=[7.9, 14.4, 8.12, 17.0]),
        dict(label="仕切り", rect=[0.0, 8.6, 8.0, 9.0]),
    ],
    doors=[
        dict(label="厨房の出入り口（裏の通路へ）", rect=[12.0, 16.1, 12.1, 17.0], kind="staff", door=True),
        dict(label="厨房の出入り口（キッチンへ）", rect=[12.0, 15.0, 12.1, 15.95], kind="staff", door=True),
    ],
    toilet_doors=[[8.3, 15.8, 9.2, 17.0]],
    dish_up=[
        dict(label="受け渡し口（客席側）", rect=[9.8, 6.0, 12.0, 6.4]),
        dict(label="受け渡し口（トイレ側の通路）", rect=[9.8, 13.6, 12.0, 14.0]),
    ],
    robot_standby=dict(label="ロボット待機・充電（仮）", rect=[12.0, 5.2, 13.6, 6.0], slots=2),
    tables=[
        # 左上（仕切りより奥）
        T("booth6L", 0.0, 13.2, 2.6, 16.8, 6, flip=True, mirror=False),
        T("booth6L", 0.0, 9.4, 2.6, 13.0, 6, flip=False, mirror=False),
        T("t4", 4.0, 15.05, 5.2, 16.17, 4, chairs="左右"),
        T("t4", 6.0, 15.05, 7.2, 16.17, 4, chairs="左右"),
        T("r4", 3.645, 12.665, 4.745, 13.765, 4),
        T("r4", 6.85, 12.655, 7.95, 13.755, 4),
        T("t4", 3.64, 9.6, 4.76, 10.8, 4, chairs="上下"),
        T("t4", 5.84, 9.6, 6.96, 10.8, 4, chairs="上下"),
        # 左下（仕切りより手前）
        T("booth6L", 0.0, 4.6, 2.8, 8.4, 6, flip=True, mirror=False),
        T("booth6L", 0.0, 0.2, 2.8, 4.2, 6, flip=False, mirror=False),
        T("t4", 4.2, 6.86, 5.4, 7.97, 4, chairs="左右"),
        T("t4", 6.33, 6.81, 7.45, 8.01, 4, chairs="上下"),
        T("long6", 4.4, 3.645, 7.6, 5.585, 6),
        T("t4", 4.2, 0.46, 5.4, 1.57, 4, chairs="左右"),
        T("t4", 6.7, 0.41, 7.9, 1.59, 4, chairs="左右"),
        # 右下
        T("t4", 14.4, 0.46, 15.61, 1.57, 4, chairs="左右"),
        T("t4", 18.3, 0.41, 19.5, 1.59, 4, chairs="左右"),
        T("t4", 22.19, 0.46, 23.39, 1.57, 4, chairs="左右"),
        T("booth6L", 21.2, 3.0, 24.0, 6.0, 6, flip=True, mirror=True),
    ],
)
json.dump({"layout": L}, open(out, "w"), ensure_ascii=False, indent=1)
print(out, len(L["tables"]), "卓", sum(t["seats"] for t in L["tables"]), "席")
```

### 寸法つきの図面の final を書き出す work/make_final.py（全文）

```python
# 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（全文）

```python
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 の出力（全文）

```text
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（全文）

````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**
- 縦：19200mm ÷ (1291−157)=1134px → **16.931 mm/px**。縦と横の差は 0.04%（1画素未満）で、縦横同じ縮尺とみなした
- 途中の寸法も合う：上 7600/2200/2200/7000/5000 → 計算 7.60/9.80/12.00/18.99/24.00m、下 9600/2800/11600 → 9.60/12.39/24.00m、右 6000/11000/2200 → 6.01/17.01/19.20m、左 8600/8400 → 8.60/17.01m。ずれは最大 ±0.02m
- 寸法は壁の内側の線（通り芯の点線と一致）に付いている。太い壁の線はその外側に描かれている。原点＝外壁の内側の左下（画素 192, 1291）。outer は 24.0×19.2m

## 2. 図面から読めたこと
- 建物の形：四角でない。左上（x 0〜7.6m、y 17.0〜19.2m）が建物の外（上の寸法 7600 と、左の寸法が 8600+8400=17000 で止まることから）。outline と kind="outside" の部屋で表した
- 入口：下の辺、x 9.6〜12.4m（下の寸法 9600 / 2800）。両開きの扉
- 卓：**19卓・88席**（椅子・ソファの数で数えた）
  - booth6L（L字ソファ6人）×5＝30席：左の壁に4つ（左上2・左下2、上下で向きが逆）、右の壁の奥に1つ（flip・mirror）。ソファの帯の厚み・列の幅・テーブル（0.76×1.22m）が booth6L の比（帯0.45h・列0.55w）とぴったり合った
  - t4（4人の角テーブル）×11＝44席：椅子が左右のもの9、上下のもの2（`chairs` に記録）
  - r4（4人の丸テーブル）×2＝8席
  - long6（6人の長テーブル、天板3.22×0.76m＋スツール上下3つずつ、外形3.22×1.94m）×1＝6席
- 部屋：パントリー（x 9.8〜12.0、y 6.0〜14.0）、キッチン、冷蔵庫・こおり・冷凍庫・保存庫・社員トイレ・更衣室（右の列）、洗い場（x 12.0〜21.0、y 17.0〜19.2）、物置、裏の通路（キッチンと洗い場の間 y 16〜17）、ドリンクバー（x 13.4〜18.6、y 3.2〜3.93、kind="drink"）、レジ（2.52×0.81m）、待合（1.2×0.5m、入口の右の壁ぎわ）
- 受け渡し口：2つ。パントリーの下の端（客席側、y 6.0〜6.4）と上の端（トイレ側の通路、y 13.6〜14.0）
- 洗い場：右上の奥。キッチン側から裏の通路を通って入る。店員の扉が2つ（x=12.0、トイレ側の通路から裏の通路へ・キッチンへ）を doors に入れた
- トイレ：1つ（x 7.6〜9.2、y 17.0〜19.2）。入口は下の辺（x 8.3〜9.2）。入口の前は、目隠しの壁（x 7.9〜8.12、y 14.4〜17.0）とパントリー側の壁（x 9.5〜9.8、y 15.0〜17.0）にはさまれた通路（幅1.38m）で、客席へ出る。壁の厚みは0.3m（図面の太線 17〜19px）
- 仕切り：左の客席を上下に分ける斜線の帯（x 0〜8.0、y 8.6〜9.0、厚さ0.42m）。walls に入れた

## 3. 図面から読んだ通路の幅
| 場所 | 幅 |
|---|---|
| 仕切りの右端（x 8.0）〜パントリーの壁（x 9.5）。左の上下をつなぐ所 | 1.50m |
| 左上：L字席の右端（x 2.6）〜 t4（x 4.0） | 1.40m |
| 左上：t4 と t4 の間（x 5.2〜6.0） | 0.80m |
| 左上：t4（x 7.2）〜トイレ前の目隠しの壁（x 7.9） | 0.70m |
| 左上：丸テーブル（x 7.95）〜パントリーの壁（x 9.5） | 1.55m |
| 左下：L字席の右端（x 2.8）〜 t4・長テーブル（x 4.2〜4.4） | 1.40〜1.60m |
| 左下：長テーブルの下（y 3.645）〜手前の t4 の上（y 1.57〜1.59） | 2.06m |
| 手前の t4 〜下の外壁 | 0.41〜0.46m |
| レジの右端（x 12.3）〜ドリンクバー（x 13.4） | 1.10m |
| ドリンクバーの上（y 3.93）〜キッチンの壁（y 6.0）。受け渡し口から右の客席へ出る道 | 2.07m（待機場所を置いた所は 1.27m） |
| ドリンクバーの下（y 3.2）〜右の t4 の上（y 1.57〜1.59） | 1.61m |
| ドリンクバーの右端（x 18.6）〜右の L字席（x 21.2） | 2.60m |
| トイレ前の通路（目隠しの壁〜パントリー側の壁） | 1.38m |

## 4. 家具の種類に合わなかった形
- 丸テーブル r4 の2つは、椅子の描き方が標準と違った。左は椅子込みの外形が 0.95×1.10m、右は 1.10×1.05m。天板（直径0.81m）の中心を保って標準の 1.1×1.1m で置いた（ずれ：左は横に各0.075m広がる、右は縦に0.025m広がる）
- t4 は標準 1.2m 四方だが、図面は椅子込みで 1.2×1.12m（椅子が左右）・1.12×1.2m（椅子が上下）、手前の2卓は 1.2×1.19m。図面の大きさを正として w・h を図面どおりにした
- booth6L は大きさが4通り（2.6×3.6m、2.8×3.8m、2.8×4.0m、右の壁の1つは2.8×3.0m）。種類は同じで w・h を図面どおりにした
- 斜めの卓・半円・カウンター席はなかった。walls に回した家具もない
- 道具は t4 の4辺すべてを止まってよい辺として扱う。椅子が左右の t4 に、椅子のない上下の辺から配る結果になる卓がある（T03・T04・T11・T15）。椅子の向きは `chairs` に残したが、計算には使われない

## 5. 図面に描かれておらず、仮に置いたこと
- **dish_up の1つ目＝客席側の受け渡し口**（パントリーの下の端）。理由：前（下）がそのまま客席の真ん中に開いていて、左右どちらの客席へも1本で出られる。上の受け渡し口の前はトイレ前の通路（幅1.0mの開口と1.38mの通路）を通らないと客席へ出られず、トイレの客とぶつかる。また道具は受け渡し口の下の辺の前を出発点にするので、下向きに開いた客席側が向きにも合う。上の受け渡し口は2つ目として残した（道具は1つ目しか使わない）
- **待機場所**：受け渡し口の右どなり、キッチンの壁ぎわ（x 12.0〜13.6、y 5.2〜6.0）に**2台分**（0.8×0.8m ×2）。受け渡し口の前（y 4.8〜6.0、x 9.8〜12.0）には重ならない。台数（2台）も仮
- **レジ内**：図面はカウンター（レジ 2.52×0.81m）だけ。店員の立つ所をカウンターの奥（パントリー側）に 0.8m 置いた。これで会計の列（客の側）は入口の側になる
- キッチンの奥（冷蔵庫・冷凍庫・保存庫・社員トイレ・更衣室・裏の通路）は細かく分けず、客が入らない部屋（back）としてまとめた
- パントリー上のトイレ側の通路（x 9.8〜12.0、y 14.0〜19.2）は、どの部屋にもしなかった（図面で客席とつながっているため）。店員だけの通路かは不明
- パントリーとキッチンの間の出入り口は図面から読めず、doors に入れていない
- 壁の高さ・仕切りの高さは道具の仮の値のまま

## 6. 決まりの違反と直し方（v0 → final）
- **v0 の違反 0**。図面を読んだままで、決まりをすべて満たした。final は v0 と同じ形（名前だけ変えた）。直した所はない
- ロボットの届き方（BellaBot、ふだん走らせる幅1.0m）：19卓・88席すべてが「ロボットが届く」。狭い道なら届く 0、人が運ぶ 0。受け渡し口の前から止まる位置まで平均 8.5m・最長 13.76m（左上の奥の L字席）。2つのマス目の細かさで結果が同じ
- 決まりに引っかかりそうだった所：
  - トイレに一番近い t4（x 6.0〜7.2、y 15.05〜16.17）はトイレから 0.92m で 1m 以内。図面にある目隠しの壁で、席の端からトイレの入口への4本の線がすべてさえぎられるので、決まりを満たした（図面の意図どおり）
  - 会計に立つ点・並ぶ点がロボットの道から0.75m以上かは、evaluate_layout.py では確かめない（Unity 用の書き出しで確かめる決まり）。会計の列（幅1.0m・長さ2.15m）をロボットの障害物にしているので、ロボットの道は列の真ん中から1.0m以上離れ、作りの上では満たす
- 日本のファミレス向けの決まりと合わなかった所・気になる所：
  - レジのカウンターが入口の正面（入口から2.2m）に、入口の幅2.8mのほぼ全幅（2.52m）をふさぐ形で置かれている。客はレジの左右を回って入る。会計の列（入口の側に仮に置いた）が入口から入る客の動線と重なる
  - 手前の t4（5卓）は外壁との間が 0.41〜0.46m。決まりでは問題ないが、壁側の椅子に人が回り込みにくい
  - 左上の t4 と t4 の間が 0.80m、t4 と目隠しの壁の間が 0.70m。ロボットはそこを通らず別の辺から配るが、人の通り道としては狭め
  - 待機場所を置いたことで、受け渡し口から右の客席への道が 2.07m → 1.27m に狭くなる（ふだん走らせる幅 1.0m は残る）

## 7. 店の人に聞けば確かになること
- レジの店員はカウンターのどちら側（パントリー側か、入口側か）に立ちますか。会計の列はどこにできますか
- ロボットは何台入れる予定で、待機・充電はどこに置けますか（コンセントの位置は）
- 料理の受け渡しは、客席側の受け渡し口とトイレ側の受け渡し口のどちらで行いますか。トイレ側は何に使っていますか（下げ物の返却口など）
- パントリー上のトイレ側の通路は、客も通りますか、店員だけですか
- パントリーとキッチンの間に出入り口はありますか。洗い場への下げ物はどこから入れますか
- 丸テーブル・角テーブルの椅子の実際の数と置き方は図面どおりですか（席数88で合っていますか）
- 仕切り（斜線）の高さはどのくらいですか（座った客から見通せるか）
- 待合のベンチは何人掛けですか。入口の右の壁ぎわだけですか
````

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

```text
##### 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 の出力（全文）

```text
$ 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（全文）

```json
{
 "layout": {
  "name": "⓪ 手描きの見取り図から自動で起こした店（記憶のファミレス）",
  "style": "効率重視",
  "outer": {
   "w": 16.4,
   "d": 12.1
  },
  "entrance": {
   "side": "bottom",
   "a0": 7.62,
   "a1": 9.47
  },
  "rooms": [
   {
    "label": "パントリー",
    "rect": [
     8.07,
     3.5,
     9.35,
     10.1
    ],
    "kind": "back"
   },
   {
    "label": "キッチン",
    "rect": [
     9.35,
     3.52,
     15.12,
     10.1
    ],
    "kind": "back"
   },
   {
    "label": "冷凍庫・保存庫",
    "rect": [
     15.12,
     3.52,
     16.4,
     10.1
    ],
    "kind": "back"
   },
   {
    "label": "洗い場",
    "rect": [
     10.09,
     10.1,
     15.26,
     12.1
    ],
    "kind": "back"
   },
   {
    "label": "倉庫",
    "rect": [
     15.26,
     10.1,
     16.4,
     12.1
    ],
    "kind": "back"
   },
   {
    "label": "トイレ",
    "rect": [
     6.27,
     11.33,
     7.71,
     12.1
    ],
    "kind": "guest"
   },
   {
    "label": "ドリンクバー",
    "rect": [
     7.8,
     11.52,
     9.84,
     12.1
    ],
    "kind": "drink"
   },
   {
    "label": "待合",
    "rect": [
     9.55,
     0.0,
     10.1,
     1.2
    ],
    "kind": "guest"
   },
   {
    "label": "レジ",
    "rect": [
     6.6,
     1.5,
     9.1,
     1.9
    ],
    "kind": "guest"
   },
   {
    "label": "レジ内",
    "rect": [
     6.6,
     1.9,
     9.1,
     2.3
    ],
    "kind": "back"
   }
  ],
  "walls": [
   {
    "label": "仕切り",
    "rect": [
     0.0,
     5.86,
     6.55,
     6.24
    ]
   },
   {
    "label": "パントリーの壁",
    "rect": [
     7.53,
     3.11,
     8.07,
     10.1
    ]
   },
   {
    "label": "キッチンの壁",
    "rect": [
     9.91,
     2.91,
     16.4,
     3.52
    ]
   },
   {
    "label": "トイレの壁",
    "rect": [
     6.27,
     11.33,
     6.57,
     12.1
    ]
   },
   {
    "label": "トイレの壁",
    "rect": [
     7.41,
     11.33,
     7.71,
     12.1
    ]
   },
   {
    "label": "トイレの目隠しの壁",
    "rect": [
     6.27,
     10.13,
     6.57,
     11.33
    ]
   }
  ],
  "doors": [
   {
    "label": "パントリーの下の出入り口",
    "rect": [
     8.07,
     3.4,
     9.35,
     3.6
    ],
    "kind": "staff",
    "door": false
   },
   {
    "label": "パントリーの上の出入り口",
    "rect": [
     8.07,
     10.0,
     9.35,
     10.2
    ],
    "kind": "staff",
    "door": false
   },
   {
    "label": "洗い場の出入り口",
    "rect": [
     9.99,
     10.66,
     10.19,
     11.5
    ],
    "kind": "staff",
    "door": true
   }
  ],
  "toilet_doors": [
   [
    6.57,
    10.13,
    7.41,
    11.33
   ]
  ],
  "dish_up": [
   {
    "label": "受け渡し口",
    "rect": [
     8.07,
     3.5,
     9.35,
     3.9
    ]
   }
  ],
  "robot_standby": {
   "label": "ロボット待機・充電",
   "rect": [
    15.6,
    2.0,
    16.4,
    2.8
   ],
   "slots": 1
  },
  "tables": [
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 9.45,
    "w": 2.3,
    "h": 2.65,
    "seats": 6,
    "flip": true,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 3.03,
    "y": 10.67,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 4.86,
    "y": 10.7,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "r4",
    "x": 2.47,
    "y": 8.81,
    "w": 1.1,
    "h": 1.1,
    "seats": 4
   },
   {
    "type": "r4",
    "x": 4.5,
    "y": 8.83,
    "w": 1.1,
    "h": 1.1,
    "seats": 4
   },
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 6.45,
    "w": 1.85,
    "h": 2.55,
    "seats": 6,
    "flip": false,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 2.61,
    "y": 6.52,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 4.96,
    "y": 6.46,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 2.9,
    "w": 2.65,
    "h": 2.95,
    "seats": 6,
    "flip": true,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 3.58,
    "y": 4.26,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 5.24,
    "y": 4.28,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "long6",
    "x": 3.15,
    "y": 1.57,
    "w": 3.2,
    "h": 1.94,
    "seats": 6
   },
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 0.0,
    "w": 2.65,
    "h": 2.9,
    "seats": 6,
    "flip": false,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 3.46,
    "y": 0.0,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 6.01,
    "y": 0.0,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 10.17,
    "y": 0.0,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 12.41,
    "y": 0.0,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "booth",
    "axis": "v",
    "x": 14.6,
    "y": 0.0,
    "w": 1.8,
    "h": 1.85,
    "seats": 8
   }
  ]
 }
}
```

### 店の幅24mだけ教えて広げた店の形 auto_scaled_w24.json（全文）

```json
{
 "layout": {
  "name": "⓪ 手描きの見取り図から自動で起こした店（記憶のファミレス）",
  "style": "効率重視",
  "outer": {
   "w": 24.0,
   "d": 17.707
  },
  "entrance": {
   "side": "bottom",
   "a0": 11.151,
   "a1": 13.859
  },
  "rooms": [
   {
    "label": "パントリー",
    "rect": [
     11.81,
     5.122,
     13.683,
     14.78
    ],
    "kind": "back"
   },
   {
    "label": "キッチン",
    "rect": [
     13.683,
     5.151,
     22.127,
     14.78
    ],
    "kind": "back"
   },
   {
    "label": "冷凍庫・保存庫",
    "rect": [
     22.127,
     5.151,
     24.0,
     14.78
    ],
    "kind": "back"
   },
   {
    "label": "洗い場",
    "rect": [
     14.766,
     14.78,
     22.332,
     17.707
    ],
    "kind": "back"
   },
   {
    "label": "倉庫",
    "rect": [
     22.332,
     14.78,
     24.0,
     17.707
    ],
    "kind": "back"
   },
   {
    "label": "トイレ",
    "rect": [
     9.176,
     16.58,
     11.283,
     17.707
    ],
    "kind": "guest"
   },
   {
    "label": "ドリンクバー",
    "rect": [
     11.415,
     16.859,
     14.4,
     17.707
    ],
    "kind": "drink"
   },
   {
    "label": "待合",
    "rect": [
     13.976,
     0.0,
     14.78,
     1.756
    ],
    "kind": "guest"
   },
   {
    "label": "レジ",
    "rect": [
     9.659,
     2.195,
     13.317,
     2.78
    ],
    "kind": "guest"
   },
   {
    "label": "レジ内",
    "rect": [
     9.659,
     2.78,
     13.317,
     3.366
    ],
    "kind": "back"
   }
  ],
  "walls": [
   {
    "label": "仕切り",
    "rect": [
     0.0,
     8.576,
     9.585,
     9.132
    ]
   },
   {
    "label": "パントリーの壁",
    "rect": [
     11.02,
     4.551,
     11.81,
     14.78
    ]
   },
   {
    "label": "キッチンの壁",
    "rect": [
     14.502,
     4.259,
     24.0,
     5.151
    ]
   },
   {
    "label": "トイレの壁",
    "rect": [
     9.176,
     16.58,
     9.615,
     17.707
    ]
   },
   {
    "label": "トイレの壁",
    "rect": [
     10.844,
     16.58,
     11.283,
     17.707
    ]
   },
   {
    "label": "トイレの目隠しの壁",
    "rect": [
     9.176,
     14.824,
     9.615,
     16.58
    ]
   }
  ],
  "doors": [
   {
    "label": "パントリーの下の出入り口",
    "rect": [
     11.81,
     4.976,
     13.683,
     5.268
    ],
    "kind": "staff",
    "door": false
   },
   {
    "label": "パントリーの上の出入り口",
    "rect": [
     11.81,
     14.634,
     13.683,
     14.927
    ],
    "kind": "staff",
    "door": false
   },
   {
    "label": "洗い場の出入り口",
    "rect": [
     14.62,
     15.6,
     14.912,
     16.829
    ],
    "kind": "staff",
    "door": true
   }
  ],
  "toilet_doors": [
   [
    9.615,
    14.824,
    10.844,
    16.58
   ]
  ],
  "dish_up": [
   {
    "label": "受け渡し口",
    "rect": [
     11.81,
     5.122,
     13.683,
     5.707
    ]
   }
  ],
  "robot_standby": {
   "label": "ロボット待機・充電",
   "rect": [
    23.015,
    3.112,
    23.815,
    3.912
   ],
   "slots": 1
  },
  "tables": [
   {
    "type": "booth6L",
    "x": 0.533,
    "y": 14.443,
    "w": 2.3,
    "h": 2.65,
    "seats": 6,
    "flip": true,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 4.712,
    "y": 15.893,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 7.39,
    "y": 15.937,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "r4",
    "x": 3.87,
    "y": 13.148,
    "w": 1.1,
    "h": 1.1,
    "seats": 4
   },
   {
    "type": "r4",
    "x": 6.84,
    "y": 13.177,
    "w": 1.1,
    "h": 1.1,
    "seats": 4
   },
   {
    "type": "booth6L",
    "x": 0.429,
    "y": 10.03,
    "w": 1.85,
    "h": 2.55,
    "seats": 6,
    "flip": false,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 4.098,
    "y": 9.82,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 7.537,
    "y": 9.732,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "booth6L",
    "x": 0.614,
    "y": 4.927,
    "w": 2.65,
    "h": 2.95,
    "seats": 6,
    "flip": true,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 5.517,
    "y": 6.512,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 7.946,
    "y": 6.541,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "long6",
    "x": 5.351,
    "y": 2.747,
    "w": 3.2,
    "h": 1.94,
    "seats": 6
   },
   {
    "type": "booth6L",
    "x": 0.614,
    "y": 0.672,
    "w": 2.65,
    "h": 2.9,
    "seats": 6,
    "flip": false,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 5.341,
    "y": 0.278,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 9.073,
    "y": 0.278,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 15.161,
    "y": 0.278,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 18.439,
    "y": 0.278,
    "w": 1.2,
    "h": 1.2,
    "seats": 4
   },
   {
    "type": "booth",
    "axis": "v",
    "x": 21.783,
    "y": 0.429,
    "w": 1.8,
    "h": 1.85,
    "seats": 8
   }
  ]
 }
}
```

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

```json
{
 "layout": {
  "name": "寸法つき図面のファミレス（自動で起こした版・final）",
  "style": "効率重視",
  "outer": {
   "w": 24.0,
   "d": 19.2
  },
  "outline": [
   [
    0,
    0
   ],
   [
    24.0,
    0
   ],
   [
    24.0,
    19.2
   ],
   [
    7.6,
    19.2
   ],
   [
    7.6,
    17.0
   ],
   [
    0,
    17.0
   ]
  ],
  "entrance": {
   "side": "bottom",
   "a0": 9.6,
   "a1": 12.4
  },
  "rooms": [
   {
    "label": "建物の外",
    "rect": [
     0.0,
     17.0,
     7.6,
     19.2
    ],
    "kind": "outside"
   },
   {
    "label": "パントリー",
    "rect": [
     9.8,
     6.0,
     12.0,
     14.0
    ],
    "kind": "back"
   },
   {
    "label": "キッチン",
    "rect": [
     12.0,
     6.0,
     19.0,
     16.0
    ],
    "kind": "back"
   },
   {
    "label": "冷蔵庫・こおり・冷凍庫・保存庫・社員トイレ・更衣室",
    "rect": [
     19.0,
     6.0,
     24.0,
     16.0
    ],
    "kind": "back"
   },
   {
    "label": "裏の通路",
    "rect": [
     12.0,
     16.0,
     24.0,
     17.0
    ],
    "kind": "back"
   },
   {
    "label": "洗い場",
    "rect": [
     12.0,
     17.0,
     21.0,
     19.2
    ],
    "kind": "back"
   },
   {
    "label": "裏の通路（洗い場の横）",
    "rect": [
     21.0,
     17.0,
     21.6,
     19.2
    ],
    "kind": "back"
   },
   {
    "label": "物置",
    "rect": [
     21.6,
     17.0,
     24.0,
     19.2
    ],
    "kind": "back"
   },
   {
    "label": "トイレ",
    "rect": [
     7.6,
     17.0,
     9.2,
     19.2
    ],
    "kind": "guest"
   },
   {
    "label": "待合",
    "rect": [
     12.7,
     0.0,
     13.9,
     0.5
    ],
    "kind": "guest"
   },
   {
    "label": "レジ",
    "rect": [
     9.78,
     2.2,
     12.3,
     3.0
    ],
    "kind": "guest"
   },
   {
    "label": "レジ内",
    "rect": [
     9.78,
     3.0,
     12.3,
     3.8
    ],
    "kind": "back"
   },
   {
    "label": "ドリンクバー",
    "rect": [
     13.4,
     3.2,
     18.6,
     3.93
    ],
    "kind": "drink"
   }
  ],
  "walls": [
   {
    "label": "パントリーの壁",
    "rect": [
     9.5,
     6.0,
     9.8,
     14.0
    ]
   },
   {
    "label": "トイレ前の通路の壁",
    "rect": [
     9.5,
     15.0,
     9.8,
     17.0
    ]
   },
   {
    "label": "トイレの壁",
    "rect": [
     9.2,
     17.0,
     9.5,
     19.2
    ]
   },
   {
    "label": "トイレの壁",
    "rect": [
     7.6,
     17.0,
     8.3,
     17.3
    ]
   },
   {
    "label": "トイレ前の目隠しの壁",
    "rect": [
     7.9,
     14.4,
     8.12,
     17.0
    ]
   },
   {
    "label": "仕切り",
    "rect": [
     0.0,
     8.6,
     8.0,
     9.0
    ]
   }
  ],
  "doors": [
   {
    "label": "厨房の出入り口（裏の通路へ）",
    "rect": [
     12.0,
     16.1,
     12.1,
     17.0
    ],
    "kind": "staff",
    "door": true
   },
   {
    "label": "厨房の出入り口（キッチンへ）",
    "rect": [
     12.0,
     15.0,
     12.1,
     15.95
    ],
    "kind": "staff",
    "door": true
   }
  ],
  "toilet_doors": [
   [
    8.3,
    15.8,
    9.2,
    17.0
   ]
  ],
  "dish_up": [
   {
    "label": "受け渡し口（客席側）",
    "rect": [
     9.8,
     6.0,
     12.0,
     6.4
    ]
   },
   {
    "label": "受け渡し口（トイレ側の通路）",
    "rect": [
     9.8,
     13.6,
     12.0,
     14.0
    ]
   }
  ],
  "robot_standby": {
   "label": "ロボット待機・充電（仮）",
   "rect": [
    12.0,
    5.2,
    13.6,
    6.0
   ],
   "slots": 2
  },
  "tables": [
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 13.2,
    "w": 2.6,
    "h": 3.6,
    "seats": 6,
    "flip": true,
    "mirror": false
   },
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 9.4,
    "w": 2.6,
    "h": 3.6,
    "seats": 6,
    "flip": false,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 4.0,
    "y": 15.05,
    "w": 1.2,
    "h": 1.12,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "t4",
    "x": 6.0,
    "y": 15.05,
    "w": 1.2,
    "h": 1.12,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "r4",
    "x": 3.645,
    "y": 12.665,
    "w": 1.1,
    "h": 1.1,
    "seats": 4
   },
   {
    "type": "r4",
    "x": 6.85,
    "y": 12.655,
    "w": 1.1,
    "h": 1.1,
    "seats": 4
   },
   {
    "type": "t4",
    "x": 3.64,
    "y": 9.6,
    "w": 1.12,
    "h": 1.2,
    "seats": 4,
    "chairs": "上下"
   },
   {
    "type": "t4",
    "x": 5.84,
    "y": 9.6,
    "w": 1.12,
    "h": 1.2,
    "seats": 4,
    "chairs": "上下"
   },
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 4.6,
    "w": 2.8,
    "h": 3.8,
    "seats": 6,
    "flip": true,
    "mirror": false
   },
   {
    "type": "booth6L",
    "x": 0.0,
    "y": 0.2,
    "w": 2.8,
    "h": 4.0,
    "seats": 6,
    "flip": false,
    "mirror": false
   },
   {
    "type": "t4",
    "x": 4.2,
    "y": 6.86,
    "w": 1.2,
    "h": 1.11,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "t4",
    "x": 6.33,
    "y": 6.81,
    "w": 1.12,
    "h": 1.2,
    "seats": 4,
    "chairs": "上下"
   },
   {
    "type": "long6",
    "x": 4.4,
    "y": 3.645,
    "w": 3.2,
    "h": 1.94,
    "seats": 6
   },
   {
    "type": "t4",
    "x": 4.2,
    "y": 0.46,
    "w": 1.2,
    "h": 1.11,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "t4",
    "x": 6.7,
    "y": 0.41,
    "w": 1.2,
    "h": 1.18,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "t4",
    "x": 14.4,
    "y": 0.46,
    "w": 1.21,
    "h": 1.11,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "t4",
    "x": 18.3,
    "y": 0.41,
    "w": 1.2,
    "h": 1.18,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "t4",
    "x": 22.19,
    "y": 0.46,
    "w": 1.2,
    "h": 1.11,
    "seats": 4,
    "chairs": "左右"
   },
   {
    "type": "booth6L",
    "x": 21.2,
    "y": 3.0,
    "w": 2.8,
    "h": 3.0,
    "seats": 6,
    "flip": true,
    "mirror": true
   }
  ]
 }
}
```

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

| 何 | 場所 |
|---|---|
| 手描きの見取り図・起こした店の形・確かめの結果・道具 | 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） |
