agent-l / tests /test_semantic_evaluator.py
zhou777's picture
Add files using upload-large-folder tool
0525670 verified
Raw
History Blame Contribute Delete
14.7 kB
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from agents.semantic_evaluator import SemanticEvaluator
from pipelines.evaluate_semantic_predictions import run_semantic_evaluation
from pipelines.prepare_validation_reference import run_validation_reference_build
from pipelines.run_validation_inference import ValidationInferencePipeline
class SemanticEvaluatorTest(unittest.TestCase):
def test_evaluate_predictions_matches_targets_and_reports_metrics(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
reference_path = temp_path / "reference.jsonl"
prediction_path = temp_path / "prediction.jsonl"
reference_rows = [
{
"image_path": "/tmp/img_a.jpg",
"annotations": [
{
"bbox_1000": [100, 100, 200, 200],
"maturity_level": "未成熟",
"maturity_ratio": 0.1,
"occlusion_degree": "无",
"reasoning": "ref1",
},
{
"bbox_1000": [300, 300, 400, 400],
"maturity_level": "完熟",
"maturity_ratio": 0.9,
"occlusion_degree": "轻度",
"reasoning": "ref2",
},
],
}
]
prediction_rows = [
{
"image_path": "/workspace/img_a.jpg",
"annotations": [
{
"bbox_1000": [102, 102, 198, 198],
"maturity_level": "未成熟",
"maturity_ratio": 0.12,
"occlusion_degree": "无",
"reasoning": "pred1",
},
{
"bbox_1000": [302, 302, 398, 398],
"maturity_level": "半成熟",
"maturity_ratio": 0.5,
"occlusion_degree": "轻度",
"reasoning": "pred2",
},
{
"bbox_1000": [700, 700, 800, 800],
"maturity_level": "未成熟",
"maturity_ratio": 0.1,
"occlusion_degree": "无",
"reasoning": "extra",
},
],
}
]
reference_path.write_text("\n".join(json.dumps(row, ensure_ascii=False) for row in reference_rows) + "\n", encoding="utf-8")
prediction_path.write_text("\n".join(json.dumps(row, ensure_ascii=False) for row in prediction_rows) + "\n", encoding="utf-8")
evaluator = SemanticEvaluator(config={"match_iou_threshold": 0.5, "risk_top_k": 5, "bbox_coordinate_space": "bbox_1000"})
result = evaluator.evaluate_predictions(
reference_dataset_path=str(reference_path),
prediction_dataset_path=str(prediction_path),
)
self.assertEqual(result["summary"]["image_count"], 1)
self.assertEqual(result["summary"]["reference_annotations_total"], 2)
self.assertEqual(result["summary"]["prediction_annotations_total"], 3)
self.assertEqual(result["summary"]["matched_annotations_total"], 2)
self.assertAlmostEqual(result["summary"]["precision"], 0.6667, places=4)
self.assertAlmostEqual(result["summary"]["recall"], 1.0, places=4)
self.assertAlmostEqual(result["summary"]["f1"], 0.8, places=4)
self.assertAlmostEqual(result["summary"]["maturity_accuracy_on_matched"], 0.5, places=4)
per_image = result["per_image_results"][0]
self.assertEqual(per_image["image_key"], "img_a.jpg")
self.assertEqual(per_image["matched_count"], 2)
self.assertEqual(per_image["unmatched_prediction_count"], 1)
self.assertAlmostEqual(per_image["f1"], 0.8, places=4)
def test_evaluator_can_compare_pixel_predictions_against_bbox_1000_reference(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
reference_path = temp_path / "reference.jsonl"
prediction_path = temp_path / "prediction.jsonl"
reference_rows = [
{
"image_path": "/tmp/img_c.jpg",
"annotations": [
{
"bbox_1000": [100, 100, 300, 300],
"image_width": 1000,
"image_height": 500,
}
],
}
]
prediction_rows = [
{
"image_path": "/tmp/img_c.jpg",
"annotations": [
{
"bbox": [100, 50, 300, 150],
"image_width": 1000,
"image_height": 500,
}
],
}
]
reference_path.write_text(json.dumps(reference_rows[0], ensure_ascii=False) + "\n", encoding="utf-8")
prediction_path.write_text(json.dumps(prediction_rows[0], ensure_ascii=False) + "\n", encoding="utf-8")
evaluator = SemanticEvaluator(config={"bbox_coordinate_space": "bbox_1000"})
result = evaluator.evaluate_predictions(str(reference_path), str(prediction_path))
self.assertEqual(result["summary"]["matched_annotations_total"], 1)
self.assertAlmostEqual(result["summary"]["precision"], 1.0, places=4)
self.assertAlmostEqual(result["summary"]["recall"], 1.0, places=4)
def test_pipeline_writes_summary_and_jsonl_outputs(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
reference_path = temp_path / "reference.jsonl"
prediction_path = temp_path / "prediction.jsonl"
output_dir = temp_path / "semantic_eval"
reference_gold_row = {
"image_path": "/tmp/img_b.jpg",
"source_records": [
{
"bbox": [40, 50, 80, 100],
"bbox_1000": [100, 100, 200, 200],
"image_width": 400,
"image_height": 500,
"maturity_level": "未成熟",
"maturity_ratio": 0.1,
"occlusion_degree": "无",
"reasoning": "gold",
}
],
"messages": [
{"role": "assistant", "content": json.dumps({"annotations": [{"bbox": [100, 100, 200, 200]}]}, ensure_ascii=False)}
],
}
prediction_row = {
"image_path": "/tmp/img_b.jpg",
"annotations": [
{
"bbox_1000": [100, 100, 200, 200],
"maturity_level": "未成熟",
"maturity_ratio": 0.1,
"occlusion_degree": "无",
"reasoning": "pred",
}
],
}
reference_path.write_text(json.dumps(reference_gold_row, ensure_ascii=False) + "\n", encoding="utf-8")
prediction_path.write_text(json.dumps(prediction_row, ensure_ascii=False) + "\n", encoding="utf-8")
summary = run_semantic_evaluation(
reference_dataset_path=reference_path,
prediction_dataset_path=prediction_path,
output_dir=output_dir,
bbox_coordinate_space="bbox_1000",
)
self.assertTrue((output_dir / "summary.json").exists())
self.assertTrue((output_dir / "report.md").exists())
self.assertTrue((output_dir / "per_image_results.jsonl").exists())
self.assertTrue((output_dir / "risk_cases.jsonl").exists())
self.assertEqual(summary["matched_annotations_total"], 1)
self.assertEqual(summary["bbox_coordinate_space"], "bbox_1000")
def test_validation_reference_builder_keeps_only_val_images(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
gold_path = temp_path / "gold.jsonl"
val_path = temp_path / "val.jsonl"
output_dir = temp_path / "reference"
gold_rows = [
{"image_path": "/tmp/a.jpg", "source_records": []},
{"image_path": "/tmp/b.jpg", "source_records": []},
]
val_rows = [
{"images": ["/tmp/b.jpg"], "metadata": {"image_path": "/tmp/b.jpg"}},
]
gold_path.write_text("\n".join(json.dumps(row, ensure_ascii=False) for row in gold_rows) + "\n", encoding="utf-8")
val_path.write_text(json.dumps(val_rows[0], ensure_ascii=False) + "\n", encoding="utf-8")
summary = run_validation_reference_build(gold_path, val_path, output_dir)
subset_lines = (output_dir / "reference_subset.jsonl").read_text(encoding="utf-8").splitlines()
self.assertEqual(summary["matched_reference_rows"], 1)
self.assertEqual(len(subset_lines), 1)
self.assertIn("/tmp/b.jpg", subset_lines[0])
def test_validation_inference_parser_converts_bbox_1000_to_pixel(self) -> None:
pipeline = ValidationInferencePipeline()
annotations, error = pipeline._parse_prediction_annotations(
raw_text='[{"bbox_2d": [100, 200, 300, 400], "label": "未成熟番茄"}]',
image_width=1000,
image_height=500,
)
self.assertIsNone(error)
self.assertEqual(len(annotations), 1)
self.assertEqual(annotations[0]["bbox_1000"], [100.0, 200.0, 300.0, 400.0])
self.assertEqual(annotations[0]["bbox"], [100.0, 100.0, 300.0, 200.0])
self.assertEqual(annotations[0]["maturity_level"], "未成熟")
def test_validation_inference_parser_accepts_string_bbox_inside_think(self) -> None:
pipeline = ValidationInferencePipeline()
raw_text = (
'<think>\n'
'[{"bbox_2d": "[290, 526, 380, 754]", "label": "未成熟番茄"}]\n'
'</think>\n\n'
'[{"bbox_2d": "[290, 526, 380, 754]", "label": "未成熟番茄"}]'
)
annotations, error = pipeline._parse_prediction_annotations(
raw_text=raw_text,
image_width=4480,
image_height=2016,
)
self.assertIsNone(error)
self.assertEqual(len(annotations), 1)
self.assertEqual(annotations[0]["bbox_1000"], [290.0, 526.0, 380.0, 754.0])
self.assertEqual(annotations[0]["maturity_level"], "未成熟")
def test_validation_inference_parser_ignores_bbox_lists_in_reasoning(self) -> None:
pipeline = ValidationInferencePipeline()
raw_text = (
'<think>\n'
'候选目标位于 [290, 526, 380, 754],另一个位于 [452, 703, 544, 926]。\n'
'</think>\n\n'
'[{"bbox_2d": "[452, 703, 544, 926]", "label": "未成熟番茄", '
'"attribute_text": "无遮挡,局部可见绿色"}]'
)
annotations, error = pipeline._parse_prediction_annotations(
raw_text=raw_text,
image_width=4480,
image_height=2016,
)
self.assertIsNone(error)
self.assertEqual(len(annotations), 1)
self.assertEqual(annotations[0]["bbox_1000"], [452.0, 703.0, 544.0, 926.0])
self.assertEqual(annotations[0]["occlusion_degree"], "无")
def test_validation_inference_request_keeps_output_schema_as_user_constraint(self) -> None:
class DummyInferRequest:
def __init__(self, messages: list[dict[str, str]], images: list[str]) -> None:
self.messages = messages
self.images = images
pipeline = ValidationInferencePipeline()
row = {
"messages": [
{"role": "user", "content": "<image>找到图像中的番茄。"},
{
"role": "assistant",
"content": '[{"bbox_2d": "<bbox>", "label": "<ref-object>", "attribute_text": "无遮挡,局部可见绿色"}]',
},
],
"images": ["/tmp/demo.jpg"],
}
request = pipeline._build_infer_request(row, DummyInferRequest)
self.assertEqual(len(request.messages), 2)
self.assertEqual(request.messages[0]["role"], "user")
self.assertEqual(request.messages[0]["content"], "<image>找到图像中的番茄。")
self.assertEqual(request.messages[1]["role"], "user")
self.assertIn("严格按照下面的 JSON 模板输出最终答案", request.messages[1]["content"])
self.assertIn('"attribute_text"', request.messages[1]["content"])
self.assertEqual(request.images, ["/tmp/demo.jpg"])
def test_validation_inference_can_override_output_template_for_ablation(self) -> None:
pipeline = ValidationInferencePipeline(output_template_mode="official")
prompt = pipeline._build_output_format_prompt(
'[{"bbox_2d": "<bbox>", "label": "<ref-object>", "attribute_text": "无遮挡,局部可见绿色"}]'
)
self.assertIn('"bbox_2d"', prompt)
self.assertIn('"label"', prompt)
self.assertNotIn("attribute_text", prompt)
def test_validation_inference_parser_accepts_structured_occlusion_field(self) -> None:
pipeline = ValidationInferencePipeline()
annotations, parse_error = pipeline._parse_prediction_annotations(
raw_text='[{"bbox_2d": "[100, 200, 300, 400]", "label": "未成熟番茄", '
'"occlusion_degree": "轻度", "visible_color": "绿色"}]',
image_width=1000,
image_height=1000,
)
self.assertIsNone(parse_error)
self.assertEqual(annotations[0]["occlusion_degree"], "轻度")
self.assertEqual(annotations[0]["visible_color"], "绿色")
if __name__ == "__main__":
unittest.main()