Files
Prabhaav Pillai c532223fa0 Restore V-Adj Spot Monitoring warnings and polish Galaxy settings, cameras, and updates
Bundle the verified VASM model, enable it at startup, and split VASM and vision speed-sign inference across little cores when both are active.
Restore VASM warnings in PiP activation, red outlines, and onroad blind-spot borders, including expanded Android Auto views.
Improve cabin snapshots with longer warm-up, adjusted capture timeouts, a fresh preview frame, and clearer capture buttons and save prompts.
Polish Galaxy settings with dimmed gated controls and icons, clearer vehicle fingerprinting and steering availability descriptions, rounded speed-offset ranges, and immediate refresh after saving.
Improve updater startup messages and preserve update/reconnect screens across service interruptions and restarts.
Add regression tests for the updated behavior.
2026-10-19 23:07:19 -04:00

105 lines
4.7 KiB
Python

"""Exact-asset CPU V-ASM inference from an explicit local NV12 frame."""
import hashlib
import os
from pathlib import Path
import stat
import numpy as np
from openpilot.starpilot.spot_monitor.policy import (
Annotation, MODEL_INPUT_SIZE, class_one_confidence, crop_for_side, polygon_pixels,
)
# Frozen tri-class artifact. Embedded metadata names class 1 as 1_car and
# states AGPL-3.0/Ultralytics; the bundled bytes must match this identity.
MODEL_SIZE_BYTES = 6_166_229
MODEL_SHA256 = "5d20cdbb457ba18db51a537ee2e305bbe442264b1613956068d473e35d15900d"
DEFAULT_MODEL_PATH = Path(__file__).resolve().parents[3] / "starpilot/assets/vision_models/v_asm_model.onnx"
def _verified_model_bytes(path: Path) -> bytes:
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(fd, "rb") as source:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_size != MODEL_SIZE_BYTES:
raise ValueError("Unsupported V-ASM model size or file type")
raw = source.read(MODEL_SIZE_BYTES + 1)
if len(raw) != MODEL_SIZE_BYTES or hashlib.sha256(raw).hexdigest() != MODEL_SHA256:
raise ValueError("V-ASM model hash changed")
return raw
def prepare_nv12(frame: np.ndarray, *, width: int, height: int, annotation: Annotation, side: str, cv2_module) -> np.ndarray:
crop = crop_for_side(annotation, side, width, height)
if crop is None or type(frame) is not np.ndarray or frame.dtype != np.uint8 or frame.ndim != 2 or \
frame.shape[0] != height * 3 // 2 or frame.shape[1] < width:
raise ValueError("Unsupported NV12 frame or unconfigured polygon")
x, y, w, h = crop.x, crop.y, crop.width, crop.height
nv12 = np.vstack((frame[y:y + h, x:x + w],
frame[height + y // 2:height + (y + h) // 2, x:x + w]))
rgb = cv2_module.cvtColor(nv12, cv2_module.COLOR_YUV2RGB_NV12)
if rgb.shape != (h, w, 3) or rgb.dtype != np.uint8:
raise ValueError("Invalid RGB crop")
mask = np.zeros((h, w), dtype=np.uint8)
points = np.array([(px - x, py - y) for px, py in polygon_pixels(annotation, side, width, height)], dtype=np.int32)
cv2_module.fillPoly(mask, [points], 255)
rgb = cv2_module.bitwise_and(rgb, rgb, mask=mask)
resized = cv2_module.resize(rgb, (crop.model_width, crop.model_height), interpolation=cv2_module.INTER_LINEAR)
if resized.shape != (crop.model_height, crop.model_width, 3) or resized.dtype != np.uint8:
raise ValueError("Invalid resized crop")
square = np.zeros((MODEL_INPUT_SIZE, MODEL_INPUT_SIZE, 3), dtype=np.uint8)
square[crop.pad_top:crop.pad_top + crop.model_height, crop.pad_left:crop.pad_left + crop.model_width] = resized
return np.expand_dims(np.transpose(square.astype(np.float32) / 255.0, (2, 0, 1)), 0)
class VASMInference:
def __init__(self, model_path: Path, *, cv2_module=None):
self.model_path = model_path
self.cv2 = cv2_module
self.net = None
self.loaded_sha256: str | None = None
self.last_error = ""
def load(self) -> bool:
self.net = None
self.loaded_sha256 = None
try:
raw = _verified_model_bytes(self.model_path)
if self.cv2 is None:
import cv2 # optional runtime dependency; no model load on import
self.cv2 = cv2
self.cv2.setNumThreads(1)
net = self.cv2.dnn.readNetFromONNX(raw)
net.setPreferableBackend(self.cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(self.cv2.dnn.DNN_TARGET_CPU)
self.net = net
self.loaded_sha256 = MODEL_SHA256
self.last_error = ""
return True
except Exception as error: # OpenCV raises its own extension exception type.
self.last_error = f"V-ASM model unavailable: {error}"
return False
def infer_nv12(self, frame: np.ndarray, *, width: int, height: int, annotation: Annotation, side: str) -> float | None:
if self.net is None:
return None
try:
# The network was constructed from verified bytes, not a pathname.
# Later file changes cannot alter this loaded network; reloading
# requires a new exact-hash verification in load().
blob = prepare_nv12(frame, width=width, height=height, annotation=annotation, side=side, cv2_module=self.cv2)
self.net.setInput(blob)
output = np.asarray(self.net.forward())
if output.shape != (1, 3) or not np.isfinite(output).all():
raise ValueError("Unexpected tri-class V-ASM output")
scores = output[0].tolist()
if any(not 0.0 <= score <= 1.0 for score in scores):
raise ValueError("Invalid V-ASM class scores")
return class_one_confidence(scores)
except Exception as error: # OpenCV raises its own extension exception type.
self.net = None
self.last_error = f"V-ASM inference unavailable: {error}"
return None