diff --git a/CLAUDE.md b/CLAUDE.md index 51f7cf4..c2ea6bd 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× がこのバグのアーティファクト**と判明(バグは 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). +最新: フェーズ 7(実装)— Step 8 基準設計キャリブレーションの **Step 8-14(物理モデル精緻化)** 進行中.面光源スケール軸バグ修正([TEST_08](docs/06_TEST/TEST_08_面光源スケール軸バグの発見と修正.md))後の「形の不一致」切り分け([TEST_09](docs/06_TEST/TEST_09_照明分布の形不一致の原因切り分け.md))で,bottom 単灯 spp=1024・d=16 の統一条件により**実験1 反射回数・実験2 反射率・実験3 配光(spot cutoff)・実験5 壁の光沢化(blend BSDF w×α 12 ジョブ,バグ込み旧 A1 の正式再検証)をすべてクリア=光源側・材料側レバー出尽くし**(いずれも実機の平らさ peak/mean 1.22 に届かず,光沢化は全条件で悪化).実験5 で**実機の灯間 NCC を初定量化: 0.85/0.61/0.41 と強く非対称な相補性**(シミュ diffuse は 0.96 で 3 灯ほぼ同形,光沢化でも一様に 0.86 止まり)=等方な材料変更では原理的に再現不能で,**形と相補性の残差はともに B1 筐体幾何に確定**.次は実験4(幾何の漏れ点検=漏れの定量化・開口の閉じテスト,到達目標アンカーは実機灯間 NCC).詳細な経緯・設計判断・失敗パターンは [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_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 a35c8ba..682b340 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" @@ -54,6 +54,10 @@ | `output/led_distribution_sweep/c{030,060,090,120,150}/` (5 ディレクトリ) | 8-14 候補 ξ | `configs/scene_baseline_spot.yaml` (Step 8-10 で作成) をテンプレに `scripts/sweep_led_distribution.py` で全 spot light の `cutoff_angle` を {30, 60, 90, 120, 150}° に,`beam_width = cutoff/3` で連動,max_depth を 16 に更新 (R=0.85, spp=1024) | `configs/eval_baseline_dng_target_shape.yaml` (新指標 5 つ含む) | c090 で CoV=0.165, angular_ratio=0.352; c030 で angular_ratio=0.338 — [TEST_01 § Step 8-14 候補 ξ 結果表](TEST_01_キャリブレーション検証履歴.md#step-8-14-候補-ξ-a2a3-led-配光-cutoff_angle-5-値スイープ-30-60-90-120-150) 参照 | TEST_01 Step 8-14 候補 ξ 検証.**配光モデルが形改善の本物の候補として初実証**.area→spot で angular_ratio が 0.45→0.34 で 25% 低下 | | `output/led_distribution_sweep/summary.csv` | 8-14 候補 ξ | — | — | 5 ジョブ集約 CSV (cutoff_angle, beam_width + 既存 12 + 新 12 指標) | `scripts/sweep_led_distribution.py` の集約出力 | | `output/_slide_panels/led_distribution_{sweep,convergence,shape_metrics}.png` | 8-14 候補 ξ | — | — | 輝度マップ 2 段 / 既存 4 指標カーブ / 新指標カーブ | TEST_01 「形」追跡 — **angular_ratio が cutoff 30° → 150° でほぼ flat (0.338→0.356) を視覚確認.area→spot の遷移が本質と判明** | +| `output/blend_bottom/w{W}_a{A}/` (12 ディレクトリ) | 8-14 TEST_09 実験5 | `configs/_tmp_blend_bottom/w{W}_a{A}.yaml`(`_tmp_sanity_1024/scene_bottom.yaml` ベース,printer_white を blendbsdf 化,w ∈ {0.05, 0.15, 0.3, 0.6} × α ∈ {0.1, 0.3, 0.6},bottom 6 灯のみ,d=16, spp=1024,**修正後 LED**) | 形指標のみ (`scripts/build_blend_bottom_panel.py`) | 全 12 ジョブが diffuse 基準 (peak/mean 1.585) より悪化,最悪 w0.6_a0.1 で 2.145 — [TEST_09 § 実験5](TEST_09_照明分布の形不一致の原因切り分け.md) 参照 | TEST_09 実験5(壁の光沢化)Phase 1.**光沢は w に単調に形悪化=材料側レバー出尽くしを確定** | +| `output/blend_bottom/{left,right}_w{W}_a{A}/` (各 5 ディレクトリ) | 8-14 TEST_09 実験5 拡張 | `configs/_tmp_blend_bottom/{left,right}_w{W}_a{A}.yaml`(縮小グリッド w ∈ {0.05, 0.3} × α ∈ {0.1, 0.6} + Phase 2 の w0.6_a0.1) | 同上 (`--side left/right`) | 「w 単調悪化」3 灯共通.灯間 NCC は diffuse 0.96 → w0.6 で 0.86 だが常に 3 ペア均等(実機 0.85/0.61/0.41 の非対称に届かず) | TEST_09 実験5 Phase 2+拡張.**実機の灯間 NCC(相補性の非対称)を初定量化=実験4 の到達目標アンカー** | +| `output/blend_bottom/summary{,_left,_right}.csv` | 8-14 TEST_09 実験5 | — | — | 各灯の形指標集約 (job, weight, alpha, peak_mean, top_bot, ncc) | `scripts/build_blend_bottom_panel.py` の集約出力 | +| `output/_slide_panels/blend_{bottom,left,right}_shape_roi.png` | 8-14 TEST_09 実験5 | — | — | 基準+実機+w×α グリッドの形比較パネル(ROI 揃え・各セル独立 1–99% 正規化・実寸アスペクト) | TEST_09 実験5 視覚比較用・発表スライド素材 | ### sweep_summary.csv の値(参考、TEST_01 line と一致確認用) @@ -95,6 +99,8 @@ | 反射率比較パネル生成 | `uv run python scripts/build_reflectance_panel.py` (輝度マップ 2 段 + 既存指標 + 新指標カーブ.要 `output/reflectance_sweep/summary.csv` + 各 `luminance.npy`) | | LED 配光 cutoff 5 値スイープ (Step 8-14 候補 ξ) | `$env:DRJIT_LIBLLVM_PATH = "C:\Program Files\LLVM\bin\LLVM-C.dll"; uv run python scripts/sweep_led_distribution.py` (5 ジョブ × 約 17 分 = 約 85 分) | | LED 配光比較パネル生成 | `uv run python scripts/build_led_distribution_panel.py` (輝度マップ 2 段 + 既存指標 + 新指標カーブ.要 `output/led_distribution_sweep/summary.csv` + 各 `luminance.npy`) | +| blend 単灯スイープ (TEST_09 実験5) | `configs/_tmp_blend_bottom/` の各 YAML を `uv run sm-render --scene configs/_tmp_blend_bottom/{job}.yaml --output output/blend_bottom/{job}` で順次実行 (1 ジョブ約 10.5 分) | +| blend 単灯の形指標解析・パネル生成 | `uv run python scripts/build_blend_bottom_panel.py --side {bottom,left,right}` (要 `output/blend_bottom/` の各 `luminance.npy` + 実機 `lightsim/luminance_maps/*.npy`) | ## 過去の整理実績 (Cleanup History) 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 1101fa0..4179775 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" @@ -129,16 +129,99 @@ > **過去結論の訂正**: PROGRESS の Step 8-14「area→spot は形改善の本物候補(angular_ratio −25%)」は**バグ込み(2m ストリップ)の結論**であり,正しい面積で再検証すると効果は誤差レベル(NCC 0.72→0.74).TEST_07 と同じく ξ も過大評価だった. -## 3 実験の総括と次の予定 +## 実験5 — bottom 単灯 blend BSDF(壁の光沢化)再検証(spp=1024, d=16) + +### 目的・条件 + +過去 Step 8-14 β の blend BSDF 実験(A1 非 Lambertian)は**面光源バグ込み・3 灯フルシーン・絶対指標評価**だったため,修正後 LED・TEST_09 の形指標で再検証する.検証仮説は 2 つ: + +1. **平らさ**: 光沢成分が単灯の形(peak/mean・NCC)を実機に近づけるか(実験1〜3 の残差) +2. **相補性**: §追加観察の「3 単灯ほぼ同形(灯間 NCC 高止まり)」は壁が完全 Lambertian であることが直接原因(鏡面なら光源形状が板に焼き付く)のため,**光沢化で LED 位置の効きが戻るか** + +条件は実験1〜3 と同一(bottom 単灯・spp=1024・max_depth=16).`printer_white` を `blendbsdf`(diffuse 0.85 + roughconductor ggx)化し,**w ∈ {0.05, 0.15, 0.3, 0.6} × α ∈ {0.1, 0.3, 0.6} の 12 ジョブ**(旧グリッド+強光沢 w=0.6 行)をスイープ. + +- config: `configs/_tmp_blend_bottom/w{W}_a{A}.yaml`(`_tmp_sanity_1024/scene_bottom.yaml` ベース) +- 出力: `output/blend_bottom/{job}/luminance.npy` + `summary.csv` +- 解析: `scripts/build_blend_bottom_panel.py`,比較図 `output/_slide_panels/blend_bottom_shape_roi.png`(ROI 揃え) + +### 結果 Phase 1 — 単灯の形(ROI 揃え) + +| 条件 | peak/mean(ROI) | top/bot(ROI) | NCC(ROI,対実機) | +| --- | --- | --- | --- | +| area diffuse(基準) | 1.585 | 0.879 | 0.721 | +| w0.05(3α 中最良) | 1.604 | 0.878 | 0.717 | +| w0.15(3α 中最良) | 1.691 | 0.876 | 0.711 | +| w0.3(3α 中最良) | 1.698 | 0.875 | 0.702 | +| w0.6(3α 中最良) | 1.736 | 0.879 | 0.669 | +| w0.6_a0.1(最悪) | 2.145 | 0.833 | 0.667 | +| **実機 bottom** | **1.218** | **0.931** | – | + +**全 12 ジョブが純 diffuse より悪化し,w に対し単調悪化**.光沢成分は評価板にホットスポットを作って尖りを増やし,NCC も下げる.α では鏡面寄り(α=0.1)が最も悪く,「鏡面ほど光源形状が焼き付く」の物理と整合.**旧結論「A1 は形改善レバーでない」は正しい LED でも確定**(ただし旧実験の根拠は無効で,本実験が正式な根拠). + +### 結果 Phase 2 — 灯間 NCC(相補性) + +最強光沢条件 w0.6_a0.1 で left / right 単灯も同条件レンダし,3 灯相互の形の一致度を比較(mean 正規化・フル frame,spp=1024): + +| ペア | diffuse | blend w0.6_a0.1 | **実機** | +| --- | --- | --- | --- | +| bottom vs left | 0.961 | 0.862 | **0.853** | +| bottom vs right | 0.963 | 0.860 | **0.606** | +| left vs right | 0.956 | 0.856 | **0.414** | + +※ diffuse の灯間 NCC は spp=1024 の `output/sanity_fix_1024/` で 0.96(§追加観察の 0.92 は spp が低い `led_group_sanity_fix` 計測でノイズにより過小だった).**実機の灯間 NCC を初めて定量化**: 0.85 / 0.61 / 0.41 と**強く非対称な相補性**(特に left vs right はほぼ別物の分布)を持つ. + +### 読み取れること + +1. **機構としては仮説どおり**: 光沢化で灯間 NCC は 0.96 → 0.86 に低下=LED 位置の効きは確かに戻る(Lambertian 壁が相補性を消している,という §追加観察の物理解釈を実験で裏付け). +2. **しかし実機の相補性には全く届かない**: 実機は 3 ペアが 0.85 / 0.61 / 0.41 と**非対称**なのに,blend は 3 ペア一様に 0.86 止まり.光沢を強めても「各灯のホットスポットが少しずれる」だけで,実機の「各灯が別方向を照らす」構造は生まれない. +3. **形とのトレードオフが致命的**: 相補性がわずかに戻る代償に,単灯の形は全灯悪化(NCC 対実機: bottom 0.720→0.667,left 0.664→0.565,right 0.787→0.702). +4. 3D プリンタ白フィラメントに w=0.6 の鏡面性は物理的にも非現実的.**壁の反射特性(大きさ・種類とも)は形の残差の本筋でないことが最終確定**. + +### 拡張 — left / right 単灯でも同傾向を確認(縮小グリッド) + +bottom の結論が灯に依存しないことを確認するため,left / right 単灯でも縮小グリッド +**w ∈ {0.05, 0.3} × α ∈ {0.1, 0.6}**(+Phase 2 の w0.6_a0.1)を同条件でスイープ +(`configs/_tmp_blend_bottom/{left,right}_w*_a*.yaml`,基準 diffuse は `output/sanity_fix_1024/`, +図 `output/_slide_panels/blend_{left,right}_shape_roi.png`,数値 `output/blend_bottom/summary_{left,right}.csv`): + +| 条件 | left peak/mean | left NCC | right peak/mean | right NCC | +| --- | --- | --- | --- | --- | +| area diffuse(基準) | 1.555 | 0.664 | 1.610 | 0.787 | +| w0.05(2α 中最良) | 1.579 | 0.664 | 1.546 | 0.789 | +| w0.3(2α 中最良) | 1.746 | 0.638 | 1.846 | 0.773 | +| w0.6_a0.1 | 2.025 | 0.565 | 2.198 | 0.702 | +| **実機** | **1.267** | – | **1.201** | – | + +**bottom と同じ「w 単調悪化」を 3 灯すべてで確認**(w0.05 は誤差レベルで基準と同等,w0.3 以上で明確に悪化). +また 3 灯揃った 5 つの材料条件で灯間 NCC(相補性)を比較すると: + +| 条件 | b-l | b-r | l-r | +| --- | --- | --- | --- | +| diffuse | 0.961 | 0.963 | 0.956 | +| w0.05_a0.1 | 0.959 | 0.961 | 0.954 | +| w0.3_a0.1 | 0.919 | 0.921 | 0.911 | +| w0.6_a0.1 | 0.862 | 0.860 | 0.856 | +| **実機** | **0.853** | **0.606** | **0.414** | + +灯間 NCC は w に対し滑らかに低下するが,**どの条件でも 3 ペアが一様**(差 < 0.01)で, +実機の非対称パターン(b-l だけ似ていて l-r はほぼ別物)にはどこまで行っても近づかない. +「材料の等方な変更では 3 ペアを一様にしか動かせない」ことがスイープ全域で裏付けられた. + +### 解釈 + +実機の非対称な相補性(left vs right NCC 0.41)は,等方な材料変更では原理的に作れない.**LED から評価板への光の経路そのものが灯ごとに異なる幾何**(カバーの開口形状・仕切り・箱の閉じ方の非対称)でしか説明できず,B1 筐体幾何が最有力という実験1〜3 の結論を相補性の面からも強く裏付ける.実機灯間 NCC(0.85/0.61/0.41)は**実験4 以降の幾何修正の到達目標**として使える新しい定量アンカーである. + +## 実験群の総括と次の予定 | レバー | 実験 | 形への効果(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(予定) | — | **最有力(光源側レバー出尽くし)** | +| 壁の光沢化(blend w×α) | 実験5 | 全 12 ジョブ悪化(w 単調).灯間 NCC 0.96→0.86 で機構は確認したが実機の非対称相補性(0.85/0.61/0.41)に届かず | 本筋でない | +| **筐体幾何 B1** | 実験4(予定) | — | **最有力(光源側・材料側レバー出尽くし)** | -光源側のレバー(反射回数・反射率・配光)を出し尽くしても形は実機に届かない=**残るは筐体幾何のみ**. +光源側(反射回数・反射率・配光)と材料側(反射率の大きさ・光沢の種類)のレバーを出し尽くしても形も相補性も実機に届かない=**残るは筐体幾何のみ**. ### 次(実験4)— 幾何の漏れ点検 @@ -167,6 +250,8 @@ **重要な含意**: [TEST_04(実機単灯比較)](TEST_07_LEDグループ別動作確認.md) では実機の 3 灯は**相補的に異なる方向を照らす**(下=上側/左=右寄り…)とされていた.しかし**修正後シミュは 3 灯ほぼ同じ形=実機の「相補性」を再現できていない**.これは「シミュでは形が筐体で決まり LED 位置の効きが弱すぎる」ことを意味し,(a) 筐体幾何 B1 の不正確さ,または (b) シミュが間接化しすぎ(実機は各灯の直接性/指向性がもっと残る)を示唆する.実験4(幾何)の追加手がかり. +※ 上記の灯間 NCC≈0.92 は spp が低い計測でノイズにより過小だった(spp=1024 では 0.96).(b) の仮説「壁の光沢化で指向性を戻す」は実験5 で検証済み=機構は働くが実機の非対称相補性(0.85/0.61/0.41)には届かず,形も悪化するため不採用. + ## 関連ドキュメント - [TEST_08 面光源スケール軸バグの発見と修正](TEST_08_面光源スケール軸バグの発見と修正.md) — 本調査の前提(バグ修正で灯間バランスは解決済み) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 7abdb7b..a48c36d 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -68,3 +68,5 @@ - **バグの独立検証・修正前後比較・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 の追加手がかり. +- **実験5(壁の光沢化 blend BSDF)再検証完了 — 材料側レバー出尽くし・実機の非対称相補性を初定量化 (2026-07-14)**: 旧 Step 8-14 β の A1 blend 実験はバグ込み・3 灯・絶対指標のため,修正後 LED・bottom 単灯・形指標(TEST_09 流)で正式に再検証.`printer_white` を blendbsdf 化し w∈{0.05,0.15,0.3,0.6}×α∈{0.1,0.3,0.6} の 12 ジョブ(spp=1024, d=16,約 2h)+最強光沢 w0.6_a0.1 で left/right 単灯を追加レンダ.**結果 ①(平らさ)**: 全 12 ジョブが純 diffuse(peak/mean 1.585・NCC 0.721)より悪化し w に単調悪化(最悪 w0.6_a0.1 で 2.145/0.667)=光沢はホットスポットを作り実機の平らさと逆方向.**結果 ②(相補性)**: 灯間 NCC は diffuse 0.96 → blend 0.86 に低下し「Lambertian 壁が光源形状を洗い流す」機構自体は実証されたが,**実機の灯間 NCC は 0.85/0.61/0.41 と強く非対称**(今回初定量化,実験4 以降の到達目標アンカー)で,等方な材料変更では原理的に再現不能.単灯の対実機 NCC も 3 灯とも悪化するトレードオフ.**結論: 壁の反射特性は大きさ(実験2)・種類(実験5)とも本筋でなく,B1 筐体幾何が唯一の残存候補として確定**.副次修正: TEST_09 §追加観察の灯間 NCC≈0.92 は低 spp ノイズによる過小(spp=1024 で 0.96)と判明し訂正.成果物: `output/blend_bottom/`(summary.csv 含む),解析 `scripts/build_blend_bottom_panel.py`,図 `output/_slide_panels/blend_bottom_shape_roi.png`,記録 [TEST_09 §実験5](06_TEST/TEST_09_照明分布の形不一致の原因切り分け.md).次: 実験4(幾何の漏れ点検=漏れの定量化・開口の閉じテスト). + - **left/right 拡張 (2026-07-14)**: 時間短縮のため縮小グリッド w∈{0.05,0.3}×α∈{0.1,0.6}(+w0.6_a0.1)を left/right 単灯でも実走(8 ジョブ).**「w 単調悪化」は 3 灯共通**(w0.6_a0.1 で left 2.03/right 2.20 まで尖る).3 灯揃う 5 材料条件の灯間 NCC は w に対し一様に低下(0.96→0.92→0.86)するが**常に 3 ペア均等で実機の非対称(0.85/0.61/0.41)に近づかず**=「等方な材料変更は 3 ペアを一様にしか動かせない」をスイープ全域で確認.解析スクリプトは --side 引数で 3 灯対応化. diff --git a/scripts/build_blend_bottom_panel.py b/scripts/build_blend_bottom_panel.py new file mode 100644 index 0000000..a89231e --- /dev/null +++ b/scripts/build_blend_bottom_panel.py @@ -0,0 +1,328 @@ +"""TEST_09 実験5: 単灯 blendbsdf スイープ(バグ修正後再検証)の解析とパネル生成. + +sweep レンダ(output/blend_bottom/{job}/luminance.npy)の完了後に実行すること. +旧 blend 実験(scripts/sweep_blend_bsdf.py,3 灯フルシーン・絶対指標)は面光源スケール軸 +バグ込みの結果のため,本スクリプトは TEST_09 の「形」評価に統一して再検証する: + +- peak/mean(ROI) = max / mean(実機 bottom ≈ 1.218) +- top/bot(ROI) = 上半分 mean / 下半分 mean(実機 bottom ≈ 0.931) +- NCC(ROI, 対実機) = シミュ ROI を実機解像度へ resize して正規化相関(area diffuse 基準 0.720) + +対象灯は --side で選ぶ(既定 bottom=フルグリッド 4×3.left / right は時間短縮のため +縮小グリッド w{0.05, 0.3}×α{0.1, 0.6} + Phase 2 の w0.6_a0.1). + +生成するファイル: + output/blend_bottom/summary{_side}.csv -- 全ジョブの形指標 + output/_slide_panels/blend_{side}_shape_roi.png -- 輝度マップ + 指標パネル(ROI 揃え) + +使い方: + uv run python scripts/build_blend_bottom_panel.py [--side {bottom,left,right}] + +前提: + - output/blend_bottom/ に対象ジョブの luminance.npy が存在すること + - area diffuse 基準(bottom: output/depth_bottom/d16,left/right: output/sanity_fix_1024/) + - lightsim/luminance_maps/{side}_*.npy(実機単灯実測,ROI 適用済み)が存在すること +""" + +from __future__ import annotations + +import argparse +import csv +import sys +from pathlib import Path + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +from skimage.transform import resize + +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh +from sm_tias_lightsim.evaluation.shape_metrics import calc_ncc + +# 日本語ラベル用フォント(Windows 標準.無ければ DejaVu にフォールバック) +matplotlib.rcParams["font.family"] = ["Meiryo", "Yu Gothic", "DejaVu Sans"] + +# --------------------------------------------------------------------------- +# 定数 +# --------------------------------------------------------------------------- + +ROOT = Path(__file__).resolve().parents[1] + +SWEEP_BASE = ROOT / "output" / "blend_bottom" +PANEL_DIR = ROOT / "output" / "_slide_panels" +REAL_DIR = ROOT / "lightsim" / "luminance_maps" + +# 灯ごとの設定(configs/_tmp_blend_bottom/ のジョブ命名・グリッドと整合) +# baseline: area diffuse(純 Lambertian)の同条件出力. +# grid: (weights, alphas).left/right は縮小グリッド+Phase 2 の w0.6_a0.1 のみ存在し, +# 欠けセルはパネル上 [MISSING] 表示になる. +SIDES: dict[str, dict] = { + "bottom": { + "baseline": ROOT / "output" / "depth_bottom" / "d16" / "luminance.npy", + "real": REAL_DIR / "bottom_SmTIAS_QM_20260615_141420.npy", + "job_prefix": "", + "weights": [0.05, 0.15, 0.3, 0.6], + "alphas": [0.1, 0.3, 0.6], + }, + "left": { + "baseline": ROOT / "output" / "sanity_fix_1024" / "left" / "luminance.npy", + "real": REAL_DIR / "left_SmTIAS_QM_20260615_150300.npy", + "job_prefix": "left_", + "weights": [0.05, 0.3, 0.6], + "alphas": [0.1, 0.6], + }, + "right": { + "baseline": ROOT / "output" / "sanity_fix_1024" / "right" / "luminance.npy", + "real": REAL_DIR / "right_SmTIAS_QM_20260615_170948.npy", + "job_prefix": "right_", + "weights": [0.05, 0.3, 0.6], + "alphas": [0.1, 0.6], + }, +} + +# 共有 colorbar のパーセンタイル(mean=1 正規化後) +VMIN_PERCENTILE = 1.0 +VMAX_PERCENTILE = 99.0 + + +# --------------------------------------------------------------------------- +# 指標計算(TEST_09 実験1〜3 と同一定義) +# --------------------------------------------------------------------------- + + +def load_sim_roi(npy_path: Path) -> np.ndarray: + """シミュ luminance.npy を読み込み SIM_ROI で切り出す. + + Args: + npy_path: luminance.npy のパス(2D 輝度 or 3D RGB). + + Returns: + ROI 切り出し済み輝度配列(H × W, float64). + + Raises: + FileNotFoundError: ファイルが存在しない場合. + """ + if not npy_path.exists(): + msg = f"luminance.npy が見つかりません: {npy_path}" + raise FileNotFoundError(msg) + arr = np.load(npy_path) + if arr.ndim == 3: + arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2] + return extract_roi_xywh(arr, SIM_ROI) + + +def calc_shape_metrics(roi: np.ndarray, real: np.ndarray) -> dict[str, float]: + """TEST_09 の 3 形指標(peak/mean・top/bot・NCC 対実機)を算出する. + + NCC はシミュ ROI を実機解像度へ resize してから計算する + (TEST_09 掲載値と同一の手順: d16 で 0.7207 を再現確認済み). + + Args: + roi: シミュ ROI 輝度配列(H × W). + real: 実機実測輝度配列(ROI 適用済み,H' × W'). + + Returns: + {"peak_mean", "top_bot", "ncc"} の辞書. + """ + h = roi.shape[0] + sim_resized = resize(roi, real.shape, anti_aliasing=True) + return { + "peak_mean": float(roi.max() / roi.mean()), + "top_bot": float(roi[: h // 2].mean() / roi[h // 2 :].mean()), + "ncc": float(calc_ncc(sim_resized, real)), + } + + +# --------------------------------------------------------------------------- +# パネル描画 +# --------------------------------------------------------------------------- + + +def _build_panel( + side: str, + job_rois: dict[str, np.ndarray], + job_metrics: dict[str, dict[str, float]], + baseline_roi: np.ndarray, + baseline_metrics: dict[str, float], + real: np.ndarray, + real_metrics: dict[str, float], + output_path: Path, +) -> None: + """(1 + len(weights)) × len(alphas) グリッドの形比較パネルを生成・保存する. + + 先頭行に基準(area diffuse)と実機を置き,以下 weight × alpha グリッド. + 形の比較のため全画像を mean=1 に正規化し共有スケールで描画する. + + Args: + side: 対象灯("bottom" / "left" / "right").グリッド定義は SIDES から引く. + job_rois: ジョブ名 → ROI 輝度配列. + job_metrics: ジョブ名 → 形指標辞書. + baseline_roi: area diffuse 基準の ROI 輝度配列. + baseline_metrics: 基準の形指標辞書. + real: 実機実測輝度配列. + real_metrics: 実機の形指標辞書(NCC は 1.0). + output_path: 保存先 PNG パス. + """ + cfg = SIDES[side] + weights, alphas, prefix = cfg["weights"], cfg["alphas"], cfg["job_prefix"] + + normed = {k: v / v.mean() for k, v in job_rois.items()} + baseline_n = baseline_roi / baseline_roi.mean() + real_n = real / real.mean() + + n_rows = 1 + len(weights) + n_cols = max(len(alphas), 2) + fig, axes = plt.subplots( + n_rows, n_cols, figsize=(n_cols * 4.2, n_rows * 4.0), constrained_layout=True + ) + + # 各セルを自身の 1–99 パーセンタイルで独立ストレッチする(形の比較専用パネル). + # 共有スケールだとレンジの広いセルに引きずられ個々の形のコントラストが薄まる. + # 厳密 min–max でなくパーセンタイルなのは,裾の外れ画素(実機は裾 2% が + # カラーレンジの約 4 割を占有,シミュは MC ノイズ)にレンジを食われないため. + def _show(ax: plt.Axes, img: np.ndarray | None, title: str) -> None: + if img is not None: + cell_vmin = float(np.percentile(img, VMIN_PERCENTILE)) + cell_vmax = float(np.percentile(img, VMAX_PERCENTILE)) + ax.imshow(img, cmap="hot", vmin=cell_vmin, vmax=cell_vmax, aspect="equal") + else: + ax.set_facecolor("0.15") + ax.set_title(title, fontsize=9, pad=4) + ax.set_axis_off() + + def _fmt(m: dict[str, float]) -> str: + return f"p/m={m['peak_mean']:.3f} t/b={m['top_bot']:.3f} NCC={m['ncc']:.3f}" + + # 先頭行: 基準 area diffuse / 実機 / 空 + _show(axes[0][0], baseline_n, f"area diffuse(基準)\n{_fmt(baseline_metrics)}") + _show(axes[0][1], real_n, f"実機 {side}\n{_fmt(real_metrics)}") + for col_idx in range(2, n_cols): + _show(axes[0][col_idx], None, "") + + for row_idx, weight in enumerate(weights, start=1): + for col_idx, alpha in enumerate(alphas): + job = f"{prefix}w{weight}_a{alpha}" + if job in normed: + _show( + axes[row_idx][col_idx], + normed[job], + f"w={weight}, α={alpha}\n{_fmt(job_metrics[job])}", + ) + else: + _show(axes[row_idx][col_idx], None, f"w={weight}, α={alpha}\n[MISSING]") + for col_idx in range(len(alphas), n_cols): + _show(axes[row_idx][col_idx], None, "") + + fig.suptitle( + f"TEST_09 実験5 — {side} 単灯 blend BSDF 再検証(バグ修正後, spp=1024, d=16, ROI 揃え)\n" + f"各セル独立 1–99% 正規化(形の比較専用・明るさの絶対比較は不可)/ " + f"実機目標: peak/mean={real_metrics['peak_mean']:.3f}, " + f"top/bot={real_metrics['top_bot']:.3f}", + fontsize=11, + y=1.03, + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"パネル保存: {output_path}") + + +# --------------------------------------------------------------------------- +# メイン処理 +# --------------------------------------------------------------------------- + + +def main() -> int: + """解析とパネル生成のメインエントリポイント. + + Returns: + 終了コード(成功なら 0,入力不足なら 1). + """ + parser = argparse.ArgumentParser(description="単灯 blend BSDF スイープの形指標解析とパネル生成") + parser.add_argument( + "--side", + choices=sorted(SIDES), + default="bottom", + help="対象灯(既定: bottom)", + ) + args = parser.parse_args() + side = args.side + cfg = SIDES[side] + weights, alphas, prefix = cfg["weights"], cfg["alphas"], cfg["job_prefix"] + + real_npy: Path = cfg["real"] + if not real_npy.exists(): + print(f"エラー: 実機 npy が見つかりません: {real_npy}", file=sys.stderr) + return 1 + real = np.load(real_npy).astype(np.float64) + real_metrics = { + "peak_mean": float(real.max() / real.mean()), + "top_bot": float(real[: real.shape[0] // 2].mean() / real[real.shape[0] // 2 :].mean()), + "ncc": 1.0, + } + + baseline_roi = load_sim_roi(cfg["baseline"]) + baseline_metrics = calc_shape_metrics(baseline_roi, real) + + job_rois: dict[str, np.ndarray] = {} + job_metrics: dict[str, dict[str, float]] = {} + for weight in weights: + for alpha in alphas: + job = f"{prefix}w{weight}_a{alpha}" + try: + roi = load_sim_roi(SWEEP_BASE / job / "luminance.npy") + except FileNotFoundError: + print(f" 警告: {job} 未完了(スキップ)") + continue + job_rois[job] = roi + job_metrics[job] = calc_shape_metrics(roi, real) + + if not job_rois: + print("エラー: 有効なジョブが 1 件もありません.", file=sys.stderr) + return 1 + + # summary CSV(bottom は既存名 summary.csv を維持) + summary_csv = SWEEP_BASE / ("summary.csv" if side == "bottom" else f"summary_{side}.csv") + summary_csv.parent.mkdir(parents=True, exist_ok=True) + with summary_csv.open("w", encoding="utf-8", newline="") as f: + writer = csv.writer(f) + writer.writerow(["job", "weight", "alpha", "peak_mean", "top_bot", "ncc"]) + writer.writerow( + ["area_diffuse", "", "", *(f"{baseline_metrics[k]:.4f}" for k in + ("peak_mean", "top_bot", "ncc"))] + ) + for weight in weights: + for alpha in alphas: + job = f"{prefix}w{weight}_a{alpha}" + if job in job_metrics: + m = job_metrics[job] + writer.writerow( + [job, weight, alpha, f"{m['peak_mean']:.4f}", + f"{m['top_bot']:.4f}", f"{m['ncc']:.4f}"] + ) + print(f"summary 保存: {summary_csv}") + + # コンソール表 + print(f"\n{'job':<22}{'peak/mean':>10}{'top/bot':>10}{'NCC':>8}") + print(f"{'area diffuse':<22}{baseline_metrics['peak_mean']:>10.3f}" + f"{baseline_metrics['top_bot']:>10.3f}{baseline_metrics['ncc']:>8.3f}") + for weight in weights: + for alpha in alphas: + job = f"{prefix}w{weight}_a{alpha}" + if job in job_metrics: + m = job_metrics[job] + print(f"{job:<22}{m['peak_mean']:>10.3f}{m['top_bot']:>10.3f}{m['ncc']:>8.3f}") + print(f"{'実機 ' + side:<22}{real_metrics['peak_mean']:>10.3f}" + f"{real_metrics['top_bot']:>10.3f}{'-':>8}") + + panel_path = PANEL_DIR / f"blend_{side}_shape_roi.png" + _build_panel( + side, job_rois, job_metrics, baseline_roi, baseline_metrics, real, real_metrics, + panel_path, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/build_blend_bsdf_panel.py b/scripts/build_blend_bsdf_panel.py index db92d5d..47098a2 100644 --- a/scripts/build_blend_bsdf_panel.py +++ b/scripts/build_blend_bsdf_panel.py @@ -164,14 +164,6 @@ job_metrics: job 名 → metrics 辞書. output_path: 保存先 PNG パス. """ - # 共有 vmin/vmax: 全 9 ジョブ + ベースラインの 1%/99% パーセンタイル - all_vals = np.concatenate( - [baseline_roi.ravel()] - + [img.ravel() for img in job_images.values()] - ) - vmin = float(np.percentile(all_vals, VMIN_PERCENTILE)) - vmax = float(np.percentile(all_vals, VMAX_PERCENTILE)) - n_rows = len(WEIGHTS) n_cols = len(ALPHAS) @@ -192,7 +184,11 @@ cov_str = f"{float(m.get('cov', 0)):.4f}" if m.get("cov") else "N/A" grad_val = m.get("gradient_magnitude") grad_str = f"{float(grad_val):.2f}%" if grad_val else "N/A" - ax.imshow(roi, cmap="hot", vmin=vmin, vmax=vmax, aspect="auto") + # 各セル独立の 1–99 パーセンタイルでストレッチ(形の比較専用. + # 共有スケールだとレンジの広いセルに引きずられ形のコントラストが薄まる) + cell_vmin = float(np.percentile(roi, VMIN_PERCENTILE)) + cell_vmax = float(np.percentile(roi, VMAX_PERCENTILE)) + ax.imshow(roi, cmap="hot", vmin=cell_vmin, vmax=cell_vmax, aspect="equal") title = f"w={weight}, α={alpha}\nCoV={cov_str} grad={grad_str}" else: # ジョブが失敗していた場合は空セル @@ -202,21 +198,10 @@ ax.set_title(title, fontsize=9, pad=4) ax.set_axis_off() - # 共有 colorbar - fig.colorbar( - plt.cm.ScalarMappable( - norm=plt.Normalize(vmin=vmin, vmax=vmax), cmap="hot" - ), - ax=axes, - fraction=0.015, - pad=0.02, - label="Luminance (mean-anchored)", - ) - fig.suptitle( - f"Step 8-14 blend BSDF sweep — luminance maps\n" - f"shared vmin/vmax = [{vmin:.2f}, {vmax:.2f}] (1%–99% of all 9 jobs + baseline)\n" - f"rows=weight (specular ratio), cols=alpha (GGX roughness)", + "Step 8-14 blend BSDF sweep — luminance maps\n" + "per-cell 1%-99% normalization (shape comparison only; no absolute scale)\n" + "rows=weight (specular ratio), cols=alpha (GGX roughness)", fontsize=10, y=1.01, )