From 8507987ea9c956d10427b7dcf526376e9d5bab0a Mon Sep 17 00:00:00 2001 From: perco Date: Tue, 2 Jun 2026 15:48:12 +0200 Subject: [PATCH] Capture ffmpeg 5fps/15s au lieu de OpenCV 1fps/20s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Dockerfile | 2 +- app/watcher.py | 67 ++++++++++++++++++++++++++++++++-------------- docker-compose.yml | 3 ++- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3ff60b3..2e2e39d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.11-slim 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/* WORKDIR /app diff --git a/app/watcher.py b/app/watcher.py index 53a15ae..a1f1226 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -2,6 +2,9 @@ import os import time import uuid import logging +import subprocess +import tempfile +import glob import requests import cv2 import numpy as np @@ -20,7 +23,8 @@ 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", "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")) _token: str | None = None @@ -86,34 +90,57 @@ def _frame_score(frame: np.ndarray, plates: list) -> float: 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 + log.info(f"Capturing {CAPTURE_DURATION}s at {CAPTURE_FPS}fps via ffmpeg...") - best_frame: np.ndarray | None = None - best_score = -1.0 - start = time.time() + with tempfile.TemporaryDirectory() as tmpdir: + clip_path = os.path.join(tmpdir, "clip.mp4") + frames_pattern = os.path.join(tmpdir, "frame_%04d.jpg") - try: - while time.time() - start < CAPTURE_DURATION: - ret, frame = cap.read() - if not ret: - log.warning("RTSP read failed, retrying...") - time.sleep(0.5) + # 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() - time.sleep(0.8) - finally: - cap.release() - log.info(f"Capture done. Best frame score: {best_score:.2f}") - return best_frame + log.info(f"Best frame score: {best_score:.2f}") + return best_frame def _process_event(): diff --git a/docker-compose.yml b/docker-compose.yml index 28cd9e2..055abaf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,8 @@ services: - DB_PATH=/data/camwatch.db - SNAPSHOTS_DIR=/data/snapshots - POLL_INTERVAL=2 - - CAPTURE_DURATION=20 + - CAPTURE_DURATION=15 + - CAPTURE_FPS=5 - COOLDOWN=60 labels: - traefik.enable=true