简介这是一份面向视频流实时检测场景的YOLOOpenCVPython项目资源核心解决通过RTSP协议对网络摄像头或视频流进行目标识别的问题适合有一定Python基础的计算机视觉开发者参考。包内共14个文件以Python脚本、YOLO模型配置、说明文档及示例视频为主压缩包大小约56MB。已有6514人学习下载上手门槛适中。资源提供yolov3和yolov3-tiny两套配置附带commuters.mp4交管路口示例视频可快速跑通从RTSP拉流、模型推理到检测结果输出的完整流程同时可将识别到的目标按日期分类存储到对应文件夹便于后续扩展训练数据集或接入人脸识别任务整体结构清晰适合直接改造用于安防、交通、工业检测等场景。1. RTSP 拉流配 YOLO 检测为什么这是视频分析里最实用的组合做实时视频分析的开发者大概率都经历过这种阶段摄像机画面已经上墙了下一步要在 Python 里对画面做对象检测而厂商 SDK 要么绑定平台要么只给个播放器。RTSP 是网络摄像机几乎都支持的通用取流协议OpenCV 一行 VideoCapture 就能拉流YOLO 负责把每一帧里的目标框出来这个组合不需要引入视频服务器或流媒体网关几行代码就能把深度学习对象检测直接架到现有摄像头网络上。下面直接按 yolo-python-rtsp 这条链路展开从协议原理讲到断流重连把参数、延迟和踩坑一次性说透。适合正在做安防监控、作业区人员检测、车间违规行为识别这类实时检测需求的人也适合只想先接上开发机摄像头验证一下的入门者。2. 先把链路拆开RTSP、YOLO 与 OpenCV 各自负责什么用这个组合的第一件事不是写代码而是搞清楚三样东西各自管到哪一段否则出了问题都不知道该查哪一层。RTSP 管“怎么把画面拿过来”YOLO 管“画面里有什么”OpenCV 夹在中间做解码、预处理和绘制。每一层都有自己固定的坑先立住这几块再动手后面排错会快很多。2.1 RTSP 不是视频格式而是一套会话控制协议RTSPReal Time Streaming Protocol的角色更像一个遥控器客户端通过它发出 DESCRIBE、SETUP、PLAY、TEARDOWN 这些命令告诉设备“我要看哪条流、用什么传输方式、开始播还是停止播”。真正的视频画面数据通常是 H.264 或 H.265 编码的帧走的是 RTP 协议可以基于 UDP 也可以基于 TCP 传输。OpenCV 拉流时把这些全部封装在 VideoCapture 里你感知不到握手过程但必须知道两件事。第一RTSP URL 的路径是设备厂商自定义的同一台设备的主码流和子码流路径完全不同。我拿到一台新设备的第一动作永远是登录管理页找到“取流地址”或者“RTSP URL”那一栏而不是猜路径。第二RTSP 本身不提供加密鉴权靠 URL 里的用户名密码很多设备默认用 Digest 鉴权账密要正确拼进 URL。URL 的一般形式是 rtsp://user:password设备IP:554/路径端口常见 554。这里容易踩坑的是账密里若带 、:、/ 这类字符URL 会被解析错位第五章会给专门的处理方案。传输方式上很多客户端默认走 UDP因为实时性好、延迟低。但 UDP 丢包不重传画面一旦花屏就要等下一个关键帧才能恢复检测场景里这会导致检测框乱飞。我一般会强制走 TCP代价是延迟略高换来的是稳定的解码结果。这个设置后面代码里会用到。2.2 为什么视频流检测选 YOLO 而不是两阶段模型YOLO 属于一阶段检测器核心是把目标检测当成回归问题一张图一次前向推理同时输出所有候选框的坐标、宽高和类别置信度。相比两阶段方法“先提候选区域、再做分类”的流程YOLO 在同样算力下能跑出更高的帧率。这对实时流场景几乎是硬指标监控画面里一个目标从进入到离开往往只有几秒检测延迟每多几百毫秒告警就可能晚一步所以视频流检测的主流选择一直是 YOLO 这类一阶段模型。部署到 RTSP 链路上最常见的做法是把训练好的 YOLO 权重导出成 ONNX 格式再用 OpenCV 自带的 dnn 模块加载。这样推理机器上只需要 opencv-python不需要安装完整的深度学习训练框架依赖面小换机器也方便。模型选型要按算力来CPU 推理就用体积最小的 nano 档位输入 640 尺寸时单帧推理大概在几十毫秒量级有 GPU 再考虑更大档位的模型精度会明显好一些。注意YOLO 默认对每一帧独立检测没有跨帧关联所以目标被短暂遮挡时会出现漏报很多正式项目后面会再接跟踪模块做轨迹平滑那是另一个话题。如果需求只是“框出来并告警”单帧 YOLO 已经够用先别把方案复杂度抬上去。2.3 OpenCV 的 VideoCapture 拉流到底做了什么cv2.VideoCapture 依赖 FFmpeg 后端完成三件事与设备建立 RTSP 会话、持续接收 RTP 包并按 H.264/H.265 解码、把解码后的 YUV 帧转成 OpenCV 常用的 BGR 格式。cap.read() 是一个阻塞调用没有新帧就一直在等网络抖动时这个阻塞时间会明显拉长。这不是 bug而是底层库的真实行为做多线程时要去适应它而不是硬扛。另一个关键行为是 VideoCapture 内部通常有一小块帧缓冲。虽然默认缓冲不大但读取速度比消费速度快时缓冲里就会积攒旧帧表现出来就是画面越来越延迟。这也解释了为什么很多教程里推荐 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)把缓冲压到最小。个别编译版本不支持这个属性设置后用 cap.get(cv2.CAP_PROP_BUFFERSIZE) 确认一下返回还是 0 就说明没生效那就在代码层做丢帧策略。还有一点容易翻车OpenCV 里图像是 BGR 顺序而大多数 ONNX 模型训练时用的是 RGB喂给网络前必须做通道交换。dnn 模块的 blobFromImage 里 swapRBTrue 干的就是这件事。漏掉这个交换检测结果常出现颜色相关的误判或者干脆什么都检不到这是这条链路上最典型的黑匣子问题之一。3. 从零跑通 RTSP 对象检测最小脚本与参数设置原理立住之后先不要上多线程、重连这些工程化内容把一条最简单的链路跑通确认拉流、推理、画框每一步都正常再往上面加复杂度。这样出了问题能明确知道是自己代码的问题还是链路本身的问题。3.1 装环境与准备 ONNX 模型文件依赖非常简单pip install opencv-python numpyopencv-python 的预编译包自带 FFmpeg 解码能力不需要单独再装解码库。Python 版本建议 3.8 以上倒不是强制的只是低了会对 f-string 和类型注解支持不好。部署到无图形界面的服务器时要换装 opencv-python-headless因为普通版会试图加载 GUI 模块在服务器上会报错。模型文件方面我是先拿一个训练好的 YOLO 权重做验证导出成 ONNX 后放在项目目录下。导出这一步通常在你的训练环境里完成部署机只需要 .onnx 文件和类别名列表。类别顺序必须和训练时一致后处理里 argmax 出来的下标才能对应上正确类别这也是检测结果“类别全乱”的最常见原因之一。3.2 最小可用的 YOLO 检测脚本把拉流、预处理、推理、后处理串起来的完整脚本如下import cv2 import numpy as np RTSP_URL rtsp://user:password192.0.2.10:554/Streaming/Channels/101 MODEL_PATH yolov8n.onnx INPUT_SIZE 640 CONF_THRESHOLD 0.4 NMS_THRESHOLD 0.5 net cv2.dnn.readNetFromONNX(MODEL_PATH) def letterbox(img, size): # 保持宽高比缩放并补边目标居中放在 size x size 的画布上 h, w img.shape[:2] scale min(size / w, size / h) nw, nh int(w * scale), int(h * scale) resized cv2.resize(img, (nw, nh)) canvas np.full((size, size, 3), 114, dtypenp.uint8) x0 (size - nw) // 2 y0 (size - nh) // 2 canvas[y0:y0 nh, x0:x0 nw] resized return canvas, scale, x0, y0 def postprocess(output, scale, x0, y0): # 兼容 [1,84,8400] 和 [1,8400,84] 两种常见 ONNX 输出布局 if output.shape[1] output.shape[2]: output output.transpose(0, 2, 1) preds output[0] boxes, scores, labels [], [], [] for row in preds: cls_scores row[4:] score float(cls_scores.max()) if score CONF_THRESHOLD: continue cx, cy, w, h row[:4] # 坐标从 640 画布换算回原始画面先减补边再除缩放比 x1 (cx - w / 2 - x0) / scale y1 (cy - h / 2 - y0) / scale x2 (cx w / 2 - x0) / scale y2 (cy h / 2 - y0) / scale boxes.append([x1, y1, x2 - x1, y2 - y1]) scores.append(score) labels.append(int(cls_scores.argmax())) idxs cv2.dnn.NMSBoxes(boxes, scores, CONF_THRESHOLD, NMS_THRESHOLD) result [] if len(idxs) 0: for i in idxs.flatten(): result.append((boxes[i], scores[i], labels[i])) return result cap cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) while True: ok, frame cap.read() if not ok: # 一次性代码里先做简单重连工程化的做法见第四章 print(read failed, retry...) cap.release() cap cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) continue canvas, scale, x0, y0 letterbox(frame, INPUT_SIZE) blob cv2.dnn.blobFromImage( canvas, 1/255.0, (INPUT_SIZE, INPUT_SIZE), swapRBTrue ) net.setInput(blob) output net.forward() for box, score, label in postprocess(output, scale, x0, y0): x, y, w, h [int(v) for v in box] cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, fid{label} {score:.2f}, (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 调试时用 imshow 看效果服务器部署时把这段删掉 cv2.imshow(rtsp-yolo, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明拉流用 CAP_FFMPEG 显式指定后端letterbox 先等比缩放再补灰边126 行里 114 这个填充值是 YOLO 官方训练时常用的直接 resize 会破坏宽高比、把目标压变形blobFromImage 一次完成 BGR 转 RGB、像素归一化、布局转 CHW 三件事postprocess 里先对输出做了布局判断把常见的两种 ONNX 输出统一成“框加类别分数”的行序坐标换算回原始画面时要先减补边再除以缩放比最后用 NMSBoxes 抑制同一目标上的重复框。参数说明CONF_THRESHOLD 是类别分数阈值调低召回变多但误报也变多NMS_THRESHOLD 控制两个重叠框合并的激进程度INPUT_SIZE 直接决定推理耗时640 是速度和精度的常见平衡点。这几个参数下一节展开说。3.3 置信度、NMS 和输入尺寸三个必调参数参数常见取值调大调小CONF_THRESHOLD0.25 ~ 0.5误报少、漏报多漏报少、误报多NMS_THRESHOLD0.4 ~ 0.6重叠框合并少会一目标多框合并激进挨着的目标被吞INPUT_SIZE416 / 640精度好、耗时涨速度快、小目标易漏每个参数都得按实际画面调我一般把 conf 从 0.5 往下试直到误报数量让人能接受。比如车间通道检测里 0.35 效果就不错而室外广场树影晃动时得抬到 0.5 以上因为树叶边缘很容易被当作目标。NMS 阈值不建议低于 0.4否则两个人贴在一起走时会只输出一个框这在人员计数场景里会直接少算一个。除了这三个还有一个容易被忽略的跳帧参数如果推理速度跑不满码流帧率就每 2 帧处理 1 帧而不是让帧在内部缓冲里排队堆积。检测这类任务对单帧时效敏感处理不过来的帧直接丢弃比处理“过期的旧帧”有意义得多。第四章的队列设计正是围绕这个思路做的。4. 让它能 7×24 小时跑断流重连与多线程读取单线程脚本能跑通 demo但没人敢拿它做正式部署原因就两个read() 阻塞会卡住整个检测循环网络闪断一次程序就卡在等待里出不来。这一章的结构是真实项目里最常用的——读线程加定长队列加自动重连。4.1 拉流和推理为什么要拆到两个线程cap.read() 是网络 IO 加解码耗时很不稳定正常时二三十毫秒网络抖动时可能几百毫秒甚至抛错。如果让它和推理顺序执行推理节奏就被拉流拖垮反过来推理慢了接收端缓冲又越堆越多。两个线程中间夹一个定长队列读线程只负责“把最新帧放进队列”并自行处理断流重连推理线程只负责“从队列拿帧去算”互不阻塞。有人担心 Python 的 GIL 会让多线程白搭实际上 VideoCapture 的读和解码、dnn 的 forward 都在底层 C 层释放锁实测下来拉流场景的多线程收益非常明显。单线程里一次网络抖动会让整个检测停顿多线程里最多是队列空一下推理线程拿不到帧就空转下一帧来了继续干活。4.2 带自动重连的 RTSP 读取线程import cv2 import queue import threading import time class RtspReader: def __init__(self, url, maxsize4, reconnect_delay3.0): self.url url self.frames queue.Queue(maxsizemaxsize) self.reconnect_delay reconnect_delay self.running False self._thread None def _open(self): cap cv2.VideoCapture(self.url, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 强制走 TCP避免 UDP 丢包导致的花屏 cap.set(cv2.CAP_PROP_OPENCV_FFMPEG_CAPTURE_OPTIONS, rtsp_transport;tcp) return cap def _loop(self): while self.running: cap self._open() while self.running: ok, frame cap.read() if not ok: print(fread fail, reconnect after {self.reconnect_delay}s) cap.release() time.sleep(self.reconnect_delay) break if self.frames.full(): # 队列满时丢最旧帧保证消费到的一定是最新画面 try: self.frames.get_nowait() except queue.Empty: pass self.frames.put(frame) cap.release() def start(self): self.running True self._thread threading.Thread(targetself._loop, daemonTrue) self._thread.start() def read(self, timeout1.0): try: return True, self.frames.get(timeouttimeout) except queue.Empty: return False, None def stop(self): self.running False if self._thread: self._thread.join(timeout2.0)逻辑说明_open 每次重建 VideoCapture同时把所有连接强制到 TCP这是解决花屏最省事的一步内层循环遇到 read 失败就释放连接、sleep 固定秒数再重连避免高频重连把设备打挂外层 while 保证重连不是一次性行为设备恢复后能自动续上。消费端用带超时的 get超时返回 False 和空帧主线程不会因为读线程异常而卡死。参数说明maxsize 是队列容量按“延迟预算等于队列长度乘帧间隔”粗算25fps 码流里队列堆 10 帧画面就比现场慢了约 0.4 秒reconnect_delay 设 2 到 3 秒是比较稳的起点设太小会在设备还没释放旧连接时反复撞墙设太大则设备恢复后画面要等很久才回来。4.3 队列深度怎么定延迟与丢帧的取舍队列深度直接决定画面延迟这是把“实时性”从一个模糊感受变成可计算指标的关键延迟等于队列长度乘以帧间隔。有些项目要求告警延迟在一秒内队列 maxsize 给到 4 到 8 就足够吸收网络抖动如果业务不能接受丢帧比如要做连续轨迹分析那就不该丢旧帧而是把推理提速到跟得上码流否则只能在延迟和丢帧之间二选一。读取端丢旧帧的策略配合 CAP_PROP_BUFFERSIZE1能让消费端永远拿到“尽可能新”的帧这是监控类项目最重要的体验指标。下游接到这个读取线程后可以把结果转推 RTMP、写视频片段或直接触发 HTTP 告警读取模块保持独立换下游不用动拉流代码。这个分层思路也方便测试单独把帧落盘写视频确认拉流本身没问题再去排查检测逻辑。5. RTSP 检测避坑清单延迟、花屏、断流三座山这一章是我做过的监控项目里反复踩的坑每一条都曾让程序看起来“在跑”但结果完全不可用。先说排查结论这类问题八成不在模型而在拉流和解码层顺序永远是先看帧有没有进来、再看画面对不对、最后才怀疑检测。5.1 延迟越跑越大画面比现场慢半拍现象刚启动时检测框和现场基本同步跑十几分钟后画面明显滞后严重时延迟到 5 秒以上。原因读取速度高于推理速度时VideoCapture 内部缓冲和自建队列里不断堆积旧帧后处理看到的画面永远比现场慢。慢的根源不是网络而是“旧帧没有被丢弃一直被排队消费”。解决把 CAP_PROP_BUFFERSIZE 设为 1队列满时丢最旧帧如果 CPU 推理只能跑到 10fps 而码流是 25fps就显式跳帧处理宁可每 2 帧算 1 次也不要让帧在队列里排队老化。还有一个细节调试窗口 waitKey 里的延迟不计入业务延迟别把显示卡顿误判成链路卡顿。5.2 画面突然花屏绿屏UDP 丢包与关键帧失效现象画面随机出现马赛克块或整帧变绿持续几秒后自己恢复期间检测框乱飞甚至把噪点当成目标。原因默认的 UDP 传输丢包不重传而 H.264 里 P 帧依赖 I 帧做参考一旦参考帧数据损坏后面一连串帧都解不对直到下一个关键帧到达才恢复。绿屏本质是解码器用错误的参考数据重建了画面。解决在 _open 里加上 rtsp_transport;tcp实测花屏大幅减少还能在设备端把码率调低、关键帧间隔调短让坏帧自愈周期变短。如果换了 TCP 仍然花屏就去查交换机端口流量和网线别在代码里死磕。5.3 设备连接数超限RTSP 连接被踢现象程序运行几小时后 read 持续失败重连也失败甚至设备管理页面都打不开。原因绝大多数网络摄像机对单路码流限制并发取流数量常见是 2 到 4 路调试时每次崩溃没释放连接、或者重连逻辑 1 秒重试一次把设备连接占满后它就拒绝新连接。解决程序退出统一调用 stop 和 cap.release每路流在全局范围内只保留一个读取实例别为调试方便开多个窗口重连固定 sleep 2 到 3 秒而不是立即重试真被占满就等设备端旧连接超时释放一般几十秒内恢复。另外别把摄像机端口直接暴露到公网内网组网或加一层流媒体网关更稳妥取流地址和账密尽量不要硬编码在脚本里。5.4 账密里的特殊字符让 URL 鉴权失败现象取流地址看着没问题用通用播放器能播Python 里却一直 401 Unauthorized 或直接连接被拒。原因URL 里 、:、/ 这类字符在解析时有特殊含义账密里出现它们会把 URL 拆错最常见的就是密码里带了 。解决账密拼接前做百分号编码from urllib.parse import quote user admin password ab:c host 192.0.2.10 path /Streaming/Channels/101 url frtsp://{quote(user)}:{quote(password)}{host}:554{path} print(url) # rtsp://admin:a%40b%3Ac192.0.2.10:554/Streaming/Channels/101注意只对账密做 quote路径里的斜杠必须保持原样。编码后密码里的 变成 %40、冒号变成 %3A设备端解码回来就能正确鉴权。这个问题排查起来很隐蔽因为播放器通常会自动处理转义而 OpenCV 后端不会。6. 值得养成的习惯让检测结果反哺拉流与验证策略6.1 给检测循环加一个延迟探针正式部署别一上来就调模型先在检测循环里统计三个数实际处理帧率、队列当前深度、单帧推理耗时。每 30 秒打印一次跑一个晚上。判断逻辑很简单队列一直满说明算力跟不上去跳帧或换更小模型队列一直空说明读流有问题去看网络和重连日志推理耗时突然翻倍则要去查散热和降频。这个统计代码量很小但能把所谓“玄学”变成可量化数据。我之前一个项目就是靠它发现推理耗时从 30ms 涨到 80ms最后定位到是显卡被动降频跟模型和拉流毫无关系。6.2 用手机秒表验证整条链路延迟验证对象检测效果时最实用的土办法是在镜头前放手机打开秒表让检测程序画面同时拍清手机屏幕再对比程序显示画面里的秒数与真实秒数差距。这个差值就是端到端延迟包含拍摄、编码、传输、解码、推理、显示的全部时间。对大多数监控告警场景端到端 1 到 2 秒可接受超过 3 秒就回头检查缓冲和队列而不是怀疑模型。检测效果本身也要记录负样本把误报帧和漏报帧分别截图存档攒一批再去调置信度阈值而不是凭感觉改参数。我现在每个项目都保留这个探针和截图目录排查“检测不准”时先看延迟和队列再看错误样本多数问题在没到模型那层就被拦住了。这个习惯帮我省了很多半夜跑到现场排查的时间希望帮到你。本文还有配套的精品资源点击获取