# SmTIAS-Evaluation DNG 対応要求仕様 (DNG Support Specification for SmTIAS-Evaluation)

> **本ドキュメントについて（取り込み注記）**: SmTIAS-LightSim プロジェクトから受領した DNG 対応要求仕様（先方での名称は TECH_05）を，本プロジェクトの命名規則（GUIDE_03）に従い `docs/05_TECH/TECH_01_DNG対応要求仕様.md` として取り込んだもの．文中の他ドキュメント参照（TECH_01/03/04・TEST_01・SPEC_04 等）は **SmTIAS-LightSim 側のドキュメント**を指し，本プロジェクト内には存在しない．本プロジェクトでの対応状況は `docs/PROGRESS.md` を参照．

本ドキュメントは SmTIAS-LightSim プロジェクトから **SmTIAS-Evaluation** プロジェクトに対する **DNG 対応要求仕様** である．SmTIAS 側で実装された定量撮影モード（[TECH_01](TECH_01_定量撮影モード要求仕様.md) / [TECH_03](TECH_03_定量モード実装報告とSHADING_MODE相談.md)）の出力フォーマットが PNG から DNG (RAW_SENSOR) に変更されたことに伴い，SmTIAS-Evaluation 側にも DNG 読み込み・解析機能の追加を依頼する．

## 想定読者 (Intended Reader)

- **SmTIAS-Evaluation 側 Claude セッション**: 本ドキュメントを最初に読んで作業着手する
- **人間レビュー (rinto)**: 両セッションを橋渡しする

## 概要 (Overview)

