Newer
Older
SmTIAS-LightSim / scripts / build_loft_w1p4_panel.py
"""TEST_10 後側ロフト変形 w=1.4 実験の形比較パネル生成.

output/loft_w1p4/ のレンダ完了後に実行すること.
行 = bottom / left / right / full,列 = 元メッシュ基準 / 後側_w1p4 / 実機 の 4×3 グリッド.
シミュ ROI と実機マップは解像度が異なる(450×464 vs 約 1400×1409)ため,
全セルを共通解像度へ resize してから描画し,行・列の枠を完全に揃える.
各セルは独立 1–99 パーセンタイル正規化(形の比較専用・明るさの絶対比較は不可).

生成するファイル:
    output/loft_w1p4/summary.csv                    -- 全ジョブの形指標
    output/_slide_panels/loft_w1p4_shape_roi.png    -- 4×3 形比較パネル(ROI 揃え)

使い方:
    uv run python scripts/build_loft_w1p4_panel.py

前提:
    - output/loft_w1p4/{bottom,left,right,full,full_base}/luminance.npy が存在すること
    - 元メッシュ単灯基準 output/blend_bottom/{,left_,right_}w0.05_a0.1/luminance.npy
    - 実機マップ lightsim/luminance_maps/{side}_*.npy(単灯 3 種 + full.full は
      reference_measurement/quantitative/MiniTIAS_QM_20260601_124500.dng から
      単灯と同一手順(linear raw + LSC 順適用 + Rec.709 + REAL_ROI)で生成したもの)
"""

from __future__ import annotations

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

matplotlib.rcParams["font.family"] = ["Meiryo", "Yu Gothic", "DejaVu Sans"]

ROOT = Path(__file__).resolve().parents[1]
PANEL_PATH = ROOT / "output" / "_slide_panels" / "loft_w1p4_shape_roi.png"
SUMMARY_CSV = ROOT / "output" / "loft_w1p4" / "summary.csv"

REAL = {
    "bottom": ROOT / "lightsim" / "luminance_maps" / "bottom_SmTIAS_QM_20260615_141420.npy",
    "left": ROOT / "lightsim" / "luminance_maps" / "left_SmTIAS_QM_20260615_150300.npy",
    "right": ROOT / "lightsim" / "luminance_maps" / "right_SmTIAS_QM_20260615_170948.npy",
    "full": ROOT / "lightsim" / "luminance_maps" / "full_MiniTIAS_QM_20260601_124500.npy",
}
BASE = {
    "bottom": ROOT / "output" / "blend_bottom" / "w0.05_a0.1" / "luminance.npy",
    "left": ROOT / "output" / "blend_bottom" / "left_w0.05_a0.1" / "luminance.npy",
    "right": ROOT / "output" / "blend_bottom" / "right_w0.05_a0.1" / "luminance.npy",
    "full": ROOT / "output" / "loft_w1p4" / "full_base" / "luminance.npy",
}

SIDES = ["bottom", "left", "right", "full"]
VERSIONS = ["base", "w1p4", "real"]
VERSION_LABELS = {"base": "元メッシュ基準", "w1p4": "後側_w1p4", "real": "実機"}

# 全セルの共通描画解像度(シミュ ROI と同程度.実機はダウンサンプル)
CELL_SHAPE = (450, 464)
VMIN_PERCENTILE = 1.0
VMAX_PERCENTILE = 99.0


def _load_luminance(path: Path) -> np.ndarray:
    """luminance.npy を読み込み 2D 輝度にする(3D RGB は Rec.709 変換)."""
    arr = np.load(path)
    if arr.ndim == 3:
        arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2]
    return arr.astype(np.float64)


