mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-17 07:12:08 +08:00
distilled-moonstone v4
This commit is contained in:
@@ -94,6 +94,9 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
if candidate == CAR.PORSCHE_MACAN_MK1:
|
||||
ret.steerActuatorDelay = 0.07
|
||||
elif candidate == CAR.VOLKSWAGEN_TAOS_MK1:
|
||||
# Logged Taos braking response aligns about 0.1 s later than the MQB default.
|
||||
ret.longitudinalActuatorDelay = 0.25
|
||||
|
||||
ret.pcmCruise = not ret.openpilotLongitudinalControl
|
||||
ret.stopAccel = -0.55
|
||||
|
||||
@@ -2,6 +2,7 @@ import random
|
||||
import re
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.volkswagen.interface import CarInterface
|
||||
from opendbc.car.volkswagen.values import CAR, FW_QUERY_CONFIG, WMI
|
||||
from opendbc.car.volkswagen.fingerprints import FW_VERSIONS
|
||||
|
||||
@@ -13,6 +14,13 @@ SPARE_PART_FW_PATTERN = re.compile(b'\xf1\x87(?P<gateway>[0-9][0-9A-Z]{2})(?P<un
|
||||
|
||||
|
||||
class TestVolkswagenPlatformConfigs:
|
||||
def test_taos_longitudinal_actuator_delay(self):
|
||||
taos_cp = CarInterface.get_non_essential_params(CAR.VOLKSWAGEN_TAOS_MK1)
|
||||
golf_cp = CarInterface.get_non_essential_params(CAR.VOLKSWAGEN_GOLF_MK7)
|
||||
|
||||
assert abs(taos_cp.longitudinalActuatorDelay - 0.25) < 1e-6
|
||||
assert abs(golf_cp.longitudinalActuatorDelay - 0.15) < 1e-6
|
||||
|
||||
def test_spare_part_fw_pattern(self, subtests):
|
||||
# Relied on for determining if a FW is likely VW
|
||||
for platform, ecus in FW_VERSIONS.items():
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from build_manual_review_queue import FIELDNAMES # type: ignore
|
||||
from common import ensure_dir # type: ignore # noqa: TID251
|
||||
from serve_manual_review_queue import LABEL_FIELDNAMES # type: ignore
|
||||
else:
|
||||
from .build_manual_review_queue import FIELDNAMES
|
||||
from .common import ensure_dir
|
||||
from .serve_manual_review_queue import LABEL_FIELDNAMES
|
||||
|
||||
|
||||
FRAME_KEY_RE = re.compile(r"^(?P<key>.+_bookmark_\d+_rank_\d+)$")
|
||||
DETECTOR_CLASSES = {
|
||||
"0": "regulatory_speed_limit",
|
||||
"1": "advisory_speed_limit",
|
||||
"2": "school_zone_speed_limit",
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Convert inspected bookmark localizations into the standard manual-review format.")
|
||||
parser.add_argument("--localized-manifest", type=Path, required=True, help="localized_bookmarks.csv to convert.")
|
||||
parser.add_argument("--annotations", type=Path, required=True, help="CSV containing one reviewed row per localized record_key.")
|
||||
parser.add_argument("--output-dir", type=Path, required=True, help="Directory for queue, labels, and conversion summary.")
|
||||
parser.add_argument("--route", action="append", default=[], help="Optional log id or dongle/log id to include. Repeatable.")
|
||||
parser.add_argument("--allow-unreviewed", action="store_true", help="Allow localized rows without a matching annotation.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_csv(path: Path) -> list[dict[str, str]]:
|
||||
with path.open("r", encoding="utf-8", newline="") as handle:
|
||||
return list(csv.DictReader(handle))
|
||||
|
||||
|
||||
def write_csv(path: Path, fieldnames: list[str], rows: list[dict[str, str]]) -> None:
|
||||
ensure_dir(path.parent)
|
||||
with path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="ignore")
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def record_key(row: dict[str, str]) -> str:
|
||||
match = FRAME_KEY_RE.match(Path(row.get("frame_path", "")).stem)
|
||||
if match is None:
|
||||
raise ValueError(f"Cannot derive record key from frame path: {row.get('frame_path', '')}")
|
||||
return match.group("key")
|
||||
|
||||
|
||||
def route_identity(row: dict[str, str]) -> tuple[str, str, str]:
|
||||
session_id = row.get("session_id", "")
|
||||
prefix = "connect_"
|
||||
if not session_id.startswith(prefix):
|
||||
raise ValueError(f"Unsupported bookmark session id: {session_id}")
|
||||
identity = session_id[len(prefix):]
|
||||
dongle_id, separator, log_id = identity.partition("_")
|
||||
if not separator or not dongle_id or not log_id:
|
||||
raise ValueError(f"Cannot parse route identity from session id: {session_id}")
|
||||
return f"{dongle_id}/{log_id}", dongle_id, log_id
|
||||
|
||||
|
||||
def source_position(row: dict[str, str]) -> tuple[str, str]:
|
||||
if row.get("source_segment", "") and row.get("source_time_s", ""):
|
||||
return row["source_segment"], row["source_time_s"]
|
||||
|
||||
bookmark_segment = int(row["segment"])
|
||||
relative_time_s = float(row["relative_time_s"])
|
||||
if relative_time_s < 0.0:
|
||||
return str(bookmark_segment - 1), f"{relative_time_s + 60.0:.3f}"
|
||||
return str(bookmark_segment), f"{relative_time_s:.3f}"
|
||||
|
||||
|
||||
def parse_read(text: str) -> tuple[str, str]:
|
||||
speed, separator, confidence = (text or "").partition("@")
|
||||
return (speed, confidence) if separator else ("", "")
|
||||
|
||||
|
||||
def queue_row(row: dict[str, str]) -> dict[str, str]:
|
||||
key = record_key(row)
|
||||
route, dongle_id, log_id = route_identity(row)
|
||||
segment, frame_time_s = source_position(row)
|
||||
candidate_speed, candidate_confidence = parse_read(row.get("model_read", ""))
|
||||
detector_class = DETECTOR_CLASSES.get(row.get("class_id", ""), "regulatory_speed_limit")
|
||||
read_sources = "model"
|
||||
if row.get("full_detection", ""):
|
||||
read_sources += ";full_detection"
|
||||
|
||||
item = dict.fromkeys(FIELDNAMES, "")
|
||||
item.update({
|
||||
"record_key": key,
|
||||
"mining_fingerprint": "localized_bookmark_review_v1",
|
||||
"route": route,
|
||||
"dongle_id": dongle_id,
|
||||
"log_id": log_id,
|
||||
"segment": segment,
|
||||
"frame_time_s": frame_time_s,
|
||||
"frame_path": row.get("frame_path", ""),
|
||||
"crop_path": row.get("crop_path", ""),
|
||||
"source_video_path": row.get("source_video_path", ""),
|
||||
"bbox": row.get("box", ""),
|
||||
"crop_bbox": row.get("box", ""),
|
||||
"class_id": row.get("class_id", ""),
|
||||
"detector_class": detector_class,
|
||||
"proposal_confidence": row.get("proposal_confidence", ""),
|
||||
"candidate_speed_limit_mph": candidate_speed,
|
||||
"candidate_confidence": candidate_confidence,
|
||||
"model_read": row.get("model_read", ""),
|
||||
"ocr_read": row.get("ocr_read", ""),
|
||||
"full_detection": row.get("full_detection", ""),
|
||||
"read_sources": read_sources,
|
||||
"read_support_count": "1",
|
||||
"is_regulatory": row.get("is_regulatory", ""),
|
||||
"review_priority": row.get("score", ""),
|
||||
"review_reasons": "route_bookmark;corrected_source_timing",
|
||||
})
|
||||
return item
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
localized_path = args.localized_manifest.expanduser().resolve()
|
||||
annotations_path = args.annotations.expanduser().resolve()
|
||||
output_dir = ensure_dir(args.output_dir.expanduser().resolve())
|
||||
|
||||
annotations = read_csv(annotations_path)
|
||||
annotations_by_key = {row["record_key"]: row for row in annotations if row.get("record_key")}
|
||||
if len(annotations_by_key) != len(annotations):
|
||||
raise ValueError("Annotations contain an empty or duplicate record_key")
|
||||
|
||||
selected_routes = set(args.route)
|
||||
queue_rows: list[dict[str, str]] = []
|
||||
label_rows: list[dict[str, str]] = []
|
||||
localized_keys: set[str] = set()
|
||||
missing_annotations: list[str] = []
|
||||
for localized_row in read_csv(localized_path):
|
||||
route, _, log_id = route_identity(localized_row)
|
||||
if selected_routes and route not in selected_routes and log_id not in selected_routes:
|
||||
continue
|
||||
key = record_key(localized_row)
|
||||
localized_keys.add(key)
|
||||
annotation = annotations_by_key.get(key)
|
||||
if annotation is None:
|
||||
missing_annotations.append(key)
|
||||
if not args.allow_unreviewed:
|
||||
continue
|
||||
queue_rows.append(queue_row(localized_row))
|
||||
if annotation is not None:
|
||||
label = {field: annotation.get(field, "") for field in LABEL_FIELDNAMES}
|
||||
label["record_key"] = key
|
||||
label_rows.append(label)
|
||||
|
||||
unused_annotations = sorted(set(annotations_by_key) - localized_keys)
|
||||
if missing_annotations and not args.allow_unreviewed:
|
||||
preview = ", ".join(missing_annotations[:5])
|
||||
raise ValueError(f"Missing annotations for {len(missing_annotations)} localized row(s): {preview}")
|
||||
if unused_annotations and not selected_routes:
|
||||
preview = ", ".join(unused_annotations[:5])
|
||||
raise ValueError(f"Annotations reference {len(unused_annotations)} unknown row(s): {preview}")
|
||||
|
||||
queue_path = output_dir / "manual_review_queue.csv"
|
||||
labels_path = output_dir / "manual_review_labels.csv"
|
||||
write_csv(queue_path, FIELDNAMES, queue_rows)
|
||||
write_csv(labels_path, LABEL_FIELDNAMES, label_rows)
|
||||
|
||||
summary = {
|
||||
"localized_manifest": str(localized_path),
|
||||
"annotations": str(annotations_path),
|
||||
"selected_routes": sorted(selected_routes),
|
||||
"queue_rows": len(queue_rows),
|
||||
"label_rows": len(label_rows),
|
||||
"missing_annotations": missing_annotations,
|
||||
"unused_annotations": unused_annotations,
|
||||
"queue": str(queue_path),
|
||||
"labels": str(labels_path),
|
||||
}
|
||||
summary_path = output_dir / "localized_review_conversion_summary.json"
|
||||
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(f"Wrote {len(queue_rows)} queue row(s) and {len(label_rows)} label row(s) to {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -17,7 +17,7 @@ import starpilot.system.speed_limit_vision as slv
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import ensure_dir, preferred_clip_root, resolve_workspace # type: ignore # noqa: TID251
|
||||
from common import ensure_dir, preferred_clip_root, resolve_workspace, source_video_fps # type: ignore # noqa: TID251
|
||||
from localize_bookmark_signs import configure_models # type: ignore
|
||||
from mine_route_training_samples import ( # type: ignore
|
||||
MapContext,
|
||||
@@ -35,7 +35,7 @@ if __package__ in (None, ""):
|
||||
transition_times,
|
||||
)
|
||||
else:
|
||||
from .common import ensure_dir, preferred_clip_root, resolve_workspace
|
||||
from .common import ensure_dir, preferred_clip_root, resolve_workspace, source_video_fps
|
||||
from .localize_bookmark_signs import configure_models
|
||||
from .mine_route_training_samples import (
|
||||
MapContext,
|
||||
@@ -458,7 +458,7 @@ def mine_route(
|
||||
break
|
||||
contexts = load_segment_map_context(segment.path)
|
||||
capture = cv2.VideoCapture(str(segment.video_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(segment.video_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
frame_count = capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0
|
||||
duration_s = frame_count / fps if frame_count > 0 else 60.0
|
||||
times = sample_times(duration_s, args.sample_every, transition_times(contexts), args.transition_radius, args.transition_step)
|
||||
|
||||
@@ -10,6 +10,9 @@ import shutil
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
VALID_SPEEDS = frozenset((15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75))
|
||||
|
||||
@@ -26,6 +29,13 @@ def parse_args() -> argparse.Namespace:
|
||||
default=[],
|
||||
help="Remove inherited samples whose staged filename contains this corrected record key. Repeat as needed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repeat-positive-record",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="RECORD_KEY=COUNT",
|
||||
help="Stage a reviewed positive COUNT times to give a hard example more training weight.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repeat-reject-record",
|
||||
action="append",
|
||||
@@ -49,6 +59,7 @@ def parse_args() -> argparse.Namespace:
|
||||
default=1.0,
|
||||
help="Deterministic fraction of training advisories staged as reject; validation advisories are always retained.",
|
||||
)
|
||||
parser.add_argument("--input-size", type=int, default=128, help="Square letterbox size used by the runtime classifier.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -106,23 +117,40 @@ def remove_inherited_records(root: Path, record_keys: list[str]) -> int:
|
||||
return removed
|
||||
|
||||
|
||||
def parse_reject_repeat_counts(specs: list[str]) -> dict[str, int]:
|
||||
def parse_record_repeat_counts(specs: list[str], sample_kind: str) -> dict[str, int]:
|
||||
repeat_counts: dict[str, int] = {}
|
||||
for spec in specs:
|
||||
record_key, separator, count_text = spec.rpartition("=")
|
||||
if not separator or not record_key:
|
||||
raise ValueError(f"Invalid --repeat-reject-record value: {spec!r}")
|
||||
raise ValueError(f"Invalid --repeat-{sample_kind}-record value: {spec!r}")
|
||||
try:
|
||||
count = int(count_text)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Invalid reject repeat count: {spec!r}") from exc
|
||||
raise ValueError(f"Invalid {sample_kind} repeat count: {spec!r}") from exc
|
||||
if count < 1:
|
||||
raise ValueError(f"Reject repeat count must be at least 1: {spec!r}")
|
||||
raise ValueError(f"{sample_kind.capitalize()} repeat count must be at least 1: {spec!r}")
|
||||
repeat_counts[record_key] = count
|
||||
return repeat_counts
|
||||
|
||||
|
||||
def stage_crop(source: Path, destination_dir: Path, record_key: str) -> bool:
|
||||
def parse_reject_repeat_counts(specs: list[str]) -> dict[str, int]:
|
||||
return parse_record_repeat_counts(specs, "reject")
|
||||
|
||||
|
||||
def square_resize(image: np.ndarray, size: int, color: tuple[int, int, int] = (114, 114, 114)) -> np.ndarray:
|
||||
image_height, image_width = image.shape[:2]
|
||||
ratio = min(size / max(image_height, 1), size / max(image_width, 1))
|
||||
resized_width = max(int(round(image_width * ratio)), 1)
|
||||
resized_height = max(int(round(image_height * ratio)), 1)
|
||||
resized = cv2.resize(image, (resized_width, resized_height), interpolation=cv2.INTER_LINEAR)
|
||||
canvas = np.full((size, size, image.shape[2]), color, dtype=image.dtype)
|
||||
offset_x = (size - resized_width) // 2
|
||||
offset_y = (size - resized_height) // 2
|
||||
canvas[offset_y:offset_y + resized_height, offset_x:offset_x + resized_width] = resized
|
||||
return canvas
|
||||
|
||||
|
||||
def stage_crop(source: Path, destination_dir: Path, record_key: str, input_size: int) -> bool:
|
||||
if not source.is_file():
|
||||
return False
|
||||
digest = hashlib.sha256(source.read_bytes()).hexdigest()[:16]
|
||||
@@ -131,7 +159,12 @@ def stage_crop(source: Path, destination_dir: Path, record_key: str) -> bool:
|
||||
destination_dir.mkdir(parents=True, exist_ok=True)
|
||||
destination = destination_dir / f"review_{safe_key}_{digest}{suffix}"
|
||||
if not destination.exists():
|
||||
shutil.copyfile(source, destination)
|
||||
image = cv2.imread(str(source))
|
||||
if image is None or image.size == 0:
|
||||
return False
|
||||
normalized = square_resize(image, input_size)
|
||||
if not cv2.imwrite(str(destination), normalized, [cv2.IMWRITE_JPEG_QUALITY, 95]):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -150,6 +183,7 @@ def main() -> int:
|
||||
shutil.copytree(base, output, copy_function=shutil.copyfile)
|
||||
appledouble_removed = remove_appledouble_files(output)
|
||||
inherited_records_removed = remove_inherited_records(output, args.exclude_base_record_key)
|
||||
positive_repeat_counts = parse_record_repeat_counts(args.repeat_positive_record, "positive")
|
||||
reject_repeat_counts = parse_reject_repeat_counts(args.repeat_reject_record)
|
||||
|
||||
positive_counts: Counter[str] = Counter()
|
||||
@@ -162,7 +196,9 @@ def main() -> int:
|
||||
elif args.advisory_as_reject and keep_advisory_reject(row, args.advisory_reject_fraction):
|
||||
split = row.get("split", "")
|
||||
source = Path(row.get("crop_path", "")).expanduser()
|
||||
if split in ("train", "val") and stage_crop(source, output / split / "reject", row.get("record_key", "advisory")):
|
||||
if split in ("train", "val") and stage_crop(
|
||||
source, output / split / "reject", row.get("record_key", "advisory"), args.input_size,
|
||||
):
|
||||
reject_counts[f"advisory_{split}"] += 1
|
||||
else:
|
||||
skipped += 1
|
||||
@@ -172,10 +208,16 @@ def main() -> int:
|
||||
split = row.get("split", "")
|
||||
speed = parse_speed(row.get("speed_limit_mph", ""))
|
||||
source = Path(row.get("crop_path", "")).expanduser()
|
||||
if split not in ("train", "val") or not speed or not stage_crop(source, output / split / str(speed), row.get("record_key", "positive")):
|
||||
record_key = row.get("record_key", "positive")
|
||||
repeat_count = positive_repeat_counts.get(record_key, 1) if split == "train" else 1
|
||||
staged = split in ("train", "val") and bool(speed)
|
||||
for repeat_index in range(repeat_count):
|
||||
staged_key = record_key if repeat_index == 0 else f"{record_key}_repeat_{repeat_index:03d}"
|
||||
staged = staged and stage_crop(source, output / split / str(speed), staged_key, args.input_size)
|
||||
if not staged:
|
||||
skipped += 1
|
||||
continue
|
||||
positive_counts[f"{split}/{speed}"] += 1
|
||||
positive_counts[f"{split}/{speed}"] += repeat_count
|
||||
|
||||
for row in read_rows(args.reject_manifest):
|
||||
split = row.get("split", "")
|
||||
@@ -185,7 +227,7 @@ def main() -> int:
|
||||
staged = split in ("train", "val")
|
||||
for repeat_index in range(repeat_count):
|
||||
staged_key = record_key if repeat_index == 0 else f"{record_key}_repeat_{repeat_index:03d}"
|
||||
staged = staged and stage_crop(source, output / split / "reject", staged_key)
|
||||
staged = staged and stage_crop(source, output / split / "reject", staged_key, args.input_size)
|
||||
if not staged:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
@@ -26,6 +26,14 @@ DEFAULT_SPEED_VALUES = (15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75)
|
||||
|
||||
DETECTOR_EXPORT_NAME = "speed_limit_us_detector.onnx"
|
||||
CLASSIFIER_EXPORT_NAME = "speed_limit_us_value_classifier.onnx"
|
||||
COMMA_ROAD_CAMERA_FPS = 20.0
|
||||
|
||||
|
||||
def source_video_fps(video_path: str | Path, reported_fps: float) -> float:
|
||||
# Raw comma HEVC streams have no timing metadata, so OpenCV invents 25 FPS.
|
||||
if Path(video_path).name == "fcamera.hevc":
|
||||
return COMMA_ROAD_CAMERA_FPS
|
||||
return float(reported_fps) if reported_fps > 0.0 else COMMA_ROAD_CAMERA_FPS
|
||||
|
||||
|
||||
def resolve_workspace(path: str | Path | None) -> Path:
|
||||
|
||||
@@ -13,7 +13,12 @@ import cv2
|
||||
|
||||
import starpilot.system.speed_limit_vision as slv
|
||||
|
||||
from scripts.speed_limit_vision import common
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import common # type: ignore # noqa: TID251
|
||||
else:
|
||||
from . import common
|
||||
|
||||
|
||||
DEFAULT_SESSION_ROOT = Path(".tmp/live_drive_debug")
|
||||
@@ -133,7 +138,7 @@ def locate_window(route: str, event: dict, route_mtimes: dict[str, dict[int, int
|
||||
|
||||
def iter_video_window(path: Path, start_s: float, end_s: float, sample_fps: float | None = None):
|
||||
capture = cv2.VideoCapture(str(path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = common.source_video_fps(path, capture.get(cv2.CAP_PROP_FPS))
|
||||
start_frame = max(int(start_s * fps), 0)
|
||||
end_frame = max(int(end_s * fps), start_frame)
|
||||
frame_step = 1
|
||||
|
||||
@@ -13,10 +13,10 @@ from ultralytics import YOLO
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import DEFAULT_SPEED_VALUES # type: ignore
|
||||
from common import DEFAULT_SPEED_VALUES, source_video_fps # type: ignore # noqa: TID251
|
||||
from generate_value_roi_classifier_dataset import extract_value_mask # type: ignore
|
||||
else:
|
||||
from .common import DEFAULT_SPEED_VALUES
|
||||
from .common import DEFAULT_SPEED_VALUES, source_video_fps
|
||||
from .generate_value_roi_classifier_dataset import extract_value_mask
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def iter_frames(path: Path):
|
||||
return
|
||||
|
||||
cap = cv2.VideoCapture(str(path))
|
||||
fps = cap.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(path, cap.get(cv2.CAP_PROP_FPS))
|
||||
frame_index = 0
|
||||
while True:
|
||||
ok, frame = cap.read()
|
||||
|
||||
@@ -17,10 +17,12 @@ import starpilot.system.speed_limit_vision as slv
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import source_video_fps # type: ignore # noqa: TID251
|
||||
from evaluate_runtime_manifest import expected_value, first_present, is_negative, load_rows # type: ignore
|
||||
from evaluate_reviewed_route_events import load_cases # type: ignore
|
||||
from replay_route_runtime import RouteReplayDaemon # type: ignore
|
||||
else:
|
||||
from .common import source_video_fps
|
||||
from .evaluate_runtime_manifest import expected_value, first_present, is_negative, load_rows
|
||||
from .evaluate_reviewed_route_events import load_cases
|
||||
from .replay_route_runtime import RouteReplayDaemon
|
||||
@@ -175,7 +177,7 @@ def evaluate_manifest(args: argparse.Namespace, detector: DirectValueDetector) -
|
||||
def replay_video_cases(cases, detector: DirectValueDetector, args: argparse.Namespace):
|
||||
daemons = {case.record_key: DirectRouteReplayDaemon(detector, args.measured_inference_seconds) for case in cases}
|
||||
capture = cv2.VideoCapture(str(cases[0].source_video_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(cases[0].source_video_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
windows = {
|
||||
case.record_key: (max(case.frame_time_s - args.window_before, 0.0), case.frame_time_s + args.window_after)
|
||||
for case in cases
|
||||
|
||||
@@ -16,9 +16,11 @@ import starpilot.system.speed_limit_vision as slv
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import source_video_fps # type: ignore # noqa: TID251
|
||||
from import_manual_review_queue import merged_review_rows, parse_speed # type: ignore
|
||||
from replay_route_runtime import RouteReplayDaemon, configure_models # type: ignore
|
||||
else:
|
||||
from .common import source_video_fps
|
||||
from .import_manual_review_queue import merged_review_rows, parse_speed
|
||||
from .replay_route_runtime import RouteReplayDaemon, configure_models
|
||||
|
||||
@@ -164,7 +166,7 @@ def replay_video_cases(cases: list[ReviewedCase], args: argparse.Namespace) -> d
|
||||
daemon.published_speed_limit_mph = args.initial_speed_limit
|
||||
daemon.last_published_support_at = 0.0
|
||||
capture = cv2.VideoCapture(str(cases[0].source_video_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(cases[0].source_video_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
||||
duration_s = frame_count / fps if frame_count > 0 else 60.0
|
||||
windows = {
|
||||
|
||||
@@ -14,12 +14,13 @@ import numpy as np
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import ( # type: ignore
|
||||
from common import ( # type: ignore # noqa: TID251
|
||||
BOOKMARK_LEADIN_MANIFEST_FIELDS,
|
||||
DEFAULT_WORKSPACE,
|
||||
ensure_dir,
|
||||
preferred_clip_root,
|
||||
resolve_workspace,
|
||||
source_video_fps,
|
||||
write_csv_header,
|
||||
)
|
||||
else:
|
||||
@@ -29,6 +30,7 @@ else:
|
||||
ensure_dir,
|
||||
preferred_clip_root,
|
||||
resolve_workspace,
|
||||
source_video_fps,
|
||||
write_csv_header,
|
||||
)
|
||||
|
||||
@@ -64,7 +66,7 @@ def load_existing_rows(manifest_path: Path) -> dict[str, dict[str, str]]:
|
||||
|
||||
def read_frames_at(video_path: Path, target_times_s: list[float]):
|
||||
capture = cv2.VideoCapture(str(video_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(video_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
targets = sorted((max(int(round(target_time_s * fps)), 0), target_time_s) for target_time_s in target_times_s)
|
||||
results = {}
|
||||
frame_index = 0
|
||||
|
||||
@@ -12,9 +12,14 @@ import cv2
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore
|
||||
from common import ( # type: ignore # noqa: TID251
|
||||
DEFAULT_WORKSPACE,
|
||||
ensure_dir,
|
||||
resolve_workspace,
|
||||
source_video_fps,
|
||||
)
|
||||
else:
|
||||
from .common import DEFAULT_WORKSPACE, ensure_dir, resolve_workspace
|
||||
from .common import DEFAULT_WORKSPACE, ensure_dir, resolve_workspace, source_video_fps
|
||||
|
||||
|
||||
LOCALIZED_MANIFEST = Path(".tmp/bookmark_sign_localization/localized_bookmarks.csv")
|
||||
@@ -96,7 +101,7 @@ def expand_bbox(x1: int, y1: int, x2: int, y2: int, image_shape: tuple[int, int,
|
||||
|
||||
def read_frame_at(video_path: Path, target_time_s: float):
|
||||
capture = cv2.VideoCapture(str(video_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(video_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
frame_index = max(int(round(target_time_s * fps)), 0)
|
||||
capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
|
||||
ok, frame_bgr = capture.read()
|
||||
|
||||
@@ -10,7 +10,12 @@ import cv2
|
||||
|
||||
import starpilot.system.speed_limit_vision as slv
|
||||
|
||||
from scripts.speed_limit_vision import common
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import common # type: ignore # noqa: TID251
|
||||
else:
|
||||
from . import common
|
||||
from scripts.speed_limit_vision import evaluate_bookmark_leadins as ebl
|
||||
|
||||
|
||||
@@ -22,7 +27,12 @@ def parse_args():
|
||||
parser.add_argument("--clip-root", type=Path, default=ebl.DEFAULT_CLIP_ROOT, help="Copied route clip root.")
|
||||
parser.add_argument("--qlog-mtimes", type=Path, default=ebl.DEFAULT_QLOG_MTIMES, help="Text file with '<qlog path> <mtime epoch>' lines.")
|
||||
parser.add_argument("--session-root", type=Path, default=ebl.DEFAULT_SESSION_ROOT, help="Directory containing debug session folders.")
|
||||
parser.add_argument("--session-route-map", type=Path, default=common.preferred_session_route_map_path(), help="JSON file mapping debug session ids to route log ids.")
|
||||
parser.add_argument(
|
||||
"--session-route-map",
|
||||
type=Path,
|
||||
default=common.preferred_session_route_map_path(),
|
||||
help="JSON file mapping debug session ids to route log ids.",
|
||||
)
|
||||
parser.add_argument("--models-dir", type=Path, help="Directory containing speed_limit_us_detector.onnx and speed_limit_us_value_classifier.onnx.")
|
||||
parser.add_argument("--search-before", type=float, default=18.0, help="Seconds before the bookmark to scan.")
|
||||
parser.add_argument("--search-after", type=float, default=2.0, help="Seconds after the bookmark to scan.")
|
||||
@@ -48,7 +58,7 @@ def configure_models(models_dir: Path | None):
|
||||
|
||||
def iter_video_samples(clip_path: Path, start_s: float, end_s: float, sample_every: float):
|
||||
capture = cv2.VideoCapture(str(clip_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = common.source_video_fps(clip_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
start_frame = max(int(start_s * fps), 0)
|
||||
end_frame = max(int(end_s * fps), start_frame)
|
||||
|
||||
@@ -216,6 +226,8 @@ def write_manifest(rows: list[dict], path: Path):
|
||||
"route",
|
||||
"segment",
|
||||
"relative_time_s",
|
||||
"source_segment",
|
||||
"source_time_s",
|
||||
"source_video_path",
|
||||
"score",
|
||||
"proposal_confidence",
|
||||
@@ -270,7 +282,9 @@ def main():
|
||||
ranked.append((scored["score"], relative_time_s, source_video_path, source_time_s, frame_bgr, scored))
|
||||
|
||||
ranked.sort(key=lambda item: item[0], reverse=True)
|
||||
for rank_index, (_, relative_time_s, source_video_path, _, frame_bgr, scored) in enumerate(ranked[:max(args.top_k, 1)], start=1):
|
||||
for rank_index, (_, relative_time_s, source_video_path, source_time_s, frame_bgr, scored) in enumerate(
|
||||
ranked[:max(args.top_k, 1)], start=1,
|
||||
):
|
||||
x1, y1, x2, y2 = scored["box"]
|
||||
crop = frame_bgr[y1:y2, x1:x2]
|
||||
|
||||
@@ -293,6 +307,8 @@ def main():
|
||||
"route": route,
|
||||
"segment": window.segment,
|
||||
"relative_time_s": f"{relative_time_s:.3f}",
|
||||
"source_segment": window.segment - int(relative_time_s < 0.0),
|
||||
"source_time_s": f"{source_time_s:.3f}",
|
||||
"source_video_path": str(source_video_path),
|
||||
"score": f"{scored['score']:.4f}",
|
||||
"proposal_confidence": f"{scored['proposal_confidence']:.4f}",
|
||||
|
||||
@@ -16,7 +16,7 @@ import starpilot.system.speed_limit_vision as slv
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import ensure_dir, preferred_clip_root, resolve_workspace # type: ignore
|
||||
from common import ensure_dir, preferred_clip_root, resolve_workspace # type: ignore # noqa: TID251
|
||||
from evaluate_bookmark_leadins import BookmarkWindow # type: ignore
|
||||
from import_bookmark_leadins import extract_window_frames, write_contact_sheet # type: ignore
|
||||
from localize_bookmark_signs import configure_models, iter_context_frames, score_frame # type: ignore
|
||||
@@ -126,6 +126,8 @@ def write_localized_manifest(path: Path, rows: list[dict]) -> None:
|
||||
"route",
|
||||
"segment",
|
||||
"relative_time_s",
|
||||
"source_segment",
|
||||
"source_time_s",
|
||||
"source_video_path",
|
||||
"score",
|
||||
"proposal_confidence",
|
||||
@@ -221,7 +223,7 @@ def main() -> int:
|
||||
write_contact_sheet(contact_sheet_path, contact_sheet_frames, contact_sheet_labels, args.overwrite)
|
||||
|
||||
ranked = []
|
||||
for relative_time_s, source_video_path, _, frame_bgr in iter_context_frames(
|
||||
for relative_time_s, source_video_path, source_time_s, frame_bgr in iter_context_frames(
|
||||
clip_root,
|
||||
window,
|
||||
args.search_before,
|
||||
@@ -231,10 +233,12 @@ def main() -> int:
|
||||
scored = score_frame(daemon, frame_bgr, use_ocr=not args.model_only)
|
||||
if scored is None:
|
||||
continue
|
||||
ranked.append((scored["score"], relative_time_s, source_video_path, frame_bgr, scored))
|
||||
ranked.append((scored["score"], relative_time_s, source_video_path, source_time_s, frame_bgr, scored))
|
||||
|
||||
ranked.sort(key=lambda item: item[0], reverse=True)
|
||||
for rank_index, (_, relative_time_s, source_video_path, frame_bgr, scored) in enumerate(ranked[:max(args.top_k, 1)], start=1):
|
||||
for rank_index, (_, relative_time_s, source_video_path, source_time_s, frame_bgr, scored) in enumerate(
|
||||
ranked[:max(args.top_k, 1)], start=1,
|
||||
):
|
||||
x1, y1, x2, y2 = scored["box"]
|
||||
crop = frame_bgr[y1:y2, x1:x2]
|
||||
frame_name = f"{session_id}_bookmark_{bookmark_number:03d}_rank_{rank_index:02d}.jpg"
|
||||
@@ -253,6 +257,8 @@ def main() -> int:
|
||||
"route": log_id,
|
||||
"segment": window.segment,
|
||||
"relative_time_s": f"{relative_time_s:.3f}",
|
||||
"source_segment": window.segment - int(relative_time_s < 0.0),
|
||||
"source_time_s": f"{source_time_s:.3f}",
|
||||
"source_video_path": str(source_video_path),
|
||||
"score": f"{scored['score']:.4f}",
|
||||
"proposal_confidence": f"{scored['proposal_confidence']:.4f}",
|
||||
|
||||
@@ -18,9 +18,11 @@ import starpilot.system.speed_limit_vision as slv
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import source_video_fps # type: ignore # noqa: TID251
|
||||
from import_manual_review_queue import merged_review_rows, parse_speed # type: ignore
|
||||
from replay_route_runtime import configure_models # type: ignore
|
||||
else:
|
||||
from .common import source_video_fps
|
||||
from .import_manual_review_queue import merged_review_rows, parse_speed
|
||||
from .replay_route_runtime import configure_models
|
||||
|
||||
@@ -372,7 +374,7 @@ def mine_backward_samples(
|
||||
|
||||
def mine_case(case: TrackCase, daemon: slv.SpeedLimitVisionDaemon, args: argparse.Namespace) -> list[TrackSample]:
|
||||
capture = cv2.VideoCapture(str(case.video_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(case.video_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
anchor_frame_index = max(int(round(case.frame_time_s * fps)), 0)
|
||||
before_frame_count = max(int(round(args.window_before * fps)), 0)
|
||||
earlier_frames: deque[tuple[int, np.ndarray]] = deque(maxlen=before_frame_count)
|
||||
|
||||
@@ -20,10 +20,16 @@ import starpilot.system.speed_limit_vision as slv
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import VALUE_LABEL_FIELDS, ensure_dir, preferred_clip_root, resolve_workspace # type: ignore
|
||||
from common import ( # type: ignore # noqa: TID251
|
||||
VALUE_LABEL_FIELDS,
|
||||
ensure_dir,
|
||||
preferred_clip_root,
|
||||
resolve_workspace,
|
||||
source_video_fps,
|
||||
)
|
||||
from localize_bookmark_signs import configure_models, score_frame # type: ignore
|
||||
else:
|
||||
from .common import VALUE_LABEL_FIELDS, ensure_dir, preferred_clip_root, resolve_workspace
|
||||
from .common import VALUE_LABEL_FIELDS, ensure_dir, preferred_clip_root, resolve_workspace, source_video_fps
|
||||
from .localize_bookmark_signs import configure_models, score_frame
|
||||
|
||||
|
||||
@@ -542,7 +548,7 @@ def mine_route(
|
||||
break
|
||||
contexts = load_segment_map_context(segment.path)
|
||||
capture = cv2.VideoCapture(str(segment.video_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(segment.video_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
frame_count = capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0
|
||||
duration_s = frame_count / fps if frame_count > 0 else 60.0
|
||||
times = sample_times(duration_s, args.sample_every, transition_times(contexts), args.transition_radius, args.transition_step)
|
||||
|
||||
@@ -15,6 +15,13 @@ from cereal import log
|
||||
|
||||
import starpilot.system.speed_limit_vision as slv
|
||||
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import source_video_fps # type: ignore # noqa: TID251
|
||||
else:
|
||||
from .common import source_video_fps
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RouteSummary:
|
||||
@@ -419,7 +426,7 @@ def replay_route(
|
||||
for segment_path in segments:
|
||||
segment = segment_index(segment_path)
|
||||
capture = cv2.VideoCapture(str(segment_path))
|
||||
fps = capture.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(segment_path, capture.get(cv2.CAP_PROP_FPS))
|
||||
total_frames = int(capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
||||
segment_start_s = segment * 60.0
|
||||
frame_index = max(int(round(max(start_s - segment_start_s, 0.0) * fps)), 0)
|
||||
|
||||
@@ -10,9 +10,9 @@ import cv2
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore
|
||||
from common import DEFAULT_WORKSPACE, ensure_dir, resolve_workspace, source_video_fps # type: ignore # noqa: TID251
|
||||
else:
|
||||
from .common import DEFAULT_WORKSPACE, ensure_dir, resolve_workspace
|
||||
from .common import DEFAULT_WORKSPACE, ensure_dir, resolve_workspace, source_video_fps
|
||||
|
||||
|
||||
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
|
||||
@@ -49,7 +49,7 @@ def sample_images(source_files: list[Path], output_dir: Path, max_per_file: int)
|
||||
|
||||
def sample_video(video_path: Path, output_dir: Path, seconds_between_frames: float, max_frames: int):
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
fps = cap.get(cv2.CAP_PROP_FPS) or 20.0
|
||||
fps = source_video_fps(video_path, cap.get(cv2.CAP_PROP_FPS))
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
||||
frame_step = max(int(round(seconds_between_frames * fps)), 1)
|
||||
frame_indices = range(0, total_frames if total_frames > 0 else frame_step * max_frames, frame_step)
|
||||
|
||||
@@ -5,7 +5,6 @@ import pytest
|
||||
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)
|
||||
@@ -16,7 +15,9 @@ def load_local_module(name: str):
|
||||
|
||||
|
||||
import_queue = load_local_module("import_manual_review_queue")
|
||||
common = load_local_module("common")
|
||||
build_review_classifier = load_local_module("build_review_classifier_dataset")
|
||||
localized_review = load_local_module("build_localized_bookmark_review_queue")
|
||||
select_queue = load_local_module("select_manual_review_queue")
|
||||
compare_queues = load_local_module("compare_manual_review_queues")
|
||||
rescore_queue = load_local_module("rescore_manual_review_queue")
|
||||
@@ -26,6 +27,22 @@ split_group_key = import_queue.split_group_key
|
||||
select_rows = select_queue.select_rows
|
||||
|
||||
|
||||
def test_raw_comma_camera_uses_real_frame_rate():
|
||||
assert common.source_video_fps(Path("route/fcamera.hevc"), 25.0) == 20.0
|
||||
assert common.source_video_fps(Path("clip.mp4"), 29.97) == 29.97
|
||||
assert common.source_video_fps(Path("clip.mp4"), 0.0) == 20.0
|
||||
|
||||
|
||||
def test_localized_bookmark_source_position_normalizes_previous_segment():
|
||||
previous = {"segment": "26", "relative_time_s": "-18.950"}
|
||||
current = {"segment": "26", "relative_time_s": "12.500"}
|
||||
explicit = {"segment": "26", "relative_time_s": "-18.950", "source_segment": "25", "source_time_s": "41.050"}
|
||||
|
||||
assert localized_review.source_position(previous) == ("25", "41.050")
|
||||
assert localized_review.source_position(current) == ("26", "12.500")
|
||||
assert localized_review.source_position(explicit) == ("25", "41.050")
|
||||
|
||||
|
||||
def review_row(key: str, route: str, speed: int, priority: float) -> dict[str, str]:
|
||||
return {
|
||||
"record_key": key,
|
||||
@@ -253,13 +270,31 @@ def test_corrected_record_removes_inherited_classifier_sample(tmp_path):
|
||||
|
||||
def test_reject_repeat_spec_preserves_record_key_punctuation():
|
||||
counts = build_review_classifier.parse_reject_repeat_counts(["route/sign=track:55=32"])
|
||||
positive_counts = build_review_classifier.parse_record_repeat_counts(["route/sign=track:75=16"], "positive")
|
||||
|
||||
assert counts == {"route/sign=track:55": 32}
|
||||
assert positive_counts == {"route/sign=track:75": 16}
|
||||
|
||||
with pytest.raises(ValueError, match="at least 1"):
|
||||
build_review_classifier.parse_reject_repeat_counts(["bad-record=0"])
|
||||
|
||||
|
||||
def test_review_crop_staging_matches_runtime_letterbox(tmp_path):
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
source = tmp_path / "portrait.jpg"
|
||||
image = np.full((80, 40, 3), 255, dtype=np.uint8)
|
||||
cv2.imwrite(str(source), image)
|
||||
|
||||
assert build_review_classifier.stage_crop(source, tmp_path / "train" / "75", "portrait", 128)
|
||||
staged = cv2.imread(str(next((tmp_path / "train" / "75").iterdir())))
|
||||
|
||||
assert staged is not None and staged.shape == (128, 128, 3)
|
||||
assert staged[:, :20].mean() == pytest.approx(114, abs=2)
|
||||
assert staged[:, 32:96].mean() > 245
|
||||
|
||||
|
||||
def test_conditional_reject_generates_runtime_crop_expansions(tmp_path):
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user