"""cli/evaluate.py の _run_radial_min_max 関数と CLI 統合動作の仕様ベーステスト.
仕様出典:
- docs/04_SPEC/SPEC_04_評価仕様.md(動径指標・評価パイプライン)
- docs/04_SPEC/SPEC_02_シーン定義仕様.md(evaluation/metrics セクション)
- src/sm_tias_lightsim/cli/evaluate.py(_run_radial_min_max 実装)
- src/sm_tias_lightsim/evaluation/radial.py(calc_radial_min_max_ratio 実装)
テスト方針:
- subprocess ではなく main() / _run_radial_min_max() を直接呼ぶ(速度・カバレッジのため)
- ファイル I/O はモックせず tmp_path フィクスチャで実ファイルを使う (testing.md)
- 乱数を使う場合は seed を必ず固定する (testing.md)
- monkeypatch で calc_radial_profile の呼び出し有無を検証する
注意:
matplotlib の Agg バックエンドは evaluate.py 内部で設定される.
テスト側でも安全のため import 前に設定する.
"""
from __future__ import annotations
import matplotlib # noqa: I001
# headless 環境でも動くよう Agg バックエンドを pyplot より前に設定する
matplotlib.use("Agg")
from pathlib import Path
from unittest.mock import patch
import numpy
import pytest
from sm_tias_lightsim.cli.evaluate import _run_radial_min_max, main
from sm_tias_lightsim.evaluation.radial import calc_radial_profile
from sm_tias_lightsim.scene.schema import MetricsConfig
# ---------------------------------------------------------------------------
# テスト用ヘルパー
# ---------------------------------------------------------------------------
def _make_luminance_array(seed: int = 0, size: int = 128) -> numpy.ndarray:
"""ゼロを含まない合成輝度配列を返す(seed 固定).
center が明るく periphery が暗い放射状勾配で radial_min_max が意味を持つ値になる.
"""
rng = numpy.random.RandomState(seed)
h, w = size, size
ys, xs = numpy.mgrid[0:h, 0:w]
cy, cx = h / 2.0, w / 2.0
dist = numpy.sqrt(((xs - cx) / cx) ** 2 + ((ys - cy) / cy) ** 2)
luminance = 100.0 - 30.0 * dist + rng.normal(0, 2.0, (h, w))
# ゼロを避けるためクリップ
return numpy.clip(luminance, 1.0, None).astype(numpy.float64)
def _make_eval_yaml_with_radial_min_max(
tmp_path: Path,
radial_min_max: bool = True,
radial_profile: bool = True,
x: int = 0,
y: int = 0,
width: int = 128,
height: int = 128,
) -> Path:
"""radial_min_max フラグを含む評価 YAML を tmp_path に書き出す."""
content = f"""\
roi:
type: image_pixel
x: {x}
y: {y}
width: {width}
height: {height}
zones:
enabled: true
boundaries: [0.33, 0.66]
metrics:
cov: true
std: true
max_min: true
radial_profile: {"true" if radial_profile else "false"}
radial_min_max: {"true" if radial_min_max else "false"}
"""
yaml_path = tmp_path / "eval_radial_min_max.yaml"
yaml_path.write_text(content, encoding="utf-8")
return yaml_path
def _run_main(npy_path: Path, config_path: Path, output_dir: Path) -> int:
"""main() を呼ぶ共通ラッパー(行長削減のため)."""
return main(
["--image", str(npy_path), "--config", str(config_path), "--output", str(output_dir)]
)
# ---------------------------------------------------------------------------
# B1. 無効時は空辞書: metrics_config is None で {} を返す
# ---------------------------------------------------------------------------
def test_run_radial_min_max_returns_empty_dict_when_metrics_config_is_none():
"""metrics_config が None のとき _run_radial_min_max が {} を返すこと.
仕様: metrics_config is None → 無効扱いで空辞書を返す
(cli/evaluate.py _run_radial_min_max)
"""
roi_luminance = _make_luminance_array(seed=0)
result = _run_radial_min_max(roi_luminance, None, None)
assert result == {}
# ---------------------------------------------------------------------------
# B2. 無効時は空辞書(False): radial_min_max=False で {} を返す
# ---------------------------------------------------------------------------
def test_run_radial_min_max_returns_empty_dict_when_radial_min_max_false():
"""MetricsConfig(radial_min_max=False) で _run_radial_min_max が {} を返すこと.
仕様: metrics_config.radial_min_max=False → 無効扱いで空辞書を返す
(cli/evaluate.py _run_radial_min_max)
"""
roi_luminance = _make_luminance_array(seed=1)
metrics_cfg = MetricsConfig(radial_min_max=False)
result = _run_radial_min_max(roi_luminance, metrics_cfg, None)
assert result == {}
def test_run_radial_min_max_returns_empty_dict_when_radial_min_max_default():
"""MetricsConfig() のデフォルト(radial_min_max=False)で {} を返すこと.
仕様: MetricsConfig.radial_min_max の既定値 False → 無効扱い
"""
roi_luminance = _make_luminance_array(seed=2)
metrics_cfg = MetricsConfig() # radial_min_max=False がデフォルト
result = _run_radial_min_max(roi_luminance, metrics_cfg, None)
assert result == {}
# ---------------------------------------------------------------------------
# B3. 有効時は 4 キー辞書
# ---------------------------------------------------------------------------
def test_run_radial_min_max_returns_four_keys_when_enabled():
"""MetricsConfig(radial_min_max=True) + プロファイル提供で 4 キー辞書を返すこと.
仕様: radial_min_max=True のとき
radial_min_max_ratio / radial_min_distance / radial_min / radial_max
の 4 キーを持つ辞書を返す(cli/evaluate.py _run_radial_min_max)
"""
roi_luminance = _make_luminance_array(seed=3)
metrics_cfg = MetricsConfig(radial_min_max=True)
profile = calc_radial_profile(roi_luminance)
result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
assert "radial_min_max_ratio" in result
assert "radial_min_distance" in result
assert "radial_min" in result
assert "radial_max" in result
def test_run_radial_min_max_ratio_in_valid_range():
"""radial_min_max_ratio が [0, 1] の範囲内であること(均一性指標として妥当).
仕様: calc_radial_min_max_ratio — 最小輝度 / 最大輝度(0〜1)
(evaluation/radial.py calc_radial_min_max_ratio)
"""
roi_luminance = _make_luminance_array(seed=4)
metrics_cfg = MetricsConfig(radial_min_max=True)
profile = calc_radial_profile(roi_luminance)
result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
ratio = result["radial_min_max_ratio"]
assert 0.0 <= ratio <= 1.0
def test_run_radial_min_max_radial_min_lte_radial_max():
"""radial_min が radial_max 以下であること(min ≤ max).
仕様: radial_min = プロファイルの最小輝度値,radial_max = 最大輝度値
(evaluation/radial.py calc_radial_min_max_ratio)
"""
roi_luminance = _make_luminance_array(seed=5)
metrics_cfg = MetricsConfig(radial_min_max=True)
profile = calc_radial_profile(roi_luminance)
result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
assert result["radial_min"] <= result["radial_max"]
def test_run_radial_min_max_ratio_equals_min_over_max():
"""radial_min_max_ratio が radial_min / radial_max と一致すること.
仕様: radial_min_max_ratio = 最小輝度 / 最大輝度
(evaluation/radial.py calc_radial_min_max_ratio)
"""
roi_luminance = _make_luminance_array(seed=6)
metrics_cfg = MetricsConfig(radial_min_max=True)
profile = calc_radial_profile(roi_luminance)
result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
expected_ratio = result["radial_min"] / result["radial_max"]
assert result["radial_min_max_ratio"] == pytest.approx(expected_ratio, rel=1e-10)
# ---------------------------------------------------------------------------
# B4. radial_profile 再利用: プロファイル提供時は calc_radial_profile を呼ばない
# ---------------------------------------------------------------------------
def test_run_radial_min_max_reuses_provided_profile_and_does_not_recalculate():
"""radial_profile 引数が None でない時は calc_radial_profile が呼ばれないこと.
仕様: radial_profile が既に算出されていればそれを再利用し,重複算出を避ける
(cli/evaluate.py _run_radial_min_max docstring)
"""
roi_luminance = _make_luminance_array(seed=7)
metrics_cfg = MetricsConfig(radial_min_max=True)
profile = calc_radial_profile(roi_luminance)
with patch("sm_tias_lightsim.cli.evaluate.calc_radial_profile") as mock_calc:
_run_radial_min_max(roi_luminance, metrics_cfg, profile)
mock_calc.assert_not_called()
def test_run_radial_min_max_reuses_profile_values_unchanged():
"""提供した radial_profile のデータが再利用されて結果が変わらないこと.
仕様: radial_profile を再利用する — プロファイルの値は変更されない
"""
roi_luminance = _make_luminance_array(seed=8)
metrics_cfg = MetricsConfig(radial_min_max=True)
profile = calc_radial_profile(roi_luminance)
# 同じプロファイルを 2 回渡しても結果が同じ
result1 = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
result2 = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
assert result1["radial_min_max_ratio"] == pytest.approx(result2["radial_min_max_ratio"])
assert result1["radial_min_distance"] == pytest.approx(result2["radial_min_distance"])
# ---------------------------------------------------------------------------
# B5. radial_profile 内部算出: None + radial_min_max=True で calc_radial_profile が呼ばれる
# ---------------------------------------------------------------------------
def test_run_radial_min_max_calls_calc_radial_profile_when_profile_is_none():
"""radial_profile=None かつ radial_min_max=True で calc_radial_profile が呼ばれること.
仕様: radial_profile が None(未算出)の場合はその場で calc_radial_profile を呼び出す
(cli/evaluate.py _run_radial_min_max)
"""
roi_luminance = _make_luminance_array(seed=9)
metrics_cfg = MetricsConfig(radial_min_max=True)
real_profile = calc_radial_profile(roi_luminance)
with patch(
"sm_tias_lightsim.cli.evaluate.calc_radial_profile",
return_value=real_profile,
) as mock_calc:
result = _run_radial_min_max(roi_luminance, metrics_cfg, None)
mock_calc.assert_called_once_with(roi_luminance)
# 結果も正常に返ること
assert "radial_min_max_ratio" in result
def test_run_radial_min_max_with_none_profile_returns_valid_result():
"""radial_profile=None で呼んでも有効な 4 キー辞書が返ること(内部算出).
仕様: 未算出の場合はここで算出する(重複算出を避けるため既存結果を優先)
(cli/evaluate.py _run_radial_min_max)
"""
roi_luminance = _make_luminance_array(seed=10)
metrics_cfg = MetricsConfig(radial_min_max=True)
result = _run_radial_min_max(roi_luminance, metrics_cfg, None)
assert "radial_min_max_ratio" in result
assert "radial_min_distance" in result
assert "radial_min" in result
assert "radial_max" in result
def test_run_radial_min_max_none_profile_yields_same_result_as_provided_profile():
"""radial_profile=None の内部算出結果がプロファイル提供版と一致すること.
仕様: 内部算出は calc_radial_profile(roi_luminance) を呼ぶので同じ結果になる
"""
roi_luminance = _make_luminance_array(seed=11)
metrics_cfg = MetricsConfig(radial_min_max=True)
profile = calc_radial_profile(roi_luminance)
result_with_profile = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
result_without_profile = _run_radial_min_max(roi_luminance, metrics_cfg, None)
assert result_with_profile["radial_min_max_ratio"] == pytest.approx(
result_without_profile["radial_min_max_ratio"], rel=1e-10
)
assert result_with_profile["radial_min_distance"] == pytest.approx(
result_without_profile["radial_min_distance"], rel=1e-10
)
# ---------------------------------------------------------------------------
# C1. 既存 config では radial_min_max_ratio 列が存在しない
# ---------------------------------------------------------------------------
def test_existing_config_without_radial_min_max_flag_no_column(tmp_path: Path):
"""radial_min_max を指定しない既存 config では CSV に radial_min_max_ratio 列が存在しないこと.
仕様: radial_min_max 既定 False で既存 configs は無修正で動作する
(cli/evaluate.py 実装サマリー「後方互換」)
"""
import pandas
luminance = _make_luminance_array(seed=100)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
# radial_min_max を含まない既存相当 YAML
yaml_content = """\
roi:
type: image_pixel
x: 0
y: 0
width: 128
height: 128
metrics:
cov: true
std: true
max_min: true
radial_profile: true
"""
config_path = tmp_path / "eval_no_radial_min_max.yaml"
config_path.write_text(yaml_content, encoding="utf-8")
output_dir = tmp_path / "output"
result = _run_main(npy_path, config_path, output_dir)
assert result == 0
df = pandas.read_csv(output_dir / "metrics.csv")
assert "radial_min_max_ratio" not in df.columns
assert "radial_min_distance" not in df.columns
assert "radial_min" not in df.columns
assert "radial_max" not in df.columns
# ---------------------------------------------------------------------------
# C2. 新フラグで 4 列追加される
# ---------------------------------------------------------------------------
def test_radial_min_max_true_adds_four_columns_to_csv(tmp_path: Path):
"""metrics.radial_min_max: true を有効化すると CSV に 4 列が追加されること.
仕様: 出力 CSV: 既存末尾に radial_min_max_ratio / radial_min_distance /
radial_min / radial_max の 4 列追加
(cli/evaluate.py 実装サマリー「出力 CSV」)
"""
import pandas
luminance = _make_luminance_array(seed=101)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
result = _run_main(npy_path, config_path, output_dir)
assert result == 0
df = pandas.read_csv(output_dir / "metrics.csv")
assert "radial_min_max_ratio" in df.columns
assert "radial_min_distance" in df.columns
assert "radial_min" in df.columns
assert "radial_max" in df.columns
def test_radial_min_max_true_csv_values_are_float(tmp_path: Path):
"""radial_min_max 列の値が float であること.
仕様: すべて float(metrics.csv カラム定義,SPEC_04)
"""
import pandas
luminance = _make_luminance_array(seed=102)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
_run_main(npy_path, config_path, output_dir)
df = pandas.read_csv(output_dir / "metrics.csv")
# 型として float に変換可能であること
ratio = float(df["radial_min_max_ratio"].iloc[0])
dist = float(df["radial_min_distance"].iloc[0])
rmin = float(df["radial_min"].iloc[0])
rmax = float(df["radial_max"].iloc[0])
# 値域チェック
assert 0.0 <= ratio <= 1.0
assert 0.0 <= dist <= 1.0
assert rmin <= rmax
def test_radial_min_max_true_main_returns_0(tmp_path: Path):
"""radial_min_max: true を含む YAML で main() が 0 を返すこと.
仕様: 正常終了時は 0 を返す(cli/evaluate.py main)
"""
luminance = _make_luminance_array(seed=103)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
result = _run_main(npy_path, config_path, output_dir)
assert result == 0
# ---------------------------------------------------------------------------
# C3. CSV 列順序の後方互換
# ---------------------------------------------------------------------------
def test_radial_min_max_columns_appended_after_existing_columns(tmp_path: Path):
"""radial_min_max の 4 列が既存列(cov/std/max_min_ratio 等)の後に追加されること.
仕様: 既存列(cov/std/max 等)の順序が変わらない(4 列は末尾に追加)
(cli/evaluate.py 実装サマリー「出力 CSV」)
"""
import pandas
luminance = _make_luminance_array(seed=104)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
_run_main(npy_path, config_path, output_dir)
df = pandas.read_csv(output_dir / "metrics.csv")
cols = list(df.columns)
# 既存列が先頭側に存在すること
expected_early_cols = ["cov", "std", "max_min_ratio", "mean", "max", "min"]
for col in expected_early_cols:
assert col in cols, f"列 {col} が CSV に存在しない"
# radial_min_max_ratio の位置が cov より後ろであること
cov_idx = cols.index("cov")
ratio_idx = cols.index("radial_min_max_ratio")
assert ratio_idx > cov_idx, "radial_min_max_ratio は cov より後ろにあるべき"
def test_existing_columns_order_unchanged_with_radial_min_max(tmp_path: Path):
"""radial_min_max 有効時と無効時で既存列の順序が変わらないこと.
仕様: 既存列の順序が変わらない(後方互換)
"""
import pandas
luminance = _make_luminance_array(seed=105)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
# radial_min_max=False(既存相当)
cfg_dir_no = tmp_path / "no"
cfg_dir_no.mkdir(parents=True, exist_ok=True)
config_no = _make_eval_yaml_with_radial_min_max(cfg_dir_no, radial_min_max=False)
out_no = tmp_path / "output_no"
_run_main(npy_path, config_no, out_no)
df_no = pandas.read_csv(out_no / "metrics.csv")
# radial_min_max=True
cfg_dir_yes = tmp_path / "yes"
cfg_dir_yes.mkdir(parents=True, exist_ok=True)
config_yes = _make_eval_yaml_with_radial_min_max(cfg_dir_yes, radial_min_max=True)
out_yes = tmp_path / "output_yes"
_run_main(npy_path, config_yes, out_yes)
df_yes = pandas.read_csv(out_yes / "metrics.csv")
# 既存列は両方の CSV に存在し,df_no の列順序が df_yes でも保たれること
for col in df_no.columns:
assert col in df_yes.columns, f"既存列 {col} が radial_min_max=True の CSV に存在しない"
# 既存列部分の順序が同じ
existing_in_yes = [c for c in df_yes.columns if c in list(df_no.columns)]
assert existing_in_yes == list(df_no.columns)
# ---------------------------------------------------------------------------
# C4. radial_profile: false + radial_min_max: true で独立動作
# ---------------------------------------------------------------------------
def test_radial_profile_false_with_radial_min_max_true_no_png(tmp_path: Path):
"""radial_profile: false かつ radial_min_max: true のとき
radial_profile.png が生成されないこと.
仕様: radial_min_max は radial_profile フラグとは独立して動作する
radial_profile.png は metrics.radial_profile=True のときのみ生成される
(cli/evaluate.py 実装サマリー「C4. radial_profile: false + radial_min_max: true」)
"""
luminance = _make_luminance_array(seed=106)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(
tmp_path, radial_min_max=True, radial_profile=False
)
output_dir = tmp_path / "output"
result = _run_main(npy_path, config_path, output_dir)
assert result == 0
# radial_profile.png は生成されない
assert not (output_dir / "figures" / "radial_profile.png").exists()
def test_radial_profile_false_with_radial_min_max_true_has_four_csv_columns(tmp_path: Path):
"""radial_profile: false かつ radial_min_max: true のとき CSV に 4 列が追加されること.
仕様: radial_min_max は radial_profile フラグとは独立して動作する
radial_profile.png がなくても 4 列は CSV に追加される
"""
import pandas
luminance = _make_luminance_array(seed=107)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(
tmp_path, radial_min_max=True, radial_profile=False
)
output_dir = tmp_path / "output"
_run_main(npy_path, config_path, output_dir)
df = pandas.read_csv(output_dir / "metrics.csv")
assert "radial_min_max_ratio" in df.columns
assert "radial_min_distance" in df.columns
assert "radial_min" in df.columns
assert "radial_max" in df.columns
def test_radial_profile_true_with_radial_min_max_true_both_work(tmp_path: Path):
"""radial_profile: true かつ radial_min_max: true のとき PNG も 4 列も生成されること.
仕様: radial_profile と radial_min_max は独立して有効化できる
両方 True のとき radial_profile.png + 4 列が揃う
"""
import pandas
luminance = _make_luminance_array(seed=108)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(
tmp_path, radial_min_max=True, radial_profile=True
)
output_dir = tmp_path / "output"
result = _run_main(npy_path, config_path, output_dir)
assert result == 0
# radial_profile.png が生成される
assert (output_dir / "figures" / "radial_profile.png").exists()
# 4 列も存在する
df = pandas.read_csv(output_dir / "metrics.csv")
assert "radial_min_max_ratio" in df.columns
# ---------------------------------------------------------------------------
# C5. エラー処理: _run_radial_min_max 内で例外が出たとき main() が 1 を返す
# ---------------------------------------------------------------------------
def test_radial_min_max_exception_returns_1(tmp_path: Path):
"""_run_radial_min_max が例外を投げたとき main() が 1 を返すこと.
仕様: 動径 min/max 比の算出に失敗したとき return 1
(cli/evaluate.py main: except Exception → return 1)
"""
luminance = _make_luminance_array(seed=109)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
with patch(
"sm_tias_lightsim.cli.evaluate._run_radial_min_max",
side_effect=RuntimeError("テスト用エラー"),
):
result = _run_main(npy_path, config_path, output_dir)
assert result == 1
def test_radial_min_max_exception_writes_stderr(tmp_path: Path, capsys):
"""_run_radial_min_max が例外を投げたとき stderr にエラーメッセージが出ること.
仕様: 動径 min/max 比の算出に失敗しました — {exc} が stderr に出力される
(cli/evaluate.py main)
"""
luminance = _make_luminance_array(seed=110)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
with patch(
"sm_tias_lightsim.cli.evaluate._run_radial_min_max",
side_effect=RuntimeError("テスト用エラー"),
):
main(["--image", str(npy_path), "--config", str(config_path), "--output", str(output_dir)])
captured = capsys.readouterr()
assert "エラー" in captured.err
def test_radial_min_max_inner_exception_returns_1(tmp_path: Path):
"""calc_radial_min_max_ratio が例外を投げたとき main() が 1 を返すこと.
仕様: 内部関数の例外もキャッチして return 1 になる
(cli/evaluate.py main: except Exception → return 1)
"""
luminance = _make_luminance_array(seed=111)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
with patch(
"sm_tias_lightsim.cli.evaluate.calc_radial_min_max_ratio",
side_effect=ValueError("テスト用内部エラー"),
):
result = _run_main(npy_path, config_path, output_dir)
assert result == 1
# ---------------------------------------------------------------------------
# コンソール出力の確認
# ---------------------------------------------------------------------------
def test_radial_min_max_summary_printed_when_enabled(tmp_path: Path, capsys):
"""radial_min_max: true のとき radial min/max と radial min dist がコンソールに出ること.
仕様: コンソール出力: 代表 2 行(radial min/max, radial min dist)のみ追加
(cli/evaluate.py _print_summary)
"""
luminance = _make_luminance_array(seed=112)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
output_dir = tmp_path / "output"
_run_main(npy_path, config_path, output_dir)
captured = capsys.readouterr()
assert "radial min/max" in captured.out
assert "radial min dist" in captured.out
def test_radial_min_max_summary_not_printed_when_disabled(tmp_path: Path, capsys):
"""radial_min_max: false のとき radial min/max がコンソールに出ないこと.
仕様: radial_min_max_ratio がメトリクスにないとき _print_summary は出力しない
(cli/evaluate.py _print_summary: if "radial_min_max_ratio" in metrics)
"""
luminance = _make_luminance_array(seed=113)
npy_path = tmp_path / "luminance.npy"
numpy.save(npy_path, luminance)
config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=False)
output_dir = tmp_path / "output"
_run_main(npy_path, config_path, output_dir)
captured = capsys.readouterr()
assert "radial min/max" not in captured.out
# ---------------------------------------------------------------------------
# D2. slow テスト(実 output/baseline/luminance.npy がある場合)
# ---------------------------------------------------------------------------
#: output/baseline/luminance.npy の絶対パス
BASELINE_LUMINANCE_NPY = (
Path(__file__).parent.parent.parent / "output" / "baseline" / "luminance.npy"
)
@pytest.mark.slow
@pytest.mark.skipif(
not BASELINE_LUMINANCE_NPY.exists(),
reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_radial_min_max_with_real_baseline_npy(tmp_path: Path):
"""実 output/baseline/luminance.npy で radial_min_max: true が動作すること.
仕様: 動径 min/max 比 — 20 点輪帯平均の min/max で 1.0 で均一(SPEC_04 動径指標)
"""
import pandas
config_path = _make_eval_yaml_with_radial_min_max(
tmp_path,
radial_min_max=True,
radial_profile=True,
# baseline の ROI に合わせる
x=24,
y=37,
width=464,
height=450,
)
output_dir = tmp_path / "output"
result = main(
[
"--image",
str(BASELINE_LUMINANCE_NPY),
"--config",
str(config_path),
"--output",
str(output_dir),
]
)
assert result == 0
df = pandas.read_csv(output_dir / "metrics.csv")
assert "radial_min_max_ratio" in df.columns
ratio = float(df["radial_min_max_ratio"].iloc[0])
# シミュ linear の均一性は悪いため ratio は 1.0 より相当小さいはず
# 存在確認として 0.0〜1.0 の範囲内であることのみチェック
assert 0.0 < ratio <= 1.0