diff --git a/.gitignore b/.gitignore index ad261b9..8f69504 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # シミュ・評価結果(コミット対象外) output/ +# 実機実測データ(SmTIAS-Evaluation からの引き渡し入力.コミット対象外) +lightsim/ + # docs 下の生成画像(再生成可能なので追跡しない.生成スクリプトは追跡する) docs/**/figures/*.png diff --git a/CLAUDE.md b/CLAUDE.md index 6c3ed4f..8e5009d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ ## 開発進捗 -最新: フェーズ 7(実装)— Step 8 基準設計キャリブレーションの **Step 8-14(物理モデル精緻化)** 進行中.A1 非 Lambertian / A8 反射率 / A9 max_depth はクリア(数値改善のみ or 逆効果),LED 配光(ξ)が形改善の本物候補と判明,**形改善の本筋は B1 筐体幾何精度に確定**(実機 CoV=0.0695 に対しシミュ CoV=0.1826,残差 2.63×).詳細な経緯・設計判断・失敗パターンは [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/05_TECH/TECH_10_\346\256\213\345\267\256\344\270\273\345\233\240\345\200\231\350\243\234\343\201\256\347\266\262\347\276\205\346\225\264\347\220\206.md" "b/docs/05_TECH/TECH_10_\346\256\213\345\267\256\344\270\273\345\233\240\345\200\231\350\243\234\343\201\256\347\266\262\347\276\205\346\225\264\347\220\206.md" index e164ef3..45d59a2 100644 --- "a/docs/05_TECH/TECH_10_\346\256\213\345\267\256\344\270\273\345\233\240\345\200\231\350\243\234\343\201\256\347\266\262\347\276\205\346\225\264\347\220\206.md" +++ "b/docs/05_TECH/TECH_10_\346\256\213\345\267\256\344\270\273\345\233\240\345\200\231\350\243\234\343\201\256\347\266\262\347\276\205\346\225\264\347\220\206.md" @@ -1,5 +1,7 @@ # 残差 2.63× の主因候補の網羅整理 (Residual Cause Candidates Enumeration) +> ⚠️ **BEFORE FIX(バグ込み・要再検証)**: 本ドキュメントの優先度評価(A1/A8/A9/ξ 等)と参照図は,すべて面光源スケール軸バグ(全 LED が幅3mm×高さ2mの巨大ストリップ化)が入った状態の結果に基づく.バグ修正後の再検証([TEST_08](../06_TEST/TEST_08_面光源スケール軸バグの発見と修正.md) / [TEST_09](../06_TEST/TEST_09_照明分布の形不一致の原因切り分け.md))では,A8 反射率・A9 max_depth・ξ 配光はいずれも単灯の形を実機に近づけられず,残差の本筋は **B1 筐体幾何** に絞り込まれた.本ドキュメントの候補評価は**修正後に再評価が必要**. + 本ドキュメントは [TEST_01 Step 8-13](../06_TEST/TEST_01_キャリブレーション検証履歴.md) で確定した **シミュ vs 実機の残差 2.63×** の主因候補を網羅的に整理したものである.Step 8-14 以降の物理モデル精緻化で「何を試すか」の判断材料として作成した. ## 結論(先に) (Conclusion First) diff --git "a/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" "b/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" index 3e878e6..abfad54 100644 --- "a/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" +++ "b/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" @@ -1,5 +1,7 @@ # キャリブレーション検証履歴 (Calibration Verification History) +> ⚠️ **BEFORE FIX(バグ込み・要再検証)**: 本ドキュメントの Step 8 系イテレーション(特に A1 BSDF・A8 反射率・A9 max_depth・ξ 配光スイープ)の**数値・図はすべて面光源スケール軸バグ(全 LED が幅3mm×高さ2mの巨大ストリップ化)が入った状態で取得**したもの.バグの発見・修正・影響は [TEST_08](TEST_08_面光源スケール軸バグの発見と修正.md),正しい面積での再検証は [TEST_09](TEST_09_照明分布の形不一致の原因切り分け.md) を参照.記録としては残すが,個々の数値・結論は**そのまま信用せず修正後に再検証すること**. + 本ドキュメントは MiniTIAS 実機(baseline 1 枚 `MiniTIAS_20260408_144434`)に対するシミュレーション結果の **キャリブレーション** と **検証イテレーション** を時系列で記録する. 目的: diff --git "a/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" "b/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" index 756a725..a35c8ba 100644 --- "a/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" +++ "b/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" @@ -1,5 +1,7 @@ # シミュレーション実行記録 (Simulation Run Catalog) +> ⚠️ **BEFORE FIX 注記**: 本索引のうち面光源スケール軸バグ修正([TEST_08](TEST_08_面光源スケール軸バグの発見と修正.md))**以前**に生成された出力(`baseline` / `baseline_v3_leds` / `led_group_sanity` / `max_depth_sweep` / `reflectance_sweep` / `sweep_reflectance` / `blend_sweep` / `led_distribution_sweep` と対応する `_slide_panels` 図)は,**全 LED が 2m ストリップ化したバグ込み**の結果.実験記録として保持するが数値はそのまま信用しないこと.修正後の出力は `sanity_fix_1024` / `baseline_fix_1024` / `depth_bottom` / `refl_bottom` / `distrib_bottom`,図は `*_roi.png`(修正前を明記復元した版は `*_buggy.png`). + 本ドキュメントは `output/` 配下に蓄積されたシミュレーション・評価結果ディレクトリを **「何の試行か」が分かる形に索引化** したものである.`output/` は `.gitignore` で追跡対象外(容量・再生成可能性のため)であり,**手元の `output/` が無くてもこのドキュメントだけ読めば「過去にどんな試行があったか・どう再現するか」が辿れる**ことを目的とする. 関連: diff --git "a/docs/06_TEST/TEST_07_LED\343\202\260\343\203\253\343\203\274\343\203\227\345\210\245\345\213\225\344\275\234\347\242\272\350\252\215.md" "b/docs/06_TEST/TEST_07_LED\343\202\260\343\203\253\343\203\274\343\203\227\345\210\245\345\213\225\344\275\234\347\242\272\350\252\215.md" index 5ca1db7..e6db296 100644 --- "a/docs/06_TEST/TEST_07_LED\343\202\260\343\203\253\343\203\274\343\203\227\345\210\245\345\213\225\344\275\234\347\242\272\350\252\215.md" +++ "b/docs/06_TEST/TEST_07_LED\343\202\260\343\203\253\343\203\274\343\203\227\345\210\245\345\213\225\344\275\234\347\242\272\350\252\215.md" @@ -1,5 +1,7 @@ # LED グループ別動作確認 (LED Group Sanity Check) +> ⚠️ **訂正(重要)**: 本ドキュメントの中心的結論「**下灯 2.1× は直接光効率の幾何的優位で物理的に正しい/意図した設計**」は**誤り**である.その後の実機単灯データとの突き合わせで,`lights.py` の面光源スケールに**座標軸取り違えバグ**(高さを縦 Y でなく潰れた奥行き Z に入れ,LED が「幅 3 mm × 高さ 2 m」の巨大ストリップ化)が見つかり,**下灯 2.1× はこのバグのアーティファクト**だったと判明した.修正後は 3 灯がほぼ均等(実機どおり・下灯最暗)になる.本ドキュメントの数値・「形」の観察・物理的考察(特に §結果〜§bottom が 2.1× 強い理由)は**バグ込みの出力に基づく**ため,そのまま信用しないこと.詳細・修正・再検証は [TEST_08 面光源スケール軸バグの発見と修正](TEST_08_面光源スケール軸バグの発見と修正.md) を参照. + 本ドキュメントは **シミュレーションが下/右/左の 3 つの LED 基板を物理位置どおりに独立にモデル化できているか** を,各グループの LED だけを ON にした輝度マップの形と数値で検証した結果のレポートである.Step 8 系キャリブの主軸ではなく **シミュ動作の基礎健全性確認** として位置付ける. ## 目的・背景 @@ -26,11 +28,12 @@ スクリプト: `scripts/sanity_led_groups.py`(3 ジョブ順次実行)+ `scripts/build_led_group_sanity_panel.py`(6 列パネル + 線形性誤差)+ 拡大確認用の 1×3 / 1×2 パネル生成. -成果物: -- `output/led_group_sanity/{bottom,right,left}/luminance.npy` -- `output/_slide_panels/led_group_sanity.png` (6 列: ①〜③ 単独 / ④ 全 ON / ⑤ 和 / ⑥ 線形性誤差) -- `output/_slide_panels/led_group_sanity_focus.png` (1×3 拡大) -- `output/_slide_panels/led_group_sanity_right_left.png` (1×2 左右対称性確認) +成果物(**本ドキュメントの数値・図はすべて修正前=バグ込み**.バグ実在の経緯は [TEST_08](TEST_08_面光源スケール軸バグの発見と修正.md)): +- `output/led_group_sanity/{bottom,right,left}/luminance.npy`(修正前の生データ=実験記録として保持) +- `output/_slide_panels/led_group_sanity_buggy.png` (6 列: ①〜③ 単独 / ④ 全 ON / ⑤ 和 / ⑥ 線形性誤差.**BEFORE FIX 明記・ROI 揃え**) +- `output/_slide_panels/led_group_sanity_focus_buggy.png` (1×3 拡大,BEFORE FIX) +- `output/_slide_panels/led_group_sanity_right_left_buggy.png` (1×2 左右対称性確認,BEFORE FIX) +- 参考(**修正後**=同名サフィックス無し): `led_group_sanity{,_focus,_right_left}.png`(`sanity_fix_1024`/`baseline_fix_1024` 由来,3 灯均等・線形性 OK.TEST_08 の検証結果に対応) ## 結果 — 数値 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" new file mode 100644 index 0000000..735217f --- /dev/null +++ "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" @@ -0,0 +1,214 @@ +# 面光源スケール軸バグの発見と修正 (Area-Light Scale-Axis Bug) + +本ドキュメントは,**実機の単灯(下/左/右)実測データとシミュレーションを 1 灯ずつ突き合わせた結果,シミュ側の面光源(area emitter)変換に座標軸の取り違えバグが見つかり,これが「下灯がほかの 2 倍強い」という長年の不一致の主因だった**ことの発見・原因・修正・検証を記録する. + +> ⚠️ 本件により [TEST_07 LED グループ別動作確認](TEST_07_LEDグループ別動作確認.md) の結論「下灯 2.1× は物理的に正しい/意図どおり」は**誤り**であったことが判明した(TEST_07 はバグ込みの出力を物理的に正当化してしまっていた).TEST_07 冒頭にも訂正注記を追加済み. + +## 発端 — 実機単灯データとの突き合わせ + +実機(SmTIAS-Evaluation)で 3 灯を 1 灯ずつ単独点灯して撮影した白板 ROI 輝度マップ一式を入力として受領した(`lightsim/`:`luminance_maps/{bottom,left,right}_*.npy`,0〜255 linear / Rec.709 luma / LSC 補正済 / portrait). + +ユーザー観察:**「シミュでは下側がほかの約 2 倍になっているが,実機では 3 灯ほぼ均等」**.これを灯ごとに分解して原因を特定するのが目的. + +灯ごとの白板 ROI 平均輝度(実機)と,既存シミュ単灯出力(`output/led_group_sanity/`,spp=1024)を比較: + +| 灯 | 実機 mean | 実機 比 | シミュ(バグ) mean | シミュ(バグ) 比 | +| --- | --- | --- | --- | --- | +| bottom | 57.75 | **1.00(最暗)** | 98.82 | **2.10** | +| left | 61.46 | 1.06 | 47.08 | 1.00 | +| right | 63.55 | 1.10 | 47.10 | 1.00 | + +→ **実機は 3 灯ほぼ均等で下灯が最暗**.シミュは**下灯だけ 2.10×**で,しかも左右が 47.08 vs 47.10 と完全対称.傾向が逆転している. + +LED 定義(`configs/leds_extracted.yaml`)は全 18 灯が `direction:(0,0,-1)`・`radiance:2807.25` で完全同一.**LED の位置・向き・強度は Fusion 360 STL 由来で実機どおり**であり,radiance 差では説明できない.したがって原因は LED 以外(周囲幾何・変換処理)にあると絞り込んだ. + +## 原因 — 面光源スケールの軸取り違え + +### Mitsuba の rectangle 仕様(実機検証) + +Mitsuba 3 の `rectangle` 形状を実際に生成して確認した: + +``` +中心法線 n = (0, 0, 1) # 面は +Z を向く +面内範囲 = X∈[-1,1], Y∈[-1,1] # XY 平面に寝た 1 辺 2 の正方形 +Z 方向 = 厚みゼロ(潰れている) +``` + +→ **rectangle は「XY 平面・法線 +Z」**.サイズを与えるには **横 = X 軸,高さ = Y 軸**をスケールしなければならない(Z は潰れているのでスケールしても無意味). + +### バグ箇所 + +`src/sm_tias_lightsim/scene/lights.py` `_build_area_light_transform`: + +```python +# 修正前(バグ) +scale = mi.ScalarTransform4f().scale([sw / 2.0, 1.0, sh / 2.0]) +# 横X 縦Y 奥Z +# 横(X) = sw/2 … OK +# 縦(Y) = 1.0 … ★ スケールされず基準のまま = ±1 m(2000 mm) +# 奥(Z) = sh/2 … 高さを“潰れた厚み方向”に入れてしまい無効 +``` + +コード上のコメントは「rectangle は **XZ 平面**上の正方形(**Y 軸正方向が前面**)」と書かれていたが,実体は XY 平面・+Z 前面.**前提を 1 軸勘違いしたまま実装されており,高さ `sh` を縦(Y) ではなく奥行き(Z) に入れ,縦(Y) は無補正のまま放置**されていた. + +単位はメートル系(mm→m に position_scale=0.001 済)なので,縦が無補正 = **±1 m**.結果: + +> 実物 **2.8 × 3.5 mm** の小さな LED が,シミュ内では **「幅 ~3 mm × 高さ 2000 mm」の細長い巨大ストリップ**になっていた. + +法線(発光方向)は look_at で正しく `direction` を向くため,発光の向き自体は正しい(このバグでは形状サイズだけが壊れる). + +### なぜ「下灯だけ 2 倍」になったか + +全 18 灯が同一の look_at(direction=(0,0,-1), up=(0,1,0))を使うため local Y→world Y.つまり**全 LED が world Y 方向に 2 m 伸びた縦ストリップ**になる.各灯列の横位置(X)と評価板の関係: + +| 灯 | LED 横位置 X | 評価板 X 範囲 [-49.5, +49.5] との関係 | +| --- | --- | --- | +| **bottom** | X∈[-10, +18] | **板中央の真裏**(中央に密集) | +| left | X = -66 | 板の外(左端より 16.5 mm 外側)+側壁/カバー脇 | +| right | X = +66 | 板の外(右端より 16.5 mm 外側)+側壁/カバー脇 | + +2 m 縦長化により,**板中央の真裏にある下灯だけが板中央を効率よく照らし**,板の外側にある左右灯は伸びた光の多くが壁/カバーに当たって板に届きにくい.本来の小さな ~3.5 mm 面光源ならこの縦長効果は生じず偏らない.**バグによる巨大化が,たまたま中央裏にある下灯を過剰に効かせて 2.1× を生んでいた**. + +TEST_07 はこの 2.1× を「直接光効率の幾何的優位=意図した設計」と解釈してしまったが,実際は**変換バグによるアーティファクト**だった. + +## 修正 + +スケールの軸を 1 か所入れ替えるのみ: + +```python +# 修正後 +scale = mi.ScalarTransform4f().scale([sw / 2.0, sh / 2.0, 1.0]) +# 横X 縦Y 奥Z +# 横(X) = sw/2 縦(Y) = sh/2 奥(Z) = 1.0(潰れた厚みは無補正でよい) +``` + +高さ `sh` を,誤って入れていた奥行き(Z) から本来の縦(Y) へ移しただけ.これで LED が実物どおりの ~2.8 × 3.5 mm に戻る. + +## 検証結果(spp=1024, max_depth=16 確定) + +修正後に 3 単灯+全灯合成を再レンダ(単灯 `output/sanity_fix_1024/`,全灯 `output/baseline_fix_1024/`,いずれも **spp=1024 / max_depth=16**).暫定 spp=256 出力は `output/led_group_sanity_fix/`. + +### ① 灯間バランス(mean 比,spp=1024) + +| 灯 | バグ 比 | **修正後 比** | 実機 比 | +| --- | --- | --- | --- | +| bottom | 2.099 | **1.000(最暗)** | 1.000(最暗)| +| left | 1.000 | **1.028** | 1.064 | +| right | 1.000 | **1.026** | 1.100 | + +→ **下灯 2 倍は消滅し,3 灯が ±3% で均等化**.さらに**「下灯が最暗」という実機の順序まで一致**した.灯間の振幅バランスは実機パターンに乗った. + +### ② 各灯の形(peak/mean,spp=1024,ROI 揃え) + +| 灯 | バグ peak/mean | 修正後 peak/mean | 実機 peak/mean | +| --- | --- | --- | --- | +| bottom | 1.47 | 1.64 | 1.22 | +| left | 1.63 | 1.56 | 1.27 | +| right | 1.58 | 1.61 | 1.20 | + +- 絶対 mean は ~98/47 → ~4.5 へ低下:発光面積が 2.8mm×2000mm → 2.8mm×3.5mm へ約 570 倍縮小したため.絶対スケールは下流の露出ソルバ / mean anchor 正規化で吸収されるため灯間比・形が本質. +- **peak/mean はなお実機(1.2 台)に届かず(修正後 1.6 台)**=各単灯フットプリントの**拡散不足**が残る.形の残差は [TEST_09](TEST_09_照明分布の形不一致の原因切り分け.md) で B1 筐体幾何に絞り込み中. + +### ③ 線形性(単灯和 ≈ 全灯) + +単灯 3 本の和と全灯合成の ROI mean を比較:**相対誤差 0.34%**.Mitsuba の理論線形性どおりで,レンダの健全性を確認. + +### ④ 全灯合成 CoV(公式 sm-evaluate, ROI image_pixel 464×450) + +| シーン | linear CoV | 対実機残差 | center/周辺比 | gradient | +| --- | --- | --- | --- | --- | +| 修正前(`baseline_v3_leds`,バグ) | 0.1815 | 2.61× | 1.318 | 24.13 | +| **修正後(`baseline_fix_1024`)** | **0.1641** | **2.36×** | 1.311 | 23.71 | +| 実機 | 0.0695 | — | — | — | + +(参考:OETF/sRGB ドメインでは修正後 CoV=0.0790) + +→ **合成 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 視点**で見ると,バグの主効果が明確になる. + +> 以下の数値・図は **ROI 揃え**版(シミュを `SIM_ROI`=(24,37,464,450) で切り出し).旧版はフル 512² frame で算出していた([TEST_09 の ROI 注記](TEST_09_照明分布の形不一致の原因切り分け.md)参照)が,結論は不変. + +### 視点 A — mean 正規化(=「形」の比較) + +各マップを自身の mean で割って形だけを比べる(図 `output/_slide_panels/before_after_shape_roi.png`,ROI 揃え): + +| 灯 | 修正前 peak/mean | 修正後 peak/mean | +| --- | --- | --- | +| bottom | 1.47 | 1.64 | +| left | 1.63 | 1.56 | +| right | 1.58 | 1.61 | +| **combined** | **1.45** | **1.47** | + +→ **正規化した「形」は修正前後でほとんど変わらない**(合成は 1.45→1.47 でほぼ不変).バグの主効果は形ではなかった. + +### 視点 B — 共通スケール(=「灯間の明るさ」の比較) + +3 単灯を「left LED の mean = 1.0」基準の共通スケールで並べる(図 `output/_slide_panels/before_after_amplitude_roi.png`,ROI 揃え): + +| 灯 | 修正前(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 灯均等・下灯最暗)になった. +2. **論点が移動**:残る不一致は「灯間バランス」ではなく**各単灯フットプリントの拡散不足(peak/mean がシミュ 2.9 vs 実機 1.2)**.候補は LED 配光 ξ(指向の広がり)・ディフューザーのモデル化・面光源サイズの実測整合など. +3. **最終目標(3 灯合成 CoV)への効果は小さい(確定)**:spp=1024 で合成 CoV は 0.1815→0.1641(残差 2.63×→2.36×)の小改善にとどまった.残差の主因は灯間バランスではなく**各単灯フットプリントの拡散不足**であり,本修正だけでは合成 CoV は大きく動かない.次の本筋は配光 ξ/ディフューザー拡散のモデル化. +4. **過去の正当化に注意**:TEST_07 のように「シミュ出力を物理で後付け説明」すると,変換バグを設計意図と取り違える危険がある.実機グラウンドトゥルースとの灯ごと突き合わせが有効だった. + +## 関連ドキュメント + +- [TEST_07 LED グループ別動作確認](TEST_07_LEDグループ別動作確認.md) — バグ込み出力に基づく旧結論(本件で訂正) +- [TEST_01 キャリブレーション検証履歴](TEST_01_キャリブレーション検証履歴.md) — Step 8 系残差 2.63× の経緯 +- [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" new file mode 100644 index 0000000..1101fa0 --- /dev/null +++ "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" @@ -0,0 +1,174 @@ +# 照明分布の形不一致の原因切り分け (Shape Mismatch Root-Cause Investigation) + +本ドキュメントは,[TEST_08 面光源スケール軸バグ](TEST_08_面光源スケール軸バグの発見と修正.md) の修正後もなお残る**「照明分布の“形”が実機と一致しない」問題**について,原因を実験で切り分けていく継続記録である. + +> **最優先の判定基準**: 本調査は CoV/勾配などの**数値の絶対量ではなく,実機マップとの「形の一致」**を唯一の判定基準とする.形が一致しない限り「筐体を仮想的に変えて結果を予測する」というツール本来の目的(筐体改善の事前検討)が成立しないため.判定には実機マップを参照とした **NCC/SSIM + 視覚比較 + 放射プロファイル**を用い,peak/mean(中心の尖り具合)を補助指標とする. + +> **【重要・ROI 揃え更新】** 本ドキュメントの数値・図は **シミュを公式 ROI(`evaluation.roi_presets.SIM_ROI` = image_pixel(24,37,464,450))で切り出し,実機 ROI(`REAL_ROI`,実機 npy は適用済み)と物理領域を揃えて**算出した版に更新済み.初期検証はシミュをフル 512² フレームのまま測っており(板の縁・周辺暗部を含む),peak/mean を ~0.07 過大・NCC を ~0.05 過小に見せていた.**ROI 揃えで NCC は +0.05 程度改善するが,結論(混ぜ具合・配光は本筋でない/形は実機に届かない/B1 幾何が最有力)は不変**.ROI 版の図は `output/_slide_panels/*_roi.png`. + +## 出発点 — バグ修正後も「形」は不一致 + +TEST_08 の面光源バグ修正で**灯間バランス(下灯 2.1×)は解消**したが,各単灯・合成の**分布の形**は実機と一致しないまま: + +- **実機 単灯**: ほぼ一様(peak/mean ≈ 1.2,放射プロファイルは中心 1.05 → 周辺 0.9 でほぼ平坦) +- **シミュ 単灯**: 中心が尖り(peak/mean ≈ 1.7)+上暗下明の縦勾配,周辺の落ち込みが急 + +比較図: `output/_slide_panels/sim_vs_real_shape_roi.png` / `sim_vs_real_colormap_roi.png`(ROI 揃え) + +## 重要な前提確認 — ディフューザーは存在しない + +実機ハードの確認結果(ユーザー回答): + +- **LED カバー(`LED基板カバー*.stl`)は筐体と同じ不透明白材**(3D プリンタ白フィラメント,printer_white).光は透過しない. +- **LED とは別のディフューザー(すりガラス・乳白アクリル・拡散シート等)は一切挟んでいない**. +- → 実機は **むき出し LED + 白い筐体のみ**. + +含意:**実機単灯のあの「平らさ」を作っているのは光学部品ではなく,白い筐体内の多重反射(積分球的な拡散)**でしかありえない.逆に言えば,形を再現する鍵は新部品ではなく**筐体(反射+幾何)の正しいモデル化**にある. + +## 重要な注意 — 過去の「形は変わらない」結論はバグ込み + +[PROGRESS](../PROGRESS.md) に記録された「**A8 反射率・A9 max_depth・A1 BSDF を振っても形は変わらない**」という Step 8-14 の結論は,**すべて面光源バグ(2m 縦長ストリップ)が入った状態で得たもの**であり,TEST_07 と同様に**再検証が必要**.特に「筐体が光をかき混ぜて平らにする」効果は反射回数・反射率に依存するはずで,バグで巨大ストリップが支配していたため形が動かなかった可能性がある.本調査は**修正後の正しい LED で**やり直す. + +## 実験1 — bottom 単灯 max_depth バウンス分解(spp=1024) + +### 目的・条件 + +「形を平らにするレバーが現エミッタで存在するか」を確かめる第一歩.**bottom 単灯のみ・spp=1024 固定**で,**max_depth 以外をすべて一定**にし,反射回数を `{1, 2, 4, 8, 16, 32}` で振る.「バウンスを増やすほど実機の平らな形に近づくか/頭打ちか」を見る. + +- config: `configs/_tmp_depth_bottom/d{1,2,4,8,16,32}.yaml`(`_tmp_sanity_1024/scene_bottom.yaml` ベース,max_depth のみ変更) +- 出力: `output/depth_bottom/d{N}/luminance.npy` +- 比較図: `output/_slide_panels/depth_bottom_shape_roi.png`(ROI 揃え) + +### 結果 + +(数値は **ROI 揃え**版.図 `output/_slide_panels/depth_bottom_shape_roi.png`) + +| max_depth | mean(参考,フル) | peak/mean(ROI) | NCC(ROI,対実機) | +| --- | --- | --- | --- | +| 1 | **0.000(真っ暗)** | – | – | +| 2 | **0.000(真っ暗)** | – | – | +| 4 | 1.474 | 2.073 | 0.731 | +| 8 | 3.484 | 1.730 | 0.735 | +| 16 | 4.678 | **1.585** | 0.720 | +| 32 | 4.904 | **1.591** | 0.717 | +| **実機 bottom** | – | **1.218** | – | + +### 読み取れること + +1. **depth=1, 2 が完全に真っ暗(mean=0)** → LED は板に背を向けており(emitter 法線 -Z,板は +Z 側),**板は完全に間接光のみ**で照らされる(3〜4 回反射してようやく到達).TEST_08 で示した「LED は板と反対を向き間接光で照らす」を実証. +2. **バウンスを増やすと多少は平らになる**(peak/mean 2.07 → 1.59)が,**depth=16 で完全に頭打ち**(d16 と d32 がほぼ同一:1.585 vs 1.591,NCC 0.720 vs 0.717). +3. しかし**頭打ちの到達点(peak/mean 1.59)が実機(1.22)に全く届かない**.NCC も 0.72 で頭打ち(むしろ深さとともに微減).上暗下明の縦勾配も最後まで消えない. + +### 解釈 + +- **「混ざり不足(バウンス不足)」は原因ではない**.反射を理論上限(d32)まで積んでもシミュは尖ったまま. +- **完全に閉じた白い箱なら,無限反射で必ず均一(積分球)になる**はず.シミュが d32 でも尖り+縦勾配を残す=**光が箱内で混ざりきる前にどこかへ逃げている**=**シミュ筐体が実機ほど閉じていない(壁の欠け・隙間・開口)**疑いが濃厚. +- → 形の残差の本筋は **B1 筐体幾何**側にある可能性が高い,と実験で絞り込めた(反射回数では詰められない). + +## 実験2 — bottom 単灯 反射率スイープ(spp=1024, d=16) + +### 目的・条件 + +実験1で「混ぜ不足ではない」と分かったため,**「反射率が足りないだけ」なのか「幾何が漏れている」のか**を切り分ける.**bottom 単灯・spp=1024・max_depth=16 固定**(実験1で d16≈d32 と反射飽和を確認済み)で,筐体材 `printer_white` の反射率を `{0.85, 0.90, 0.95, 0.98}` に上げる.holder_dark(0.5)・phone_body_dark(0.05) は据え置き. + +- config: `configs/_tmp_refl_bottom/r{90,95,98}.yaml`(R=0.85 は実験1の `output/depth_bottom/d16` を再利用) +- 出力: `output/refl_bottom/run_r{90,95,98}/luminance.npy` +- 比較図: `output/_slide_panels/refl_bottom_shape_roi.png`(ROI 版) +- 判定: 閉じた箱なら R↑で一気に平ら(実機方向)→ 反射率の問題/漏れた箱なら R↑でも尖ったまま → 幾何(閉じ漏れ)が確定 + +### 結果(ROI 揃え) + +| R (printer_white) | peak/mean(ROI) | top/bot(ROI) | NCC(ROI,対実機) | +| --- | --- | --- | --- | +| 0.85 | 1.585 | 0.879 | 0.720 | +| 0.90 | 1.541 | 0.890 | 0.714 | +| 0.95 | 1.408 | 0.901 | 0.708 | +| 0.98 | 1.392 | 0.908 | 0.701 | +| **実機 bottom** | **1.218** | **0.931** | – | + +### 読み取れること + +1. **R を上げると確かに少し平らになる**(peak/mean 1.59→1.39,縦勾配 top/bot 0.879→0.908 で実機方向). +2. **しかし R=0.98(3D プリンタ白では非現実的に高い値)でも peak/mean 1.39 止まりで,実機 1.22 に届かない**. +3. **肝心の形の一致度(NCC)は改善しない**(0.720→0.701 でむしろ微減). + +### 解釈 + +- **反射率も「形の本筋」ではない**.実験1(バウンス回数)と実験2(反射率)で,**「筐体が光をかき混ぜる強さ」を司る 2 つのレバーを両方とも上限まで振っても,シミュは実機の平らさに構造的に届かない**ことが確定.NCC が頭打ち=形の不一致は混ぜ具合では埋まらない. +- 反射率を上げても recirculation が効ききらない=**箱が閉じておらず光が逃げている**疑いが,実験1(無限バウンスでも尖り+縦勾配が残る)と整合して裏付けられた. +- → **形の残差の本筋は B1 筐体幾何(光の入り方・箱の閉じ方)に絞り込まれた**. + +## 実験3 — bottom 単灯 配光(spot cutoff)スイープ(spp=1024, d=16) + +### 目的・条件 + +LED の面積がバグ修正で正しくなったため,光源側の最後のレバー=**配光(指向性)**を再検証する(過去 Step 8-14 の ξ 実験はバグ込みのため無効).**bottom 単灯・spp=1024・max_depth=16 固定**で,Lambertian 面光源(現状)に対し spot emitter の cutoff_angle を `{15,30,60,90,120,150}°`(beam_width = cutoff/3)でスイープ. + +- config: `configs/_tmp_distrib_bottom/c{015..150}.yaml`(bottom 6 灯のみ,`scene_baseline_spot.yaml` ベース) +- 出力: `output/distrib_bottom/c{NNN}/luminance.npy`,基準 area は `output/depth_bottom/d16` +- 比較図: `output/_slide_panels/distrib_bottom_shape_roi.png`(ROI 版) + +### 結果(ROI 揃え) + +| 光源 | peak/mean(ROI) | top/bot(ROI) | NCC(ROI,対実機) | +| --- | --- | --- | --- | +| area(Lambertian, 現状) | 1.585 | 0.879 | 0.720 | +| spot 15° | 1.950 | 0.761 | 0.652 | +| spot 30° | 1.657 | 0.800 | **0.737** | +| spot 60° | 1.443 | 0.861 | 0.730 | +| spot 90° | **1.436** | 0.879 | 0.722 | +| spot 120° | 1.704 | 0.883 | 0.705 | +| spot 150° | 1.828 | 0.883 | 0.681 | +| **実機 bottom** | **1.218** | **0.931** | – | + +### 読み取れること + +1. **中程度の spot(60〜90°)が最も平ら**(peak/mean 1.44)で Lambertian(1.59)よりわずかに改善.狭すぎ(15°→1.95)・広すぎ(150°→1.83)は悪化=中庸が谷. +2. **最良でも peak/mean 1.44 で実機 1.22 に届かず,NCC は全域 0.65〜0.74 でほぼ横ばい**(最良 spot30 で 0.737,area 0.720 から誤差レベル). +3. → **配光も形の決定的レバーではない**(動くが届かない).物理的には板が間接光のみ+Lambertian 拡散壁なので,配光を変えても背面壁への初当たりが変わるだけで拡散反射が大半を均す. + +> **過去結論の訂正**: PROGRESS の Step 8-14「area→spot は形改善の本物候補(angular_ratio −25%)」は**バグ込み(2m ストリップ)の結論**であり,正しい面積で再検証すると効果は誤差レベル(NCC 0.72→0.74).TEST_07 と同じく ξ も過大評価だった. + +## 3 実験の総括と次の予定 + +| レバー | 実験 | 形への効果(ROI) | 判定 | +| --- | --- | --- | --- | +| 反射回数 max_depth | 実験1 | d16 で頭打ち,peak/mean 1.59 止まり | 本筋でない | +| 反射率 R | 実験2 | R=0.98 でも 1.39 止まり,NCC 改善せず | 本筋でない | +| 配光 ξ(spot cutoff) | 実験3 | 中庸 spot で 1.44 止まり,NCC 横ばい | 本筋でない | +| **筐体幾何 B1** | 実験4(予定) | — | **最有力(光源側レバー出尽くし)** | + +光源側のレバー(反射回数・反射率・配光)を出し尽くしても形は実機に届かない=**残るは筐体幾何のみ**. + +### 次(実験4)— 幾何の漏れ点検 + +「光がどこから逃げているか/箱が閉じているか」を直接調べる: + +- **漏れの定量化**: bottom 単灯で,放出フラックスのうち何割が評価板に届き,何割が開口から外へ逃げているかを測る +- **閉じテスト**: シミュ筐体の開いている面(センサ側開口など)を仮の白壁で塞いで再レンダ → 平らになれば「閉じ漏れ」が原因と確定 + +### 補足 — 実機の平らさの素性確認(実験4 前に要検討) + +実機マップは **LSC 補正済み**(`dng_luminance(apply_lsc=True)`).LSC はレンズ周辺減光を割り戻す処理で**本来は照明分布を平らにしない**はずだが,「実機の平らさが本物の照明分布か,処理由来か」を実験4 前に一度確認しておくと安全([TECH_07 LSC 取り扱い方針](../05_TECH/TECH_07_LSC取り扱い方針.md) 参照). + +## 追加観察 — 修正後の 3 単灯は「形」がほぼ同一(実機の相補性を再現できていない) + +なぜ光源の形(点 vs 2m 帯)を変えても板の形がほとんど変わらないのか([TEST_08 §修正前後のカラーマップ比較](TEST_08_面光源スケール軸バグの発見と修正.md))の物理的根拠は,**板が「間接光のみ(LED は板に背を向け,depth=1,2 が真っ暗)+ Lambertian 拡散壁」で照らされ,形が光源でなく筐体で決まる**ため.拡散壁での多重反射が光源の形状情報を洗い流す(積分球の原理;壁が鏡面なら光源形状が板に焼き付くが Lambertian なので消える). + +この帰結として,**修正後シミュでは下・左・右の 3 単灯の「形」が互いにほぼ同一**になる(同じ筐体に光を注ぐため).3 単灯どうしの形の一致度(mean 正規化,フル frame 計測): + +| ペア | 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 灯は互いにかなり似た形**.違いは「自分の側がほんの少し明るい」弱い横バイアスのみ(縦勾配 top/bot ≈ 0.85/1.06 が 3 灯ほぼ同一). + +**重要な含意**: [TEST_04(実機単灯比較)](TEST_07_LEDグループ別動作確認.md) では実機の 3 灯は**相補的に異なる方向を照らす**(下=上側/左=右寄り…)とされていた.しかし**修正後シミュは 3 灯ほぼ同じ形=実機の「相補性」を再現できていない**.これは「シミュでは形が筐体で決まり LED 位置の効きが弱すぎる」ことを意味し,(a) 筐体幾何 B1 の不正確さ,または (b) シミュが間接化しすぎ(実機は各灯の直接性/指向性がもっと残る)を示唆する.実験4(幾何)の追加手がかり. + +## 関連ドキュメント + +- [TEST_08 面光源スケール軸バグの発見と修正](TEST_08_面光源スケール軸バグの発見と修正.md) — 本調査の前提(バグ修正で灯間バランスは解決済み) +- [TEST_07 LED グループ別動作確認](TEST_07_LEDグループ別動作確認.md) — バグ込みの旧結論(訂正済み) +- 実機入力: `lightsim/luminance_maps/bottom_*.npy`(単灯実測,`.gitignore` 対象) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a701f0c..f41081d 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -57,3 +57,14 @@ - **Step 8-14 候補 α 後半 (A8 反射率スイープ) 完了 — 数値改善のみ・形は逆効果 / 「数値の偽物の一致」の初の定量実証 (2026-06-03)**: `scripts/sweep_reflectance.py` で d=16 新基準のもと R ∈ {0.75, 0.80, 0.85, 0.89, 0.92} を 5 値スイープ.新指標 (shape_metrics) を初めて感度分析に投入.結果: 既存 4 指標 (CoV/勾配/C/P/動径 min/max) は単調改善 (R=0.75→0.92 で CoV 0.186→0.142 で残差 24% 縮小) するが,**新指標 angular_ratio は 0.408→0.494 で実機方向と逆に増加**.実機は中央ピーク楕円 (同心円的) なので **形は実機から遠ざかる**.物理的解釈: R 上昇で ambient 増 → 半径方向の偏りは圧縮されるが角度方向ばらつきは相対的に圧縮されにくい → angular_ratio 増加.**形改善の本筋は B1 筐体幾何精度に最終確定**(A1/A8/A9 すべてクリア済で構造的再配分系のみ残る).副次発見: R=0.85 の CoV=0.1604 が max_depth スイープ d=16 と完全一致.新指標を導入していなければ気づけない発見で,形状指標見直し (TECH_11) の正当性が定量データで裏付けられた. - **Step 8-14 候補 α 前半 (A9 max_depth スイープ) 完了 — 数値改善のみクリア・形不変 (2026-06-03)**: ユーザー仮説「現状 d=8 では 0.85^8≈0.27 で取りこぼし大」を 5 値スイープ {8, 12, 16, 20, 28} で検証 (`scripts/sweep_max_depth.py` 約 85 分 + `scripts/build_max_depth_panel.py`).結果: 全 4 形状指標が **単調に実機方向に改善** (d=8→28 で CoV 0.1817→0.1583 で残差 20.9% 縮小).mean は 207.82 で固定.**実用的な収束は d=16 で達成** (d=16→28 で CoV 1.3% のみ).**「形」は完全に不変** (Y 軸単調勾配のまま contrast が緩むだけ).物理的解釈: 多重反射は **空間的に均一な ambient 成分**で,足しても暗/明の絶対差は縮むが空間配置は不変.「LED が下にしかない」配置の非対称性は除去不能.**ユーザー仮説の評価**: 方向性は正しい (21-28% 残差縮小) だが実用収束は d=16,実機 (CoV=0.0695) には届かず残差は形改善で詰める必要.新キャリブベースライン: **d=16**. - **形状指標の本質的見直しと新指標導入 (2026-06-03)**: 「数値が実機方向に動いても形は変わらない」が続けて確定したため,ユーザー指摘「現状 4 指標は不均一性の絶対量しか測れず,Y 軸単調勾配と中央ピーク楕円を構造的に区別できない」に対応する **形を直接評価する 5 指標** を追加実装.`src/sm_tias_lightsim/evaluation/shape_metrics.py` (新規): `calc_intensity_centroid` / `calc_angular_radial_decomposition` (angular_ratio: 0=同心円, 1=方向依存) / `calc_ssim` (scikit-image 依存) / `calc_ncc` / `calc_intensity_pca` の純関数 5 つ.`MetricsConfig` に `shape_metrics` / `shape_reference` を追加(後方互換).`configs/eval_baseline_dng_target_shape.yaml` を新規作成.`pyproject.toml` に `scikit-image>=0.21.0` を追加.[TECH_11](05_TECH/TECH_11_形状指標の見直しと新指標導入.md) で背景・計算式・予想値・遡及的注記を記録.今後の評価方針: **数値指標 4 + 形指標 5 + 視覚比較の 3 軸評価**で「数値改善のみで形が変わらない候補」と「形を変えうる候補」を明確に区別する. +- **面光源スケール軸バグの発見・修正 — 下灯 2.1× の真因確定 (2026-06-29)**: 実機の単灯(下/左/右)実測データ(`lightsim/`,SmTIAS-Evaluation 引き渡し,各灯単独点灯の白板 ROI 輝度マップ)とシミュ単灯を**灯ごとに突き合わせ**た結果,`scene/lights.py` の `_build_area_light_transform` に**座標軸取り違えバグ**を発見.Mitsuba の rectangle は実体「XY 平面・法線 +Z」(実機 Mitsuba で検証)なのに,コードは旧コメント「XZ 平面・+Y 前面」を前提に `scale([sw/2, 1.0, sh/2])` と書かれ,**高さ `sh` を潰れた奥行き Z に入れ・縦 Y を無補正(±1m)のまま**にしていた → **全 18 LED が「幅 ~3mm × 高さ 2000mm」の縦長ストリップ**化.全灯が同一 look_at(direction=(0,0,-1), up=(0,1,0))のため local Y→world Y で全灯が world Y に 2m 伸び,**板中央の真裏にある下灯だけが過剰に効いて 2.1×** を生んでいた(左右灯は x=±66 で板外・壁脇のため損失大).**修正は 1 行**: `scale([sw/2, sh/2, 1.0])`. + - **検証 (spp=1024, max_depth=16)**: 灯間 mean 比は bottom 2.099→**1.000(最暗)** / left 1.000→1.028 / right 1.000→1.026 となり,**実機の「3 灯均等・下灯最暗」順序に一致**.線形性(単灯和≈全灯)誤差 0.34%.全灯合成 linear CoV は **0.1815→0.1641**(公式 sm-evaluate,残差 2.63×→2.36×)の**小改善にとどまる**.center/周辺比・勾配はほぼ不変. + - **論点の移動**: 灯間バランス問題は解決したが,**残る不一致は各単灯フットプリントの拡散不足**(修正後 peak/mean≈1.7 vs 実機≈1.2,シミュは中央尖り+上暗下明,実機はほぼ一様).本筋は**配光 ξ/ディフューザー拡散のモデル化**へ移った.成果物: 修正後出力 `output/{sanity_fix_1024,baseline_fix_1024}/`,比較図 `output/_slide_panels/{sim_vs_real_shape_fixed,sim_vs_real_colormap_fixed}.png`,記録 [TEST_08](06_TEST/TEST_08_面光源スケール軸バグの発見と修正.md). + - **過去結論の訂正**: [TEST_07](06_TEST/TEST_07_LEDグループ別動作確認.md) の「下灯 2.1× は直接光効率の幾何的優位で物理的に正しい/意図した設計」は**誤り**(バグ込み出力を物理で後付け正当化していた).TEST_07 冒頭に訂正注記を追加.**教訓**: シミュ出力を物理で後付け説明するとバグを設計意図と取り違える危険があり,実機グラウンドトゥルースとの灯ごと突合が有効だった.なお `lightsim/` は実測入力として `.gitignore` 対象. +- **照明分布の「形」不一致の原因切り分け開始 — 混ぜ具合では詰められず B1 幾何に絞り込み (2026-06-29)**: バグ修正後も残る「形が実機と一致しない」問題(実機単灯は peak/mean≈1.2 のほぼ一様,シミュは≈1.7 で中央尖り+上暗下明)を,**形の一致(実機マップ参照の NCC/SSIM+視覚+放射プロファイル)を唯一の判定基準**に切り分け開始.[TEST_09](06_TEST/TEST_09_照明分布の形不一致の原因切り分け.md) に記録.**前提確認**: 実機は **LED カバー=筐体と同じ不透明白材・別ディフューザー無し**(ユーザー確認)=むき出し LED+白筐体のみ → 実機の平らさは筐体多重反射(積分球的拡散)由来でしかありえず,鍵は新部品でなく筐体(反射+幾何)のモデル化.**重要**: 過去 Step 8-14 の「A8/A9/A1 を振っても形は変わらない」はすべて面光源バグ込みの結論で,TEST_07 同様に要再検証. + - **実験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_roi.png`(ROI 揃え.数値は TEST_09 が正:ROI 版で d16 peak/mean=1.59・NCC=0.72). + - **実験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_roi.png`(ROI 揃え.数値は TEST_09 が正). + - **総括**: 「筐体が光をかき混ぜる強さ」を司る 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/build_blend_bsdf_panel.py b/scripts/build_blend_bsdf_panel.py index f0a4c4a..db92d5d 100644 --- a/scripts/build_blend_bsdf_panel.py +++ b/scripts/build_blend_bsdf_panel.py @@ -32,6 +32,7 @@ import numpy as np from sm_tias_lightsim.evaluation.normalize import apply_mean_anchor +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh # --------------------------------------------------------------------------- # 定数 @@ -44,9 +45,6 @@ BASELINE_NPY = ROOT / "output" / "baseline" / "luminance.npy" BLEND_OUTPUT_BASE = ROOT / "output" / "blend_sweep" -# ROI 設定(configs/eval_baseline.yaml / eval_baseline_dng_target.yaml と整合) -SIM_ROI = {"x": 24, "y": 37, "w": 464, "h": 450} - # mean 正規化の target_mean(eval_baseline_dng_target.yaml の target_mean と整合) TARGET_MEAN = 207.82 @@ -82,12 +80,6 @@ return f"w{_format_param(weight)}_a{_format_param(alpha)}" -def _extract_roi(arr: np.ndarray, roi: dict) -> np.ndarray: - """ROI 矩形を切り出して float64 に変換する.""" - x, y, w, h = roi["x"], roi["y"], roi["w"], roi["h"] - return arr[y: y + h, x: x + w].astype(np.float64) - - def _rec709_luma(rgb: np.ndarray) -> np.ndarray: """RGB 配列を Rec.709 luma に変換する (H, W, 3) → (H, W).""" return 0.2126 * rgb[..., 0] + 0.7152 * rgb[..., 1] + 0.0722 * rgb[..., 2] @@ -112,7 +104,7 @@ arr = np.load(npy_path) if arr.ndim == 3: arr = _rec709_luma(arr) - roi = _extract_roi(arr, SIM_ROI) + roi = extract_roi_xywh(arr, SIM_ROI) normalized, _ = apply_mean_anchor(roi, TARGET_MEAN) return normalized diff --git a/scripts/build_led_distribution_panel.py b/scripts/build_led_distribution_panel.py index 2b579af..455d88d 100644 --- a/scripts/build_led_distribution_panel.py +++ b/scripts/build_led_distribution_panel.py @@ -22,6 +22,8 @@ import matplotlib.font_manager as fm import matplotlib.pyplot as plt +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh + ROOT = Path(__file__).resolve().parents[1] SWEEP_BASE = ROOT / "output" / "led_distribution_sweep" SUMMARY_CSV = SWEEP_BASE / "summary.csv" @@ -66,7 +68,7 @@ if not npy.exists(): lums.append(None) continue - arr = np.load(npy) + arr = extract_roi_xywh(np.load(npy), SIM_ROI) lums.append(arr.mean(axis=-1) if arr.ndim == 3 else arr) valid = [x for x in lums if x is not None] diff --git a/scripts/build_led_group_sanity_panel.py b/scripts/build_led_group_sanity_panel.py index 0d40937..dcca696 100644 --- a/scripts/build_led_group_sanity_panel.py +++ b/scripts/build_led_group_sanity_panel.py @@ -32,6 +32,8 @@ import matplotlib.font_manager as fm import matplotlib.pyplot as plt +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh + # --------------------------------------------------------------------------- # 定数 # --------------------------------------------------------------------------- @@ -63,13 +65,13 @@ if not npy_path.exists(): print(f"エラー: {npy_path} が見つかりません", file=sys.stderr) return 1 - lums[g] = np.load(npy_path) + lums[g] = extract_roi_xywh(np.load(npy_path), SIM_ROI) # ベースライン (全 ON) 読み込み if not BASELINE_NPY.exists(): print(f"エラー: ベースライン {BASELINE_NPY} が見つかりません", file=sys.stderr) return 1 - baseline = np.load(BASELINE_NPY) + baseline = extract_roi_xywh(np.load(BASELINE_NPY), SIM_ROI) # スカラ輝度に変換 (RGB なら平均,2D ならそのまま) def to_2d(arr: np.ndarray) -> np.ndarray: diff --git a/scripts/build_luminance_evolution_panel.py b/scripts/build_luminance_evolution_panel.py index 5d4a809..689a0c4 100644 --- a/scripts/build_luminance_evolution_panel.py +++ b/scripts/build_luminance_evolution_panel.py @@ -32,14 +32,11 @@ from sm_tias_lightsim.evaluation.normalize import apply_mean_anchor from sm_tias_lightsim.evaluation.oetf import apply_srgb_oetf, solve_exposure +from sm_tias_lightsim.evaluation.roi_presets import REAL_ROI, SIM_ROI, extract_roi_xywh ROOT = Path(__file__).resolve().parents[1] TARGET_MEAN = 146.7146 -# ROI 設定(configs と整合) -SIM_ROI = {"x": 24, "y": 37, "w": 464, "h": 450} # configs/eval_baseline.yaml -REAL_ROI = {"x": 539, "y": 1015, "w": 1400, "h": 1409} # configs/eval_real.yaml - # 共有 colorbar の値域決定方針: 実機 ROI のパーセンタイル # 5〜95 パーセンタイルで外れ値(センサノイズ・最暗最明画素)を除く. REAL_VMIN_PERCENTILE = 5.0 @@ -51,12 +48,6 @@ return 0.2126 * rgb[..., 0] + 0.7152 * rgb[..., 1] + 0.0722 * rgb[..., 2] -def _extract_roi(arr: np.ndarray, roi: dict) -> np.ndarray: - """ROI 矩形を切り出して float64 に変換する.""" - x, y, w, h = roi["x"], roi["y"], roi["w"], roi["h"] - return arr[y : y + h, x : x + w].astype(np.float64) - - def _load_real() -> np.ndarray: """実機 PNG → Rec.709 luma → ROI 抽出 → mean アンカー(恒等近い).""" png = ( @@ -68,7 +59,7 @@ luma = _rec709_luma(img.astype(np.float64)) else: luma = img.astype(np.float64) - roi = _extract_roi(luma, REAL_ROI) + roi = extract_roi_xywh(luma, REAL_ROI) # 実機は既に mean ≈ 146.71 なので alpha ≈ 1.0(揺れ吸収) normalized, _ = apply_mean_anchor(roi, TARGET_MEAN) return normalized @@ -79,7 +70,7 @@ arr = np.load(npy_path) if arr.ndim == 3: arr = _rec709_luma(arr) - roi = _extract_roi(arr, SIM_ROI) + roi = extract_roi_xywh(arr, SIM_ROI) normalized, _ = apply_mean_anchor(roi, TARGET_MEAN) return normalized @@ -89,7 +80,7 @@ arr = np.load(npy_path) if arr.ndim == 3: arr = _rec709_luma(arr) - roi = _extract_roi(arr, SIM_ROI) + roi = extract_roi_xywh(arr, SIM_ROI) scale, _ = solve_exposure(roi, TARGET_MEAN) return apply_srgb_oetf(roi, scale, quantize_uint8=True) diff --git a/scripts/build_max_depth_panel.py b/scripts/build_max_depth_panel.py index e4cced1..99c785e 100644 --- a/scripts/build_max_depth_panel.py +++ b/scripts/build_max_depth_panel.py @@ -29,6 +29,8 @@ import matplotlib.font_manager as fm import matplotlib.pyplot as plt +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh + ROOT = Path(__file__).resolve().parents[1] SWEEP_BASE = ROOT / "output" / "max_depth_sweep" SUMMARY_CSV = SWEEP_BASE / "summary.csv" @@ -75,7 +77,7 @@ print(f"警告: {npy} 見つからずスキップ", file=sys.stderr) lums.append(None) continue - arr = np.load(npy) + arr = extract_roi_xywh(np.load(npy), SIM_ROI) lums.append(arr.mean(axis=-1) if arr.ndim == 3 else arr) # 共有 vmin/vmax diff --git a/scripts/build_reflectance_panel.py b/scripts/build_reflectance_panel.py index bd7da9b..fed3750 100644 --- a/scripts/build_reflectance_panel.py +++ b/scripts/build_reflectance_panel.py @@ -28,6 +28,8 @@ import matplotlib.font_manager as fm import matplotlib.pyplot as plt +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh + ROOT = Path(__file__).resolve().parents[1] SWEEP_BASE = ROOT / "output" / "reflectance_sweep" SUMMARY_CSV = SWEEP_BASE / "summary.csv" @@ -74,7 +76,7 @@ if not npy.exists(): lums.append(None) continue - arr = np.load(npy) + arr = extract_roi_xywh(np.load(npy), SIM_ROI) lums.append(arr.mean(axis=-1) if arr.ndim == 3 else arr) valid = [x for x in lums if x is not None] diff --git a/scripts/build_sim_vs_darkroom_panel.py b/scripts/build_sim_vs_darkroom_panel.py index 2b34419..ed1f090 100644 --- a/scripts/build_sim_vs_darkroom_panel.py +++ b/scripts/build_sim_vs_darkroom_panel.py @@ -46,6 +46,7 @@ calc_radial_min_max_ratio, calc_radial_profile, ) +from sm_tias_lightsim.evaluation.roi_presets import REAL_ROI, SIM_ROI, extract_roi_xywh from sm_tias_lightsim.evaluation.zones import ( calc_zone_stats, classify_zones, @@ -62,12 +63,10 @@ REAL_META = ( ROOT / "reference_measurement" / "quantitative" / "MiniTIAS_QM_20260601_124500.meta.json" ) -# 白板 ROI (portrait, 旧 PNG と同一座標. DNG portrait は 2448x3264 で同一フレーミング) -REAL_ROI = {"x": 539, "y": 1015, "w": 1400, "h": 1409} +# REAL_ROI(白板 portrait)/ SIM_ROI は roi_presets に集約(configs と整合). # シミュ baseline (max_depth=16, R=0.85, spp=1024) SIM_NPY = ROOT / "output" / "baseline" / "luminance.npy" -SIM_ROI = {"x": 24, "y": 37, "w": 464, "h": 450} # configs/eval_baseline_dng_target.yaml OUT_PATH = ROOT / "output" / "_slide_panels" / "sim_vs_darkroom_normalized.png" @@ -77,12 +76,6 @@ return _REC709[0] * rgb[..., 0] + _REC709[1] * rgb[..., 1] + _REC709[2] * rgb[..., 2] -def _extract_roi(arr: np.ndarray, roi: dict) -> np.ndarray: - """ROI 矩形を float64 で切り出す.""" - x, y, w, h = roi["x"], roi["y"], roi["w"], roi["h"] - return arr[y : y + h, x : x + w].astype(np.float64) - - def _upsample(grid: np.ndarray, shape: tuple[int, int]) -> np.ndarray: """(rows, cols) グリッドを (H, W) へバイリニア補間する (run_uniformity_dng.py と同一).""" rows, cols = grid.shape @@ -117,7 +110,7 @@ rgb = rgb * _lsc_gain_rgb(REAL_META, rgb.shape[:2]) # LSC 順方向適用 (ビネット除去) luma = _rec709_luma(rgb) * 255.0 # 0〜255 linear スケール luma = np.rot90(luma, -1) # portrait (PNG / ROI と同じ向き) - return _extract_roi(luma, REAL_ROI) + return extract_roi_xywh(luma, REAL_ROI) def _load_sim() -> np.ndarray: @@ -125,7 +118,7 @@ arr = np.load(SIM_NPY) if arr.ndim == 3: arr = _rec709_luma(arr) - return _extract_roi(arr, SIM_ROI) + return extract_roi_xywh(arr, SIM_ROI) def _minmax_normalize(roi: np.ndarray) -> np.ndarray: 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() diff --git a/src/sm_tias_lightsim/evaluation/roi_presets.py b/src/sm_tias_lightsim/evaluation/roi_presets.py new file mode 100644 index 0000000..e0c3332 --- /dev/null +++ b/src/sm_tias_lightsim/evaluation/roi_presets.py @@ -0,0 +1,36 @@ +"""キャリブレーション基準 ROI のプリセットと dict ベース抽出ヘルパ. + +スライド/比較パネル生成スクリプト(scripts/build_*_panel.py 等)が共通で使う +ROI 座標を一元管理する.座標は評価パイプライン(sm-evaluate)の +configs/eval_baseline*.yaml / eval_real.yaml と同一. + +評価本体(シーンスキーマ ``RoiConfig`` を使う側)は +``sm_tias_lightsim.evaluation.roi.extract_roi`` を使うこと. +本モジュールは ``{"x", "y", "w", "h"}`` dict を扱うスクリプト向けの軽量ヘルパ. +""" + +from __future__ import annotations + +import numpy as np + +# シミュ luminance.npy 用 ROI(configs/eval_baseline*.yaml と整合). +SIM_ROI = {"x": 24, "y": 37, "w": 464, "h": 450} + +# 実機測定画像用 ROI(configs/eval_real.yaml と整合). +REAL_ROI = {"x": 539, "y": 1015, "w": 1400, "h": 1409} + + +def extract_roi_xywh(arr: np.ndarray, roi: dict) -> np.ndarray: + """ROI 矩形(``{"x", "y", "w", "h"}``)を切り出して float64 で返す. + + 先頭 2 軸を (H, W) とみなすため,2D(輝度)・3D(RGB)どちらの配列も扱える. + + Args: + arr: 先頭 2 軸が (H, W) の配列. + roi: ``{"x", "y", "w", "h"}`` 形式の ROI 辞書(``SIM_ROI`` / ``REAL_ROI``). + + Returns: + ROI でクロップした float64 配列(入力と同じ次元数). + """ + x, y, w, h = roi["x"], roi["y"], roi["w"], roi["h"] + return arr[y : y + h, x : x + w].astype(np.float64) diff --git a/src/sm_tias_lightsim/scene/lights.py b/src/sm_tias_lightsim/scene/lights.py index b8907ba..c80ed03 100644 --- a/src/sm_tias_lightsim/scene/lights.py +++ b/src/sm_tias_lightsim/scene/lights.py @@ -61,7 +61,7 @@ transform = mi.ScalarTransform4f().look_at(origin=origin, target=target, up=up) # サイズに合わせてスケール(rectangle は ±1 なので half-size でスケール) - scale = mi.ScalarTransform4f().scale([sw / 2.0, 1.0, sh / 2.0]) + scale = mi.ScalarTransform4f().scale([sw / 2.0, sh / 2.0, 1.0]) return transform @ scale