feat(vision): vehicle stage, phase A — YOLOX-S (Apache-2.0 ONNX) beside the plate recognizer
Fills /analyze vehicle.body_type + confidence (car / motorcycle / bus / truck from COCO, mapped to the shared vocabulary) for the Car Wash desk's category suggestion (venue-modules.md §Vehicle category from vision). Advisory: the operator decides, a confident downgrade is flagged, nothing is gated on it. - vision_service/vehicle.py: pure numpy/cv2 letterbox (pad 114, raw BGR), stride-grid decode, class-agnostic NMS, one vehicle per frame (the box holding the plate's centre, else the largest); YoloxVehicleDetector on onnxruntime CPU, 2 intra-op threads. - recognizer.py: WithVehicle composes the stage over any plate recognizer (stub included); a failing stage yields vehicle=null + a "vehicle: …" note in /health.detail — never costs the plate read. model_version reads "<plate>+yolox:yolox_s.onnx@640". - settings: VISION_VEHICLE_MODEL_PATH (unset = off), _INPUT_SIZE (640), _MIN_CONFIDENCE (0.4, the detector's floor; the flag threshold is site config). - Dockerfile bakes yolox_s.onnx (best-effort curl at build; no network → stage off) and sets the path; compose forwards it (empty = off); .env.example documents it. - Measured on four real dev entry frames (DS-2CD1047G3H, 2560×1440): car at 0.83–0.88 in ~240–330 ms; empty lane with a person → none. - tests/test_vehicle.py: decode/NMS/pick/letterbox on synthetic tensors, the composition, and a missing-model /health. Wiki: opencv-anpr-service, venue-modules, log. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
@@ -19,6 +19,7 @@ from typing import Protocol
|
||||
|
||||
from .schemas import AnalyzeResponse, BBox, PlateResult
|
||||
from .settings import Settings
|
||||
from .vehicle import VehicleDetector, YoloxVehicleDetector
|
||||
|
||||
|
||||
class Recognizer(Protocol):
|
||||
@@ -165,10 +166,59 @@ class FastAlprRecognizer:
|
||||
)
|
||||
|
||||
|
||||
class WithVehicle:
|
||||
"""Composition: any plate recognizer + the vehicle stage. Runs the plate stage first
|
||||
(its box picks WHICH vehicle), then fills `vehicle`. A failing vehicle stage is
|
||||
logged into `error` and yields null — it must never cost the plate read."""
|
||||
|
||||
def __init__(self, inner: Recognizer, detector: VehicleDetector) -> None:
|
||||
self._inner = inner
|
||||
self._detector = detector
|
||||
self.vehicle_error: str | None = None
|
||||
|
||||
@property
|
||||
def model_version(self) -> str:
|
||||
return f"{self._inner.model_version}+{self._detector.model_version}"
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
return bool(self._inner.ready)
|
||||
|
||||
@property
|
||||
def error(self) -> str | None:
|
||||
inner = getattr(self._inner, "error", None)
|
||||
det = getattr(self._detector, "error", None) or self.vehicle_error
|
||||
parts = [p for p in (inner, f"vehicle: {det}" if det else None) if p]
|
||||
return "; ".join(parts) if parts else None
|
||||
|
||||
def analyze(self, image_bytes: bytes) -> AnalyzeResponse:
|
||||
started = time.perf_counter()
|
||||
res = self._inner.analyze(image_bytes)
|
||||
try:
|
||||
vehicle = self._detector.detect(image_bytes, res.plate.bbox if res.plate else None)
|
||||
except Exception as exc: # noqa: BLE001 - advisory stage, never fatal
|
||||
self.vehicle_error = f"{type(exc).__name__}: {exc}"
|
||||
vehicle = None
|
||||
took_ms = (time.perf_counter() - started) * 1000.0
|
||||
return res.model_copy(
|
||||
update={"vehicle": vehicle, "model_version": self.model_version, "took_ms": took_ms}
|
||||
)
|
||||
|
||||
|
||||
def build_recognizer(settings: Settings) -> Recognizer:
|
||||
"""Factory: pick the recognizer from settings. Falls back to the stub if the real
|
||||
one can't load, so the service always comes up (with ready=False surfaced)."""
|
||||
one can't load, so the service always comes up (with ready=False surfaced). The
|
||||
vehicle stage wraps whichever recognizer runs when a model path is configured."""
|
||||
rec: Recognizer
|
||||
if settings.recognizer == "fast_alpr":
|
||||
rec = FastAlprRecognizer(settings)
|
||||
return rec
|
||||
return StubRecognizer(settings)
|
||||
else:
|
||||
rec = StubRecognizer(settings)
|
||||
if settings.vehicle_model_path:
|
||||
detector = YoloxVehicleDetector(
|
||||
settings.vehicle_model_path,
|
||||
input_size=settings.vehicle_input_size,
|
||||
min_confidence=settings.vehicle_min_confidence,
|
||||
)
|
||||
return WithVehicle(rec, detector)
|
||||
return rec
|
||||
|
||||
Reference in New Issue
Block a user