OPi: shadow-распознавание Nomeroff (офлайн ANPR) параллельно с Claude
- фоновый вызов Nomeroff (:5010/recognize?crop=1&enhance=0) после захвата кадра - результат -> /api/local-plate с ретраем на 404 (гонка с capture) - полностью изолирован: ошибка/таймаут не влияют на боевой поток - только для боевых снимков (не is_test)
This commit is contained in:
@@ -30,6 +30,7 @@ import os
|
||||
import base64
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# ── outbox (store-and-forward, встроенный) ──────────────────────────────────
|
||||
try:
|
||||
@@ -58,6 +59,9 @@ MQTT2_PASS = 'Esp32Scales#2026'
|
||||
CAPTURE_ENABLED = True
|
||||
RTSP_URL = 'rtsp://iivideo:Vk9mT2xLp7nQ4wBj@192.168.20.3:9784/cameras/124/streaming/main'
|
||||
SERVER_CAPTURE_URL = 'https://scales.zeroday.su/api/capture'
|
||||
# ── офлайн-распознавание (Nomeroff shadow) ───────────────────────────────────
|
||||
NOMEROFF_URL = 'http://192.168.20.4:5010/recognize?crop=1&enhance=0'
|
||||
LOCAL_PLATE_URL = SERVER_CAPTURE_URL.rsplit('/api/capture', 1)[0] + '/api/local-plate'
|
||||
CAPTURE_DEVICE_ID = 'scales_opi'
|
||||
FRAME_PATH = '/tmp/scale_frame.jpg'
|
||||
|
||||
@@ -219,6 +223,57 @@ def grab_frame():
|
||||
print(f"[CAPTURE] Ошибка захвата кадра: {e}")
|
||||
return None
|
||||
|
||||
def _offline_anpr(img, event_id):
|
||||
"""Фоновый shadow-вызов Nomeroff + отправка результата на сервер.
|
||||
Полностью изолирован: любая ошибка/таймаут НЕ влияет на боевой поток взвешивания."""
|
||||
try:
|
||||
boundary = '----opi' + str(int(time.time() * 1000))
|
||||
body = (
|
||||
('--%s\r\n' % boundary).encode()
|
||||
+ b'Content-Disposition: form-data; name="photo"; filename="frame.jpg"\r\n'
|
||||
+ b'Content-Type: image/jpeg\r\n\r\n' + img
|
||||
+ ('\r\n--%s--\r\n' % boundary).encode()
|
||||
)
|
||||
req = urllib.request.Request(
|
||||
NOMEROFF_URL, data=body,
|
||||
headers={'Content-Type': 'multipart/form-data; boundary=%s' % boundary})
|
||||
with urllib.request.urlopen(req, timeout=8) as r:
|
||||
resp = json.loads(r.read().decode())
|
||||
except Exception as e:
|
||||
print(f"[ANPR] {event_id}: Nomeroff недоступен: {e}")
|
||||
return
|
||||
|
||||
if not resp.get('found') or not resp.get('plate'):
|
||||
print(f"[ANPR] {event_id}: офлайн не распознал")
|
||||
return
|
||||
|
||||
payload = json.dumps({
|
||||
"event_id": event_id,
|
||||
"local_plate": resp.get('plate'),
|
||||
"local_confidence": resp.get('confidence'),
|
||||
"local_model": "nomeroff",
|
||||
"local_elapsed": resp.get('elapsed_sec'),
|
||||
}).encode()
|
||||
# ретрай на 404: событие могло ещё не создаться на сервере (capture летит параллельно)
|
||||
for attempt in range(3):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
LOCAL_PLATE_URL, data=payload,
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
print(f"[ANPR] {event_id}: офлайн {resp.get('plate')} → сервер, ответ {r.status}")
|
||||
return
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404 and attempt < 2:
|
||||
time.sleep(1.2)
|
||||
continue
|
||||
print(f"[ANPR] {event_id}: ошибка отправки: {e}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"[ANPR] {event_id}: ошибка отправки: {e}")
|
||||
return
|
||||
|
||||
|
||||
def capture_and_send(weight, event_id, is_test=False):
|
||||
"""
|
||||
Кадр + вес → локальную очередь (outbox).
|
||||
@@ -229,6 +284,10 @@ def capture_and_send(weight, event_id, is_test=False):
|
||||
print(f"[CAPTURE] {event_id}: кадр не получен, пропуск")
|
||||
return
|
||||
|
||||
# shadow-распознавание Nomeroff в фоне (не для тестовых снимков камеры)
|
||||
if not is_test:
|
||||
threading.Thread(target=_offline_anpr, args=(img, event_id), daemon=True).start()
|
||||
|
||||
# Положить в outbox
|
||||
if outbox:
|
||||
outbox.enqueue(
|
||||
|
||||
Reference in New Issue
Block a user