import json
from pathlib import Path
import pytest
from evalscope.api.dataset import Sample
from evalscope.api.evaluator import TaskState
from evalscope.api.metric import AggScore, MetricIdentity, SampleScore, Score
from evalscope.api.registry import get_benchmark
from evalscope.benchmarks.surds.utils import (
IMAGE_SIZE,
SUBSET_LIST,
build_official_vqa_records,
compute_centerness,
extract_tagged_answer,
normalize_answer,
parse_point,
)
from evalscope.config import TaskConfig
from evalscope.report import ReportGenerator
def _source_record(file_name: str, descriptions: list[str]) -> dict:
count = len(descriptions)
return {
'file_name': file_name,
'descs': descriptions,
'xy2Ds': [[400 + 300 * index, 450] for index in range(count)],
'xyz3Ds': [[0.0, 0.0, 5.0 + 5 * index] for index in range(count)],
'bboxes2D': [[350 + 300 * index, 400, 450 + 300 * index, 500] for index in range(count)],
'categories': ['car'] * count,
'yaws': [0.0] * count,
'yaw_descs': ['North'] * count,
'depths': [5.0 + 5 * index for index in range(count)],
'distances': [5 + 5 * index for index in range(count)],
}
def test_official_vqa_generation_is_deterministic() -> None:
records = [
_source_record('image/CAM_FRONT/single-a.webp', ['white car']),
_source_record('image/CAM_FRONT/single-b.webp', ['adult wearing blue clothes']),
_source_record('image/CAM_FRONT/multi.webp', ['white car', 'adult wearing red clothes']),
]
first = build_official_vqa_records(records)
second = build_official_vqa_records(records)
assert first == second
assert {subset: len(first[subset]) for subset in SUBSET_LIST} == dict.fromkeys(SUBSET_LIST, 1)
assert len(first['yaw'][0]['prompts']) == 2
assert len(first['distance'][0]['prompts']) == 2
assert len(first['left_right'][0]['prompts']) == 2
assert len(first['front_behind'][0]['prompts']) == 2
assert len(first['xy2d'][0]['prompts']) == 1
assert len(first['depth'][0]['prompts']) == 1
assert first['distance'][0]['answers'] == ['The white car', 'The adult wearing red clothes']
assert first['left_right'][0]['answers'] == ['The white car', 'The adult wearing red clothes']
assert first['front_behind'][0]['answers'] == ['No', 'Yes']
def test_answer_extraction_and_normalization_match_official_rules() -> None:
assert extract_tagged_answer('... The white car. ') == 'The white car.'
assert extract_tagged_answer('firstsecond') == 'first'
assert extract_tagged_answer('The white car') == ''
assert normalize_answer(' The White Car. ') == 'white car'
assert normalize_answer('Between 2 meters, and 8 meters!') == 'between 2 meters and 8 meters'
def test_point_parsing_and_centerness_match_official_rules() -> None:
assert parse_point('[0.5, 0.5]') == (800.0, 450.0)
assert parse_point('[10, 20, 30, 40]') == (20.0, 30.0)
assert parse_point('[1600, 450]') is None
assert parse_point('[1, 1]') == (1.0, 1.0)
assert parse_point('no coordinates') is None
bbox = [100, 100, 300, 300]
assert compute_centerness((200, 200), bbox) == pytest.approx(1.0)
assert compute_centerness((100, 200), bbox) == pytest.approx(0.0)
assert compute_centerness((99, 200), bbox) == pytest.approx(0.0)
def test_adapter_applies_official_validity_and_pixel_scoring() -> None:
adapter = get_benchmark('surds', TaskConfig(model='mock', datasets=['surds']))
categorical_state = TaskState(
model='mock',
sample=Sample(
input='question',
target='The white car',
metadata={'task': 'distance', 'options': ['The white car', 'The red car']},
),
)
exact = adapter.match_score('', 'The white car', 'The white car', categorical_state)
missing_article = adapter.match_score('', 'white car', 'The white car', categorical_state)
assert exact.value == {'normalized_score': 1.0}
assert missing_article.value == {'normalized_score': 0.0}
pixel_state = TaskState(
model='mock',
sample=Sample(
input='question',
target='[50, 50]',
metadata={'task': 'xy2d', 'bbox': [0, 0, 100, 100], 'image_size': [1600, 900]},
),
)
pixel = adapter.match_score('', '[50, 50]', '[50, 50]', pixel_state)
assert pixel.value == {'normalized_score': 1.0}
def test_pair_aggregation_requires_both_complementary_answers() -> None:
adapter = get_benchmark('surds', TaskConfig(model='mock', datasets=['surds']))
scores = [
SampleScore(
score=Score(value={'normalized_score': 1.0}),
sample_id=0,
generation_index=0,
sample_metadata={'paired': True, 'pair_id': 'a'},
),
SampleScore(
score=Score(value={'normalized_score': 1.0}),
sample_id=1,
generation_index=0,
sample_metadata={'paired': True, 'pair_id': 'a'},
),
SampleScore(
score=Score(value={'normalized_score': 1.0}),
sample_id=2,
generation_index=1,
sample_metadata={'paired': True, 'pair_id': 'a'},
),
SampleScore(
score=Score(value={'normalized_score': 0.0}),
sample_id=3,
generation_index=1,
sample_metadata={'paired': True, 'pair_id': 'a'},
),
]
aggregate = adapter.aggregate_scores(scores)[0]
assert aggregate.metric_name == 'normalized_score'
assert aggregate.score == pytest.approx(0.5)
assert aggregate.num == 2
incomplete = adapter.aggregate_scores([
SampleScore(
score=Score(value={'normalized_score': 1.0}),
sample_id=4,
sample_metadata={'paired': True, 'pair_id': 'incomplete'},
)
])[0]
assert incomplete.score == 0.0
assert incomplete.num == 1
def test_report_table_uses_official_equal_task_average() -> None:
adapter = get_benchmark('surds', TaskConfig(model='mock', datasets=['surds']))
subset_scores = {
subset: [AggScore(metric_name='normalized_score', aggregation='mean', score=index / 5, num=5)]
for index, subset in enumerate(SUBSET_LIST)
}
report = ReportGenerator.generate_report(subset_scores, 'mock', adapter)
table = report.to_dataframe(add_overall_metric=True)
overall = table[table['Subset'] == 'OVERALL'].iloc[0]
assert report.primary_metric_identity == MetricIdentity(name='normalized_score', aggregation='mean')
assert table[table['Subset'] != 'OVERALL']['Num'].tolist() == [5] * 6
assert overall['Num'] == 30
assert overall['Score'] == pytest.approx(0.5)
assert report.metrics[0].macro_score == pytest.approx(0.5)
def test_local_dataset_loads_all_six_subsets(tmp_path: Path) -> None:
split_root = tmp_path / 'validation'
image_root = split_root / 'image' / 'CAM_FRONT'
image_root.mkdir(parents=True)
records = [
_source_record('image/CAM_FRONT/single-a.webp', ['white car']),
_source_record('image/CAM_FRONT/single-b.webp', ['adult wearing blue clothes']),
_source_record('image/CAM_FRONT/multi.webp', ['white car', 'adult wearing red clothes']),
]
for record in records:
(split_root / record['file_name']).write_bytes(b'image')
with open(split_root / 'metadata.jsonl', 'w', encoding='utf-8') as file:
for record in records:
file.write(json.dumps(record) + '\n')
config = TaskConfig(
model='mock',
datasets=['surds'],
dataset_args={'surds': {
'local_path': str(tmp_path)
}},
)
datasets = get_benchmark('surds', config).load_dataset()
assert {subset: len(datasets[subset]) for subset in SUBSET_LIST} == {
'yaw': 2,
'xy2d': 1,
'depth': 1,
'distance': 2,
'left_right': 2,
'front_behind': 2,
}
assert all(sample.metadata['image_size'] == list(IMAGE_SIZE) for dataset in datasets.values() for sample in dataset)