Newer
Older
SmTIAS-Evaluation / src / io / dng_pipeline.py
"""DNG 定量モードの輝度化パイプライン.

DNG(RAW_SENSOR)を読み込み,LSC(レンズシェーディング)補正を順適用して
PNG 経路と同じ portrait・linear・Rec.709 輝度画像へ変換する一連の処理をまとめる.

- 読み込みは load_image_dng(output_format="rgb_float32", linear, output_color=raw)
- DNG は RAW_SENSOR で未補正のため,meta.json の lscMap でレンズビネットを順適用(任意)
- DNG はセンサ native が landscape のため,PNG と同じ portrait へ rot90(-1) で向き合わせ
"""

import json
from pathlib import Path

import numpy as np

from src.analysis.uniformity import (
    REC709_COEFF_B,
    REC709_COEFF_G,
    REC709_COEFF_R,
)
from src.io.dng_loader import load_image_dng


def _upsample(grid: np.ndarray, shape: tuple[int, int]) -> np.ndarray:
    """(rows, cols) グリッドを画像サイズ shape へバイリニア補間する.

    Args:
        grid: 補間元グリッドの NumPy 配列(rows x cols, float64).
        shape: 補間先の画像サイズ (height, width).

    Returns:
        補間後の NumPy 配列(height x width, float64).
    """
    rows, cols = grid.shape
    h, w = shape
    ys = np.linspace(0, rows - 1, h)
    xs = np.linspace(0, cols - 1, w)
    y0 = np.floor(ys).astype(int)
    y1 = np.minimum(y0 + 1, rows - 1)
    x0 = np.floor(xs).astype(int)
    x1 = np.minimum(x0 + 1, cols - 1)
    wy = (ys - y0)[:, None]
    wx = (xs - x0)[None, :]
    top = grid[y0][:, x0] * (1 - wx) + grid[y0][:, x1] * wx
    bot = grid[y1][:, x0] * (1 - wx) + grid[y1][:, x1] * wx
    return top * (1 - wy) + bot * wy


def lsc_gain_rgb(meta_path: Path, shape: tuple[int, int]) -> np.ndarray:
    """meta.json の lscMap (4, M, N) を (H, W, 3) の RGB ゲインへ補間する.

    4ch = [R, G_even, G_odd, B].G は G_even/G_odd の平均を用いる.

    Args:
        meta_path: meta.json ファイルのパス.
        shape: 補間先の画像サイズ (height, width).

    Returns:
        RGB ゲインの NumPy 配列(H x W x 3, float64).
    """
    meta = json.loads(meta_path.read_text(encoding="utf-8"))
    rows, cols = meta["lscMapRowCount"], meta["lscMapColumnCount"]
    m = np.array(meta["lscMap"], dtype=np.float64).reshape(rows, cols, 4)
    gr = _upsample(m[:, :, 0], shape)
    gg = _upsample((m[:, :, 1] + m[:, :, 2]) / 2.0, shape)
    gb = _upsample(m[:, :, 3], shape)
    return np.stack([gr, gg, gb], axis=2)


def dng_luminance(image_path: str, apply_lsc: bool) -> np.ndarray:
    """DNG から portrait・linear・(任意で LSC 補正済)の輝度画像を返す.

    - output_format="rgb_float32"(output_color=raw, linear)で読み込み
    - apply_lsc=True なら lscMap を順適用してレンズビネットを除去
    - Rec.709 で輝度化(uniformity.py と同じ係数)
    - PNG と同じ portrait へ rot90(-1) で向き合わせ

    Args:
        image_path: DNG ファイルのパス.
        apply_lsc: True の場合,同名 .meta.json から LSC ゲインを読み込んで補正する.

    Returns:
        portrait 向きの輝度画像(H x W, float64).値域は 0〜255 の linear スケール.
    """
    rgb = load_image_dng(image_path, output_format="rgb_float32")  # (H,W,3) linear landscape

    if apply_lsc:
        meta_path = Path(image_path).with_suffix("").with_suffix(".meta.json")
        if not meta_path.exists():
            # ".meta.json" は二重拡張子なので with_suffix では拾えない場合がある
            meta_path = Path(str(Path(image_path).with_suffix("")) + ".meta.json")
        if not meta_path.exists():
            raise FileNotFoundError(f"meta.json が見つかりません(LSC 補正に必要): {meta_path}")
        rgb = rgb * lsc_gain_rgb(meta_path, rgb.shape[:2])

    luma = (
        REC709_COEFF_R * rgb[:, :, 0]
        + REC709_COEFF_G * rgb[:, :, 1]
        + REC709_COEFF_B * rgb[:, :, 2]
    )
    # linear のまま 0〜255 スケールで表現する(PNG 図と軸を揃えて比較するため).
    # CoV・max/min・中心周辺比はスケール不変なので不変,mean/std のみ ×255 となる.
    # 既存 plot_histogram が range=(0,255) 固定であることにも整合する.
    luma = luma * 255.0
    return np.rot90(luma, -1)  # portrait(PNG と同じ向き)