"""LED グループ別 sanity check の比較パネル生成.
scripts/sanity_led_groups.py の出力から 5 列比較パネルを作る:
① bottom 単独 ON
② right 単独 ON
③ left 単独 ON
④ 全 18 個 ON (output/baseline/luminance.npy)
⑤ ①+②+③ の和 (線形性確認用)
加えて差分パネル ⑥ |④ - ⑤| を生成し,線形性誤差を可視化する.
実行方法:
uv run python scripts/build_led_group_sanity_panel.py
前提条件:
- output/led_group_sanity/{bottom,right,left}/luminance.npy が揃っていること
- output/baseline/luminance.npy が全 ON ベースラインとして存在すること
出力:
- output/_slide_panels/led_group_sanity.png
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib
import numpy as np
matplotlib.use("Agg")
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
# ---------------------------------------------------------------------------
# 定数
# ---------------------------------------------------------------------------
ROOT = Path(__file__).resolve().parents[1]
SANITY_BASE = ROOT / "output" / "led_group_sanity"
BASELINE_NPY = ROOT / "output" / "baseline" / "luminance.npy"
OUTPUT_PANEL = ROOT / "output" / "_slide_panels" / "led_group_sanity.png"
def _setup_japanese_font() -> None:
"""matplotlib に日本語フォントを設定する (Windows 標準)."""
for path in ("C:/Windows/Fonts/meiryo.ttc", "C:/Windows/Fonts/YuGothM.ttc"):
if Path(path).exists():
fm.fontManager.addfont(path)
plt.rcParams["font.family"] = fm.FontProperties(fname=path).get_name()
return
def main() -> int:
"""パネル生成のメイン処理."""
_setup_japanese_font()
# 各グループの luminance.npy を読み込み
groups = ["bottom", "right", "left"]
lums: dict[str, np.ndarray] = {}
for g in groups:
npy_path = SANITY_BASE / g / "luminance.npy"
if not npy_path.exists():
print(f"エラー: {npy_path} が見つかりません", file=sys.stderr)
return 1
lums[g] = np.load(npy_path)
# ベースライン (全 ON) 読み込み
if not BASELINE_NPY.exists():
print(f"エラー: ベースライン {BASELINE_NPY} が見つかりません", file=sys.stderr)
return 1
baseline = np.load(BASELINE_NPY)
# スカラ輝度に変換 (RGB なら平均,2D ならそのまま)
def to_2d(arr: np.ndarray) -> np.ndarray:
return arr.mean(axis=-1) if arr.ndim == 3 else arr
lum_bottom = to_2d(lums["bottom"])
lum_right = to_2d(lums["right"])
lum_left = to_2d(lums["left"])
lum_all = to_2d(baseline)
lum_sum = lum_bottom + lum_right + lum_left
diff = np.abs(lum_all - lum_sum)
# 線形性指標
rel_err = diff.mean() / max(lum_all.mean(), 1e-9)
max_rel_err = diff.max() / max(lum_all.mean(), 1e-9)
# 共有 vmin/vmax (① 〜 ⑤ の 1%/99% percentile を集約)
stacked = np.stack([lum_bottom, lum_right, lum_left, lum_all, lum_sum])
vmin = float(np.percentile(stacked, 1))
vmax = float(np.percentile(stacked, 99))
# mean 各値の表示
means = {
"bottom": float(lum_bottom.mean()),
"right": float(lum_right.mean()),
"left": float(lum_left.mean()),
"all": float(lum_all.mean()),
"sum": float(lum_sum.mean()),
}
# ============ パネル生成 ============
fig = plt.figure(figsize=(22, 5))
titles = [
f"① bottom (6 LED)\nmean={means['bottom']:.2f}",
f"② right (6 LED)\nmean={means['right']:.2f}",
f"③ left (6 LED)\nmean={means['left']:.2f}",
f"④ 全 18 LED ON\nmean={means['all']:.2f}",
f"⑤ ①+②+③ の和\nmean={means['sum']:.2f}",
f"⑥ |④-⑤| 線形性誤差\n相対誤差 mean={rel_err*100:.3f}%, max={max_rel_err*100:.3f}%",
]
data = [lum_bottom, lum_right, lum_left, lum_all, lum_sum, diff]
for i, (img, title) in enumerate(zip(data, titles, strict=False)):
ax = fig.add_subplot(1, 6, i + 1)
if i < 5:
im = ax.imshow(img, cmap="hot", vmin=vmin, vmax=vmax)
else:
# 差分は別 colormap (viridis) + 独立 scale
diff_vmax = float(np.percentile(img, 99))
im = ax.imshow(img, cmap="viridis", vmin=0, vmax=max(diff_vmax, 1e-9))
ax.set_title(title, fontsize=10)
ax.set_xticks([])
ax.set_yticks([])
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
fig.suptitle(
"LED グループ別 sanity check — 下/右/左 単独 ON と線形性確認",
fontsize=13, fontweight="bold",
)
plt.tight_layout()
OUTPUT_PANEL.parent.mkdir(parents=True, exist_ok=True)
plt.savefig(OUTPUT_PANEL, dpi=110, bbox_inches="tight")
print(f"パネル保存: {OUTPUT_PANEL}")
print()
print("=== 線形性確認 ===")
print(f" 4 全 ON mean : {means['all']:.4f}")
print(f" 5 ①+②+③ 和 mean : {means['sum']:.4f}")
print(f" 相対誤差 mean : {rel_err*100:.5f}%")
print(f" 相対誤差 max : {max_rel_err*100:.5f}%")
print()
print("=== グループ別 mean ===")
print(f" ① bottom : {means['bottom']:.4f}")
print(f" ② right : {means['right']:.4f}")
print(f" ③ left : {means['left']:.4f}")
print(f" → bottom+right+left = {means['bottom']+means['right']+means['left']:.4f}")
print(f" → 全 ON = {means['all']:.4f}")
return 0
if __name__ == "__main__":
sys.exit(main())