"""最小/最大比の「最大・最小の定義」による違いを軸上スイープ(TEST_15/16)で比較する.
均一性の指標として使っている最小/最大比は,最大・最小をどう取るかで値が変わる.
本スクリプトは同じ luminance.npy に対して次の定義を計算し,サイズ依存の傾向と定義間の差を
表と図にする(TEST_18 の非対称な分布に進む前に,対称な分布で各定義の基準を押さえる).
定義:
pixel : 画素の min / max(評価パイプラインの max_min_ratio と同じ.MC ノイズに支配される)
ring : 20 輪帯平均の min / max(評価パイプラインの radial_min_max_ratio と同じ)
ring_inner : 輪帯平均のうち四隅だけの最外輪帯(d > 0.95)を除いた min / max
(scripts/build_radial_comparison.py の ring_min_max_ratio と同じ)
smooth : 平滑化(メディアン 5 → ガウス σ=4)後の画素の min / max
robust : 平滑化後の 1 / 99 パーセンタイル(方位に依らないロバスト定義.TEST_18 の
マップ表示と同じ「最大」)
出力(output/_normalization_comparison/):
minmax_definitions.csv : 条件 × 定義の値
minmax_definitions.png : サイズ依存を定義ごとに重ね描き(球・立方体)
実行方法:
uv run python scripts/build_minmax_definition_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 ROBUST_PERCENTILE, smoothed_roi # noqa: E402
from build_normalization_comparison import OUT_DIR, conditions # noqa: E402
from build_radial_comparison import MAX_DISTANCE # 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"]
DEFINITIONS = ("pixel", "ring", "ring_inner", "smooth", "robust")
DEF_LABEL = {
"pixel": "画素 min/max",
"ring": "輪帯平均 min/max(20 ビン)",
"ring_inner": "輪帯平均 min/max(最外ビン除外)",
"smooth": "平滑化後 画素 min/max",
"robust": f"平滑化後 {100 - ROBUST_PERCENTILE:g}/{ROBUST_PERCENTILE:g} %ile",
}
DEF_STYLE = {
"pixel": dict(marker="x", ls=":", color="gray"),
"ring": dict(marker="o", ls="-", color="tab:blue"),
"ring_inner": dict(marker="o", ls="--", color="tab:cyan"),
"smooth": dict(marker="s", ls="-.", color="tab:orange"),
"robust": dict(marker="D", ls="-", color="tab:red"),
}
LOW_PERCENTILE = 100.0 - ROBUST_PERCENTILE
def min_max_ratios(path: Path) -> dict[str, float]:
"""1 条件の輝度マップについて全定義の最小/最大比を返す."""
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)
centers, means = profile[:, 0], profile[:, 1]
valid = ~np.isnan(means)
inner = valid & (centers <= MAX_DISTANCE)
smooth = smoothed_roi(path)
return {
"pixel": float(roi.min() / roi.max()),
"ring": float(means[valid].min() / means[valid].max()),
"ring_inner": float(means[inner].min() / means[inner].max()),
"smooth": float(smooth.min() / smooth.max()),
"robust": float(np.percentile(smooth, LOW_PERCENTILE)
/ np.percentile(smooth, ROBUST_PERCENTILE)),
}
def draw(rows: list[dict], output: Path) -> None:
"""定義ごとの最小/最大比をサイズに対して描く(球・立方体で 1 枚ずつ + 定義間の差)."""
fig, axes = plt.subplots(1, 3, figsize=(19, 5.5))
for ax, shape, xlabel in ((axes[0], "sphere", "球 内径 φ [mm]"),
(axes[1], "cube", "立方体 内寸 L [mm]")):
sel = [r for r in rows if r["shape"] == shape]
scale = 2.0 if shape == "sphere" else 1.0 # 球は内径で描く
sizes = [r["size_mm"] * scale for r in sel]
for d in DEFINITIONS:
ax.plot(sizes, [r[d] for r in sel], label=DEF_LABEL[d], ms=5, lw=1.4, **DEF_STYLE[d])
ax.set_xlabel(xlabel)
ax.set_ylabel("最小/最大比(1 が理想)")
ax.set_title("球 similar_radial(TEST_15)" if shape == "sphere" else "立方体(TEST_16)",
fontsize=11)
ax.grid(alpha=0.3)
ax.legend(fontsize=8)
ax = axes[2]
for shape, marker in (("sphere", "o"), ("cube", "s")):
sel = [r for r in rows if r["shape"] == shape]
areas = [r["area"] for r in sel]
label = "球" if shape == "sphere" else "立方体"
ax.plot(areas, [r["ring_inner"] - r["robust"] for r in sel], marker=marker, ls="-",
color="tab:red", label=f"{label}: 輪帯(最外除外)− ロバスト")
ax.plot(areas, [r["ring"] - r["ring_inner"] for r in sel], marker=marker, ls="--",
color="tab:blue", label=f"{label}: 輪帯(全) − 輪帯(最外除外)")
ax.plot(areas, [r["smooth"] - r["robust"] for r in sel], marker=marker, ls=":",
color="tab:orange", label=f"{label}: 平滑化 min/max − ロバスト")
ax.axhline(0.0, color="gray", lw=0.8)
ax.set_xscale("log")
ax.set_xlabel("キャビティ内面積 (mm²・対数)")
ax.set_ylabel("定義間の差")
ax.set_title("定義間の差(画素 min/max は桁が違うので除外)", fontsize=11)
ax.grid(alpha=0.3, which="both")
ax.legend(fontsize=7)
fig.suptitle("最小/最大比の定義による違い — 軸上 1 灯・板向き・バッフル付き(対称な分布)",
fontsize=13)
fig.tight_layout()
output.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(output, dpi=110)
plt.close(fig)
print(f"出力: {output}")
def main() -> int:
"""全定義の最小/最大比を計算して表と図を出す."""
parser = argparse.ArgumentParser(description="最小/最大比の定義による違いを比較")
parser.parse_args()
from sweep_cube_size import cavity_geometry as cube_geo
from sweep_sphere_radius import cavity_geometry as sphere_geo
rows = []
for shape, items in conditions().items():
for label, path in items:
size = float(label[1:]) / 2.0 if label.startswith("φ") else float(label.split("=")[1])
area = (sphere_geo(size, "similar_radial") if shape == "sphere"
else cube_geo(size))["cavity_area_mm2"]
row = {"shape": shape, "size_mm": size, "area": area}
row.update(min_max_ratios(path))
rows.append(row)
if not rows:
print("エラー: TEST_15/16 の結果が見つかりません")
return 1
OUT_DIR.mkdir(parents=True, exist_ok=True)
with (OUT_DIR / "minmax_definitions.csv").open("w", newline="", encoding="utf-8") as fh:
writer = csv.DictWriter(fh, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
draw(rows, OUT_DIR / "minmax_definitions.png")
print(f"\n{'形状':>7} {'サイズ':>6} " + " ".join(f"{d:>11}" for d in DEFINITIONS)
+ f" {'輪帯内−ロバスト':>14}")
for r in rows:
print(f"{r['shape']:>7} {r['size_mm']:6g} "
+ " ".join(f"{r[d]:11.4f}" for d in DEFINITIONS)
+ f" {r['ring_inner'] - r['robust']:+14.4f}")
return 0
if __name__ == "__main__":
sys.exit(main())