diff --git a/scripts/build_led_placement_maps.py b/scripts/build_led_placement_maps.py index 045dd07..5d8b467 100644 --- a/scripts/build_led_placement_maps.py +++ b/scripts/build_led_placement_maps.py @@ -2,7 +2,12 @@ 数値指標(build_led_placement_comparison.py)とは別に,輝度分布の「形」を目で比べるための 画像パネル.各パネルは球(上段)と同内面積の立方体(下段)を同じ条件の並びで置き, -輪帯ピーク正規化(最大の輪帯平均 = 1.0)・共通カラースケールで描く.LED は +X=画像右側. +平滑化後の 99 パーセンタイルを 1.0 とするロバスト最大値正規化(`--normalize` で輪帯ピーク・ +ROI 平均にも切替可)・共通カラースケールで描く.LED は +X=画像右側. + +正規化の注意: 従来の輪帯ピーク正規化(20 輪帯平均の最大 = 1.0)は同心円状の分布を前提にしており, +非対称な分布では同じ輪帯に明るい側と暗い側が混ざって基準が下がり,明るい側が 1.0 を超えて飛ぶ. +本スクリプトの既定はこれを避けるため方位に依らないロバスト最大値を使う. 出力(output/led_placement_sweep/maps/): maps_position_<ペア>.png : θ を横に並べる(13 段階).4 ペア分 @@ -24,6 +29,7 @@ import matplotlib import numpy as np +from scipy.ndimage import gaussian_filter, median_filter matplotlib.use("Agg") import matplotlib.pyplot as plt # noqa: E402 @@ -47,15 +53,30 @@ Condition, ) +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"] SHAPE_LABEL = {"sphere": "球", "cube": "立方体"} AIM_LABEL = {"board": "板向き+バッフル", "normal": "壁法線向き+バッフル", "down": "真下向き"} CMAP = "hot" -# 共通スケールの下限(輪帯ピーク=1.0 に対する比).全パネルで固定して見比べられるようにする +# 共通スケールの下限(最大=1.0 に対する比).全パネルで固定して見比べられるようにする VMIN = 0.80 REP_PAIR = 2 # 代表ペア(r=170 / L=234) +# 正規化の方式(--normalize): +# robust_peak: 平滑化後の 99 パーセンタイルを 1.0 にする(既定).方位に依らず「最も明るい所 +# からの落ち込み」を表す.画素の最大値は MC ノイズの外れ値に張り付くので +# パーセンタイルで避ける +# ring_peak : 20 輪帯平均の最大値を 1.0(従来).同心円状の分布向けで,非対称な分布では同じ輪帯に +# 明るい側と暗い側が混ざって基準が下がり,明るい側が 1.0 を超えて飛ぶ +# mean : ROI 平均を 1.0.CoV・mean anchor 正規化と同じ基準(明るい側が 1 を超える) +NORMALIZE_MODES = ("robust_peak", "ring_peak", "mean") +ROBUST_PERCENTILE = 99.0 +SMOOTH_MEDIAN = 5 +SMOOTH_SIGMA = 4.0 +_normalize = "robust_peak" + def output_root(material: str) -> Path: """材質系列に対応する出力ディレクトリ.""" @@ -68,10 +89,40 @@ return Condition("", shape, size, theta, aim, n, rep, azimuth).name +def smoothed_roi(path: Path) -> np.ndarray: + """luminance.npy の ROI を表示用に平滑化して返す(normalized_map と同じ平滑化).""" + 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) + return gaussian_filter(median_filter(roi, size=SMOOTH_MEDIAN), sigma=SMOOTH_SIGMA) + + +def normalize_map(path: Path, mode: str = "robust_peak") -> np.ndarray: + """輝度マップを指定方式で正規化して返す(表示用に平滑化済み).""" + if mode == "ring_peak": + return normalized_map(path) + smooth = smoothed_roi(path) + if mode == "mean": + return smooth / float(smooth.mean()) + if mode == "robust_peak": + return smooth / float(np.percentile(smooth, ROBUST_PERCENTILE)) + raise ValueError(f"未知の正規化方式: {mode}({', '.join(NORMALIZE_MODES)})") + + def load_map(name: str, material: str = "legacy") -> np.ndarray | None: - """条件名から輪帯ピーク正規化・平滑化済みの輝度マップを返す(無ければ None).""" + """条件名から正規化・平滑化済みの輝度マップを返す(無ければ None).""" path = output_root(material) / name / "luminance.npy" - return normalized_map(path) if path.exists() else None + return normalize_map(path, _normalize) if path.exists() else None + + +def colorbar_label() -> str: + """正規化方式に応じたカラーバーの説明.""" + return { + "robust_peak": f"輝度(平滑化後 {ROBUST_PERCENTILE:g} パーセンタイル = 1.0)", + "ring_peak": "輝度(最大の輪帯平均 = 1.0)", + "mean": "輝度(ROI 平均 = 1.0)", + }[_normalize] def row_label(shape: str, size: float) -> str: @@ -105,13 +156,14 @@ ax.axis("off") continue label, data = row[c] - image = ax.imshow(data, cmap=CMAP, vmin=VMIN, vmax=1.0) + vmax = 1.0 if _normalize != "mean" else 1.0 + (1.0 - VMIN) + image = ax.imshow(data, cmap=CMAP, vmin=VMIN, vmax=vmax) ax.set_title(label, fontsize=8) if c == 0: ax.set_ylabel(row_labels[r], fontsize=10, fontweight="bold") if image is not None: fig.colorbar(image, ax=axes.ravel().tolist(), fraction=0.015, pad=0.01, - label="輝度(最大の輪帯平均 = 1.0)") + label=colorbar_label()) fig.suptitle(title, fontsize=13) output.parent.mkdir(parents=True, exist_ok=True) fig.savefig(output, dpi=110, bbox_inches="tight") @@ -238,7 +290,11 @@ """全パネルを生成する.""" parser = argparse.ArgumentParser(description="照明配置スイープの輝度マップパネルを生成") parser.add_argument("--output", type=Path, default=None, help="出力ディレクトリ") + parser.add_argument("--normalize", choices=NORMALIZE_MODES, default="robust_peak", + help="正規化方式(既定 robust_peak=平滑化後 99 パーセンタイル=1.0)") args = parser.parse_args() + global _normalize # noqa: PLW0603 + _normalize = args.normalize out_dir = args.output or output_root("legacy") / "maps" panel_position(out_dir) panel_direction(out_dir)