diff --git a/receive_cam.py b/receive_cam.py index 6468ceb..28bc509 100644 --- a/receive_cam.py +++ b/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()