"""evaluation/roi.py の仕様ベーステスト.
仕様出典:
- docs/04_SPEC/SPEC_01_アーキテクチャ設計.md(evaluation/roi 層の責務)
- docs/04_SPEC/SPEC_02_シーン定義仕様.md(evaluation.roi スキーマ)
- docs/01_GUIDE/GUIDE_08_テスト方針.md(テスト方針)
evaluation/roi の責務:
画像 + ROI 定義 → ROI 内の輝度配列
image_pixel / scene_face 両対応(現在は image_pixel のみ実装)
テスト方針:
- RoiConfig を直接構築して extract_roi の振る舞いを検証する
- ファイル I/O はモックせず実 ndarray を使う (GUIDE_08)
- numpy.testing.assert_array_equal で配列値の一致を確認する
"""
from __future__ import annotations
import numpy
import numpy.testing
import pytest
from sm_tias_lightsim.evaluation.roi import extract_roi
from sm_tias_lightsim.scene.schema import RoiConfig # noqa: I001
# ---------------------------------------------------------------------------
# テスト用ヘルパー
# ---------------------------------------------------------------------------
def _make_roi(x: int, y: int, width: int, height: int) -> RoiConfig:
"""テスト用 RoiConfig を構築するヘルパー(image_pixel 固定)."""
return RoiConfig(type="image_pixel", x=x, y=y, width=width, height=height)
# ---------------------------------------------------------------------------
# 1. 2D 入力(輝度配列)
# ---------------------------------------------------------------------------
def test_extract_roi_2d_shape():
"""2D 輝度配列から切り出した ROI の shape が (height, width) になること.
仕様: 画像 + ROI 定義 → ROI 内の輝度配列(SPEC_01 evaluation/roi 責務)
入力 100x100,中央 ROI (x=20, y=20, w=40, h=40) → shape (40, 40)
"""
image = numpy.zeros((100, 100), dtype=numpy.float64)
roi = _make_roi(x=20, y=20, width=40, height=40)
result = extract_roi(image, roi)
assert result.shape == (40, 40)
def test_extract_roi_2d_values_match():
"""2D 配列の ROI 切り出し値が元の配列の対応領域と一致すること.
仕様: ROI クロップは元配列の対応領域と値が同一でなければならない
"""
rng = numpy.random.RandomState(42)
image = rng.uniform(1.0, 200.0, (100, 100)).astype(numpy.float64)
roi = _make_roi(x=20, y=20, width=40, height=40)
result = extract_roi(image, roi)
expected = image[20:60, 20:60]
numpy.testing.assert_array_equal(result, expected)
# ---------------------------------------------------------------------------
# 2. 3D 入力(RGB 配列)
# ---------------------------------------------------------------------------
def test_extract_roi_3d_shape():
"""3D RGB 配列から切り出した ROI の shape が (height, width, 3) になること.
仕様: 先頭 2 軸が H, W であること(evaluation/roi.py extract_roi docstring)
"""
image = numpy.zeros((100, 100, 3), dtype=numpy.uint8)
roi = _make_roi(x=20, y=20, width=40, height=40)
result = extract_roi(image, roi)
assert result.shape == (40, 40, 3)
def test_extract_roi_3d_values_match():
"""3D RGB 配列の ROI 切り出し値が元の配列の対応領域と一致すること."""
rng = numpy.random.RandomState(0)
image = rng.randint(0, 256, (100, 100, 3), dtype=numpy.uint8)
roi = _make_roi(x=20, y=20, width=40, height=40)
result = extract_roi(image, roi)
expected = image[20:60, 20:60, :]
numpy.testing.assert_array_equal(result, expected)
# ---------------------------------------------------------------------------
# 3. 境界ケース(ぴったり端)
# ---------------------------------------------------------------------------
def test_extract_roi_2d_exact_boundary():
"""x+width == img_width かつ y+height == img_height でエラーにならないこと.
仕様: 画像範囲内であれば ROI 抽出は成功しなければならない
"""
image = numpy.ones((100, 100), dtype=numpy.float64)
roi = _make_roi(x=60, y=60, width=40, height=40) # 60+40 == 100 == img_width
result = extract_roi(image, roi)
assert result.shape == (40, 40)
def test_extract_roi_3d_exact_boundary():
"""3D 配列でも x+width == img_width かつ y+height == img_height でエラーにならないこと."""
image = numpy.ones((100, 100, 3), dtype=numpy.float64)
roi = _make_roi(x=60, y=60, width=40, height=40)
result = extract_roi(image, roi)
assert result.shape == (40, 40, 3)
def test_extract_roi_starts_at_origin():
"""x=0, y=0 から始まる ROI(左上角からのクロップ)でエラーにならないこと."""
image = numpy.arange(10000, dtype=numpy.float64).reshape(100, 100)
roi = _make_roi(x=0, y=0, width=10, height=10)
result = extract_roi(image, roi)
assert result.shape == (10, 10)
numpy.testing.assert_array_equal(result, image[0:10, 0:10])
# ---------------------------------------------------------------------------
# 4. 範囲外(はみ出し)
# ---------------------------------------------------------------------------
def test_extract_roi_raises_when_x_overflow():
"""x + width > img_width で ValueError が発生すること.
仕様: ROI が画像の範囲外にはみ出す場合は ValueError (evaluation/roi.py docstring)
"""
image = numpy.zeros((100, 100), dtype=numpy.float64)
roi = _make_roi(x=70, y=0, width=40, height=10) # 70+40=110 > 100
with pytest.raises(ValueError):
extract_roi(image, roi)
def test_extract_roi_raises_when_y_overflow():
"""y + height > img_height で ValueError が発生すること."""
image = numpy.zeros((100, 100), dtype=numpy.float64)
roi = _make_roi(x=0, y=70, width=10, height=40) # 70+40=110 > 100
with pytest.raises(ValueError):
extract_roi(image, roi)
def test_extract_roi_raises_when_both_overflow():
"""x + width > img_width かつ y + height > img_height で ValueError が発生すること."""
image = numpy.zeros((100, 100), dtype=numpy.float64)
roi = _make_roi(x=70, y=70, width=40, height=40) # 110 > 100 in both axes
with pytest.raises(ValueError):
extract_roi(image, roi)
# ---------------------------------------------------------------------------
# 5. ゼロサイズではないが範囲外(x が画像幅以上)
# ---------------------------------------------------------------------------
def test_extract_roi_raises_when_x_equals_img_width():
"""x が img_width と等しい(x = img_width)場合に ValueError が発生すること.
仕様: x + width > img_width の場合にエラー (roi.py docstring)
x=100, width=1 → 101 > 100 → ValueError
"""
image = numpy.zeros((100, 100), dtype=numpy.float64)
roi = _make_roi(x=100, y=0, width=1, height=10) # 100+1=101 > 100
with pytest.raises(ValueError):
extract_roi(image, roi)
def test_extract_roi_raises_when_y_equals_img_height():
"""y が img_height と等しい(y = img_height)場合に ValueError が発生すること."""
image = numpy.zeros((100, 100), dtype=numpy.float64)
roi = _make_roi(x=0, y=100, width=10, height=1) # 100+1=101 > 100
with pytest.raises(ValueError):
extract_roi(image, roi)
# ---------------------------------------------------------------------------
# 6. 1D 配列の取り扱い
# ---------------------------------------------------------------------------
def test_extract_roi_1d_array_raises():
"""1D 配列を渡したとき ValueError または IndexError が発生すること.
仕様: 先頭 2 軸が H, W であること(docstring).
1D 配列では image.shape[:2] のアンパックが失敗するため例外が発生する.
実装は shape[:2] を img_height, img_width にアンパックするので
ValueError か IndexError(または同等のエラー)が発生することを確認する.
"""
image = numpy.ones(100, dtype=numpy.float64) # 1D
roi = _make_roi(x=0, y=0, width=10, height=10)
with pytest.raises((ValueError, IndexError)):
extract_roi(image, roi)
# ---------------------------------------------------------------------------
# 7. image_pixel 以外の ROI type
# ---------------------------------------------------------------------------
def test_extract_roi_raises_not_implemented_for_scene_face():
"""roi.type が 'image_pixel' 以外のとき NotImplementedError が発生すること.
仕様: scene_face 型は将来実装予定(SPEC_02 evaluation.roi節),
現在は image_pixel のみサポート(evaluation/roi.py docstring)
"""
# Pydantic の Literal["image_pixel"] バリデーションを回避するため
# model_construct でダミーオブジェクトを構築する
roi = RoiConfig.model_construct(type="scene_face", x=0, y=0, width=10, height=10)
image = numpy.zeros((100, 100), dtype=numpy.float64)
with pytest.raises(NotImplementedError):
extract_roi(image, roi)