"""max_depth スイープの比較パネル生成.
scripts/sweep_max_depth.py の出力から以下を生成:
- 1×5 輝度マップパネル (shared vmin/vmax)
- 収束カーブ (max_depth vs CoV / 勾配 / C/P / 動径 min/max)
- 数値表
実行方法:
uv run python scripts/build_max_depth_panel.py
前提条件:
- output/max_depth_sweep/summary.csv + d{N}/luminance.npy が揃っていること
出力:
- output/_slide_panels/max_depth_sweep.png (1×5 輝度マップ)
- output/_slide_panels/max_depth_convergence.png (収束カーブ)
"""
from __future__ import annotations
import csv
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]
SWEEP_BASE = ROOT / "output" / "max_depth_sweep"
SUMMARY_CSV = SWEEP_BASE / "summary.csv"
OUT_LUM_PANEL = ROOT / "output" / "_slide_panels" / "max_depth_sweep.png"
OUT_CONV_PANEL = ROOT / "output" / "_slide_panels" / "max_depth_convergence.png"
# 実機 TARGET (linear + LSC,eval_baseline_dng_target.yaml と整合)
TARGET = {"cov": 0.0695, "gradient": 8.35, "cp": 1.091, "rmm": 0.860}
def _setup_japanese_font() -> None:
for p in ("C:/Windows/Fonts/meiryo.ttc", "C:/Windows/Fonts/YuGothM.ttc"):
if Path(p).exists():
fm.fontManager.addfont(p)
plt.rcParams["font.family"] = fm.FontProperties(fname=p).get_name()
return
def main() -> int:
_setup_japanese_font()
if not SUMMARY_CSV.exists():
print(f"エラー: {SUMMARY_CSV} が見つかりません", file=sys.stderr)
return 1
with SUMMARY_CSV.open(encoding="utf-8", newline="") as f:
rows = list(csv.DictReader(f))
if not rows:
print("エラー: summary.csv が空", file=sys.stderr)
return 1
depths = [int(r["max_depth"]) for r in rows]
covs = [float(r["cov"]) for r in rows]
grads = [float(r["gradient_magnitude"]) for r in rows]
cps = [float(r["center_periphery_ratio"]) for r in rows]
rmms = [float(r["radial_min_max_ratio"]) for r in rows]
means = [float(r["mean"]) for r in rows]
# 各 npy 読み込み
lums = []
for r in rows:
npy = SWEEP_BASE / r["job"] / "luminance.npy"
if not npy.exists():
print(f"警告: {npy} 見つからずスキップ", file=sys.stderr)
lums.append(None)
continue
arr = np.load(npy)
lums.append(arr.mean(axis=-1) if arr.ndim == 3 else arr)
# 共有 vmin/vmax
valid = [x for x in lums if x is not None]
stacked = np.stack(valid)
vmin = float(np.percentile(stacked, 1))
vmax = float(np.percentile(stacked, 99))
# ============ 1×5 輝度マップパネル ============
n = len(rows)
fig, axes = plt.subplots(1, n, figsize=(4.5 * n, 5.5))
if n == 1:
axes = [axes]
for ax, d, lum, cov, grad in zip(axes, depths, lums, covs, grads):
if lum is None:
ax.set_title(f"d={d} (missing)")
ax.set_xticks([]); ax.set_yticks([])
continue
im = ax.imshow(lum, cmap="hot", vmin=vmin, vmax=vmax)
ax.set_title(f"max_depth = {d}\nCoV={cov:.4f} 勾配={grad:.2f}%", fontsize=11)
ax.set_xticks([]); ax.set_yticks([])
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
fig.suptitle("max_depth スイープ — 輝度マップ (shared vmin/vmax)", fontsize=14, fontweight="bold")
plt.tight_layout()
OUT_LUM_PANEL.parent.mkdir(parents=True, exist_ok=True)
plt.savefig(OUT_LUM_PANEL, dpi=110, bbox_inches="tight")
print(f"輝度マップパネル保存: {OUT_LUM_PANEL}")
# ============ 収束カーブ ============
fig, axes = plt.subplots(2, 2, figsize=(13, 9))
metrics = [
("CoV", covs, TARGET["cov"], "CoV (低いほど均一)"),
("勾配 [%]", grads, TARGET["gradient"], "勾配 [%] (低いほど均一)"),
("C/P 比", cps, TARGET["cp"], "C/P 比 (1.0 が理想)"),
("動径 min/max", rmms, TARGET["rmm"], "動径 min/max (1.0 が理想)"),
]
for ax, (label, vals, target, ylabel) in zip(axes.flat, metrics):
ax.plot(depths, vals, marker="o", linewidth=2, markersize=10, color="C0", label="シミュ")
ax.axhline(target, color="red", linestyle="--", linewidth=1.5,
label=f"実機 TARGET ({target})")
for d, v in zip(depths, vals):
ax.annotate(f"{v:.4f}", (d, v), textcoords="offset points",
xytext=(0, 10), ha="center", fontsize=9)
ax.set_xlabel("max_depth")
ax.set_ylabel(ylabel)
ax.set_title(label)
ax.grid(alpha=0.3)
ax.legend(loc="best", fontsize=9)
fig.suptitle(
"max_depth スイープ — 形状指標の収束 (赤破線=実機 TARGET)",
fontsize=14, fontweight="bold",
)
plt.tight_layout()
plt.savefig(OUT_CONV_PANEL, dpi=110, bbox_inches="tight")
print(f"収束カーブ保存: {OUT_CONV_PANEL}")
# ============ コンソールサマリ ============
print()
print("=== サマリ ===")
print(f" {'max_depth':<10}{'mean':<10}{'CoV':<10}{'勾配%':<10}{'C/P':<10}{'r min/max':<10}")
for d, mean, cov, grad, cp, rmm in zip(depths, means, covs, grads, cps, rmms):
print(f" {d:<10}{mean:<10.2f}{cov:<10.4f}{grad:<10.2f}{cp:<10.4f}{rmm:<10.4f}")
print(f" 実機 TARGET: - {TARGET['cov']:<10}{TARGET['gradient']:<10}"
f"{TARGET['cp']:<10}{TARGET['rmm']:<10}")
return 0
if __name__ == "__main__":
sys.exit(main())