diff --git a/.gitignore b/.gitignore index 7839521..bf6eadd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ # シミュ・評価結果(コミット対象外) output/ +# docs 下の生成画像(再生成可能なので追跡しない.生成スクリプトは追跡する) +docs/**/figures/*.png + # configs/ 配下の生成物(シーン YAML 本体と STL は追跡対象) configs/*.png configs/*.npz diff --git a/CLAUDE.md b/CLAUDE.md index c48ac31..c7c2f95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,7 @@ 既知の制約(受容済み): per-face ベイクはメッシュ境界の継ぎ目・粗メッシュのブロック感・低 samples の MC ノイズ・scalar_rgb の遅さ(高 samples 非現実的)あり.精細な評価は 2D `sm-evaluate` で行う(不変).隣接内壁の見かけ段差は調査の結果「外向き ≈0 面の混在」が主因と判明=幾何カリングで解消(内壁照度自体は物理どおりほぼ均一). 次のアクション(可視化・コミット後の別タスク): シーム/粒状感の解消には全メッシュ UV 統合+シームレスライトマップベイク(別プロジェクト規模).必要時に別途 `/implement`.path ベイク高速化(wavefront/GPU)も別タスク. 補足: **実機搭載 LED の物性確定**(LED Paradise LP-2835D90VW / Ra98 SunLightLED 2835 4000K、Vf 3.1V・If 60mA・Φv 16–19 lm @60mA・配光角 120°・規格通り 60mA 駆動前提).規格値から独立推定した radiance ≈ 2030 W/(m²·sr) は現状校正値 2807 と 1.38× 差で、反射率 0.85→0.89 への調整で物理範囲内に説明可能.既知差分 #3(LED 実 W 値未検証)クローズ.**形状指標への影響なし**(線形ゲイン).配光は厳密 Lambertian 仮定継続(詳細 IES 未提供、実 SMD 2835 のやや前方凸性は ISP LSC と打ち消し合う近似として現状妥当).詳細: [SPEC_03 LED セクション](docs/04_SPEC/SPEC_03_基準設計の物理構成.md#led光源). +補足: **輝度マップ「形」の追跡を導入**([TEST_01 輝度マップ形状の追跡セクション](docs/06_TEST/TEST_01_キャリブレーション検証履歴.md#輝度マップ形状の追跡-luminance-map-shape-tracking) + `scripts/build_luminance_evolution_panel.py`).重要な発見: **これまでの全 7 改善ステップ (8-1〜8-7) で輝度マップの「形」は全く変わっていない**.OETF は単調変換で形を変えず,発光面サイズ変更も形を変えない.**実機は中央付近にピークを持つ楕円状,シミュは下端のみ明の Y 軸単調勾配**.**LSC モデル化は「数値残差縮小」と「形の構造的改善」の両方を担う唯一の改善候補**.以後の改善判断は「数値 + 形」の 2 軸で行う. 次のアクション(キャリブ再開時): 残差に **LSC(ISP レンズシェーディング)順方向モデル**を当てる(物理動機づけ低自由度形、overfitting 回避・取得パラメータ数明記). 実装済みモジュール: 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 93be200..9acbb54 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" @@ -287,6 +287,43 @@ - **形状指標と絶対値は別物**として扱う.多くのレバーは形状指標には不変(線形ゲイン)だが,絶対値は局所幾何との縮退で動きうる. - **失敗・予想外の記録は学びの宝庫**.方向性ドリフトを防ぐ核は「失敗を消さずに残す」こと. +## 輝度マップ形状の追跡 (Luminance Map Shape Tracking) + +数値指標 (CoV / 勾配 / C/P) は分布のばらつきを集約する量であり,**「形そのもの」は捕えない**.シミュレーションの「形」を実機に近づける観点は,指標残差の縮小とは別軸で追跡する必要がある. + +### 比較パネル + +![輝度マップ形状の追跡: 実機 (TARGET) と Step 8-1 / 8-3 / 8-7 の比較](figures/luminance_map_evolution.png) + +### 観察された事実 + +実機 vs シミュ全 Step を並べて見て判明したこと: + +- **実機 (TARGET)**: ROI 中央〜下に明るい楕円状の大きな領域.左右にほぼ対称.カメラ系の異方性 (光学系の周辺減光と ISP LSC 補正残差) に支配される典型パターン. +- **Step 8-1 (出発点, linear)**: ROI 下端のみ明るく,上端に向かって単調に減衰する **Y 軸単調勾配**.これは LED 配置の物理的事実 (下基板が Y=-70.2 にあって上方向に対称な基板がない) を素直に反映している. +- **Step 8-3 (OETF 適用後, sRGB)**: 数値指標は半減 (CoV 0.183 → 0.091) したが,**輝度マップの形は出発点と区別がつかない**.OETF はピクセル値の単調変換でしかなく,**ピクセル間の相対順序を変えない**ため形は構造的に不変. +- **Step 8-7 (発光面 size 1.5×1.5, linear)**: 絶対値スケールは 2.83× 上昇したが,**形は出発点と同じ Y 軸単調勾配**.形状指標がほぼ不変だったことと整合. + +### 学び (新規) + +- **数値の縮小 ≠ 形の改善**.Step 8-3 OETF は「指標数値の符号化ドメイン不一致」を解消したのみで,**シミュの形は実機の形に近づいていない**. +- これまで実施した 7 つの改善ステップ (8-1〜8-7) は **すべて「形を変えない変更」**だった: spp,反射率,OETF,max_depth,可視化,LED 物性,発光面サイズ.これらは線形ゲイン・単調変換・局所幾何依存のいずれかであり,**「ピクセル間の空間構造」を変える物理を含まない**. +- 実機の形 (中央付近にピークを持つ楕円状) = **シミュの Y 軸勾配** + **カメラ系の動径補正パターン** の合成として説明可能.シミュ側にこの「動径補正」を加えない限り,形を実機に合わせることはできない. + +### LSC モデル化の意義 (再認識) + +[Step 8-N 候補 (LSC 順方向モデル)](#step-8-n-候補-lsc-順方向モデル未着手) は,単に「指標残差 1.6〜1.9× を縮める」だけでなく,**「シミュの空間勾配パターンに動径補正を重ね,実機の中央ピーク形状を作る」根本的な構造的改善**である.これは数値指標の改善より重要な観点で,**以後の改善判断はこの 2 軸 (数値 + 形) で行う**. + +### 再現コマンド + +新規 Step 完了後はこの比較パネルを再生成して形の変化を追跡する: + +```powershell +uv run python scripts/build_luminance_evolution_panel.py +``` + +新規 Step を追加するには `scripts/build_luminance_evolution_panel.py` の `PANELS` リストに 1 エントリ追加するだけで自動で並ぶ. + ## 参照 (References) - [SPEC_03_基準設計の物理構成](../04_SPEC/SPEC_03_基準設計の物理構成.md) @@ -301,4 +338,7 @@ - `configs/eval_baseline_linear.yaml`(Step 8-7) - `output/emitter_real/`(Step 8-7 検証レンダ) - `output/baseline_linear_npy/`(Step 8-7 比較基準) +- 輝度マップ形状の追跡: + - `scripts/build_luminance_evolution_panel.py`(比較パネル生成スクリプト) + - `docs/06_TEST/figures/luminance_map_evolution.png`(生成された比較パネル画像) - 関連コミット: `26904da`(OETF 正規実装),`7181efa` / `c32ccd6` / `a0fe07d`(3D ビュー改善),`f5048a4`(LED 物性反映) diff --git a/scripts/build_luminance_evolution_panel.py b/scripts/build_luminance_evolution_panel.py new file mode 100644 index 0000000..bf21c4b --- /dev/null +++ b/scripts/build_luminance_evolution_panel.py @@ -0,0 +1,109 @@ +"""TEST_01 用「輝度マップ形状の追跡」比較パネルを生成する. + +各改善 Step での輝度マップの「形」を 2×2 グリッドで並べ, +タイトルに主要指標 (mean / CoV / 勾配 % / C/P) を併記する. + +形を見せる目的なので各画像は個別正規化 (sm-evaluate デフォルト) のまま使う. +共有 colorbar にすると絶対値の差にばかり目が行き「形の異同」が見えにくくなる. + +使い方: + uv run python scripts/build_luminance_evolution_panel.py + +出力: + docs/06_TEST/figures/luminance_map_evolution.png + +新しい改善 Step を追加する場合は PANELS リストに 1 エントリ追加するだけ. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import matplotlib.image as mpimg +import matplotlib.pyplot as plt + +ROOT = Path(__file__).resolve().parents[1] + + +def _load_metrics(csv_path: Path) -> dict[str, float]: + """sm-evaluate が書き出す metrics.csv から主要指標を取り出す.""" + with csv_path.open(encoding="utf-8") as f: + row = next(csv.DictReader(f)) + return { + "mean": float(row["mean"]), + "CoV": float(row["cov"]), + "grad": float(row["gradient_magnitude"]), + "CP": float(row["center_periphery_ratio"]), + } + + +# パネル定義. ここに新規 Step を追加すれば自動で並ぶ. +PANELS = [ + { + "image": ROOT / "MiniTias-Evaluation" / "figures" / "luminance_map_sample.png", + "title": "Real Measurement (TARGET)", + "subtitle": "central peak, near-symmetric (camera optics anisotropy)", + "metrics": {"mean": 146.71, "CoV": 0.0427, "grad": 6.10, "CP": 1.065}, + }, + { + "image": ROOT / "output" / "baseline_linear_npy" / "figures" / "luminance_map.png", + "title": "Step 8-1: Starting Point", + "subtitle": "R=0.85, d=8, spp=1024, linear (no OETF)", + "metrics_csv": ROOT / "output" / "baseline_linear_npy" / "metrics.csv", + }, + { + "image": ROOT / "output" / "eval_oetf_r085" / "figures" / "luminance_map.png", + "title": "Step 8-3: After OETF Forward Application", + "subtitle": "R=0.85, d=8, spp=1024, sRGB domain", + "metrics_csv": ROOT / "output" / "eval_oetf_r085" / "metrics.csv", + }, + { + "image": ROOT / "output" / "emitter_real" / "figures" / "luminance_map.png", + "title": "Step 8-7: Real-chip Emitter Size (Φe preserved)", + "subtitle": "size 1.5x1.5 mm, linear", + "metrics_csv": ROOT / "output" / "emitter_real" / "metrics.csv", + }, +] + + +def main() -> None: + fig, axes = plt.subplots(2, 2, figsize=(14, 13)) + for ax, panel in zip(axes.flatten(), PANELS): + img_path = panel["image"] + if not img_path.is_file(): + ax.text(0.5, 0.5, f"MISSING\n{img_path}", ha="center", va="center") + ax.set_axis_off() + continue + ax.imshow(mpimg.imread(img_path)) + ax.set_axis_off() + + metrics = panel.get("metrics") + if metrics is None and "metrics_csv" in panel: + metrics = _load_metrics(panel["metrics_csv"]) + + m = metrics + title = ( + f"{panel['title']}\n" + f"{panel['subtitle']}\n" + f"mean={m['mean']:.2f} CoV={m['CoV']:.4f} " + f"grad={m['grad']:.2f}% C/P={m['CP']:.3f}" + ) + ax.set_title(title, fontsize=10, loc="left") + + fig.suptitle( + "Luminance Map Shape Tracking across Calibration Steps", + fontsize=14, + fontweight="bold", + ) + plt.tight_layout(rect=(0, 0, 1, 0.97)) + + out_dir = ROOT / "docs" / "06_TEST" / "figures" + out_dir.mkdir(parents=True, exist_ok=True) + out_path = out_dir / "luminance_map_evolution.png" + plt.savefig(out_path, dpi=120, bbox_inches="tight") + print(f"Saved: {out_path}") + + +if __name__ == "__main__": + main()