---
title: "サービスロボット導入シミュレーション Log#41 | 病院の搬送ロボット、立体で見せて車いすを1%に直す"
slug: hospital-transport-robot-3d-view-wheelchair-share
date: 2026-11-10
day: 483
meta_description: "病院の外来の廊下にいる車いすの人は約0.6〜1.9%、杖や歩行器の人は約1〜9%と公開統計から推計でき、搬送ロボットのシミュレーションで仮に置いた各10%は多すぎた。車いす1%・杖の人3%に直し、Unity の病院を立体で見られるようにした。"
tags: [サービスロボット導入シミュレーション, 搬送ロボット, 病院, 車いす, 外来, Unity, シミュレーション]
series: "導入シミュレーション"
---

病院の外来の廊下にいる人のうち、車いすの人は約0.6〜1.9%、杖や歩行器を使う人は約1〜9%と、公開統計から推計できました。病院で検体を運ぶ搬送ロボットのシミュレーション（Unity で再現した仮の病院）では、前の回で根拠なしに車いす・杖の人を各10%置いていたので、**車いす1%・杖の人3%に直しました。** あわせて、それまで真上からの平らな図でしか見ていなかった Unity の病院を、配膳ロボットのときと同じ立体の見た目にし、廊下で起きる問題が壁に隠れずに見えるカメラの角度を数で比べて決めました。

この回で良くなったこと、できるようになったことです。

