Passe de Frigate polling à GetAiState Reolink + LPR ONNX local
- Remplace la source Frigate par l'API GetAiState de la caméra Reolink - Capture RTSP en temps réel (~20s) quand véhicule détecté, garde la meilleure frame - LPR avec YOLOv9 (détection plaque) + PaddleOCR v4 (lecture texte) via ONNX - Modèles partagés avec Frigate (volume local ./models/) - Cooldown 60s entre événements pour éviter les doublons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
f6422b0e7e
commit
65b74ce46d
+132
-62
@@ -1,94 +1,164 @@
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import logging
|
||||
import requests
|
||||
import shutil
|
||||
from database import event_exists, insert_event
|
||||
from analyzer import extract_dominant_color
|
||||
import cv2
|
||||
import numpy as np
|
||||
from urllib.parse import quote
|
||||
|
||||
from database import insert_event
|
||||
from analyzer import extract_dominant_color_from_frame
|
||||
from lpr import PlateAnalyzer
|
||||
|
||||
log = logging.getLogger("watcher")
|
||||
|
||||
FRIGATE_URL = os.environ.get("FRIGATE_URL", "http://frigate:5000")
|
||||
CAMERA_URL = os.environ.get("CAMERA_URL", "http://192.168.1.44")
|
||||
CAMERA_USER = os.environ.get("CAMERA_USER", "admin")
|
||||
CAMERA_PASS = os.environ.get("CAMERA_PASS", "")
|
||||
CAMERA_RTSP = os.environ.get("CAMERA_RTSP", "")
|
||||
CAMERA_NAME = os.environ.get("CAMERA_NAME", "portail")
|
||||
SNAPSHOTS_DIR = os.environ.get("SNAPSHOTS_DIR", "/data/snapshots")
|
||||
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "30"))
|
||||
POLL_INTERVAL = float(os.environ.get("POLL_INTERVAL", "2"))
|
||||
CAPTURE_DURATION = int(os.environ.get("CAPTURE_DURATION", "20"))
|
||||
COOLDOWN = int(os.environ.get("COOLDOWN", "60"))
|
||||
|
||||
_token: str | None = None
|
||||
_token_time = 0.0
|
||||
_last_event_time = 0.0
|
||||
_analyzer: PlateAnalyzer | None = None
|
||||
|
||||
|
||||
def fetch_new_events() -> list[dict]:
|
||||
def _rtsp_url() -> str:
|
||||
if CAMERA_RTSP:
|
||||
return CAMERA_RTSP
|
||||
host = CAMERA_URL.replace("http://", "").replace("https://", "").split(":")[0]
|
||||
return f"rtsp://{quote(CAMERA_USER, safe='')}:{quote(CAMERA_PASS, safe='')}@{host}/h264Preview_01_main"
|
||||
|
||||
|
||||
def _login() -> str | None:
|
||||
global _token, _token_time
|
||||
if _token and (time.time() - _token_time) < 3000:
|
||||
return _token
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{FRIGATE_URL}/api/events",
|
||||
params={"labels": "car", "has_snapshot": "1", "limit": "50"},
|
||||
timeout=10
|
||||
resp = requests.post(
|
||||
f"{CAMERA_URL}/api.cgi?cmd=Login",
|
||||
json=[{"cmd": "Login", "param": {"User": {"userName": CAMERA_USER, "password": CAMERA_PASS}}}],
|
||||
timeout=5,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
data = resp.json()
|
||||
if data[0]["code"] == 0:
|
||||
_token = data[0]["value"]["Token"]["name"]
|
||||
_token_time = time.time()
|
||||
log.debug("Camera login OK")
|
||||
return _token
|
||||
except Exception as e:
|
||||
log.warning(f"Frigate fetch error: {e}")
|
||||
return []
|
||||
log.warning(f"Login error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def download_snapshot(event_id: str, dest_path: str) -> bool:
|
||||
def _get_ai_state() -> dict | None:
|
||||
token = _login()
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{FRIGATE_URL}/api/events/{event_id}/snapshot.jpg",
|
||||
params={"bbox": "1", "crop": "1", "quality": "95"},
|
||||
timeout=15, stream=True
|
||||
resp = requests.post(
|
||||
f"{CAMERA_URL}/api.cgi?cmd=GetAiState&token={token}",
|
||||
json=[{"cmd": "GetAiState", "action": 0, "param": {"channel": 0}}],
|
||||
timeout=5,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
with open(dest_path, "wb") as f:
|
||||
shutil.copyfileobj(resp.raw, f)
|
||||
return True
|
||||
data = resp.json()
|
||||
if data[0]["code"] == 0:
|
||||
return data[0]["value"]
|
||||
except Exception as e:
|
||||
log.warning(f"Snapshot download error for {event_id}: {e}")
|
||||
return False
|
||||
log.warning(f"GetAiState error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def process_event(ev: dict):
|
||||
event_id = ev.get("id", "")
|
||||
if not event_id or event_exists(event_id):
|
||||
def _frame_score(frame: np.ndarray, plates: list) -> float:
|
||||
"""Score a frame: prefer large, high-confidence plates. Fallback to sharpness."""
|
||||
if plates:
|
||||
x1, y1, x2, y2, conf = plates[0]
|
||||
return (x2 - x1) * (y2 - y1) * conf
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
return cv2.Laplacian(gray, cv2.CV_64F).var() * 0.001
|
||||
|
||||
|
||||
def _capture_best_frame() -> np.ndarray | None:
|
||||
url = _rtsp_url()
|
||||
log.info(f"Capturing RTSP for {CAPTURE_DURATION}s...")
|
||||
cap = cv2.VideoCapture(url)
|
||||
if not cap.isOpened():
|
||||
log.error("Cannot open RTSP stream")
|
||||
return None
|
||||
|
||||
best_frame: np.ndarray | None = None
|
||||
best_score = -1.0
|
||||
start = time.time()
|
||||
|
||||
try:
|
||||
while time.time() - start < CAPTURE_DURATION:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
log.warning("RTSP read failed, retrying...")
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
plates = _analyzer.detect_plates(frame) if _analyzer else []
|
||||
score = _frame_score(frame, plates)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_frame = frame.copy()
|
||||
time.sleep(0.8)
|
||||
finally:
|
||||
cap.release()
|
||||
|
||||
log.info(f"Capture done. Best frame score: {best_score:.2f}")
|
||||
return best_frame
|
||||
|
||||
|
||||
def _process_event():
|
||||
frame = _capture_best_frame()
|
||||
if frame is None:
|
||||
log.warning("No frame captured")
|
||||
return
|
||||
|
||||
camera = ev.get("camera", "unknown")
|
||||
start_time = int(ev.get("start_time", time.time()))
|
||||
|
||||
# Plate from Frigate LPR
|
||||
plate = None
|
||||
data = ev.get("data", {})
|
||||
if data.get("sub_label"):
|
||||
plate = data["sub_label"]
|
||||
if isinstance(plate, list):
|
||||
plate = plate[0] if plate else None
|
||||
|
||||
# Download snapshot
|
||||
event_id = str(uuid.uuid4())
|
||||
snapshot_file = f"{event_id}.jpg"
|
||||
snapshot_path = os.path.join(SNAPSHOTS_DIR, snapshot_file)
|
||||
if not download_snapshot(event_id, snapshot_path):
|
||||
return
|
||||
cv2.imwrite(snapshot_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
|
||||
|
||||
# Extract color from vehicle bounding box
|
||||
bbox = None
|
||||
if ev.get("box"):
|
||||
b = ev["box"]
|
||||
bbox = {"x": b[0], "y": b[1], "width": b[2] - b[0], "height": b[3] - b[1]}
|
||||
elif data.get("box"):
|
||||
b = data["box"]
|
||||
if len(b) == 4:
|
||||
bbox = {"x": b[0], "y": b[1], "width": b[2] - b[0], "height": b[3] - b[1]}
|
||||
plate, conf = "", 0.0
|
||||
if _analyzer:
|
||||
plate, conf = _analyzer.read_plate(frame)
|
||||
|
||||
hex_color, color_name = extract_dominant_color(snapshot_path, bbox)
|
||||
log.info(f"LPR: plate={plate!r} conf={conf:.2f}")
|
||||
|
||||
insert_event(event_id, camera, start_time, f"snapshots/{snapshot_file}",
|
||||
plate, hex_color, color_name)
|
||||
log.info(f"Processed event {event_id} | plate={plate} | color={color_name} | camera={camera}")
|
||||
hex_color, color_name = extract_dominant_color_from_frame(frame)
|
||||
insert_event(
|
||||
event_id, CAMERA_NAME, int(time.time()),
|
||||
f"snapshots/{snapshot_file}", plate or None, hex_color, color_name,
|
||||
)
|
||||
log.info(f"Stored: {event_id} | plate={plate} | color={color_name}")
|
||||
|
||||
|
||||
def run_watcher():
|
||||
log.info(f"Watcher started — polling Frigate every {POLL_INTERVAL}s")
|
||||
global _last_event_time, _analyzer
|
||||
|
||||
log.info("Loading plate analyzer...")
|
||||
_analyzer = PlateAnalyzer()
|
||||
|
||||
log.info(f"Watcher started — polling {CAMERA_URL} every {POLL_INTERVAL}s")
|
||||
while True:
|
||||
events = fetch_new_events()
|
||||
for ev in events:
|
||||
try:
|
||||
process_event(ev)
|
||||
except Exception as e:
|
||||
log.error(f"Error processing event: {e}")
|
||||
try:
|
||||
state = _get_ai_state()
|
||||
if state:
|
||||
vehicle = state.get("vehicle", {})
|
||||
if vehicle.get("alarm_state") == 1:
|
||||
now = time.time()
|
||||
if now - _last_event_time > COOLDOWN:
|
||||
_last_event_time = now
|
||||
log.info("Vehicle detected! Triggering capture...")
|
||||
_process_event()
|
||||
except Exception as e:
|
||||
log.error(f"Watcher loop error: {e}", exc_info=True)
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
Reference in New Issue
Block a user