"""TEST_12 相対結論再確認: 面法線修正後の単灯 3 ジョブの形指標・灯間 NCC 集計.
TEST_09 の相対結論「シミュは 3 灯ほぼ同形(灯間 NCC 0.95x)=実機の非対称相補性
(0.85/0.61/0.41)を再現できない」を面法線修正後のデータで再検証する.
単灯の形(peak/mean・top/bot・NCC 対実機)も修正前後で対比する.
生成するファイル:
output/singles_fix/summary.csv -- 単灯形指標(修正前後の対比付き)
output/singles_fix/pairwise_ncc.csv -- 灯間 NCC(修正前 / 修正後 / 実機)
output/_slide_panels/singles_fix_shape_roi.png -- 3 行(灯)× 3 列のパネル
使い方:
uv run python scripts/build_singles_fix_summary.py
前提:
- output/singles_fix/{bottom,left,right}/luminance.npy(面法線修正後)
- output/blend_bottom/{,left_,right_}w0.05_a0.1/luminance.npy(修正前,対比用)
- lightsim/luminance_maps/{bottom,left,right}_*.npy(実機単灯)
"""
from __future__ import annotations
import csv
import sys
from itertools import combinations
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]
SUMMARY_CSV = ROOT / "output" / "singles_fix" / "summary.csv"
PAIRWISE_CSV = ROOT / "output" / "singles_fix" / "pairwise_ncc.csv"
PANEL_PATH = ROOT / "output" / "_slide_panels" / "singles_fix_shape_roi.png"
SIDES = ["bottom", "left", "right"]
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",
}
OLD_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",
}
FIX_BASE = {s: ROOT / "output" / "singles_fix" / s / "luminance.npy" for s in SIDES}
CELL_SHAPE = (450, 464)
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 _shape_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 _pairwise_ncc(maps: dict[str, np.ndarray]) -> dict[str, float]:
"""3 灯マップの灯間 NCC(mean 正規化・フル frame,TEST_10 の流儀)."""
out = {}
for a, b in combinations(SIDES, 2):
ma, mb = maps[a], maps[b]
if ma.shape != mb.shape:
mb = resize(mb, ma.shape, anti_aliasing=True)
out[f"{a}_vs_{b}"] = float(calc_ncc(ma / ma.mean(), mb / mb.mean()))
return out
def main() -> int:
"""集計 CSV とパネルを生成するメインエントリポイント.
Returns:
終了コード(成功なら 0,入力不足なら 1).
"""
for paths in (FIX_BASE, REAL, OLD_BASE):
for p in paths.values():
if not p.exists():
print(f"エラー: {p} が見つかりません", file=sys.stderr)
return 1
real = {s: _load_luminance(REAL[s]) for s in SIDES}
sets = {
"old": {s: extract_roi_xywh(_load_luminance(OLD_BASE[s]), SIM_ROI) for s in SIDES},
"fix": {s: extract_roi_xywh(_load_luminance(FIX_BASE[s]), SIM_ROI) for s in SIDES},
}
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"])
for s in SIDES:
for ver, maps in sets.items():
m = _shape_metrics(maps[s], real[s])
writer.writerow(
[s, ver, f"{m['peak_mean']:.4f}", f"{m['top_bot']:.4f}", f"{m['ncc']:.4f}"]
)
rm = _shape_metrics(real[s], real[s])
writer.writerow([s, "real", f"{rm['peak_mean']:.4f}", f"{rm['top_bot']:.4f}", "1.0000"])
print(f"summary 保存: {SUMMARY_CSV}")
# raw の NCC は MC ノイズで 0 方向へ希釈される(単灯 spp=1024 はノイズ > 構造振幅)ため,
# 平滑化(median 5px + gaussian σ=4)後の構造のみの NCC を併記する.解釈は smooth を正とする
from scipy.ndimage import gaussian_filter, median_filter
def _smooth(m: np.ndarray) -> np.ndarray:
return gaussian_filter(median_filter(m, size=5), sigma=4)
with PAIRWISE_CSV.open("w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["set", "bottom_vs_left", "bottom_vs_right", "left_vs_right"])
for label, maps in [("old", sets["old"]), ("fix", sets["fix"]), ("real", real)]:
smoothed = {s: _smooth(m) for s, m in maps.items()}
for suffix, mm in (("raw", maps), ("smooth", smoothed)):
ncc = _pairwise_ncc(mm)
writer.writerow(
[
f"{label}_{suffix}",
f"{ncc['bottom_vs_left']:.4f}",
f"{ncc['bottom_vs_right']:.4f}",
f"{ncc['left_vs_right']:.4f}",
]
)
print(f"pairwise 保存: {PAIRWISE_CSV}")
# パネル: 行=灯,列=修正前 / 修正後 / 実機(各セル独立 1–99% 正規化)
cols = [("修正前(スムーズ法線)", sets["old"]), ("修正後(面法線)", sets["fix"])]
fig, axes = plt.subplots(3, 3, figsize=(3 * 4.2, 3 * 4.2), constrained_layout=True)
for r, s in enumerate(SIDES):
cells = [maps[s] for _, maps in cols] + [real[s]]
titles = [label for label, _ in cols] + ["実機"]
for c, (img, title) in enumerate(zip(cells, titles, strict=True)):
ax = axes[r][c]
disp = resize(img / img.mean(), CELL_SHAPE, anti_aliasing=True)
ax.imshow(
disp,
cmap="hot",
vmin=float(np.percentile(disp, 1.0)),
vmax=float(np.percentile(disp, 99.0)),
aspect="equal",
)
m = _shape_metrics(img, None)
t = f"p/m={m['peak_mean']:.3f} t/b={m['top_bot']:.3f}"
ax.set_title(f"{title}\n{t}" if r == 0 else t, fontsize=10, pad=6)
ax.set_axis_off()
axes[r][0].text(
-0.06, 0.5, s, ha="right", va="center", rotation=90,
transform=axes[r][0].transAxes, fontsize=13, fontweight="bold",
)
fig.suptitle(
"TEST_12 相対結論再確認 — 単灯 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())