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>
This commit is contained in:
perco
2026-06-02 15:48:12 +02:00
co-authored by Claude Sonnet 4.6
parent 65b74ce46d
commit 8507987ea9
3 changed files with 50 additions and 22 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
FROM python:3.11-slim FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1 libglib2.0-0 libgomp1 \ libgl1 libglib2.0-0 libgomp1 ffmpeg \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
+47 -20
View File
@@ -2,6 +2,9 @@ import os
import time import time
import uuid import uuid
import logging import logging
import subprocess
import tempfile
import glob
import requests import requests
import cv2 import cv2
import numpy as np import numpy as np
@@ -20,7 +23,8 @@ CAMERA_RTSP = os.environ.get("CAMERA_RTSP", "")
CAMERA_NAME = os.environ.get("CAMERA_NAME", "portail") CAMERA_NAME = os.environ.get("CAMERA_NAME", "portail")
SNAPSHOTS_DIR = os.environ.get("SNAPSHOTS_DIR", "/data/snapshots") SNAPSHOTS_DIR = os.environ.get("SNAPSHOTS_DIR", "/data/snapshots")
POLL_INTERVAL = float(os.environ.get("POLL_INTERVAL", "2")) POLL_INTERVAL = float(os.environ.get("POLL_INTERVAL", "2"))
CAPTURE_DURATION = int(os.environ.get("CAPTURE_DURATION", "20")) 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")) COOLDOWN = int(os.environ.get("COOLDOWN", "60"))
_token: str | None = None _token: str | None = None
@@ -86,34 +90,57 @@ def _frame_score(frame: np.ndarray, plates: list) -> float:
def _capture_best_frame() -> np.ndarray | None: def _capture_best_frame() -> np.ndarray | None:
url = _rtsp_url() url = _rtsp_url()
log.info(f"Capturing RTSP for {CAPTURE_DURATION}s...") log.info(f"Capturing {CAPTURE_DURATION}s at {CAPTURE_FPS}fps via ffmpeg...")
cap = cv2.VideoCapture(url)
if not cap.isOpened():
log.error("Cannot open RTSP stream")
return None
best_frame: np.ndarray | None = None with tempfile.TemporaryDirectory() as tmpdir:
best_score = -1.0 clip_path = os.path.join(tmpdir, "clip.mp4")
start = time.time() frames_pattern = os.path.join(tmpdir, "frame_%04d.jpg")
try: # Step 1: capture clip with ffmpeg (TCP for reliability)
while time.time() - start < CAPTURE_DURATION: ret = subprocess.run([
ret, frame = cap.read() "ffmpeg", "-y",
if not ret: "-rtsp_transport", "tcp",
log.warning("RTSP read failed, retrying...") "-i", url,
time.sleep(0.5) "-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 continue
plates = _analyzer.detect_plates(frame) if _analyzer else [] plates = _analyzer.detect_plates(frame) if _analyzer else []
score = _frame_score(frame, plates) score = _frame_score(frame, plates)
if score > best_score: if score > best_score:
best_score = score best_score = score
best_frame = frame.copy() best_frame = frame.copy()
time.sleep(0.8)
finally:
cap.release()
log.info(f"Capture done. Best frame score: {best_score:.2f}") log.info(f"Best frame score: {best_score:.2f}")
return best_frame return best_frame
def _process_event(): def _process_event():
+2 -1
View File
@@ -15,7 +15,8 @@ services:
- DB_PATH=/data/camwatch.db - DB_PATH=/data/camwatch.db
- SNAPSHOTS_DIR=/data/snapshots - SNAPSHOTS_DIR=/data/snapshots
- POLL_INTERVAL=2 - POLL_INTERVAL=2
- CAPTURE_DURATION=20 - CAPTURE_DURATION=15
- CAPTURE_FPS=5
- COOLDOWN=60 - COOLDOWN=60
labels: labels:
- traefik.enable=true - traefik.enable=true