import os import time import uuid import logging import subprocess import tempfile import glob import requests 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") 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 = float(os.environ.get("POLL_INTERVAL", "2")) CAPTURE_DURATION = int(os.environ.get("CAPTURE_DURATION", "15")) CAPTURE_FPS = int(os.environ.get("CAPTURE_FPS", "5")) 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 _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.post( f"{CAMERA_URL}/api.cgi?cmd=Login", json=[{"cmd": "Login", "param": {"User": {"userName": CAMERA_USER, "password": CAMERA_PASS}}}], timeout=5, ) 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"Login error: {e}") return None def _get_ai_state() -> dict | None: token = _login() if not token: return None try: resp = requests.post( f"{CAMERA_URL}/api.cgi?cmd=GetAiState&token={token}", json=[{"cmd": "GetAiState", "action": 0, "param": {"channel": 0}}], timeout=5, ) data = resp.json() if data[0]["code"] == 0: return data[0]["value"] except Exception as e: log.warning(f"GetAiState error: {e}") return None 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 {CAPTURE_DURATION}s at {CAPTURE_FPS}fps via ffmpeg...") with tempfile.TemporaryDirectory() as tmpdir: clip_path = os.path.join(tmpdir, "clip.mp4") frames_pattern = os.path.join(tmpdir, "frame_%04d.jpg") # Step 1: capture clip with ffmpeg (TCP for reliability) ret = subprocess.run([ "ffmpeg", "-y", "-rtsp_transport", "tcp", "-i", url, "-t", str(CAPTURE_DURATION), "-c", "copy", clip_path, ], capture_output=True, timeout=CAPTURE_DURATION + 10) if not os.path.exists(clip_path) or os.path.getsize(clip_path) < 1000: log.error(f"ffmpeg capture failed: {ret.stderr[-200:].decode(errors='ignore')}") return None # Step 2: extract frames at CAPTURE_FPS subprocess.run([ "ffmpeg", "-y", "-i", clip_path, "-vf", f"fps={CAPTURE_FPS}", "-q:v", "2", frames_pattern, ], capture_output=True, timeout=30) frame_files = sorted(glob.glob(os.path.join(tmpdir, "frame_*.jpg"))) log.info(f"Extracted {len(frame_files)} frames") if not frame_files: return None # Step 3: score each frame, keep the best best_frame: np.ndarray | None = None best_score = -1.0 for path in frame_files: frame = cv2.imread(path) if frame is None: 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() log.info(f"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 event_id = str(uuid.uuid4()) snapshot_file = f"{event_id}.jpg" snapshot_path = os.path.join(SNAPSHOTS_DIR, snapshot_file) cv2.imwrite(snapshot_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 90]) plate, conf = "", 0.0 if _analyzer: plate, conf = _analyzer.read_plate(frame) log.info(f"LPR: plate={plate!r} conf={conf:.2f}") 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(): 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: 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)