# 評価仕様 (Evaluation Specification)

`sm-evaluate` CLI が担う **シミュレーション結果 / 実測 PNG の照明均一性評価** の方法論を定義する．本書はアルゴリズム・物理動機・規約をまとめ，実装の根拠（why）と入出力（what）を記述する．評価設定 YAML のスキーマ詳細は [SPEC_02_シーン定義仕様](SPEC_02_シーン定義仕様.md) の `evaluation` セクションを参照．

関連:

- 評価結果の歴史: [TEST_01_キャリブレーション検証履歴](../06_TEST/TEST_01_キャリブレーション検証履歴.md)
- 物理シーン仕様: [SPEC_03_基準設計の物理構成](SPEC_03_基準設計の物理構成.md)
- 評価 YAML スキーマ: [SPEC_02_シーン定義仕様 §evaluation](SPEC_02_シーン定義仕様.md)
- 実装エントリ: `src/sm_tias_lightsim/cli/evaluate.py`, `src/sm_tias_lightsim/evaluation/`, `src/sm_tias_lightsim/uniformity.py`

## 概要 (Overview)

シミュレーション結果（`output/<run>/luminance.npy` または `render_uint8.png`）と実測 PNG (`reference_measurement/.../*.png`) を **同一の評価パイプライン**に通し，無次元の均一性指標（CoV，勾配 %，中心/周辺比 等）を算出する．シミュと実測を比較可能にする鍵は **OETF 順方向適用** で，シミュの linear ドメインを実機の sRGB 符号化ドメインに揃える操作である（後述）．

設計目的は MiniTias-Evaluation との **数値整合性**（実測 PNG に対し小数点 14 桁レベルで一致確認済み，[TEST_01](../06_TEST/TEST_01_キャリブレーション検証履歴.md) 既知差分 #9）と，シミュ⇄実測の **公平な比較**．

## 評価パイプライン (Evaluation Pipeline)

```text
入力 (PNG / luminance.npy)
    │
    ▼  uint8 PNG → 0..255 配列, luminance.npy → float32 配列のまま
[1] 読み込み
    │
    ▼  Rec.709 加重和 Y = 0.2126R + 0.7152G + 0.0722B (RGB 入力時)
[2] luma 変換
    │
    ▼  ROI 設定 (image_pixel 型) に従い (x, y, width, height) 領域を切り出す
[3] ROI 抽出
    │
    ▼  oetf.enabled = true のとき: 標準 sRGB OETF を順方向適用 + 実測 mean アンカー
[4] OETF 順方向適用 (任意・シミュ用)
    │
    ▼  (任意) quantize_uint8: float → uint8 量子化
[5] 量子化
    │
    ▼  CoV, std, max-min, mean, max, min を計算
[6] 全体指標
    │
    ▼  3 ゾーン分類 (center/middle/periphery) → 中心/周辺比，勾配 %
[7] ゾーン指標
    │
    ▼  20 ビンの放射状プロファイル
[8] 動径指標
    │
    ▼  metrics.csv, figures/{luminance_map, histogram, radial_profile, zone_map}.png
[9] 出力
```

各ステージの実装は以下のモジュールに分かれる:

| ステージ | モジュール |
| --- | --- |
| [1] 読み込み | `cli/evaluate.py`（PNG: PIL，npy: numpy） |
| [2] luma 変換 | `uniformity.py`（Rec.709 加重和） |
| [3] ROI 抽出 | `evaluation/roi.py` |
| [4] OETF | `evaluation/oetf.py` |
| [5-7] 指標 | `uniformity.py` + `evaluation/zones.py` |
| [8] 動径 | `evaluation/radial.py` |
| [9] 可視化 | `evaluation/visualize.py` |

## ROI 仕様 (Region of Interest)

評価板の **撮影範囲内の有効領域** を矩形で切り出す．評価設定 YAML の `roi` セクションで指定する．

### `image_pixel` 型

実測 PNG の座標系（左上原点，X 右，Y 下）で **絶対ピクセル座標**を直接指定する．シミュ画像と実測画像で **物理対応する同じ領域**を切り出すために，シミュ側の ROI は実測 ROI の物理対応関係から決定する．

```yaml
roi:
  type: image_pixel
  x: 24          # 左上 X
  y: 37          # 左上 Y
  width: 464
  height: 450
```

### 実機 ⇄ シミュの物理対応（既存値）

