"""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())