"""plotter.py の単体テスト.
仕様 (SPEC_01_アーキテクチャ設計.md):
- plot_luminance_map: ヒートマップを PNG 保存
- plot_histogram: ヒストグラムを PNG 保存
- plot_radial_profile: 放射状プロファイルを折れ線グラフで PNG 保存
- plot_zone_map: ゾーンオーバーレイ輝度マップを PNG 保存
テスト方針 (GUIDE_08_テスト方針.md):
- 出力ファイルが生成されることを確認する程度でよい
- tmp_path フィクスチャを使用
"""
import matplotlib
matplotlib.use("Agg") # GUI なし環境でも動作するバックエンドを設定
from pathlib import Path
import numpy
import pytest
from src.visualization.plotter import (
plot_histogram,
plot_luminance_map,
plot_radial_profile,
plot_zone_map,
)
@pytest.fixture
def sample_luminance() -> numpy.ndarray:
"""テスト用の 8x8 輝度画像(float64)を返す fixture."""
return numpy.random.uniform(50.0, 200.0, (8, 8)).astype(numpy.float64)
# ---------------------------------------------------------------------------
# plot_luminance_map テスト
# ---------------------------------------------------------------------------
class TestPlotLuminanceMap:
"""plot_luminance_map 関数のテスト群."""
def test_output_file_is_created(
self, sample_luminance: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ファイルが生成されること."""
output_path = str(tmp_path / "luminance_map.png")
plot_luminance_map(sample_luminance, output_path)
assert Path(output_path).exists()
def test_output_file_is_not_empty(
self, sample_luminance: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ファイルが空でないこと(PNG バイナリが書き込まれていること)."""
output_path = str(tmp_path / "luminance_map.png")
plot_luminance_map(sample_luminance, output_path)
assert Path(output_path).stat().st_size > 0
def test_creates_parent_directory_automatically(
self, sample_luminance: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ディレクトリが存在しない場合に自動作成されること."""
nested_path = str(tmp_path / "figures" / "sub" / "luminance_map.png")
plot_luminance_map(sample_luminance, nested_path)
assert Path(nested_path).exists()
def test_uniform_luminance_is_saved(self, tmp_path: Path) -> None:
"""正常: 均一輝度画像でも正常に保存されること."""
uniform_luminance = numpy.full((8, 8), 128.0, dtype=numpy.float64)
output_path = str(tmp_path / "uniform_map.png")
plot_luminance_map(uniform_luminance, output_path)
assert Path(output_path).exists()
# ---------------------------------------------------------------------------
# plot_histogram テスト
# ---------------------------------------------------------------------------
class TestPlotHistogram:
"""plot_histogram 関数のテスト群."""
def test_output_file_is_created(
self, sample_luminance: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ファイルが生成されること."""
output_path = str(tmp_path / "histogram.png")
plot_histogram(sample_luminance, output_path)
assert Path(output_path).exists()
def test_output_file_is_not_empty(
self, sample_luminance: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ファイルが空でないこと."""
output_path = str(tmp_path / "histogram.png")
plot_histogram(sample_luminance, output_path)
assert Path(output_path).stat().st_size > 0
def test_creates_parent_directory_automatically(
self, sample_luminance: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ディレクトリが存在しない場合に自動作成されること."""
nested_path = str(tmp_path / "figures" / "sub" / "histogram.png")
plot_histogram(sample_luminance, nested_path)
assert Path(nested_path).exists()
def test_uniform_luminance_is_saved(self, tmp_path: Path) -> None:
"""正常: 均一輝度画像でも正常に保存されること."""
uniform_luminance = numpy.full((8, 8), 200.0, dtype=numpy.float64)
output_path = str(tmp_path / "uniform_histogram.png")
plot_histogram(uniform_luminance, output_path)
assert Path(output_path).exists()
# ---------------------------------------------------------------------------
# plot_radial_profile テスト
# ---------------------------------------------------------------------------
class TestPlotRadialProfile:
"""plot_radial_profile 関数のテスト群."""
@pytest.fixture
def sample_radial_profile(self) -> numpy.ndarray:
"""テスト用の放射状プロファイル(20 x 2)を返す fixture."""
bins = numpy.linspace(0.025, 0.975, 20)
luminance = numpy.linspace(200.0, 150.0, 20)
return numpy.column_stack([bins, luminance])
def test_output_file_is_created(
self, sample_radial_profile: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ファイルが生成されること."""
output_path = str(tmp_path / "radial_profile.png")
plot_radial_profile(sample_radial_profile, output_path)
assert Path(output_path).exists()
def test_output_file_is_not_empty(
self, sample_radial_profile: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ファイルが空でないこと(PNG バイナリが書き込まれていること)."""
output_path = str(tmp_path / "radial_profile.png")
plot_radial_profile(sample_radial_profile, output_path)
assert Path(output_path).stat().st_size > 0
def test_creates_parent_directory_automatically(
self, sample_radial_profile: numpy.ndarray, tmp_path: Path
) -> None:
"""正常: 出力ディレクトリが存在しない場合に自動作成されること."""
nested_path = str(tmp_path / "figures" / "sub" / "radial_profile.png")
plot_radial_profile(sample_radial_profile, nested_path)
assert Path(nested_path).exists()
def test_uniform_profile_is_saved(self, tmp_path: Path) -> None:
"""正常: 均一輝度プロファイルでも正常に保存されること."""
bins = numpy.linspace(0.025, 0.975, 20)
luminance = numpy.full(20, 200.0)
profile = numpy.column_stack([bins, luminance])
output_path = str(tmp_path / "uniform_radial.png")
plot_radial_profile(profile, output_path)
assert Path(output_path).exists()
# ---------------------------------------------------------------------------
# plot_zone_map テスト
# ---------------------------------------------------------------------------
class TestPlotZoneMap:
"""plot_zone_map 関数のテスト群."""
@pytest.fixture
def sample_inputs(self) -> tuple[numpy.ndarray, numpy.ndarray, dict]:
"""テスト用の輝度画像・ゾーンマップ・ゾーン統計を返す fixture."""
luminance = numpy.random.uniform(100.0, 200.0, (20, 20)).astype(numpy.float64)
zone_map = numpy.zeros((20, 20), dtype=numpy.uint8)
zone_map[7:13, 7:13] = 0 # center
zone_map[4:16, 4:16][zone_map[4:16, 4:16] != 0] = 1 # middle
zone_map[zone_map == 0] = 2 # outer regions → periphery
# Rebuild: center=0, then middle=1, rest=2
zone_map[:] = 2
zone_map[4:16, 4:16] = 1
zone_map[7:13, 7:13] = 0
zone_stats = {
"center_mean": 180.0,
"center_std": 5.0,
"middle_mean": 165.0,
"middle_std": 8.0,
"periphery_mean": 140.0,
"periphery_std": 10.0,
"center_periphery_ratio": 1.29,
"gradient_magnitude": 22.2,
}
return luminance, zone_map, zone_stats
def test_output_file_is_created(
self,
sample_inputs: tuple[numpy.ndarray, numpy.ndarray, dict],
tmp_path: Path,
) -> None:
"""正常: 出力ファイルが生成されること."""
luminance, zone_map, zone_stats = sample_inputs
output_path = str(tmp_path / "zone_map.png")
plot_zone_map(luminance, zone_map, zone_stats, output_path)
assert Path(output_path).exists()
def test_output_file_is_not_empty(
self,
sample_inputs: tuple[numpy.ndarray, numpy.ndarray, dict],
tmp_path: Path,
) -> None:
"""正常: 出力ファイルが空でないこと(PNG バイナリが書き込まれていること)."""
luminance, zone_map, zone_stats = sample_inputs
output_path = str(tmp_path / "zone_map.png")
plot_zone_map(luminance, zone_map, zone_stats, output_path)
assert Path(output_path).stat().st_size > 0
def test_creates_parent_directory_automatically(
self,
sample_inputs: tuple[numpy.ndarray, numpy.ndarray, dict],
tmp_path: Path,
) -> None:
"""正常: 出力ディレクトリが存在しない場合に自動作成されること."""
luminance, zone_map, zone_stats = sample_inputs
nested_path = str(tmp_path / "figures" / "sub" / "zone_map.png")
plot_zone_map(luminance, zone_map, zone_stats, nested_path)
assert Path(nested_path).exists()