SmTIAS-Evaluation（[SmTIAS-Evaluation](https://github.com/rintoHasegawa/SmTIAS-Evaluation) リポジトリ）に以下の機能追加を要求する：

1. **DNG 読み込み I/F** の新規実装（10-bit BGGR Bayer 展開）
2. **メタデータ JSON 読み込み I/F** の新規実装（撮影条件・LSC マップ・hot pixel リスト等）
3. **既存 PNG 経路と並列対応**（後方互換）
4. **既存評価指標との互換**（CoV / 勾配 / C/P / max-min 等が DNG 入力でも同じロジックで算出可能）

これらの実装は **SmTIAS-LightSim 側にコピーされる前提**で，再利用性の高い設計とする．

## 背景 (Background)

### 三項プロジェクトの関係

```text
┌─────────────────────────┐      ┌──────────────────────────┐
│  SmTIAS                │      │  SmTIAS-Evaluation      │
│  (Flutter / Android)     │ ──→  │  (Python)                 │
│  撮影アプリ                │      │  実機データ評価ツール       │
│  - PNG 保存（通常モード）   │      │  - 評価指標算出           │
│  - DNG 保存（定量モード）   │      │  - 可視化                 │
└─────────────────────────┘      └──────────────────────────┘
                                          │
                                          │ コピー（uniformity.py 等）
                                          ▼
                              ┌──────────────────────────┐
                              │  SmTIAS-LightSim          │
                              │  (Python / Mitsuba 3)     │
                              │  シミュ vs 実機比較        │
                              └──────────────────────────┘
```

### 経緯のまとめ

- 2026-05-28: SmTIAS-LightSim 側 Step 8 キャリブで「実機 PNG 自体が定量データとして揺らぐ」を発見 ([TEST_01](../06_TEST/TEST_01_キャリブレーション検証履歴.md))
- 2026-05-28: SmTIAS 側に [TECH_01 定量撮影モード要求仕様](TECH_01_定量撮影モード要求仕様.md) を発行
- 2026-06-01: SmTIAS 側で実装完了．AQUOS sense3 (SH-02M) が LEVEL_3 で DNG 経路採用 ([TECH_03](TECH_03_定量モード実装報告とSHADING_MODE相談.md))
- 2026-06-01: SHADING_MODE = HIGH_QUALITY を本流維持で合意 ([TECH_04](TECH_04_SmTIASへの回答.md))
- **本ドキュメント (2026-06-01)**: SmTIAS-Evaluation 側にも DNG 対応を依頼

### SmTIAS-Evaluation の現状

SmTIAS-Evaluation の I/O は現状 PNG 専用：

```python
# 既存 I/F (src/io/loader.py)
def load_image(path: str) -> numpy.ndarray:
    """PNG 画像を読み込み RGB 配列として返す（H x W x 3, uint8）．"""
```

→ DNG 経路の追加が必要．

## 役割分担 (Role Distribution)

DNG 読み込みの責務は **SmTIAS-Evaluation を一次窓口**とする．既存パターン（`uniformity.py` が SmTIAS-Evaluation から SmTIAS-LightSim にコピー）を踏襲する：

| プロジェクト | 責務 |
| --- | --- |
| **SmTIAS** | DNG + meta.json の保存（実装済） |
| **SmTIAS-Evaluation（一次窓口）** | DNG/meta.json の読み込み I/F を正規実装．既存評価指標との並列対応 |
| **SmTIAS-LightSim（コピー側）** | SmTIAS-Evaluation の DNG 読み込み I/F をコピー（`uniformity.py` パターン継承） |

→ SmTIAS-Evaluation の実装完了後，SmTIAS-LightSim 側 Claude セッションでコピー作業を実施する．

## 要求仕様 (Requirements)

### 1. DNG 読み込み I/F

新規モジュール `src/io/dng_loader.py` を作成：

```python
def load_image_dng(
    path: str,
    demosaic: str = "bilinear",
    black_level: int | None = None,
    output_format: str = "rgb_float32",
) -> numpy.ndarray:
    """SmTIAS 定量モード DNG を読み込み配列として返す．

    Args:
        path: DNG ファイルのパス．
        demosaic: デモザイクアルゴリズム．既定 "bilinear"．
            "nearest"（最近傍）/ "bilinear"（双線形）/ "ahd"（適応的）から選択．
        black_level: 黒レベル．None なら meta.json から自動取得 or DNG Exif から取得．
            通常は 64（SH-02M）．
        output_format: 出力形式．既定 "rgb_float32"．
            "rgb_float32": (H, W, 3) float32, linear, 値域 [0.0, 1.0]（飽和は 1.0 でクリップしない）
            "luma_float32": (H, W) float32, linear, 値域 [0.0, 1.0]（Rec.709 重み）
            "rgb_uint8": (H, W, 3) uint8, sRGB OETF 適用後（既存 PNG 経路と互換）

    Returns:
        numpy.ndarray. 形状・型は output_format に依存．

    Raises:
        FileNotFoundError: ファイルが存在しない場合．
        ValueError: rawpy が DNG を読み込めなかった場合．
    """
```

#### 実装方針

- `rawpy.imread()` で 10-bit BGGR Bayer を読み込み
- 黒レベル除去（meta.json `sensorCharacteristics.black_level_pattern` から取得，未提供時は DNG Exif）
- デモザイク（`postprocess(demosaic_algorithm=...)` で実装）
- カラーマトリクス適用（DNG Exif `ColorMatrix1` を参照．無彩色シーンでは影響軽微だが選択肢として用意）
- 出力形式の変換

#### `output_format` の使い分け

- `rgb_float32` / `luma_float32`: SmTIAS-LightSim のシミュ vs 実機比較に推奨（linear ドメインで突合）
- `rgb_uint8`: 既存 SmTIAS-Evaluation パイプラインとの互換用（既存 `analysis/uniformity.py` 等が uint8 前提の場合に有用）

### 2. メタデータ JSON 読み込み I/F

新規モジュール `src/io/metadata_loader.py` を作成：

```python
def load_metadata(path: str) -> dict:
    """SmTIAS 定量モード meta.json を読み込み辞書として返す．

    Args:
        path: meta.json のパス．

    Returns:
        辞書．キー: settings / actual / sensorCharacteristics / lscMap / lscMapRowCount /
        lscMapColumnCount / hardwareLevel 等．
        構造は TECH_03 のスキーマ準拠．

    Raises:
        FileNotFoundError: ファイルが存在しない場合．
        json.JSONDecodeError: JSON 形式が不正な場合．
    """


def load_lsc_map(path: str) -> numpy.ndarray:
    """meta.json から LSC マップを (4, M, N) 配列として抽出する．

    Args:
        path: meta.json のパス．

    Returns:
        numpy.ndarray of shape (4, M, N), dtype=float32.
        4 = [R, G_even, G_odd, B], M = lscMapRowCount, N = lscMapColumnCount.
        値域: ≥ 1.0（無補正）．

    Raises:
        FileNotFoundError / KeyError / ValueError: 仕様外の場合．
    """


def load_hot_pixel_list(path: str) -> list[tuple[int, int]]:
    """meta.json からホット画素リストを (col, row) のタプルリストとして抽出する．

    Args:
        path: meta.json のパス．

    Returns:
        ホット画素座標のリスト．SmTIAS 側で取得実装後に有効化．未提供時は空リスト．
    """
```

### 3. Hot pixel マスク機能

メタデータから取得したホット画素を画像上でマスクする機能：

```python
def mask_hot_pixels(
    image: numpy.ndarray,
    hot_pixels: list[tuple[int, int]],
    method: str = "neighbor_mean",
) -> numpy.ndarray:
    """ホット画素位置を周辺平均で置換する．

    Args:
        image: 入力画像 (H, W) or (H, W, 3)．
        hot_pixels: ホット画素座標 (col, row) のリスト．
        method: 置換方法．"neighbor_mean"（周辺 3x3 平均）/ "zero"（0 で置換）．

    Returns:
        マスク済み画像．形状・型は入力と同じ．
    """
```

### 4. LSC マップ参照機能（オプション・将来検証用）

本流の処理では使わないが，将来のフォールバック検証用に実装：

```python
def apply_lsc_inverse(
    image: numpy.ndarray,
    lsc_map: numpy.ndarray,
) -> numpy.ndarray:
    """LSC マップを画像に逆適用する（補正前 raw 相当に戻す）．

    Args:
        image: 補正済み画像 (H, W) or (H, W, 3)．DNG (HIGH_QUALITY) を想定．
        lsc_map: LSC ゲインマップ (4, M, N)．

    Returns:
        逆適用後の画像．image_raw = image / interpolate(lsc_map, image.shape)．
    """
```

- LSC マップ (13×17) を画像解像度 (例 3264×2448) にバイリニア補間
- 画素ごとに割り算で逆適用
- SHADING_MODE = OFF 撮影との比較検証に使用（[TECH_04](TECH_04_SmTIASへの回答.md) フォールバック条件 C1〜C3）

### 5. 既存 PNG 経路との並列対応（後方互換）

既存 `load_image` は **無修正**で温存．新規 DNG 関数を並列追加する形：

```python
# 既存（無修正）
load_image(path) -> RGB uint8

# 新規追加
load_image_dng(path, ...) -> 設定可能形式
```

ユーザー側で拡張子を見て呼び分ける，もしくは統一インターフェースを別途用意する（後述）．

#### 統一インターフェースの選択肢

```python
def load_image_any(path: str, **kwargs) -> numpy.ndarray:
    """拡張子から自動判定して読み込む．

    .png → load_image (uint8 RGB)
    .dng → load_image_dng (kwargs で形式指定)
    """
    ext = Path(path).suffix.lower()
    if ext == ".png":
        return load_image(path)
    elif ext == ".dng":
        return load_image_dng(path, **kwargs)
    else:
        raise ValueError(f"未対応のフォーマット: {ext}")
```

これは SmTIAS-Evaluation 側の判断で実装するかは任意．推奨は実装．

### 6. 評価指標との互換

既存 `src/analysis/uniformity.py` 等の評価関数は **無修正**で DNG 入力に対応できるよう，DNG 読み込み I/F の出力を既存関数の入力仕様に合わせる：

| 既存関数の入力仕様 | DNG 経路での対応 |
| --- | --- |
| RGB uint8 (H, W, 3) | `output_format="rgb_uint8"` で uint8 にスケール（10-bit → 8-bit + sRGB OETF 適用） |
| Luma uint8 (H, W) | 上記後に Rec.709 重みでスカラー化 |
| Float32 linear (H, W, 3) | `output_format="rgb_float32"` で直接出力 |

→ 既存解析パイプラインの修正コストを最小化する設計．

## データフォーマット詳細 (Data Format Details)

### DNG (RAW_SENSOR) の値域

- 10-bit raw: 整数値 0〜1023
- 黒レベル: 64（`SENSOR_BLACK_LEVEL_PATTERN` から取得）
- 黒レベル除去後: 0〜959
- 飽和判定: 値 > 900 を要警告（飽和の影響）
- DNG Exif にカラーマトリクス `ColorMatrix1/2` 等が埋め込み済み（rawpy が自動で参照）

### sRGB OETF の扱い

- 定量モード DNG は **linear ドメイン**（TONEMAP_MODE = CONTRAST_CURVE 線形のため sRGB OETF 未適用）
- 既存通常モード PNG は sRGB OETF 適用済（端末の TONEMAP デフォルト）
- 評価時の OETF の扱いは **解析対象次第**：
  - 形状指標（CoV / 勾配等）: linear ドメインで算出が物理的に整合
  - 既存実機との互換比較: sRGB ドメインで算出（既存 SmTIAS-Evaluation の挙動と一致）

→ `output_format` パラメータでユーザーが選択可能にする．既定は `rgb_float32`（linear）を推奨．

### デモザイクアルゴリズム

`rawpy.imread().postprocess(demosaic_algorithm=...)` のパラメータ：

| 値 | 速度 | 品質 | 用途 |
| --- | --- | --- | --- |
| `rawpy.DemosaicAlgorithm.LINEAR` | 速 | 中 | バイリニア．**既定推奨** |
| `rawpy.DemosaicAlgorithm.AHD` | 中 | 高 | 適応的．カラー画像で有利だが照明均一性評価には過剰 |
| `rawpy.DemosaicAlgorithm.DHT` | 中 | 高 | 高品質．処理重め |

→ 既定は `LINEAR` で運用．`demosaic` 引数でユーザーが切り替え可能．

## 推奨実装方針 (Recommended Implementation)

### ディレクトリ構造（提案）

SmTIAS-Evaluation 既存構造に従い：

```text
src/io/
├── __init__.py
├── loader.py             # 既存 PNG 読み込み（無修正）
├── dng_loader.py         # 新規: DNG 読み込み
├── metadata_loader.py    # 新規: meta.json 読み込み
└── unified_loader.py     # 新規（任意）: 統一 I/F load_image_any
```

### 追加依存パッケージ

`pyproject.toml` の dependencies に：

```toml
[project]
dependencies = [
    "rawpy>=0.21.0",  # DNG 読み込み
    # 既存依存はそのまま
]
```

または `requirements.txt` に：

```text
rawpy>=0.21.0
```

### 段階的実装（Phase 分割）

| Phase | 内容 | 推定工数 |
| --- | --- | --- |
| Phase 1 | DNG 読み込み I/F (`dng_loader.py`) と単体テスト | 1 日 |
| Phase 2 | メタデータ JSON 読み込み I/F (`metadata_loader.py`) と単体テスト | 半日 |
| Phase 3 | LSC マップ参照・hot pixel マスク機能の実装 | 半日 |
| Phase 4 | 既存解析パイプラインとの統合・動作確認 | 半日 |
| Phase 5 | 既存 PNG / 新規 DNG の並列対応検証（V1〜V3） | 半日 |
| Phase 6 | ドキュメント更新（SPEC_01/SPEC_02 等） | 半日 |

合計: 約 3.5 日．`/implement` パイプラインで Phase ごとに進める．

## SmTIAS-LightSim 側へのコピー手順 (Copy to SmTIAS-LightSim)

SmTIAS-Evaluation での実装完了後，SmTIAS-LightSim 側 Claude セッションで以下を実施：

1. **コピー対象ファイル**:
   - `SmTIAS-Evaluation/src/io/dng_loader.py` → `SmTIAS-LightSim/src/sm_tias_lightsim/evaluation/dng_loader.py`
   - `SmTIAS-Evaluation/src/io/metadata_loader.py` → `SmTIAS-LightSim/src/sm_tias_lightsim/evaluation/metadata.py`
2. **既存パターンに従う** (`uniformity.py` と同じ流れ)
3. **ファイル先頭にコピー元を明記**:
   ```python
   """DNG 読み込みモジュール．

   SmTIAS-Evaluation/src/io/dng_loader.py を SmTIAS-LightSim にコピーしたもの．
   オリジナルは SmTIAS-Evaluation 側で管理する．バグ修正・機能追加はそちらで行い，
   本プロジェクトに反映する形を取る．
   """
   ```
4. **既存 sm-evaluate との統合**: 既存 `cli/evaluate.py` に DNG 入力経路を追加
5. **テストを SmTIAS-LightSim 側でも実装**（SmTIAS-Evaluation 側のテストをコピー＋プロジェクト固有部分を追加）

## 検証方法 (Verification)

実装完了後，以下を実施して受け入れ判定とする：

### V1. PNG vs DNG 整合性

- 同一シーン（白板）を SmTIAS で **通常モード PNG** + **定量モード DNG** で連続撮影
- 両方を SmTIAS-Evaluation で評価
- 評価指標（CoV / 勾配 / C/P 等）の差を確認
  - **完全一致は期待しない**（定量モードは ISP 介入最小化のため差が出るのが正常）
  - **DNG 経路でも妥当な値が出る**ことを確認（CoV > 0, 勾配 > 0 等の基本検証）

### V2. 既存 PNG 経路の回帰

- 既存通常モード PNG での評価指標が **変化しない**こと
- 既存テストが全合格すること

### V3. メタデータ JSON 読み込み

- `load_metadata` で `settings.exposure_time_ns` 等が正しく取得できる
- `load_lsc_map` で (4, 13, 17) の配列が返る
- `load_hot_pixel_list` で空リスト or リスト（SmTIAS 側実装次第）が返る

### V4. SmTIAS-LightSim 側へのコピー後検証

- コピー後の SmTIAS-LightSim 側で同じ DNG を読み込んで **同じ評価指標**が出ること
- SmTIAS-LightSim の既存テストが全合格

## 開放質問 (Open Questions)

実装着手時に確認が必要な事項：

| ID | 質問 | 確認方法 |
| --- | --- | --- |
| Q1 | rawpy のバージョン互換性は問題ないか | 環境で `pip install rawpy` → 実 DNG で動作確認 |
| Q2 | デモザイク手法の既定値は `LINEAR` で OK か | 数枚の DNG で AHD と比較して照明均一性指標がほぼ同じか確認 |
| Q3 | カラーマトリクス適用は無彩色シーンで影響軽微か | 既定 OFF（rawpy デフォルト）か ON（DNG Exif 参照）かを実機で比較 |
| Q4 | 既存 `analysis/uniformity.py` は uint8 / float32 のどちらを受けるか | コードを確認．既存仕様に合わせて `output_format` の既定を決める |
| Q5 | 統一 I/F `load_image_any` を実装するか | SmTIAS-Evaluation 側判断．SmTIAS-LightSim 側では便利だが任意 |
| Q6 | LSC 逆適用機能は Phase 3 で実装するか後回しか | フォールバック条件 C1〜C3 未発動なので Phase 3 では雛形のみで OK |
| Q7 | 既存ドキュメント (SPEC_01/SPEC_02) の更新範囲 | DNG 経路追加に伴う仕様変更を反映．Phase 6 で対応 |

## 引き継ぎ手順 (Hand-off Procedure)

SmTIAS-Evaluation 側 Claude セッションでの作業着手手順．[TECH_02 SmTIAS 引き継ぎ手順](TECH_02_SmTIAS引き継ぎ手順.md) と同様の構造．

### Step A: 環境準備

- rawpy パッケージを追加（`pip install rawpy` または `pyproject.toml` 更新 + `uv sync` 等）
- サンプル DNG を取得（SmTIAS 側で撮影済の `reference_measurement/quantitative/` から 1 枚）

### Step B: Phase 1（DNG 読み込み）から順次実装

- `/implement TECH_05 Phase 1: DNG 読み込み I/F` で起動
- coder → tester → refactorer → doc 更新の標準パイプライン
- Phase 1 完了後に動作確認 → Phase 2 へ

### Step C: 完了の伝達方法

- SmTIAS-Evaluation 側で `/implement` 完了後，SmTIAS-LightSim 側で確認できる形にする：
  - SmTIAS-Evaluation `src/io/dng_loader.py` の最終形をリポジトリ master にマージ
  - rinto が SmTIAS-LightSim 側 Claude セッションを開いて「コピーしてほしい」と指示
  - SmTIAS-LightSim 側 Claude が SmTIAS-Evaluation のローカルリポジトリから読み込んでコピー

## 既存ドキュメントとの整合 (Cross-References)

- [TECH_01 定量撮影モード要求仕様](TECH_01_定量撮影モード要求仕様.md) — 撮影モード側の要求書
- [TECH_02 SmTIAS 引き継ぎ手順](TECH_02_SmTIAS引き継ぎ手順.md) — 引き継ぎパターンの参考
- [TECH_03 定量モード実装報告と SHADING_MODE 相談](TECH_03_定量モード実装報告とSHADING_MODE相談.md) — SmTIAS 側実装報告（meta.json スキーマ等）
- [TECH_04 SmTIAS への回答](TECH_04_SmTIASへの回答.md) — SHADING_MODE 判断とフェーズ A/B 分割
- [TEST_01 キャリブレーション検証履歴](../06_TEST/TEST_01_キャリブレーション検証履歴.md) — 経緯
- [TEST_03 輝度マップ形状追跡](../06_TEST/TEST_03_輝度マップ形状追跡.md) — 形のずれ議論
- [SPEC_04 評価仕様](../04_SPEC/SPEC_04_評価仕様.md) — sm-evaluate パイプライン
- [Android Camera2 API DNG](https://developer.android.com/reference/android/hardware/camera2/DngCreator)
- [rawpy ドキュメント](https://letmaik.github.io/rawpy/api/)
