"""セグメンテーションの指標(`evaluation/segmentation_metrics.py`)のテスト.
仕様(実装サマリーを仕様として扱う):
- IoU = |P ∩ T| / |P ∪ T|,Dice = 2|P ∩ T| / (|P| + |T|)(前景について)
- 予測が空で正解に前景があれば IoU = Dice = 0(検出できなかった画像の扱い)
- 両方とも前景が無い場合は一致とみなして 1.0(評価では正解に前景が無い画像を ValueError で
止めるため,評価の集計には現れない)
- 形状が違えば ValueError
- 集計: n・平均・中央値・p10・p25・最小・最大・しきい値**未満**の件数.0 件なら n = 0 で他は None
"""
from __future__ import annotations
import numpy as np
import pytest
from tias_judge_model.evaluation.segmentation_metrics import iou_and_dice, summarize_scores
def _mask(shape: tuple[int, int], *slices: tuple[slice, slice]) -> np.ndarray:
m = np.zeros(shape, bool)
for s in slices:
m[s] = True
return m
def test_identical_masks() -> None:
m = _mask((10, 10), (slice(2, 6), slice(2, 6)))
assert iou_and_dice(m, m) == (1.0, 1.0)
def test_known_overlap() -> None:
# P: 4×4 = 16,T: 4×4 = 16,重なり 2×4 = 8 → IoU 8/24,Dice 16/32
p = _mask((10, 10), (slice(0, 4), slice(0, 4)))
t = _mask((10, 10), (slice(2, 6), slice(0, 4)))
iou, dice = iou_and_dice(p, t)
assert iou == pytest.approx(8 / 24)
assert dice == pytest.approx(0.5)
def test_dice_iou_relation_holds() -> None:
rng = np.random.default_rng(0)
for _ in range(20):
p = rng.random((16, 16)) > 0.5
t = rng.random((16, 16)) > 0.5
iou, dice = iou_and_dice(p, t)
assert dice == pytest.approx(2 * iou / (1 + iou))
def test_disjoint_masks_are_zero() -> None:
p = _mask((6, 6), (slice(0, 2), slice(0, 2)))
t = _mask((6, 6), (slice(4, 6), slice(4, 6)))
assert iou_and_dice(p, t) == (0.0, 0.0)
def test_empty_prediction_is_zero() -> None:
t = _mask((6, 6), (slice(1, 3), slice(1, 3)))
assert iou_and_dice(np.zeros((6, 6), bool), t) == (0.0, 0.0)
def test_empty_target_with_prediction_is_zero() -> None:
p = _mask((6, 6), (slice(1, 3), slice(1, 3)))
assert iou_and_dice(p, np.zeros((6, 6), bool)) == (0.0, 0.0)
def test_both_empty_counts_as_match() -> None:
z = np.zeros((6, 6), bool)
assert iou_and_dice(z, z) == (1.0, 1.0)
def test_non_bool_inputs_are_treated_as_masks() -> None:
p = np.array([[0, 255], [255, 0]], np.uint8)
t = np.array([[0, 1], [0, 0]], np.int64)
iou, dice = iou_and_dice(p, t)
assert iou == pytest.approx(0.5)
assert dice == pytest.approx(2 / 3)
def test_shape_mismatch_raises() -> None:
with pytest.raises(ValueError):
iou_and_dice(np.zeros((4, 4), bool), np.zeros((4, 5), bool))
def test_summarize_scores_known_values() -> None:
values = [0.9, 0.5, 0.8, 0.95, 0.7]
s = summarize_scores(values, 0.8)
assert s["n"] == 5
assert s["mean"] == pytest.approx(np.mean(values))
assert s["median"] == pytest.approx(0.8)
assert s["p10"] == pytest.approx(np.percentile(values, 10))
assert s["p25"] == pytest.approx(0.7)
assert s["min"] == 0.5
assert s["max"] == 0.95
assert s["n_below"] == 2 # 0.5・0.7(0.8 ちょうどは数えない)
assert s["below_threshold"] == 0.8
def test_summarize_scores_empty() -> None:
s = summarize_scores([], 0.8)
assert s["n"] == 0
for key in ("mean", "median", "p10", "p25", "min", "max", "n_below"):
assert s[key] is None
assert s["below_threshold"] == 0.8
def test_summarize_scores_values_are_json_native() -> None:
s = summarize_scores(np.array([0.1, 0.2], np.float32), 0.5)
assert isinstance(s["n"], int) and isinstance(s["n_below"], int)
assert all(isinstance(s[k], float) for k in ("mean", "median", "p10", "p25", "min", "max"))