- 病院の人が動画を見て、ロボットがどこで、だれの前で止まったかを自分の目で確かめられるようになった。前は点と四角の図で、どれが車いすかも見分けにくかった
- 廊下の人の内訳に、出典と計算の式が付いた。前の回（[LOG477](https://pixelstomotors.com/record/hospital-transport-robot-avoid-outpatient-hall)）では、ロボットが止まりきれなかった相手の多くが車いすで、問題の回数が多めに出ていた。その分を、次に回し直すときに正せる
- 見た目の作り直しは、記録を1バイトも変えない形で入れた。色3案×カメラ4つのサンプル12枚は27分、角度を変えた14枚と比べる数は16分で、Claude Code（依頼箱）が作った。人が Unity を開いて撮り直す手間はなかった（前に手で撮ったときの時間は測っていない）

前の回で困っていたのは、病院の Unity を一度も立体で見ていなかったことと、廊下の人の内訳に根拠がなかったことです。配膳ロボットのファミレスでは、元ファミレス店員の実感と、立体の動画を見ての手直しを重ねて、店の形や人の動きを詰めてきました。病院ではそのやり方を使わずに数え始めてしまい、記録用の平らな図のまま数を出していました。人の内訳も、外来の廊下で10人に1人が車いす、というのは多すぎる、という実感がありました。

| やったこと | なぜ | 結果 |
|---|---|---|
| Unity の病院を立体の見た目にし、色3案×カメラ4つのサンプルを撮った（依頼139） | 配膳ロボットで作った見た目（立体・斜めのカメラ・右のパネル）を病院にも使い、見ながら直せるようにするため | 色は A（ミント×木目）に決めた。フロア全体の引きは灰色の部屋ばかりで人が点になり、60°の寄りは廊下の手前が壁に隠れた |
| 引きを廊下と待合に絞り、寄りの見下ろす角度4つ×向き2つを数で比べた（依頼140） | 壁に隠れる問題を、見た目の好みではなく数で選ぶため | 寄りは左30°・75°見下ろし（廊下の93%と問題の輪5個すべてが見える）、引きは西半分・左30°・75°に決めた |
| 外来の廊下の車いす・杖の人の割合を、公開統計から推計した | 前の回の各10%に根拠がなく、多すぎると感じていたため | 車いす約0.6〜1.9%、杖・歩行器約1.0〜8.5%。車いす1%・杖の人3%を選び、サイトの前提の表に入れた |

【図解】LOG483_people_mix.html

配膳ロボットを自分の店に入れたらどうなるかは、このシリーズの試算をもとにした別のページで試せます。病院の搬送も、試算が固まったら同じ形にしていきます。

【バナー】serving-robot-one-less-staff


## 作業ログ ― Unity の病院を、配膳ロボットと同じ立体の見た目にする（依頼139）

ここから下は作業ログです。数字は、特に断りがなければシミュレーションの値です。効果の欄は、測った値か見込みかを書いています。

### 病院の Unity は、記録用の真上の平らな図のままだった

前の回（LOG477）で病院の搬送ロボット（KEENON の W3）が廊下で起こす問題を数えた Unity の場面は、真上から見た平らな図で撮っていました。人は色の点、車いすは桃色の四角、部屋は灰色の塊です。数を出すには足りますが、病院の人が見て「どこで、何が起きたか」を確かめる絵にはなっていませんでした。

配膳ロボットの Unity では、人とロボットを立った筒で描き、壁に高さを付け、60°見下ろしの斜めのカメラで、引き（全体の流れ）と寄り（問題が起きている所）を組み合わせて撮ってきました。録画の横には、ロボットとスタッフがいま何をしているかのパネルを付けています。この形を病院にも持ってくることにしました。配膳に全部合わせる必要はなく、病院に合う椅子や色は変えてよい。ただし床・壁・背景の色、人とロボットの形と色、パネルの作りといったベースは変えない、という決め方です。

色の参考には、病院の待合室の写真を検索した画面を使いました（検索結果の画面なので、記事には載せていません）。床は明るいアイボリーか明るい木目、長いすは薄いミント・青緑が多く、明るい木の脚やひじ掛け、差し色に若草色やベージュ、という傾向でした。ここから3案を作りました。

| 案 | 長いす | 木目（脚・ひじ掛け・受付の前板） | 受付の天板 | 待合の床 |
|---|---|---|---|---|
| A ミント×木目 | #9FD6CB | #D7B88C | #F3F0E8 | #E4CFAA |
| B 生成り×木目 | #E6DAC3 | #CFA77A | #F6F3EC | #D8BF98 |
| C 黄緑×木目 | #B3CC6E | #D7B88C | #F3F0E8 | #E4CFAA |

待合（総合受付・会計・待合の部屋）だけは灰色の箱にせず、中を見せました。長いす50台と、座っている患者86人を置いています。この患者は飾りで、動かず、数える人にも入りません。待合の床は、はじめ廊下と同じ色で撮ったら境が分からなかったので、明るい木の色を差しました（Claude Code が決めた値で、そのまま使うことにしました）。

人とロボットの形は配膳と同じ決まりです。歩く患者・付き添いは黄色の筒、杖の人は同じ黄色の筒に細い杖、車いすは濃い灰色の車いすに黄色の人を座らせた形、スタッフは青の筒、ストレッチャーは白に近い灰色の台を青のスタッフが押す形、ロボットは配膳と同じ白の筒で、高さを W3 の公表値 1.081m にしました。車いすや杖を色ではなく形で分けたのは、色の意味を増やさないためです。

【画像】LOG483_01_look139_overview.png 依頼139 のサンプル12枚。色3案（ミント×木目・生成り×木目・黄緑×木目）とカメラ4つ（真上の引き・真上の寄り・60°見下ろし左30°の引き・60°見下ろし右30°の寄り）

### フロア全体の引きは灰色ばかりで、60°の寄りは廊下の手前が壁に隠れた

12枚を並べると、色の違いが分かるのは寄りの絵だけでした。引きではどの案もほとんど同じに見えます。

困ったのはカメラのほうです。フロア全体（120m×60m）を映す引きは、画面のほとんどが灰色の部屋で、人が動く廊下は細い帯になり、人は点にしか見えませんでした。病院で見せたいのは廊下なので、全体を映す意味がありません。

60°見下ろしの寄りでは、問題がいちばん多く起きた所（積む所から西の縦廊下の角までの大廊下）の手前半分が、手前の部屋の壁（高さ2.4m）に隠れました。隠れた所で起きた問題の輪は、壁の上に重なって描かれます。配膳のファミレスは客席が広く開けていたので60°で足りましたが、病院の廊下は両側が部屋なので、どちらから見ても同じことが起きます。

【画像】LOG483_02_near60_hidden.png 60°見下ろし・右30°の寄り。大廊下の手前（下側）が手前の部屋の壁に隠れ、問題の輪が壁の上に重なっている

この2つを受けて、色は A（ミント×木目）に決め、引きは廊下と待合が入る範囲に絞り、寄りは見やすい角度を探ることにしました。

### 見た目だけの直しであることを、記録の md5 で確かめた

見た目を入れたことで数が変わってしまうと、前の回と比べられなくなります。依頼139 では、見た目を入れた状態で前の回（依頼138）と同じ条件（時間で切り替える道・0.6m/秒・種0）を回し、ロボットの道のり・出来事・助け・ロボットの位置・便・移り・混み具合の記録7つが、依頼138 と1バイトも違わないことを確かめました。足した物には当たり判定を付けず、道を探す地図（NavMesh）を作ったあとに置き、乱数を使わない、という作りにしています。

#### 依頼139 の依頼文（病院の Unity を立体の見た目にして色3案とカメラ4つのサンプルを撮る）

```markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 180
---
# 依頼139：病院の Unity を立体の見た目にして、色3案とカメラ4つのサンプルを撮る

エイイチと決めたこと（2026-11-10）：
1 病院の Unity は今まで真上の平らな図（点と四角）でしか撮っていない。配膳ロボットの Unity で作ってきた見た目と撮り方（立体・60°見下ろしの斜めのカメラ・右のパネル）を病院にも使う
2 配膳に全部合わせる必要はない（病院に合う椅子の色などはある）が、基本のベースデザイン（床・壁・背景の色、人とロボットの形と色、パネルの作り）は変えない
3 色の方向はサンプルを並べて決める。参考はエイイチが共有した病院の待合室の写真（iCloud の …/サービスロボット導入シミュレーション/hospital/ref_colors/ の4枚。Google の画像検索の画面のスクショ）
4 カメラは真上のものも用意する
5 いい加減に進めない。サンプルは比べられるように、同じ回・同じ時刻・同じ構図で色だけ・カメラだけを変える

**今回は見た目だけ。試算の数え方・人やロボットの動き・記録の中身は1行も変えない。**
**配膳の場面・配膳の実行ファイル・配膳の台帳、配膳と共通の部品には触らない。** 配膳の色・カメラ・パネルの作りは読んで合わせる（使いたい部品があれば Hospital/ の中に写して使う）。変えるのは Assets/ServiceSim/Hospital/ の中だけ。
**エイイチは見ていない。人に聞かないと進めない所に当たったら、その項目は止めて報告に書き、ほかは続ける。**

## 正（読むもの）
- 条件：p2m-site main の tools/hospital_sim/hospital-unity-params.json（依頼138 と同じ md5 f72d3810674b31dadd9dfa4ce0141178 のはず。違えば回さずに報告して終わる）
- フロアの形：tools/hospital_layouts/hospital_h0.json（a29cd1d9…）・hospital_h0b.json（ddefbd8c…）
- 配膳の見た目の見本：KIOXIA service_sim/unity/rec_lp_20261105/p1_s7/oblique_loop.mp4・top_busy_loop.mp4、p0_round/after/round_after_panel.mp4（右のパネル付き）

## 始める前に確かめること（違えば回さずに報告して終わる）
1. 作業場所に保留がない（cs:115 のあと）。Unity・回し係が動いていない。KIOXIA がマウント済み
2. 上の md5

## 1. 立体の見た目（引数 -h139Look で切り替え。既定は今の平らな図のまま＝夜の回し方は変えない）
### ベース（3案とも同じ。配膳と同じ値を使う）
- 床（廊下）・壁・背景・影・光の当て方は配膳の場面と同じ値
- 部屋（外来・検査室・薬剤部など）は、配膳のキッチンと同じ扱いの灰色の箱。高さも配膳の壁・キッチンに合わせる
- 人とロボットの形と色は配膳と同じ決まり：
  - 歩く患者・付き添い＝配膳のお客さんと同じ黄色のカプセル
  - 杖の人＝同じ黄色のカプセル＋細い杖（色で分けず形で分ける）
  - 車いす＝同じ黄色のカプセルを濃い灰色の車いすに座らせる（真上から見ても四角い外形で分かる大きさ）
  - スタッフ＝配膳のスタッフと同じ青のカプセル
  - ストレッチャー＝白に近い灰色の台を、青のスタッフが押す形
  - ロボット＝配膳のロボットと同じ白。KEENON W3 の背の高さ（params に寸法があればそれ、なければ配膳ロボットと同じ）
- 問題の輪（1〜5・速いままのひやり・止まりきれなかった）は今のまま床の上に出す
### 病院だけのもの（色3案はここだけが違う）
- 「総合受付・会計・待合」（h0 の rect [25.1,15.1,70,27]）だけは灰色の箱にせず、中を見せる：大廊下側に向いた待合の長いす（3〜4人掛けの列）と、奥の壁ぞいに受付・会計のカウンター。長いすには座っている患者（黄色）を置いてよいが、これは飾りで、試算の人にも数えにも入れない（動かない。ロボット・人の道にもかからない）。置き方は報告に書く
- 色3案（長いすの張り地・カウンターの前板・待合の床の差し色。廊下の床・壁はベースのまま）
  - A ミント×木目：長いす #9FD6CB、脚・ひじ掛けと受付の前板は明るい木目色 #D7B88C、受付の天板 #F3F0E8
  - B 生成り×木目：長いす #E6DAC3、木目 #CFA77A、天板 #F6F3EC（落ち着いたクリニック寄り）
  - C 黄緑×木目：長いす #B3CC6E、木目 #D7B88C、天板 #F3F0E8
  - どれも写真の雰囲気を見て細かく直してよい。直したら値を報告に書く。差し色はこの3か所以外に増やさない

## 2. カメラ4つ（配膳の斜めのカメラの決まりに合わせる）
- 1 真上・引き：フロア全体（120×60m）
- 2 真上・寄り：大廊下の人の多い区間（x 20〜60 あたり）と待合が入る範囲
- 3 60°見下ろし・左30°・引き：フロア全体
- 4 60°見下ろし・右30°・寄り：問題がいちばん多く起きた所に寄る（20261109_day138 の「廊下だけ・0.6m/秒・種0」の問題の位置から一番多い所を選ぶ。決まった場所に寄るだけにしない）。選んだ所と理由を報告に書く
- 右のパネルは配膳と同じ作り（白地・大きな時刻・「ロボット・スタッフがしていること」・数）。中身は病院の値：時刻、ロボットの状態（待っている／行き／帰り／表と裏口を移る）、大廊下の人数、撮り始めからの問題1〜5の数。パネルの色は立体の色に合わせる（スタッフ青・ロボット白・患者黄）

## 3. 撮る（同じ回・同じ時刻）
- 回：時間で切替ではなく「廊下だけ」・0.6m/秒・種0（依頼138 と同じ条件）
- 時刻：外来の山場（8:00〜11:00）の中で、ロボットが大廊下の人の多い区間を走っている時刻を1つ選ぶ（選んだ時刻を報告に書く）
- 静止画：色3案 × カメラ4つ ＝ 12枚（パネル込み、1920×1080）
- 並べた図：色3案を横に並べた図をカメラごとに1枚（4枚）と、12枚を1枚にまとめた一覧
- 動画：色A で、カメラ4つそれぞれ20秒（店の時刻の5倍で流す・パネル付き）
- 出力：/Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look139/。並べた図（5枚）は iCloud の …/サービスロボット導入シミュレーション/hospital/look139/ にも写す

## 4. 確かめ（違えば止める）
- 見た目を入れた状態で、依頼138 の check138a と同じ回（時間で切替・0.6m/秒・種0）を回し、記録7つ（legs・events・rescues・robot_track・trips・moves・crowd）の md5 が依頼138 と1バイトも違わないこと（見た目だけの直しであることの証明）
- 瞬間移動0
- 撮った12枚を自分で開いて、人・車いす・ストレッチャー・ロボットが見分けられるか、パネルや文字が切れていないか、待合の長いすが道や人に重なっていないかを見る。見て直したことを書く

check-in（保留0）して、病院の実行ファイルを Builds/hospital139/ に作る。前の実行ファイルは残す。

## 報告
claude_code_reports/20261110_hospital_139.txt に：始める前の確かめ、1 の作り（使った配膳の値・3案の最終の色の値・待合の置き方）、2 のカメラの値（高さ・角度・範囲）と寄りの場所を選んだ理由、3 で選んだ時刻、4 の確かめ（md5 の比べ）、出力の場所、check-in の番号、決めてほしいこと（直し方の候補は作らない）。
**報告の最後の1行は必ず次のどれか**：「判定：合格」／「判定：止めた（決めてほしいことあり）」／「判定：失敗」
```

#### 依頼139 の結果（依頼箱の結果の全文）

```markdown
# 結果：20261110_139_病院のUnityを立体の見た目にして色3案とカメラ4つのサンプルを撮る

- 状態：完了
- 始めた時刻：2026-11-10 08:12
- 終わった時刻：2026-11-10 08:39（27分）
- 作業フォルダ：/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume 836390ff-8655-4a45-afaa-6d4b2646f731`

## Claude Code の報告

**やったこと**
- 始める前の確かめはすべて合格でした（保留0・cs:115 のあと、Unity と回し係は止まっている、KIOXIA はマウント済み、params と形の md5 は依頼のとおり）。
- 病院に立体の見た目を入れました。引数 -h139Look on のときだけ立体になり、既定は今の平らな図のままです。
  - 床・壁・光・影・背景・人とロボットの形と色は、配膳の値を読んでそのまま写しました。
  - 部屋は灰色の箱（高さ 2.4m）にしました。
  - 待合には、大廊下を向いた4人掛けの長いすを50台と、受付・会計のカウンターを置きました。座っている患者86人も置きましたが、どれも飾りで、当たりは無く数えにも入りません。
- カメラは4つ作り、パネル（時刻・ロボットの状態・大廊下の人数・問題1〜5）と問題の輪を重ねました。
  - カメラ4の寄る場所は大廊下 x9〜31 にしました。走っている間の問題がいちばん多い x17〜23 と、待っている間の問題が集まる積む所 x12 の両方が入るからです。
- 撮った回は「廊下だけ・0.6m/秒・種0」、時刻は9時57分42秒（7:30から8862秒）です。ロボットがこの問題の多い区間を行きで走っていて、大廊下に60人いる時刻です。
  - 静止画は12枚、色3案を並べた図は4枚、12枚の一覧は1枚です。
  - 動画は色Aでカメラ4つ、それぞれ20.04秒です（店の時刻の5倍）。
- 12枚を自分で開いて、次の5つを直しました。
  - 待合と大廊下の境が見えなかったので、待合の床に差し色を入れた
  - 全体図の空白を灰色の箱で埋めた
  - 真上・寄りの範囲を広げた
  - 引きのカメラでロボットの位置に白い輪を足した
  - パネルの文字の切れを直した

**結果**
- 見た目を入れた状態で、依頼138 の check138a と同じ回（時間で切替・0.6m/秒・種0）を回しました。記録7つ（legs・events・rescues・robot_track・trips・moves・crowd）の md5 は、7つとも依頼138 と一致しました。
- 瞬間移動はロボット0・人0でした。
- 撮った回も、止めた8945秒まで依頼138 と同じでした（robot_track 17,890行・crowd 150行が同じ、events は8940秒までに閉じた1,213件が同じ）。
- 撮った時刻（8845〜8945秒）にはストレッチャーが1台も出ていません。そこで別の時刻（300秒）を確かめ用に撮り、白に近い灰色の台を青のスタッフが押す形で見分けられることを見ました。
- check-in は cs:116（保留0）です。実行ファイルは Builds/hospital139 に作り、前の実行ファイルは残しています。
- 出力は KIOXIA の hospital/20261110_look139/ です。並べた図5枚は iCloud の hospital/look139/ にも写しました。
- 報告は claude_code_reports/20261110_hospital_139.txt です。最後の行は「判定：止めた（決めてほしいことあり）」です。

**変えたファイル**
- Assets/ServiceSim/Hospital/HospitalLook.cs（新しく作った）
- Assets/ServiceSim/Hospital/HospitalRun.cs
- Assets/ServiceSim/Hospital/Editor/HospitalSetup.cs
- Assets/ServiceSim/Hospital/Hospital.unity
- Assets/ServiceSim/Hospital/Materials/H139_*.mat（10個）
- 配膳の場面・実行ファイル・台帳、配膳と共通の部品には触っていません。

**決めてほしいこと**
1. 待合の床の差し色は依頼に値が無かったので、写真の明るい木の床から選びました（A・C は #E4CFAA、B は #D8BF98）。この値でよいか、差し色を入れないかを決めてください。
2. カメラ4は南東から見るので、大廊下の南の半分（約1.4m）が手前の部屋の箱に隠れます。隠れた所で起きた問題の輪は、箱の上に重なって見えます。このままでよいか決めてください。
3. シミュレーションで建てていない部屋も含めて、外形 120×60m の床でない所を、つながった灰色の箱で描きました（部屋どうしの境は描いていません）。これでよいか決めてください。
4. 撮った時刻にストレッチャーがいないので、12枚と動画には写っていません。このままでよいか決めてください。
```

#### 依頼139 の報告（claude_code_reports/20261110_hospital_139.txt の全文）

```text
依頼139：病院の Unity を立体の見た目にして、色3案とカメラ4つのサンプルを撮る（2026-11-10）
====================================================================

--------------------------------------------------------------------
0. 始める前の確かめ（すべて合格）
--------------------------------------------------------------------
  作業場所の保留：0（最後の check-in は cs:115）
  Unity・回し係：動いていない（Unity Hub だけ）。KIOXIA：マウント済み
  params（p2m-site main tools/hospital_sim/hospital-unity-params.json）md5 f72d3810674b31dadd9dfa4ce0141178（依頼138 と同じ）
  hospital_h0.json a29cd1d981e5b3086fb623fc647926d9・hospital_h0b.json ddefbd8ca0db243f41601532e7b8dee8（どちらも依頼のとおり）


--------------------------------------------------------------------
1. 立体の見た目の作り（-h139Look on のときだけ。既定は off＝今の平らな図のまま。夜の回し方は変わらない）
--------------------------------------------------------------------
  直したのは Assets/ServiceSim/Hospital/ の中だけ：
    HospitalLook.cs（新しい。見た目と撮影）・HospitalRun.cs（引数と呼び出しを足しただけ）・Editor/HospitalSetup.cs・Hospital.unity・
    Materials/H139_*.mat（10 個）
  見た目だけにするための決まり：
    ・足した物には当たり（Collider）を付けない。NavMesh を焼いたあとに置く。乱数を使わない
    ・人・ロボットの元の見た目（Body）は描かないだけにして、そのまま残す（当たり・動き・数え方・記録は触らない）
    ・部屋の箱（StoreBuilder の Room…）も描かないだけで、当たりは残す

  使った配膳の値（読んだ所：Scenes/ServiceSim_Family.unity・Store/Materials・People/Guest.prefab・SeatedGuestBody.prefab・
  Robot/ServingRobot.prefab・Recording/SimTimeRecorder.cs・rec_lp_20261105 の録画）
    床：Mat_Floor（0.86,0.84,0.79）・壁と部屋の箱：Mat_Wall（0.40,0.43,0.48）・高さ 2.4 m（StoreLayout.wallHeight）。
        床と壁は病院でもはじめから配膳の Floor/Wall の部品を使っている
    光：Sun 強さ 1.1・色（1,0.98,0.94）・やわらかい影・影の濃さ 1・bias 0.05・normal bias 0.4、向きは録画のときの高さ 75°
        （SimTimeRecorder の既定）・方角 -34°。Fill 強さ 0.35・色（0.85,0.9,1）・(28°,160°)・影なし。
        まわりの光 Flat（0.46,0.47,0.50）
    背景：配膳の録画の背景の画素（102,96,91）。配膳は空の既定の「地面」の色がそのまま写っているので、同じ色を単色で出した
         （撮った画像の隅が 102,96,91 になっているのを確かめた）
    影を描く距離：配膳の店より広い（120 m）ので 400 m に延ばした（Ultra の 150 m では奥に影が出ない）。影の向き・濃さは同じ
    材質：配膳の Mat_Guest・Mat_Staff・Mat_Robot・Mat_RobotFront をそのまま写した（H139_Guest など。光沢 0.12 も同じ）
  人とロボット
    歩く患者・付き添い：配膳のお客さんと同じ黄色のカプセル（体 直径 0.4・高さ 1.59＋頭 0.22）
    杖の人：同じ黄色のカプセル＋細い杖（直径 0.045・長さ 0.9・右手の前・握り付き。色は濃い灰色 0.30,0.31,0.33）
    車いす：濃い灰色（0.30,0.31,0.33）の台（外形 0.65×1.10＝数える長方形と同じ）・座面・背・車輪（直径 0.6）・足台に、
           配膳の座った人と同じ形の黄色を座らせた。真上から濃い灰色の四角に黄色の点で見える
    スタッフ：配膳のスタッフと同じ青のカプセル
    ストレッチャー：外形 0.70×2.60（数える長方形と同じ）のうち、前の 2.1 m を白に近い灰色（0.86,0.87,0.88）の台（高さ 0.87・脚 4 本・枕）、
           後ろに押す青のスタッフ
    ロボット：配膳と同じ白の円柱＋橙の前の印。直径 0.549（数える丸と同じ）・高さ 1.081（params の KEENON W3 の h）。
           前の印は配膳と同じ割合（高さの 0.8・前へ直径の 0.45・幅 直径の 0.6）
  部屋：シミュレーションで建てているのは廊下のまわり（z13〜31）だけなので、見た目では h0 の外形 120×60 m のうち
        「建てた床」と「待合」でない所を、配膳のキッチンと同じ灰色の箱（高さ 2.4 m）で埋めた（廊下だけの回で 10 個）。
        外来・検査室・薬剤部などは1つながりの灰色の箱に見える（部屋どうしの境は描いていない）
  問題の輪：今までどおり床の上に出す（Unity では描かず、カメラの行列で床の点を画素に移して重ねる。色と印は依頼136 と同じ・10 秒出す）

  総合受付・会計・待合（h0 rect [25.1,15.1,70,27]）の置き方（飾り。当たりなし・動かない・人数にも数えにも入れない）
    床：待合の範囲いっぱいに差し色の床（下の色3案）。大廊下の側（z27）は壁を置かず開けた
    壁：奥（z15.1、職員の中廊下との境）と西（x25.1、西の縦廊下との境）に厚さ 0.15・高さ 2.4。玄関の通り（x44〜52）は奥の壁を空けた
    受付のカウンター x28〜40・会計のカウンター x56〜67。どちらも z16.6〜17.2（奥の壁から 1.5 m 前）・高さ 1.0。
      本体（前板）＝木目、天板（厚さ 0.05・まわりに 0.08 出す）＝天板の色
    長いす：4人掛け（座 0.55 m×4＋ひじ掛け 0.06×2＝長さ 2.32 m・奥行き 0.55・座面の高さ 0.45・背 0.42）。大廊下（+z）を向く。
      玄関の通りの西（x25.25〜44）と東（x52〜70）に、それぞれ 5 台×5 列（列の間 1.3 m、いちばん後ろの列の背 z18.7・
      いちばん前の列の前 z24.45）＝50 台。大廊下の端（z27）から 2.55 m、壁と玄関の通りから 0.9 m 以上離した
    座っている患者（飾り）：長いすの決まった席だけ（乱数なし）に 86 人。形は配膳の座った人と同じ黄色
    人・ロボットの道にかからないこと：待合の範囲は当たりのある箱のままで、NavMesh も無い（人もロボットも入れない）。
      撮った画像でも長いす・座った人が廊下や歩く人に重なっていないのを見た

  色3案（長いすの張り地・木目（脚・ひじ掛け・カウンターの前板）・カウンターの天板・待合の床の差し色）。最終の値：
    A ミント×木目：長いす #9FD6CB・木目 #D7B88C・天板 #F3F0E8・待合の床 #E4CFAA
    B 生成り×木目：長いす #E6DAC3・木目 #CFA77A・天板 #F6F3EC・待合の床 #D8BF98
    C 黄緑×木目　：長いす #B3CC6E・木目 #D7B88C・天板 #F3F0E8・待合の床 #E4CFAA
    長いす・木目・天板は依頼の値のまま（直していない）。
    待合の床の差し色は依頼に値が無かったので、写真の明るい木の床から選んだ（下の「決めてほしいこと」1）。
    はじめは待合の床を廊下と同じ色で撮ったところ、待合と大廊下の境が見分けられなかったので入れた。
    差し色はこの4か所（張り地・木目・天板・床）だけで、ほかは増やしていない
    （B は長いすの色と床の色が近いので、床を少し濃い木の色にした）


--------------------------------------------------------------------
2. カメラ4つ（配膳の -ssRecCam と同じ決め方：透視投影・縦の画角 40°・注視点から向きの逆へ距離だけ下がる。
   方位 0＝+z（奥）を向く。2720×2160 で撮って 1360×1080 に縮め、右に 560 のパネルを付けて 1920×1080）
--------------------------------------------------------------------
  距離は、範囲の四隅（高さ 0 と 2.4 m）がすべて画面の内側（余白 3%）に入るいちばん近い距離を、tools/camfit139.py で計算した
  1 真上・引き           top_wide      見下ろす 90°・方位 0・注視点 (60, 30)・距離 142.8 m（高さ 142.8 m）・範囲 x-0.5〜120.5・z-0.5〜60.5（フロア全体）
  2 真上・寄り           top_near      見下ろす 90°・方位 0・注視点 (40.5, 22.5)・距離 73.2 m・範囲 x10〜71・z13.5〜31.5
                                        （大廊下の人の多い区間と待合の全体。はじめ x18〜62 で撮ったら待合の東の端が切れ、
                                        ロボットも画面の左の端にいたので広げた）
  3 60°見下ろし・左30°・引き obl_l30_wide 見下ろす 60°・方位 30（配膳の wide_l30 と同じ）・注視点 (54.69, 22.45)・距離 154.5 m
                                        （カメラの高さ 133.8 m）・範囲 フロア全体
  4 60°見下ろし・右30°・寄り obl_r30_near 見下ろす 60°・方位 -30・注視点 (20.69, 29.05)・距離 27.5 m（カメラの高さ 23.8 m）・
                                        範囲 x9〜31・z26〜32
    寄りの場所を選んだ理由：20261109_day138 の day138_v06_hall_s0（廊下だけ・0.6m/秒・種0）の events.csv の 1〜5 の位置を数えた。
      ・走っている間（行き・帰り）71 件：6 m 幅で数えると x17〜23 に 51 件（中の平均 x19.9。大廊下の西の縦廊下への曲がり角の手前）
      ・待っている間も入れた全部 2,786 件：x9.5〜15.5 に 2,729 件（積む所 (12,30.5) で待っている間のひやり）
      この2つ（x12 と x20）がどちらも入るよう、大廊下 x9〜31 に寄った。曲がり角（西の縦廊下の入り口）も入る
  カメラごとの行列は runs/look139_v06_hall_s0/look139/cam_*.json（床の点→画素の式つき）

  右のパネル（配膳の round_after_panel と同じ作り：白地 249,247,244・小さな見出し・大きな時刻・「ロボット・スタッフがしていること」・数）
    見出し：病院 h0・廊下だけ・0.6m/秒・種0／色の案／カメラ
    時刻（7:30 からの秒を時計にした）
    ロボット 1台：待っている／行き／帰り／表と裏口を移る のうち今のものを濃くし、白い丸と「表→検体受付」などを付ける
    スタッフ：ロボットの助け（助けに出ているスタッフの数だけ青の四角）
    大廊下の人（x0〜70）
    撮り始め（9:57:25）からの問題 1〜5 の数（輪の色つき）・うち速いままのひやり・止まりきれなかった
  左上の見本：患者・付き添い（黄）・杖の人（黄＋杖）・車いす（濃い灰色の四角に黄）・スタッフ（青）・ストレッチャー（灰の台＋青）・ロボット（白）
  引きのカメラ（1・3）だけ、ロボットの位置に白い輪を足した（引きではロボットが 6 画素ほどで見つけにくかったため。パネルの下に書いた）


--------------------------------------------------------------------
3. 撮った回と時刻
--------------------------------------------------------------------
  回：廊下だけ・0.6m/秒・種0（依頼138 の day138_v06_hall_s0 と同じ条件）を -h139Look on で回した（look139_v06_hall_s0）
  静止画の時刻：7:30 から 8862 秒＝9時57分42秒（外来の山場 8:00〜11:00 の中）。
    この日の山場の中の便は1便目だけ（8843 秒に出る）で、ロボットが大廊下の人の多い区間を走るのは 8845〜8875 秒（行き）と
    9100〜9155 秒（帰り）。8862 秒はロボットが (19.5, 28.4) を行きで走っていて、x17〜23 の問題の多い所の中・
    大廊下の人 60 人・直前の 10 秒に問題が起きている時刻なので選んだ
  動画：色A・カメラ4つ・各 8845〜8945 秒（9:57:25〜9:59:05）を 0.2 秒ごとに撮り、25 コマ/秒で流す＝店の時刻の5倍・20.04 秒・1920×1080・パネル付き
  撮る回は 8945 秒で止めた（-h139EndAfterShots。summary は status stopped）


--------------------------------------------------------------------
4. 確かめ
--------------------------------------------------------------------
  (1) 依頼138 の check138a と同じ回（時間で切替・0.6m/秒・種0）を、cs:116 の実行ファイルで -h139Look on（画面を描く設定）で回した
      （check139a_v06_switch_s0。最後まで・status completed）。記録7つの md5：
        legs.csv        c16df55d225bff9b9e2c35391751ef84  依頼138 と一致
        events.csv      7e32a56d9806c6354704a3c91af29148  一致
        rescues.csv     46f222937bb9562043663e1a3d7f8b2f  一致
        robot_track.csv 458987516003595228862fd1ebb1f31b  一致
        trips.csv       3ae372d5b5eedce24fedf3da531f49e0  一致
        moves.csv       b20f7c01f8596d37a26906ac7ef31028  一致
        crowd.csv       ad41d0201578ec8e1129ae4d76220ca2  一致
      summary.json で違うのは run・unity_cs・wall_s だけ
  (2) 瞬間移動：check139a ロボット 0・人 0。撮った回 look139 もロボット 0・人 0
  (3) 撮った回（撮りながら・色を入れ替えながら）も、依頼138 の day138_v06_hall_s0 と止めた 8945 秒まで同じ：
      robot_track 17,890 行・crowd 150 行がそのまま同じ、events は 8940 秒までに閉じた 1,213 件が同じ
  (4) 12 枚を開いて見たこと・直したこと
      ・人・車いす・ロボット：寄り（2・4）ではすべて見分けられる。車いすは真上から濃い灰色の四角＋黄色、杖は寄りで見える細い棒。
        引き（1・3）では人は小さな点で、杖は見えない。ロボットは白い点で見つけにくいので白い輪を足した（直した）
      ・ストレッチャー：撮った時刻（8845〜8945 秒）には1台も出ていない（記録で 0 台を確かめた）ので、12 枚には写っていない。
        確かめのために同じ回の 0〜3600 秒を 20 秒ごとに別に撮り（verify/）、300 秒の真上・寄りで、職員の中廊下を白に近い灰色の台＋
        青の押す人の形で見分けられるのを見た
      ・パネルの文字：はじめ見出しの2行目（色とカメラ）が右で切れていたので3行に分けた。いちばん下の注の行も右で切れたので短くした。
        直したあとは切れていない
      ・薄くなった輪が点線のように見えたので、輪を1つずつ別の層に描いて重ねるようにした
      ・待合：はじめ床が廊下と同じ色で大廊下との境が分からなかったので、差し色の床を入れた。全体図で外来などの部屋が無く空白に
        見えたので、外形 120×60 m を灰色の箱で埋めた。長いすは道・人に重なっていない
      ・カメラ4（南東から 60°）では、大廊下の南の半分が手前の部屋（お薬の窓口 x0〜23・高さ 2.4 m）に隠れる（約 1.4 m）。
        隠れた所で起きた問題の輪は箱の上に重なって見える（決めてほしいこと 2）
      ・真上・寄りの範囲を広げた（上の 2 を参照）


--------------------------------------------------------------------
5. 出力の場所
--------------------------------------------------------------------
  KIOXIA /Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look139/
    stills/still_{A|B|C}_{top_wide|top_near|obl_l30_wide|obl_r30_near}.png（12 枚・パネル込み 1920×1080）
    sheets/compare_cam1_top_wide.png・compare_cam2_top_near.png・compare_cam3_obl_l30_wide.png・compare_cam4_obl_r30_near.png（色3案を横に並べた図）・
           overview_12.png（12 枚の一覧）
    videos/hospital139_A_{top_wide|top_near|obl_l30_wide|obl_r30_near}_5x.mp4（各 20.04 秒・25 コマ/秒・1920×1080）
    runs/look139_v06_hall_s0/（撮った回の記録・look139/ に Unity で撮った元の画像・cam_*.json・look_frames.csv）
    runs/check139a_v06_switch_s0/（確かめの回）
    verify/（ストレッチャーの見え方の確かめ）・logs/
    tools/run139.sh（回す）・camfit139.py（カメラの距離）・pan139.py（輪とパネル・並べた図・動画）・vid139.sh
  iCloud …/サービスロボット導入シミュレーション/hospital/look139/ に並べた図 5 枚を写した（md5 一致を確かめた）


--------------------------------------------------------------------
6. check-in・実行ファイル
--------------------------------------------------------------------
  check-in：cs:116（保留 0）。Setup が H127 の材質を保存し直して保留にしたが、中身が同じなので元に戻した（check-in していない）
  実行ファイル：Builds/hospital139/HospitalSim.app（build_info：unity_cs ServiceSim/cs:116・pending_changes 空）。
    Builds/hospital127・132・134・136・138 はそのまま
  配膳の場面・配膳の実行ファイル・配膳の台帳・配膳と共通の部品には触っていない（材質は写して Hospital/Materials に置いた）
  使い方：今までの引数に -h139Look on を足す。撮るときは -h139Cam name,方位,見下ろす角度,注視点x,注視点z,距離,画角,w,h（何本でも）・
    -h139StillAt 秒（色3案×カメラを PNG に）・-h139VidFrom 秒 -h139VidTo 秒 -h139VidStep 秒 -h139VidScheme A（JPG のコマ）・
    -h139EndAfterShots on（撮り終えたら止める）


--------------------------------------------------------------------
決めてほしいこと（直し方の候補は作っていない）
--------------------------------------------------------------------
  1. 待合の床の差し色：依頼の色3案に値が無かったので、写真の明るい木の床から A・C #E4CFAA、B #D8BF98 を選んだ。
     この値でよいか、または差し色を入れないか
  2. カメラ4（60°見下ろし・右30°・寄り）は配膳と同じく南側から見るので、問題がいちばん多い大廊下 x12〜23 の南の半分が、
     手前の部屋の箱（高さ 2.4 m・配膳の壁と同じ）に隠れる。隠れた所の輪は箱の上に重なって見える。このままでよいか
  3. 部屋の見せ方：シミュレーションで建てていない部屋（北の外来・南の薬剤部など）も含めて、外形 120×60 m の床でない所を
     1つながりの灰色の箱で描いた（部屋どうしの境は描いていない）。これでよいか
  4. ストレッチャーは撮った時刻に出ていないので、12 枚・動画には写っていない（見分けは別の時刻で確かめた）。このままでよいか

判定：止めた（決めてほしいことあり）
```

#### camfit139.py（範囲の四隅が画面に入るカメラの距離を決めるコード）

```python
#!/usr/bin/env python3
"""依頼139：カメラの距離と注視点を決める（Unity の Euler(pitch, yaw, 0) と同じ式。透視投影・縦の画角 fov）。
決めた点（床の範囲の四隅・高さ 0 と 2.4 m）がすべて画面の内側（余白 margin）に入る、いちばん近い距離を探し、
写った範囲の真ん中が画面の真ん中に来るよう注視点を動かす。"""
import math, sys

W, H = 2720, 2160


def basis(yaw, pitch):
    y, p = math.radians(yaw), math.radians(pitch)
    f = (math.cos(p) * math.sin(y), -math.sin(p), math.cos(p) * math.cos(y))
    u = (math.sin(p) * math.sin(y), math.cos(p), math.sin(p) * math.cos(y))
    r = (u[1] * f[2] - u[2] * f[1], u[2] * f[0] - u[0] * f[2], u[0] * f[1] - u[1] * f[0])
    return f, u, r


def proj(pt, yaw, pitch, tx, tz, dist, fov):
    f, u, r = basis(yaw, pitch)
    c = (tx - f[0] * dist, -f[1] * dist, tz - f[2] * dist)
    d = (pt[0] - c[0], pt[1] - c[1], pt[2] - c[2])
    zc = sum(d[i] * f[i] for i in range(3))
    xc = sum(d[i] * r[i] for i in range(3))
    yc = sum(d[i] * u[i] for i in range(3))
    t = math.tan(math.radians(fov) / 2)
    a = W / H
    nx, ny = xc / (zc * t * a), yc / (zc * t)
    return (nx + 1) / 2 * W, (1 - ny) / 2 * H


def fit(yaw, pitch, x0, x1, z0, z1, fov=40, margin=0.03, hmax=2.4):
    pts = [(x, h, z) for x in (x0, x1) for z in (z0, z1) for h in (0.0, hmax)]
    tx, tz = (x0 + x1) / 2, (z0 + z1) / 2
    for _ in range(30):
        lo, hi = 1.0, 2000.0
        for _ in range(60):
            m = (lo + hi) / 2
            uv = [proj(p, yaw, pitch, tx, tz, m, fov) for p in pts]
            ok = all(margin * W <= u <= (1 - margin) * W and margin * H <= v <= (1 - margin) * H for u, v in uv)
            if ok: hi = m
            else: lo = m
        dist = hi
        uv = [proj(p, yaw, pitch, tx, tz, dist, fov) for p in pts]
        cu = (min(u for u, v in uv) + max(u for u, v in uv)) / 2 - W / 2
        cv = (min(v for u, v in uv) + max(v for u, v in uv)) / 2 - H / 2
        if abs(cu) < 0.5 and abs(cv) < 0.5: break
        # 画面のずれを床の上のずれに直す（小さく動かして試す）
        e = 0.05
        u0 = proj((tx, 0, tz), yaw, pitch, tx, tz, dist, fov)
        ux = proj((tx + e, 0, tz), yaw, pitch, tx, tz, dist, fov)
        uz = proj((tx, 0, tz + e), yaw, pitch, tx, tz, dist, fov)
        a11, a21 = (ux[0] - u0[0]) / e, (ux[1] - u0[1]) / e
        a12, a22 = (uz[0] - u0[0]) / e, (uz[1] - u0[1]) / e
        det = a11 * a22 - a12 * a21
        dx = (a22 * cu - a12 * cv) / det
        dz = (-a21 * cu + a11 * cv) / det
        tx += dx * 0.8; tz += dz * 0.8
    return tx, tz, dist


if __name__ == "__main__":
    yaw, pitch, x0, x1, z0, z1 = map(float, sys.argv[1:7])
    tx, tz, d = fit(yaw, pitch, x0, x1, z0, z1)
    print(f"{tx:.2f},{tz:.2f},{d:.1f}")
```

#### pan139.py（問題の輪とパネルを重ね、並べた図と動画のコマを作るコード）

```python
#!/usr/bin/env python3
"""依頼139：病院の立体の見た目の静止画・動画に、問題の輪と右のパネルを付ける（1920×1080）。

左 1360×1080＝Unity で撮った 2720×2160 を縮めたもの。輪は床の上（高さ FLOOR_Y）に、cam_{name}.json の行列で画素に移して描く
（依頼136 の vid136.py と同じ色・同じ印。10 秒出す）。引きのカメラ（top_wide・obl_l30_wide）だけ、ロボットの位置に白の輪を足す。
右 560＝配膳のパネル（rec_lp_20261105/p0_round/after/round_after_panel.png）と同じ作り：白地・小さな見出し・大きな時刻・
「ロボット・スタッフがしていること」・数。中身は病院の値（ロボットの状態・助けに出ているスタッフ・大廊下の人数・撮り始めからの問題1〜5）。

  python3 pan139.py stills <回のフォルダ>
  python3 pan139.py video <回のフォルダ> <cam> <出力.mp4>
  python3 pan139.py sheets <回のフォルダ> <出力フォルダ>
"""
import csv, json, math, os, subprocess, sys, tempfile
from PIL import Image, ImageDraw, ImageFont

FFMPEG = "/opt/homebrew/Caskroom/miniforge/base/envs/ros_env/bin/ffmpeg"
FONT_B = os.path.expanduser("~/Library/Fonts/NotoSansJP-Bold.otf")
FONT_R = os.path.expanduser("~/Library/Fonts/NotoSansJP-Regular.otf")
W, H, SW = 1920, 1080, 1360           # 全体・左の場面の幅（配膳のパネル付きと同じ割り）
FLOOR_Y = 0.2                          # 床の上の面（StoreLayout.floorThickness）
REC_FROM = 8845.0                      # 撮り始め（動画の始め。静止画の数もここから数える）
SHOW_S = 10.0
TYPES = {1: "急に止まった", 2: "ひやり", 3: "人がよけさせられた", 4: "立ち往生", 5: "横切り"}
COLORS = {1: (220, 30, 40), 2: (255, 120, 0), 3: (230, 200, 0), 4: (150, 60, 210), 5: (20, 110, 230)}
FAST_COL, OVER_COL = (255, 0, 0), (0, 0, 0)
# 立体の色（配膳の材質。画面での見え方に近い値）
YELLOW, BLUE, WHITE, DARK = (250, 204, 33), (23, 97, 230), (255, 255, 255), (77, 79, 84)
INK, GRAY, PANEL_BG, LINE = (29, 29, 27), (110, 110, 108), (249, 247, 244), (200, 198, 194)
CAM_JA = {"top_wide": "1 真上・引き", "top_near": "2 真上・寄り", "obl_l30_wide": "3 60°見下ろし・左30°・引き",
          "obl_r30_near": "4 60°見下ろし・右30°・寄り"}
CAM_ORDER = ["top_wide", "top_near", "obl_l30_wide", "obl_r30_near"]
WIDE = {"top_wide", "obl_l30_wide"}
SCHEME_JA = {"A": "A ミント×木目", "B": "B 生成り×木目", "C": "C 黄緑×木目"}
STATE = [("待ち", "待っている"), ("行き", "行き"), ("帰り", "帰り"), ("移る", "表と裏口を移る")]


def font(b, s):
    return ImageFont.truetype(FONT_B if b else FONT_R, s)


def clock(t):
    s = int(math.floor(7.5 * 3600 + t + 1e-6))
    return "%d時%02d分%02d秒" % (s // 3600, s % 3600 // 60, s % 60)


class Cam:
    def __init__(self, path):
        j = json.load(open(path, encoding="utf-8"))
        self.name, self.w, self.h = j["name"], j["width"], j["height"]
        self.v, self.p = j["worldToCameraMatrix"], j["projectionMatrix"]
        self.sx, self.sy = SW / self.w, H / self.h

    def px(self, x, z, y=FLOOR_Y):
        P = (x, y, z, 1.0)
        c = [sum(self.v[r][k] * P[k] for k in range(4)) for r in range(4)]
        q = [sum(self.p[r][k] * c[k] for k in range(4)) for r in range(4)]
        if q[3] <= 1e-6:
            return None
        u = (q[0] / q[3] + 1) / 2 * self.w * self.sx
        v = (1 - q[1] / q[3]) / 2 * self.h * self.sy
        return u, v

    def ppm(self, x, z):
        a, b = self.px(x - 0.5, z), self.px(x + 0.5, z)
        return math.hypot(b[0] - a[0], b[1] - a[1])


def ring(d, cam, x, z, r_m, min_px, col, width):
    r = max(r_m, min_px / max(cam.ppm(x, z), 1e-3))
    pts = [cam.px(x + r * math.cos(a), z + r * math.sin(a)) for a in [i * 2 * math.pi / 48 for i in range(49)]]
    if any(p is None for p in pts):
        return
    d.line(pts, fill=col, width=width, joint="curve")


def square(d, cam, x, z, r_m, min_px, col, width):
    r = max(r_m, min_px / max(cam.ppm(x, z), 1e-3))
    pts = [cam.px(x - r, z - r), cam.px(x + r, z - r), cam.px(x + r, z + r), cam.px(x - r, z + r), cam.px(x - r, z - r)]
    d.line(pts, fill=col, width=width)
    return pts


def overlay(im, cam, ev, t, fr, robot_xz):
    d = ImageDraw.Draw(im)
    if cam.name in WIDE and robot_xz is not None:
        ring(d, cam, robot_xz[0], robot_xz[1], 1.2, 15, (40, 40, 40), 6)
        ring(d, cam, robot_xz[0], robot_xz[1], 1.2, 15, (255, 255, 255), 3)
    for e in ev:
        te = float(e["t_s"])
        if te > t + 1e-6 or t - te > SHOW_S:
            continue
        k = int(e["type"])
        fade = 1.0 - (t - te) / SHOW_S * 0.7
        x, z = float(e["x"]), float(e["z"])
        layer = Image.new("RGBA", im.size, (0, 0, 0, 0))
        mark(ImageDraw.Draw(layer), cam, e, k, x, z, fr)
        alpha = layer.split()[3].point(lambda v: int(v * fade))
        layer.putalpha(alpha)
        im.paste(layer, (0, 0), layer)


def mark(d, cam, e, k, x, z, fr):
    a = 255
    if True:
        if k == 6:
            p = square(d, cam, x, z, 0.8, 22, (255, 230, 0, a), 5)
            d.line([p[0], p[2]], fill=OVER_COL + (a,), width=6)
            d.line([p[1], p[3]], fill=OVER_COL + (a,), width=6)
            return
        if k == 2 and e.get("note") == "速いまま":
            square(d, cam, x, z, 0.65, 18, FAST_COL + (a,), 4)
            p = square(d, cam, x, z, 0.85, 25, FAST_COL + (a,), 3)
            d.text((max(q[0] for q in p) + 3, min(q[1] for q in p)), "速", font=fr, fill=FAST_COL + (a,))
            return
        ring(d, cam, x, z, 0.6 if k != 4 else 0.85, 12 if k != 4 else 18, COLORS[k] + (a,), 4)


def legend(im):
    d = ImageDraw.Draw(im)
    f = font(False, 24)
    items = ["患者・付き添い", "杖の人", "車いす", "スタッフ", "ストレッチャー", "ロボット"]
    x0, y0, x = 24, 24, 44
    widths = [int(d.textlength(s, font=f)) for s in items]
    total = sum(widths) + len(items) * 52 + 20
    d.rectangle([x0, y0, x0 + total, y0 + 56], fill=(242, 241, 238))
    y = y0 + 28
    for s, wdt in zip(items, widths):
        if s == "患者・付き添い":
            d.ellipse([x, y - 11, x + 22, y + 11], fill=YELLOW)
        elif s == "杖の人":
            d.ellipse([x, y - 11, x + 22, y + 11], fill=YELLOW)
            d.line([x + 25, y - 9, x + 25, y + 12], fill=DARK, width=4)
        elif s == "車いす":
            d.rectangle([x - 1, y - 13, x + 23, y + 13], fill=DARK)
            d.ellipse([x + 4, y - 7, x + 18, y + 7], fill=YELLOW)
        elif s == "スタッフ":
            d.ellipse([x, y - 11, x + 22, y + 11], fill=BLUE)
        elif s == "ストレッチャー":
            d.rectangle([x - 2, y - 8, x + 18, y + 8], fill=(222, 223, 225), outline=(150, 150, 150))
            d.ellipse([x + 19, y - 6, x + 31, y + 6], fill=BLUE)
        else:
            d.ellipse([x, y - 11, x + 22, y + 11], fill=WHITE, outline=(60, 60, 60), width=2)
        d.text((x + 36, y), s, font=f, fill=INK, anchor="lm")
        x += wdt + 52


def counts(ev, t):
    c = {k: 0 for k in range(1, 8)}
    for e in ev:
        te = float(e["t_s"])
        if REC_FROM - 1e-6 <= te <= t + 1e-6:
            c[int(e["type"])] += 1
            if e["type"] == "2" and e.get("note") == "速いまま":
                c[7] += 1
    return c


def panel(im, row, ev, t, scheme, camname, summ):
    d = ImageDraw.Draw(im)
    d.rectangle([SW, 0, W, H], fill=PANEL_BG)
    x0, xr = SW + 40, W - 40
    f_s, f_m, f_mb, f_big, f_num = font(False, 24), font(False, 27), font(True, 29), font(True, 54), font(True, 46)
    d.text((x0, 24), "病院 h0・%s・%.1fm/秒・種%d" % (summ["plan"], summ["speed_mps"], summ["seed"]), font=f_s, fill=GRAY)
    d.text((x0, 56), "色 %s" % SCHEME_JA[scheme], font=f_s, fill=GRAY)
    d.text((x0, 88), "カメラ %s" % CAM_JA[camname], font=f_s, fill=GRAY)
    d.text((x0, 122), clock(t), font=f_big, fill=INK)
    y = 206
    d.text((x0, y), "ロボット・スタッフがしていること", font=f_mb, fill=INK)
    y += 52
    d.text((x0, y), "ロボット 1台", font=f_m, fill=INK)
    y += 44
    cur = row["leg_dir"]
    for key, ja in STATE:
        on = key == cur
        d.text((x0 + 8, y), ja, font=f_m, fill=INK if on else (150, 150, 148))
        if on:
            d.ellipse([xr - 40, y + 2, xr - 4, y + 38], fill=WHITE, outline=(80, 80, 80), width=3)
            if key in ("行き", "帰り", "移る") and row["leg_from"]:
                d.text((xr - 52, y + 20), "%s→%s" % (row["leg_from"], row["leg_to"]), font=f_s, fill=GRAY, anchor="rm")
        y += 40
    y += 10
    d.text((x0, y), "スタッフ", font=f_m, fill=INK)
    y += 44
    rs = int(row["rescue_staff"])
    d.text((x0 + 8, y), "ロボットの助け", font=f_m, fill=INK if rs else (150, 150, 148))
    for i in range(rs):
        d.rectangle([xr - 36 - i * 44, y + 3, xr - 4 - i * 44, y + 35], fill=BLUE)
    y += 56
    d.line([x0, y, xr, y], fill=LINE, width=2)
    y += 18
    d.text((x0, y + 8), "大廊下の人（x0〜70）", font=f_m, fill=INK)
    d.text((xr, y + 28), row["hall_people"], font=f_num, fill=INK, anchor="rm")
    y += 70
    d.line([x0, y, xr, y], fill=LINE, width=2)
    y += 16
    s0 = int(7.5 * 3600 + REC_FROM)
    d.text((x0, y), "撮り始め（%d:%02d:%02d）からの問題" % (s0 // 3600, s0 % 3600 // 60, s0 % 60), font=f_mb, fill=INK)
    y += 50
    c = counts(ev, t)
    for k in range(1, 6):
        d.ellipse([x0 + 6, y + 6, x0 + 30, y + 30], outline=COLORS[k], width=4)
        d.text((x0 + 44, y + 18), "%d %s" % (k, TYPES[k]), font=f_m, fill=INK, anchor="lm")
        d.text((xr, y + 18), str(c[k]), font=font(True, 34), fill=INK, anchor="rm")
        y += 46
    y += 6
    # 印の見方（速いままのひやり・止まりきれなかった・白の輪）
    d.rectangle([x0 + 9, y + 5, x0 + 27, y + 23], outline=FAST_COL, width=3)
    d.rectangle([x0 + 4, y, x0 + 32, y + 28], outline=FAST_COL, width=2)
    d.text((x0 + 44, y + 14), "うち速いままのひやり %d" % c[7], font=f_s, fill=INK, anchor="lm")
    y += 38
    d.rectangle([x0 + 5, y + 1, x0 + 31, y + 27], outline=(255, 210, 0), width=4)
    d.line([x0 + 9, y + 5, x0 + 27, y + 23], fill=OVER_COL, width=5)
    d.line([x0 + 9, y + 23, x0 + 27, y + 5], fill=OVER_COL, width=5)
    d.text((x0 + 44, y + 14), "止まりきれなかった %d" % c[6], font=f_s, fill=INK, anchor="lm")
    y += 40
    note = "輪＝問題の起きた所（10秒）" + ("・白い輪＝ロボット" if camname in WIDE else "")
    d.text((x0, min(y + 4, H - 40)), note, font=font(False, 21), fill=GRAY)


def compose(run, rowd, ev, summ, cams):
    src = Image.open(os.path.join(run, "look139", rowd["file"])).convert("RGB").resize((SW, H), Image.LANCZOS)
    im = Image.new("RGB", (W, H), PANEL_BG)
    im.paste(src, (0, 0))
    t = float(rowd["t_s"])
    cam = cams[rowd["cam"]]
    overlay(im, cam, ev, t, font(True, 22), (float(rowd["robot_x"]), float(rowd["robot_z"])))
    legend(im)
    panel(im, rowd, ev, t, rowd["scheme"], rowd["cam"], summ)
    return im


def load(run):
    rows = list(csv.DictReader(open(os.path.join(run, "look139", "look_frames.csv"), encoding="utf-8")))
    ev = list(csv.DictReader(open(os.path.join(run, "events.csv"), encoding="utf-8")))
    summ = json.load(open(os.path.join(run, "summary.json"), encoding="utf-8"))
    cams = {n: Cam(os.path.join(run, "look139", "cam_%s.json" % n)) for n in CAM_ORDER}
    return rows, ev, summ, cams


def main():
    mode, run = sys.argv[1], sys.argv[2]
    rows, ev, summ, cams = load(run)
    if mode == "stills":
        out = os.path.join(run, "look139", "panel")
        os.makedirs(out, exist_ok=True)
        for r in rows:
            if r["kind"] != "still":
                continue
            im = compose(run, r, ev, summ, cams)
            f = os.path.join(out, "still_%s_%s.png" % (r["scheme"], r["cam"]))
            im.save(f)
            print(f)
    elif mode == "video":
        camname, dst = sys.argv[3], sys.argv[4]
        tmp = tempfile.mkdtemp(prefix="pan139_")
        vr = [r for r in rows if r["kind"] == "vid" and r["cam"] == camname]
        for i, r in enumerate(vr):
            compose(run, r, ev, summ, cams).save(os.path.join(tmp, "c%05d.png" % i))
        # 0.2 秒ごとのコマを 25 コマ/秒で流す＝店の時刻の 5 倍
        subprocess.check_call([FFMPEG, "-y", "-loglevel", "error", "-framerate", "25", "-i", os.path.join(tmp, "c%05d.png"),
                               "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", dst])
        print(dst, len(vr), "コマ", len(vr) / 25.0, "秒")
    elif mode == "sheets":
        out = sys.argv[3]
        os.makedirs(out, exist_ok=True)
        pdir = os.path.join(run, "look139", "panel")
        fh, fl = font(True, 40), font(True, 30)
        t = float([r for r in rows if r["kind"] == "still"][0]["t_s"])
        head = "依頼139 病院の立体の見た目・%s・%.1fm/秒・種%d・%s" % (summ["plan"], summ["speed_mps"], summ["seed"], clock(t))
        # カメラごとに色3案を横に並べる
        cw, ch, top = 1280, 720, 130
        for i, cn in enumerate(CAM_ORDER):
            sheet = Image.new("RGB", (cw * 3 + 40, ch + top + 20), (255, 255, 255))
            d = ImageDraw.Draw(sheet)
            d.text((20, 18), head + "　カメラ " + CAM_JA[cn], font=fh, fill=INK)
            for j, s in enumerate("ABC"):
                im = Image.open(os.path.join(pdir, "still_%s_%s.png" % (s, cn))).resize((cw, ch), Image.LANCZOS)
                x = 10 + j * (cw + 10)
                sheet.paste(im, (x, top))
                d.text((x, top - 44), SCHEME_JA[s], font=fl, fill=INK)
            f = os.path.join(out, "compare_cam%d_%s.png" % (i + 1, cn))
            sheet.save(f)
            print(f)
        # 12 枚の一覧（横＝色3案・縦＝カメラ4つ）
        cw, ch, top, left = 960, 540, 130, 260
        sheet = Image.new("RGB", (left + cw * 3 + 40, top + (ch + 12) * 4 + 10), (255, 255, 255))
        d = ImageDraw.Draw(sheet)
        d.text((20, 18), head + "　色3案×カメラ4つ", font=fh, fill=INK)
        for j, s in enumerate("ABC"):
            d.text((left + j * (cw + 10), top - 46), SCHEME_JA[s], font=fl, fill=INK)
        for i, cn in enumerate(CAM_ORDER):
            y = top + i * (ch + 12)
            for k, line in enumerate(CAM_JA[cn].split("・")):
                d.text((16, y + 20 + k * 42), line, font=font(True, 28), fill=INK)
            for j, s in enumerate("ABC"):
                im = Image.open(os.path.join(pdir, "still_%s_%s.png" % (s, cn))).resize((cw, ch), Image.LANCZOS)
                sheet.paste(im, (left + j * (cw + 10), y))
        f = os.path.join(out, "overview_12.png")
        sheet.save(f)
        print(f)


if __name__ == "__main__":
    main()
```

#### run139.sh（依頼139 の回し方）

```bash
#!/bin/bash
# 依頼139：見た目入りの回を回す。check＝依頼138 の check138a と同じ回（時間で切替・0.6・種0）を -h139Look on で。shots＝撮る回（廊下だけ・0.6・種0）
cd /Users/shimazueiichi/Robotics_UX/ServiceSim
O=/Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look139
EXE=Builds/hospital139/HospitalSim.app/Contents/MacOS/ServiceSim
CS=$(python3 -c "import json;print(json.load(open('Builds/hospital139/build_info.json'))['unity_cs'])")
COMMON=(-batchmode -h127Speed 0.6 -h127Seed 0 -h127Out $O/runs -h127Cs $CS -h136Mode day -h138Mode 138 -h139Look on)
CAMS=(-h139Cam top_wide,0,90,60,30,142.8,40,2720,2160 -h139Cam top_near,0,90,40.5,22.5,73.2,40,2720,2160
      -h139Cam obl_l30_wide,30,60,54.69,22.45,154.5,40,2720,2160 -h139Cam obl_r30_near,-30,60,20.69,29.05,27.5,40,2720,2160)
case "$1" in
  check) $EXE "${COMMON[@]}" -h127Label check139a -h136Plan switch -logFile $O/logs/check139a.log; echo "check exit $?" ;;
  shots) $EXE "${COMMON[@]}" -h127Label look139 -h136Plan hall "${CAMS[@]}" -h139StillAt 8862 -h139VidFrom 8845 -h139VidTo 8945 -h139VidStep 0.2 -h139VidScheme A -h139EndAfterShots on -logFile $O/logs/look139.log; echo "shots exit $?" ;;
esac
```

#### vid139.sh（動画にまとめる）

```bash
#!/bin/bash
for c in top_wide top_near obl_l30_wide obl_r30_near; do python3 /Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look139/tools/pan139.py video /Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look139/runs/look139_v06_hall_s0 $c /Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look139/videos/hospital139_A_${c}_5x.mp4 & done; wait; echo all done
```


## 作業ログ ― 寄りの見下ろす角度を、見える廊下と問題の輪の数で選ぶ（依頼140）

### 角度を比べる4つの数

寄りの場所と時刻は依頼139 と同じにしたまま、見下ろす角度を 60°・67.5°・75°・82.5° の4つ、向きを右30°（南東から）と左30°（南西から）の2つにして、8枚を撮りました。好みで選ばないように、角度ごとに次の4つを数えてもらいました。

- 見える廊下：寄りの範囲の大廊下の床を10cm 四方のマス8,800個に切り、部屋の箱や壁に隠れずに画面に写っているマスの割合
- 見える輪：その時刻の前10秒に出た問題の輪5個のうち、中心が隠れていない数
- ロボット：100秒（501コマ）のうち、白い筒の上の面が見えているコマの割合
- 人の見分け：廊下に立つ人の筒が、画面で何画素の高さになるか。真上に近いほど低くなり、立体に見えなくなる

| 向き | 見下ろす | 見える廊下 | 見える輪 | ロボットが見える | 人の高さ（足元〜頭の線） |
|---|---|---|---|---|---|
| 右30° | 60° | 82.5% | 2／5 | 42.3% | 56.1画素（45.6） |
| 右30° | 67.5° | 88.9% | 4／5 | 41.7% | 47.8画素（34.6） |
| 右30° | 75° | 93.5% | 5／5 | 41.3% | 39.2画素（23.2） |
| 右30° | 82.5° | 98.4% | 5／5 | 41.3% | 30.5画素（11.7） |
| 左30° | 60° | 79.9% | 2／5 | 44.7% | 56.1画素（45.6） |
| 左30° | 67.5° | 87.5% | 4／5 | 48.5% | 47.8画素（34.6） |
| 左30° | 75° | 93.1% | 5／5 | 49.7% | 39.2画素（23.2） |
| 左30° | 82.5° | 98.3% | 5／5 | 49.9% | 30.5画素（11.7） |

ロボットが見える割合が4〜5割なのは、ロボットが途中で西の縦廊下へ曲がって寄りの範囲から出ていくためです。範囲の中にいる間に壁で隠れたのは、左30°・60°の11コマと67.5°の1コマだけでした。

【画像】LOG483_03_near_angles.png 寄りの角度8枚。行が向き（右30°・左30°）、列が見下ろす角度（60°〜82.5°）。それぞれの下に4つの数

75°で、問題の輪は5個すべて見え、廊下も9割以上見えます。82.5°にすると廊下はほぼ全部見えますが、人の足元から頭までの線が12画素ほどになり、立った筒が丸い点に近づきます。真上の絵とほとんど変わらなくなるので、立体にした意味が薄れます。向きは、ロボットが画面の中にいる時間が長く、待合の長いすが画面の右に入ってどこの廊下か分かる左30°にしました。

- ねらい：廊下の両側が部屋になっている病院で、問題が起きた所を壁に隠さずに見せる
- 変わったこと：寄りの見下ろす角度を配膳の60°から75°に変え、向きを左30°にした
- 見込める効果：見える廊下が79.9%から93.1%に、見える問題の輪が5個中2個から5個に増えた（同じ時刻・同じ場所で測った値）。病院の人が動画で問題の場面を見落としにくくなる見込み

【画像】LOG483_05_near_l30_75.png 決めた寄り（左30°・75°見下ろし）。白い筒がロボット、赤い枠が速いままのひやり、黄色が患者、青がスタッフ

### 引きは廊下と待合に絞り、ふだんは西半分を使う

引きは、全長（廊下と待合だけ。x −0.5〜120.5m・z 12.5〜31.5m）と、西半分（積む所・人の多い区間・待合・西の縦廊下。x −0.5〜72m）の2つの範囲で、真上・60°・75°を撮りました。

【画像】LOG483_04_wide_ranges.png 引き6枚。行が範囲（全長・西半分）、列がカメラ（真上・60°見下ろし左30°・75°見下ろし左30°）

範囲を絞っても、全長は廊下が120m あるので細い帯になり、人は点にしか見えません。西半分の75°なら、積む所から人の多い区間、待合までが1画面に入り、患者・スタッフ・車いすも見分けられます。ふだんの引きは西半分・左30°・75°にし、全長は、ロボットがどの道を走るかを見せたいときなど、ごくたまに使うことにしました（エイイチの決定）。

【画像】LOG483_06_west_l30_75.png 決めた引き（西半分・左30°・75°見下ろし）。左下の白い輪がロボット

色の既定は A にし、B・C は引数で残しました。依頼140 でも、見た目を入れた状態で前と同じ回を回し、記録7つが依頼138・139 と1バイトも違わないことを確かめています。依頼139 では問題の輪を床から0.2m 浮かせて描いていて、60°では床の上で約0.12m ずれていたので、依頼140 で床の高さ0に直しました。

#### 依頼140 の依頼文（引きを廊下に絞り寄りの見やすい角度を探る）

```markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 150
---
# 依頼140：病院の立体の見た目で、引きを廊下と待合に絞り、寄りの見やすい角度を探る

依頼139 の報告（claude_code_reports/20261110_hospital_139.txt）を見て、エイイチと決めたこと（2026-11-10）：
1 色は A（ミント×木目）にする。-h139Look on のときの既定の色を A にする（B・C は引数で残す）
2 決めてほしいこと1：待合の床の差し色 #E4CFAA はこのままでよい
3 引きはフロア全体（120×60m）ではなく、廊下と待合が入る範囲に絞る（全体だと灰色の箱ばかりで、人が点にしか見えないため）
4 決めてほしいこと2：斜めの寄りで廊下の手前半分が部屋の壁に隠れる件は、見やすい角度を探って決める。今回は角度を変えたサンプルを並べるだけで、どれかに決めない
5 決めてほしいこと3：建てていない部屋を1つながりの灰色の箱で描くのは、このままでよい
6 決めてほしいこと4：ストレッチャーが写っていないのは、このままでよい

**今回も見た目と撮り方だけ。試算の数え方・人やロボットの動き・記録の中身は1行も変えない。**
**配膳の場面・配膳の実行ファイル・配膳の台帳、配膳と共通の部品には触らない。** 変えるのは Assets/ServiceSim/Hospital/ の中だけ。部屋の箱・壁の高さ（2.4m）は配膳のベースのまま変えない。
**エイイチは見ていない。人に聞かないと進めない所に当たったら、その項目は止めて報告に書き、ほかは続ける。**

## 正（読むもの）
- params の md5 f72d3810674b31dadd9dfa4ce0141178、h0・h0b は依頼139 と同じ（違えば回さずに報告して終わる）
- 撮る回・時刻は依頼139 と同じ（廊下だけ・0.6m/秒・種0、静止画は 8862 秒、動画は 8845〜8945 秒）。比べられるように変えない

## 始める前に確かめること（違えば回さずに報告して終わる）
作業場所の保留 0（cs:116 のあと）・Unity と回し係が動いていない・KIOXIA がマウント済み・上の md5

## 1. 引きを絞る（真上と斜め）
範囲は2つ作って並べる：
- 全長：x -0.5〜120.5、z 12.5〜31.5（職員の中廊下・待合・外来の大廊下・救急室の前までの廊下すべて）
- 西半分：x -0.5〜72、z 12.5〜31.5（積む所・人の多い区間・待合・西の縦廊下。青のロボットの行きの前半）
それぞれ、真上（90°）と、斜め（左30°）を 60° と 75° の2つ ＝ 2範囲 × 3 ＝ 6枚。
距離の決め方は依頼139 の camfit139.py と同じ（範囲の四隅が画面に入るいちばん近い距離・余白3%）。
引きのロボットの白い輪は今のまま付ける。

## 2. 寄りの角度を探る
寄りの場所は依頼139 と同じ（x9〜31・z26〜32。問題がいちばん多い所）。距離は角度ごとに同じ決め方で合わせる。
- 見下ろす角度：60°・67.5°・75°・82.5° の4つ
- 向き：右30°（依頼139 と同じ南東から）と、左30°（南西から）の2つ
- ＝ 8枚。依頼139 の右30°・60°と同じ値の1枚は、依頼139 と同じ絵になるはずなので、並べて確かめる
- 角度ごとに数で比べる表を作る（見た目の好みではなく、比べるための数）：
  - 見える廊下の割合：範囲の中の大廊下の床（z27〜31）のうち、部屋の箱・壁に隠れずに画面に写っている面積の割合
  - 見える輪の数：この時刻の前10秒に出ている輪（問題の起きた所）のうち、中心が隠れていないものの数／全部の数
  - ロボットが見えるか：ロボットの白い円柱の上の面が隠れていないか（8845〜8945 秒のうち見えている時間の割合）
  - 人の見分け：カプセル1つが画面で何画素の高さになるか（真上に近いほど低くなる。立体に見えるかの目安）
- 動画：寄りの右30°・左30° それぞれ 4角度、計8本（各 20秒・店の時刻の5倍・パネル付き・色A）

## 3. 並べた図
- 引きの6枚を1枚に並べた図（行＝範囲、列＝真上・60°・75°）
- 寄りの8枚を1枚に並べた図（行＝右30°・左30°、列＝角度）。それぞれの絵の下に 2 の4つの数を小さく書く
- 出力：/Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look140/。並べた図2枚は iCloud の …/サービスロボット導入シミュレーション/hospital/look140/ にも写す

## 4. 確かめ（違えば止める）
- 色の既定を A にした実行ファイルで、依頼139 の check139a と同じ回（時間で切替・0.6m/秒・種0）を -h139Look on で回し、記録7つの md5 が依頼138・139 と1バイトも違わないこと
- 瞬間移動 0
- 並べた図を自分で開き、文字や絵が切れていないか、輪がずれて描かれていないか（角度を変えても輪が床の正しい所に重なるか）を見る。見て直したことを書く

check-in（保留0）して、病院の実行ファイルを Builds/hospital140/ に作る。前の実行ファイルは残す。

## 報告
claude_code_reports/20261110_hospital_140.txt に：始める前の確かめ、色の既定の直し、引き6枚と寄り8枚のカメラの値（角度・方位・注視点・距離・カメラの高さ）、2 の数の表、4 の確かめ、出力の場所、check-in の番号、決めてほしいこと（直し方の候補は作らない。どの角度がよいかは書かなくてよい）。
**報告の最後の1行は必ず次のどれか**：「判定：合格」／「判定：止めた（決めてほしいことあり）」／「判定：失敗」
```

#### 依頼140 の結果（依頼箱の結果の全文）

```markdown
# 結果：20261110_140_病院の立体の見た目で引きを廊下に絞り寄りの見やすい角度を探る

- 状態：完了
- 始めた時刻：2026-11-10 08:44
- 終わった時刻：2026-11-10 09:00（16分）
- 作業フォルダ：/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume ae0a1dd2-0e55-4529-a991-df81fe01430f`

## Claude Code の報告

**やったこと**
- 始める前の確かめは4つとも合いました。作業場所の保留は 0（cs:116 のあと）、Unity と回し係は止まっていました。KIOXIA はつながっていて、params の md5 f72d3810… と h0・h0b も依頼139 と同じでした。
- 色の既定を A にしました。引数 -h140Scheme A|B|C|all を足し、既定は A です。静止画も既定では A だけを撮ります。B・C はこの引数で選べ、all にすると依頼139 と同じく3案とも撮ります。
- check-in して cs:117 になり、実行ファイルを Builds/hospital140 に作りました。前の実行ファイルは残しています。
- 引き6枚を撮りました。範囲は全長と西半分の2つで、それぞれ真上・左30° 60°・左30° 75° です。
- 寄り8枚を撮りました。向きは右30°と左30°、見下ろす角度は 60°・67.5°・75°・82.5° です。
- 寄りの動画を8本作りました。各 20 秒・店の時刻の5倍・パネル付き・色A です。
- 4つの数の表、並べた図2枚を作りました。

**結果**
- 依頼139 の右30°・60°と同じ値の1枚は、Unity で撮った元の画像が依頼139 と md5 まで同じでした。
- 寄りの数の表（60° → 67.5° → 75° → 82.5° の順）：

| 数 | 右30° | 左30° |
|---|---|---|
| 見える廊下 | 82.5 → 88.9 → 93.5 → 98.4% | 79.9 → 87.5 → 93.1 → 98.3% |
| 見える輪（5個のうち） | 2 → 4 → 5 → 5 | 2 → 4 → 5 → 5 |
| ロボットの上の面が見える | 41〜42% | 45〜50% |
| 人の高さ（画素） | 56.1 → 47.8 → 39.2 → 30.5 | 同じ |

- ロボットが見える割合が低いのは、8877 秒ごろに寄りの範囲から出ていくためです。範囲の中にいる間に箱に隠れたのは、左30°の 60° で 11 コマ、67.5° で 1 コマだけでした。
- 確かめの回（時間で切替・0.6m/秒・種0）は、記録7つの md5 が依頼138・139 と1バイトも違いませんでした。瞬間移動はロボット・人とも 0 です。
- 並べた図2枚を開いて見ました。文字や絵は切れていません。
- 見て直したこと：床の上の面は高さ 0 なのに、依頼139 では輪を 0.2 m 上に描いていました（60° で約 0.12 m ずれる）。0 に直し、床の角に印を重ねた確かめの図で、絵の床の角と合うのを見ました。
- 隠れた所の輪は、依頼139 と同じく箱の上に重なって描かれます。今回は変えていません。
- 出力は KIOXIA の …/hospital/20261110_look140/ です。並べた図2枚は iCloud の …/hospital/look140/ にも写し、md5 が合うのを確かめました。報告は claude_code_reports/20261110_hospital_140.txt に書きました。

**変えたファイル**
- Assets/ServiceSim/Hospital/HospitalLook.cs
- Assets/ServiceSim/Hospital/HospitalRun.cs（引数を読む所だけ）
- Assets/ServiceSim/Hospital/Editor/HospitalSetup.cs（出力先を hospital140 に）

**決めてほしいこと**
なし（寄りの角度は依頼のとおり、並べただけで決めていません）

判定：合格
```

#### 依頼140 の報告（claude_code_reports/20261110_hospital_140.txt の全文）

```text
依頼140：病院の立体の見た目で、引きを廊下と待合に絞り、寄りの見やすい角度を探る（2026-11-10）
====================================================================

--------------------------------------------------------------------
0. 始める前の確かめ（すべて合格）
--------------------------------------------------------------------
  作業場所の保留：0（最後の check-in は cs:116）
  Unity・回し係：動いていない（Unity Hub だけ）。KIOXIA：マウント済み
  params md5 f72d3810674b31dadd9dfa4ce0141178（作業場所の写しとサイト p2m-site main の両方）
  hospital_h0.json a29cd1d981e5b3086fb623fc647926d9・hospital_h0b.json ddefbd8ca0db243f41601532e7b8dee8（依頼139 と同じ）


--------------------------------------------------------------------
1. 色の既定の直し（Assets/ServiceSim/Hospital/ の中だけ）
--------------------------------------------------------------------
  新しい引数 -h140Scheme A|B|C|all（既定 A）。-h139Look on のとき、場面の色をはじめにこの色にする
  静止画もこの色だけ撮る（依頼139 は3案を順に撮り、最後に C のまま残っていた）。all＝依頼139 と同じく静止画を3案とも撮る（場面は A）
  -h139VidScheme を渡さなければ、動画もこの色。B・C は -h140Scheme B／C で使える
  ほかに、撮るときだけ look139/look_static.csv（動かない物の箱の外形）を書くようにした（見えるかどうかを数えるため。記録7つには入らない）
  変えたファイル：HospitalLook.cs・HospitalRun.cs（引数を読むだけ）・Editor/HospitalSetup.cs（出力先を Builds/hospital140 に）
  場面・材質・部屋の箱・壁の高さ（2.4 m）・配膳の物には触っていない


--------------------------------------------------------------------
2. カメラの値（透視投影・縦の画角 40°・2720×2160 で撮って 1360×1080 に縮める。方位 0＝+z、右30°＝方位 -30＝南東から、
   左30°＝方位 +30＝南西から。距離は依頼139 の camfit139.py と同じ決め方：範囲の四隅（高さ 0 と 2.4 m）が余白 3% の内側に入る
   いちばん近い距離・注視点は写った範囲の真ん中が画面の真ん中に来るように動かす）
--------------------------------------------------------------------
  引き 6枚（白い輪＝ロボットは今のまま付けた）
   名前            範囲    見下ろす 方位  注視点 (x, z)     距離     カメラの高さ  カメラの位置 (x, z)
   w_full_top      全長    90°      0     (60.00, 22.00)   142.8 m  142.8 m       (60.00, 22.00)
   w_full_l30_60   全長    60°      30    (53.60, 20.36)   132.5 m  114.8 m       (20.48, -37.01)
   w_full_l30_75   全長    75°      30    (56.76, 21.29)   134.4 m  129.8 m       (39.37, -8.84)
   w_west_top      西半分  90°      0     (35.75, 22.00)    86.5 m   86.5 m       (35.75, 22.00)
   w_west_l30_60   西半分  60°      30    (32.33, 20.32)    84.4 m   73.1 m       (11.23, -16.23)
   w_west_l30_75   西半分  75°      30    (34.06, 21.26)    85.8 m   82.9 m       (22.96, 2.03)
   全長＝x -0.5〜120.5・z 12.5〜31.5、西半分＝x -0.5〜72・z 12.5〜31.5
   （全長の真上は横の 121 m で距離が決まるので、依頼139 のフロア全体の真上と同じ 142.8 m になった。縦は画面の上下に余る）

  寄り 8枚（範囲 x9〜31・z26〜32。依頼139 と同じ）
   名前        向き    見下ろす  注視点 (x, z)     距離    カメラの高さ  カメラの位置 (x, z)
   n_r30_60    右30°   60°       (20.69, 29.05)   27.5 m  23.82 m       (27.57, 17.14)
   n_r30_675   右30°   67.5°     (20.48, 29.12)   27.8 m  25.68 m       (25.80, 19.91)
   n_r30_75    右30°   75°       (20.29, 29.14)   27.9 m  26.95 m       (23.90, 22.89)
   n_r30_825   右30°   82.5°     (20.15, 29.07)   28.0 m  27.76 m       (21.98, 25.91)
   n_l30_60    左30°   60°       (19.31, 29.05)   27.5 m  23.82 m       (12.44, 17.14)
   n_l30_675   左30°   67.5°     (19.52, 29.12)   27.8 m  25.68 m       (14.20, 19.91)
   n_l30_75    左30°   75°       (19.71, 29.14)   27.9 m  26.95 m       (16.10, 22.89)
   n_l30_825   左30°   82.5°     (19.85, 29.07)   28.0 m  27.76 m       (18.02, 25.91)
  n_r30_60 は依頼139 の obl_r30_near（-30,60,20.69,29.05,27.5）と同じ値。Unity で撮った元の画像（2720×2160）は依頼139 と
  md5 も同じ（31e5d4b39fde501205a982673db31875・違う画素 0）。カメラの行列・位置も同じ

  撮った回・時刻は依頼139 と同じ（廊下だけ・0.6m/秒・種0、静止画 8862 秒＝9時57分42秒、動画 8845〜8945 秒）


--------------------------------------------------------------------
3. 寄りの角度を比べる数（tools/vis140.py・sheets/near_metrics140.csv）
--------------------------------------------------------------------
  決め方：「見える」＝カメラの位置から点までの線が、部屋の箱（見た目の灰色の箱 10 個）と待合の壁（3 枚）を通らず、画面の中にある。
         長いす・カウンター・人・ロボットは隠すものに入れていない。床の上の面は高さ 0（下の 4 (4) を参照）
   ①見える廊下：大廊下の床 x9〜31・z27〜31 を 0.1 m のマス（8,800 個）に切り、マスの中心が見える割合（画面の外に出たマスはどの角度も 0）
   ②見える輪：8852〜8862 秒に出た輪 5 個（events 1202〜1206）の中心が見える数
   ③ロボット：8845〜8945 秒の 0.2 秒ごと 501 コマで、白い円柱の上の面（高さ 1.081 m）の中心が見えるコマの割合。
     ロボットは 8877 秒ごろ西の縦廊下へ曲がって寄りの範囲から出るので、画面の中にいたコマ数と、そのうち箱に隠れたコマ数も書く
   ④人の見分け：(20, 29) に立つカプセル（体 直径 0.4・高さ 1.59 m）の画面の縦の幅（左 1360×1080 の絵の画素）。
     かっこ内は足元から 1.59 m の縦の線の長さ（真上に近いほど短い）

   向き   見下ろす ①見える廊下 ②見える輪 ③ロボット（画面の中／うち隠れ）   ④人の高さ（縦の線）
   右30°  60°      82.5%        2／5      42.3%（212／501 コマ・隠れ 0）    56.1 画素（45.6）
   右30°  67.5°    88.9%        4／5      41.7%（209 コマ・隠れ 0）         47.8 画素（34.6）
   右30°  75°      93.5%        5／5      41.3%（207 コマ・隠れ 0）         39.2 画素（23.2）
   右30°  82.5°    98.4%        5／5      41.3%（207 コマ・隠れ 0）         30.5 画素（11.7）
   左30°  60°      79.9%        2／5      44.7%（235 コマ・隠れ 11）        56.1 画素（45.6）
   左30°  67.5°    87.5%        4／5      48.5%（244 コマ・隠れ 1）         47.8 画素（34.6）
   左30°  75°      93.1%        5／5      49.7%（249 コマ・隠れ 0）         39.2 画素（23.2）
   左30°  82.5°    98.3%        5／5      49.9%（250 コマ・隠れ 0）         30.5 画素（11.7）
  隠れた輪：60° は右・左とも 1202・1204・1205（大廊下の南の端 z27.6〜28.0）、67.5° は 1204（z27.6）
  上の面の縁 8 点もすべて見えるコマの割合（参考）：右 37.9／40.9／40.7／40.9%、左 37.3／44.1／47.7／49.5%（60→82.5°の順）


--------------------------------------------------------------------
4. 確かめ
--------------------------------------------------------------------
  (1) 色の既定を A にした cs:117 の実行ファイルで、check139a と同じ回（時間で切替・0.6m/秒・種0）を -h139Look on で回した
      （check140a_v06_switch_s0・status completed）。記録7つの md5 は依頼138（check138a）・依頼139（check139a）と1バイトも違わない：
        legs.csv c16df55d225bff9b9e2c35391751ef84・events.csv 7e32a56d9806c6354704a3c91af29148・rescues.csv 46f222937bb9562043663e1a3d7f8b2f・
        robot_track.csv 458987516003595228862fd1ebb1f31b・trips.csv 3ae372d5b5eedce24fedf3da531f49e0・moves.csv b20f7c01f8596d37a26906ac7ef31028・
        crowd.csv ad41d0201578ec8e1129ae4d76220ca2
      summary.json で違うのは run・unity_cs・wall_s だけ
  (2) 瞬間移動：check140a ロボット 0・人 0。撮った回 near140・wide140 も 0・0
      撮った2回は、依頼138 の day138_v06_hall_s0 と止めた所まで同じ（robot_track・crowd は先頭からそのまま同じ、
      events は閉じた件数 near 1,213 件・wide 1,204 件が同じ）
  (3) 並べた図2枚と 14 枚を開いて見た：文字・絵は切れていない（パネルの3行目「カメラ 引き・西半分・75°見下ろし・左30°」も右に収まる）。
      引きの白い輪はロボットの所に出ている
  (4) 輪のずれ：look_static.csv で床（Floor01〜04）の上の面が高さ 0 だと分かった。依頼139 の pan139.py は輪を高さ 0.2 m に描いていたので
      （60° で床の上で約 0.12 m ずれる）、依頼140 では 0 に直した（直したこと）。直したあと、寄り4枚（右・左 × 60°・82.5°）に
      床の角 (22.95,27)・(25.05,27)・(25.1,27)・(9,31)・(31,31) を印で重ね、箱・壁の床ぎわの角に重なるのを見た（verify/corners140.png）
  (5) 隠れた所の輪（60°・67.5°）は、依頼139 と同じく箱の上に重なって描かれる（決めてほしいこと2 の件。今回は変えていない）


--------------------------------------------------------------------
5. 出力の場所
--------------------------------------------------------------------
  KIOXIA /Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look140/
    stills/still_A_{w_full_top|w_full_l30_60|w_full_l30_75|w_west_top|w_west_l30_60|w_west_l30_75}.png（引き6枚・パネル付き 1920×1080）
    stills/still_A_n_{r30|l30}_{60|675|75|825}.png（寄り8枚）
    sheets/wide6_140.png（引き6枚を並べた図・行＝範囲・列＝真上・60°・75°）・near8_140.png（寄り8枚・行＝右30°・左30°・列＝角度・絵の下に4つの数）・
           near_metrics140.csv（3 の表）
    videos/hospital140_A_n_{r30|l30}_{60|675|75|825}_5x.mp4（8本・各 20.04 秒・25 コマ/秒・店の時刻の5倍・1920×1080・パネル付き・色A）
    runs/near140_v06_hall_s0・wide140_v06_hall_s0（撮った回。look139/ に元の画像・cam_*.json・look_frames.csv・look_static.csv）・
    runs/check140a_v06_switch_s0（確かめの回）・verify/corners140.png・logs/
    tools/run140.sh（回す）・cams140.py（14 のカメラ）・camfit139.py（依頼139 の写し）・vis140.py（3 の数）・pan140.py（輪とパネル・動画）・sheet140.py（並べた図）
  iCloud …/サービスロボット導入シミュレーション/hospital/look140/ に wide6_140.png・near8_140.png を写した（md5 一致）


--------------------------------------------------------------------
6. check-in・実行ファイル
--------------------------------------------------------------------
  check-in：cs:117（保留 0）
  実行ファイル：Builds/hospital140/HospitalSim.app（build_info：unity_cs ServiceSim/cs:117・pending_changes 空）。
    Builds/hospital127・132・134・136・138・139 はそのまま
  配膳の場面・配膳の実行ファイル・配膳の台帳・配膳と共通の部品には触っていない。試算の数え方・人やロボットの動き・記録の中身は変えていない


--------------------------------------------------------------------
決めてほしいこと
--------------------------------------------------------------------
  なし（寄りの角度は、依頼のとおり 8 枚と数を並べただけで決めていない。エイイチが決める）

判定：合格
```

#### near_metrics140.csv（寄り8枚の4つの数）

```csv
cam,yaw,pitch,tx,tz,dist,cam_h,cam_x,cam_z,hall_visible_pct,hall_inframe_pct,rings_visible,rings_total,rings_hidden_ids,robot_top_center_visible_pct,robot_top_all_visible_pct,robot_frames,robot_inframe_frames,robot_hidden_inframe_frames,person_px,person_axis_px
n_l30_60,30.0,60.0,19.31,29.05,27.5,23.82,12.44,17.14,79.9,100.0,2,5,1202 1204 1205,44.7,37.3,501,235,11,56.1,45.6
n_l30_675,30.0,67.5,19.52,29.12,27.8,25.68,14.2,19.91,87.5,100.0,4,5,1204,48.5,44.1,501,244,1,47.8,34.6
n_l30_75,30.0,75.0,19.71,29.14,27.9,26.95,16.1,22.89,93.1,100.0,5,5,,49.7,47.7,501,249,0,39.2,23.2
n_l30_825,30.0,82.5,19.85,29.07,28.0,27.76,18.02,25.91,98.3,100.0,5,5,,49.9,49.5,501,250,0,30.5,11.7
n_r30_60,-30.0,60.0,20.69,29.05,27.5,23.82,27.57,17.14,82.5,100.0,2,5,1202 1204 1205,42.3,37.9,501,212,0,56.1,45.6
n_r30_675,-30.0,67.5,20.48,29.12,27.8,25.68,25.8,19.91,88.9,100.0,4,5,1204,41.7,40.9,501,209,0,47.8,34.6
n_r30_75,-30.0,75.0,20.29,29.14,27.9,26.95,23.9,22.89,93.5,100.0,5,5,,41.3,40.7,501,207,0,39.2,23.2
n_r30_825,-30.0,82.5,20.15,29.07,28.0,27.76,21.98,25.91,98.4,100.0,5,5,,41.3,40.9,501,207,0,30.5,11.7
```

#### cams140.py（引き6つ・寄り8つのカメラ）

```python
#!/usr/bin/env python3
"""依頼140：引き6・寄り8のカメラ（距離は依頼139 の camfit139.py と同じ決め方）。-h139Cam の引数を出す"""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from camfit139 import fit

WIDE = [("full", "全長", -0.5, 120.5, 12.5, 31.5), ("west", "西半分", -0.5, 72.0, 12.5, 31.5)]
NEAR = (9.0, 31.0, 26.0, 32.0)
NEAR_PITCH = [60.0, 67.5, 75.0, 82.5]


def cams():
    out = []
    for key, ja, x0, x1, z0, z1 in WIDE:
        for yaw, pitch, tag in ((0.0, 90.0, "top"), (30.0, 60.0, "l30_60"), (30.0, 75.0, "l30_75")):
            tx, tz, d = fit(yaw, pitch, x0, x1, z0, z1)
            out.append(("w_%s_%s" % (key, tag), yaw, pitch, round(tx, 2), round(tz, 2), round(d, 1)))
    for yaw, side in ((-30.0, "r30"), (30.0, "l30")):
        for p in NEAR_PITCH:
            tx, tz, d = fit(yaw, p, *NEAR)
            out.append(("n_%s_%s" % (side, ("%g" % p).replace(".", "")), yaw, p, round(tx, 2), round(tz, 2), round(d, 1)))
    return out


if __name__ == "__main__":
    for n, yaw, p, tx, tz, d in cams():
        print("%s,%g,%g,%g,%g,%g,40,2720,2160" % (n, yaw, p, tx, tz, d))
```

#### vis140.py（見える廊下・輪・ロボット・人の高さを数えるコード）

```python
#!/usr/bin/env python3
"""依頼140：寄りのカメラごとに、比べるための数を出す（見た目の好みではない）。

  python3 vis140.py <回のフォルダ> <出力.csv>

隠れの判定：カメラの位置から点までの線分が、動かない物の箱（look_static.csv）のうち「部屋の箱・壁」
（group rooms の全部・waiting の WallBack*/WallWest・world の高さ 0.5 m 超えのもの）を通るかで決める。
長いす・カウンター・座った人・人・ロボットは隠すものに入れない。画面の外（0≤u≤w・0≤v≤h の外）も「写っていない」に数える。
  1 見える廊下の割合：x9〜31・z27〜31 の大廊下の床（床の上の面＝高さ 0）を 0.1 m のマスに切り、マスの中心が写っている割合
  2 見える輪の数：静止画の時刻（8862 秒）の前 10 秒（8852〜8862 秒）に出ている輪（events.csv）の中心（床の上）が写っている数／全部
  3 ロボットが見える割合：8845〜8945 秒の 0.2 秒ごとのコマで、白い円柱の上の面の中心が写っているコマの割合
     （上の面の 8 つの縁の点も数え、すべて写っているコマの割合も出す。画面の中にいたコマ数と、画面の中で箱に隠れたコマ数も出す）
  4 人の見分け：範囲の真ん中 (20, 29) に立つカプセル（体：直径 0.4・高さ 1.59）が、パネル付きの絵（左 1360×1080）で何画素の高さか
     （カプセルの表面の点を画面に移したときの縦の幅）。あわせて足元から頭の上（1.59 m）の縦の線の長さ（画素）
"""
import csv, json, math, os, sys
import numpy as np

FLOOR_Y = 0.0   # 床の上の面（look_static.csv の Floor01〜04 の max_y）
ROBOT_H = 1.081
ROBOT_D = 0.549
STILL_T, SHOW_S = 8862.0, 10.0
CX, CZ = 20.0, 29.0
SW, SH = 1360, 1080


class Cam:
    def __init__(self, path):
        j = json.load(open(path, encoding="utf-8"))
        self.name, self.w, self.h = j["name"], j["width"], j["height"]
        self.V, self.P = np.array(j["worldToCameraMatrix"]), np.array(j["projectionMatrix"])
        self.pos = np.array(j["position_unity"])
        self.spec = j["h139Cam"].split(",")

    def uv(self, pts):
        pts = np.atleast_2d(pts)
        P4 = np.c_[pts, np.ones(len(pts))]
        q = (self.P @ (self.V @ P4.T)).T
        w = q[:, 3]
        u = (q[:, 0] / w + 1) / 2 * self.w
        v = (1 - q[:, 1] / w) / 2 * self.h
        return u, v, w

    def inframe(self, pts):
        u, v, w = self.uv(pts)
        return (w > 1e-6) & (u >= 0) & (u <= self.w) & (v >= 0) & (v <= self.h)


def load_boxes(run):
    path = os.path.join(run, "look139", "look_static.csv")
    lines = open(path, encoding="utf-8").read().splitlines()
    head = dict(kv.split("=") for kv in lines[0][2:].split())
    boxes, kinds = [], {"rooms": 0, "waiting_wall": 0, "world": 0}
    for r in csv.DictReader(lines[1:]):
        mn = [float(r["min_x"]), float(r["min_y"]), float(r["min_z"])]
        mx = [float(r["max_x"]), float(r["max_y"]), float(r["max_z"])]
        name = r["path"].split("/")[-1]
        g = r["group"]
        if g == "rooms":
            k = "rooms"
        elif g == "waiting" and name.startswith(("WallBack", "WallWest")):
            k = "waiting_wall"
        elif g == "world" and mx[1] - mn[1] > 0.5:
            k = "world"
        else:
            continue
        if r["axis_aligned"] != "1":
            raise SystemExit("軸にそろっていない箱があります: " + r["path"])
        boxes.append(mn + mx)
        kinds[k] += 1
    return np.array(boxes), kinds, float(head["robot_base_y"]), float(head["walker_base_y"])


def visible(cam, boxes, pts):
    """pts (N,3) の点がカメラから見えるか（画面の中・箱に隠れていない）"""
    pts = np.atleast_2d(pts).astype(float)
    ok = cam.inframe(pts)
    o = cam.pos
    d = pts - o                                   # (N,3)
    with np.errstate(divide="ignore", invalid="ignore"):
        inv = 1.0 / d
    bmn, bmx = boxes[:, :3], boxes[:, 3:]
    hid = np.zeros(len(pts), bool)
    for i in range(0, len(pts), 2000):
        dd = inv[i:i + 2000, None, :]             # (n,1,3)
        t1 = (bmn[None] - o) * dd
        t2 = (bmx[None] - o) * dd
        t1 = np.where(np.isnan(t1), -np.inf, t1); t2 = np.where(np.isnan(t2), np.inf, t2)
        tmin = np.max(np.minimum(t1, t2), axis=2)
        tmax = np.min(np.maximum(t1, t2), axis=2)
        hit = (tmax >= tmin) & (tmax > 1e-6) & (tmin < 1 - 1e-6)
        hid[i:i + 2000] = hit.any(axis=1)
    return ok & ~hid


def capsule_px(cam, base_y):
    pts = []
    r, h = 0.2, 1.59
    for a in np.linspace(0, 2 * math.pi, 48, endpoint=False):
        for y in np.linspace(r, h - r, 12):
            pts.append((CX + r * math.cos(a), base_y + y, CZ + r * math.sin(a)))
        for b in np.linspace(0, math.pi / 2, 8):
            pts.append((CX + r * math.cos(b) * math.cos(a), base_y + h - r + r * math.sin(b), CZ + r * math.cos(b) * math.sin(a)))
            pts.append((CX + r * math.cos(b) * math.cos(a), base_y + r - r * math.sin(b), CZ + r * math.cos(b) * math.sin(a)))
    u, v, _ = cam.uv(np.array(pts))
    sy = SH / cam.h
    ext = (v.max() - v.min()) * sy
    _, va, _ = cam.uv(np.array([(CX, base_y, CZ), (CX, base_y + h, CZ)]))
    return ext, abs(va[0] - va[1]) * sy


def main():
    run, out = sys.argv[1], sys.argv[2]
    boxes, kinds, rby, wby = load_boxes(run)
    ev = [e for e in csv.DictReader(open(os.path.join(run, "events.csv"), encoding="utf-8"))
          if STILL_T - SHOW_S - 1e-6 <= float(e["t_s"]) <= STILL_T + 1e-6]
    frames = list(csv.DictReader(open(os.path.join(run, "look139", "look_frames.csv"), encoding="utf-8")))
    names = sorted({r["cam"] for r in frames if r["cam"].startswith("n_")})
    xs = np.arange(9.05, 31.0, 0.1); zs = np.arange(27.05, 31.0, 0.1)
    gx, gz = np.meshgrid(xs, zs)
    floor = np.c_[gx.ravel(), np.full(gx.size, FLOOR_Y), gz.ravel()]
    rows = []
    for n in names:
        cam = Cam(os.path.join(run, "look139", "cam_%s.json" % n))
        a = visible(cam, boxes, floor)
        fr_in = cam.inframe(floor)
        ring_pts = np.array([(float(e["x"]), FLOOR_Y, float(e["z"])) for e in ev])
        rv = visible(cam, boxes, ring_pts)
        vf = [r for r in frames if r["kind"] == "vid" and r["cam"] == n]
        top = rby + ROBOT_H
        c_pts = np.array([(float(r["robot_x"]), top, float(r["robot_z"])) for r in vf])
        c_vis = visible(cam, boxes, c_pts)
        c_in = cam.inframe(c_pts)
        rim_all = np.ones(len(vf), bool)
        for k in range(8):
            ang = k * math.pi / 4
            p = c_pts + np.array([ROBOT_D / 2 * math.cos(ang), 0, ROBOT_D / 2 * math.sin(ang)])
            rim_all &= visible(cam, boxes, p)
        ext, axis = capsule_px(cam, wby)
        yaw, pitch, tx, tz, dist = (float(v) for v in cam.spec[1:6])
        rows.append({
            "cam": n, "yaw": yaw, "pitch": pitch, "tx": tx, "tz": tz, "dist": dist, "cam_h": round(float(cam.pos[1]), 2),
            "cam_x": round(float(cam.pos[0]), 2), "cam_z": round(float(cam.pos[2]), 2),
            "hall_visible_pct": round(100 * float(a.mean()), 1), "hall_inframe_pct": round(100 * float(fr_in.mean()), 1),
            "rings_visible": int(rv.sum()), "rings_total": len(ev),
            "rings_hidden_ids": " ".join(e["event_id"] for e, v in zip(ev, rv) if not v),
            "robot_top_center_visible_pct": round(100 * float(c_vis.mean()), 1), "robot_top_all_visible_pct": round(100 * float(rim_all.mean()), 1),
            "robot_frames": len(vf), "robot_inframe_frames": int(c_in.sum()), "robot_hidden_inframe_frames": int((c_in & ~c_vis).sum()), "person_px": round(float(ext), 1), "person_axis_px": round(float(axis), 1),
        })
    with open(out, "w", encoding="utf-8", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        w.writeheader(); w.writerows(rows)
    print("隠すもの:", kinds, "robot_base_y", rby, "walker_base_y", wby, "輪", len(ev))
    for r in rows:
        print(r)


if __name__ == "__main__":
    main()
```

#### pan140.py（輪とパネルを重ねるコード。輪を床の高さ0に直した版）

```python
#!/usr/bin/env python3
"""依頼140（依頼139 の pan139.py をもとに、カメラの名前を cams140.py から取るようにした）。依頼139：病院の立体の見た目の静止画・動画に、問題の輪と右のパネルを付ける（1920×1080）。

左 1360×1080＝Unity で撮った 2720×2160 を縮めたもの。輪は床の上（高さ FLOOR_Y）に、cam_{name}.json の行列で画素に移して描く
（依頼136 の vid136.py と同じ色・同じ印。10 秒出す）。引きのカメラ（top_wide・obl_l30_wide）だけ、ロボットの位置に白の輪を足す。
右 560＝配膳のパネル（rec_lp_20261105/p0_round/after/round_after_panel.png）と同じ作り：白地・小さな見出し・大きな時刻・
「ロボット・スタッフがしていること」・数。中身は病院の値（ロボットの状態・助けに出ているスタッフ・大廊下の人数・撮り始めからの問題1〜5）。

  python3 pan139.py stills <回のフォルダ>
  python3 pan139.py video <回のフォルダ> <cam> <出力.mp4>
  python3 pan139.py sheets <回のフォルダ> <出力フォルダ>
"""
import csv, json, math, os, subprocess, sys, tempfile
from PIL import Image, ImageDraw, ImageFont

FFMPEG = "/opt/homebrew/Caskroom/miniforge/base/envs/ros_env/bin/ffmpeg"
FONT_B = os.path.expanduser("~/Library/Fonts/NotoSansJP-Bold.otf")
FONT_R = os.path.expanduser("~/Library/Fonts/NotoSansJP-Regular.otf")
W, H, SW = 1920, 1080, 1360           # 全体・左の場面の幅（配膳のパネル付きと同じ割り）
FLOOR_Y = 0.0                          # 床の上の面（依頼140：look_static.csv の床 Floor01〜04 の上の面は 0。依頼139 は 0.2 で描いていた）
REC_FROM = 8845.0                      # 撮り始め（動画の始め。静止画の数もここから数える）
SHOW_S = 10.0
TYPES = {1: "急に止まった", 2: "ひやり", 3: "人がよけさせられた", 4: "立ち往生", 5: "横切り"}
COLORS = {1: (220, 30, 40), 2: (255, 120, 0), 3: (230, 200, 0), 4: (150, 60, 210), 5: (20, 110, 230)}
FAST_COL, OVER_COL = (255, 0, 0), (0, 0, 0)
# 立体の色（配膳の材質。画面での見え方に近い値）
YELLOW, BLUE, WHITE, DARK = (250, 204, 33), (23, 97, 230), (255, 255, 255), (77, 79, 84)
INK, GRAY, PANEL_BG, LINE = (29, 29, 27), (110, 110, 108), (249, 247, 244), (200, 198, 194)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cams140 import cams as _cams


def _ja(n, yaw, p):
    if n.startswith("w_"):
        rng = "全長" if n.startswith("w_full") else "西半分"
        return "引き・%s・%s" % (rng, "真上" if p == 90 else "%g°見下ろし・左30°" % p)
    return "寄り・%g°見下ろし・%s30°" % (p, "右" if yaw < 0 else "左")


CAM_LIST = _cams()
CAM_JA = {c[0]: _ja(c[0], c[1], c[2]) for c in CAM_LIST}
CAM_ORDER = [c[0] for c in CAM_LIST]
WIDE = {c[0] for c in CAM_LIST if c[0].startswith("w_")}
SCHEME_JA = {"A": "A ミント×木目", "B": "B 生成り×木目", "C": "C 黄緑×木目"}
STATE = [("待ち", "待っている"), ("行き", "行き"), ("帰り", "帰り"), ("移る", "表と裏口を移る")]


def font(b, s):
    return ImageFont.truetype(FONT_B if b else FONT_R, s)


def clock(t):
    s = int(math.floor(7.5 * 3600 + t + 1e-6))
    return "%d時%02d分%02d秒" % (s // 3600, s % 3600 // 60, s % 60)


class Cam:
    def __init__(self, path):
        j = json.load(open(path, encoding="utf-8"))
        self.name, self.w, self.h = j["name"], j["width"], j["height"]
        self.v, self.p = j["worldToCameraMatrix"], j["projectionMatrix"]
        self.sx, self.sy = SW / self.w, H / self.h

    def px(self, x, z, y=FLOOR_Y):
        P = (x, y, z, 1.0)
        c = [sum(self.v[r][k] * P[k] for k in range(4)) for r in range(4)]
        q = [sum(self.p[r][k] * c[k] for k in range(4)) for r in range(4)]
        if q[3] <= 1e-6:
            return None
        u = (q[0] / q[3] + 1) / 2 * self.w * self.sx
        v = (1 - q[1] / q[3]) / 2 * self.h * self.sy
        return u, v

    def ppm(self, x, z):
        a, b = self.px(x - 0.5, z), self.px(x + 0.5, z)
        return math.hypot(b[0] - a[0], b[1] - a[1])


def ring(d, cam, x, z, r_m, min_px, col, width):
    r = max(r_m, min_px / max(cam.ppm(x, z), 1e-3))
    pts = [cam.px(x + r * math.cos(a), z + r * math.sin(a)) for a in [i * 2 * math.pi / 48 for i in range(49)]]
    if any(p is None for p in pts):
        return
    d.line(pts, fill=col, width=width, joint="curve")


def square(d, cam, x, z, r_m, min_px, col, width):
    r = max(r_m, min_px / max(cam.ppm(x, z), 1e-3))
    pts = [cam.px(x - r, z - r), cam.px(x + r, z - r), cam.px(x + r, z + r), cam.px(x - r, z + r), cam.px(x - r, z - r)]
    d.line(pts, fill=col, width=width)
    return pts


def overlay(im, cam, ev, t, fr, robot_xz):
    d = ImageDraw.Draw(im)
    if cam.name in WIDE and robot_xz is not None:
        ring(d, cam, robot_xz[0], robot_xz[1], 1.2, 15, (40, 40, 40), 6)
        ring(d, cam, robot_xz[0], robot_xz[1], 1.2, 15, (255, 255, 255), 3)
    for e in ev:
        te = float(e["t_s"])
        if te > t + 1e-6 or t - te > SHOW_S:
            continue
        k = int(e["type"])
        fade = 1.0 - (t - te) / SHOW_S * 0.7
        x, z = float(e["x"]), float(e["z"])
        layer = Image.new("RGBA", im.size, (0, 0, 0, 0))
        mark(ImageDraw.Draw(layer), cam, e, k, x, z, fr)
        alpha = layer.split()[3].point(lambda v: int(v * fade))
        layer.putalpha(alpha)
        im.paste(layer, (0, 0), layer)


def mark(d, cam, e, k, x, z, fr):
    a = 255
    if True:
        if k == 6:
            p = square(d, cam, x, z, 0.8, 22, (255, 230, 0, a), 5)
            d.line([p[0], p[2]], fill=OVER_COL + (a,), width=6)
            d.line([p[1], p[3]], fill=OVER_COL + (a,), width=6)
            return
        if k == 2 and e.get("note") == "速いまま":
            square(d, cam, x, z, 0.65, 18, FAST_COL + (a,), 4)
            p = square(d, cam, x, z, 0.85, 25, FAST_COL + (a,), 3)
            d.text((max(q[0] for q in p) + 3, min(q[1] for q in p)), "速", font=fr, fill=FAST_COL + (a,))
            return
        ring(d, cam, x, z, 0.6 if k != 4 else 0.85, 12 if k != 4 else 18, COLORS[k] + (a,), 4)


def legend(im):
    d = ImageDraw.Draw(im)
    f = font(False, 24)
    items = ["患者・付き添い", "杖の人", "車いす", "スタッフ", "ストレッチャー", "ロボット"]
    x0, y0, x = 24, 24, 44
    widths = [int(d.textlength(s, font=f)) for s in items]
    total = sum(widths) + len(items) * 52 + 20
    d.rectangle([x0, y0, x0 + total, y0 + 56], fill=(242, 241, 238))
    y = y0 + 28
    for s, wdt in zip(items, widths):
        if s == "患者・付き添い":
            d.ellipse([x, y - 11, x + 22, y + 11], fill=YELLOW)
        elif s == "杖の人":
            d.ellipse([x, y - 11, x + 22, y + 11], fill=YELLOW)
            d.line([x + 25, y - 9, x + 25, y + 12], fill=DARK, width=4)
        elif s == "車いす":
            d.rectangle([x - 1, y - 13, x + 23, y + 13], fill=DARK)
            d.ellipse([x + 4, y - 7, x + 18, y + 7], fill=YELLOW)
        elif s == "スタッフ":
            d.ellipse([x, y - 11, x + 22, y + 11], fill=BLUE)
        elif s == "ストレッチャー":
            d.rectangle([x - 2, y - 8, x + 18, y + 8], fill=(222, 223, 225), outline=(150, 150, 150))
            d.ellipse([x + 19, y - 6, x + 31, y + 6], fill=BLUE)
        else:
            d.ellipse([x, y - 11, x + 22, y + 11], fill=WHITE, outline=(60, 60, 60), width=2)
        d.text((x + 36, y), s, font=f, fill=INK, anchor="lm")
        x += wdt + 52


def counts(ev, t):
    c = {k: 0 for k in range(1, 8)}
    for e in ev:
        te = float(e["t_s"])
        if REC_FROM - 1e-6 <= te <= t + 1e-6:
            c[int(e["type"])] += 1
            if e["type"] == "2" and e.get("note") == "速いまま":
                c[7] += 1
    return c


def panel(im, row, ev, t, scheme, camname, summ):
    d = ImageDraw.Draw(im)
    d.rectangle([SW, 0, W, H], fill=PANEL_BG)
    x0, xr = SW + 40, W - 40
    f_s, f_m, f_mb, f_big, f_num = font(False, 24), font(False, 27), font(True, 29), font(True, 54), font(True, 46)
    d.text((x0, 24), "病院 h0・%s・%.1fm/秒・種%d" % (summ["plan"], summ["speed_mps"], summ["seed"]), font=f_s, fill=GRAY)
    d.text((x0, 56), "色 %s" % SCHEME_JA[scheme], font=f_s, fill=GRAY)
    d.text((x0, 88), "カメラ %s" % CAM_JA[camname], font=f_s, fill=GRAY)
    d.text((x0, 122), clock(t), font=f_big, fill=INK)
    y = 206
    d.text((x0, y), "ロボット・スタッフがしていること", font=f_mb, fill=INK)
    y += 52
    d.text((x0, y), "ロボット 1台", font=f_m, fill=INK)
    y += 44
    cur = row["leg_dir"]
    for key, ja in STATE:
        on = key == cur
        d.text((x0 + 8, y), ja, font=f_m, fill=INK if on else (150, 150, 148))
        if on:
            d.ellipse([xr - 40, y + 2, xr - 4, y + 38], fill=WHITE, outline=(80, 80, 80), width=3)
            if key in ("行き", "帰り", "移る") and row["leg_from"]:
                d.text((xr - 52, y + 20), "%s→%s" % (row["leg_from"], row["leg_to"]), font=f_s, fill=GRAY, anchor="rm")
        y += 40
    y += 10
    d.text((x0, y), "スタッフ", font=f_m, fill=INK)
    y += 44
    rs = int(row["rescue_staff"])
    d.text((x0 + 8, y), "ロボットの助け", font=f_m, fill=INK if rs else (150, 150, 148))
    for i in range(rs):
        d.rectangle([xr - 36 - i * 44, y + 3, xr - 4 - i * 44, y + 35], fill=BLUE)
    y += 56
    d.line([x0, y, xr, y], fill=LINE, width=2)
    y += 18
    d.text((x0, y + 8), "大廊下の人（x0〜70）", font=f_m, fill=INK)
    d.text((xr, y + 28), row["hall_people"], font=f_num, fill=INK, anchor="rm")
    y += 70
    d.line([x0, y, xr, y], fill=LINE, width=2)
    y += 16
    s0 = int(7.5 * 3600 + REC_FROM)
    d.text((x0, y), "撮り始め（%d:%02d:%02d）からの問題" % (s0 // 3600, s0 % 3600 // 60, s0 % 60), font=f_mb, fill=INK)
    y += 50
    c = counts(ev, t)
    for k in range(1, 6):
        d.ellipse([x0 + 6, y + 6, x0 + 30, y + 30], outline=COLORS[k], width=4)
        d.text((x0 + 44, y + 18), "%d %s" % (k, TYPES[k]), font=f_m, fill=INK, anchor="lm")
        d.text((xr, y + 18), str(c[k]), font=font(True, 34), fill=INK, anchor="rm")
        y += 46
    y += 6
    # 印の見方（速いままのひやり・止まりきれなかった・白の輪）
    d.rectangle([x0 + 9, y + 5, x0 + 27, y + 23], outline=FAST_COL, width=3)
    d.rectangle([x0 + 4, y, x0 + 32, y + 28], outline=FAST_COL, width=2)
    d.text((x0 + 44, y + 14), "うち速いままのひやり %d" % c[7], font=f_s, fill=INK, anchor="lm")
    y += 38
    d.rectangle([x0 + 5, y + 1, x0 + 31, y + 27], outline=(255, 210, 0), width=4)
    d.line([x0 + 9, y + 5, x0 + 27, y + 23], fill=OVER_COL, width=5)
    d.line([x0 + 9, y + 23, x0 + 27, y + 5], fill=OVER_COL, width=5)
    d.text((x0 + 44, y + 14), "止まりきれなかった %d" % c[6], font=f_s, fill=INK, anchor="lm")
    y += 40
    note = "輪＝問題の起きた所（10秒）" + ("・白い輪＝ロボット" if camname in WIDE else "")
    d.text((x0, min(y + 4, H - 40)), note, font=font(False, 21), fill=GRAY)


def compose(run, rowd, ev, summ, cams):
    src = Image.open(os.path.join(run, "look139", rowd["file"])).convert("RGB").resize((SW, H), Image.LANCZOS)
    im = Image.new("RGB", (W, H), PANEL_BG)
    im.paste(src, (0, 0))
    t = float(rowd["t_s"])
    cam = cams[rowd["cam"]]
    overlay(im, cam, ev, t, font(True, 22), (float(rowd["robot_x"]), float(rowd["robot_z"])))
    legend(im)
    panel(im, rowd, ev, t, rowd["scheme"], rowd["cam"], summ)
    return im


def load(run):
    rows = list(csv.DictReader(open(os.path.join(run, "look139", "look_frames.csv"), encoding="utf-8")))
    ev = list(csv.DictReader(open(os.path.join(run, "events.csv"), encoding="utf-8")))
    summ = json.load(open(os.path.join(run, "summary.json"), encoding="utf-8"))
    cams = {n: Cam(os.path.join(run, "look139", "cam_%s.json" % n)) for n in CAM_ORDER
            if os.path.exists(os.path.join(run, "look139", "cam_%s.json" % n))}
    return rows, ev, summ, cams


def main():
    mode, run = sys.argv[1], sys.argv[2]
    rows, ev, summ, cams = load(run)
    if mode == "stills":
        out = os.path.join(run, "look139", "panel")
        os.makedirs(out, exist_ok=True)
        for r in rows:
            if r["kind"] != "still":
                continue
            im = compose(run, r, ev, summ, cams)
            f = os.path.join(out, "still_%s_%s.png" % (r["scheme"], r["cam"]))
            im.save(f)
            print(f)
    elif mode == "video":
        camname, dst = sys.argv[3], sys.argv[4]
        tmp = tempfile.mkdtemp(prefix="pan139_")
        vr = [r for r in rows if r["kind"] == "vid" and r["cam"] == camname]
        for i, r in enumerate(vr):
            compose(run, r, ev, summ, cams).save(os.path.join(tmp, "c%05d.png" % i))
        # 0.2 秒ごとのコマを 25 コマ/秒で流す＝店の時刻の 5 倍
        subprocess.check_call([FFMPEG, "-y", "-loglevel", "error", "-framerate", "25", "-i", os.path.join(tmp, "c%05d.png"),
                               "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", dst])
        print(dst, len(vr), "コマ", len(vr) / 25.0, "秒")


if __name__ == "__main__":
    main()
```

#### sheet140.py（並べた図を作るコード）

```python
#!/usr/bin/env python3
"""依頼140：並べた図2枚。引き6枚（行＝範囲・列＝真上・60°・75°）と寄り8枚（行＝右30°・左30°・列＝角度。絵の下に4つの数）

  python3 sheet140.py <出力フォルダ>   （stills/ のパネル付きの絵と sheets/near_metrics140.csv を読む）
"""
import csv, os, sys
from PIL import Image, ImageDraw, ImageFont

O = "/Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look140"
FB = os.path.expanduser("~/Library/Fonts/NotoSansJP-Bold.otf")
FR = os.path.expanduser("~/Library/Fonts/NotoSansJP-Regular.otf")
INK, GRAY = (29, 29, 27), (90, 90, 88)
HEAD = "病院 h0・廊下だけ・0.6m/秒・種0・9時57分42秒・色A"


def f(b, s):
    return ImageFont.truetype(FB if b else FR, s)


def wide(out):
    cw, ch, top, left, gap = 1280, 720, 150, 300, 14
    rows = [("full", "全長", "x-0.5〜120.5・z12.5〜31.5"), ("west", "西半分", "x-0.5〜72・z12.5〜31.5")]
    cols = [("top", "真上（90°）"), ("l30_60", "60°見下ろし・左30°"), ("l30_75", "75°見下ろし・左30°")]
    sheet = Image.new("RGB", (left + (cw + gap) * 3 + 10, top + (ch + 70) * 2 + 10), (255, 255, 255))
    d = ImageDraw.Draw(sheet)
    d.text((20, 20), "依頼140 引きを廊下と待合に絞る（6枚）　" + HEAD, font=f(True, 44), fill=INK)
    for j, (k, ja) in enumerate(cols):
        d.text((left + j * (cw + gap), top - 56), ja, font=f(True, 36), fill=INK)
    for i, (k, ja, rng) in enumerate(rows):
        y = top + i * (ch + 70)
        d.text((20, y + 20), ja, font=f(True, 40), fill=INK)
        d.text((20, y + 80), rng.split("・")[0], font=f(False, 28), fill=GRAY)
        d.text((20, y + 118), rng.split("・")[1], font=f(False, 28), fill=GRAY)
        for j, (c, _) in enumerate(cols):
            im = Image.open(os.path.join(O, "stills", "still_A_w_%s_%s.png" % (k, c))).resize((cw, ch), Image.LANCZOS)
            sheet.paste(im, (left + j * (cw + gap), y))
    p = os.path.join(out, "wide6_140.png")
    sheet.save(p)
    print(p, sheet.size)


def near(out):
    m = {r["cam"]: r for r in csv.DictReader(open(os.path.join(O, "sheets", "near_metrics140.csv"), encoding="utf-8"))}
    cw, ch, top, left, gap, note = 1120, 630, 150, 240, 14, 150
    pitches = [("60", "60°"), ("675", "67.5°"), ("75", "75°"), ("825", "82.5°")]
    rows = [("r30", "右30°", "南東から"), ("l30", "左30°", "南西から")]
    sheet = Image.new("RGB", (left + (cw + gap) * 4 + 10, top + (ch + note + 20) * 2 + 60), (255, 255, 255))
    d = ImageDraw.Draw(sheet)
    d.text((20, 20), "依頼140 寄りの角度（8枚・x9〜31・z26〜32）　" + HEAD, font=f(True, 44), fill=INK)
    for j, (k, ja) in enumerate(pitches):
        d.text((left + j * (cw + gap), top - 56), "見下ろす " + ja, font=f(True, 36), fill=INK)
    fs = f(False, 26)
    for i, (k, ja, sub) in enumerate(rows):
        y = top + i * (ch + note + 20)
        d.text((20, y + 20), ja, font=f(True, 40), fill=INK)
        d.text((20, y + 80), sub, font=f(False, 28), fill=GRAY)
        for j, (p, _) in enumerate(pitches):
            n = "n_%s_%s" % (k, p)
            x = left + j * (cw + gap)
            im = Image.open(os.path.join(O, "stills", "still_A_%s.png" % n)).resize((cw, ch), Image.LANCZOS)
            sheet.paste(im, (x, y))
            r = m[n]
            lines = ["見える廊下 %s%%　見える輪 %s／%s" % (r["hall_visible_pct"], r["rings_visible"], r["rings_total"]),
                     "ロボットの上の面が見える %s%%（画面の中 %s／%s コマ・うち隠れ %s）" % (
                         r["robot_top_center_visible_pct"], r["robot_inframe_frames"], r["robot_frames"], r["robot_hidden_inframe_frames"]),
                     "人（カプセル）の高さ %s 画素（足元〜頭の線 %s 画素）" % (r["person_px"], r["person_axis_px"])]
            for k2, s in enumerate(lines):
                d.text((x + 6, y + ch + 10 + k2 * 40), s, font=fs, fill=INK)
    y = top + (ch + note + 20) * 2
    d.text((20, y), "数の決め方：見える＝カメラから見て部屋の箱・壁に隠れず画面の中にある。廊下＝x9〜31・z27〜31 の床。輪＝8852〜8862 秒に出た輪の中心。"
           "ロボット＝8845〜8945 秒・0.2 秒ごと 501 コマで白い円柱の上の面の中心。人の高さ＝(20, 29) に立つカプセル（1.59 m）の画面の縦の幅（左 1360×1080 の絵で）",
           font=f(False, 24), fill=GRAY)
    pth = os.path.join(out, "near8_140.png")
    sheet.save(pth)
    print(pth, sheet.size)


if __name__ == "__main__":
    out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(O, "sheets")
    wide(out)
    near(out)
```

#### run140.sh（依頼140 の回し方）

```bash
#!/bin/bash
# 依頼140：check＝依頼139 の check139a と同じ回（時間で切替・0.6・種0）を cs:117 の実行ファイル・-h139Look on（色の既定 A）で。
# wide＝引き6枚（静止画だけ）。near＝寄り8枚（静止画＋動画 8845〜8945 秒・0.2 秒ごと）。撮る回は廊下だけ・0.6・種0（依頼139 と同じ）
cd /Users/shimazueiichi/Robotics_UX/ServiceSim
O=/Volumes/KIOXIA/r2r2r-data/service_sim/unity/hospital/20261110_look140
EXE=Builds/hospital140/HospitalSim.app/Contents/MacOS/ServiceSim
CS=$(python3 -c "import json;print(json.load(open('Builds/hospital140/build_info.json'))['unity_cs'])")
COMMON=(-batchmode -h127Speed 0.6 -h127Seed 0 -h127Out $O/runs -h127Cs $CS -h136Mode day -h138Mode 138 -h139Look on)
WCAMS=(); NCAMS=()
while read -r c; do
  case "$c" in w_*) WCAMS+=(-h139Cam "$c");; n_*) NCAMS+=(-h139Cam "$c");; esac
done < <(python3 $O/tools/cams140.py)
case "$1" in
  check) $EXE "${COMMON[@]}" -h127Label check140a -h136Plan switch -logFile $O/logs/check140a.log; echo "check exit $?" ;;
  wide) $EXE "${COMMON[@]}" -h127Label wide140 -h136Plan hall "${WCAMS[@]}" -h139StillAt 8862 -h139EndAfterShots on -logFile $O/logs/wide140.log; echo "wide exit $?" ;;
  near) $EXE "${COMMON[@]}" -h127Label near140 -h136Plan hall "${NCAMS[@]}" -h139StillAt 8862 -h139VidFrom 8845 -h139VidTo 8945 -h139VidStep 0.2 -h139EndAfterShots on -logFile $O/logs/near140.log; echo "near exit $?" ;;
esac
```


## 作業ログ ― 外来の廊下の車いす・杖の人の割合を、公開統計から推計する

### 病院の廊下で人を数えた公開データは見つからなかった

まず、病院の外来の廊下や玄関で、車いすや杖の人が何人通ったかを数えた調査を探しました。外来の待合の座席を観察した建築の研究や、外来の満足度調査はありましたが、廊下を通る人のうちの車いす・杖の割合を数字で出したものは、読める形では見つかりませんでした。そこで、全国の統計を掛け合わせて推計する形にしました。

### 掛け合わせた3つの数

廊下の人のうちの割合 ＝ 来院者の割合 × 来院者のうち患者の割合 × 患者のうち65歳以上の割合 × 65歳以上で道具を使う割合

| 数 | 値 | 出典 |
|---|---|---|
| 廊下の人のうち来院者 | 90% | 残りの10%はスタッフ（前の回のまま） |
| 来院者のうち患者 | 約2/3 | 仮置き（残りは付き添い）。図解のつまみで1/2〜3/4に動かせる |
| 患者のうち65歳以上 | 50.8% | [患者調査 2023](https://www.mhlw.go.jp/toukei/saikin/hw/kanja/23/dl/kanjya.pdf)。外来の推計患者 727.5万人のうち65歳以上 369.8万人（75歳以上は 227.5万人で31.3%） |
| 65歳以上で車いすを使う | 1.96%〜6.1% | 下＝介護保険の車いすの貸与 70.99万件（2024年4月）÷ 65歳以上 3,625万人、上＝米国 NHATS の 6.1% |
| 65歳以上で杖・歩行器を使う | 3.41%〜28.0% | 下＝介護保険の歩行器 94.26万件＋歩行補助つえ 29.34万件 ÷ 3,625万人、上＝NHATS の杖 16.4%＋歩行器 11.6% |

患者調査は、厚生労働省が3年ごとに、全国の病院・診療所を受診した患者を調べる統計です。2023年10月の指定した1日に、どんな年齢の患者が何人受診したかが分かります。

介護保険の貸与の件数は、厚生労働省の[介護保険制度における福祉用具の資料](https://www.mhlw.go.jp/content/12300000/001483102.pdf)にある、介護給付費等実態統計の種目ごとの件数です。介護保険で車いすや歩行器を借りている人の数に近いものですが、自分で買った人は入りません。杖は買う人が多いので、杖の数はかなり少なく出ます。65歳以上の人口は総務省の[統計からみた我が国の高齢者](https://www.stat.go.jp/data/topics/pdf/topics142.pdf)（2024年9月）の値です。

NHATS（National Health and Aging Trends Study）は、米国の65歳以上を毎年追いかけている全国調査です。[Gell らの分析（2015年）](https://www.sciencedaily.com/releases/2015/05/150511154921.htm)では、65歳以上の4人に1人以上が何かの移動の道具を使い、杖16.4%・歩行器11.6%・車いす6.1%でした。家の中だけで使う人も含み、杖と歩行器の両方を使う人を重ねて数えているので、廊下にいる人の割合としては多めに出ます。

### 推計の幅は、車いす約0.6〜1.9%、杖・歩行器約1.0〜8.5%

患者の割合を2/3にすると、廊下の人に掛かる数は 0.9 × 2/3 × 0.508 ＝ 約0.305 です。

| | 下（日本の介護保険の貸与） | 上（米国 NHATS） |
|---|---|---|
| 車いす | 0.305 × 1.96% ＝ 0.60% | 0.305 × 6.1% ＝ 1.86% |
| 杖・歩行器 | 0.305 × 3.41% ＝ 1.04% | 0.305 × 28.0% ＝ 8.54% |

前の回の各10%は、上の値と比べても車いすで5倍以上、杖でも1割以上多い値でした。前の回では、ロボットが止まりきれなかった相手の多くが車いすだったので、そのぶん問題の回数が多めに出ていたことになります。

この幅を出す途中で、一度「車いす約1〜2%・杖と歩行器約3〜9%」と伝えてから、計算の式を書き直しました。杖の下側の3%は、日本の貸与の値を使うと約1%になり、車いすの下側も約0.6%でした。選んだ値（車いす1%・杖の人3%）は、書き直した幅の中にも入っています。

### 選んだ値：車いす1%・杖の人3%

推計の幅のうち、車いすは真ん中より少し下、杖の人は下寄りの値を、エイイチが選びました。杖の人には、歩行器や押し車の人も含めます。歩く患者・付き添いは86%、スタッフは10%のままです。

- ねらい：根拠のない仮置きで多めに出ていた問題の回数を、公開統計から見て無理のない人の内訳に直す
- 変わったこと：廊下の人の内訳を、車いす10%・杖の人10%から、車いす1%・杖の人3%に変えた。根拠の欄は「仮置き」から「公開統計からの推計」になり、計算の式と出典を前提の表に書いた
- 見込める効果：車いすの前を横切る・車いすに止まりきれない、といった問題の回数が、前の回より減る見込み。どれだけ減るかは、新しい内訳で Unity を回し直して確かめる（まだ回していない）

値は、サイトの前提の表（p2m-site の tools/hospital_sim/assumptions.json）に入れ、Unity が読む条件（hospital-unity-params.json）を書き出し直しました。Unity の側はこの条件を読むだけなので、Unity のコードは変えていません。現場の廊下で数えた値が手に入ったら、それで置き換えます。

#### tools/hospital_sim/assumptions.json の people_mix（入れ直した後の全文）

```json
"people_mix": {
 "value": {
  "歩く患者・付き添い": 0.86,
  "杖の人": 0.03,
  "車いす": 0.01,
  "スタッフ": 0.1
 },
 "unit": "割合",
 "what": "人の内訳（外来の大廊下）。杖の人には歩行器・押し車の人も含める",
 "basis": "公開統計からの推計",
 "source": "外来の大廊下の人のうち、スタッフ10%を除いた9割を来院者とし、その2/3を患者（仮置き）、患者の50.8%を65歳以上（患者調査 2023 の外来の推計患者）とした（係数 0.9×2/3×0.508＝約0.305）。これに65歳以上で道具を使う割合を掛けた。車いす：下＝介護保険の車いすの貸与 70.99万件（介護給付費等実態統計 2024年4月）÷65歳以上 3,625万人（総務省 2024年9月）＝1.96%、上＝米国 NHATS 6.1% → 廊下の約0.6〜1.9%。杖・歩行器：下＝介護保険の歩行器と歩行補助つえの貸与 123.6万件÷3,625万人＝3.41%（杖は買う人が多く、少なく出る）、上＝NHATS の杖16.4%＋歩行器11.6%（重なりを引いていないので多めに出る）→ 廊下の約1.0〜8.5%。幅の中から、エイイチが車いす1%・杖の人3%を選んだ（2026-11-10）。前は車いす・杖とも10%（根拠なしの仮置き・LOG477）",
 "n": null,
 "updated": "2026-11-10",
 "range": {
  "車いす": [
   0.006,
   0.019
  ],
  "杖の人": [
   0.01,
   0.085
  ]
 },
 "note": "病院の廊下で数えた公開データは見つからなかった。現場で数えられたら置き換える"
}
```

#### hospital-unity-params.json の変わった所（git diff）

```diff
diff --git a/tools/hospital_sim/hospital-unity-params.json b/tools/hospital_sim/hospital-unity-params.json
index 40bcda0..43d32b9 100644
--- a/tools/hospital_sim/hospital-unity-params.json
+++ b/tools/hospital_sim/hospital-unity-params.json
@@ -65,9 +65,9 @@
    "混んでいる": 60
   },
   "mix": {
-   "歩く患者・付き添い": 0.7,
-   "杖の人": 0.1,
-   "車いす": 0.1,
+   "歩く患者・付き添い": 0.86,
+   "杖の人": 0.03,
+   "車いす": 0.01,
    "スタッフ": 0.1
   },
   "stretcher_per_h": 2
@@ -2688,7 +2688,7 @@
  "basis": {
   "speeds": "決まり",
   "crowd_people": "仮置き",
-  "people_mix": "仮置き",
+  "people_mix": "公開統計からの推計",
   "stretcher_per_h": "仮置き"
  }
 }
\ No newline at end of file
```

### 残したことと、次に回すこと

- 前の回（LOG477）の Unity の数は、人の内訳を直す前のもの。新しい内訳（車いす1%・杖の人3%）で回し直し、問題の回数がどう変わるかを見る
- 回し直すときは、決めたカメラ（寄り 左30°・75°、引き 西半分 左30°・75°）で動画も撮る
- 依頼138 で Claude Code から聞かれた3つ（積む所で待っている間のひやりの数え方、16:30 より前に戻った回の終わり方、16:30 以降のストレッチャーと最後の便を待つ上限）は、まだ決めていない
- 来院者のうち患者の割合（2/3）は仮置き。外来の廊下で人を数えた値が手に入れば、内訳ごと置き換える
- 寄りの角度は、病院の廊下（両側が部屋）では75°がよかった。配膳のファミレスの60°は変えていない

### 出典

- [令和5年（2023）患者調査の概況（厚生労働省）](https://www.mhlw.go.jp/toukei/saikin/hw/kanja/23/dl/kanjya.pdf)
- [介護保険制度における福祉用具の資料 ― 福祉用具貸与の種目別の件数（厚生労働省）](https://www.mhlw.go.jp/content/12300000/001483102.pdf)
- [統計からみた我が国の高齢者（総務省統計局 2024年9月）](https://www.stat.go.jp/data/topics/pdf/topics142.pdf)
- [Use of walking aids is increasing as population ages（ScienceDaily 2015-05-11。Gell ら、NHATS の分析）](https://www.sciencedaily.com/releases/2015/05/150511154921.htm)
- [サービスロボット導入シミュレーション Log#40（LOG477）](https://pixelstomotors.com/record/hospital-transport-robot-avoid-outpatient-hall)
