AVIS / eval /harness.py
X2-0's picture
HF Clean Deploy
1c0c94d
Raw
History Blame Contribute Delete
3.63 kB
"""Run the pipeline over a labelled dataset and compute metrics.
Ground truth is image-level: each sample lists the violation *types* truly present
(plus an optional ground-truth plate). We report violation-level Precision/Recall/F1
two ways — **rule-only** (every rule candidate) vs **rule+VLM routed** (only auto/VLM
confirmed) — which is the ablation that shows what the VLM verification buys.
Detection mAP needs bbox-level annotations and is out of scope for this image-level
harness; use the base detector's COCO mAP for object-detection mAP.
"""
from __future__ import annotations
import json
import time
from dataclasses import dataclass
from core.pipeline import process
from core.schemas import Route
from eval.metrics import PRF, char_accuracy, confusion, macro_f1
CONFIRMED = {Route.auto_confirmed.value, Route.vlm_confirmed.value}
ALL_TYPES = [
"HELMET_NON_COMPLIANCE",
"TRIPLE_RIDING",
"SEATBELT_NON_COMPLIANCE",
"STOP_LINE_VIOLATION",
"RED_LIGHT_VIOLATION",
"ILLEGAL_PARKING",
"WRONG_SIDE_DRIVING",
]
@dataclass
class Sample:
image: str
expected: set[str]
camera_id: str | None = None
plate: str | None = None
@dataclass
class EvalReport:
n: int
rule_only: dict[str, PRF]
routed: dict[str, PRF]
macro_f1_rule_only: float
macro_f1_routed: float
dispositions: dict[str, int]
mean_latency_s: float
plate_whole_accuracy: float | None
plate_char_accuracy: float | None
def load_dataset(path: str) -> list[Sample]:
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
return [
Sample(
image=s["image"],
expected=set(s.get("expected", [])),
camera_id=s.get("camera_id"),
plate=s.get("plate"),
)
for s in data["samples"]
]
def evaluate(
dataset: list[Sample], *, labels: list[str] | None = None, **pipeline_kwargs
) -> EvalReport:
labels = labels or ALL_TYPES
expected: list[set[str]] = []
pred_rule_only: list[set[str]] = []
pred_routed: list[set[str]] = []
dispositions: dict[str, int] = {}
latencies: list[float] = []
plate_whole: list[float] = []
plate_char: list[float] = []
for s in dataset:
t0 = time.perf_counter()
violations, graph = process(
"eval", s.image, camera_id=s.camera_id, **pipeline_kwargs
)
latencies.append(time.perf_counter() - t0)
expected.append(s.expected)
pred_rule_only.append({v.type for v in violations})
pred_routed.append({v.type for v in violations if v.route.value in CONFIRMED})
for v in violations:
dispositions[v.route.value] = dispositions.get(v.route.value, 0) + 1
if s.plate:
reads = [p.text for p in graph.plates if p.text]
pred_plate = reads[0] if reads else ""
plate_whole.append(1.0 if pred_plate == s.plate else 0.0)
plate_char.append(char_accuracy(pred_plate, s.plate))
rule_only = confusion(expected, pred_rule_only, labels)
routed = confusion(expected, pred_routed, labels)
return EvalReport(
n=len(dataset),
rule_only=rule_only,
routed=routed,
macro_f1_rule_only=macro_f1(rule_only),
macro_f1_routed=macro_f1(routed),
dispositions=dispositions,
mean_latency_s=(sum(latencies) / len(latencies)) if latencies else 0.0,
plate_whole_accuracy=(sum(plate_whole) / len(plate_whole))
if plate_whole
else None,
plate_char_accuracy=(sum(plate_char) / len(plate_char)) if plate_char else None,
)