Files
StarPilot/scripts/speed_limit_vision/evaluate_review_classifier.py
T
firestar5683 e577502f4b VACATION
2026-07-12 17:53:20 -05:00

123 lines
5.0 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
from collections import Counter
from pathlib import Path
import cv2
import starpilot.system.speed_limit_vision as slv
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Evaluate the integrated value/reject classifier on reviewed crops.")
parser.add_argument("--models-dir", type=Path, default=Path("starpilot/assets/vision_models"))
parser.add_argument("--positive-manifest", type=Path, required=True)
parser.add_argument("--reject-manifest", type=Path, required=True)
parser.add_argument("--split", choices=("train", "val"), help="Optional source split filter.")
parser.add_argument("--output-csv", type=Path)
return parser.parse_args()
def read_rows(path: Path) -> list[dict[str, str]]:
with path.expanduser().resolve().open("r", encoding="utf-8", newline="") as handle:
return list(csv.DictReader(handle))
def parse_speed(text: str) -> int | None:
try:
return int(float((text or "").strip()))
except ValueError:
return None
def main() -> int:
args = parse_args()
models_dir = args.models_dir.expanduser().resolve()
slv.US_DETECTOR_MODEL_PATH = models_dir / "speed_limit_us_detector.onnx"
slv.US_CLASSIFIER_MODEL_PATH = models_dir / "speed_limit_us_value_classifier.onnx"
reject_path = models_dir / "speed_limit_us_reject_classifier.onnx"
if reject_path.is_file():
slv.US_REJECT_CLASSIFIER_MODEL_PATH = reject_path
daemon = slv.SpeedLimitVisionDaemon(use_runtime=False)
cases: list[tuple[str, dict[str, str], int | None]] = []
for row in read_rows(args.positive_manifest):
if args.split and row.get("split") != args.split:
continue
kind = "advisory" if row.get("review_sign_type", "").strip().lower() == "advisory" else "regulatory"
cases.append((kind, row, parse_speed(row.get("speed_limit_mph", "")) if kind == "regulatory" else None))
for row in read_rows(args.reject_manifest):
if not args.split or row.get("split") == args.split:
cases.append(("hard_negative", row, None))
counts: Counter[str] = Counter()
output_rows = []
for kind, row, expected in cases:
crop_path = Path(row.get("crop_path", "")).expanduser()
crop = cv2.imread(str(crop_path))
if crop is None:
counts[f"{kind}_unreadable"] += 1
continue
result = daemon._classify_speed_limit_from_model(crop)
predicted = result[0] if result is not None else None
confidence = result[1] if result is not None else None
counts[f"{kind}_total"] += 1
if kind == "regulatory":
counts[f"regulatory_speed_{expected}_total"] += 1
if predicted is not None:
counts["regulatory_any"] += 1
if predicted == expected:
counts["regulatory_exact"] += 1
counts[f"regulatory_speed_{expected}_exact"] += 1
elif predicted is not None:
counts["regulatory_wrong"] += 1
elif predicted is None:
counts[f"{kind}_rejected"] += 1
else:
counts[f"{kind}_false_read"] += 1
output_rows.append({
"record_key": row.get("record_key", ""),
"split": row.get("split", ""),
"kind": kind,
"crop_path": str(crop_path),
"expected_speed_limit_mph": "" if expected is None else expected,
"predicted_speed_limit_mph": "" if predicted is None else predicted,
"confidence": "" if confidence is None else f"{confidence:.6f}",
})
regulatory_total = counts["regulatory_total"]
advisory_total = counts["advisory_total"]
hard_negative_total = counts["hard_negative_total"]
exact_rate = counts["regulatory_exact"] / regulatory_total if regulatory_total else 0.0
advisory_reject_rate = counts["advisory_rejected"] / advisory_total if advisory_total else 0.0
hard_negative_reject_rate = counts["hard_negative_rejected"] / hard_negative_total if hard_negative_total else 0.0
regulatory_summary = f"Regulatory exact: {counts['regulatory_exact']}/{regulatory_total} ({exact_rate:.3f})"
print(f"{regulatory_summary}; wrong reads: {counts['regulatory_wrong']}")
print(f"Advisory rejected: {counts['advisory_rejected']}/{advisory_total} ({advisory_reject_rate:.3f})")
hard_negative_summary = f"Hard negatives rejected: {counts['hard_negative_rejected']}/{hard_negative_total}"
print(f"{hard_negative_summary} ({hard_negative_reject_rate:.3f})")
speed_parts = []
for speed in slv.US_CLASSIFIER_SPEED_VALUES:
total = counts[f"regulatory_speed_{speed}_total"]
if total:
speed_parts.append(f"{speed}:{counts[f'regulatory_speed_{speed}_exact']}/{total}")
print("Exact by speed: " + " ".join(speed_parts))
if args.output_csv:
output = args.output_csv.expanduser().resolve()
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=tuple(output_rows[0]) if output_rows else ("record_key",))
writer.writeheader()
writer.writerows(output_rows)
print(f"Wrote {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())