diff --git a/config/templates/report.html b/config/templates/report.html new file mode 100644 index 0000000..901b40d --- /dev/null +++ b/config/templates/report.html @@ -0,0 +1,218 @@ + + +
+ + +| 画像名 | +平均輝度 | +標準偏差 | +CoV | +最大/最小比 | +最大輝度 | +最小輝度 | +
|---|---|---|---|---|---|---|
| {{ row.image_name }} | +{{ "%.2f"|format(row.mean|float) }} | +{{ "%.2f"|format(row.std|float) }} | +{{ "%.4f"|format(row.cov|float) }} | +{{ "%.4f"|format(row.max_min_ratio|float) }} | +{{ "%.2f"|format(row.max|float) }} | +{{ "%.2f"|format(row.min|float) }} | +
| 画像名 | +中心平均輝度 | +中間平均輝度 | +周辺平均輝度 | +中心/周辺比 | +勾配量 (%) | +
|---|---|---|---|---|---|
| {{ row.image_name }} | +{{ "%.2f"|format(row.center_mean|float) }} | +{{ "%.2f"|format(row.middle_mean|float) }} | +{{ "%.2f"|format(row.periphery_mean|float) }} | +{{ "%.4f"|format(row.center_periphery_ratio|float) }} | +{{ "%.2f"|format(row.gradient_magnitude|float) }} | +
輝度マップ
+輝度ヒストグラム
+放射状輝度プロファイル
+ゾーンマップ(中心 / 中間 / 周辺)
+Generated: {{ generated_at }}
" + "{% for row in summary_rows %}" + "{{ row.image_name }}
" + "{% endfor %}" + "", + encoding="utf-8", + ) + return template_path + + @pytest.fixture + def simple_context(self) -> dict: + """シンプルなレンダリングコンテキストを返す fixture.""" + return { + "generated_at": "2026-04-08 12:00:00", + "summary_rows": [{"image_name": "image_001"}], + "spatial_rows": [], + "image_details": [], + } + + def test_output_html_file_is_created( + self, + simple_template: Path, + simple_context: dict, + tmp_path: Path, + ) -> None: + """正常: HTML ファイルが生成されること.""" + output_path = str(tmp_path / "report.html") + render_html_report(simple_context, str(simple_template), output_path) + assert Path(output_path).exists() + + def test_output_html_file_is_not_empty( + self, + simple_template: Path, + simple_context: dict, + tmp_path: Path, + ) -> None: + """正常: 出力ファイルが空でないこと.""" + output_path = str(tmp_path / "report.html") + render_html_report(simple_context, str(simple_template), output_path) + assert Path(output_path).stat().st_size > 0 + + def test_output_html_contains_rendered_content( + self, + simple_template: Path, + simple_context: dict, + tmp_path: Path, + ) -> None: + """正常: テンプレートがコンテキストで正しくレンダリングされること.""" + output_path = str(tmp_path / "report.html") + render_html_report(simple_context, str(simple_template), output_path) + + html_content = Path(output_path).read_text(encoding="utf-8") + assert "2026-04-08 12:00:00" in html_content + assert "image_001" in html_content + + def test_creates_parent_directory_automatically( + self, + simple_template: Path, + simple_context: dict, + tmp_path: Path, + ) -> None: + """正常: 出力ディレクトリが存在しない場合に自動作成されること.""" + nested_output = str(tmp_path / "output" / "reports" / "report.html") + render_html_report(simple_context, str(simple_template), nested_output) + assert Path(nested_output).exists() + + def test_output_is_utf8_encoded( + self, + simple_template: Path, + simple_context: dict, + tmp_path: Path, + ) -> None: + """正常: 出力ファイルが UTF-8 エンコードで書き込まれること.""" + output_path = str(tmp_path / "report.html") + render_html_report(simple_context, str(simple_template), output_path) + # UTF-8 として読み込めること(例外が発生しないこと) + content = Path(output_path).read_text(encoding="utf-8") + assert isinstance(content, str) diff --git a/tests/visualization/test_plotter.py b/tests/visualization/test_plotter.py index b66821f..98e4975 100644 --- a/tests/visualization/test_plotter.py +++ b/tests/visualization/test_plotter.py @@ -3,6 +3,8 @@ 仕様 (SPEC_01_アーキテクチャ設計.md): - plot_luminance_map: ヒートマップを PNG 保存 - plot_histogram: ヒストグラムを PNG 保存 + - plot_radial_profile: 放射状プロファイルを折れ線グラフで PNG 保存 + - plot_zone_map: ゾーンオーバーレイ輝度マップを PNG 保存 テスト方針 (GUIDE_08_テスト方針.md): - 出力ファイルが生成されることを確認する程度でよい @@ -17,7 +19,12 @@ import numpy import pytest -from src.visualization.plotter import plot_histogram, plot_luminance_map +from src.visualization.plotter import ( + plot_histogram, + plot_luminance_map, + plot_radial_profile, + plot_zone_map, +) @pytest.fixture @@ -104,3 +111,119 @@ 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()