"""可視化モジュール.
matplotlib を使って輝度マップ・ヒストグラム・放射状プロファイル・ゾーンマップを
PNG として保存する.画面表示は行わない(Agg バックエンド前提).
各関数は figure を生成 → savefig → plt.close(fig) の形式で実装し,
メモリリークを防ぐ.
カラーマップ・軸ラベル・全体のスタイルは MiniTias-Evaluation の出力と揃えている
(実測 PNG とシミュ PNG を並べて比較できるように).
"""
from __future__ import annotations
from pathlib import Path
import matplotlib
# headless 環境(サーバ・CI 等)でも動くよう Agg バックエンドを強制する.
# pyplot を import する前に設定しないと既存バックエンドが優先されてしまうため,
# このモジュール内では matplotlib.use() を pyplot import より前に置く.
matplotlib.use("Agg")
import matplotlib.figure # noqa: E402
import matplotlib.pyplot as plt # noqa: E402
import numpy # noqa: E402
# MiniTias-Evaluation の出力に合わせて hot カラーマップを統一使用する.
_LUMINANCE_CMAP = "hot"
# ゾーン境界の表示色(中心-中間,中間-周辺の 2 本)
_ZONE_BOUNDARY_COLORS = ["cyan", "lime"]
# フォールバック描画(luminance=None 時)のゾーン塗り分け色(center/middle/periphery)
_ZONE_FALLBACK_COLORS_HEX = ["#e74c3c", "#f39c12", "#2ecc71"]
# hex カラーを [0, 1] RGB タプルに変換済み(save_zone_map 呼び出しごとの変換を避けるため)
_ZONE_FALLBACK_COLORS_RGB: list[tuple[float, float, float]] = [
(int(h[1:3], 16) / 255.0, int(h[3:5], 16) / 255.0, int(h[5:7], 16) / 255.0)
for h in _ZONE_FALLBACK_COLORS_HEX
]
def _save_figure(fig: matplotlib.figure.Figure, output_path: Path, dpi: int = 150) -> None:
"""figure を PNG として保存してメモリを解放する.
tight_layout → savefig → plt.close の順序を統一するためのヘルパ.
Args:
fig: 保存対象の Figure.
output_path: 保存先 PNG パス.
dpi: 解像度(デフォルト 150).
"""
fig.tight_layout()
fig.savefig(output_path, dpi=dpi)
plt.close(fig)
def save_luminance_map(luminance: numpy.ndarray, output_path: Path) -> None:
"""輝度マップを PNG として保存する.
Args:
luminance: 輝度画像(H x W, float).
output_path: 保存先 PNG パス.
"""
fig, ax = plt.subplots(figsize=(6, 5))
im = ax.imshow(luminance, cmap=_LUMINANCE_CMAP, origin="upper")
fig.colorbar(im, ax=ax, label="Luminance")
ax.set_title("Luminance Map")
ax.set_xlabel("X (px)")
ax.set_ylabel("Y (px)")
_save_figure(fig, output_path)
def save_histogram(luminance: numpy.ndarray, output_path: Path, bins: int = 50) -> None:
"""輝度ヒストグラムを PNG として保存する.
横軸は uint8 全範囲(0〜255)に固定して MiniTias-Evaluation と比較しやすくする.
Args:
luminance: 輝度画像(H x W, float).
output_path: 保存先 PNG パス.
bins: ヒストグラムのビン数.デフォルトは 50.
"""
fig, ax = plt.subplots(figsize=(8, 5))
ax.hist(luminance.ravel(), bins=bins, color="#1f77b4", edgecolor="none")
ax.set_title("Luminance Histogram")
ax.set_xlabel("Luminance")
ax.set_ylabel("Pixel Count")
ax.set_xlim(0, 255)
_save_figure(fig, output_path)
def save_radial_profile(profile: numpy.ndarray, output_path: Path) -> None:
"""放射状輝度プロファイルを線グラフとして PNG に保存する.
`evaluation/radial.calc_radial_profile` が返す (n_bins, 2) 配列を受け取る.
Args:
profile: 放射状プロファイル配列(shape: (n_bins, 2)).
列 0: 正規化距離(ビン中心),列 1: 平均輝度.空ビンは nan.
output_path: 保存先 PNG パス.
"""
distances = profile[:, 0]
mean_luminance = profile[:, 1]
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(distances, mean_luminance, marker="o", markersize=6, linewidth=2, color="#1f77b4")
ax.set_title("Radial Luminance Profile")
ax.set_xlabel("Normalized Distance from Center")
ax.set_ylabel("Mean Luminance")
ax.set_xlim(0.0, 1.0)
ax.grid(True, linestyle="--", alpha=0.5)
_save_figure(fig, output_path)
def save_zone_map(
zone_map: numpy.ndarray,
output_path: Path,
luminance: numpy.ndarray | None = None,
) -> None:
"""ゾーンマップを PNG として保存する.
MiniTias-Evaluation 互換:背景に輝度マップ(hot)を表示し、
その上にゾーン境界(center-middle,middle-periphery)を線で重ねる.
`luminance` が None の場合は zone_map のみを 3 色で塗り分けるフォールバック動作.
Args:
zone_map: ゾーンラベルマップ(shape: (H, W), dtype: int8).
値は 0(center), 1(middle), 2(periphery).
output_path: 保存先 PNG パス.
luminance: 背景に重ねる輝度マップ(H x W, float).None ならゾーンの色塗りのみ.
"""
fig, ax = plt.subplots(figsize=(6, 5))
if luminance is not None:
im = ax.imshow(luminance, cmap=_LUMINANCE_CMAP, origin="upper")
fig.colorbar(im, ax=ax, label="Luminance")
# ゾーン境界線(zone_map のラベル変化を contour で抽出)
# zone_map: 0/1/2 → 境界は level=0.5(0-1)と level=1.5(1-2)
ax.contour(
zone_map.astype(float),
levels=[0.5, 1.5],
colors=_ZONE_BOUNDARY_COLORS,
linewidths=2,
)
else:
# フォールバック: 3 色塗り分け
height, width = zone_map.shape
rgb = numpy.zeros((height, width, 3), dtype=numpy.float32)
for label, color_rgb in enumerate(_ZONE_FALLBACK_COLORS_RGB):
mask = zone_map == label
rgb[mask] = color_rgb
ax.imshow(rgb, origin="upper")
ax.set_title("Zone Map (center / middle / periphery)")
ax.set_xlabel("X (px)")
ax.set_ylabel("Y (px)")
_save_figure(fig, output_path)