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:
nicoboy
2026-06-08 20:08:54 +02:00
parent 34d1d69500
commit 0d628d0431

View File

@ -1,15 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Réception flux H.264 UDP depuis Pi Zero (port 5600) via GStreamer. 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 threading
import time import time
from http.server import BaseHTTPRequestHandler, HTTPServer 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 = ( GSTREAMER_PIPELINE = (
"udpsrc port=5600 " "udpsrc port=5600 "
"! application/x-rtp,payload=96 " "! application/x-rtp,payload=96 "
@ -20,37 +33,91 @@ GSTREAMER_PIPELINE = (
"! appsink drop=true max-buffers=1" "! appsink drop=true max-buffers=1"
) )
MJPEG_PORT = 5601 # Frame annotée partagée entre le thread de capture et le serveur MJPEG
MJPEG_QUALITY = 70 # Compromis qualité/débit pour le réseau local _frame_lock = threading.Lock()
# Frame partagée entre le thread GStreamer et le serveur MJPEG
_frame_lock = threading.Lock()
_current_frame = None _current_frame = None
_running = True _running = True
def capture_loop(): def _probe_gstreamer(result: list) -> None:
"""Lit les frames depuis GStreamer et met à jour _current_frame.""" """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 global _current_frame, _running
cap = cv2.VideoCapture(GSTREAMER_PIPELINE, cv2.CAP_GSTREAMER) cap, source = open_capture()
if not cap.isOpened(): print(f"[CAM] Source active : {source}")
print("[CAM] Impossible d'ouvrir le pipeline GStreamer — Pi Zero connecté ?")
_running = False # Frame synthétique réutilisée en boucle (pas d'inférence : frame vide)
return synthetic_frame = _make_waiting_frame() if source == "synthetic" else None
print("[CAM] Pipeline GStreamer ouvert, réception flux...")
while _running: while _running:
ret, frame = cap.read() if source == "synthetic":
if not ret: frame = synthetic_frame
print("[CAM] Flux perdu, nouvelle tentative...") time.sleep(1 / 10) # 10 fps synthétiques
time.sleep(1) else:
continue ret, frame = cap.read()
with _frame_lock: if not ret:
_current_frame = frame 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() # Inférence YOLOv8 — skip en mode synthétique (frame vide sans objet)
print("[CAM] Pipeline fermé.") 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): class MJPEGHandler(BaseHTTPRequestHandler):
@ -74,7 +141,9 @@ class MJPEGHandler(BaseHTTPRequestHandler):
time.sleep(0.05) time.sleep(0.05)
continue 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() data = jpeg.tobytes()
try: try:
@ -87,18 +156,31 @@ class MJPEGHandler(BaseHTTPRequestHandler):
except (BrokenPipeError, ConnectionResetError): except (BrokenPipeError, ConnectionResetError):
break break
time.sleep(1 / 30) # ~30 fps max time.sleep(1 / 30)
def main(): def main():
global _running global _running
# Thread de capture GStreamer parser = argparse.ArgumentParser(description="WireClaw — réception caméra + YOLOv8")
t = threading.Thread(target=capture_loop, daemon=True) 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() t.start()
server = HTTPServer(("0.0.0.0", MJPEG_PORT), MJPEGHandler) 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] 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: try:
server.serve_forever() server.serve_forever()