mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-08 23:22:04 +08:00
185 lines
6.4 KiB
Python
185 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import random
|
|
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
|
|
import starpilot.system.speed_limit_vision as slv
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Evaluate runtime speed-limit ONNX models on a mined frame manifest.")
|
|
parser.add_argument(
|
|
"--models-dir",
|
|
type=Path,
|
|
default=Path("starpilot/assets/vision_models"),
|
|
help="Directory containing speed_limit_us_detector.onnx and speed_limit_us_value_classifier.onnx.",
|
|
)
|
|
parser.add_argument("--manifest", type=Path, required=True, help="CSV manifest with dataset_image/frame_path and labels.")
|
|
parser.add_argument("--split", action="append", help="Optional split filter. Repeat for multiple splits.")
|
|
parser.add_argument("--max-rows", type=int, default=0, help="Optional cap after filtering.")
|
|
parser.add_argument("--seed", type=int, default=0, help="Sampling seed used with --max-rows.")
|
|
parser.add_argument("--output-csv", type=Path, help="Optional per-row prediction output.")
|
|
parser.add_argument("--strict-positive-recall", type=float, help="Exit non-zero if positive exact recall is below this value.")
|
|
parser.add_argument("--strict-negative-fpr", type=float, help="Exit non-zero if negative false-positive rate is above this value.")
|
|
return parser.parse_args()
|
|
|
|
|
|
def first_present(row: dict[str, str], keys: tuple[str, ...]) -> str:
|
|
for key in keys:
|
|
value = row.get(key, "").strip()
|
|
if value:
|
|
return value
|
|
return ""
|
|
|
|
|
|
def expected_value(row: dict[str, str]) -> int | None:
|
|
value_text = first_present(row, ("speed_limit_mph", "dominant_value"))
|
|
if value_text:
|
|
try:
|
|
return int(float(value_text))
|
|
except ValueError:
|
|
return None
|
|
|
|
for key in ("full_detection", "model_read", "ocr_read"):
|
|
read_text = row.get(key, "").strip()
|
|
if "@" in read_text:
|
|
try:
|
|
return int(float(read_text.split("@", 1)[0]))
|
|
except ValueError:
|
|
return None
|
|
return None
|
|
|
|
|
|
def is_negative(row: dict[str, str]) -> bool:
|
|
sample_type = row.get("sample_type", "").lower()
|
|
if "negative" in sample_type:
|
|
return True
|
|
return expected_value(row) is None
|
|
|
|
|
|
def load_rows(manifest_path: Path, splits: set[str] | None) -> list[dict[str, str]]:
|
|
with manifest_path.open("r", encoding="utf-8", newline="") as manifest_file:
|
|
reader = csv.DictReader(manifest_file)
|
|
rows = []
|
|
for row in reader:
|
|
if splits is not None and row.get("split", "") not in splits:
|
|
continue
|
|
rows.append(row)
|
|
return rows
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
models_dir = args.models_dir.expanduser().resolve()
|
|
detector_path = models_dir / "speed_limit_us_detector.onnx"
|
|
classifier_path = models_dir / "speed_limit_us_value_classifier.onnx"
|
|
if not detector_path.is_file():
|
|
raise FileNotFoundError(detector_path)
|
|
if not classifier_path.is_file():
|
|
raise FileNotFoundError(classifier_path)
|
|
|
|
rows = load_rows(args.manifest.expanduser().resolve(), set(args.split) if args.split else None)
|
|
if args.max_rows > 0 and len(rows) > args.max_rows:
|
|
rng = random.Random(args.seed)
|
|
rows = rng.sample(rows, args.max_rows)
|
|
|
|
slv.US_DETECTOR_MODEL_PATH = detector_path
|
|
slv.US_CLASSIFIER_MODEL_PATH = classifier_path
|
|
daemon = slv.SpeedLimitVisionDaemon(use_runtime=False)
|
|
|
|
output_rows: list[dict[str, str]] = []
|
|
positive_count = 0
|
|
positive_exact = 0
|
|
positive_detected = 0
|
|
negative_count = 0
|
|
negative_false_positive = 0
|
|
unreadable_count = 0
|
|
|
|
for row in rows:
|
|
image_text = first_present(row, ("dataset_image", "frame_path", "source_frame"))
|
|
if not image_text:
|
|
unreadable_count += 1
|
|
continue
|
|
|
|
image_path = Path(image_text).expanduser().resolve()
|
|
frame_bgr = cv2.imread(str(image_path))
|
|
if frame_bgr is None:
|
|
unreadable_count += 1
|
|
continue
|
|
|
|
detection = daemon._detect_sign(frame_bgr)
|
|
predicted_value = detection.speed_limit_mph if detection is not None else None
|
|
confidence = detection.confidence if detection is not None else None
|
|
expected = expected_value(row)
|
|
negative = is_negative(row)
|
|
|
|
if negative:
|
|
negative_count += 1
|
|
if predicted_value is not None:
|
|
negative_false_positive += 1
|
|
else:
|
|
positive_count += 1
|
|
if predicted_value is not None:
|
|
positive_detected += 1
|
|
if predicted_value == expected:
|
|
positive_exact += 1
|
|
|
|
if args.output_csv:
|
|
output_rows.append({
|
|
"record_key": row.get("record_key", ""),
|
|
"split": row.get("split", ""),
|
|
"sample_type": row.get("sample_type", ""),
|
|
"image_path": str(image_path),
|
|
"expected_speed_limit_mph": "" if expected is None else str(expected),
|
|
"predicted_speed_limit_mph": "" if predicted_value is None else str(predicted_value),
|
|
"confidence": "" if confidence is None else f"{confidence:.6f}",
|
|
"negative": str(negative),
|
|
})
|
|
|
|
positive_exact_recall = positive_exact / positive_count if positive_count else 0.0
|
|
positive_any_recall = positive_detected / positive_count if positive_count else 0.0
|
|
negative_fpr = negative_false_positive / negative_count if negative_count else 0.0
|
|
|
|
print(f"Rows evaluated: {positive_count + negative_count}")
|
|
print(f"Unreadable rows: {unreadable_count}")
|
|
print(
|
|
f"Positive exact: {positive_exact}/{positive_count} "
|
|
f"({positive_exact_recall:.3f}); any detection: {positive_detected}/{positive_count} ({positive_any_recall:.3f})"
|
|
)
|
|
print(f"Negative false positives: {negative_false_positive}/{negative_count} ({negative_fpr:.3f})")
|
|
|
|
if args.output_csv:
|
|
args.output_csv.parent.mkdir(parents=True, exist_ok=True)
|
|
with args.output_csv.open("w", encoding="utf-8", newline="") as output_file:
|
|
fieldnames = (
|
|
"record_key",
|
|
"split",
|
|
"sample_type",
|
|
"image_path",
|
|
"expected_speed_limit_mph",
|
|
"predicted_speed_limit_mph",
|
|
"confidence",
|
|
"negative",
|
|
)
|
|
writer = csv.DictWriter(output_file, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
writer.writerows(output_rows)
|
|
print(f"Wrote {args.output_csv}")
|
|
|
|
failed = False
|
|
if args.strict_positive_recall is not None and positive_exact_recall < args.strict_positive_recall:
|
|
failed = True
|
|
if args.strict_negative_fpr is not None and negative_fpr > args.strict_negative_fpr:
|
|
failed = True
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|