Newer
Older
SmTIAS-Evaluation / src / analysis / spatial.py
"""照明均一性の空間分析モジュール(中心-周辺勾配)."""

import numpy


def calc_distance_map(height: int, width: int) -> numpy.ndarray:
    """各ピクセルの中心からの正規化楕円距離を算出する.

    楕円距離を使用することで、縦横比が異なる画像でも中心を基準に
    等方的なゾーン分類が行えるようにする.

    Args:
        height: 画像の高さ(ピクセル).
        width: 画像の幅(ピクセル).

    Returns:
        正規化楕円距離の NumPy 配列(H x W, float64).値域は 0.0〜1.0.
    """
    cy = height / 2
    cx = width / 2

    # mgrid で全ピクセルの y, x 座標を生成
    y_coords, x_coords = numpy.mgrid[0:height, 0:width]

    # 中心を原点とした楕円距離(縦横それぞれ中心距離で正規化)
    dist = numpy.sqrt(((x_coords - cx) / cx) ** 2 + ((y_coords - cy) / cy) ** 2)

    # 最大値で再正規化して 0.0〜1.0 に揃える
    max_dist = dist.max()
    if max_dist > 0:
        dist = dist / max_dist

    return dist


def classify_zones(
    distance_map: numpy.ndarray,
    center_threshold: float = 0.33,
    periphery_threshold: float = 0.66,
) -> numpy.ndarray:
    """距離マップを 3 ゾーン(中心・中間・周辺)に分類する.

    ゾーン境界を正規化距離の三等分付近に置くことで、
    均一性評価の標準的な領域分割に対応する.

    Args:
        distance_map: 正規化楕円距離の NumPy 配列(H x W, float64).
        center_threshold: 中心ゾーンの上限距離(デフォルト: 0.33).
        periphery_threshold: 中間ゾーンの上限距離(デフォルト: 0.66).

    Returns:
        ゾーンマップの NumPy 配列(H x W, uint8).
        0=center, 1=middle, 2=periphery.
    """
    zone_map = numpy.zeros(distance_map.shape, dtype=numpy.uint8)
    # 中間ゾーンと周辺ゾーンを順に上書きする
    zone_map[distance_map >= center_threshold] = 1
    zone_map[distance_map >= periphery_threshold] = 2
    return zone_map


def calc_zone_stats(luminance: numpy.ndarray, zone_map: numpy.ndarray) -> dict:
    """ゾーン別の輝度統計と中心-周辺勾配指標を算出する.

    center_periphery_ratio が 1.0 に近いほど均一、
    gradient_magnitude が 0 に近いほど空間的ムラが小さい.

    Args:
        luminance: 輝度画像の NumPy 配列(H x W, float64).
        zone_map: ゾーンマップの NumPy 配列(H x W, uint8).
            0=center, 1=middle, 2=periphery.

    Returns:
        ゾーン統計を格納した辞書.キーは:
        "center_mean", "center_std", "middle_mean", "middle_std",
        "periphery_mean", "periphery_std",
        "center_periphery_ratio", "gradient_magnitude".
    """
    center_pixels = luminance[zone_map == 0]
    middle_pixels = luminance[zone_map == 1]
    periphery_pixels = luminance[zone_map == 2]

    center_mean = float(numpy.mean(center_pixels))
    center_std = float(numpy.std(center_pixels))
    middle_mean = float(numpy.mean(middle_pixels))
    middle_std = float(numpy.std(middle_pixels))
    periphery_mean = float(numpy.mean(periphery_pixels))
    periphery_std = float(numpy.std(periphery_pixels))

    # 中心/周辺比: 照明が中心に集中しているかを示す
    center_periphery_ratio = (
        center_mean / periphery_mean if periphery_mean != 0.0 else float("inf")
    )

    # 勾配量(%): 中心と周辺の輝度差を中心輝度に対する比率で表す
    gradient_magnitude = (
        (center_mean - periphery_mean) / center_mean * 100
        if center_mean != 0.0
        else float("inf")
    )

    return {
        "center_mean": center_mean,
        "center_std": center_std,
        "middle_mean": middle_mean,
        "middle_std": middle_std,
        "periphery_mean": periphery_mean,
        "periphery_std": periphery_std,
        "center_periphery_ratio": center_periphery_ratio,
        "gradient_magnitude": gradient_magnitude,
    }


def calc_radial_profile(
    luminance: numpy.ndarray,
    distance_map: numpy.ndarray,
    num_bins: int = 20,
) -> numpy.ndarray:
    """放射状輝度プロファイルを算出する.

    距離ビンごとに平均輝度を集計することで、
    中心から周辺にかけての輝度変化の傾向を把握する.

    Args:
        luminance: 輝度画像の NumPy 配列(H x W, float64).
        distance_map: 正規化楕円距離の NumPy 配列(H x W, float64).
        num_bins: 距離方向の分割数(デフォルト: 20).

    Returns:
        放射状プロファイルの NumPy 配列(num_bins x 2, float64).
        列 0 = ビン中心距離, 列 1 = 平均輝度.
    """
    bin_edges = numpy.linspace(0.0, 1.0, num_bins + 1)
    profile = numpy.zeros((num_bins, 2), dtype=numpy.float64)

    for i in range(num_bins):
        bin_center = (bin_edges[i] + bin_edges[i + 1]) / 2.0
        mask = (distance_map >= bin_edges[i]) & (distance_map < bin_edges[i + 1])

        # 最外ビンは上端を含める
        if i == num_bins - 1:
            mask = distance_map >= bin_edges[i]

        pixels = luminance[mask]
        mean_lum = float(numpy.mean(pixels)) if pixels.size > 0 else 0.0

        profile[i, 0] = bin_center
        profile[i, 1] = mean_lum

    return profile


def calc_spatial_uniformity(luminance: numpy.ndarray) -> dict:
    """空間的均一性指標を一括算出するファサード関数.

    中心-周辺勾配の定量化に必要な distance_map, zone_map,
    zone_stats, radial_profile を一度の呼び出しで取得できる.

    Args:
        luminance: 輝度画像の NumPy 配列(H x W, float64).

    Returns:
        空間分析結果を格納した辞書.キーは:
        "zone_stats" (dict), "radial_profile" (numpy.ndarray),
        "zone_map" (numpy.ndarray).
    """
    height, width = luminance.shape
    distance_map = calc_distance_map(height, width)
    zone_map = classify_zones(distance_map)
    zone_stats = calc_zone_stats(luminance, zone_map)
    radial_profile = calc_radial_profile(luminance, distance_map)

    return {
        "zone_stats": zone_stats,
        "radial_profile": radial_profile,
        "zone_map": zone_map,
    }