Always query 3 frames per passage with vote + history correction
- Remove early break: always send all 3 top frames to PlateRecognizer (3 calls/passage × ~26 passages/day = ~2340/month, within quota) - Vote across results by alnum form so "GV-665-FJ" and "GV665FJ" count as the same candidate — majority wins over higher confidence - Levenshtein history correction: if the winning plate is 1 char away from a known plate in DB, substitute the known plate (fixes noise like "GV665FJW" → "GV-665-FJ") Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8fb021bc2b
commit
a660af2bd7
+52
-6
@@ -41,6 +41,34 @@ _FR_PLATE_RE = _re.compile(r'^([A-Z]{2})(\d{3})([A-Z]{2})$')
|
|||||||
_ZONE_PATH = "/data/zone.json"
|
_ZONE_PATH = "/data/zone.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _levenshtein(a: str, b: str) -> int:
|
||||||
|
if a == b:
|
||||||
|
return 0
|
||||||
|
m, n = len(a), len(b)
|
||||||
|
if m == 0: return n
|
||||||
|
if n == 0: return m
|
||||||
|
dp = list(range(n + 1))
|
||||||
|
for i in range(1, m + 1):
|
||||||
|
prev, dp[0] = dp[0], i
|
||||||
|
for j in range(1, n + 1):
|
||||||
|
temp = dp[j]
|
||||||
|
dp[j] = prev if a[i-1] == b[j-1] else min(prev, dp[j], dp[j-1]) + 1
|
||||||
|
prev = temp
|
||||||
|
return dp[n]
|
||||||
|
|
||||||
|
|
||||||
|
def _match_history(plate_alnum: str) -> str | None:
|
||||||
|
"""Return a known plate from DB if it's within 1 edit of plate_alnum (alnum only)."""
|
||||||
|
from database import get_plate_stats
|
||||||
|
best_dist, best_plate = 2, None
|
||||||
|
for row in get_plate_stats():
|
||||||
|
known_alnum = "".join(c for c in row["plate"] if c.isalnum())
|
||||||
|
d = _levenshtein(plate_alnum, known_alnum)
|
||||||
|
if 0 < d < best_dist:
|
||||||
|
best_dist, best_plate = d, row["plate"]
|
||||||
|
return best_plate
|
||||||
|
|
||||||
|
|
||||||
def _read_zone_points() -> list | None:
|
def _read_zone_points() -> list | None:
|
||||||
try:
|
try:
|
||||||
with open(_ZONE_PATH) as f:
|
with open(_ZONE_PATH) as f:
|
||||||
@@ -297,13 +325,31 @@ def _process_clip(event_id: str, event_dir: str, clip_path: str, camera_name: st
|
|||||||
|
|
||||||
if PLATERECOGNIZER_KEY:
|
if PLATERECOGNIZER_KEY:
|
||||||
from lpr import call_platerecognizer
|
from lpr import call_platerecognizer
|
||||||
for _, frame, _ in scored[:3]:
|
from collections import Counter
|
||||||
p, c = call_platerecognizer(_zone_crop(frame), PLATERECOGNIZER_KEY)
|
|
||||||
|
# Always query all 3 top frames — 3 calls/passage fits comfortably in quota
|
||||||
|
pr_results: list[tuple[str, float]] = []
|
||||||
|
for _, api_frame, _ in scored[:3]:
|
||||||
|
p, c = call_platerecognizer(_zone_crop(api_frame), PLATERECOGNIZER_KEY)
|
||||||
p = _normalize_plate(p)
|
p = _normalize_plate(p)
|
||||||
if p and c > conf:
|
if p:
|
||||||
plate, conf = p, c
|
pr_results.append((p, c))
|
||||||
if conf >= 0.7:
|
|
||||||
break
|
if pr_results:
|
||||||
|
# Vote by alnum form so "GV-665-FJ" and "GV665FJ" count as the same
|
||||||
|
alnum_list = ["".join(ch for ch in p if ch.isalnum()) for p, _ in pr_results]
|
||||||
|
vote = Counter(alnum_list)
|
||||||
|
best_alnum, votes = vote.most_common(1)[0]
|
||||||
|
candidates = [(p, c) for (p, c), a in zip(pr_results, alnum_list) if a == best_alnum]
|
||||||
|
plate, conf = max(candidates, key=lambda x: x[1])
|
||||||
|
log.info(f"PlateRecognizer: {plate!r} conf={conf:.2f} ({votes}/{len(pr_results)} frames agree)")
|
||||||
|
|
||||||
|
# Fuzzy history correction — fix single-char OCR noise against known plates
|
||||||
|
hist = _match_history("".join(c for c in plate if c.isalnum()))
|
||||||
|
if hist:
|
||||||
|
log.info(f"History correction: {plate!r} → {hist!r}")
|
||||||
|
plate = hist
|
||||||
|
|
||||||
if plate:
|
if plate:
|
||||||
# Get local perspective-corrected crop for display
|
# Get local perspective-corrected crop for display
|
||||||
if _analyzer:
|
if _analyzer:
|
||||||
|
|||||||
Reference in New Issue
Block a user