mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-14 22:02:09 +08:00
distilled-moonstone v2
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import yaml
|
||||
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from import_manual_review_queue import merged_review_rows, parse_speed # type: ignore
|
||||
else:
|
||||
from .import_manual_review_queue import merged_review_rows, parse_speed
|
||||
|
||||
|
||||
POSITIVE_STATUSES = frozenset(("accepted", "corrected"))
|
||||
IMPORTANT_SPEEDS = frozenset(range(30, 70, 5))
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Add clean reviewed sign tracks to the single-class runtime proposal detector.")
|
||||
parser.add_argument("--base-yaml", type=Path, required=True, help="Existing single-class detector dataset YAML to preserve.")
|
||||
parser.add_argument("--queue", type=Path, required=True, help="Reviewed manual_review_queue.csv used to create the tracks.")
|
||||
parser.add_argument("--labels", type=Path, help="Defaults to manual_review_labels.csv beside the queue.")
|
||||
parser.add_argument("--track-samples", type=Path, required=True, help="track_samples.csv from mine_reviewed_sign_tracks.py.")
|
||||
parser.add_argument("--output", type=Path, required=True, help="Output directory for added images, labels, and dataset.yaml.")
|
||||
parser.add_argument("--train-ratio", type=float, default=0.85, help="Route-level train split ratio.")
|
||||
parser.add_argument("--min-growth", type=float, default=1.0, help="Minimum tracked box area relative to its reviewed anchor.")
|
||||
parser.add_argument("--min-exact-confidence", type=float, default=0.80)
|
||||
parser.add_argument("--min-detector-confidence", type=float, default=0.30)
|
||||
parser.add_argument("--max-track-rank", type=int, default=4)
|
||||
parser.add_argument("--important-repeat", type=int, default=2, help="Train repeats for accepted 30-65 mph samples.")
|
||||
parser.add_argument("--other-repeat", type=int, default=1, help="Train repeats for other accepted speed samples.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_bbox(value: str) -> tuple[int, int, int, int] | None:
|
||||
try:
|
||||
values = tuple(int(round(float(part.strip()))) for part in value.split(","))
|
||||
except ValueError:
|
||||
return None
|
||||
if len(values) != 4:
|
||||
return None
|
||||
x1, y1, x2, y2 = values
|
||||
return values if x2 > x1 and y2 > y1 else None
|
||||
|
||||
|
||||
def split_for_key(key: str, train_ratio: float) -> str:
|
||||
fraction = int(hashlib.sha1(key.encode()).hexdigest()[:8], 16) / 0xFFFFFFFF
|
||||
return "train" if fraction < train_ratio else "val"
|
||||
|
||||
|
||||
def link_or_copy(source: Path, destination: Path) -> None:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if destination.exists():
|
||||
return
|
||||
try:
|
||||
os.link(source, destination)
|
||||
except OSError:
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
|
||||
def yolo_label(bbox: tuple[int, int, int, int], image_path: Path) -> str | None:
|
||||
image = cv2.imread(str(image_path))
|
||||
if image is None:
|
||||
return None
|
||||
height, width = image.shape[:2]
|
||||
x1, y1, x2, y2 = bbox
|
||||
x1 = max(min(x1, width - 1), 0)
|
||||
y1 = max(min(y1, height - 1), 0)
|
||||
x2 = max(min(x2, width), 0)
|
||||
y2 = max(min(y2, height), 0)
|
||||
if x2 <= x1 or y2 <= y1:
|
||||
return None
|
||||
values = (
|
||||
(x1 + x2) / (2 * width),
|
||||
(y1 + y2) / (2 * height),
|
||||
(x2 - x1) / width,
|
||||
(y2 - y1) / height,
|
||||
)
|
||||
return f"0 {values[0]:.8f} {values[1]:.8f} {values[2]:.8f} {values[3]:.8f}\n"
|
||||
|
||||
|
||||
def reviewed_positive_rows(queue_path: Path, labels_path: Path) -> dict[str, dict[str, str]]:
|
||||
return {
|
||||
row.get("record_key", ""): row
|
||||
for row in merged_review_rows(queue_path, labels_path)
|
||||
if row.get("record_key") and row.get("review_status") in POSITIVE_STATUSES and parse_speed(row.get("review_speed_limit_mph", ""))
|
||||
}
|
||||
|
||||
|
||||
def trusted_track_row(row: dict[str, str], reviewed_row: dict[str, str], args: argparse.Namespace) -> bool:
|
||||
original_bbox = parse_bbox(reviewed_row.get("bbox", ""))
|
||||
corrected_bbox = parse_bbox(reviewed_row.get("review_bbox", ""))
|
||||
if corrected_bbox is not None and corrected_bbox != original_bbox:
|
||||
# Existing tracks predate manual box correction, so their propagated boxes are stale.
|
||||
return False
|
||||
try:
|
||||
expected = int(row.get("expected_speed_limit_mph", ""))
|
||||
reviewed_speed = int(parse_speed(reviewed_row.get("review_speed_limit_mph", "")) or 0)
|
||||
predicted = int(row.get("predicted_speed_limit_mph", "") or 0)
|
||||
read_confidence = float(row.get("read_confidence", "") or 0.0)
|
||||
detector_confidence = float(row.get("detector_confidence", "") or 0.0)
|
||||
growth = float(row.get("area_ratio_to_anchor", "") or 0.0)
|
||||
rank = int(row.get("rank", "") or 999)
|
||||
except ValueError:
|
||||
return False
|
||||
exact_read = predicted == expected and read_confidence >= args.min_exact_confidence
|
||||
detector_snap = detector_confidence >= args.min_detector_confidence
|
||||
return expected == reviewed_speed and growth >= args.min_growth and rank <= args.max_track_rank and (exact_read or detector_snap)
|
||||
|
||||
|
||||
def add_sample(
|
||||
output: Path,
|
||||
split: str,
|
||||
stem: str,
|
||||
image_path: Path,
|
||||
bbox: tuple[int, int, int, int],
|
||||
repeats: int,
|
||||
) -> int:
|
||||
label = yolo_label(bbox, image_path)
|
||||
if label is None:
|
||||
return 0
|
||||
created = 0
|
||||
for repeat in range(max(repeats, 1) if split == "train" else 1):
|
||||
suffix = f"_r{repeat:02d}" if split == "train" and repeats > 1 else ""
|
||||
destination_stem = f"{stem}{suffix}"
|
||||
destination_image = output / "images" / split / f"{destination_stem}{image_path.suffix.lower()}"
|
||||
destination_label = output / "labels" / split / f"{destination_stem}.txt"
|
||||
link_or_copy(image_path, destination_image)
|
||||
destination_label.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination_label.write_text(label, encoding="ascii")
|
||||
created += 1
|
||||
return created
|
||||
|
||||
|
||||
def add_reviewed_anchors(
|
||||
reviewed_rows: dict[str, dict[str, str]],
|
||||
output: Path,
|
||||
args: argparse.Namespace,
|
||||
) -> Counter[str]:
|
||||
counts: Counter[str] = Counter()
|
||||
for record_key, row in reviewed_rows.items():
|
||||
speed = int(parse_speed(row.get("review_speed_limit_mph", "")) or 0)
|
||||
bbox = parse_bbox(row.get("review_bbox") or row.get("bbox", ""))
|
||||
image_path = Path(row.get("frame_path", "")).expanduser().resolve()
|
||||
if bbox is None or not image_path.is_file():
|
||||
counts["anchor_rejected"] += 1
|
||||
continue
|
||||
split = split_for_key(row.get("route") or record_key, args.train_ratio)
|
||||
repeats = args.important_repeat if speed in IMPORTANT_SPEEDS else args.other_repeat
|
||||
created = add_sample(output, split, f"anchor_{record_key}", image_path, bbox, repeats)
|
||||
counts[f"anchor_{split}"] += created
|
||||
counts[f"speed_{speed}"] += created
|
||||
return counts
|
||||
|
||||
|
||||
def add_track_samples(
|
||||
reviewed_rows: dict[str, dict[str, str]],
|
||||
output: Path,
|
||||
args: argparse.Namespace,
|
||||
) -> Counter[str]:
|
||||
counts: Counter[str] = Counter()
|
||||
with args.track_samples.expanduser().resolve().open(encoding="utf-8", newline="") as input_file:
|
||||
for row in csv.DictReader(input_file):
|
||||
reviewed_row = reviewed_rows.get(row.get("track_key", ""))
|
||||
if reviewed_row is None or not trusted_track_row(row, reviewed_row, args):
|
||||
counts["track_rejected"] += 1
|
||||
continue
|
||||
bbox = parse_bbox(row.get("bbox", ""))
|
||||
image_path = Path(row.get("frame_path", "")).expanduser().resolve()
|
||||
if bbox is None or not image_path.is_file():
|
||||
counts["track_rejected"] += 1
|
||||
continue
|
||||
speed = int(row["expected_speed_limit_mph"])
|
||||
split = split_for_key(row.get("route") or row.get("track_key", ""), args.train_ratio)
|
||||
repeats = args.important_repeat if speed in IMPORTANT_SPEEDS else args.other_repeat
|
||||
stem = f"track_{row.get('track_key', '')}_{row.get('rank', '')}_{row.get('frame_time_s', '').replace('.', 'p')}"
|
||||
created = add_sample(output, split, stem, image_path, bbox, repeats)
|
||||
counts[f"track_{split}"] += created
|
||||
counts[f"speed_{speed}"] += created
|
||||
return counts
|
||||
|
||||
|
||||
def resolved_dataset_paths(base_yaml: Path, key: str) -> list[str]:
|
||||
data = yaml.safe_load(base_yaml.read_text(encoding="utf-8")) or {}
|
||||
base_root = Path(data.get("path", base_yaml.parent)).expanduser()
|
||||
if not base_root.is_absolute():
|
||||
base_root = (base_yaml.parent / base_root).resolve()
|
||||
entries = data.get(key, [])
|
||||
if isinstance(entries, str):
|
||||
entries = [entries]
|
||||
return [str((base_root / entry).resolve()) if not Path(entry).is_absolute() else str(Path(entry).resolve()) for entry in entries]
|
||||
|
||||
|
||||
def write_dataset_yaml(base_yaml: Path, output: Path) -> Path:
|
||||
train_paths = (*resolved_dataset_paths(base_yaml, "train"), str((output / "images" / "train").resolve()))
|
||||
val_paths = (*resolved_dataset_paths(base_yaml, "val"), str((output / "images" / "val").resolve()))
|
||||
lines = ["train:", *(f" - {path}" for path in train_paths), "val:", *(f" - {path}" for path in val_paths), "names:", " 0: speed_limit_sign"]
|
||||
dataset_yaml = output / "dataset.yaml"
|
||||
dataset_yaml.write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
return dataset_yaml
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if not 0.0 < args.train_ratio < 1.0:
|
||||
raise ValueError("--train-ratio must be between zero and one")
|
||||
queue_path = args.queue.expanduser().resolve()
|
||||
labels_path = args.labels.expanduser().resolve() if args.labels else queue_path.with_name("manual_review_labels.csv")
|
||||
output = args.output.expanduser().resolve()
|
||||
for split in ("train", "val"):
|
||||
(output / "images" / split).mkdir(parents=True, exist_ok=True)
|
||||
(output / "labels" / split).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
reviewed_rows = reviewed_positive_rows(queue_path, labels_path)
|
||||
counts = add_reviewed_anchors(reviewed_rows, output, args)
|
||||
counts.update(add_track_samples(reviewed_rows, output, args))
|
||||
dataset_yaml = write_dataset_yaml(args.base_yaml.expanduser().resolve(), output)
|
||||
summary = {
|
||||
"accepted_source_records": len(reviewed_rows),
|
||||
"base_yaml": str(args.base_yaml.expanduser().resolve()),
|
||||
"dataset_yaml": str(dataset_yaml),
|
||||
"output": str(output),
|
||||
"counts": dict(sorted(counts.items())),
|
||||
}
|
||||
(output / "track_proposal_dataset_summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import starpilot.system.speed_limit_vision as slv
|
||||
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from replay_route_runtime import ( # type: ignore
|
||||
build_runtime_context,
|
||||
configure_models,
|
||||
qlog_paths,
|
||||
replay_route,
|
||||
route_log_id,
|
||||
segment_paths,
|
||||
)
|
||||
else:
|
||||
from .replay_route_runtime import build_runtime_context, configure_models, qlog_paths, replay_route, route_log_id, segment_paths
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundTruthEvent:
|
||||
route: str
|
||||
time_s: float
|
||||
speed_limit_mph: int
|
||||
sign_type: str
|
||||
initial_speed_limit_mph: int
|
||||
notes: str
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Score continuous route replay against independently labeled speed-sign events.")
|
||||
parser.add_argument("--ground-truth", type=Path, required=True, help="CSV with route,time_s,speed_limit_mph and optional metadata.")
|
||||
parser.add_argument("--clip-root", type=Path, default=Path("/Volumes/T5/starpilot_speed_limit/realdata"))
|
||||
parser.add_argument("--models-dir", type=Path, default=Path("starpilot/assets/vision_models"))
|
||||
parser.add_argument("--output-csv", type=Path, required=True)
|
||||
parser.add_argument("--window-before", type=float, default=2.0)
|
||||
parser.add_argument("--window-after", type=float, default=4.0)
|
||||
parser.add_argument("--event-dedupe-seconds", type=float, default=2.0)
|
||||
parser.add_argument("--qlog-context", action="store_true")
|
||||
parser.add_argument("--measured-base-inference-seconds", type=float, default=0.44)
|
||||
parser.add_argument("--measured-classifier-forward-seconds", type=float, default=0.066)
|
||||
parser.add_argument("--crop-ocr", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def int_value(text: str, default: int = 0) -> int:
|
||||
try:
|
||||
return int(float(text))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def load_ground_truth(path: Path) -> list[GroundTruthEvent]:
|
||||
events: list[GroundTruthEvent] = []
|
||||
with path.open(encoding="utf-8", newline="") as input_file:
|
||||
for row in csv.DictReader(input_file):
|
||||
route = route_log_id(row.get("route", ""))
|
||||
try:
|
||||
time_s = float(row.get("time_s", ""))
|
||||
except ValueError:
|
||||
continue
|
||||
speed = int_value(row.get("speed_limit_mph", ""))
|
||||
applicable = row.get("applicable", "true").strip().lower() not in ("0", "false", "no")
|
||||
if not route or time_s < 0 or speed <= 0 or not applicable:
|
||||
continue
|
||||
events.append(GroundTruthEvent(
|
||||
route=route,
|
||||
time_s=time_s,
|
||||
speed_limit_mph=speed,
|
||||
sign_type=row.get("sign_type", "regulatory").strip() or "regulatory",
|
||||
initial_speed_limit_mph=int_value(row.get("initial_speed_limit_mph", "")),
|
||||
notes=row.get("notes", "").strip(),
|
||||
))
|
||||
return sorted(events, key=lambda event: (event.route, event.time_s))
|
||||
|
||||
|
||||
def runtime_value(event: dict[str, str]) -> int:
|
||||
key = "candidateSpeedLimitMph" if event.get("event") == "candidate" else "speedLimitMph"
|
||||
return int_value(event.get(key, ""))
|
||||
|
||||
|
||||
def clustered_runtime_events(events: list[dict[str, str]], event_type: str, dedupe_seconds: float) -> list[dict[str, str]]:
|
||||
clusters: list[dict[str, str]] = []
|
||||
for event in events:
|
||||
if event.get("event") != event_type or runtime_value(event) <= 0:
|
||||
continue
|
||||
if (
|
||||
clusters and
|
||||
runtime_value(clusters[-1]) == runtime_value(event) and
|
||||
float(event["time_s"]) - float(clusters[-1]["time_s"]) <= dedupe_seconds
|
||||
):
|
||||
continue
|
||||
clusters.append(event)
|
||||
return clusters
|
||||
|
||||
|
||||
def events_in_window(events: list[dict[str, str]], truth: GroundTruthEvent, before: float, after: float) -> list[dict[str, str]]:
|
||||
return [event for event in events if truth.time_s - before <= float(event["time_s"]) <= truth.time_s + after]
|
||||
|
||||
|
||||
def first_exact(events: list[dict[str, str]], speed: int) -> dict[str, str] | None:
|
||||
return next((event for event in events if runtime_value(event) == speed), None)
|
||||
|
||||
|
||||
def score_route(
|
||||
truth_events: list[GroundTruthEvent],
|
||||
runtime_events: list[dict[str, str]],
|
||||
window_before: float,
|
||||
window_after: float,
|
||||
dedupe_seconds: float,
|
||||
) -> tuple[list[dict[str, object]], Counter[str]]:
|
||||
candidates = clustered_runtime_events(runtime_events, "candidate", dedupe_seconds)
|
||||
publishes = clustered_runtime_events(runtime_events, "publish", dedupe_seconds)
|
||||
rows: list[dict[str, object]] = []
|
||||
totals: Counter[str] = Counter(total=len(truth_events))
|
||||
|
||||
for truth in truth_events:
|
||||
candidate_window = events_in_window(candidates, truth, window_before, window_after)
|
||||
publish_window = events_in_window(publishes, truth, window_before, window_after)
|
||||
exact_candidate = first_exact(candidate_window, truth.speed_limit_mph)
|
||||
exact_publish = first_exact(publish_window, truth.speed_limit_mph)
|
||||
wrong_candidates = sorted({runtime_value(event) for event in candidate_window if runtime_value(event) != truth.speed_limit_mph})
|
||||
wrong_publishes = sorted({runtime_value(event) for event in publish_window if runtime_value(event) != truth.speed_limit_mph})
|
||||
totals.update(
|
||||
candidate_hit=int(exact_candidate is not None),
|
||||
publish_hit=int(exact_publish is not None),
|
||||
wrong_candidate=int(bool(wrong_candidates)),
|
||||
wrong_publish=int(bool(wrong_publishes)),
|
||||
)
|
||||
rows.append({
|
||||
"route": truth.route,
|
||||
"time_s": f"{truth.time_s:.3f}",
|
||||
"speed_limit_mph": truth.speed_limit_mph,
|
||||
"sign_type": truth.sign_type,
|
||||
"candidate_hit": exact_candidate is not None,
|
||||
"publish_hit": exact_publish is not None,
|
||||
"candidate_latency_s": f"{float(exact_candidate['time_s']) - truth.time_s:.3f}" if exact_candidate else "",
|
||||
"publish_latency_s": f"{float(exact_publish['time_s']) - truth.time_s:.3f}" if exact_publish else "",
|
||||
"candidate_values": "|".join(str(runtime_value(event)) for event in candidate_window),
|
||||
"publish_values": "|".join(str(runtime_value(event)) for event in publish_window),
|
||||
"wrong_candidate_values": "|".join(map(str, wrong_candidates)),
|
||||
"wrong_publish_values": "|".join(map(str, wrong_publishes)),
|
||||
"notes": truth.notes,
|
||||
})
|
||||
|
||||
unmatched_publishes = [
|
||||
event for event in publishes
|
||||
if not any(
|
||||
truth.time_s - window_before <= float(event["time_s"]) <= truth.time_s + window_after and
|
||||
runtime_value(event) == truth.speed_limit_mph
|
||||
for truth in truth_events
|
||||
)
|
||||
]
|
||||
totals["publish_bursts"] = len(publishes)
|
||||
totals["unmatched_publish_bursts"] = len(unmatched_publishes)
|
||||
return rows, totals
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
configure_models(args.models_dir)
|
||||
slv.DETECTOR_CLASSIFIER_CROP_OCR_ENABLED = args.crop_ocr
|
||||
clip_root = args.clip_root.expanduser().resolve()
|
||||
truth_by_route: dict[str, list[GroundTruthEvent]] = defaultdict(list)
|
||||
for event in load_ground_truth(args.ground_truth.expanduser().resolve()):
|
||||
truth_by_route[event.route].append(event)
|
||||
if not truth_by_route:
|
||||
raise ValueError("Ground-truth manifest contains no applicable speed-sign events")
|
||||
|
||||
output_rows: list[dict[str, object]] = []
|
||||
aggregate: Counter[str] = Counter()
|
||||
by_speed: dict[int, Counter[str]] = defaultdict(Counter)
|
||||
route_summaries: dict[str, dict[str, int]] = {}
|
||||
for route, truth_events in truth_by_route.items():
|
||||
segments = segment_paths(clip_root, route)
|
||||
if not segments:
|
||||
raise FileNotFoundError(f"No camera segments for {route} under {clip_root}")
|
||||
runtime_context = None
|
||||
if args.qlog_context:
|
||||
qlogs = qlog_paths(clip_root, route)
|
||||
runtime_context = build_runtime_context(qlogs) if qlogs else None
|
||||
initial_speed = next((event.initial_speed_limit_mph for event in truth_events if event.initial_speed_limit_mph > 0), 0)
|
||||
_summary, runtime_events = replay_route(
|
||||
route,
|
||||
segments,
|
||||
runtime_context,
|
||||
0.0,
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
0.0,
|
||||
args.measured_base_inference_seconds,
|
||||
args.measured_classifier_forward_seconds,
|
||||
initial_speed,
|
||||
)
|
||||
rows, totals = score_route(truth_events, runtime_events, args.window_before, args.window_after, args.event_dedupe_seconds)
|
||||
output_rows.extend(rows)
|
||||
aggregate.update(totals)
|
||||
route_summaries[route] = dict(totals)
|
||||
for row in rows:
|
||||
speed_counts = by_speed[int(row["speed_limit_mph"])]
|
||||
speed_counts.update(
|
||||
total=1,
|
||||
candidate_hit=int(bool(row["candidate_hit"])),
|
||||
publish_hit=int(bool(row["publish_hit"])),
|
||||
wrong_publish=int(bool(row["wrong_publish_values"])),
|
||||
)
|
||||
|
||||
output_path = args.output_csv.expanduser().resolve()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output_path.open("w", encoding="utf-8", newline="") as output_file:
|
||||
writer = csv.DictWriter(output_file, fieldnames=list(output_rows[0]))
|
||||
writer.writeheader()
|
||||
writer.writerows(output_rows)
|
||||
summary = {
|
||||
"models_dir": str(args.models_dir.expanduser().resolve()),
|
||||
"ground_truth": str(args.ground_truth.expanduser().resolve()),
|
||||
"continuous_route_replay": True,
|
||||
"detector_selected_inputs": False,
|
||||
"measured_base_inference_seconds": args.measured_base_inference_seconds,
|
||||
"measured_classifier_forward_seconds": args.measured_classifier_forward_seconds,
|
||||
"totals": dict(aggregate),
|
||||
"by_speed": {str(speed): dict(counts) for speed, counts in sorted(by_speed.items())},
|
||||
"by_route": route_summaries,
|
||||
}
|
||||
output_path.with_suffix(".json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_local_module(name: str):
|
||||
path = Path(__file__).resolve().with_name(f"{name}.py")
|
||||
spec = importlib.util.spec_from_file_location(f"test_local_{name}", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
route_eval = load_local_module("evaluate_route_ground_truth")
|
||||
GroundTruthEvent = route_eval.GroundTruthEvent
|
||||
|
||||
|
||||
def test_clustered_runtime_events_collapses_same_value_burst():
|
||||
events = [
|
||||
{"event": "publish", "time_s": "10.0", "speedLimitMph": "35"},
|
||||
{"event": "publish", "time_s": "10.5", "speedLimitMph": "35"},
|
||||
{"event": "publish", "time_s": "11.0", "speedLimitMph": "45"},
|
||||
]
|
||||
clusters = route_eval.clustered_runtime_events(events, "publish", 2.0)
|
||||
assert [route_eval.runtime_value(event) for event in clusters] == [35, 45]
|
||||
|
||||
|
||||
def test_score_route_separates_candidate_publish_and_wrong_value():
|
||||
truth = [GroundTruthEvent("route", 10.0, 35, "regulatory", 40, "")]
|
||||
events = [
|
||||
{"event": "candidate", "time_s": "10.2", "candidateSpeedLimitMph": "35"},
|
||||
{"event": "publish", "time_s": "10.4", "speedLimitMph": "45"},
|
||||
]
|
||||
rows, totals = route_eval.score_route(truth, events, 1.0, 2.0, 0.5)
|
||||
assert totals["candidate_hit"] == 1
|
||||
assert totals["publish_hit"] == 0
|
||||
assert totals["wrong_publish"] == 1
|
||||
assert totals["unmatched_publish_bursts"] == 1
|
||||
assert rows[0]["wrong_publish_values"] == "45"
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_local_module(name: str):
|
||||
path = Path(__file__).resolve().with_name(f"{name}.py")
|
||||
spec = importlib.util.spec_from_file_location(f"test_local_{name}", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
track_dataset = load_local_module("build_track_proposal_detector_dataset")
|
||||
split_for_key = track_dataset.split_for_key
|
||||
trusted_track_row = track_dataset.trusted_track_row
|
||||
|
||||
|
||||
def args() -> Namespace:
|
||||
return Namespace(min_exact_confidence=0.8, min_detector_confidence=0.3, min_growth=1.0, max_track_rank=4)
|
||||
|
||||
|
||||
def test_split_for_key_keeps_route_samples_together():
|
||||
assert split_for_key("route-a", 0.85) == split_for_key("route-a", 0.85)
|
||||
|
||||
|
||||
def test_trusted_track_requires_current_review_speed_to_match():
|
||||
track = {
|
||||
"expected_speed_limit_mph": "45",
|
||||
"predicted_speed_limit_mph": "45",
|
||||
"read_confidence": "0.99",
|
||||
"detector_confidence": "0.0",
|
||||
"area_ratio_to_anchor": "1.2",
|
||||
"rank": "1",
|
||||
}
|
||||
assert trusted_track_row(track, {"review_speed_limit_mph": "45"}, args())
|
||||
assert not trusted_track_row(track, {"review_speed_limit_mph": "55"}, args())
|
||||
|
||||
|
||||
def test_trusted_track_accepts_detector_snap_without_classifier_read():
|
||||
track = {
|
||||
"expected_speed_limit_mph": "35",
|
||||
"predicted_speed_limit_mph": "",
|
||||
"read_confidence": "",
|
||||
"detector_confidence": "0.7",
|
||||
"area_ratio_to_anchor": "1.0",
|
||||
"rank": "4",
|
||||
}
|
||||
assert trusted_track_row(track, {"review_speed_limit_mph": "35"}, args())
|
||||
|
||||
|
||||
def test_trusted_track_rejects_low_growth_and_rank():
|
||||
track = {
|
||||
"expected_speed_limit_mph": "35",
|
||||
"predicted_speed_limit_mph": "35",
|
||||
"read_confidence": "0.99",
|
||||
"detector_confidence": "0.7",
|
||||
"area_ratio_to_anchor": "0.9",
|
||||
"rank": "5",
|
||||
}
|
||||
assert not trusted_track_row(track, {"review_speed_limit_mph": "35"}, args())
|
||||
|
||||
|
||||
def test_trusted_track_rejects_tracks_from_redrawn_anchor():
|
||||
track = {
|
||||
"expected_speed_limit_mph": "35",
|
||||
"predicted_speed_limit_mph": "35",
|
||||
"read_confidence": "0.99",
|
||||
"detector_confidence": "0.7",
|
||||
"area_ratio_to_anchor": "1.2",
|
||||
"rank": "1",
|
||||
}
|
||||
review = {
|
||||
"review_speed_limit_mph": "35",
|
||||
"bbox": "10,10,40,50",
|
||||
"review_bbox": "12,12,35,45",
|
||||
}
|
||||
assert not trusted_track_row(track, review, args())
|
||||
Binary file not shown.
Reference in New Issue
Block a user