"""下・左・右の単灯白板画像 3 枚を 1 枚に並べて比較するスクリプト.
各 LED を単独点灯(他 2 灯を遮光)して撮影した白板 DNG 3 枚について,
照射の偏り・明るさ・均一性を横並びで比較する.3 灯合成時に互いを補い合えるか
(合成均一性の根拠)を読み取るための図.
構成:
- 上段: 下・左・右の輝度マップ 3 枚を **共通絶対カラースケール** で並置
(どの LED が明るいか/どこを照らすかを同時に比較できる)
- 下段左: 動径プロファイル重ね描き(絶対値.明るさ差と減衰の形を同時に表示)
- 下段右: 数値表(平均・CoV・max/min 比・中心/周辺比・勾配・動径 min/max 比)
すべて linear + LSC 補正・ROI=smtias.whiteboard で揃えて算出する.
使い方:
python scripts/compare_single_leds.py
"""
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from src.analysis.spatial import ( # noqa: E402
calc_distance_map,
calc_radial_min_max_ratio,
calc_radial_profile,
calc_spatial_uniformity,
)
from src.analysis.uniformity import calc_uniformity # noqa: E402
from src.config import load_roi_config # noqa: E402
from src.io.dng_pipeline import dng_luminance # noqa: E402
from src.io.loader import extract_roi # noqa: E402
# 比較対象(単灯 3 枚.label, ファイル名, 描画色)
QUANT_DIR = PROJECT_ROOT / "data" / "smtias" / "quantitative"
LEDS = [
("Bottom LED", QUANT_DIR / "SmTIAS_QM_20260615_141420.dng", "steelblue"),
("Left LED", QUANT_DIR / "SmTIAS_QM_20260615_150300.dng", "seagreen"),
("Right LED", QUANT_DIR / "SmTIAS_QM_20260615_170948.dng", "indianred"),
]
ROI_CONFIG = PROJECT_ROOT / "config" / "roi_config.json"
ROI_KEY = "smtias.whiteboard"
OUTPUT_PATH = PROJECT_ROOT / "output" / "smtias" / "figures" / "compare_single_leds.png"
# 共通絶対カラースケールの設定(外れ値に強いパーセンタイルで vmin/vmax を決める)
SCALE_PERCENTILES = (1.0, 99.0)
def _luminance(dng_path: Path, roi: dict) -> np.ndarray:
"""DNG を linear + LSC 補正で輝度化し ROI を切り出す(portrait)."""
luma_full = dng_luminance(str(dng_path), apply_lsc=True)
return extract_roi(luma_full, roi["x"], roi["y"], roi["width"], roi["height"])
def _metrics(luminance: np.ndarray) -> dict:
"""均一性・空間・動径の指標をまとめて算出する."""
distance_map = calc_distance_map(*luminance.shape)
profile = calc_radial_profile(luminance, distance_map)
radial = calc_radial_min_max_ratio(profile)
metrics = {
**calc_uniformity(luminance),
**calc_spatial_uniformity(luminance)["zone_stats"],
"radial_min_max_ratio": radial["radial_min_max_ratio"],
"profile": profile,
}
return metrics
def main() -> None:
"""3 単灯の比較図を生成して保存する."""
roi = load_roi_config(str(ROI_CONFIG), ROI_KEY)
if roi is None:
raise SystemExit(f"ROI '{ROI_KEY}' が config に見つかりません")
labels = [label for label, _, _ in LEDS]
colors = [color for _, _, color in LEDS]
lumas = [_luminance(path, roi) for _, path, _ in LEDS]
metrics = [_metrics(luma) for luma in lumas]
# 共通絶対カラースケール(3 枚をまとめてパーセンタイル)
flat_all = np.concatenate([luma.ravel() for luma in lumas])
vmin, vmax = np.percentile(flat_all, SCALE_PERCENTILES)
fig = plt.figure(figsize=(15, 9))
gs = fig.add_gridspec(2, 3, height_ratios=[1.0, 0.85], hspace=0.28, wspace=0.22)
# 上段: 輝度マップ 3 枚(共通絶対スケール)
map_axes = []
im = None
for col, (label, luma) in enumerate(zip(labels, lumas)):
ax = fig.add_subplot(gs[0, col])
im = ax.imshow(luma, cmap="hot", interpolation="nearest", vmin=vmin, vmax=vmax)
ax.set_title(f"{label} (mean={metrics[col]['mean']:.1f})", fontsize=11)
ax.set_xlabel("X (px)")
if col == 0:
ax.set_ylabel("Y (px)")
map_axes.append(ax)
cbar = fig.colorbar(im, ax=map_axes, fraction=0.020, pad=0.02)
cbar.set_label(f"Luminance (common scale, p{SCALE_PERCENTILES[0]:g}-p{SCALE_PERCENTILES[1]:g})")
# 下段左: 動径プロファイル重ね描き(絶対値)
ax_prof = fig.add_subplot(gs[1, 0:2])
markers = ["o", "s", "^"]
for label, color, marker, m in zip(labels, colors, markers, metrics):
profile = m["profile"]
ax_prof.plot(
profile[:, 0], profile[:, 1], marker=marker, ms=4, color=color,
label=f"{label} CoV={m['cov']:.3f}",
)
ax_prof.set_title("Radial profile (absolute luminance)", fontsize=11)
ax_prof.set_xlabel("Normalized distance from center")
ax_prof.set_ylabel("Mean luminance")
ax_prof.grid(True, linestyle="--", alpha=0.5)
ax_prof.legend(loc="lower left", fontsize=9)
# 下段右: 数値表
ax_tbl = fig.add_subplot(gs[1, 2])
ax_tbl.axis("off")
metric_rows = [
("Mean luminance", "mean", "{:.2f}"),
("CoV", "cov", "{:.3f}"),
("Max/min ratio", "max_min_ratio", "{:.3f}"),
("Center/periphery", "center_periphery_ratio", "{:.3f}"),
("Gradient (%)", "gradient_magnitude", "{:.2f}"),
("Radial min/max", "radial_min_max_ratio", "{:.3f}"),
]
rows = [
[name] + [fmt.format(m[key]) for m in metrics]
for name, key, fmt in metric_rows
]
table = ax_tbl.table(
cellText=rows,
colLabels=["Metric"] + labels,
cellLoc="center",
loc="center",
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1.0, 1.7)
for j in range(len(labels) + 1):
table[0, j].set_facecolor("#dfe6ee")
table[0, j].set_text_props(weight="bold")
fig.suptitle(
"Single-LED illumination comparison (bottom / left / right)",
fontsize=14, y=0.98,
)
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(str(OUTPUT_PATH), dpi=150, bbox_inches="tight")
plt.close(fig)
print(f"3 単灯比較図を保存しました: {OUTPUT_PATH}")
for label, m in zip(labels, metrics):
print(f" {label}: mean={m['mean']:.2f}, CoV={m['cov']:.4f}, "
f"radial min/max={m['radial_min_max_ratio']:.4f}")
if __name__ == "__main__":
main()