diff --git a/CLAUDE.md b/CLAUDE.md index de92b4a..8e5009d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ ## 開発進捗 -最新: フェーズ 7(実装)— Step 8 基準設計キャリブレーションの **Step 8-14(物理モデル精緻化)** 進行中.**面光源スケール軸バグを発見・修正**(`lights.py` が LED の高さを潰れた Z 軸に入れ縦 Y を無補正→全 LED が幅3mm×高さ2mの巨大ストリップ化).実機単灯実測との突合で **下灯 2.1× がこのバグのアーティファクト**と判明,修正後は 3 灯均等・下灯最暗で実機一致.ただし合成 CoV は 0.1815→0.1641 の小改善にとどまり,**残る不一致は各単灯フットプリントの形(拡散不足)**(シミュ peak/mean≈1.7 vs 実機≈1.2)=**形の一致を最優先**に方針転換([TEST_08](docs/06_TEST/TEST_08_面光源スケール軸バグの発見と修正.md)).**実機にディフューザー無しを確認**(むき出し LED+白筐体),実機の平らさは筐体多重反射由来.bottom 単灯 spp=1024 で max_depth 分解(実験1)と反射率スイープ(実験2)を実施→**いずれも形を平らにできず**(実機 1.22 に対し d16 で 1.65・R0.98 で 1.44 止まり,NCC 改善せず)=**混ぜ具合は本筋でなく,形の残差は B1 筐体幾何(箱の閉じ漏れ)に絞り込み**.次は幾何の漏れ点検(実験3)([TEST_09](docs/06_TEST/TEST_09_照明分布の形不一致の原因切り分け.md)).A1 非 Lambertian / A8 反射率 / A9 max_depth はクリア(数値改善のみ or 逆効果,ただし旧結論はバグ込みのため要再検証).詳細な経緯・設計判断・失敗パターンは [docs/PROGRESS.md](docs/PROGRESS.md). +最新: フェーズ 7(実装)— Step 8 基準設計キャリブレーションの **Step 8-14(物理モデル精緻化)** 進行中.**面光源スケール軸バグを発見・修正**(`lights.py` が LED の高さを潰れた Z 軸に入れ縦 Y を無補正→全 LED が幅3mm×高さ2mの巨大ストリップ化).実機単灯実測との突合で **下灯 2.1× がこのバグのアーティファクト**と判明(バグは Mitsuba `bbox()` で 2.8×2000mm と独立検証済,3D 確認ツール `scripts/show_led_geometry.py` 追加),修正後は 3 灯均等・下灯最暗で実機一致.ただし合成 CoV は 0.1815→0.1641 の小改善にとどまり,**残る不一致は各単灯フットプリントの形(拡散不足)**(シミュ peak/mean≈1.7 vs 実機≈1.2)=**形の一致を最優先**に方針転換([TEST_08](docs/06_TEST/TEST_08_面光源スケール軸バグの発見と修正.md)).**実機にディフューザー無しを確認**(むき出し LED+白筐体),実機の平らさは筐体多重反射由来.bottom 単灯 spp=1024 で max_depth 分解(実験1)と反射率スイープ(実験2)を実施→**いずれも形を平らにできず**(実機 1.22 に対し d16 で 1.65・R0.98 で 1.44 止まり,NCC 改善せず)=**混ぜ具合は本筋でなく,形の残差は B1 筐体幾何(箱の閉じ漏れ)に絞り込み**.さらに**修正後シミュは 3 灯ほぼ同形(NCC≈0.92)で実機の相補性(各灯が別方向を照らす)を再現できず**=形が筐体で決まり LED 位置の効きが弱い(板が間接光のみ+Lambertian 拡散壁のため光源形状が洗い流される).次は幾何の漏れ点検(実験3)([TEST_09](docs/06_TEST/TEST_09_照明分布の形不一致の原因切り分け.md)).A1 非 Lambertian / A8 反射率 / A9 max_depth はクリア(数値改善のみ or 逆効果,ただし旧結論はバグ込みのため要再検証).詳細な経緯・設計判断・失敗パターンは [docs/PROGRESS.md](docs/PROGRESS.md). 実装済みモジュール: - `src/sm_tias_lightsim/uniformity.py` — Rec.709 輝度変換 + CoV/std/max-min(MiniTias-Evaluation コピー) diff --git "a/docs/06_TEST/TEST_08_\351\235\242\345\205\211\346\272\220\343\202\271\343\202\261\343\203\274\343\203\253\350\273\270\343\203\220\343\202\260\343\201\256\347\231\272\350\246\213\343\201\250\344\277\256\346\255\243.md" "b/docs/06_TEST/TEST_08_\351\235\242\345\205\211\346\272\220\343\202\271\343\202\261\343\203\274\343\203\253\350\273\270\343\203\220\343\202\260\343\201\256\347\231\272\350\246\213\343\201\250\344\277\256\346\255\243.md" index 248374e..b2d1888 100644 --- "a/docs/06_TEST/TEST_08_\351\235\242\345\205\211\346\272\220\343\202\271\343\202\261\343\203\274\343\203\253\350\273\270\343\203\220\343\202\260\343\201\256\347\231\272\350\246\213\343\201\250\344\277\256\346\255\243.md" +++ "b/docs/06_TEST/TEST_08_\351\235\242\345\205\211\346\272\220\343\202\271\343\202\261\343\203\274\343\203\253\350\273\270\343\203\220\343\202\260\343\201\256\347\231\272\350\246\213\343\201\250\344\277\256\346\255\243.md" @@ -125,6 +125,76 @@ → **合成 CoV は 0.1815→0.1641(残差 2.63×→2.36×)と小改善にとどまる**.center/周辺比・勾配はほぼ不変.灯間バランスは直ったが,合成の主な不均一(中央明・縦勾配)は**灯間バランスではなく各単灯フットプリントの拡散不足**に由来するため,本バグ修正だけでは合成 CoV は大きく動かない(ユーザーの事前直感どおり). +## バグ実在の独立検証(報告用の一次証拠) + +「全 LED が高さ 2m のストリップ化していた」という主張を,可視化マーカーの再現に頼らず**修正前の実コードと Mitsuba 自身の測定**で独立に裏付けた.3 重の証拠: + +### 証拠1 — 修正前の実コード(git で確定) + +修正コミット `84b0ee5` の diff より,修正前の実行は文字どおり以下だった: + +```python +# src/sm_tias_lightsim/scene/lights.py _build_area_light_transform(修正前) +scale = mi.ScalarTransform4f().scale([sw / 2.0, 1.0, sh / 2.0]) +# 横X 縦Y 奥Z +# 縦 Y を 1.0 のまま(無補正)=基準矩形 ±1(単位 m)のまま,高さ sh を潰れた Z に誤って入れていた +``` + +### 証拠2 — Mitsuba 自身が報告する矩形サイズ + +修正前の式をそのまま使って実 LED(`led_bottom_0`, center=(-10,-71,-66.1)mm, size=2.8×3.5mm, dir=(0,0,-1))の `rectangle` を生成し,**Mitsuba の `shape.bbox()` に世界サイズを報告させた**結果: + +| | X 幅 | **Y 高さ** | Z | 面積 | +| --- | --- | --- | --- | --- | +| **修正前 `scale([sw/2, 1.0, sh/2])`** | 2.80 mm | **2000.00 mm** | 0 | 5600 mm² | +| 修正後 `scale([sw/2, sh/2, 1.0])` | 2.80 mm | 3.50 mm | 0 | 9.8 mm²(実物一致)| + +修正前の bbox は **Y[-1.071m, +0.929m]**=LED 中心 y=-71mm を中心に **±1000mm,高さちょうど 2000mm**.実物 9.8mm² に対し **約 570 倍の面積(5600mm²)の 2m ストリップ**であったことが,Mitsuba 自身の測定で確定. + +### 証拠3 — この変換が実レンダで使われていた経路 + +可視化専用ではなく,本線のレンダリングで使われていたことを確認: + +``` +builder.py:139 build_light_dict() → lights.py:98 _build_area_light_transform()(バグの scale) + → scene_dict → mi.render() +``` + +→ **結論: 「にわかに信じがたいが本当にそうなっていた」が正しい.** 全 18 LED が幅 2.8mm × 高さ 2000mm の縦ストリップとしてレンダされていた.検証スクリプトは一時的に実行(`mi.ScalarTransform4f` で修正前後の式を適用し `rectangle.bbox()` を比較). + +## 修正前後のカラーマップ比較 — 「バグが壊したのは形ではなく灯間バランス」 + +修正前(バグ)と修正後のレンダ結果(輝度マップ)を,単灯3+合成で並べて比較した.**正規化(形)と共通スケール(灯間明るさ)の 2 視点**で見ると,バグの主効果が明確になる. + +### 視点 A — mean 正規化(=「形」の比較) + +各マップを自身の mean で割って形だけを比べる(図 `output/_slide_panels/before_after_shape.png`): + +| 灯 | 修正前 peak/mean | 修正後 peak/mean | +| --- | --- | --- | +| bottom | 1.56 | 1.71 | +| left | 1.74 | 1.62 | +| right | 1.69 | 1.68 | +| **combined** | **1.54** | **1.54** | + +→ **正規化した「形」は修正前後でほとんど変わらない**(合成は 1.54→1.54 で不変).バグの主効果は形ではなかった. + +### 視点 B — 共通スケール(=「灯間の明るさ」の比較) + +3 単灯を「left LED の mean = 1.0」基準の共通スケールで並べる(図 `output/_slide_panels/before_after_amplitude.png`): + +| 灯 | 修正前(left=1.0) | 修正後(left=1.0) | +| --- | --- | --- | +| bottom | **2.10** | **0.97** | +| left | 1.00 | 1.00 | +| right | 1.00 | 1.00 | + +→ **バグが実際に壊していたのはこれ**.修正前は下灯だけ 2.10× 突出,修正後は 3 灯均等(下灯やや最暗で実機順序一致). + +### 結論 + +**バグ修正で直ったのは「下灯が 2 倍明るい」という灯間バランスであり,各灯の分布の“形”自体は元々あまり変わっていない**(だから合成 CoV も 0.1815→0.1641 の小改善にとどまった).「カラーマップの形状」という観点では正規化すると修正前後で大差なく,違いは**明るさの偏り**に出ていた.形の残差は別問題([TEST_09](TEST_09_照明分布の形不一致の原因切り分け.md) で B1 筐体幾何に絞り込み中). + ## 学びと残課題 1. **灯間の量的バランス問題は解決**:下灯 2.1× は変換バグのアーティファクトであり,修正で実機どおり(3 灯均等・下灯最暗)になった. @@ -139,3 +209,4 @@ - [TEST_02 シミュレーション実行記録](TEST_02_シミュレーション実行記録.md) — `output/led_group_sanity{,_fix}/` の索引 - 修正箇所: `src/sm_tias_lightsim/scene/lights.py` `_build_area_light_transform` - 実機入力: `lightsim/`(単灯実測.出典 SmTIAS-Evaluation TEST_04) +- LED 幾何の 3D 目視確認ツール: `scripts/show_led_geometry.py`(LED を実サイズ長方形+発光方向矢印で表示.`--buggy` で修正前の 2m ストリップ形を再現,`--screenshot` で 4 アングル保存).修正前後の比較図 `output/_slide_panels/led_geom{,_buggy}_{iso,front_XY,side_XZ,top_YZ}.png` diff --git "a/docs/06_TEST/TEST_09_\347\205\247\346\230\216\345\210\206\345\270\203\343\201\256\345\275\242\344\270\215\344\270\200\350\207\264\343\201\256\345\216\237\345\233\240\345\210\207\343\202\212\345\210\206\343\201\221.md" "b/docs/06_TEST/TEST_09_\347\205\247\346\230\216\345\210\206\345\270\203\343\201\256\345\275\242\344\270\215\344\270\200\350\207\264\343\201\256\345\216\237\345\233\240\345\210\207\343\202\212\345\210\206\343\201\221.md" index c6b1181..fac341d 100644 --- "a/docs/06_TEST/TEST_09_\347\205\247\346\230\216\345\210\206\345\270\203\343\201\256\345\275\242\344\270\215\344\270\200\350\207\264\343\201\256\345\216\237\345\233\240\345\210\207\343\202\212\345\210\206\343\201\221.md" +++ "b/docs/06_TEST/TEST_09_\347\205\247\346\230\216\345\210\206\345\270\203\343\201\256\345\275\242\344\270\215\344\270\200\350\207\264\343\201\256\345\216\237\345\233\240\345\210\207\343\202\212\345\210\206\343\201\221.md" @@ -113,6 +113,30 @@ 実機マップは **LSC 補正済み**(`dng_luminance(apply_lsc=True)`).LSC はレンズ周辺減光を割り戻す処理で**本来は照明分布を平らにしない**はずだが,「実機の平らさが本物の照明分布か,処理由来か」を実験3 前に一度確認しておくと安全([TECH_07 LSC 取り扱い方針](../05_TECH/TECH_07_LSC取り扱い方針.md) 参照). +## 追加観察 — 修正後の 3 単灯は「形」がほぼ同一(実機の相補性を再現できていない) + +なぜ光源の形(点 vs 2m 帯)を変えても板の形がほとんど変わらないのか([TEST_08 §修正前後のカラーマップ比較](TEST_08_面光源スケール軸バグの発見と修正.md))の物理的根拠は,**板が「間接光のみ(LED は板に背を向け,depth=1,2 が真っ暗)+ Lambertian 拡散壁」で照らされ,形が光源でなく筐体で決まる**ため.拡散壁での多重反射が光源の形状情報を洗い流す(積分球の原理;壁が鏡面なら光源形状が板に焼き付くが Lambertian なので消える). + +この帰結として,**修正後シミュでは下・左・右の 3 単灯の「形」が互いにほぼ同一**になる(同じ筐体に光を注ぐため): + +| 灯 | peak/mean | top / bot | imgleft / imgright | +| --- | --- | --- | --- | +| bottom | 1.71 | 0.852 / 1.064 | 0.961 / 0.955 | +| left | 1.62 | 0.848 / 1.067 | 0.951 / 0.964 | +| right | 1.68 | 0.857 / 1.059 | 0.960 / 0.955 | + +縦勾配(top/bot ≈ 0.85/1.06)が 3 灯ほぼ同一.3 単灯どうしの形の一致度: + +| ペア | NCC | SSIM | +| --- | --- | --- | +| bottom vs left | 0.924 | 0.401 | +| bottom vs right | 0.927 | 0.406 | +| left vs right | 0.913 | 0.360 | + +→ **NCC ≈ 0.92 で 3 灯は互いにかなり似た形**.違いは「自分の側がほんの少し明るい」弱い横バイアスのみ. + +**重要な含意**: [TEST_04(実機単灯比較)](TEST_07_LEDグループ別動作確認.md) では実機の 3 灯は**相補的に異なる方向を照らす**(下=上側/左=右寄り…)とされていた.しかし**修正後シミュは 3 灯ほぼ同じ形=実機の「相補性」を再現できていない**.これは「シミュでは形が筐体で決まり LED 位置の効きが弱すぎる」ことを意味し,(a) 筐体幾何 B1 の不正確さ,または (b) シミュが間接化しすぎ(実機は各灯の直接性/指向性がもっと残る)を示唆する.実験3(幾何)の追加手がかり. + ## 関連ドキュメント - [TEST_08 面光源スケール軸バグの発見と修正](TEST_08_面光源スケール軸バグの発見と修正.md) — 本調査の前提(バグ修正で灯間バランスは解決済み) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 0e5f7f8..01b1eb1 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -65,3 +65,6 @@ - **実験1(bottom 単灯 spp=1024 max_depth バウンス分解 {1,2,4,8,16,32})**: depth=1,2 が**真っ暗(mean=0)**=LED は板に背を向け **板は完全に間接光のみ**(3〜4 反射でやっと到達).バウンス増で多少平らになるが **d16 で頭打ち**(d16≈d32),到達点 peak/mean **1.65** が実機 **1.22** に届かず・NCC 0.67 頭打ち.**混ぜ不足は原因でない**.完全閉箱なら無限反射で均一化するはずが尖り+縦勾配が残る=**光が混ざりきる前に逃げている**疑い.出力 `output/depth_bottom/d{N}/`,図 `output/_slide_panels/depth_bottom_shape.png`. - **実験2(bottom 単灯 spp=1024 d=16 反射率スイープ R={0.85,0.90,0.95,0.98})**: R↑で少し平ら(peak/mean 1.65→1.44,縦勾配 top/bot 0.867→0.894 で実機方向)だが **R=0.98(非現実的高反射)でも 1.44 止まりで実機 1.22 に届かず,NCC は改善せず微減**(0.670→0.640).**反射率も本筋でない**.出力 `output/refl_bottom/run_r{90,95,98}/`,図 `output/_slide_panels/refl_bottom_shape.png`. - **総括**: 「筐体が光をかき混ぜる強さ」を司る 2 レバー(反射回数・反射率)を両方上限まで振っても形は実機の平らさに構造的に届かない → **形の残差の本筋は B1 筐体幾何(光の入り方・箱の閉じ漏れ)に絞り込み**.次は実験3(漏れの定量化/開口を白壁で塞ぐ閉じテスト).補足: 実機マップは LSC 補正済みのため,実験3 前に「実機の平らさが本物の照明分布か処理由来か」を一度確認すると安全. +- **バグの独立検証・修正前後比較・LED 幾何 3D 確認ツールを整備 (2026-06-29)**: 上司報告向けに「2m ストリップ化」を可視化マーカーに頼らず裏付け.[TEST_08](06_TEST/TEST_08_面光源スケール軸バグの発見と修正.md) に 3 重の一次証拠を追記: ① git で修正前の実コード行を確定,② 修正前の式で `rectangle` を生成し **Mitsuba の `bbox()` が X=2.80mm / Y=2000.00mm(面積 5600mm²=実物の約 570 倍)と報告**,③ `builder→_build_area_light_transform→mi.render` の呼び出し経路で実レンダ使用を確認.新規ツール `scripts/show_led_geometry.py`(LED を実サイズ長方形+発光方向矢印で 3D 表示,`--buggy` で修正前の 2m ストリップ再現,`--screenshot` で 4 アングル保存)を追加.これで **LED 位置・サイズ・向きは実機どおり(評価板を背にする設計で正しい)=形不一致の容疑者から無罪**を 3D で確認. + - **修正前後カラーマップ比較**(TEST_08 に追記,図 `output/_slide_panels/before_after_{shape,amplitude}.png`): mean 正規化した「形」は修正前後でほぼ不変(合成 peak/mean 1.54→1.54).**バグが壊していたのは形でなく灯間の明るさ**(下灯 left基準で 2.10→0.97 へ均等化).だから合成 CoV も小改善にとどまった. + - **修正後 3 単灯は形がほぼ同一**(TEST_09 に追記): 板が間接光のみ+Lambertian 拡散壁で照らされるため形が光源でなく筐体で決まる(積分球原理=多重反射が光源形状を洗い流す).結果,下・左・右の単灯フットプリントが互いに **NCC≈0.92** とほぼ同形(縦勾配 top/bot≈0.85/1.06 が 3 灯共通).**実機(TEST_04)の「3 灯が相補的に別方向を照らす」を再現できていない**=シミュでは LED 位置の効きが弱すぎる→ (a) B1 幾何不正確 or (b) 間接化しすぎ(実機は各灯の指向性がもっと残る)を示唆.実験3 の追加手がかり. diff --git a/scripts/show_led_geometry.py b/scripts/show_led_geometry.py new file mode 100644 index 0000000..f384be3 --- /dev/null +++ b/scripts/show_led_geometry.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +"""LED 配置・サイズ・向きを 3D で確認するビューア. + +筐体を半透明にして,各 LED を実サイズ(light.size)の長方形+発光方向の矢印で +グループ色(下=赤 / 左=緑 / 右=青)に描く.評価板(シアン)・センサ(黒+視線矢印)・ +XYZ 向き軸も表示する.render の変換とは独立に config の center/size/direction を +そのまま描くので「config が意図している LED 幾何」の目視検証に使える. + +使い方: + # インタラクティブ表示(マウスで回転・ズーム) + python scripts/show_led_geometry.py + python scripts/show_led_geometry.py --scene configs/scene_baseline.yaml + + # スクリーンショットを 4 アングル保存(画面を開かない) + python scripts/show_led_geometry.py --screenshot output/_slide_panels/led_geom +""" +from __future__ import annotations + +import argparse +import os + +import numpy as np +import pyvista as pv + +import sm_tias_lightsim.geometry.loader as G +import sm_tias_lightsim.scene.schema as S + +_GROUP_COLORS = {"bottom": "red", "left": "green", "right": "blue"} +_ARROW_LEN_MM = 12.0 + +# バグ修正前の縦ストリップ高さ(mm).バグ時 scale の Y が 1.0(m)=±1m のままだったため +# world Y 方向に ±1000mm = 高さ 2000mm に伸びていた(幅は light.size[0] のまま). +_BUGGY_STRIP_HALF_Y_MM = 1000.0 + + +def _led_polydata(light, *, buggy: bool) -> pv.PolyData: + """LED の発光面 PolyData を返す. + + fixed: light.size どおりの実サイズ長方形(修正後). + buggy: 修正前の変換が実際に作っていた形=幅 light.size[0] × 高さ 2000mm の縦ストリップ + (全 LED が direction=(0,0,-1)/up=(0,1,0) のため local X→world X・local Y→world Y で + Y 方向に伸びる).忠実に 4 隅から構成する. + """ + center = np.array(light.center, dtype=float) + direction = np.array(light.direction, dtype=float) + direction = direction / np.linalg.norm(direction) + if not buggy: + return pv.Plane( + center=center.tolist(), + direction=direction.tolist(), + i_size=light.size[0], + j_size=light.size[1], + ) + half_x = light.size[0] / 2.0 + x_hat = np.array([1.0, 0.0, 0.0]) + y_hat = np.array([0.0, 1.0, 0.0]) + corners = np.array([ + center - half_x * x_hat - _BUGGY_STRIP_HALF_Y_MM * y_hat, + center + half_x * x_hat - _BUGGY_STRIP_HALF_Y_MM * y_hat, + center + half_x * x_hat + _BUGGY_STRIP_HALF_Y_MM * y_hat, + center - half_x * x_hat + _BUGGY_STRIP_HALF_Y_MM * y_hat, + ]) + return pv.PolyData(corners, np.array([4, 0, 1, 2, 3])) + + +def _resolve_mesh_path(path: str) -> str: + """scene YAML のメッシュパスを解決する(絶対 → そのまま,無ければ configs/meshes/basename).""" + if os.path.isfile(path): + return path + base = os.path.basename(path.replace(chr(92), "/")) + return os.path.join("configs", "meshes", base) + + +def _build_plotter(scene_config: S.SceneConfig, *, off_screen: bool, buggy: bool = False) -> pv.Plotter: + pv.global_theme.allow_empty_mesh = True + plotter = pv.Plotter(off_screen=off_screen, window_size=(1400, 1050)) + plotter.set_background("white") + + # 筐体(半透明)・評価板(シアン)・暗い phone/holder + for mesh_config in scene_config.geometry.meshes: + loaded = G.load_stl(_resolve_mesh_path(mesh_config.path), "mm") + vertices = np.array(loaded.vertices) + faces_arr = np.array(loaded.faces) + faces = np.hstack([np.full((faces_arr.shape[0], 1), 3), faces_arr]).ravel() + poly = pv.PolyData(vertices, faces) + if mesh_config.id == "evaluation_board": + plotter.add_mesh(poly, color="cyan", opacity=0.35) + elif "phone" in mesh_config.id or "holder" in mesh_config.id: + plotter.add_mesh(poly, color="dimgray", opacity=0.12) + else: + plotter.add_mesh(poly, color="lightgray", opacity=0.10) + + # LED(実サイズ長方形+発光方向矢印) + for light in scene_config.lights: + group = next((g for g in _GROUP_COLORS if g in light.id), "other") + color = _GROUP_COLORS.get(group, "orange") + center = np.array(light.center, dtype=float) + direction = np.array(light.direction, dtype=float) + direction = direction / np.linalg.norm(direction) + plotter.add_mesh(_led_polydata(light, buggy=buggy), color=color, opacity=0.6 if buggy else 1.0) + plotter.add_mesh( + pv.Arrow(start=center.tolist(), direction=direction.tolist(), scale=_ARROW_LEN_MM), + color=color, + ) + + # センサ(球+視線矢印) + sensor_pos = np.array(scene_config.sensor.position, dtype=float) + sensor_target = np.array(scene_config.sensor.target, dtype=float) + plotter.add_mesh(pv.Sphere(radius=_ARROW_LEN_MM * 0.4, center=sensor_pos.tolist()), color="black") + view_dir = sensor_target - sensor_pos + view_dir = view_dir / np.linalg.norm(view_dir) + plotter.add_mesh( + pv.Arrow(start=sensor_pos.tolist(), direction=view_dir.tolist(), scale=_ARROW_LEN_MM * 2.5), + color="black", + ) + + plotter.add_legend( + [ + ["bottom LED", "red"], + ["left LED", "green"], + ["right LED", "blue"], + ["eval board", "cyan"], + ["sensor->view", "black"], + ], + bcolor="white", + size=(0.20, 0.20), + ) + plotter.show_axes() + plotter.show_bounds(color="gray", location="outer", xtitle="X(mm)", ytitle="Y(mm)", ztitle="Z(mm)") + return plotter + + +def main() -> None: + parser = argparse.ArgumentParser(description="LED 配置・サイズ・向きを 3D で確認する") + parser.add_argument("--scene", "-s", default="configs/scene_baseline.yaml", help="シーン定義 YAML") + parser.add_argument( + "--screenshot", + default=None, + help="指定時は画面を開かず {prefix}_{iso,front_XY,side_XZ,top_YZ}.png を保存する", + ) + parser.add_argument( + "--buggy", + action="store_true", + help="バグ修正前の形(幅 light.size[0] × 高さ 2000mm の縦ストリップ)を描く", + ) + args = parser.parse_args() + + scene_config = S.load_scene_yaml(args.scene) + mode = "BUGGY (2m strip)" if args.buggy else "FIXED" + print(f"[{mode}] LED 数: {len(scene_config.lights)} size(mm): {scene_config.lights[0].size} " + f"direction: {scene_config.lights[0].direction}") + + if args.screenshot: + plotter = _build_plotter(scene_config, off_screen=True, buggy=args.buggy) + views = [ + ("iso", plotter.view_isometric), + ("front_XY", plotter.view_xy), + ("side_XZ", plotter.view_xz), + ("top_YZ", plotter.view_yz), + ] + for name, view_fn in views: + view_fn() + out_path = f"{args.screenshot}_{name}.png" + plotter.screenshot(out_path) + print("saved", out_path) + plotter.close() + else: + plotter = _build_plotter(scene_config, off_screen=False, buggy=args.buggy) + plotter.view_isometric() + plotter.show() + + +if __name__ == "__main__": + main()