Files
camwatch/app/watcher.py
T
percoandClaude Sonnet 4.6 8507987ea9 Capture ffmpeg 5fps/15s au lieu de OpenCV 1fps/20s
- Remplace OpenCV VideoCapture par ffmpeg pour la capture RTSP (plus fiable)
- Capture 15s → extrait ~75 frames à 5fps via ffmpeg
- Le LPR analyse toutes les frames et garde la meilleure (plaque la plus grande/confiante)
- Ajoute CAPTURE_FPS env var

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 15:48:12 +02:00

192 lines
6.1 KiB
Python

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)