def _metrics(img: np.ndarray, real: np.ndarray | None) -> dict[str, float]:
    """TEST_09/10 の形指標(peak/mean・top/bot・NCC 対実機)を算出する."""
    h = img.shape[0]
    m = {
        "peak_mean": float(img.max() / img.mean()),
        "top_bot": float(img[: h // 2].mean() / img[h // 2 :].mean()),
    }
    if real is not None:
        m["ncc"] = float(calc_ncc(resize(img, real.shape, anti_aliasing=True), real))
    else:
        m["ncc"] = float("nan")
    return m


def main() -> int:
    """パネルと summary CSV を生成するメインエントリポイント.

    Returns:
        終了コード(成功なら 0,入力不足なら 1).
    """
    cells: dict[tuple[str, str], np.ndarray] = {}
    rows: list[list[str]] = []
    for side in SIDES:
        real = _load_luminance(REAL[side]) if side in REAL else None
        for ver in ["base", "w1p4"]:
            if ver == "base":
                path = BASE[side]
            else:
                path = ROOT / "output" / "loft_w1p4" / side / "luminance.npy"
            if not path.exists():
                print(f"エラー: {path} が見つかりません", file=sys.stderr)
                return 1
            roi = extract_roi_xywh(_load_luminance(path), SIM_ROI)
            cells[(side, ver)] = roi
            m = _metrics(roi, real)
            rows.append(
                [side, ver, f"{m['peak_mean']:.4f}", f"{m['top_bot']:.4f}", f"{m['ncc']:.4f}"]
            )
        if real is not None:
            cells[(side, "real")] = real
            rm = _metrics(real, real)
            rows.append([side, "real", f"{rm['peak_mean']:.4f}", f"{rm['top_bot']:.4f}", "1.0000"])

    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(["side", "version", "peak_mean", "top_bot", "ncc_vs_real"])
        writer.writerows(rows)
    print(f"summary 保存: {SUMMARY_CSV}")

    # 全セルを共通解像度に resize して行・列の枠を完全に揃える
    fig, axes = plt.subplots(
        len(SIDES),
        len(VERSIONS),
        figsize=(len(VERSIONS) * 4.2, len(SIDES) * 4.2),
        constrained_layout=True,
    )
    for r, side in enumerate(SIDES):
        for c, ver in enumerate(VERSIONS):
            ax = axes[r][c]
            ax.set_axis_off()
            if (side, ver) not in cells:
                ax.set_facecolor("0.15")
                if r == 0:
                    ax.set_title(VERSION_LABELS[ver], fontsize=11, pad=6)
                ax.text(
                    0.5,
                    0.5,
                    "実機全灯 npy 無し",
                    ha="center",
                    va="center",
                    transform=ax.transAxes,
                    fontsize=10,
                    color="0.4",
                )
                continue
            img = cells[(side, ver)]
            img = resize(img / img.mean(), CELL_SHAPE, anti_aliasing=True)
            ax.imshow(
                img,
                cmap="hot",
                vmin=float(np.percentile(img, VMIN_PERCENTILE)),
                vmax=float(np.percentile(img, VMAX_PERCENTILE)),
                aspect="equal",
            )
            m = _metrics(cells[(side, ver)], None)
            title = f"p/m={m['peak_mean']:.3f}  t/b={m['top_bot']:.3f}"
            if r == 0:
                title = f"{VERSION_LABELS[ver]}\n{title}"
            ax.set_title(title, fontsize=10, pad=6)
        # 行ラベル(左端セルの外側)
        axes[r][0].text(
            -0.06,
            0.5,
            side,
            ha="right",
            va="center",
            rotation=90,
            transform=axes[r][0].transAxes,
            fontsize=13,
            fontweight="bold",
        )

    fig.suptitle(
        "TEST_10 後側ロフト変形 w=1.4 — blend w0.05_a0.1, spp=1024, d=16, ROI 揃え\n"
        "各セル独立 1–99% 正規化(形の比較専用)・全セル共通解像度",
        fontsize=12,
    )
    PANEL_PATH.parent.mkdir(parents=True, exist_ok=True)
    fig.savefig(PANEL_PATH, dpi=150, bbox_inches="tight")
    plt.close(fig)
    print(f"パネル保存: {PANEL_PATH}")
    return 0


if __name__ == "__main__":
    sys.exit(main())