mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
c532223fa0
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.
105 lines
4.7 KiB
Python
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
|