feat(vision): add recognize CLI + verify fast-alpr end-to-end
Add a dev CLI (uv run python -m vision_service.cli <image>) that runs a recognizer on an image file and prints the parsed plate(s) + confidence + region — fast feedback with no HTTP. Also a package.json `recognize` script and a vision-recognize entry point. Verified fast-alpr for real: installed the `alpr` extra, downloaded the YOLOv9 + CCT ONNX weights (~11MB, cached offline under ~/.cache), and ran recognition on the project's test image → "5AU5341" at 1.000 confidence, region "Czech Republic", ~40ms on CPU, via both the CLI and POST /analyze. Fixes result parsing against the actual fast-alpr API: ocr.confidence is a LIST of per-character confidences (not a scalar) — reduced to one plate confidence via the MIN (a plate is only as trustworthy as its weakest character); also surface ocr.region. Extracted the per-result mapping into a pure plate_from_alpr_result + _reduce_confidence and unit-tested them (no model weights needed). 7 tests pass; ruff + mypy strict clean; full turbo build/lint/test green. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""Unit tests for fast-alpr result parsing — no model weights required (the objects
|
||||
are duck-typed stand-ins shaped like fast-alpr's ALPRResult)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from vision_service.recognizer import _reduce_confidence, plate_from_alpr_result
|
||||
|
||||
|
||||
def test_reduce_confidence_takes_min_of_list() -> None:
|
||||
# The weakest character governs trust in the whole plate.
|
||||
assert _reduce_confidence([0.99, 0.80, 0.95]) == 0.80
|
||||
|
||||
|
||||
def test_reduce_confidence_handles_scalar_and_junk() -> None:
|
||||
assert _reduce_confidence(0.7) == 0.7
|
||||
assert _reduce_confidence(None) == 0.0
|
||||
assert _reduce_confidence([]) == 0.0
|
||||
assert _reduce_confidence("nope") == 0.0
|
||||
|
||||
|
||||
def _fake_result(text: str, conf: list[float], region: str | None = None) -> SimpleNamespace:
|
||||
box = SimpleNamespace(x1=10, y1=20, x2=110, y2=60)
|
||||
return SimpleNamespace(
|
||||
ocr=SimpleNamespace(text=text, confidence=conf, region=region),
|
||||
detection=SimpleNamespace(bounding_box=box),
|
||||
)
|
||||
|
||||
|
||||
def test_plate_from_result_maps_fields() -> None:
|
||||
plate = plate_from_alpr_result(_fake_result("5AU5341", [0.999, 0.9995, 0.97], "Czech Republic"))
|
||||
assert plate is not None
|
||||
assert plate.text == "5AU5341"
|
||||
assert plate.confidence == 0.97 # min of the per-character list
|
||||
assert plate.region == "Czech Republic"
|
||||
assert plate.bbox is not None
|
||||
assert (plate.bbox.x1, plate.bbox.y1, plate.bbox.x2, plate.bbox.y2) == (10, 20, 110, 60)
|
||||
|
||||
|
||||
def test_plate_from_result_skips_empty_text() -> None:
|
||||
assert plate_from_alpr_result(_fake_result("", [0.9])) is None
|
||||
assert plate_from_alpr_result(SimpleNamespace(ocr=None, detection=None)) is None
|
||||
Reference in New Issue
Block a user