diff --git a/scripts/speed_limit_vision/evaluate_runtime_manifest.py b/scripts/speed_limit_vision/evaluate_runtime_manifest.py index 431ec2141..466ed7b01 100644 --- a/scripts/speed_limit_vision/evaluate_runtime_manifest.py +++ b/scripts/speed_limit_vision/evaluate_runtime_manifest.py @@ -28,12 +28,50 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--detector-min-confidence", type=float, help="Override runtime US detector confidence threshold.") parser.add_argument("--classifier-min-confidence", type=float, help="Override runtime US classifier confidence threshold.") parser.add_argument("--classifier-reject-min-confidence", type=float, help="Override runtime reject-class confidence threshold.") + parser.add_argument( + "--detector-region-mode", + choices=("full", "right_roi", "full_and_right_roi"), + help="Override the detector/classifier region mode used by speed_limit_vision.py.", + ) + parser.add_argument("--right-roi-bounds", help="Override the right ROI as left,top,right,bottom ratios, for example 0.45,0,1,0.82.") + parser.add_argument("--right-roi-min-confidence", type=float, help="Override the right ROI detector minimum confidence.") + parser.add_argument("--full-frame-ocr", action="store_true", help="Enable the expensive full-frame OCR fallback during eval.") parser.add_argument("--include-uncertain", action="store_true", help="Include uncertain_positive review rows in positive metrics.") 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 configure_runtime_options(args: argparse.Namespace) -> None: + if args.detector_region_mode: + slv.DETECTOR_CLASSIFIER_REGION_MODE = args.detector_region_mode + + if args.full_frame_ocr: + slv.FULL_FRAME_OCR_FALLBACK_ENABLED = True + + if args.right_roi_bounds: + parts = [float(part.strip()) for part in args.right_roi_bounds.split(",")] + if len(parts) != 4: + raise ValueError("--right-roi-bounds must contain four comma-separated ratios") + left, top, right, bottom = parts + if not (0.0 <= left < right <= 1.0 and 0.0 <= top < bottom <= 1.0): + raise ValueError("--right-roi-bounds must be normalized as 0 <= left < right <= 1 and 0 <= top < bottom <= 1") + + min_confidence = args.right_roi_min_confidence + if min_confidence is None: + min_confidence = float(slv.ROI_WINDOWS[-1]["min_confidence"]) if slv.ROI_WINDOWS else slv.US_DETECTOR_MIN_CONFIDENCE + right_roi = {"bounds": (left, top, right, bottom), "min_confidence": float(min_confidence)} + slv.ROI_WINDOWS = (*slv.ROI_WINDOWS[:-1], right_roi) if slv.ROI_WINDOWS else (right_roi,) + elif args.right_roi_min_confidence is not None: + if not slv.ROI_WINDOWS: + right_roi = {"bounds": (0.72, 0.05, 1.00, 0.82), "min_confidence": float(args.right_roi_min_confidence)} + slv.ROI_WINDOWS = (right_roi,) + else: + right_roi = dict(slv.ROI_WINDOWS[-1]) + right_roi["min_confidence"] = float(args.right_roi_min_confidence) + slv.ROI_WINDOWS = (*slv.ROI_WINDOWS[:-1], right_roi) + + def first_present(row: dict[str, str], keys: tuple[str, ...]) -> str: for key in keys: value = row.get(key, "").strip() @@ -113,6 +151,7 @@ def main() -> int: if args.classifier_reject_min_confidence is not None: slv.US_CLASSIFIER_REJECT_MIN_CONFIDENCE = args.classifier_reject_min_confidence slv.US_REJECT_CLASSIFIER_MIN_CONFIDENCE = args.classifier_reject_min_confidence + configure_runtime_options(args) daemon = slv.SpeedLimitVisionDaemon(use_runtime=False) output_rows: list[dict[str, str]] = [] diff --git a/scripts/speed_limit_vision/replay_route_runtime.py b/scripts/speed_limit_vision/replay_route_runtime.py index f162af0d9..a57a2a80a 100644 --- a/scripts/speed_limit_vision/replay_route_runtime.py +++ b/scripts/speed_limit_vision/replay_route_runtime.py @@ -61,9 +61,11 @@ class QlogRuntimeContext: class RouteReplayDaemon(slv.SpeedLimitVisionDaemon): - def __init__(self, runtime_context: QlogRuntimeContext | None): + def __init__(self, runtime_context: QlogRuntimeContext | None, measured_inference_seconds: float): super().__init__(use_runtime=False) self.runtime_context = runtime_context + self.measured_inference_seconds = max(float(measured_inference_seconds), 0.0) + self.next_available_at = -float("inf") self.now = 0.0 self.sampled_frames = 0 self.inference_frames = 0 @@ -116,16 +118,20 @@ class RouteReplayDaemon(slv.SpeedLimitVisionDaemon): self.sampled_frames += 1 if not self.prepare_tick(now): return + if now < self.next_available_at: + return self.current_frame_bgr = frame_bgr inference_interval = self._inference_interval(now) - if now - self.last_inference_at < inference_interval: + next_due = max(self.next_available_at, self.last_inference_at + inference_interval) + if now < next_due: if self.published_speed_limit_mph > 0 and self._published_detection_stale(now): self._write_debug_event("stale_clear", reason="inference_interval") self._clear_detection() return self.last_inference_at = now + self.next_available_at = now + self.measured_inference_seconds self.inference_frames += 1 detection = self._detect_sign(frame_bgr) if detection is not None: @@ -146,6 +152,15 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--progress", action="store_true", help="Print a one-line progress update after each segment.") parser.add_argument("--fast-seek", action="store_true", help="Use VideoCapture seeks when skipping frames. Faster, but less faithful for HEVC.") parser.add_argument("--qlog-context", action="store_true", help="Replay with logged deviceState/livePose/mapdOut context for closer runtime cadence.") + parser.add_argument("--measured-inference-seconds", type=float, default=0.0, help="Simulate wall-clock time spent inside one runtime inference on the comma.") + parser.add_argument( + "--detector-region-mode", + choices=("full", "right_roi", "full_and_right_roi"), + help="Override the detector/classifier region mode used by speed_limit_vision.py.", + ) + parser.add_argument("--right-roi-bounds", help="Override the right ROI as left,top,right,bottom ratios, for example 0.45,0,1,0.82.") + parser.add_argument("--right-roi-min-confidence", type=float, help="Override the right ROI detector minimum confidence.") + parser.add_argument("--full-frame-ocr", action="store_true", help="Enable the expensive full-frame OCR fallback during replay.") return parser.parse_args() @@ -243,6 +258,36 @@ def configure_models(models_dir: Path) -> None: slv.US_REJECT_CLASSIFIER_MODEL_PATH = models_dir / "speed_limit_us_reject_classifier.onnx" +def configure_runtime_options(args: argparse.Namespace) -> None: + if args.detector_region_mode: + slv.DETECTOR_CLASSIFIER_REGION_MODE = args.detector_region_mode + + if args.full_frame_ocr: + slv.FULL_FRAME_OCR_FALLBACK_ENABLED = True + + if args.right_roi_bounds: + parts = [float(part.strip()) for part in args.right_roi_bounds.split(",")] + if len(parts) != 4: + raise ValueError("--right-roi-bounds must contain four comma-separated ratios") + left, top, right, bottom = parts + if not (0.0 <= left < right <= 1.0 and 0.0 <= top < bottom <= 1.0): + raise ValueError("--right-roi-bounds must be normalized as 0 <= left < right <= 1 and 0 <= top < bottom <= 1") + + min_confidence = args.right_roi_min_confidence + if min_confidence is None: + min_confidence = float(slv.ROI_WINDOWS[-1]["min_confidence"]) if slv.ROI_WINDOWS else slv.US_DETECTOR_MIN_CONFIDENCE + right_roi = {"bounds": (left, top, right, bottom), "min_confidence": float(min_confidence)} + slv.ROI_WINDOWS = (*slv.ROI_WINDOWS[:-1], right_roi) if slv.ROI_WINDOWS else (right_roi,) + elif args.right_roi_min_confidence is not None: + if not slv.ROI_WINDOWS: + right_roi = {"bounds": (0.72, 0.05, 1.00, 0.82), "min_confidence": float(args.right_roi_min_confidence)} + slv.ROI_WINDOWS = (right_roi,) + else: + right_roi = dict(slv.ROI_WINDOWS[-1]) + right_roi["min_confidence"] = float(args.right_roi_min_confidence) + slv.ROI_WINDOWS = (*slv.ROI_WINDOWS[:-1], right_roi) + + def skip_to_frame(capture, frame_index: int, target_index: int, fast_seek: bool) -> int: if target_index <= frame_index: return frame_index @@ -264,8 +309,9 @@ def replay_route( end_s: float | None, progress: bool, fast_seek: bool, + measured_inference_seconds: float, ) -> tuple[RouteSummary, list[dict[str, str]]]: - daemon = RouteReplayDaemon(runtime_context) + daemon = RouteReplayDaemon(runtime_context, measured_inference_seconds) for segment_path in segments: segment = segment_index(segment_path) capture = cv2.VideoCapture(str(segment_path)) @@ -291,8 +337,8 @@ def replay_route( continue inference_interval = daemon._inference_interval(now) - if now - daemon.last_inference_at < inference_interval: - next_due = daemon.last_inference_at + inference_interval + next_due = max(daemon.next_available_at, daemon.last_inference_at + inference_interval) + if now < next_due: target_index = max(frame_index + 1, int(round((next_due - segment_start_s) * fps))) if total_frames > 0: target_index = min(target_index, total_frames) @@ -372,6 +418,7 @@ def publish_speed_changes(events: list[dict[str, str]]) -> list[tuple[float, str def main() -> int: args = parse_args() configure_models(args.models_dir) + configure_runtime_options(args) clip_root = args.clip_root.expanduser().resolve() all_events: list[tuple[str, dict[str, str]]] = [] @@ -390,12 +437,22 @@ def main() -> int: else: runtime_context = build_runtime_context(qlogs) - summary, events = replay_route(log_id, paths, runtime_context, args.start, args.end, args.progress, args.fast_seek) + summary, events = replay_route( + log_id, + paths, + runtime_context, + args.start, + args.end, + args.progress, + args.fast_seek, + args.measured_inference_seconds, + ) all_events.extend((log_id, event) for event in events) print( f"{summary.route}: segments={summary.segments} qlog_context={int(summary.qlog_context)} sampled={summary.sampled_frames} " f"inference={summary.inference_frames} candidate={summary.candidate_events} " - f"publish={summary.publish_events} stale_clear={summary.stale_clear_events} road_change={summary.road_change_events}", + f"publish={summary.publish_events} stale_clear={summary.stale_clear_events} road_change={summary.road_change_events} " + f"measured_inference_s={args.measured_inference_seconds:.3f} region={slv.DETECTOR_CLASSIFIER_REGION_MODE}", flush=True, ) publish_values = [event.get("speedLimitMph") for event in events if event["event"] == "publish"] diff --git a/starpilot/assets/vision_models/speed_limit_us_detector.onnx b/starpilot/assets/vision_models/speed_limit_us_detector.onnx index 42a5bef2a..8ec18b8a6 100755 Binary files a/starpilot/assets/vision_models/speed_limit_us_detector.onnx and b/starpilot/assets/vision_models/speed_limit_us_detector.onnx differ diff --git a/starpilot/system/speed_limit_vision.py b/starpilot/system/speed_limit_vision.py index 121924740..969d4cca3 100644 --- a/starpilot/system/speed_limit_vision.py +++ b/starpilot/system/speed_limit_vision.py @@ -25,6 +25,10 @@ LIVE_POSE_RECOVERY_THROTTLE_SECONDS = 2.0 LIVE_POSE_RECOVERY_INFERENCE_INTERVAL = 1.0 RUNTIME_TELEMETRY_INTERVAL_SECONDS = 2.0 DEBUG_HEARTBEAT_INTERVAL_SECONDS = 30.0 +DEFAULT_DETECTOR_INPUT_SIZE = 640 +DETECTOR_INPUT_SIZE_CANDIDATES = (640, 512, 448, 416, 384, 320) +FULL_FRAME_OCR_FALLBACK_ENABLED = False +DETECTOR_CLASSIFIER_REGION_MODE = "right_roi" # full, right_roi, full_and_right_roi DEVICE_BUSY_AVG_CPU_USAGE_PERCENT = 78.0 DEVICE_BUSY_MAX_CPU_USAGE_PERCENT = 92.0 DEVICE_BUSY_HOT_CORE_COUNT = 4 @@ -66,7 +70,7 @@ ROI_WINDOWS = ( {"bounds": (0.48, 0.00, 0.98, 0.42), "min_confidence": MIN_DETECTION_CONFIDENCE}, {"bounds": (0.52, 0.02, 0.97, 0.58), "min_confidence": 0.22}, {"bounds": (0.62, 0.02, 0.99, 0.68), "min_confidence": 0.18}, - {"bounds": (0.72, 0.05, 1.00, 0.82), "min_confidence": 0.15}, + {"bounds": (0.45, 0.00, 1.00, 0.82), "min_confidence": 0.10}, ) EDGE_MARGIN_RATIO = 0.03 MAX_BOX_AREA_RATIO = 0.22 @@ -98,10 +102,18 @@ REGULATORY_RED_LOW_HUE_MAX = 12 REGULATORY_RED_HIGH_HUE_MIN = 168 REGULATORY_RED_SAT_MIN = 80 REGULATORY_RED_VALUE_MIN = 60 +REGULATORY_GREEN_HUE_MIN = 45 +REGULATORY_GREEN_HUE_MAX = 90 +REGULATORY_BLUE_HUE_MIN = 90 +REGULATORY_BLUE_HUE_MAX = 135 +REGULATORY_COLORED_SAT_MIN = 70 +REGULATORY_COLORED_VALUE_MIN = 70 REGULATORY_MIN_WHITE_RATIO = 0.08 REGULATORY_MIN_DARK_RATIO = 0.01 REGULATORY_MAX_YELLOW_RATIO = 0.12 REGULATORY_MAX_RED_RATIO = 0.10 +REGULATORY_MAX_GREEN_RATIO = 0.35 +REGULATORY_MAX_BLUE_RATIO = 0.35 REGULATORY_MIN_WHITE_COMPONENT_RATIO = 0.012 REGULATORY_MIN_COMPONENT_FILL = 0.36 REGULATORY_MIN_COMPONENT_HEIGHT_RATIO = 0.2 @@ -125,6 +137,7 @@ SPEED_LIMIT_CLASSES = { } VALID_SPEED_LIMITS_MPH = set(range(10, 125, 5)) +MIN_PUBLISHABLE_SPEED_LIMIT_MPH = 20 LEGACY_MODEL_PATH = Path(__file__).resolve().parents[1] / "assets" / "vision_models" / "speed_limit_vision.onnx" US_DETECTOR_MODEL_PATH = Path(__file__).resolve().parents[1] / "assets" / "vision_models" / "speed_limit_us_detector.onnx" US_CLASSIFIER_MODEL_PATH = Path(__file__).resolve().parents[1] / "assets" / "vision_models" / "speed_limit_us_value_classifier.onnx" @@ -248,6 +261,7 @@ class SpeedLimitVisionDaemon: self.net = None self.classifier_net = None self.model_mode = "legacy" + self.detector_input_size = DEFAULT_DETECTOR_INPUT_SIZE self.last_error = "" self.last_inference_at = -float("inf") self.last_detection_at = 0.0 @@ -749,14 +763,37 @@ class SpeedLimitVisionDaemon: self.last_inference_interval_reason = reason return interval + @staticmethod + def _read_onnx_square_input_size(model_path): + try: + import onnx + + model = onnx.load(str(model_path), load_external_data=False) + if not model.graph.input: + return DEFAULT_DETECTOR_INPUT_SIZE + + shape = model.graph.input[0].type.tensor_type.shape.dim + if len(shape) < 4: + return DEFAULT_DETECTOR_INPUT_SIZE + + height = int(shape[2].dim_value) + width = int(shape[3].dim_value) + if height == width and height in DETECTOR_INPUT_SIZE_CANDIDATES: + return height + except Exception: + pass + return DEFAULT_DETECTOR_INPUT_SIZE + def _load_model(self): self.net = None self.classifier_net = None self.reject_classifier_net = None self.model_mode = "legacy" + self.detector_input_size = DEFAULT_DETECTOR_INPUT_SIZE if US_DETECTOR_MODEL_PATH.is_file() and US_CLASSIFIER_MODEL_PATH.is_file(): try: + self.detector_input_size = self._read_onnx_square_input_size(US_DETECTOR_MODEL_PATH) self.net = cv2.dnn.readNetFromONNX(str(US_DETECTOR_MODEL_PATH)) self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) @@ -787,6 +824,7 @@ class SpeedLimitVisionDaemon: return try: + self.detector_input_size = self._read_onnx_square_input_size(LEGACY_MODEL_PATH) self.net = cv2.dnn.readNetFromONNX(str(LEGACY_MODEL_PATH)) self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) @@ -917,23 +955,35 @@ class SpeedLimitVisionDaemon: def _detect_sign(self, frame_bgr): if self.net is None: - return self._detect_sign_from_ocr_candidates(frame_bgr) + if FULL_FRAME_OCR_FALLBACK_ENABLED: + return self._publishable_detection(self._detect_sign_from_ocr_candidates(frame_bgr)) + return None if self.model_mode == "detector_classifier" and self.classifier_net is not None: detector_detection = self._detect_sign_from_detector_classifier(frame_bgr) if detector_detection is not None: - return detector_detection - return self._detect_sign_from_ocr_candidates(frame_bgr) + return self._publishable_detection(detector_detection) + if FULL_FRAME_OCR_FALLBACK_ENABLED: + return self._publishable_detection(self._detect_sign_from_ocr_candidates(frame_bgr)) + return None model_detection = self._detect_sign_from_model_proposals(frame_bgr) if model_detection is not None and model_detection.confidence >= MODEL_DETECTION_SHORT_CIRCUIT_CONFIDENCE: - return model_detection + return self._publishable_detection(model_detection) - ocr_detection = self._detect_sign_from_ocr_candidates(frame_bgr) - if ocr_detection is not None and (model_detection is None or ocr_detection.confidence > model_detection.confidence): - return ocr_detection + if FULL_FRAME_OCR_FALLBACK_ENABLED: + ocr_detection = self._detect_sign_from_ocr_candidates(frame_bgr) + if ocr_detection is not None and (model_detection is None or ocr_detection.confidence > model_detection.confidence): + return self._publishable_detection(ocr_detection) - return model_detection + return self._publishable_detection(model_detection) + + def _publishable_detection(self, detection): + if detection is None: + return None + if detection.speed_limit_mph < MIN_PUBLISHABLE_SPEED_LIMIT_MPH: + return None + return detection def _is_regulatory_speed_sign(self, sign_crop): if sign_crop.size == 0: @@ -962,11 +1012,25 @@ class SpeedLimitVisionDaemon: (saturation >= REGULATORY_RED_SAT_MIN) & (value >= REGULATORY_RED_VALUE_MIN) ).astype(np.uint8) + green_mask = ( + (hue >= REGULATORY_GREEN_HUE_MIN) & + (hue <= REGULATORY_GREEN_HUE_MAX) & + (saturation >= REGULATORY_COLORED_SAT_MIN) & + (value >= REGULATORY_COLORED_VALUE_MIN) + ).astype(np.uint8) + blue_mask = ( + (hue >= REGULATORY_BLUE_HUE_MIN) & + (hue <= REGULATORY_BLUE_HUE_MAX) & + (saturation >= REGULATORY_COLORED_SAT_MIN) & + (value >= REGULATORY_COLORED_VALUE_MIN) + ).astype(np.uint8) white_ratio = float(white_mask.mean()) dark_ratio = float(dark_mask.mean()) yellow_ratio = float(yellow_mask.mean()) red_ratio = float(red_mask.mean()) + green_ratio = float(green_mask.mean()) + blue_ratio = float(blue_mask.mean()) if white_ratio < REGULATORY_MIN_WHITE_RATIO or dark_ratio < REGULATORY_MIN_DARK_RATIO: return False @@ -974,6 +1038,10 @@ class SpeedLimitVisionDaemon: return False if red_ratio > REGULATORY_MAX_RED_RATIO and red_ratio > white_ratio * 0.35: return False + if green_ratio > REGULATORY_MAX_GREEN_RATIO and green_ratio > white_ratio * 0.60: + return False + if blue_ratio > REGULATORY_MAX_BLUE_RATIO and blue_ratio > white_ratio * 0.60: + return False white_binary = (white_mask * 255).astype(np.uint8) contours, _ = cv2.findContours(white_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) @@ -1069,8 +1137,9 @@ class SpeedLimitVisionDaemon: return [] frame_height, frame_width = frame_bgr.shape[:2] - letterboxed, ratio, pad_width, pad_height = self._letterbox(frame_bgr) - blob = cv2.dnn.blobFromImage(letterboxed, scalefactor=1 / 255.0, size=(640, 640), swapRB=True, crop=False) + detector_shape = (self.detector_input_size, self.detector_input_size) + letterboxed, ratio, pad_width, pad_height = self._letterbox(frame_bgr, shape=detector_shape) + blob = cv2.dnn.blobFromImage(letterboxed, scalefactor=1 / 255.0, size=detector_shape, swapRB=True, crop=False) self.net.setInput(blob) predictions = np.squeeze(self.net.forward()) @@ -1118,8 +1187,9 @@ class SpeedLimitVisionDaemon: return [] region_height, region_width = frame_bgr.shape[:2] - letterboxed, ratio, pad_width, pad_height = self._letterbox(frame_bgr) - blob = cv2.dnn.blobFromImage(letterboxed, scalefactor=1 / 255.0, size=(640, 640), swapRB=True, crop=False) + detector_shape = (self.detector_input_size, self.detector_input_size) + letterboxed, ratio, pad_width, pad_height = self._letterbox(frame_bgr, shape=detector_shape) + blob = cv2.dnn.blobFromImage(letterboxed, scalefactor=1 / 255.0, size=detector_shape, swapRB=True, crop=False) self.net.setInput(blob) predictions = np.squeeze(self.net.forward()) @@ -1171,17 +1241,19 @@ class SpeedLimitVisionDaemon: return [] frame_height, frame_width = frame_bgr.shape[:2] - candidates = self._collect_detector_classifier_proposals_from_region( - frame_bgr, - 0, - 0, - frame_width, - frame_height, - US_DETECTOR_MIN_CONFIDENCE, - ) + candidates = [] + if DETECTOR_CLASSIFIER_REGION_MODE in ("full", "full_and_right_roi"): + candidates.extend(self._collect_detector_classifier_proposals_from_region( + frame_bgr, + 0, + 0, + frame_width, + frame_height, + US_DETECTOR_MIN_CONFIDENCE, + )) # A second pass on a focused right-side ROI materially improves small U.S. sign reads. - if ROI_WINDOWS: + if DETECTOR_CLASSIFIER_REGION_MODE in ("right_roi", "full_and_right_roi") and ROI_WINDOWS: left_ratio, top_ratio, right_ratio, bottom_ratio = ROI_WINDOWS[-1]["bounds"] left = int(frame_width * left_ratio) top = int(frame_height * top_ratio) @@ -1431,11 +1503,15 @@ class SpeedLimitVisionDaemon: max(support_count - 1, 0) * DETECTOR_CLASSIFIER_SUPPORT_BONUS, 0.95, ) + selection_score = score + published_score = score if class_id == 2: if speed_limit_mph in SCHOOL_ZONE_SPEED_VALUES: - score = min(score + 0.06, 0.95) + selection_score = min(score + 0.06, 0.95) + published_score = selection_score else: - score = max(score - 0.06, 0.0) + selection_score = max(score - 0.06, 0.0) + published_score = selection_score elif is_small_box: if ( speed_regulatory_support.get(speed_limit_mph, 0) < 1 and @@ -1446,11 +1522,13 @@ class SpeedLimitVisionDaemon: continue if read_confidence < DETECTOR_CLASSIFIER_RESCUE_MIN_CONFIDENCE: continue - score = min(score, DETECTOR_CLASSIFIER_RESCUE_MAX_SCORE) - if score > best_score: - best_score = score - best_detection = Detection(speed_limit_mph, score) - if best_score >= MODEL_DETECTION_SHORT_CIRCUIT_CONFIDENCE: + published_score = min(score, DETECTOR_CLASSIFIER_RESCUE_MAX_SCORE) + if speed_trusted_model_support.get(speed_limit_mph, 0) < DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_SUPPORT: + selection_score = published_score + if selection_score > best_score: + best_score = selection_score + best_detection = Detection(speed_limit_mph, published_score) + if best_detection is not None and best_detection.confidence >= MODEL_DETECTION_SHORT_CIRCUIT_CONFIDENCE: return best_detection return best_detection @@ -1820,6 +1898,8 @@ class SpeedLimitVisionDaemon: "started": started, "startedPrev": self.started_prev, "modelMode": self.model_mode, + "detectorInputSize": self.detector_input_size, + "detectorRegionMode": DETECTOR_CLASSIFIER_REGION_MODE, "stream": self.stream_name, "cameraConnected": camera_connected, "debugSession": self.debug_session_id,