"""軸上スイープ(TEST_15 球 10 半径・TEST_16 立方体 10 サイズ)の輝度マップを正規化方式で比較する.
同じ luminance.npy を 3 方式で正規化して並べ,正規化の違いだけで見え方がどう変わるかを確かめる.
軸上配置は同心円状の分布なので輪帯ピーク正規化が本来の想定どおり働く条件であり,
非対称な分布(TEST_18)で採用したロバスト最大値との差が小さければ,過去の図の読みは正規化に
依存していなかったと言える.
方式(build_led_placement_maps と同じ定義):
ring_peak : 20 輪帯平均の最大値 = 1.0(TEST_15/16 の図で使った従来方式)
robust_peak : 平滑化後の 99 パーセンタイル = 1.0(TEST_18 の既定)
mean : ROI 平均 = 1.0
出力(output/_normalization_comparison/):
norm_sphere.png / norm_cube.png : 行 = 方式,列 = サイズ(10).共通スケール
norm_summary.csv : 各マップの基準値(ピーク・パーセンタイル・平均)と比
実行方法:
uv run python scripts/build_normalization_comparison.py
"""
from __future__ import annotations
import argparse
import csv
import sys
from pathlib import Path
import matplotlib
import numpy as np
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from build_led_placement_maps import ( # noqa: E402
CMAP,
ROBUST_PERCENTILE,
normalize_map,
smoothed_roi,
)
from sweep_cube_size import OUTPUT_BASE as CUBE_OUTPUT_BASE # noqa: E402
from sweep_cube_size import RUN_DIR as CUBE_RUN_DIR # noqa: E402
from sweep_cube_size import SIZES as CUBE_SIZES # noqa: E402
from sweep_cube_size import job_name as cube_job_name # noqa: E402
from sweep_sphere_radius import OUTPUT_BASE as SPHERE_OUTPUT_BASE # noqa: E402
from sweep_sphere_radius import RADII as SPHERE_RADII # noqa: E402
from sweep_sphere_radius import job_name as sphere_job_name # noqa: E402
from sm_tias_lightsim.evaluation.radial import calc_radial_profile # noqa: E402
from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh # noqa: E402
plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"]
ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "output" / "_normalization_comparison"
SPHERE_RUN_DIR = "board_baffle_similar_radial"
MODES = ("ring_peak", "robust_peak", "mean")
MODE_LABEL = {
"ring_peak": "輪帯ピーク\n(20 輪帯平均の最大=1)",
"robust_peak": f"ロバスト最大\n(平滑化後 {ROBUST_PERCENTILE:g}%ile=1)",
"mean": "ROI 平均\n(mean=1)",
}
# 共通スケール.mean は 1 を中心に上下対称,他は上限 1.0
VMIN = 0.80
VMAX = {"ring_peak": 1.0, "robust_peak": 1.0, "mean": 1.0 + (1.0 - VMIN)}
def conditions() -> dict[str, list[tuple[str, Path]]]:
"""形状 → [(ラベル, luminance.npy)] を返す(存在するものだけ)."""
sphere_dir = SPHERE_OUTPUT_BASE / SPHERE_RUN_DIR
sphere = [(f"φ{2 * r:g}", sphere_dir / sphere_job_name(r) / "luminance.npy")
for r in SPHERE_RADII]
cube = [(f"L={size:g}", CUBE_OUTPUT_BASE / CUBE_RUN_DIR / cube_job_name(size) / "luminance.npy")
for size in CUBE_SIZES]
return {"sphere": [c for c in sphere if c[1].exists()],
"cube": [c for c in cube if c[1].exists()]}
def reference_values(path: Path) -> dict[str, float]:
"""3 方式の基準値(正規化に使う分母)と生の ROI 統計を返す."""
arr = np.load(path).astype(np.float64)
if arr.ndim == 3:
arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2]
roi = extract_roi_xywh(arr, SIM_ROI)
profile = calc_radial_profile(roi, n_bins=20)
smooth = smoothed_roi(path)
return {
"raw_mean": float(roi.mean()),
"ring_peak": float(np.nanmax(profile[:, 1])),
"robust_peak": float(np.percentile(smooth, ROBUST_PERCENTILE)),
"mean": float(smooth.mean()),
"smooth_max": float(smooth.max()),
}
def draw(shape: str, items: list[tuple[str, Path]], output: Path) -> None:
"""行 = 正規化方式,列 = サイズのパネルを描く."""
n_cols = len(items)
figsize = (1.9 * n_cols + 1.5, 2.1 * len(MODES) + 1.0)
fig, axes = plt.subplots(len(MODES), n_cols, figsize=figsize, squeeze=False)
images = {}
for r, mode in enumerate(MODES):
for c, (label, path) in enumerate(items):
ax = axes[r, c]
data = normalize_map(path, mode)
images[mode] = ax.imshow(data, cmap=CMAP, vmin=VMIN, vmax=VMAX[mode])
ax.set_xticks([])
ax.set_yticks([])
if r == 0:
ax.set_title(label, fontsize=9)
if c == 0:
ax.set_ylabel(MODE_LABEL[mode], fontsize=9, fontweight="bold")
fig.colorbar(images[mode], ax=axes[r, :].tolist(), fraction=0.012, pad=0.01)
title = "球 similar_radial(TEST_15)" if shape == "sphere" else "立方体(TEST_16)"
fig.suptitle(f"正規化方式の比較 — {title} 軸上 1 灯・板向き・バッフル付き."
f"上 2 行は {VMIN:g}〜1.0,mean 行は {VMIN:g}〜{VMAX['mean']:g}", fontsize=12)
output.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(output, dpi=110, bbox_inches="tight")
plt.close(fig)
print(f"出力: {output}")
def write_summary(conds: dict[str, list[tuple[str, Path]]]) -> None:
"""各マップの基準値と比を CSV と標準出力に書く."""
rows = []
for shape, items in conds.items():
for label, path in items:
ref = reference_values(path)
rows.append({
"shape": shape, "size": label,
**{k: round(v, 6) for k, v in ref.items()},
"ring_over_robust": round(ref["ring_peak"] / ref["robust_peak"], 4),
"ring_over_mean": round(ref["ring_peak"] / ref["mean"], 4),
"robust_over_mean": round(ref["robust_peak"] / ref["mean"], 4),
})
OUT_DIR.mkdir(parents=True, exist_ok=True)
with (OUT_DIR / "norm_summary.csv").open("w", newline="", encoding="utf-8") as fh:
writer = csv.DictWriter(fh, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
print(f"\n{'形状':>7} {'サイズ':>6} {'輪帯ピーク':>10} {'ロバスト最大':>12} {'平均':>8} "
f"{'輪帯/ロバスト':>12} {'輪帯/平均':>9} {'ロバスト/平均':>12}")
for r in rows:
print(f"{r['shape']:>7} {r['size']:>6} {r['ring_peak']:10.4f} {r['robust_peak']:12.4f} "
f"{r['mean']:8.4f} {r['ring_over_robust']:12.4f} {r['ring_over_mean']:9.4f} "
f"{r['robust_over_mean']:12.4f}")
def main() -> int:
"""正規化方式の比較パネルと集約表を生成する."""
parser = argparse.ArgumentParser(description="軸上スイープの輝度マップを正規化方式で比較")
parser.parse_args()
conds = conditions()
if not conds["sphere"] and not conds["cube"]:
print("エラー: TEST_15/16 の結果が見つかりません")
return 1
for shape, items in conds.items():
if items:
draw(shape, items, OUT_DIR / f"norm_{shape}.png")
write_summary(conds)
return 0
if __name__ == "__main__":
sys.exit(main())