| ROI 名 | 用途 | x, y, w, h |
| --- | --- | --- |
| `configs/eval_baseline.yaml` | シミュ 512×512 | 24, 37, 464, 450 |
| `configs/eval_real.yaml` | 実機 1500×1500 ベース | reference_measurement の `roi_config.json` 参照 |

シミュ ROI と実機 ROI は **物理対応済み**（評価板の同じ XY 領域）．シミュの orthographic センサと実機カメラの向き変換は [SPEC_03 画像 ↔ 物理 の向き](SPEC_03_基準設計の物理構成.md#画像--物理-の向き検証済み) を参照．

## OETF 順方向適用の仕様 (Forward OETF Application)

**最重要セクション**．シミュ linear 結果と実測 sRGB PNG を直接比較すると CoV や勾配の絶対値は不一致になる．これを **データに合わせて回す係数ゼロ**で解消するのが OETF 順方向適用．

### 物理動機

- 実機 PNG は ISP 内で **sRGB OETF (IEC 61966-2-1)** が掛かった非線形符号化値．Camera2 `YUV_420_888` → BT.601 は定義上ガンマ済み R'G'B' 上の量．
- シミュは Mitsuba の **linear 放射輝度**．
- **無次元指標 (CoV / 勾配 / C/P) は線形変換に対してのみ不変**．非線形 OETF を介すと値が変わる．
- 両者を同じドメイン（gamma 空間 luma）に揃えれば，「物理が同じなら指標も同じ」が成り立つ．

### 標準 OETF（IEC 61966-2-1）

```text
        ┌─ 12.92 · L                       (L ≤ 0.0031308)
V(L) = ─┤
        └─ 1.055 · L^(1/2.4) − 0.055       (L  > 0.0031308)
```

`L` は linear（0..1 正規化），`V` は sRGB 符号化値（0..1）．実装は `evaluation/oetf.py` の `apply_srgb_oetf()`．

### 露出アンカー（mean 一致）

露出（線形ゲイン）は AE が制御する量で，シミュ側で任意に決められない．**実測 ROI mean に対数二分でアンカー**する：

1. シミュ linear ROI 全体に係数 `α` を掛ける
2. OETF を順方向適用し uint8 量子化
3. 量子化後 mean が実測 mean (`146.7146`) に一致する `α` を対数二分で求める
4. 線形ゲインなので **形状指標 (CoV / 勾配 / C/P) は α に対し不変**．データに合わせて回す自由度はゼロ．

### 二重適用禁止規則

- **実測 PNG には絶対に OETF を適用しない**．ISP で既に sRGB 符号化されているため二重適用になる．
- `configs/eval_real.yaml` は `oetf` セクション無し（無効）．`configs/eval_baseline.yaml` は `oetf.enabled: true`（シミュ用）．
- スキーマ詳細は [SPEC_02 §evaluation](SPEC_02_シーン定義仕様.md) 参照．

### luma vs luminance と演算順序の整合

実測 MiniTias-Evaluation の「輝度値」は厳密には **Rec.709 luma Y'**（ガンマ済み成分への加重和 `Y' = 0.2126R' + 0.7152G' + 0.0722B'`，ガンマを戻さない）．シミュ側は linear luminance に直接 OETF を当てている．OETF は非線形なので一般に `Σwᵢ·f(xᵢ) ≠ f(Σwᵢ·xᵢ)` だが，**矛盾しない**:

- Rec.709 重みは合計 1 (0.2126 + 0.7152 + 0.0722 = 1.0000)．
- **R=G=B=x の無彩色画素**では「チャネル別 OETF→加重和」`Σwᵢ·f(x) = f(x)` も「加重和→OETF」`f(x·Σwᵢ) = f(x)` も**厳密に一致**．
- シミュは白色 LED（RGB 等価 radiance）＋無彩色拡散材で厳密に R=G=B → 「luminance に直接 OETF」 ＝ 「実測の演算順（チャネル別 OETF → Rec.709 加重和）」と数値同値．
- **本質: OETF は不整合を作る操作ではなく消す操作**．OETF 前のシミュ＝linear 空間 luma，実測＝gamma 空間 luma で別ドメイン（真の不整合）．OETF 後は両者とも gamma 空間 luma になり，突合が正当化される．

### 残る近似（モデル誤差として正直に保持）

1. 無彩色等価が厳密なのはシミュ側のみ．実測白板画像は LED 分光・ISP ホワイトバランス・センサ応答で微小に非グレー（R'≈G'≈B' が完全でない分の luma vs luminance 差）．
2. カメラ実 OETF ≠ 厳密 sRGB（独自トーンマップ）．
3. ISP レンズシェーディング補正の残差（[TEST_01](../06_TEST/TEST_01_キャリブレーション検証履歴.md) 既知差分 #6，未対処）．

### 量子化（任意）

`oetf.quantize_uint8: true` で float → uint8 量子化を施す．実測 PNG が uint8 のため，公平比較には量子化を入れる．量子化ノイズ ≈ ±0.5 単位は無次元指標にほぼ影響しない（spp=1024 の MC ノイズの方が大きい）．

## メトリクス定義 (Metrics Definitions)

ROI 内の輝度配列 `Y` を入力に，以下を算出．計算式は `uniformity.py` 実装と一致．

### 全体指標

| 指標 | 記号 | 定義 | 物理的意味 |
| --- | --- | --- | --- |
| 平均 | mean | `Y.mean()` | ROI 平均輝度（露出に依存） |
| 標準偏差 | std | `Y.std(ddof=0)` | ばらつきの絶対量 |
| 変動係数 | **CoV** | `std / mean` | **無次元のばらつき**．均一性の主要指標 |
| max/min 比 | max-min | `Y.max() / Y.min()` | 最大/最小比．外れ値に敏感（MC ノイズ感度高） |

### ゾーン分類 (3 zones)

ROI 中心からの **正規化楕円距離** `d` に基づき分類:

```text
d = sqrt( ((x - cx) / (W/2))² + ((y - cy) / (H/2))² )
```

| ゾーン | 範囲 |
| --- | --- |
| center | `d < 0.33` |
| middle | `0.33 ≤ d < 0.66` |
| periphery | `0.66 ≤ d` |

### ゾーン指標

| 指標 | 定義 | 物理的意味 |
| --- | --- | --- |
| **中心/周辺比 (C/P)** | `center_mean / periphery_mean` | 中心と周辺の明るさ比 (>1 は中心優位) |
| **勾配 %** | `(center_mean − periphery_mean) / center_mean × 100` | 中心からの相対的明るさ低下率 |

ゾーン境界 (0.33, 0.66) は評価 YAML の `zones.boundaries` で変更可．

### 動径プロファイル

正規化距離 `d` を **20 ビン**（既定）に分け，各ビンの平均輝度を取る．`figures/radial_profile.png` でグラフ化．

## 可視化規約 (Figure Conventions)

`sm-evaluate` は `figures/` に以下 4 種を生成．MiniTias-Evaluation 互換スタイル．

| ファイル | 内容 | カラーマップ・スタイル |
| --- | --- | --- |
| `luminance_map.png` | ROI 輝度マップ（X-Y） | `hot` cmap，個別正規化（min-max を自動），カラーバー付き |
| `histogram.png` | 輝度ヒストグラム | 256 bin，ROI 全体 |
| `radial_profile.png` | 動径プロファイル | 20 ビン平均輝度の line plot |
| `zone_map.png` | 3 ゾーン分類マップ | luminance 背景 + 境界線（楕円） |

実装: `evaluation/visualize.py`．

## 出力ファイル仕様 (Output Files)

`sm-evaluate --output <dir>/` で以下を生成:

```text
<dir>/
├── metrics.csv          # 1 行 ヘッダ + 1 行 値（評価結果サマリ）
└── figures/
    ├── luminance_map.png
    ├── histogram.png
    ├── radial_profile.png
    └── zone_map.png
```

### metrics.csv カラム定義（ヘッダ順）

```text
cov, std, max_min_ratio, mean, max, min,
center_mean, center_std, middle_mean, middle_std, periphery_mean, periphery_std,
center_periphery_ratio, gradient_magnitude
```

すべて float．`gradient_magnitude` は上記「勾配 %」と同じ値．

## キャリブレーション目標 (Calibration Target)

[PLAN_02_開発ステップ](../03_PLAN/PLAN_02_開発ステップ.md) より，実測 baseline 1 枚 (`MiniTIAS_20260408_144434`) に対する数値:

| 指標 | 実測値 | 仮置き許容範囲 |
| --- | --- | --- |
| CoV | 0.0426 | ±0.005 |
| 中心/周辺比 | 1.064 | ±0.02 |
| 勾配 (%) | 6.23 | ±1.5 |

シミュ側はこれらの値に **数値だけでなく形（輝度マップ分布）も**近づけることが目標．形の追跡は [TEST_01 輝度マップ形状の追跡](../06_TEST/TEST_01_キャリブレーション検証履歴.md#輝度マップ形状の追跡-luminance-map-shape-tracking) を参照．

## キャリブレーション経過（要約） (Calibration Progression Summary)

Step 8 で実施した検証イテレーションの**結論サマリ**．各 Step の仮説・変更・数値・解釈・学びの**詳細**は [TEST_01_キャリブレーション検証履歴](../06_TEST/TEST_01_キャリブレーション検証履歴.md) を参照．

### 確定基準

- 設定: `R=0.85, max_depth=8, spp=1024, OETF=on`
- 評価: sRGB ドメイン（OETF 順方向適用後の uint8 量子化値で比較）
- 残差: CoV 0.089 vs 実測 0.0427 = **2.1×**（初期 spp=128 時の 5.5× から大幅縮小）

### 寄与順（訂正済み）

> **OETF（最大・約 2/3，解消済み）＞ ISP LSC（残差主因候補・未対処）＞ 反射率（弱いレバー）**

旧版「主因は ISP レンズシェーディング (83%)」は OETF を抜かした交絡判断で誤りだった．OETF 適用後で寄与順を訂正．

### 主要な発見（時系列）

| Step | 内容 | 主要な学び |
| --- | --- | --- |
| 8-1 | spp 校正 128→1024 | spp は CoV 0.183→0.174 で頭打ち．残差はノイズではなく物理勾配． |
| 8-2 | 反射率スイープ (0.75〜0.92) | 弱い二次レバー（∂勾配/∂R ≈ -38%/単位）．これ単独では差を閉じられない． |
| 8-3 | **OETF 順方向適用** | パラメータフリーで**見かけ差の 2/3 を解消**．非線形変換下では無次元指標は不変ではない． |
| 8-4 | max_depth 検証 (8→12) | depth=8 は勾配を過大評価（-2.6pt/linear）．確定 YAML は 8 のまま，3D ベイクは独立化． |
| 8-5 | 3D 可視化改善 | 「見え方」と「物理」を厳格に分離．同手法で計算すれば物理は均一． |
| 8-6 | LED 物性確定 (LP-2835D90VW) | 規格推定 radiance 2030 vs 校正値 2807 = 1.38× 差は反射率 0.89 で説明可能（既知差分 #3 クローズ）． |
| 8-7 | 発光面サイズ検証 (2.8×3.5 vs 1.5×1.5) | 形状指標は不変（点光源近似実証），絶対値は LED 周辺幾何と縮退．**校正値は局所幾何依存と理解する**． |

### 次のアクション

残差 1.6〜1.9× に対する **LSC（ISP レンズシェーディング）順方向モデル**の設計・実装．物理動機づけ低自由度形（cos⁴ ベース等）に限定し overfitting を回避．詳細は [TEST_01 Step 8-N 候補](../06_TEST/TEST_01_キャリブレーション検証履歴.md#step-8-n-候補-lsc-順方向モデル未着手) 参照．

反射率は均一性からは一意に定まらない（物理範囲内で縮退）ため暫定 0.85 据え置き．実フィラメント分光反射率データ入手時に再校正．

## 参照 (References)

- [SPEC_02_シーン定義仕様 §evaluation](SPEC_02_シーン定義仕様.md): 評価 YAML スキーマ
- [SPEC_03_基準設計の物理構成](SPEC_03_基準設計の物理構成.md): シーン物理仕様
- [TEST_01_キャリブレーション検証履歴](../06_TEST/TEST_01_キャリブレーション検証履歴.md): 評価値の歴史と検証経緯
- [TEST_02_シミュレーション実行記録](../06_TEST/TEST_02_シミュレーション実行記録.md): output/ 索引
- [TEST_03_輝度マップ形状追跡](../06_TEST/TEST_03_輝度マップ形状追跡.md): 形を実機に近づける観点
- [PLAN_02_開発ステップ](../03_PLAN/PLAN_02_開発ステップ.md): 実測ターゲット出典
- 実装エントリ:
  - `src/sm_tias_lightsim/cli/evaluate.py`: CLI
  - `src/sm_tias_lightsim/uniformity.py`: Rec.709 luma + CoV/std/max-min
  - `src/sm_tias_lightsim/evaluation/oetf.py`: 標準 sRGB OETF + 露出アンカー
  - `src/sm_tias_lightsim/evaluation/roi.py`: ROI 抽出
  - `src/sm_tias_lightsim/evaluation/zones.py`: 3 ゾーン分類 + C/P + 勾配 %
  - `src/sm_tias_lightsim/evaluation/radial.py`: 動径プロファイル
  - `src/sm_tias_lightsim/evaluation/visualize.py`: 4 種 figures 生成
