diff --git a/scripts/build_led_placement_maps.py b/scripts/build_led_placement_maps.py index 5d8b467..bea177f 100644 --- a/scripts/build_led_placement_maps.py +++ b/scripts/build_led_placement_maps.py @@ -53,6 +53,7 @@ Condition, ) +from sm_tias_lightsim.evaluation.fullimage import calc_fullimage_min_max_ratio # 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"] @@ -111,11 +112,40 @@ def load_map(name: str, material: str = "legacy") -> np.ndarray | None: - """条件名から正規化・平滑化済みの輝度マップを返す(無ければ None).""" + """条件名から正規化・平滑化済みの輝度マップを返す(無ければ None). + + 返り値の配列には属性は付けられないので,各マップの最小値(ノイズ補正付き最小/最大比)は + `min_ratio` で別に取る. + """ path = output_root(material) / name / "luminance.npy" return normalize_map(path, _normalize) if path.exists() else None +def min_ratio(name: str, material: str = "legacy") -> float | None: + """条件の「最大を 1 としたときの最小値」=ノイズ補正付き最小/最大比を返す(無ければ None). + + 表示の正規化(平滑化後 99 パーセンタイル = 1)で読んだ生の最小値は MC ノイズで押し下げ + られるので,evaluation/fullimage.py の定義(σ=12 平滑化 + ノイズ場の極値で補正)を使う. + """ + path = output_root(material) / name / "luminance.npy" + if not path.exists(): + return None + 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 float(calc_fullimage_min_max_ratio(roi)["fullimage_min_max_ratio"]) + + +def map_cell(label: str, name: str, material: str = "legacy") -> tuple[str, np.ndarray | None]: + """パネルの 1 セル(見出し + マップ)を作る.見出しの下に min(補正付き最小/最大比)を添える.""" + data = load_map(name, material) + if data is None: + return label, None + ratio = min_ratio(name, material) + return f"{label}\nmin={ratio:.3f}", data + + def colorbar_label() -> str: """正規化方式に応じたカラーバーの説明.""" return { @@ -144,7 +174,7 @@ ) -> None: """(行 × 列) のマップ格子を共通スケールで描く.cells[row][col] = (見出し, マップ).""" n_rows, n_cols = len(cells), max(len(r) for r in cells) - figsize = (cell_size * n_cols + 1.2, cell_size * n_rows + 1.0) + figsize = (cell_size * n_cols + 1.2, (cell_size + 0.35) * n_rows + 1.2) fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize, squeeze=False) image = None for r, row in enumerate(cells): @@ -158,13 +188,14 @@ label, data = row[c] 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) + ax.set_title(label, fontsize=7.5) 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=colorbar_label()) - fig.suptitle(title, fontsize=13) + fig.suptitle(f"{title}\n各マップの min = 最大を 1 としたときの最小値" + "(σ=12 平滑化・ノイズ補正付き最小/最大比)", fontsize=12) output.parent.mkdir(parents=True, exist_ok=True) fig.savefig(output, dpi=110, bbox_inches="tight") plt.close(fig) @@ -179,8 +210,8 @@ for shape in SHAPES: row = [] for theta in THETA_GRID: - data = load_map(cond_name(shape, sizes[shape], theta, "board", 1)) - row.append((f"θ={theta:g}°", data)) + row.append(map_cell(f"θ={theta:g}°", + cond_name(shape, sizes[shape], theta, "board", 1))) cells.append(row) labels.append(row_label(shape, sizes[shape])) draw_grid(cells, labels, @@ -197,8 +228,8 @@ for aim in AIMS: row = [] for theta in DIRECTION_THETAS: - data = load_map(cond_name(shape, sizes[shape], theta, aim, 1)) - row.append((f"θ={theta:g}°", data)) + row.append(map_cell(f"θ={theta:g}°", + cond_name(shape, sizes[shape], theta, aim, 1))) cells.append(row) labels.append(f"{SHAPE_LABEL[shape]}\n{AIM_LABEL[aim]}") draw_grid(cells, labels, @@ -214,9 +245,9 @@ sizes = pair_sizes(pair) for shape in SHAPES: size = sizes[shape] - row = [(f"N={n} θ=90°", load_map(cond_name(shape, size, COUNT_THETA, "board", n))) + row = [map_cell(f"N={n} θ=90°", cond_name(shape, size, COUNT_THETA, "board", n)) for n in (1, 2, 3, 4)] - row += [(f"N=4 θ={theta:g}°", load_map(cond_name(shape, size, theta, "board", 4))) + row += [map_cell(f"N=4 θ={theta:g}°", cond_name(shape, size, theta, "board", 4)) for theta in (60.0, 120.0)] cells.append(row) labels.append(row_label(shape, size)) @@ -232,7 +263,7 @@ sizes = pair_sizes(pair) for shape in SHAPES: phis = [0.0] + (AZIMUTH_CUBE if shape == "cube" else AZIMUTH_SPHERE) - row = [(f"φ={phi:g}°", load_map(cond_name(shape, sizes[shape], 90.0, "board", 1, phi))) + row = [map_cell(f"φ={phi:g}°", cond_name(shape, sizes[shape], 90.0, "board", 1, phi)) for phi in phis] cells.append(row) labels.append(row_label(shape, sizes[shape])) @@ -252,10 +283,10 @@ for n in (1, 2, 4, 8): az = CUBE_N8_AZIMUTH if (shape == "cube" and n == 8) else 0.0 name = cond_name(shape, sizes[shape], 90.0, "down", n, az) - row.append((f"真下 N={n}", load_map(name))) + row.append(map_cell(f"真下 N={n}", name)) az = CUBE_N8_AZIMUTH if shape == "cube" else 0.0 name = cond_name(shape, sizes[shape], 90.0, "board", 8, az) - row.append(("板向き N=8", load_map(name))) + row.append(map_cell("板向き N=8", name)) cells.append(row) labels.append(row_label(shape, sizes[shape])) draw_grid(cells, labels, @@ -277,8 +308,8 @@ ("calibrated", "校正 ρ=0.81 diffuse")): row = [] for theta, aim, n in spec_subset: - data = load_map(cond_name(shape, sizes[shape], theta, aim, n), material) - row.append((f"θ={theta:g}° {spec_label[aim]} N={n}", data)) + row.append(map_cell(f"θ={theta:g}° {spec_label[aim]} N={n}", + cond_name(shape, sizes[shape], theta, aim, n), material)) cells.append(row) labels.append(f"{row_label(shape, sizes[shape])}\n{mat_label}") draw_grid(cells, labels,