简介:面向需要构建实时视频分析系统的开发者,这份YOLOv8基于RTSP流的目标检测资源包,提供了从视频流接入、图像预处理、模型推理到结果可视化的完整可运行方案,并覆盖环境配置与部署运行的关键细节。借助YOLOAPI工具与YAML配置文件,可快速将预训练权重部署到视频帧处理流程中,适用于视频监控、智能交通、工业质检等场景。资源共467个文件,以Python源码(py/pyc)、YAML配置、pt模型权重为主,辅以示例图像、mp4演示视频和Shell部署脚本,并包含浏览器端交互页面,压缩包约169MB。已有220人学习,适合具备一定Python和深度学习基础的开发者用于项目集成、二次开发和算法调试。借助其中的人脸、行人等测试样例,可直观验证检测效果,并深入理解RTSP拉流、帧预处理、推理、结果标注等关键环节的实现细节。
1. YOLOv8 接 RTSP 流目标检测:核心不在模型,在“把流喂进去”这一公里
把安防摄像头、工业相机输出的 RTSP 视频流,直接交给 YOLOv8 做实时目标检测,是巡检、闸口、车间安全监控里的刚需场景:摄像头早就装好了,网络也通,缺的就是一个能把画面变成“框和类别”的服务。但这个方案里最折磨人的往往不是 YOLO 本身,而是“拉流”那一步——很多人十分钟就写好了检测代码,却在 OpenCV 打开 RTSP 地址后翻车,画面上不是花屏就是延迟半分钟,再不然就是断流后程序直接卡死。下面按我实际落地时的顺序,把 RTSP 拉流、YOLOv8 推理、参数调节和踩坑点讲透。适合已经在图片上跑通过 YOLOv8、现在想把摄像头接进来的工程师,也适合准备在边缘设备上部署实时检测的团队。
2. 从 RTSP 到帧:拉流协议怎么选,为什么直接喂 YOLOv8 而不是先转 FLV
2.1 RTSP 流的本质:URL 背后是 SDP、RTP 与解码器三件事
RTSP 拉流协议这个词,很多教程一句话带过,但实际排障时你得把它拆开看。RTSP 自己并不搬运视频数据,它只负责“协商”:客户端向摄像头 554 端口发 DESCRIBE、SETUP、PLAY,服务器把编码格式、分辨率、传输方式这些写进 SDP 描述返回。协商完成之后,真正传画面的是一路 RTP 包,解码器再把它还原成 H.264 或 H.265 帧。
| 层次 | 干什么 | 排障看什么 |
|---|---|---|
| RTSP 控制层 | 发指令、定参数、控制播放状态 | 地址和端口通不通、认证过不过 |
| RTP 传输层 | 承载视频数据,可分包传输 | 丢包率、TCP/UDP 选择 |
| SDP 描述层 | 说明编码格式、分辨率、帧率 | 摄像头是 H.264 还是 H.265 |
OpenCV 的VideoCapture把这几个步骤压成一行代码,代价是你很难知道它卡在哪一步。比如摄像头是 H.265,OpenCV 自带的 ffmpeg 后端解不了,于是画面全花;比如中途网络抖动丢了一个关键帧,解码器就卡在“等下一个 I 帧”,表现出来是画面冻结。所以后面所有坑,本质上都落在这三层里。
2.2 OpenCV 打开 RTSP 地址:CAP_FFMPEG、TCP/UDP 与缓冲区参数
最常见的做法是直接用 OpenCV 拉流,代码量最少,前提是装对了带 ffmpeg 的版本。下面这段是打开海康摄像头主码流的经典写法:
import cv2 rtsp_url = "rtsp://username:password@192.168.1.64:554/Streaming/Channels/101" cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 关键:防止帧缓冲堆积 cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 3000) # 3 秒打不开就报错 cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 3000) # 3 秒读不到就超时 if not cap.isOpened(): print("拉流失败,先用 VLC 验证这个地址能不能播") exit()逻辑上,CAP_FFMPEG显式指定用 ffmpeg 作为解码后端;接下来三个cap.set分别处理缓冲、连接超时和读取超时。其中CAP_PROP_BUFFERSIZE这个参数有点玄学,不同版本的 OpenCV 实现不一样,有的设了立刻生效,有的完全没反应,但设成 1 在很多场景下确实能缓解延迟堆积,值得先写上。
还有个容易忽略的选型点:RTSP 默认走 UDP,延迟低但容易丢包;跨网段或过防火墙时要用 TCP。纯 OpenCV 后端不一定让你自由切换传输协议,真要强制 TCP,得换 GStreamer 管道:
pipeline = ( "rtspsrc location=rtsp://user:pass@192.168.1.64:554/Streaming/Channels/101 " "protocols=tcp latency=0 ! " "rtph264depay ! h264parse ! avdec_h264 ! " "videoconvert ! video/x-raw,format=BGR ! appsink" ) cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)protocols=tcp是强制走 TCP 的关键参数;latency=0让 GStreamer 不要做额外的缓冲,延迟能少几十毫秒。注意这条管道默认按 H.264 处理,摄像头是 H.265 时要换成rtph265depay和avdec_h265。
2.3 为什么不把 RTSP 先转成 FLV 再喂给模型
很多人在网上搜到的是“RTSP 转 FLV / RTMP 给前端播放”这条路线,然后顺手把 FLV 流再接进检测程序,这是把两条完全不同的链路混在一起了。RTSP 转 FLV 是给浏览器播放用的,因为浏览器原生不能直接播 RTSP;而检测程序需要的只是“帧”,RTSP 本身就是高效可信的帧来源。中间多插一个转码进程,等于多一级缓冲、多一个故障点,延迟至少增加一两帧。
正确解耦方式是:检测端直连摄像头 RTSP 取帧;检测结果如果还要给其他端看,再单独起一个本地 RTSP 服务器,把画好框的画面编码后推出去。至于“前端浏览器播放 RTSP”,那是另一个前端工程问题,放到第 5 章一起说。
3. 在 Ubuntu 20.04 上用 YOLOv8 跑通 RTSP 实时检测:最小工程与参数拆解
3.1 环境准备:CPU 版本也能跑,先确认 ffmpeg 后端在不在
Ubuntu 20.04 上搭 YOLOv8 环境,CPU 版本也能跑,关键是确认 OpenCV 的 ffmpeg 后端可用。很多人在 conda 里装的上古 OpenCV 不带 RTSP 支持,拉流一直失败,代码怎么写都没用。先把基础环境装好:
sudo apt update sudo apt install -y python3-pip ffmpeg pip install ultralytics opencv-python python3 -c "import cv2; print(cv2.getBuildInformation())"最后一行命令会输出一大段编译信息,重点看FFMPEG: YES还是NO。如果是 NO,当前 OpenCV 根本打不开 RTSP 地址,别在这个环境上浪费时间,换 pip 的 opencv-python 重装。CPU 推理速度可以参考:yolov8n 模型在常见桌面 CPU 上,640 分辨率输入大约每帧 200 到 300 毫秒。这个速度做图片检测绰绰有余,做实时流就得靠后面说的跳帧策略。
如果官方预训练权重不够用,想换自己的数据集,推理代码不用改,只把YOLO("yolov8n.pt")换成训练出来的.pt文件即可。数据标注用 LabelImg、LabelMe 这类工具都行,标注格式转成 YOLO 的 txt 再训练,这是独立的一套流程,先不展开。
3.2 最小可运行工程:拉流、推理、画框、显示
下面这一段是能直接跑起来的最小工程,逻辑很简单:不断从 RTSP 拉到帧,每隔几帧推理一次,把结果画在画面上显示。
import cv2 from ultralytics import YOLO model = YOLO("yolov8n.pt") rtsp_url = "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4" cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) if not cap.isOpened(): raise SystemExit("RTSP 拉流失败,请先确认地址可用") cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) frame_idx = 0 while True: ret, frame = cap.read() if not ret: break frame_idx += 1 # 每 3 帧推理一次,25fps 源相当于约 8fps 的检测输出 if frame_idx % 3 != 0: continue results = model.predict(frame, conf=0.25, imgsz=640, verbose=False) annotated = results[0].plot() cv2.imshow("yolov8-rtsp", annotated) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()代码逻辑上是三件事:cap.read()取一帧;model.predict()推理并返回结果对象;results[0].plot()把检测框和标签画回 BGR 图像,然后显示。verbose=False是关掉模型的逐帧日志,否则终端会被刷屏。
两个最容易理解错的点:第一,waitKey(1)里的参数是 1 不是 0,写成 0 会让界面等待按键,实时显示就会卡住;第二,frame_idx % 3是跳帧策略的雏形,25fps 的源每 3 帧处理一次,检测输出大约 8fps,CPU 机器也能跟上。公网测试流可能受网络环境影响,生产环境换成你自己的摄像头地址。
3.3 四个关键参数:conf、imgsz、buffersize 与跳帧数
这几个参数决定了系统能不能长期跑,单独拿出来说。
| 参数 | 作用 | 我的常用值 |
|---|---|---|
| conf | 置信度阈值,低于此值不显示 | 0.25 起步;漏检多降到 0.15,误报多抬到 0.4 |
| imgsz | 推理输入分辨率 | 640 通用;远距离小目标用 1280,但延迟翻倍 |
| buffersize | OpenCV 内部帧缓冲 | 1,防止延迟堆积 |
| 跳帧 | 每 N 帧推理一次 | 源 25fps 时取 3 到 5 |
conf影响的是检测灵敏度。做闸口和做车间安全监控,标准完全不同:闸口离得近、目标大,0.4 都嫌低;车间里人要是有遮挡,就得降到 0.15 左右,代价是误报增多,后面再接业务过滤逻辑。imgsz对摄像头里的远距离小目标影响最大,YOLOv8 的 Anchor-Free head 对尺度变化比较宽容,但输入分辨率直接决定它能看清多远的小目标;从 640 换到 1280,推理时间基本翻倍,要权衡。
跳帧数是一个工程手感问题。我的习惯是先看单帧推理耗时,让“推理耗时 × 跳帧间隔 < 源帧间隔”留出余量。比如单帧 200ms,源 40ms 一帧,跳 5 帧意味着 5 × 40 = 200ms 才检测一次,刚好满负荷,那实际部署就跳 3 帧留缓冲。
4. RTSP 拉流 + YOLOv8 常见坑:延迟、花屏、断流与显存溢出
4.1 延迟越拉越大:几分钟后画面比现场慢几秒
现象是刚启动时检测正常,跑几分钟后画面里的动作越来越滞后,最后比现场慢好几秒。原因是 OpenCV 或底层 ffmpeg 在网络抖动时把帧先存进缓冲区,读取速度赶不上缓冲堆积,延迟就像滚雪球一样越来越大。
解决分两步。第一步是设CAP_PROP_BUFFERSIZE为 1,尽量让缓冲区不积压;第二步是主动丢帧,用grab()和retrieve()代替直接read():
# 丢弃旧帧,只取最新画面 for _ in range(5): cap.grab() ret, frame = cap.retrieve()grab()只从流里抓下一帧但不解出图像,连续抓几次再retrieve()取最新帧。这样即使解码速度跟不上源帧率,画面也是一直追着现场走,而不是追着缓冲队列走。
4.2 花屏或绿屏:VLC 能看,OpenCV 却解不出来
最典型的现象是同一个 RTSP 地址,VLC 播放一切正常,OpenCV 拉出来却是花屏、绿屏或直接黑屏。常见原因是摄像头输出的是 H.265,VLC 带完整的解码库能解,而 OpenCV 编译时带的 ffmpeg 后端要么没编 H.265 解码器,要么版本太老解不动。
解决方法是让摄像头输出 H.264,登录摄像头 Web 后台,把视频编码从 H.265 切到 H.264,重新取流通常立刻正常。如果摄像头不可配置或必须用 H.265,就换 GStreamer 管道,把解码器明确指定出来:
pipeline = ( "rtspsrc location=rtsp://user:pass@192.168.1.64:554/Streaming/Channels/101 " "latency=0 ! rtpjitterbuffer ! rtph265depay ! h265parse ! " "avdec_h265 ! videoconvert ! video/x-raw,format=BGR ! appsink" ) cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)rtpjitterbuffer是应对网络抖动的标准组件,插在rtspsrc之后能吃掉一部分乱序和抖动;avdec_h265指定用 GStreamer 的解码器,绕开 OpenCV 内置 ffmpeg。这个管道依赖 gst-plugins-bad 和 gst-libav,装系统时把这两个包一起装掉。
4.3 断流后程序卡死:read() 为什么不返回
这是整个方案里最坑的一个问题:摄像头断网、重启或网线松动后,cap.read()会永远阻塞在底层,检测线程被吊住,程序既不退出也不报错,看起来就像彻底死机。
原因是VideoCapture.read()内部在等待解码器输出,而 ffmpeg 在 RTSP 断流后没有按时返回错误,超时逻辑形同虚设。CAP_PROP_READ_TIMEOUT_MSEC这个参数一些新版 OpenCV 有效,另一些版本完全不生效,不能把它当唯一保障。可靠做法是给拉流套单独线程,用计数心跳检测断流:
import time last_ok_time = time.time() cap = cv2.VideoCapture(rtsp_url) while True: ret, frame = cap.read() if ret: last_ok_time = time.time() else: if time.time() - last_ok_time > 5: cap.release() time.sleep(2) cap = cv2.VideoCapture(rtsp_url) # 重建连接逻辑很简单:每次成功读到帧就更新心跳时间,超过 5 秒没读到任何帧就销毁旧的VideoCapture并重建。OpenCV 的 VideoCapture 一旦断流几乎不可能原地复活,重建是最省事的办法。
4.4 多路摄像头推理,显存和 CPU 直接爆掉
有人接两三个摄像头时图省事,每个摄像头写一个进程、每个进程加载一个模型,结果 6G 显存直接爆掉。原因不是推理本身有多吃显存,而是每路都复制了一份完整模型权重和中间张量缓存。
一个 yolov8n 模型在 640 输入、fp16 精度下,显存占用量级在 2GB 左右,具体看 batch 和框架版本。正确做法是全局只保留一个模型实例,多路摄像头共享它:
model = YOLO("yolov8n.pt") # 整个进程只创建一次 # 每路摄像头只做一件事:取帧 # 推理统一走同一个 model 对象CPU 环境同理。多路拉流的线程可以并行,因为它们耗的是 C 层解码,不受 GIL 限制;但 Python 里的多线程推理实际是串行的,所以正确结构是“拉流多线程,推理单线程排队”,而不是“每路一个完整检测线程”。
4.5 RTSP 地址一个字符不对,报错却完全不像地址错误
有一回我在大华摄像头上折腾了近半小时,报错一直是“无法打开摄像头”,最后发现是 URL 里的&被 Python 字符串转义吃掉了。海康和大华的 RTSP 地址格式不完全一样,最容易踩的就是参数分隔符和子码流编号。
海康格式是rtsp://user:pass@ip:554/Streaming/Channels/101,101是主码流,102是子码流;大华格式是rtsp://user:pass@ip:554/cam/realmonitor?channel=1&subtype=0,subtype=0是主码流、subtype=1是子码流。用户名密码里如果带@、:、&这类特殊字符,要先做 URL 转义:
from urllib.parse import quote user = quote("admin") password = quote("pass:word") url = f"rtsp://{user}:{password}@192.168.1.64:554/cam/realmonitor?channel=1&subtype=0"C# 的 OpenCvSharp 里同样存在这个问题,地址里的反斜杠、&在字符串里都要处理。我踩过类似坑之后的习惯是:任何 RTSP 地址先粘到 VLC 里放一遍,播放正常再写进代码。VLC 是验证“地址本身对不对”的最快工具,它能播说明网络、认证、编码都没问题,剩下就是代码层参数的事。
5. 从单路演示到可用的检测系统:线程分离、跳帧策略与多路摄像头接入
5.1 拉流线程与推理线程分离:只留最新帧,丢旧帧
单线程做“拉流 + 推理”在 CPU 机器上很难兼顾,拉流等待解码时推理闲着,推理占用 CPU 时拉流又来不及取帧。常见做法是拆成两个线程:拉流线程只负责read()并把最新帧丢进一个容器,推理线程只负责从容器里取帧推理。
import threading import collections import time latest = collections.deque(maxlen=1) last_ok_time = time.time() def reader(cap): global last_ok_time while True: ret, frame = cap.read() if ret: latest.append(frame) last_ok_time = time.time() # 主线程里 while True: if not latest: time.sleep(0.005) continue frame = latest[-1] if time.time() - last_ok_time > 5: print("断流超过 5 秒,等待重建") break results = model(frame, conf=0.25, imgsz=640, verbose=False) annotated = results[0].plot()deque(maxlen=1)是这个方案的核心:新帧进来自动顶掉旧帧,推理线程任何时候拿到的都是最新画面,天然实现“丢旧帧”。断流检测也放在了主循环里,一旦超过 5 秒没有新帧进来,就走重建逻辑。这个结构跑上一整天,延迟都不会因为缓冲堆积而增长。
5.2 多路接入:每路一个拉流线程,共用同一个模型实例
接多路摄像头时,按路数启动多个 reader 线程,每个线程把帧写进各自的deque,推理循环轮询各路的最新帧,共用同一个模型对象。资源预算可以参考下面的经验值,注意这是量级参考,不是官方测速结果:
| 路数 | 源分辨率 | 建议推理间隔(源 25fps) | 模型 | 备注 |
|---|---|---|---|---|
| 2 路 | 1080p | 每 4 帧 | yolov8n | 消费级显卡可吃 |
| 4 路 | 720p | 每 5 帧 | yolov8n | 建议开 fp16 |
| 8 路 | 720p | 每 7 帧 | yolov8n | 考虑边缘设备,如 RK3588 转 rknn |
GTX 1660Ti 这档显卡跑 yolov8s,640 输入大约能达到 15fps 左右的推理速度,配跳帧方案带 4 路 720p 摄像头是够用的。如果换 yolov8n,能留出更多余量给画框和推流。
边缘部署是另一条分支:RK3588 这类板子不能直接跑 PyTorch,模型要转成 rknn 格式,拉流和解码也要换成板端 SDK 的接口,但“拉流线程丢帧、推理单线程排队、共享模型实例”这个框架不变,只是把推理引擎换掉。
5.3 把带框画面再推出:本地 RTSP 服务器与 rtsp 转发
检测结果只显示在本机是不够的,机房场景通常要把带框画面同时给监控室和 Web 端。常见做法是在机器上起一个轻量 RTSP 服务器,检测程序把画好框的帧编码后推给它,播放端再连上来。这其实就是 RTSP 转发:服务器负责收流和分发,检测程序只负责产流。
编码推流最皮实的方式是把裸帧通过管道喂给 ffmpeg:
ffmpeg -f rawvideo -pix_fmt bgr24 -s 1920x1080 -r 10 -i - \ -c:v libx264 -preset ultrafast -tune zerolatency \ -f rtsp rtsp://127.0.0.1:8554/livePython 侧只需要把画好框的帧写进子进程的 stdin:
import subprocess cmd = [ "ffmpeg", "-f", "rawvideo", "-pix_fmt", "bgr24", "-s", "1920x1080", "-r", "10", "-i", "-", "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency", "-f", "rtsp", "rtsp://127.0.0.1:8554/live" ] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) proc.stdin.write(annotated.tobytes())-preset ultrafast和-tune zerolatency两个参数是低延迟编码的关键,宁可在画质上牺牲一点,也要保证延迟不失控。播放端用 VLC 直接连rtsp://127.0.0.1:8554/live就能看到检测结果。如果播放端是浏览器,就在这个 RTSP 服务器上再开一路 FLV 输出,也就是前面说的 RTSP 转 FLV 路线,服务端做好分路,检测程序不用改。
6. 端到端延迟验证与选型建议:别相信“感觉快了”
6.1 用手机秒表实测端到端延迟
很多项目在演示时觉得“好像挺快”,一装到现场就被用户吐槽慢半拍,所以验收前要用客观方法量一次端到端延迟。我的土办法:把手机秒表放在摄像头正前方,让画面里能清楚看到数字跳动;再用一部手机同时拍电脑屏幕和秒表,截一帧对比画面上检测框叠加时间戳和秒表显示的时间差。反复测三次,结果参考这张表:
| 端到端延迟 | 判断 |
|---|---|
| 300ms 以内 | 本地直连 + GPU 推理,理想状态 |
| 300ms 到 800ms | 局域网 RTSP + CPU 推理,可接受 |
| 超过 1s | 有缓冲堆积或跳帧太小,必须排查 |
为了定位延迟是从哪一段起来的,我会在每一帧左上角打上推理耗时和队列深度:
cv2.putText(annotated, f"inf:{infer_ms:.0f}ms q:{len(latest)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)inf是模型推理耗时,q是当前积压的帧数。只要q一直大于 1,就说明推理速度跟不上拉流速度,优先调跳帧或降低输入分辨率,而不是换更贵的显卡。
我的习惯是,新项目先到摄像头后台把编码改成 H.264、用子码流跑通整条链路,再换主码流看画质。子码流的延迟低、带宽占用小,适合先把管线调通;主码流留给验收。每个 RTSP 地址我都先在 VLC 里验一遍才写进代码,这个习惯帮我省下了很多在黑匣子里瞎猜的时间。希望帮到你。
本文还有配套的精品资源,点击获取