diff --git a/configs/_tmp_fix_bottom.yaml b/configs/_tmp_fix_bottom.yaml new file mode 100644 index 0000000..4390f6a --- /dev/null +++ b/configs/_tmp_fix_bottom.yaml @@ -0,0 +1,196 @@ +name: fix_single_bottom +description: 'TEST_12 相対結論再確認: bottom 単灯(面法線修正後, blend w0.05_a0.1, d=16, spp=1024)' +geometry: + meshes: + - id: housing_front + path: meshes/前側.stl + unit: mm + material: printer_white + - id: housing_back + path: meshes/後側.stl + unit: mm + material: printer_white + - id: phone_holder + path: meshes/スマホホルダ.stl + unit: mm + material: holder_dark + - id: led_cover_bottom + path: meshes/LED基板カバー下.stl + unit: mm + material: printer_white + - id: led_cover_right + path: meshes/LED基板カバー右.stl + unit: mm + material: printer_white + - id: led_cover_left + path: meshes/LED基板カバー左.stl + unit: mm + material: printer_white + - id: phone_body + path: meshes/SH-02M.stl + unit: mm + material: phone_body_dark + - id: evaluation_board + path: meshes/照明ムラ補正ボード.stl + unit: mm + material: printer_white +materials: + printer_white: + type: blendbsdf + weight: 0.05 + components: + - type: diffuse + reflectance: + - 0.85 + - 0.85 + - 0.85 + - type: roughconductor + alpha: 0.1 + distribution: ggx + holder_dark: + type: diffuse + reflectance: + - 0.5 + - 0.5 + - 0.5 + phone_body_dark: + type: diffuse + reflectance: + - 0.05 + - 0.05 + - 0.05 +lights: +- id: led_bottom_0 + type: area + shape: rectangle + center: + - -10.0 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_1 + type: area + shape: rectangle + center: + - -5.8 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_2 + type: area + shape: rectangle + center: + - -1.6 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_3 + type: area + shape: rectangle + center: + - 9.2 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_4 + type: area + shape: rectangle + center: + - 13.4 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_5 + type: area + shape: rectangle + center: + - 17.6 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +sensor: + type: orthographic + position: + - 0.0 + - 0.0 + - -50.0 + target: + - 0.0 + - 0.0 + - 0.0 + up: + - 0.0 + - 1.0 + - 0.0 + ortho_scale: + - 50.0 + - 52.0 + film: + width: 512 + height: 512 + rfilter: gaussian +render: + variant: llvm_ad_rgb + integrator: path + max_depth: 16 + spp: 1024 diff --git a/configs/_tmp_fix_left.yaml b/configs/_tmp_fix_left.yaml new file mode 100644 index 0000000..015e71e --- /dev/null +++ b/configs/_tmp_fix_left.yaml @@ -0,0 +1,196 @@ +name: fix_single_left +description: 'TEST_12 相対結論再確認: left 単灯(面法線修正後, blend w0.05_a0.1, d=16, spp=1024)' +geometry: + meshes: + - id: housing_front + path: meshes/前側.stl + unit: mm + material: printer_white + - id: housing_back + path: meshes/後側.stl + unit: mm + material: printer_white + - id: phone_holder + path: meshes/スマホホルダ.stl + unit: mm + material: holder_dark + - id: led_cover_bottom + path: meshes/LED基板カバー下.stl + unit: mm + material: printer_white + - id: led_cover_right + path: meshes/LED基板カバー右.stl + unit: mm + material: printer_white + - id: led_cover_left + path: meshes/LED基板カバー左.stl + unit: mm + material: printer_white + - id: phone_body + path: meshes/SH-02M.stl + unit: mm + material: phone_body_dark + - id: evaluation_board + path: meshes/照明ムラ補正ボード.stl + unit: mm + material: printer_white +materials: + printer_white: + type: blendbsdf + weight: 0.05 + components: + - type: diffuse + reflectance: + - 0.85 + - 0.85 + - 0.85 + - type: roughconductor + alpha: 0.1 + distribution: ggx + holder_dark: + type: diffuse + reflectance: + - 0.5 + - 0.5 + - 0.5 + phone_body_dark: + type: diffuse + reflectance: + - 0.05 + - 0.05 + - 0.05 +lights: +- id: led_left_0 + type: area + shape: rectangle + center: + - -66.0067 + - -32.5952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_1 + type: area + shape: rectangle + center: + - -66.0067 + - -28.3952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_2 + type: area + shape: rectangle + center: + - -66.0067 + - -24.1952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_3 + type: area + shape: rectangle + center: + - -66.0067 + - -13.3952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_4 + type: area + shape: rectangle + center: + - -66.0067 + - -9.1952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_5 + type: area + shape: rectangle + center: + - -66.0067 + - -4.9952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +sensor: + type: orthographic + position: + - 0.0 + - 0.0 + - -50.0 + target: + - 0.0 + - 0.0 + - 0.0 + up: + - 0.0 + - 1.0 + - 0.0 + ortho_scale: + - 50.0 + - 52.0 + film: + width: 512 + height: 512 + rfilter: gaussian +render: + variant: llvm_ad_rgb + integrator: path + max_depth: 16 + spp: 1024 diff --git a/configs/_tmp_fix_right.yaml b/configs/_tmp_fix_right.yaml new file mode 100644 index 0000000..7a45a34 --- /dev/null +++ b/configs/_tmp_fix_right.yaml @@ -0,0 +1,196 @@ +name: fix_single_right +description: 'TEST_12 相対結論再確認: right 単灯(面法線修正後, blend w0.05_a0.1, d=16, spp=1024)' +geometry: + meshes: + - id: housing_front + path: meshes/前側.stl + unit: mm + material: printer_white + - id: housing_back + path: meshes/後側.stl + unit: mm + material: printer_white + - id: phone_holder + path: meshes/スマホホルダ.stl + unit: mm + material: holder_dark + - id: led_cover_bottom + path: meshes/LED基板カバー下.stl + unit: mm + material: printer_white + - id: led_cover_right + path: meshes/LED基板カバー右.stl + unit: mm + material: printer_white + - id: led_cover_left + path: meshes/LED基板カバー左.stl + unit: mm + material: printer_white + - id: phone_body + path: meshes/SH-02M.stl + unit: mm + material: phone_body_dark + - id: evaluation_board + path: meshes/照明ムラ補正ボード.stl + unit: mm + material: printer_white +materials: + printer_white: + type: blendbsdf + weight: 0.05 + components: + - type: diffuse + reflectance: + - 0.85 + - 0.85 + - 0.85 + - type: roughconductor + alpha: 0.1 + distribution: ggx + holder_dark: + type: diffuse + reflectance: + - 0.5 + - 0.5 + - 0.5 + phone_body_dark: + type: diffuse + reflectance: + - 0.05 + - 0.05 + - 0.05 +lights: +- id: led_right_0 + type: area + shape: rectangle + center: + - 65.9987 + - -24.9738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_1 + type: area + shape: rectangle + center: + - 65.9987 + - -20.7738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_2 + type: area + shape: rectangle + center: + - 65.9987 + - -16.5738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_3 + type: area + shape: rectangle + center: + - 65.9987 + - -5.7738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_4 + type: area + shape: rectangle + center: + - 65.9987 + - -1.5738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_5 + type: area + shape: rectangle + center: + - 65.9987 + - 2.6262 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +sensor: + type: orthographic + position: + - 0.0 + - 0.0 + - -50.0 + target: + - 0.0 + - 0.0 + - 0.0 + up: + - 0.0 + - 1.0 + - 0.0 + ortho_scale: + - 50.0 + - 52.0 + film: + width: 512 + height: 512 + rfilter: gaussian +render: + variant: llvm_ad_rgb + integrator: path + max_depth: 16 + spp: 1024 diff --git a/scripts/build_full_vs_singles_panel.py b/scripts/build_full_vs_singles_panel.py new file mode 100644 index 0000000..7985bde --- /dev/null +++ b/scripts/build_full_vs_singles_panel.py @@ -0,0 +1,127 @@ +"""全灯と単灯 3 灯の形比較パネル生成(面法線修正後). + +行 = シミュ(面法線修正後)/ 実機,列 = 全灯 / bottom / left / right の 2×4 グリッド. +単灯シミュ(spp=1024)はノイズ 5.3% > 構造振幅 4% のため,全セルに同一の平滑化 +(median 5px + gaussian σ=4)を適用した「形の比較専用」表示とする. +各セルは mean 正規化+独立 1–99% 正規化,シミュセルには対応する実機マップとの +NCC(平滑後)を併記する. + +生成するファイル: + output/_slide_panels/full_vs_singles_fix.png + +使い方: + uv run python scripts/build_full_vs_singles_panel.py + +前提: + - output/full_base_fix/luminance.npy・output/singles_fix/{bottom,left,right}/luminance.npy + - lightsim/luminance_maps/{full,bottom,left,right}_*.npy(実機) +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +from scipy.ndimage import gaussian_filter, median_filter +from skimage.transform import resize + +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh +from sm_tias_lightsim.evaluation.shape_metrics import calc_ncc + +matplotlib.rcParams["font.family"] = ["Meiryo", "Yu Gothic", "DejaVu Sans"] + +ROOT = Path(__file__).resolve().parents[1] +PANEL_PATH = ROOT / "output" / "_slide_panels" / "full_vs_singles_fix.png" + +COLS = ["full", "bottom", "left", "right"] +COL_LABELS = { + "full": "全灯 18 灯", + "bottom": "bottom 単灯", + "left": "left 単灯", + "right": "right 単灯", +} +SIM = { + "full": ROOT / "output" / "full_base_fix" / "luminance.npy", + "bottom": ROOT / "output" / "singles_fix" / "bottom" / "luminance.npy", + "left": ROOT / "output" / "singles_fix" / "left" / "luminance.npy", + "right": ROOT / "output" / "singles_fix" / "right" / "luminance.npy", +} +REAL = { + "full": ROOT / "lightsim" / "luminance_maps" / "full_MiniTIAS_QM_20260601_124500.npy", + "bottom": ROOT / "lightsim" / "luminance_maps" / "bottom_SmTIAS_QM_20260615_141420.npy", + "left": ROOT / "lightsim" / "luminance_maps" / "left_SmTIAS_QM_20260615_150300.npy", + "right": ROOT / "lightsim" / "luminance_maps" / "right_SmTIAS_QM_20260615_170948.npy", +} + +CELL_SHAPE = (450, 464) + + +def _load_smooth(path: Path, is_sim: bool) -> np.ndarray: + """luminance を読み込み(シミュは SIM_ROI 抽出後)平滑化して返す.""" + arr = np.load(path) + if arr.ndim == 3: + arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2] + arr = arr.astype(np.float64) + if is_sim: + arr = extract_roi_xywh(arr, SIM_ROI) + return gaussian_filter(median_filter(arr, size=5), sigma=4) + + +def main() -> int: + """2×4 形比較パネルを生成するメインエントリポイント. + + Returns: + 終了コード(成功なら 0,入力不足なら 1). + """ + for paths in (SIM, REAL): + for p in paths.values(): + if not p.exists(): + print(f"エラー: {p} が見つかりません", file=sys.stderr) + return 1 + + sim = {c: _load_smooth(SIM[c], True) for c in COLS} + real = {c: _load_smooth(REAL[c], False) for c in COLS} + + fig, axes = plt.subplots(2, 4, figsize=(4 * 4.2, 2 * 4.4), constrained_layout=True) + for r, (row_label, maps) in enumerate((("シミュ(面法線修正後)", sim), ("実機", real))): + for c, col in enumerate(COLS): + ax = axes[r][c] + img = maps[col] + disp = resize(img / img.mean(), CELL_SHAPE, anti_aliasing=True) + ax.imshow( + disp, + cmap="hot", + vmin=float(np.percentile(disp, 1.0)), + vmax=float(np.percentile(disp, 99.0)), + aspect="equal", + ) + h = img.shape[0] + pm = img.max() / img.mean() + tb = img[: h // 2].mean() / img[h // 2 :].mean() + title = f"p/m={pm:.3f} t/b={tb:.3f}" + if r == 0: + ncc = calc_ncc(resize(img, real[col].shape, anti_aliasing=True), real[col]) + title = f"{COL_LABELS[col]}\n{title} NCC対実機={ncc:.3f}" + ax.set_title(title, fontsize=10, pad=6) + ax.set_axis_off() + axes[r][0].text( + -0.06, 0.5, row_label, ha="right", va="center", rotation=90, + transform=axes[r][0].transAxes, fontsize=12, fontweight="bold", + ) + fig.suptitle( + "全灯 vs 単灯 3 灯の形比較(面法線修正後)— blend w0.05_a0.1, spp=1024, d=16, ROI 揃え\n" + "全セル同一平滑化(median5+gauss σ4)・mean 正規化・独立 1–99% 表示(形の比較専用)", + fontsize=12, + ) + PANEL_PATH.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(PANEL_PATH, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"パネル保存: {PANEL_PATH}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/build_singles_fix_summary.py b/scripts/build_singles_fix_summary.py new file mode 100644 index 0000000..103dca3 --- /dev/null +++ b/scripts/build_singles_fix_summary.py @@ -0,0 +1,185 @@ +"""TEST_12 相対結論再確認: 面法線修正後の単灯 3 ジョブの形指標・灯間 NCC 集計. + +TEST_09 の相対結論「シミュは 3 灯ほぼ同形(灯間 NCC 0.95x)=実機の非対称相補性 +(0.85/0.61/0.41)を再現できない」を面法線修正後のデータで再検証する. +単灯の形(peak/mean・top/bot・NCC 対実機)も修正前後で対比する. + +生成するファイル: + output/singles_fix/summary.csv -- 単灯形指標(修正前後の対比付き) + output/singles_fix/pairwise_ncc.csv -- 灯間 NCC(修正前 / 修正後 / 実機) + output/_slide_panels/singles_fix_shape_roi.png -- 3 行(灯)× 3 列のパネル + +使い方: + uv run python scripts/build_singles_fix_summary.py + +前提: + - output/singles_fix/{bottom,left,right}/luminance.npy(面法線修正後) + - output/blend_bottom/{,left_,right_}w0.05_a0.1/luminance.npy(修正前,対比用) + - lightsim/luminance_maps/{bottom,left,right}_*.npy(実機単灯) +""" + +from __future__ import annotations + +import csv +import sys +from itertools import combinations +from pathlib import Path + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +from skimage.transform import resize + +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh +from sm_tias_lightsim.evaluation.shape_metrics import calc_ncc + +matplotlib.rcParams["font.family"] = ["Meiryo", "Yu Gothic", "DejaVu Sans"] + +ROOT = Path(__file__).resolve().parents[1] +SUMMARY_CSV = ROOT / "output" / "singles_fix" / "summary.csv" +PAIRWISE_CSV = ROOT / "output" / "singles_fix" / "pairwise_ncc.csv" +PANEL_PATH = ROOT / "output" / "_slide_panels" / "singles_fix_shape_roi.png" + +SIDES = ["bottom", "left", "right"] +REAL = { + "bottom": ROOT / "lightsim" / "luminance_maps" / "bottom_SmTIAS_QM_20260615_141420.npy", + "left": ROOT / "lightsim" / "luminance_maps" / "left_SmTIAS_QM_20260615_150300.npy", + "right": ROOT / "lightsim" / "luminance_maps" / "right_SmTIAS_QM_20260615_170948.npy", +} +OLD_BASE = { + "bottom": ROOT / "output" / "blend_bottom" / "w0.05_a0.1" / "luminance.npy", + "left": ROOT / "output" / "blend_bottom" / "left_w0.05_a0.1" / "luminance.npy", + "right": ROOT / "output" / "blend_bottom" / "right_w0.05_a0.1" / "luminance.npy", +} +FIX_BASE = {s: ROOT / "output" / "singles_fix" / s / "luminance.npy" for s in SIDES} + +CELL_SHAPE = (450, 464) + + +def _load_luminance(path: Path) -> np.ndarray: + """luminance.npy を読み込み 2D 輝度にする(3D RGB は Rec.709 変換).""" + arr = np.load(path) + if arr.ndim == 3: + arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2] + return arr.astype(np.float64) + + +def _shape_metrics(img: np.ndarray, real: np.ndarray | None) -> dict[str, float]: + """TEST_09/10 共通の形指標(peak/mean・top/bot・NCC 対実機).""" + h = img.shape[0] + m = { + "peak_mean": float(img.max() / img.mean()), + "top_bot": float(img[: h // 2].mean() / img[h // 2 :].mean()), + } + if real is not None: + m["ncc"] = float(calc_ncc(resize(img, real.shape, anti_aliasing=True), real)) + else: + m["ncc"] = float("nan") + return m + + +def _pairwise_ncc(maps: dict[str, np.ndarray]) -> dict[str, float]: + """3 灯マップの灯間 NCC(mean 正規化・フル frame,TEST_10 の流儀).""" + out = {} + for a, b in combinations(SIDES, 2): + ma, mb = maps[a], maps[b] + if ma.shape != mb.shape: + mb = resize(mb, ma.shape, anti_aliasing=True) + out[f"{a}_vs_{b}"] = float(calc_ncc(ma / ma.mean(), mb / mb.mean())) + return out + + +def main() -> int: + """集計 CSV とパネルを生成するメインエントリポイント. + + Returns: + 終了コード(成功なら 0,入力不足なら 1). + """ + for paths in (FIX_BASE, REAL, OLD_BASE): + for p in paths.values(): + if not p.exists(): + print(f"エラー: {p} が見つかりません", file=sys.stderr) + return 1 + + real = {s: _load_luminance(REAL[s]) for s in SIDES} + sets = { + "old": {s: extract_roi_xywh(_load_luminance(OLD_BASE[s]), SIM_ROI) for s in SIDES}, + "fix": {s: extract_roi_xywh(_load_luminance(FIX_BASE[s]), SIM_ROI) for s in SIDES}, + } + + SUMMARY_CSV.parent.mkdir(parents=True, exist_ok=True) + with SUMMARY_CSV.open("w", encoding="utf-8", newline="") as f: + writer = csv.writer(f) + writer.writerow(["side", "version", "peak_mean", "top_bot", "ncc_vs_real"]) + for s in SIDES: + for ver, maps in sets.items(): + m = _shape_metrics(maps[s], real[s]) + writer.writerow( + [s, ver, f"{m['peak_mean']:.4f}", f"{m['top_bot']:.4f}", f"{m['ncc']:.4f}"] + ) + rm = _shape_metrics(real[s], real[s]) + writer.writerow([s, "real", f"{rm['peak_mean']:.4f}", f"{rm['top_bot']:.4f}", "1.0000"]) + print(f"summary 保存: {SUMMARY_CSV}") + + # raw の NCC は MC ノイズで 0 方向へ希釈される(単灯 spp=1024 はノイズ > 構造振幅)ため, + # 平滑化(median 5px + gaussian σ=4)後の構造のみの NCC を併記する.解釈は smooth を正とする + from scipy.ndimage import gaussian_filter, median_filter + + def _smooth(m: np.ndarray) -> np.ndarray: + return gaussian_filter(median_filter(m, size=5), sigma=4) + + with PAIRWISE_CSV.open("w", encoding="utf-8", newline="") as f: + writer = csv.writer(f) + writer.writerow(["set", "bottom_vs_left", "bottom_vs_right", "left_vs_right"]) + for label, maps in [("old", sets["old"]), ("fix", sets["fix"]), ("real", real)]: + smoothed = {s: _smooth(m) for s, m in maps.items()} + for suffix, mm in (("raw", maps), ("smooth", smoothed)): + ncc = _pairwise_ncc(mm) + writer.writerow( + [ + f"{label}_{suffix}", + f"{ncc['bottom_vs_left']:.4f}", + f"{ncc['bottom_vs_right']:.4f}", + f"{ncc['left_vs_right']:.4f}", + ] + ) + print(f"pairwise 保存: {PAIRWISE_CSV}") + + # パネル: 行=灯,列=修正前 / 修正後 / 実機(各セル独立 1–99% 正規化) + cols = [("修正前(スムーズ法線)", sets["old"]), ("修正後(面法線)", sets["fix"])] + fig, axes = plt.subplots(3, 3, figsize=(3 * 4.2, 3 * 4.2), constrained_layout=True) + for r, s in enumerate(SIDES): + cells = [maps[s] for _, maps in cols] + [real[s]] + titles = [label for label, _ in cols] + ["実機"] + for c, (img, title) in enumerate(zip(cells, titles, strict=True)): + ax = axes[r][c] + disp = resize(img / img.mean(), CELL_SHAPE, anti_aliasing=True) + ax.imshow( + disp, + cmap="hot", + vmin=float(np.percentile(disp, 1.0)), + vmax=float(np.percentile(disp, 99.0)), + aspect="equal", + ) + m = _shape_metrics(img, None) + t = f"p/m={m['peak_mean']:.3f} t/b={m['top_bot']:.3f}" + ax.set_title(f"{title}\n{t}" if r == 0 else t, fontsize=10, pad=6) + ax.set_axis_off() + axes[r][0].text( + -0.06, 0.5, s, ha="right", va="center", rotation=90, + transform=axes[r][0].transAxes, fontsize=13, fontweight="bold", + ) + fig.suptitle( + "TEST_12 相対結論再確認 — 単灯 blend w0.05_a0.1, spp=1024, d=16, ROI 揃え\n" + "各セル独立 1–99% 正規化(形の比較専用)・全セル共通解像度", + fontsize=12, + ) + PANEL_PATH.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(PANEL_PATH, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"パネル保存: {PANEL_PATH}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())