"""TEST_11/12 球体筐体実験(面法線修正後)の形比較パネル生成.
セル = 現行筐体全灯(面法線修正)/ 球ロフト円形光源(面法線修正)/ 実機全灯 の 1×3.
TEST_10 と同じ流儀: シミュは SIM_ROI 抽出,実機はフル解像度のまま,
全セルを共通解像度へ resize して比較する.
TEST_12 のシェーディング法線バグ修正前の出力(output/sphere_r100{,_loft,_loft_disk} 等)は
法線条件が異なるため本パネルには混ぜない.
生成するファイル:
output/sphere_r100_loft_disk_fix/summary.csv -- 形指標(peak/mean・top/bot・CoV・NCC)
output/_slide_panels/sphere_fix_shape_roi.png -- 1×3 形比較パネル(独立 1–99% 正規化)
output/_slide_panels/sphere_fix_relative.png -- 相対輝度(max=1.0・共通スケール)
使い方:
uv run python scripts/build_sphere_panel.py
前提:
- output/full_base_fix/luminance.npy(現行筐体全灯,configs/_tmp_fn_full_base.yaml)
- output/sphere_r100_loft_disk_fix/luminance.npy(球ロフト円形光源)
- lightsim/luminance_maps/full_MiniTIAS_QM_20260601_124500.npy(実機全灯)
"""
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]
SUMMARY_CSV = ROOT / "output" / "sphere_r100_loft_disk_fix" / "summary.csv"
SHAPE_PANEL_PATH = ROOT / "output" / "_slide_panels" / "sphere_fix_shape_roi.png"
REL_PANEL_PATH = ROOT / "output" / "_slide_panels" / "sphere_fix_relative.png"
# (ラベル, luminance.npy パス, シミュか否か).シミュのみ SIM_ROI を抽出する
# TEST_12 面法線修正後の出力のみで比較する(修正前の出力とはシェーディング法線が
# 異なり混在比較は不可.修正前の記録は TEST_11 の表を参照)
CELLS = [
(
"現行筐体 全灯 18 灯(面法線修正)",
ROOT / "output" / "full_base_fix" / "luminance.npy",
True,
),
(
"球ロフト 円形光源(面法線修正)",
ROOT / "output" / "sphere_r100_loft_disk_fix" / "luminance.npy",
True,
),
(
"実機 全灯 18 灯",
ROOT / "lightsim" / "luminance_maps" / "full_MiniTIAS_QM_20260601_124500.npy",
False,
),
]
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/11 共通の形指標(peak/mean・top/bot・CoV・NCC 対実機)を算出する."""
h = img.shape[0]
m = {
"peak_mean": float(img.max() / img.mean()),
"top_bot": float(img[: h // 2].mean() / img[h // 2 :].mean()),
"cov": float(img.std() / img.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).
"""
real = _load_luminance(CELLS[-1][1])
loaded: list[tuple[str, np.ndarray, dict[str, float]]] = []
for label, path, is_sim in CELLS:
if not path.exists():
print(f"エラー: {path} が見つかりません", file=sys.stderr)
return 1
img = _load_luminance(path)
if is_sim:
img = extract_roi_xywh(img, SIM_ROI)
m = _metrics(img, real if is_sim else None)
if not is_sim:
m["ncc"] = 1.0
loaded.append((label, img, m))
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(["label", "peak_mean", "top_bot", "cov_roi", "ncc_vs_real"])
for label, _, m in loaded:
writer.writerow(
[
label,
f"{m['peak_mean']:.4f}",
f"{m['top_bot']:.4f}",
f"{m['cov']:.4f}",
f"{m['ncc']:.4f}",
]
)
print(f"summary 保存: {SUMMARY_CSV}")
# 形比較パネル(各セル独立 1–99% 正規化)
n = len(loaded)
fig, axes = plt.subplots(1, n, figsize=(n * 4.2, 4.6), constrained_layout=True)
for ax, (label, img, m) in zip(axes, loaded, strict=True):
disp = resize(img / img.mean(), CELL_SHAPE, anti_aliasing=True)
ax.imshow(
disp,
cmap="hot",
vmin=float(np.percentile(disp, VMIN_PERCENTILE)),
vmax=float(np.percentile(disp, VMAX_PERCENTILE)),
aspect="equal",
)
ax.set_title(
f"{label}\np/m={m['peak_mean']:.3f} t/b={m['top_bot']:.3f} NCC={m['ncc']:.3f}",
fontsize=10,
pad=6,
)
ax.set_axis_off()
fig.suptitle(
"TEST_11 球体筐体 — blend w0.05_a0.1, d=16, ROI 揃え\n"
"各セル独立 1–99% 正規化(形の比較専用)・全セル共通解像度",
fontsize=12,
)
SHAPE_PANEL_PATH.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(SHAPE_PANEL_PATH, dpi=150, bbox_inches="tight")
plt.close(fig)
print(f"パネル保存: {SHAPE_PANEL_PATH}")
# 相対輝度(各セル max=1.0,全セル共通スケール).実機も含めて絶対均一性を並べる.
# max は resize 後(MC ノイズ平滑化後)に取る(TEST_10 の流儀を踏襲)
rel_cells = []
for label, img, _ in loaded:
disp = resize(img, CELL_SHAPE, anti_aliasing=True)
rel_cells.append((label, disp / disp.max()))
vmin = min(float(r.min()) for _, r in rel_cells)
fig2, axes2 = plt.subplots(1, n, figsize=(n * 4.4, 5.0), constrained_layout=True)
for ax, (label, rel) in zip(axes2, rel_cells, strict=True):
im = ax.imshow(rel, cmap="hot", vmin=vmin, vmax=1.0, aspect="equal")
ax.set_title(
f"{label}\nmean/max={rel.mean():.3f} min/max={rel.min():.3f}",
fontsize=10,
pad=6,
)
ax.set_axis_off()
fig2.colorbar(im, ax=axes2, shrink=0.85, label="相対輝度(max=1.0)")
fig2.suptitle(
"TEST_11 球体筐体 — 相対輝度(各セル max=1.0,全セル共通スケール)\n"
"完全均一なら全面 1.0.mean/max・min/max が 1 に近いほど均一",
fontsize=12,
)
fig2.savefig(REL_PANEL_PATH, dpi=150, bbox_inches="tight")
plt.close(fig2)
print(f"パネル保存: {REL_PANEL_PATH}")
return 0
if __name__ == "__main__":
sys.exit(main())