Newer
Older
SmTIAS-Evaluation / src / visualization / plotter.py
"""輝度マップ・ヒストグラム・空間分析可視化モジュール."""

import math
from pathlib import Path

import matplotlib.pyplot as plt
import numpy


def _ensure_parent(output_path: str) -> None:
    """出力ファイルの親ディレクトリを作成する.

    Args:
        output_path: 出力先ファイルパス.
    """
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)


def plot_luminance_map(luminance: numpy.ndarray, output_path: str) -> None:
    """輝度画像をヒートマップとして保存する.

    Args:
        luminance: 輝度画像の NumPy 配列(H x W, float64).
        output_path: 出力先ファイルパス(PNG).
    """
    _ensure_parent(output_path)

    fig, ax = plt.subplots(figsize=(8, 6))
    im = ax.imshow(luminance, cmap="hot", interpolation="nearest")
    fig.colorbar(im, ax=ax, label="Luminance")
    ax.set_title("Luminance Map")
    ax.set_xlabel("X (px)")
    ax.set_ylabel("Y (px)")
    fig.tight_layout()
    fig.savefig(output_path, dpi=150)
    plt.close(fig)

    print(f"輝度マップを保存しました: {output_path}")


def plot_histogram(luminance: numpy.ndarray, output_path: str) -> None:
    """輝度ヒストグラムを生成して保存する.

    Args:
        luminance: 輝度画像の NumPy 配列(H x W, float64).
        output_path: 出力先ファイルパス(PNG).
    """
    _ensure_parent(output_path)

    fig, ax = plt.subplots(figsize=(8, 5))
    ax.hist(
        luminance.ravel(), bins=256, range=(0, 255), color="steelblue", edgecolor="none"
    )
    ax.set_title("Luminance Histogram")
    ax.set_xlabel("Luminance")
    ax.set_ylabel("Pixel Count")
    fig.tight_layout()
    fig.savefig(output_path, dpi=150)
    plt.close(fig)

    print(f"輝度ヒストグラムを保存しました: {output_path}")


def plot_radial_profile(radial_profile: numpy.ndarray, output_path: str) -> None:
    """放射状輝度プロファイルを折れ線グラフとして保存する.

    最小輝度点(赤マーカー)と最大輝度点(緑マーカー)を強調表示し,
    動径 min/max 比をグラフ右上にアノテーションとして表示する.

    Args:
        radial_profile: 放射状プロファイルの NumPy 配列(N x 2, float64).
            列 0 = 正規化距離, 列 1 = 平均輝度.
        output_path: 出力先ファイルパス(PNG).
    """
    _ensure_parent(output_path)

    distances = radial_profile[:, 0]
    luminances = radial_profile[:, 1]

    # min/max 点のインデックスを特定する
    min_idx = int(numpy.argmin(luminances))
    max_idx = int(numpy.argmax(luminances))
    lmin = float(luminances[min_idx])
    lmax = float(luminances[max_idx])
    ratio = lmin / lmax if lmax != 0.0 else float("inf")

    fig, ax = plt.subplots(figsize=(8, 5))
    ax.plot(distances, luminances, marker="o", color="steelblue", label="Mean Luminance")

    # 最大点(緑)・最小点(赤)を強調マーカーで重ねる
    ax.scatter(
        distances[max_idx], lmax,
        color="green", s=80, zorder=5, label=f"Max: {lmax:.2f}",
    )
    ax.scatter(
        distances[min_idx], lmin,
        color="red", s=80, zorder=5, label=f"Min: {lmin:.2f}",
    )

    # 動径 min/max 比をグラフ右上にテキスト注記する
    ratio_text = "min/max ratio: inf" if math.isinf(ratio) else f"min/max ratio: {ratio:.4f}"
    ax.text(
        0.98, 0.97, ratio_text,
        transform=ax.transAxes,
        ha="right", va="top",
        fontsize=10,
        bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.8},
    )

    ax.set_title("Radial Luminance Profile")
    ax.set_xlabel("Normalized Distance from Center")
    ax.set_ylabel("Mean Luminance")
    ax.grid(True, linestyle="--", alpha=0.5)
    ax.legend(loc="lower left")
    fig.tight_layout()
    fig.savefig(output_path, dpi=150)
    plt.close(fig)

    print(f"放射状プロファイルを保存しました: {output_path}")


def plot_zone_map(
    luminance: numpy.ndarray,
    zone_map: numpy.ndarray,
    output_path: str,
) -> None:
    """輝度マップにゾーン境界をオーバーレイして保存する.

    "hot" カラーマップの輝度ヒートマップ上に、3 ゾーンの境界を
    contour で描画する.

    Args:
        luminance: 輝度画像の NumPy 配列(H x W, float64).
        zone_map: ゾーンマップの NumPy 配列(H x W, uint8).
            0=center, 1=middle, 2=periphery.
        output_path: 出力先ファイルパス(PNG).
    """
    _ensure_parent(output_path)

    fig, ax = plt.subplots(figsize=(8, 6))

    # 輝度ヒートマップ
    im = ax.imshow(luminance, cmap="hot", interpolation="nearest")
    fig.colorbar(im, ax=ax, label="Luminance")

    # ゾーン境界をコンター線で描画(境界値は 0.5, 1.5 でゾーン間を区切る)
    ax.contour(zone_map, levels=[0.5, 1.5], colors=["cyan", "lime"], linewidths=1.5)

    ax.set_title("Zone Map (center / middle / periphery)")
    ax.set_xlabel("X (px)")
    ax.set_ylabel("Y (px)")
    fig.tight_layout()
    fig.savefig(output_path, dpi=150)
    plt.close(fig)

    print(f"ゾーンマップを保存しました: {output_path}")