diff --git a/scripts/speed_limit_vision/build_track_classifier_dataset.py b/scripts/speed_limit_vision/build_track_classifier_dataset.py index d3f944a6c..2db35b0e7 100644 --- a/scripts/speed_limit_vision/build_track_classifier_dataset.py +++ b/scripts/speed_limit_vision/build_track_classifier_dataset.py @@ -15,8 +15,15 @@ import cv2 from starpilot.system.speed_limit_vision import DETECTOR_CLASSIFIER_EXPANSIONS +if __package__ in (None, ""): + import sys + sys.path.insert(0, str(Path(__file__).resolve().parent)) + from common import SUPPORTED_SPEED_VALUES # type: ignore # noqa: TID251 +else: + from .common import SUPPORTED_SPEED_VALUES -SPEED_VALUES = frozenset((15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75)) + +SPEED_VALUES = frozenset(SUPPORTED_SPEED_VALUES) def parse_args() -> argparse.Namespace: diff --git a/scripts/speed_limit_vision/common.py b/scripts/speed_limit_vision/common.py index 23f0cb872..db93bad4e 100644 --- a/scripts/speed_limit_vision/common.py +++ b/scripts/speed_limit_vision/common.py @@ -23,6 +23,11 @@ DETECTOR_CLASS_NAMES = ( "school_zone_speed_limit", ) DEFAULT_SPEED_VALUES = (15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75) +# Values the review and dataset tooling can accept. Keep DEFAULT_SPEED_VALUES +# aligned with the currently deployed classifier until an expanded model is +# promoted; adding a class changes every output index after it. +SUPPORTED_SPEED_VALUES = (10, *DEFAULT_SPEED_VALUES, 80, 90, 100) +EXTENDED_CLASSIFIER_SPEED_VALUES = tuple(sorted(SUPPORTED_SPEED_VALUES, key=str)) DETECTOR_EXPORT_NAME = "speed_limit_us_detector.onnx" CLASSIFIER_EXPORT_NAME = "speed_limit_us_value_classifier.onnx" diff --git a/scripts/speed_limit_vision/generate_synthetic_us_speed_limits.py b/scripts/speed_limit_vision/generate_synthetic_us_speed_limits.py index f11a18fbc..3c3733ca2 100644 --- a/scripts/speed_limit_vision/generate_synthetic_us_speed_limits.py +++ b/scripts/speed_limit_vision/generate_synthetic_us_speed_limits.py @@ -2,7 +2,6 @@ from __future__ import annotations import argparse -import math import random from dataclasses import dataclass @@ -16,7 +15,7 @@ from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont if __package__ in (None, ""): import sys sys.path.insert(0, str(Path(__file__).resolve().parent)) - from common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore + from common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore # noqa: TID251 else: from .common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace @@ -100,7 +99,6 @@ def render_regulatory_sign(speed_value: int, school_zone: bool, seed: int) -> Im def render_advisory_sign(speed_value: int, seed: int) -> Image.Image: rng = random.Random(seed) size = rng.randint(240, 320) - image = Image.new("RGBA", (size, size), (255, 255, 255, 0)) base = Image.new("RGBA", (size, size), (255, 255, 255, 0)) draw = ImageDraw.Draw(base) @@ -234,6 +232,7 @@ def main(): parser.add_argument("--train-count", type=int, default=9000, help="Number of synthetic training detector images.") parser.add_argument("--val-count", type=int, default=1200, help="Number of synthetic validation detector images.") parser.add_argument("--negative-ratio", type=float, default=0.18, help="Share of detector images with no sign.") + parser.add_argument("--speed-values", nargs="+", type=int, default=list(DEFAULT_SPEED_VALUES), help="Posted values to synthesize.") parser.add_argument("--seed", type=int, default=20260330, help="Random seed.") args = parser.parse_args() @@ -246,7 +245,7 @@ def main(): detector_image_dir = workspace / "detector" / "images" detector_label_dir = workspace / "detector" / "labels" classifier_dir = workspace / "classifier" - speed_values = tuple(DEFAULT_SPEED_VALUES) + speed_values = tuple(dict.fromkeys(args.speed_values)) rng = random.Random(args.seed) for split, count in (("train", max(args.train_count, 0)), ("val", max(args.val_count, 0))): diff --git a/scripts/speed_limit_vision/generate_value_roi_classifier_dataset.py b/scripts/speed_limit_vision/generate_value_roi_classifier_dataset.py index 98bc26d33..effdbd273 100644 --- a/scripts/speed_limit_vision/generate_value_roi_classifier_dataset.py +++ b/scripts/speed_limit_vision/generate_value_roi_classifier_dataset.py @@ -13,7 +13,7 @@ import numpy as np if __package__ in (None, ""): import sys sys.path.insert(0, str(Path(__file__).resolve().parent)) - from common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore + from common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore # noqa: TID251 from generate_synthetic_us_speed_limits import KNOWN_REAL_CROPS, augment_sign, render_regulatory_sign # type: ignore else: from .common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace @@ -146,6 +146,7 @@ def main(): parser.add_argument("--train-per-class", type=int, default=1800, help="Synthetic training samples per value.") parser.add_argument("--val-per-class", type=int, default=260, help="Synthetic validation samples per value.") parser.add_argument("--real-augmentations", type=int, default=28, help="Augmented mask samples to create per known real crop.") + parser.add_argument("--speed-values", nargs="+", type=int, default=list(DEFAULT_SPEED_VALUES), help="Posted values to synthesize.") parser.add_argument("--seed", type=int, default=20260330, help="Random seed.") args = parser.parse_args() @@ -157,7 +158,7 @@ def main(): ensure_dir(classifier_dir / "val") rng = random.Random(args.seed) - speed_values = tuple(DEFAULT_SPEED_VALUES) + speed_values = tuple(dict.fromkeys(args.speed_values)) for split, per_class in (("train", max(args.train_per_class, 0)), ("val", max(args.val_per_class, 0))): for speed_value in speed_values: diff --git a/scripts/speed_limit_vision/import_manifest_classifier_masks.py b/scripts/speed_limit_vision/import_manifest_classifier_masks.py index 3e6a55937..6ec13138d 100644 --- a/scripts/speed_limit_vision/import_manifest_classifier_masks.py +++ b/scripts/speed_limit_vision/import_manifest_classifier_masks.py @@ -15,11 +15,11 @@ if __package__ in (None, ""): import sys sys.path.insert(0, str(Path(__file__).resolve().parent)) from build_value_dataset import crop_box, parse_yolo_labels # type: ignore - from common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore + from common import DEFAULT_WORKSPACE, SUPPORTED_SPEED_VALUES, ensure_dir, resolve_workspace # type: ignore # noqa: TID251 from generate_value_roi_classifier_dataset import augment_mask, extract_value_mask # type: ignore else: from .build_value_dataset import crop_box, parse_yolo_labels - from .common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace + from .common import DEFAULT_WORKSPACE, SUPPORTED_SPEED_VALUES, ensure_dir, resolve_workspace from .generate_value_roi_classifier_dataset import augment_mask, extract_value_mask @@ -60,7 +60,7 @@ def parse_speed_from_read(text: str) -> int: if not match: return 0 value = int(match.group(1)) - return value if value in DEFAULT_SPEED_VALUES else 0 + return value if value in SUPPORTED_SPEED_VALUES else 0 def row_speed(row: dict[str, str]) -> int: @@ -68,7 +68,7 @@ def row_speed(row: dict[str, str]) -> int: text = (row.get(field) or "").strip() if text.isdigit(): value = int(text) - if value in DEFAULT_SPEED_VALUES: + if value in SUPPORTED_SPEED_VALUES: return value for field in ("full_detection", "model_read", "ocr_read"): value = parse_speed_from_read(row.get(field, "")) @@ -235,12 +235,12 @@ def main() -> int: if args.max_rows > 0 and attempted >= args.max_rows: break - print( - "Imported manifest classifier masks: " - f"attempted={attempted} imported={imported} written={written} " - f"skipped_no_speed={skipped_no_speed} skipped_no_crop={skipped_no_crop} skipped_no_mask={skipped_no_mask} " - f"skipped_write_failed={skipped_write_failed}" - ) + print("".join(( + "Imported manifest classifier masks: ", + f"attempted={attempted} imported={imported} written={written} ", + f"skipped_no_speed={skipped_no_speed} skipped_no_crop={skipped_no_crop} skipped_no_mask={skipped_no_mask} ", + f"skipped_write_failed={skipped_write_failed}", + ))) return 0 diff --git a/scripts/speed_limit_vision/import_manual_review_queue.py b/scripts/speed_limit_vision/import_manual_review_queue.py index a7d7002a5..71e86b2ba 100644 --- a/scripts/speed_limit_vision/import_manual_review_queue.py +++ b/scripts/speed_limit_vision/import_manual_review_queue.py @@ -14,9 +14,9 @@ import cv2 if __package__ in (None, ""): import sys sys.path.insert(0, str(Path(__file__).resolve().parent)) - from common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace # type: ignore # noqa: TID251 + from common import DEFAULT_WORKSPACE, SUPPORTED_SPEED_VALUES, ensure_dir, resolve_workspace # type: ignore # noqa: TID251 else: - from .common import DEFAULT_SPEED_VALUES, DEFAULT_WORKSPACE, ensure_dir, resolve_workspace + from .common import DEFAULT_WORKSPACE, SUPPORTED_SPEED_VALUES, ensure_dir, resolve_workspace CLASSIFIER_FIELDNAMES = [ @@ -149,7 +149,7 @@ def parse_speed(text: str) -> int: value = int(float(text)) except ValueError: return 0 - return value if value in DEFAULT_SPEED_VALUES else 0 + return value if value in SUPPORTED_SPEED_VALUES else 0 def parse_bbox(text: str) -> tuple[int, int, int, int] | None: diff --git a/scripts/speed_limit_vision/mine_connect_route_bookmarks.py b/scripts/speed_limit_vision/mine_connect_route_bookmarks.py index e0027a1e3..66a2c6603 100644 --- a/scripts/speed_limit_vision/mine_connect_route_bookmarks.py +++ b/scripts/speed_limit_vision/mine_connect_route_bookmarks.py @@ -37,6 +37,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("routes", nargs="+", help="Route ids like 'dongle/logid'.") parser.add_argument("--workspace", type=Path, default=DEFAULT_WORKSPACE, help="Training workspace root.") parser.add_argument("--clip-root", type=Path, default=preferred_clip_root(), help="Downloaded route clip root.") + parser.add_argument("--output-dir", type=Path, help="Output directory. Defaults to /review/connect_route_bookmarks.") parser.add_argument("--models-dir", type=Path, help="Optional directory containing speed_limit_us_detector.onnx and speed_limit_us_value_classifier.onnx.") parser.add_argument("--lead-in", type=float, default=7.0, help="Seconds before each bookmark to sample into review sheets.") parser.add_argument("--sample-every", type=float, default=0.5, help="Seconds between sampled lead-in frames.") @@ -154,7 +155,7 @@ def main() -> int: args = parse_args() workspace = resolve_workspace(args.workspace) clip_root = args.clip_root.expanduser().resolve() - review_root = ensure_dir(workspace / "review" / "connect_route_bookmarks") + review_root = ensure_dir(args.output_dir.expanduser().resolve()) if args.output_dir else ensure_dir(workspace / "review" / "connect_route_bookmarks") frame_dir = ensure_dir(review_root / "frames") crop_dir = ensure_dir(review_root / "crops") contact_sheet_dir = ensure_dir(review_root / "contact_sheets") diff --git a/scripts/speed_limit_vision/select_manual_review_queue.py b/scripts/speed_limit_vision/select_manual_review_queue.py index 95cf496c3..d459329f3 100644 --- a/scripts/speed_limit_vision/select_manual_review_queue.py +++ b/scripts/speed_limit_vision/select_manual_review_queue.py @@ -9,7 +9,7 @@ from collections import Counter, defaultdict, deque from pathlib import Path -PRIORITY_SPEED_ORDER = (60, 65, 55, 50, 45, 40, 35, 30, 25, 20, 70, 15, 75) +PRIORITY_SPEED_ORDER = (60, 65, 55, 50, 45, 40, 35, 30, 25, 20, 70, 75, 80, 90, 100, 15, 10) COMPARISON_PRIORITY_BONUS = { "value_changed": 4.0, "gained_read": 3.0, diff --git a/scripts/speed_limit_vision/serve_manual_review_queue.py b/scripts/speed_limit_vision/serve_manual_review_queue.py index bb882c0ea..d1efeb2c5 100644 --- a/scripts/speed_limit_vision/serve_manual_review_queue.py +++ b/scripts/speed_limit_vision/serve_manual_review_queue.py @@ -130,7 +130,7 @@ HTML = r"""