Newer
Older
SmTIAS-LightSim / scripts / build_led_group_sanity_panel.py
"""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)):
        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())