"""セグメンテーションの評価の設定(`SegEvalConfig`・`load_seg_eval_config`)のテスト.
仕様(実装サマリーの設計を仕様として扱う):
- `configs/seg/*.yaml` はすべて `load_seg_eval_config` で読める(`ExperimentConfig` とは別スキーマ)
- 環境変数 `TIAS_SEG_DATA_ROOT`・`TIAS_SEG_MODEL_ROOT` が空でなければ `data.root`・`models.root`
を上書きする(`TIAS_DATA_ROOT` は使わない)
- 既定値は DeepTIAS 2.7 と同じ(入力 256・前景チャンネル 0・検出のしきい値 0.5・IoU 0.8)
- 評価セットの空・名前の重複・絶対パス・`..`・未知の kind・未知のキーは拒否
"""
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from pydantic import ValidationError
from tias_judge_model.schema import SegEvalConfig, load_seg_eval_config
CONFIGS_DIR = Path(__file__).resolve().parents[1] / "configs"
SEG_CONFIGS = sorted((CONFIGS_DIR / "seg").glob("*.yaml"))
NO_ENV: dict[str, str] = {}
def _raw(**overrides) -> dict:
raw: dict = {
"data": {"root": "data_root", "sets": [{"name": "s1", "dir": "a/b", "kind": "cropped"}]},
"models": {"root": "model_root"},
}
raw.update(overrides)
return raw
def _write(tmp_path: Path, raw: dict) -> Path:
path = tmp_path / "seg.yaml"
path.write_text(yaml.safe_dump(raw), encoding="utf-8")
return path
# --- リポジトリの設定ファイル ---
def test_seg_configs_exist() -> None:
assert {p.stem for p in SEG_CONFIGS} >= {"eval_synthetic", "eval_newton"}
@pytest.mark.parametrize("path", SEG_CONFIGS, ids=lambda p: p.name)
def test_every_seg_config_loads(path: Path) -> None:
c = load_seg_eval_config(path, environ=NO_ENV)
assert c.data.sets
assert c.resunet.foreground_channel == 0
assert c.resunet.input_size == 256
assert c.detection.score_threshold == 0.5
kinds = {s.kind for s in c.data.sets}
assert kinds <= {"cropped", "fullsize"}
# --- 既定値 ---
def test_defaults_follow_deeptias() -> None:
c = SegEvalConfig.model_validate(_raw())
assert c.resunet.input_size == 256
assert c.resunet.foreground_channel == 0
assert c.detection.score_threshold == 0.5
assert c.evaluation.iou_threshold == 0.8
assert c.data.sample_dirname == "sample"
assert c.data.label_dirname == "label"
assert c.models.resunet_path == Path("model_root") / "ResUNet.onnx"
assert c.models.detection_path is None
def test_detection_path_resolves_relative_to_models_root() -> None:
c = SegEvalConfig.model_validate(
_raw(models={"root": "mroot", "detection": "Detection_Normal.onnx"})
)
assert c.models.detection_path == Path("mroot") / "Detection_Normal.onnx"
# --- 環境変数による上書き ---
def test_env_overrides_data_and_model_roots(tmp_path: Path) -> None:
path = _write(tmp_path, _raw())
env = {"TIAS_SEG_DATA_ROOT": "/srv/seg_data", "TIAS_SEG_MODEL_ROOT": "/srv/seg_models"}
c = load_seg_eval_config(path, environ=env)
assert c.data.root == Path("/srv/seg_data")
assert c.models.root == Path("/srv/seg_models")
def test_empty_env_does_not_override(tmp_path: Path) -> None:
path = _write(tmp_path, _raw())
c = load_seg_eval_config(path, environ={"TIAS_SEG_DATA_ROOT": "", "TIAS_SEG_MODEL_ROOT": ""})
assert c.data.root == Path("data_root")
assert c.models.root == Path("model_root")
def test_tias_data_root_is_not_used(tmp_path: Path) -> None:
path = _write(tmp_path, _raw())
c = load_seg_eval_config(path, environ={"TIAS_DATA_ROOT": "/other"})
assert c.data.root == Path("data_root")
assert c.models.root == Path("model_root")
def test_non_mapping_top_level_is_rejected(tmp_path: Path) -> None:
path = tmp_path / "bad.yaml"
path.write_text("- a\n- b\n", encoding="utf-8")
with pytest.raises(ValueError):
load_seg_eval_config(path, environ=NO_ENV)
# --- 不正な設定 ---
@pytest.mark.parametrize(
"mutate",
[
lambda r: r["data"].update(sets=[]),
lambda r: r["data"].update(
sets=[
{"name": "x", "dir": "a", "kind": "cropped"},
{"name": "x", "dir": "b", "kind": "fullsize"},
]
),
lambda r: r["data"].update(sets=[{"name": "x", "dir": "../a", "kind": "cropped"}]),
lambda r: r["data"].update(sets=[{"name": "x", "dir": "a/../../b", "kind": "cropped"}]),
lambda r: r["data"].update(sets=[{"name": "x", "dir": "a", "kind": "whole"}]),
lambda r: r["data"].update(sets=[{"name": "x/y", "dir": "a", "kind": "cropped"}]),
lambda r: r["data"].update(sets=[{"name": "", "dir": "a", "kind": "cropped"}]),
lambda r: r["data"].update(label_threshold=0),
lambda r: r["data"].update(label_threshold=256),
lambda r: r["data"].update(unknown=1),
lambda r: r.pop("models"),
lambda r: r["data"].pop("sets"),
lambda r: r.update(resunet={"foreground_channel": 2}),
lambda r: r.update(postprocess={"min_area": -1}),
lambda r: r.update(postprocess={"mask_threshold": 255}),
lambda r: r.update(detection={"score_threshold": 1.0}),
lambda r: r.update(detection={"input_color": "gray"}),
lambda r: r.update(evaluation={"iou_threshold": 0.0}),
lambda r: r.update(unknown={}),
],
ids=[
"empty_sets",
"duplicate_names",
"dotdot_dir",
"nested_dotdot_dir",
"unknown_kind",
"name_with_slash",
"empty_name",
"label_threshold_0",
"label_threshold_256",
"unknown_data_key",
"missing_models",
"missing_sets",
"bad_channel",
"negative_min_area",
"mask_threshold_255",
"score_threshold_1",
"bad_input_color",
"iou_threshold_0",
"unknown_top_key",
],
)
def test_invalid_seg_settings_are_rejected(mutate) -> None:
raw = _raw()
mutate(raw)
with pytest.raises(ValidationError):
SegEvalConfig.model_validate(raw)
def test_absolute_set_dir_is_rejected(tmp_path: Path) -> None:
raw = _raw()
raw["data"]["sets"] = [{"name": "x", "dir": tmp_path.as_posix(), "kind": "cropped"}]
with pytest.raises(ValidationError):
SegEvalConfig.model_validate(raw)