141 lines
3.9 KiB
Python
141 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
RTSP stream with YOLOv8 ball detection.
|
|
Pushes to mediamtx RTSP server using ffmpeg.
|
|
|
|
Usage:
|
|
1. Start mediamtx: /tmp/mediamtx &
|
|
2. Run this script: python3 stream_with_detection.py [source]
|
|
3. View stream: vlc rtsp://pickle:8554/live
|
|
"""
|
|
|
|
import cv2
|
|
import subprocess
|
|
import time
|
|
import sys
|
|
from ultralytics import YOLO
|
|
|
|
BALL_CLASS_ID = 32 # sports ball in COCO
|
|
WIDTH = 1280
|
|
HEIGHT = 720
|
|
FPS = 15 # Lower FPS for stable streaming
|
|
RTSP_URL = "rtsp://localhost:8554/live"
|
|
|
|
|
|
def main():
|
|
source = sys.argv[1] if len(sys.argv) > 1 else "/opt/nvidia/deepstream/deepstream/samples/streams/sample_1080p_h264.mp4"
|
|
|
|
print("Loading YOLOv8n...")
|
|
model = YOLO("yolov8n.pt")
|
|
try:
|
|
model.to("cuda")
|
|
print("Using CUDA")
|
|
except:
|
|
print("Using CPU")
|
|
|
|
print(f"Opening: {source}")
|
|
cap = cv2.VideoCapture(source)
|
|
if not cap.isOpened():
|
|
print("ERROR: Cannot open source")
|
|
return 1
|
|
|
|
# FFmpeg command to push RTSP
|
|
ffmpeg_cmd = [
|
|
'ffmpeg',
|
|
'-y',
|
|
'-f', 'rawvideo',
|
|
'-vcodec', 'rawvideo',
|
|
'-pix_fmt', 'bgr24',
|
|
'-s', f'{WIDTH}x{HEIGHT}',
|
|
'-r', str(FPS),
|
|
'-i', '-',
|
|
'-c:v', 'libx264',
|
|
'-preset', 'ultrafast',
|
|
'-tune', 'zerolatency',
|
|
'-g', str(FPS * 2),
|
|
'-f', 'rtsp',
|
|
'-rtsp_transport', 'tcp',
|
|
RTSP_URL
|
|
]
|
|
|
|
print("Starting ffmpeg RTSP publisher...")
|
|
ffmpeg = subprocess.Popen(
|
|
ffmpeg_cmd,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL
|
|
)
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f"RTSP STREAM: rtsp://pickle:8554/live")
|
|
print(f"{'='*50}")
|
|
print("Press Ctrl+C to stop\n")
|
|
|
|
frame_count = 0
|
|
start = time.time()
|
|
total_detections = 0
|
|
frame_time = 1.0 / FPS
|
|
|
|
try:
|
|
while True:
|
|
loop_start = time.time()
|
|
|
|
ret, frame = cap.read()
|
|
if not ret:
|
|
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
continue
|
|
|
|
frame = cv2.resize(frame, (WIDTH, HEIGHT))
|
|
|
|
# Detection
|
|
results = model(frame, verbose=False, classes=[BALL_CLASS_ID], conf=0.25)
|
|
det_count = 0
|
|
for r in results:
|
|
for box in r.boxes:
|
|
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
|
conf = float(box.conf[0])
|
|
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 3)
|
|
cv2.putText(frame, f"Ball {conf:.2f}", (x1, y1-10),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
|
det_count += 1
|
|
total_detections += 1
|
|
|
|
# FPS overlay
|
|
frame_count += 1
|
|
elapsed = time.time() - start
|
|
fps = frame_count / elapsed
|
|
cv2.putText(frame, f"FPS: {fps:.1f} | Det: {det_count}", (10, 30),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
|
|
|
|
# Push to ffmpeg
|
|
try:
|
|
ffmpeg.stdin.write(frame.tobytes())
|
|
except BrokenPipeError:
|
|
print("FFmpeg pipe broken, restarting...")
|
|
break
|
|
|
|
if frame_count % 100 == 0:
|
|
print(f"Frame {frame_count}, FPS: {fps:.1f}, Total Det: {total_detections}")
|
|
|
|
# Rate limiting
|
|
proc_time = time.time() - loop_start
|
|
if proc_time < frame_time:
|
|
time.sleep(frame_time - proc_time)
|
|
|
|
except KeyboardInterrupt:
|
|
print("\nStopping...")
|
|
finally:
|
|
elapsed = time.time() - start
|
|
print(f"\nProcessed {frame_count} frames in {elapsed:.1f}s")
|
|
print(f"Average FPS: {frame_count/elapsed:.1f}")
|
|
print(f"Total detections: {total_detections}")
|
|
cap.release()
|
|
ffmpeg.stdin.close()
|
|
ffmpeg.wait()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|