Files
firestar5683 9d298304c3 Vision
Implement vision annotation observations, inference and preferences through a dedicated runtime owner.
2026-10-14 20:02:39 -05:00

170 lines
5.9 KiB
Python

"""Exact-asset CPU wrapper with deterministic NV12/mask and fake-network tests."""
import hashlib
import os
from pathlib import Path
import numpy as np
import pytest
from openpilot.starpilot.spot_monitor import inference
from openpilot.starpilot.spot_monitor.policy import decode_annotation
class FakeNet:
def __init__(self):
self.blob = None
self.output = np.array([[0.05, 0.95, 0.0]], dtype=np.float32)
self.backend = None
self.target = None
def setPreferableBackend(self, backend):
self.backend = backend
def setPreferableTarget(self, target):
self.target = target
def setInput(self, blob):
self.blob = blob
def forward(self):
return self.output
class FakeDnn:
DNN_BACKEND_OPENCV = 7
DNN_TARGET_CPU = 8
def __init__(self, net):
self.net = net
self.loaded = None
def readNetFromONNX(self, raw):
self.loaded = raw
return self.net
class FakeCV2:
COLOR_YUV2RGB_NV12 = 1
INTER_LINEAR = 2
def __init__(self, net):
self.dnn = FakeDnn(net)
self.mask: np.ndarray | None = None
self.threads = None
def setNumThreads(self, count):
self.threads = count
def cvtColor(self, nv12, code):
assert code == self.COLOR_YUV2RGB_NV12
height = nv12.shape[0] * 2 // 3
return np.repeat(nv12[:height, :, None], 3, axis=2)
def fillPoly(self, mask, polygons, value):
points = polygons[0]
for y in range(mask.shape[0]):
for x in range(mask.shape[1]):
# A strict interior raster is enough to expose the wrapper's mask
# application without requiring OpenCV in the host test environment.
inside = False
for a, b in zip(points, np.roll(points, -1, axis=0), strict=True):
if (a[1] > y + 0.5) != (b[1] > y + 0.5):
boundary = a[0] + (y + 0.5 - a[1]) * (b[0] - a[0]) / (b[1] - a[1])
if x + 0.5 < boundary:
inside = not inside
if inside:
mask[y, x] = value
self.mask = mask.copy()
def bitwise_and(self, left, right, *, mask):
return np.where(mask[:, :, None] != 0, left, 0).astype(np.uint8)
def resize(self, image, size, *, interpolation):
assert interpolation == self.INTER_LINEAR
width, height = size
ys = (np.arange(height) * image.shape[0] // height).astype(int)
xs = (np.arange(width) * image.shape[1] // width).astype(int)
return image[np.ix_(ys, xs)]
@pytest.fixture
def tiny_model(tmp_path: Path, monkeypatch):
raw = b"small reviewed ONNX fixture"
monkeypatch.setattr(inference, "MODEL_SIZE_BYTES", len(raw))
monkeypatch.setattr(inference, "MODEL_SHA256", hashlib.sha256(raw).hexdigest())
path = tmp_path / "model.onnx"
path.write_bytes(raw)
return path
def test_exact_asset_cpu_mask_crop_class_one_and_loaded_bytes_pinned(tiny_model, monkeypatch):
net = FakeNet()
cv2 = FakeCV2(net)
model = inference.VASMInference(tiny_model, cv2_module=cv2)
assert model.load()
assert cv2.threads == 1
assert model.loaded_sha256 == inference.MODEL_SHA256
assert cv2.dnn.loaded == tiny_model.read_bytes()
assert (net.backend, net.target) == (cv2.dnn.DNN_BACKEND_OPENCV, cv2.dnn.DNN_TARGET_CPU)
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[4,4],[20,4],[4,20]],"poly_right":[]}')
frame = np.full((48, 32), 128, dtype=np.uint8)
frame[:32, :] = 255
assert model.infer_nv12(frame, width=32, height=32, annotation=annotation, side="left") == pytest.approx(0.95)
assert cv2.mask is not None
assert cv2.mask[2, 2] == 255
assert cv2.mask[-2, -2] == 0
assert net.blob.shape == (1, 3, 352, 352)
assert net.blob.dtype == np.float32
assert net.blob.max() == 1.0 and net.blob.min() == 0.0
tiny_model.write_bytes(b"small tampered ONNX fixture")
def fail_reopen(_):
raise AssertionError("infer reopened model source")
monkeypatch.setattr(inference, "_verified_model_bytes", fail_reopen)
assert model.infer_nv12(frame, width=32, height=32, annotation=annotation, side="left") == pytest.approx(0.95)
assert model.loaded_sha256 == inference.MODEL_SHA256
def test_changed_source_rejected_on_explicit_reload(tiny_model):
model = inference.VASMInference(tiny_model, cv2_module=FakeCV2(FakeNet()))
assert model.load()
tiny_model.write_bytes(b"small tampered ONNX fixture")
assert not model.load()
assert model.net is None and model.loaded_sha256 is None
def test_asset_missing_wrong_size_symlink_and_bad_output_fail_unavailable(tiny_model, tmp_path):
cv2 = FakeCV2(FakeNet())
missing = inference.VASMInference(tmp_path / "missing.onnx", cv2_module=cv2)
assert not missing.load()
alias = tmp_path / "alias.onnx"
alias.symlink_to(tiny_model)
assert not inference.VASMInference(alias, cv2_module=cv2).load()
tiny_model.write_bytes(b"wrong")
assert not inference.VASMInference(tiny_model, cv2_module=cv2).load()
tiny_model.write_bytes(b"small reviewed ONNX fixture")
model = inference.VASMInference(tiny_model, cv2_module=cv2)
assert model.load()
frame = np.zeros((48, 32), dtype=np.uint8)
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[4,4],[20,4],[4,20]],"poly_right":[]}')
cv2.dnn.net.output = np.array([[float("nan"), 0.95, 0.0]], dtype=np.float32)
assert model.infer_nv12(frame, width=32, height=32, annotation=annotation, side="left") is None
assert model.net is None
def test_fifo_asset_refused_without_waiting_for_writer(tmp_path):
fifo = tmp_path / "model.fifo"
os.mkfifo(fifo)
assert not inference.VASMInference(fifo, cv2_module=FakeCV2(FakeNet())).load()
def test_wrong_frame_and_unconfigured_side_fail_without_model_output(tiny_model):
net = FakeNet()
model = inference.VASMInference(tiny_model, cv2_module=FakeCV2(net))
assert model.load()
annotation = decode_annotation(b'{"version":1,"width":32,"height":32,"poly_left":[[4,4],[20,4],[4,20]],"poly_right":[]}')
assert model.infer_nv12(np.zeros((32, 32), dtype=np.uint8), width=32, height=32,
annotation=annotation, side="left") is None
assert net.blob is None