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:
co-authored by
Claude Sonnet 4.6
parent
65b74ce46d
commit
8507987ea9
+1
-1
@@ -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
|
||||||
|
|||||||
+43
-16
@@ -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,33 +90,56 @@ 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():
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
log.error("Cannot open RTSP stream")
|
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
|
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_frame: np.ndarray | None = None
|
||||||
best_score = -1.0
|
best_score = -1.0
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
try:
|
for path in frame_files:
|
||||||
while time.time() - start < CAPTURE_DURATION:
|
frame = cv2.imread(path)
|
||||||
ret, frame = cap.read()
|
if frame is None:
|
||||||
if not ret:
|
|
||||||
log.warning("RTSP read failed, retrying...")
|
|
||||||
time.sleep(0.5)
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user