Newer
Older
SmTIAS-LightSim / tests / evaluation / test_radial.py
"""evaluation/radial.py の仕様ベーステスト.

仕様出典: MiniTias-Evaluation/SPEC_02_照明均一性評価アルゴリズム.md「放射状輝度プロファイル」節
"""

import numpy
import pytest

from sm_tias_lightsim.evaluation.radial import calc_radial_profile

# ---------------------------------------------------------------------------
# 出力の形状・型テスト
# ---------------------------------------------------------------------------


def test_radial_profile_shape_and_dtype(uniform_luminance_100x100):
    """デフォルト n_bins=20 で出力 shape が (20, 2), dtype が float64 になること.

    仕様: 出力 20×2 の配列 (列 0=ビン中心距離, 列 1=平均輝度), dtype=numpy.float64
    (SPEC_02 放射状輝度プロファイル節)
    """
    profile = calc_radial_profile(uniform_luminance_100x100)
    assert profile.shape == (20, 2)
    assert profile.dtype == numpy.float64


def test_radial_profile_default_bins(uniform_luminance_100x100):
    """デフォルト引数で 20 ビンのプロファイルが返ること."""
    profile = calc_radial_profile(uniform_luminance_100x100)
    assert profile.shape[0] == 20


def test_radial_profile_custom_bins(uniform_luminance_100x100):
    """n_bins=10 で shape が (10, 2) になること.

    仕様: n_bins でビン数を指定可能 (radial.py docstring)
    """
    profile = calc_radial_profile(uniform_luminance_100x100, n_bins=10)
    assert profile.shape == (10, 2)


# ---------------------------------------------------------------------------
# ビン中心距離のテスト
# ---------------------------------------------------------------------------


def test_radial_profile_distances_increasing(uniform_luminance_100x100):
    """列 0 (ビン中心距離) が単調増加であること.

    仕様: 中心から周辺にかけての輝度変化を連続的に把握 (SPEC_02)
    ビン中心は linspace(0, 1, n_bins+1) の隣接平均なので常に単調増加する.
    """
    profile = calc_radial_profile(uniform_luminance_100x100)
    distances = profile[:, 0]
    diffs = numpy.diff(distances)
    assert numpy.all(diffs > 0)


def test_radial_profile_distances_in_unit_range(uniform_luminance_100x100):
    """列 0 の値が [0, 1] 範囲内であること.

    仕様: 正規化楕円距離 d ∈ [0, 1] を n_bins 等分 (SPEC_02)
    """
    profile = calc_radial_profile(uniform_luminance_100x100)
    distances = profile[:, 0]
    assert float(numpy.min(distances)) >= 0.0
    assert float(numpy.max(distances)) <= 1.0


def test_radial_profile_bin_centers_correct():
    """ビン中心距離が linspace(0, 1, n_bins+1) の隣接平均と一致すること."""
    n_bins = 5
    luminance = numpy.full((100, 100), 50.0, dtype=numpy.float64)
    profile = calc_radial_profile(luminance, n_bins=n_bins)

    bin_edges = numpy.linspace(0.0, 1.0, n_bins + 1)
    expected_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0
    assert numpy.allclose(profile[:, 0], expected_centers)


# ---------------------------------------------------------------------------
# 輝度値のテスト
# ---------------------------------------------------------------------------


def test_radial_profile_uniform_luminance(uniform_luminance_100x100):
    """一様輝度では全ビンの平均輝度が同じ値 (NaN ビンを除く) になること.

    仕様: 一様照明では放射プロファイルが平坦 (SPEC_02)
    """
    profile = calc_radial_profile(uniform_luminance_100x100)
    mean_values = profile[:, 1]
    valid_mask = ~numpy.isnan(mean_values)
    assert numpy.any(valid_mask), "有効なビンが 1 つ以上存在すること"
    valid_values = mean_values[valid_mask]
    assert numpy.allclose(valid_values, valid_values[0])


def test_radial_profile_includes_d_one():
    """d == 1.0 の画素が最後のビンに含まれること(最外ビンが NaN にならない).

    仕様: 最外ビンは上端を含める (d=1.0 の画素を欠損させない) (SPEC_02)
    100×100 の画像では隅のピクセルが d=1.0 付近に存在する.
    """
    luminance = numpy.full((100, 100), 128.0, dtype=numpy.float64)
    profile = calc_radial_profile(luminance, n_bins=20)
    last_bin_value = profile[-1, 1]
    assert not numpy.isnan(last_bin_value), "最外ビンが NaN であってはならない"


def test_radial_profile_radial_gradient(radial_gradient_luminance_100x100):
    """中心が明るいグラジェント輝度では, 先頭ビンの輝度が末尾ビンより大きいこと.

    仕様: 中心-周辺方向の輝度変化を連続的に把握 (SPEC_02)
    合成輝度: 200 - 100 * d なので中心 (d≈0) が周辺 (d≈1) より明るい.
    """
    profile = calc_radial_profile(radial_gradient_luminance_100x100)
    mean_values = profile[:, 1]
    valid_mask = ~numpy.isnan(mean_values)
    valid_values = mean_values[valid_mask]
    assert valid_values[0] > valid_values[-1]


def test_radial_profile_empty_bins_are_nan():
    """画素が存在しない空ビンは numpy.nan になること.

    仕様: 空ビンは numpy.nan (radial.py docstring)
    非常に小さい画像 (3×3) で n_bins=20 のとき,距離が到達しないビンが発生しうる.
    """
    luminance = numpy.full((3, 3), 50.0, dtype=numpy.float64)
    profile = calc_radial_profile(luminance, n_bins=20)
    # 3×3 画像で 20 ビンのうち少なくとも 1 ビンが空である可能性を確認
    # (実際にどのビンが空になるかは距離マップに依存するが,nan があることを検証)
    # ここでは nan が存在する可能性を確認するだけとし,存在する場合は値が nan であること
    mean_values = profile[:, 1]
    for v in mean_values:
        assert v == pytest.approx(float(v)) or numpy.isnan(v)