feat: thread sideband UDP 5602 + overlay stats Pi Zero sur MJPEG
Reçoit JSON Pi Zero (width, height, fps, encoder, temp, cpu, throttled) toutes les secondes via UDP 5602. Affiche un overlay cv2 semi-transparent (fond noir 55%) en haut à gauche de chaque frame MJPEG : ligne 1 : "1280x720 | 15fps | hw | 56.2C" ligne 2 : "CPU 42% | ok / THROTTLED" Variable globale _pi_stats protégée par _stats_lock (threading.Lock). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -2,12 +2,16 @@
|
|||||||
"""
|
"""
|
||||||
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.
|
||||||
Inférence YOLOv8n sur chaque frame, exposition MJPEG annoté sur port 5601.
|
Inférence YOLOv8n sur chaque frame, exposition MJPEG annoté sur port 5601.
|
||||||
|
Thread sideband UDP 5602 : reçoit JSON Pi Zero (résolution, fps, temp, CPU...)
|
||||||
|
et affiche un overlay semi-transparent sur chaque frame.
|
||||||
|
|
||||||
Fallback automatique vers webcam locale (cv2.VideoCapture(0)) si le flux
|
Fallback automatique vers webcam locale (cv2.VideoCapture(0)) si le flux
|
||||||
UDP n'est pas disponible, pour tester sans le Pi Zero.
|
UDP n'est pas disponible, pour tester sans le Pi Zero.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@ -19,6 +23,7 @@ import cv2
|
|||||||
from ultralytics import YOLO
|
from ultralytics import YOLO
|
||||||
|
|
||||||
MJPEG_PORT = 5601
|
MJPEG_PORT = 5601
|
||||||
|
SIDEBAND_PORT = 5602
|
||||||
MJPEG_QUALITY = 70
|
MJPEG_QUALITY = 70
|
||||||
MODEL_PATH = Path(__file__).parent / "yolov8n.pt"
|
MODEL_PATH = Path(__file__).parent / "yolov8n.pt"
|
||||||
|
|
||||||
@ -38,6 +43,75 @@ _frame_lock = threading.Lock()
|
|||||||
_current_frame = None
|
_current_frame = None
|
||||||
_running = True
|
_running = True
|
||||||
|
|
||||||
|
# Dernières stats Pi Zero reçues via UDP 5602 (thread-safe)
|
||||||
|
_stats_lock = threading.Lock()
|
||||||
|
_pi_stats: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def sideband_loop() -> None:
|
||||||
|
"""Écoute UDP 5602, stocke le dernier JSON Pi Zero dans _pi_stats."""
|
||||||
|
global _pi_stats
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.settimeout(1.0)
|
||||||
|
sock.bind(("0.0.0.0", SIDEBAND_PORT))
|
||||||
|
print(f"[SIDEBAND] Écoute UDP sur port {SIDEBAND_PORT}")
|
||||||
|
while _running:
|
||||||
|
try:
|
||||||
|
data, _ = sock.recvfrom(4096)
|
||||||
|
payload = json.loads(data.decode("utf-8"))
|
||||||
|
with _stats_lock:
|
||||||
|
_pi_stats = payload
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
|
print(f"[SIDEBAND] JSON invalide : {e}")
|
||||||
|
sock.close()
|
||||||
|
print("[SIDEBAND] Arrêt.")
|
||||||
|
|
||||||
|
|
||||||
|
def draw_overlay(frame: np.ndarray) -> np.ndarray:
|
||||||
|
"""Dessine l'overlay stats Pi Zero (fond noir semi-transparent) sur le frame."""
|
||||||
|
with _stats_lock:
|
||||||
|
stats = _pi_stats
|
||||||
|
if stats is None:
|
||||||
|
return frame
|
||||||
|
|
||||||
|
w_res = stats.get("width", "?")
|
||||||
|
h_res = stats.get("height", "?")
|
||||||
|
fps = stats.get("fps", "?")
|
||||||
|
enc = stats.get("encoder", "?")
|
||||||
|
temp = stats.get("temp", "?")
|
||||||
|
cpu = stats.get("cpu", "?")
|
||||||
|
throttled = stats.get("throttled", False)
|
||||||
|
throttle_str = "THROTTLED" if throttled else "ok"
|
||||||
|
|
||||||
|
line1 = f"{w_res}x{h_res} | {fps}fps | {enc} | {temp}C"
|
||||||
|
line2 = f"CPU {cpu}% | {throttle_str}"
|
||||||
|
|
||||||
|
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||||
|
scale = 0.55
|
||||||
|
thickness = 1
|
||||||
|
margin = 6
|
||||||
|
|
||||||
|
(tw1, th1), _ = cv2.getTextSize(line1, font, scale, thickness)
|
||||||
|
(tw2, th2), _ = cv2.getTextSize(line2, font, scale, thickness)
|
||||||
|
box_w = max(tw1, tw2) + margin * 2
|
||||||
|
box_h = th1 + th2 + margin * 3
|
||||||
|
|
||||||
|
# Copie pour ne pas altérer le frame original (réutilisé en mode synthétique)
|
||||||
|
out = frame.copy()
|
||||||
|
bg = out.copy()
|
||||||
|
cv2.rectangle(bg, (0, 0), (box_w, box_h), (0, 0, 0), -1)
|
||||||
|
cv2.addWeighted(bg, 0.55, out, 0.45, 0, out)
|
||||||
|
|
||||||
|
cv2.putText(out, line1, (margin, margin + th1),
|
||||||
|
font, scale, (255, 255, 255), thickness, cv2.LINE_AA)
|
||||||
|
cv2.putText(out, line2, (margin, margin * 2 + th1 + th2),
|
||||||
|
font, scale, (255, 255, 255), thickness, cv2.LINE_AA)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _probe_gstreamer(result: list) -> None:
|
def _probe_gstreamer(result: list) -> None:
|
||||||
"""Thread : tente une lecture GStreamer et stocke le cap si succès."""
|
"""Thread : tente une lecture GStreamer et stocke le cap si succès."""
|
||||||
@ -112,6 +186,9 @@ def capture_loop(model: YOLO) -> None:
|
|||||||
else:
|
else:
|
||||||
annotated = frame
|
annotated = frame
|
||||||
|
|
||||||
|
# Overlay stats Pi Zero (ne modifie pas annotated en place)
|
||||||
|
annotated = draw_overlay(annotated)
|
||||||
|
|
||||||
with _frame_lock:
|
with _frame_lock:
|
||||||
_current_frame = annotated
|
_current_frame = annotated
|
||||||
|
|
||||||
@ -174,6 +251,10 @@ def main():
|
|||||||
model = None
|
model = None
|
||||||
print("[CAM] Mode flux brut (YOLOv8 désactivé)")
|
print("[CAM] Mode flux brut (YOLOv8 désactivé)")
|
||||||
|
|
||||||
|
# Thread sideband : stats Pi Zero via UDP 5602
|
||||||
|
ts = threading.Thread(target=sideband_loop, daemon=True)
|
||||||
|
ts.start()
|
||||||
|
|
||||||
# Thread de capture + inférence
|
# Thread de capture + inférence
|
||||||
t = threading.Thread(target=capture_loop, args=(model,), daemon=True)
|
t = threading.Thread(target=capture_loop, args=(model,), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user