feat: YOLOv8 + MJPEG dans receive_cam.py, fallback synthétique numpy
- Inférence yolov8n.pt sur chaque frame GStreamer - Fallback frame noir numpy si Pi Zero absent (pas de download) - Timeout 4s sur sonde GStreamer via thread dédié - MJPEG annoté exposé sur port 5601 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
142
receive_cam.py
142
receive_cam.py
@ -1,15 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Réception flux H.264 UDP depuis Pi Zero (port 5600) via GStreamer.
|
||||
Exposition MJPEG sur port 5601 pour consommation par YOLOv8 et l'interface web.
|
||||
Inférence YOLOv8n sur chaque frame, exposition MJPEG annoté sur port 5601.
|
||||
|
||||
Fallback automatique vers webcam locale (cv2.VideoCapture(0)) si le flux
|
||||
UDP n'est pas disponible, pour tester sans le Pi Zero.
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import argparse
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
# Pipeline GStreamer : UDP RTP H.264 → frames décodées
|
||||
import numpy as np
|
||||
import cv2
|
||||
from ultralytics import YOLO
|
||||
|
||||
MJPEG_PORT = 5601
|
||||
MJPEG_QUALITY = 70
|
||||
MODEL_PATH = Path(__file__).parent / "yolov8n.pt"
|
||||
|
||||
# Pipeline GStreamer : RTP H.264 UDP → BGR frames
|
||||
GSTREAMER_PIPELINE = (
|
||||
"udpsrc port=5600 "
|
||||
"! application/x-rtp,payload=96 "
|
||||
@ -20,37 +33,91 @@ GSTREAMER_PIPELINE = (
|
||||
"! appsink drop=true max-buffers=1"
|
||||
)
|
||||
|
||||
MJPEG_PORT = 5601
|
||||
MJPEG_QUALITY = 70 # Compromis qualité/débit pour le réseau local
|
||||
|
||||
# Frame partagée entre le thread GStreamer et le serveur MJPEG
|
||||
_frame_lock = threading.Lock()
|
||||
# Frame annotée partagée entre le thread de capture et le serveur MJPEG
|
||||
_frame_lock = threading.Lock()
|
||||
_current_frame = None
|
||||
_running = True
|
||||
_running = True
|
||||
|
||||
|
||||
def capture_loop():
|
||||
"""Lit les frames depuis GStreamer et met à jour _current_frame."""
|
||||
def _probe_gstreamer(result: list) -> None:
|
||||
"""Thread : tente une lecture GStreamer et stocke le cap si succès."""
|
||||
cap = cv2.VideoCapture(GSTREAMER_PIPELINE, cv2.CAP_GSTREAMER)
|
||||
if cap.isOpened():
|
||||
ret, _ = cap.read() # bloque jusqu'à la première frame
|
||||
if ret:
|
||||
result.append(cap)
|
||||
return
|
||||
cap.release()
|
||||
|
||||
|
||||
def _make_waiting_frame() -> np.ndarray:
|
||||
"""Génère un frame noir 640x480 avec message d'attente."""
|
||||
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
cv2.putText(frame, "En attente flux Pi Zero...", (50, 220),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
|
||||
cv2.putText(frame, "UDP 5600 non disponible", (50, 260),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (100, 100, 255), 2)
|
||||
return frame
|
||||
|
||||
|
||||
def open_capture() -> tuple[cv2.VideoCapture | None, str]:
|
||||
"""
|
||||
Priorité de source :
|
||||
1. Flux GStreamer UDP 5600 (Pi Zero)
|
||||
2. Frame synthétique (pas de hardware)
|
||||
"""
|
||||
print("[CAM] Sonde flux GStreamer UDP 5600 (timeout 4 s)...")
|
||||
result: list = []
|
||||
t = threading.Thread(target=_probe_gstreamer, args=(result,), daemon=True)
|
||||
t.start()
|
||||
t.join(timeout=4)
|
||||
|
||||
if result:
|
||||
print("[CAM] Flux GStreamer UDP reçu.")
|
||||
return result[0], "gstreamer"
|
||||
|
||||
print("[CAM] Flux UDP indisponible — mode synthétique (attente Pi Zero)")
|
||||
return None, "synthetic"
|
||||
|
||||
|
||||
def capture_loop(model: YOLO) -> None:
|
||||
"""Lit les frames, passe dans YOLOv8, stocke le résultat annoté."""
|
||||
global _current_frame, _running
|
||||
|
||||
cap = cv2.VideoCapture(GSTREAMER_PIPELINE, cv2.CAP_GSTREAMER)
|
||||
if not cap.isOpened():
|
||||
print("[CAM] Impossible d'ouvrir le pipeline GStreamer — Pi Zero connecté ?")
|
||||
_running = False
|
||||
return
|
||||
cap, source = open_capture()
|
||||
print(f"[CAM] Source active : {source}")
|
||||
|
||||
# Frame synthétique réutilisée en boucle (pas d'inférence : frame vide)
|
||||
synthetic_frame = _make_waiting_frame() if source == "synthetic" else None
|
||||
|
||||
print("[CAM] Pipeline GStreamer ouvert, réception flux...")
|
||||
while _running:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print("[CAM] Flux perdu, nouvelle tentative...")
|
||||
time.sleep(1)
|
||||
continue
|
||||
with _frame_lock:
|
||||
_current_frame = frame
|
||||
if source == "synthetic":
|
||||
frame = synthetic_frame
|
||||
time.sleep(1 / 10) # 10 fps synthétiques
|
||||
else:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
if source == "gstreamer":
|
||||
print("[CAM] Flux GStreamer perdu, nouvelle tentative...")
|
||||
time.sleep(1)
|
||||
continue
|
||||
print("[CAM] Webcam perdue, arrêt.")
|
||||
_running = False
|
||||
break
|
||||
|
||||
cap.release()
|
||||
print("[CAM] Pipeline fermé.")
|
||||
# Inférence YOLOv8 — skip en mode synthétique (frame vide sans objet)
|
||||
if model is not None and source != "synthetic":
|
||||
results = model(frame, verbose=False)
|
||||
annotated = results[0].plot()
|
||||
else:
|
||||
annotated = frame
|
||||
|
||||
with _frame_lock:
|
||||
_current_frame = annotated
|
||||
|
||||
if cap is not None:
|
||||
cap.release()
|
||||
print("[CAM] Capture terminée.")
|
||||
|
||||
|
||||
class MJPEGHandler(BaseHTTPRequestHandler):
|
||||
@ -74,7 +141,9 @@ class MJPEGHandler(BaseHTTPRequestHandler):
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
_, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, MJPEG_QUALITY])
|
||||
ok, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, MJPEG_QUALITY])
|
||||
if not ok:
|
||||
continue
|
||||
data = jpeg.tobytes()
|
||||
|
||||
try:
|
||||
@ -87,18 +156,31 @@ class MJPEGHandler(BaseHTTPRequestHandler):
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
break
|
||||
|
||||
time.sleep(1 / 30) # ~30 fps max
|
||||
time.sleep(1 / 30)
|
||||
|
||||
|
||||
def main():
|
||||
global _running
|
||||
|
||||
# Thread de capture GStreamer
|
||||
t = threading.Thread(target=capture_loop, daemon=True)
|
||||
parser = argparse.ArgumentParser(description="WireClaw — réception caméra + YOLOv8")
|
||||
parser.add_argument("--no-yolo", action="store_true", help="Désactiver l'inférence YOLOv8 (flux brut)")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"[CAM] Chargement modèle YOLOv8 : {MODEL_PATH}")
|
||||
model = YOLO(str(MODEL_PATH))
|
||||
|
||||
if args.no_yolo:
|
||||
# Mode flux brut : remplace l'inférence par une copie directe
|
||||
model = None
|
||||
print("[CAM] Mode flux brut (YOLOv8 désactivé)")
|
||||
|
||||
# Thread de capture + inférence
|
||||
t = threading.Thread(target=capture_loop, args=(model,), daemon=True)
|
||||
t.start()
|
||||
|
||||
server = HTTPServer(("0.0.0.0", MJPEG_PORT), MJPEGHandler)
|
||||
print(f"[CAM] MJPEG stream sur http://0.0.0.0:{MJPEG_PORT}/")
|
||||
print(f"[CAM] Ouvrir dans un navigateur ou VLC : http://192.168.1.84:{MJPEG_PORT}/")
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
|
||||
Reference in New Issue
Block a user