☰
YOLO+Python+RTSP:实时视频流目标检测工程实践与避坑
2026/10/9 6:33:39 网站建设 项目流程

简介:这是一套面向 Python 开发者的实时视频流目标检测项目,基于 OpenCV DNN 模块与 YOLO 模型,实现从 RTSP 协议拉取视频流并完成对象识别。适用于视频监控、智能安防、交通场景分析等方向,也适合需要快速上手 YOLO/SSD/Faster R-CNN 等预训练模型推理流程的初中级开发者。资源压缩包约 56.34MB,共 14 个文件,以 Python 检测脚本、两套 YOLO 的 cfg 模型配置、依赖说明、示例交通视频、README 文档,以及 XML 项目配置和许可证文件为主,目录结构清晰,便于对照学习。借助 OpenCV DNN 模块,项目可加载来自 Caffe、TensorFlow 等框架的预训练模型,无需单独搭建深度学习训练环境,即可完成检测推理;检测结果还支持按日期和类别自动存储,便于后续扩充数据集或接入人脸识别流程。目前已有 6514 人学习,适合希望用 Python 快速跑通 RTSP 实时目标检测并在此基础上做二次开发的开发者。

1. yolo-python-rtsp 这条路:本地图片检测跑通了,一到摄像头就傻眼

很多从业者第一次把 yolo-python-rtsp 放到一起搜,场景几乎都一样:本地 YOLO 检测单图跑通了,想把模型接到厂区、门店或园区的实时监控画面上。可一旦输入从 JPG 换成 RTSP 摄像头流,问题就成串冒出来——延迟涨到十几秒、偶发花屏、解码线程卡死。

我顺着「RTSP 怎么拉、OpenCV 怎么解、YOLO 怎么推」这条链路,把这类方案的工程决策讲透:参数怎么调、线程怎么拆、主码流子码流怎么选、多路怎么规划。适合已经跑通本地 YOLO 检测、准备接真实摄像头的人。

下面从最小可跑链路讲起,一路到多路并发与延迟验证收尾,代码可以直接照着改。

2. 搭最小可跑链路:OpenCV 拉 RTSP 流、YOLO 出检测框

2.1 为什么 OpenCV 的 VideoCapture 能直接吃 RTSP 地址

RTSP 全称 Real Time Streaming Protocol,它本身只负责会话控制——协商传输方式、请求哪一路流、告诉服务器暂停还是继续。真正的音视频数据走 RTP,RTSP 只负责「安排」。OpenCV 的 VideoCapture 底层接了 FFmpeg,而 FFmpeg 把 RTSP/RTP 的握手、解码、重采样全部封装好了。对使用者来说,RTSP 流和本地视频文件没有本质区别:都是一个 read() 拿一帧 BGR 图像。

这里有个大家容易忽略的点:RTSP 地址本质上是一个 URL,用户名、密码、端口、流通道全在字符串里。以最常见的海康摄像头为例,RTSP 取流地址长这样:

rtsp://admin:your_password@192.168.1.64:554/Streaming/Channels/101

后面 Channels/101 里,1 是通道号,01 是主码流;102 就是子码流。大华的格式不太一样:rtsp://user:pass@ip:554/cam/realmonitor?channel=1&subtype=0,subtype=0 是主码流、1 是子码流。这个格式差异特别容易翻车——网上能找到的取流地址模板五花八门,能用的直接拷,不能用的就得去厂商 SDK 文档里翻。

提到主码流和子码流,很多人第一次开的就是主码流,因为清晰。但主码流通常是 1080p 甚至 4K、码率 4~8 Mbps,而 YOLO 推理端最后要把每一帧缩到 640 再送网络,分辨率优势在检测上很有限,还白占带宽和解码开销。所以检测场景我一般优先看子码流,具体决策放到 4.3 展开。

2.2 最小代码:拉流 + YOLO 检测 + 画框显示

先把最小闭环跑起来再谈优化。调试期我习惯先拿公开的 RTSP 测试流把链路跑通,再换成现场摄像头,能省掉大量环境排查。下面这段用 OpenCV DNN 加载 YOLOv5s 的 ONNX 模型,从 RTSP 拉帧、推理、NMS 后处理、显示:

import cv2 import numpy as np # 加载 YOLOv5s 的 ONNX 模型,输入 640x640,3 个输出层 net = cv2.dnn.readNet("yolov5s.onnx") out_names = net.getUnconnectedOutLayersNames() rtsp_url = "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/102" cap = cv2.VideoCapture(rtsp_url) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 缓冲只留 1 帧,防止延迟积压 if not cap.isOpened(): raise RuntimeError("RTSP 流打开失败,先排查地址、端口、用户名密码") # coco80 类别名,按 coco.names 里的顺序读取 with open("coco.names") as f: coco_classes = [line.strip() for line in f.readlines()] while True: ret, frame = cap.read() if not ret: print("读帧失败,进入重连逻辑") break h, w = frame.shape[:2] # 预处理:归一化到 0~1、缩放 640、转 RGB 顺序,crop=False 保持比例 blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (640, 640), swapRB=True, crop=False) net.setInput(blob) preds = net.forward(out_names) # preds 是 list,每个元素对应一个输出层 boxes, scores, class_ids = [], [], [] for pred in preds: for det in pred: cls_scores = det[5:] # 后 80 个是 COCO 类别分数 cls_id = int(np.argmax(cls_scores)) conf = float(cls_scores[cls_id]) if conf < 0.25: continue cx, cy, bw, bh = det[:4] # 中心点坐标 + 宽高,单位是 640 输入像素 x1 = int((cx - bw / 2) * w / 640) # 换算回原图坐标 y1 = int((cy - bh / 2) * h / 640) x2 = int((cx + bw / 2) * w / 640) y2 = int((cy + bh / 2) * h / 640) boxes.append([x1, y1, x2 - x1, y2 - y1]) scores.append(conf) class_ids.append(cls_id) idxs = cv2.dnn.NMSBoxes(boxes, scores, 0.25, 0.45) if len(idxs) > 0: for i in idxs.flatten(): x, y, bw, bh = boxes[i] label = f"{coco_classes[class_ids[i]]} {scores[i]:.2f}" cv2.rectangle(frame, (x, y), (x + bw, y + bh), (0, 255, 0), 2) cv2.putText(frame, label, (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imshow("yolo-python-rtsp", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

逻辑说明:blobFromImage 这一步把 BGR 帧归一化并 resize 到 640x640。YOLOv5 训练时的预处理是 letterbox(等比例缩放加灰边),上面代码为了可读性做成了直接拉伸,坐标映射用的是简单比例换算。画面比例不是 1:1 时,直接拉伸会让目标变形、检测框偏移。要做得严谨,预处理里做 letterbox,后处理时按灰边偏移量还原坐标,这个细节在第 5 章还会提到。

参数说明:det 的前 4 个值是 YOLO 输出的中心点 (cx, cy) 和宽高 (bw, bh),后 80 个是 COCO 类别分数。COCO 80 类怎么读:class_id 按 coco.names 的行号对应类别名,0 是 person,1 是 bicycle,2 是 car。NMSBoxes 的三个参数分别是「传给 NMS 的最低置信度 0.25」和「IoU 阈值 0.45」。监控场景误检多就先抬高置信度,目标重叠严重才动 IoU,这个在 4.2 细讲。

2.3 不调这三个参数,延迟会悄悄涨

最小代码能跑之后,先别急着加业务逻辑,把延迟相关的三个参数设了。

第一个是 CAP_PROP_BUFFERSIZE。VideoCapture 内部有解码缓冲,默认情况下 FFmpeg 会把解码出来的帧先堆在缓冲里。网络好、解码快时,缓冲越堆越多,read() 拿到的永远是排队排了很久的旧帧,检测结果正确但画面严重滞后。典型现象是延迟从 1 秒慢慢涨到 10 秒以上。处理方式就是像上面那样把 BUFFERSIZE 设成 1,或者在循环里连续读帧、把缓冲清空到只剩最新一帧。

第二个是传输协议。默认情况下 FFmpeg 拉 RTSP 走 UDP,UDP 在无线网络里丢包高,表现为花屏、卡顿、断流。可以强制走 TCP:

export OPENCV_FFMPEG_CAPTURE_OPTIONS="rtsp_transport;tcp"

这个环境变量要在 Python 进程启动前设置,在代码里用 os.environ 设置也可以,但必须发生在 VideoCapture 构造之前。有线局域网丢包不严重时 UDP 延迟更低,无线环境建议直接用 TCP,少很多玄学问题。

第三个是 read() 前一定要确认 isOpened()。RTSP 握手失败时 isOpened() 返回 False,但有些摄像头在弱网下握手成功、中途断流,之后 read() 持续返回 False。这种半死状态用 isOpened() 查不出来,后面重连逻辑要单独处理。

刚接触这条链路的人,最容易把单帧推理时间当成端到端延迟。单帧推理 20 毫秒不代表画面延迟 20 毫秒——从摄像头出帧、网络传输、解码缓冲、推理、显示,每一段都有积压,问题几乎都出在缓冲和网络,不在推理。排查延迟的时候先量 RTSP 本身延迟(第 6 章给方法),再谈优化 YOLO。

3. 单线程拉流为什么卡:缓冲、跳帧与三线程架构

3.1 卡顿根源:解码阻塞与队列积压

把第 2 章的最小代码直接接到实时监控上,很快会碰到一个现象:画面偶尔顿一下,然后突然跳一大截;或者延迟稳步上涨。原因在于 read() 和推理是串行的——read() 要从网络收包、解一帧 H.264/H.265,耗时可能 10~80 毫秒;推理也占 20~100 毫秒。两者相加,摄像头 25 帧每秒时单线程根本跑不满,帧之间越积越多。

更重要的是网络抖动。RTSP 的 RTP 包在弱网下会乱序、重传、丢失,某次 read() 可能突然阻塞几百毫秒甚至几秒,整条链路卡住,后续帧全部堆在解码缓冲里。等网络恢复,read() 开始疯狂吐帧,推理线程追不上,延迟就爆炸了。

这是单线程架构的天然缺陷:拉流、解码、推理、显示互相拖累,任何一环出问题都会传导到其他环节。常见做法是拆线程:抓帧线程只管从 RTSP 拉帧放进队列;推理线程从队列取帧做 YOLO;显示或推流再拆一个。线程之间用队列解耦,抓帧慢不阻塞推理,推理慢不阻塞抓帧。

3.2 抓帧线程 + 推理线程:Python 里怎么拆

用 Python 的 threading 模块就能拆出抓帧和推理两个线程,核心思路三条:

  1. 抓帧线程无条件循环 read(),发现队列满就丢旧帧,保证队列里永远是最新的帧;
  2. 推理线程从队列取帧,处理完把结果画到帧上或放进结果队列;
  3. 队列 maxsize 要小,配合超时防止死等。
import threading import queue import time import cv2 frame_queue = queue.Queue(maxsize=2) # 队列只留 2 帧,丢旧保新 stop_event = threading.Event() def capture_worker(rtsp_url): cap = cv2.VideoCapture(rtsp_url) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) while not stop_event.is_set(): ret, frame = cap.read() if not ret: time.sleep(1) # 断流后休眠,避免空转烧 CPU cap.open(rtsp_url) # 重连 continue try: frame_queue.get_nowait() # 先丢一帧旧的,保证实时性 except queue.Empty: pass frame_queue.put(frame) cap.release() def inference_worker(): net = cv2.dnn.readNet("yolov5s.onnx") # 模型在推理线程内初始化 while not stop_event.is_set(): try: frame = frame_queue.get(timeout=0.5) except queue.Empty: continue blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (640, 640), swapRB=True) net.setInput(blob) outs = net.forward() # 后处理与画框,逻辑和第 2 章一致,此处省略 t1 = threading.Thread(target=capture_worker, args=("rtsp://...",)) t2 = threading.Thread(target=inference_worker, daemon=True) t1.start() t2.start()

逻辑说明:队列 maxsize=2 意味着推理慢时,抓帧线程每一次 put 都伴随着丢帧,这其实就是「跳帧」策略的一部分——推理线程 1 帧需要 50 毫秒,每秒最多处理 20 帧,多抓的帧只会推高延迟。队列越小实时性越好,代价是目标运动太快时可能恰好被跳过。监控场景宁可偶尔丢帧,也不要让画面变成慢动作回放。

参数说明:get(timeout=0.5) 防止推理线程在队列长时间为空时阻塞死;stop_event 配合 daemon 线程,主进程退出时不用手工 join,避免僵尸线程把 RTSP 连接一直挂着。模型初始化放线程内部而不是全局,是为了避免跨线程共享同一个 DNN 网络对象出现状态错乱,这是 OpenCV DNN 的已知边界。

3.3 跳帧策略:检测多少帧、丢多少帧

跳帧要解决的是「25 帧输入、10 帧推理能力」的错配。三种常见做法:

第一种是固定间隔,frame_count % N == 0 才检测。N=2 就是每两帧检一次,简单直观,但摄像头帧率不恒定时实际检测频率也跟着变。

第二种是时间间隔,按上次检测时间戳决定,超过 interval 才检测。比如目标每 100 毫秒检一次,不依赖帧率,适合帧率不恒定的无线摄像头。

第三种是队列阈值,就是 3.2 里「队列满就丢旧」的做法,本质上由推理速度自适应决定检测频率。我一般以第三种为主,再叠加一个最长时间间隔兜底,防止推理线程偶然变慢导致长时间没有检测输出:

last_detect = 0.0 MAX_DETECT_INTERVAL = 0.2 # 兜底:200 毫秒内必须出一次检测结果 def should_detect(): global last_detect now = time.time() if now - last_detect >= MAX_DETECT_INTERVAL: last_detect = now return True return False while True: ret, frame = cap.read() if not ret: continue if should_detect(): result = infer(frame) # 推理画框,未到间隔的帧直接丢弃

跳帧还有个隐藏好处:推理间隔内目标已经移动了,你检测到的是旧位置,但如果间隔控制在 200 毫秒以内,1080p 下移动目标在画面里的偏移通常只有几个像素,对告警、计数这类业务完全够用。真正要求逐帧检测的是车辆测速这类应用,那种场景要把推理速度优化到 30 FPS 以上,光靠跳帧解决不了。先确认业务能不能接受跳帧:能接受就用轻量方案,不能接受就直接上 TensorRT,这个选型决策越早做越省事。

4. 模型与码流选择:YOLOv5/YOLOv8 尺寸、阈值和主/子码流

4.1 模型从 n 到 x:1080p 25 帧下怎么选

YOLO 现在很多时候指的不止一个模型,是一个模型家族。YOLOv5、YOLOv8 各有 n/s/m/l/x 五个尺寸,n 是 nano,x 是 extra large。选型要同时看硬件和帧率要求。先给一个经验基线(单张 T4 或同级别 GPU、输入 640、TensorRT INT8 量化后):

模型参数量(约)单帧推理耗时量级适合场景
YOLOv8n3.2M3~6 ms多路并发、嵌入式设备
YOLOv8s11.2M6~12 ms单路高清告警
YOLOv8m25.9M12~20 ms高精度离线分析
YOLOv8l/x40M+20 ms+离线视频分析

经常能在检索里看到「T4 跑 1080p 25 帧用 TensorRT YOLO 640 分辨率能支持多少路」这类问题。按上面的耗时反推:单张 T4 跑 YOLOv8s INT8,单帧 8 毫秒左右,理论 125 FPS,除以每路 25 FPS,能扛 4~5 路,留 30% 余量按 3 路规划。换 YOLOv8n 可以到 6 路以上。这个估算没算解码和画框开销,上线前一定要用自己机器实测。

模型结构本身这两年也在迭代,efficient head 这类优化主要动的是检测头,把冗余卷积裁掉,同等精度下速度能快不少,适合监控这种高频检测场景。但这类结构改动通常和具体训练框架绑定,迁移成本不低,工程上先跑标准模型,压不住再考虑换结构。

COCO 80 类是另一个选型困惑点。监控场景很多根本用不到 80 类里的「吹风机、香蕉」,类别多意味着输出头大、误检面广。常见做法是用自己数据训练一个 5~10 类的轻量模型,或者用 ultralytics 预训练权重做迁移。类别越少,输出矩阵越小,NMS 耗时越少,对低算力设备是白捡的性能优化。

4.2 置信度阈值与 NMS 参数:框多、误检多时先调什么

置信度阈值 conf_thres 决定多低的分数的框被保留。设太低比如 0.1,画面上到处都是框,NMS 要处理大量无效候选,速度变慢;设太高比如 0.7,漏检开始出现——人只有半身出画时分数通常不高,直接就被滤掉了。

NMS 的 IoU 阈值决定重叠到多少算同一个目标。默认 0.45 在人群密集场景下容易把相邻的两个人并成一个框,可以降到 0.35 试。反过来,同一个目标上反复出现两个重叠框,说明 IoU 阈值偏低,往上抬到 0.5。

监控场景我一般这么起步:conf_thres=0.3,iou=0.45,然后拿一段 10 分钟真实监控视频做离线测试,统计每一帧的检测框数量。框数量异常偏多的时段放出来看,基本能判断是阈值问题还是训练数据问题。有个血泪经验:不要一看到误检就调阈值,先看误检是不是集中在某一类——很多误检是因为训练集里那一类样本太少,阈值越调越歪,真正解法是补数据重训。

YOLO 的损失函数也会影响这个决策。YOLOv8 的损失里包含分类损失和回归损失,权重默认对小目标不友好;检测场景里小目标占比高时,训练阶段把 box 损失权重调高一些,比上线后硬压阈值更有效。模型参数在推理时改不了,只能在训练时调,这一点经常被忽略。

4.3 主码流还是子码流:海康和大华的 RTSP 地址怎么读

回到 RTSP 地址本身。海康威视摄像头的标准取流格式是:

rtsp://用户名:密码@IP地址:554/Streaming/Channels/101

101 第一位是通道号(多目摄像头有 101/201),后两位 01 是主码流、02 是子码流。大华是 query 参数风格:

rtsp://用户名:密码@IP地址:554/cam/realmonitor?channel=1&subtype=0

subtype=0 主码流,subtype=1 子码流。

选型建议:检测输入选子码流。原因很简单,检测模型接收 640x640 输入,主码流 1080p 解出来再缩到 640,真正有用的细节不多,带宽和解码开销却翻好几倍;子码流通常已经接近 640 级别,缩放损失小,码率低,多路并发时网卡压力小很多。

例外情况是鱼眼摄像头和拍大范围场景的枪机:子码流编码质量差,小目标(人、车)在子码流里会糊成一团,检测基本看不见。这种场景必须用主码流,并且建议配合 1280 输入尺寸的模型,不然「远端小目标」这个监控核心需求直接废掉。

还有一点容易踩:摄像头 RTSP 流默认编码可能是 H.265。OpenCV 的 FFmpeg 版本旧的话解 H.265 会报错或花屏。常见做法是去摄像头 web 管理后台把编码改成 H.264,或者换 4.6 以上版本的 OpenCV。这个问题在第 5 章细说。

5. 避坑排查:RTSP 拉流到 YOLO 检测的 5 个高频翻车点

5.1 画面延迟越跑越大,最后卡成幻灯片

现象:系统刚启动时延迟约 1 秒,运行 10 分钟后台延迟涨到 10 秒以上,画面像幻灯片,偶发完全卡住。

原因:内部解码帧缓冲无限积压,read() 消费速度低于解码生产速度,队列里堆的旧帧越来越多,拿到的永远是最旧的帧;另一种可能是推理线程阻塞,帧队列里没人消费。

解决:把 CAP_PROP_BUFFERSIZE 设为 1,并在循环里做「只留最新帧」的丢弃逻辑。然后优先排查推理侧,给推理加耗时打点,单帧推理超过 100 毫秒说明模型对硬件来说太大,换小模型或降输入尺寸。这个坑的典型特征是延迟随时间单调递增——只要延迟是单调涨而不是波动,90% 是缓冲问题,不是网络问题。

5.2 花屏、马赛克、偶发断流

现象:画面上出现绿色或彩色色块、大面积马赛克,持续几分钟后 read() 开始返回 False,重连才能恢复。

原因:UDP 丢包。RTSP 默认走 UDP,无线桥接、跨交换机弱网环境丢包率一高,H.264 参考帧丢失会导致解码器持续花屏,直到下一个关键帧(I 帧)到来才恢复。摄像头 GOP 设得越长,花屏持续越久。

解决:强制走 TCP,进程序启动前设置 OPENCV_FFMPEG_CAPTURE_OPTIONS="rtsp_transport;tcp"。如果 TCP 延迟不可接受,另一个做法是去摄像头后台把码率调低、GOP 缩短(I 帧间隔从 60 改到 30),让花屏自愈时间减半。注意:TCP 解决的是丢包导致的丢帧,解决不了摄像头本身编码故障——换一台摄像头出现同样花屏,说明问题在源头设备。

5.3 密码带特殊字符,URL 解析失败

现象:用户名或密码里有 @、#、:、/ 这些字符时,VideoCapture 打开失败,isOpened() 返回 False,但同一个地址在 VLC 里能播。

原因:RTSP URL 解析时 @ 后的第一个 @ 被当成用户密码分隔符,# 被当成锚点,后面的内容不参与请求。典型的「VLC 能播、代码不能播」。

解决:对用户名密码做 URL 编码,用 urllib.parse.quote 处理后再拼 URL:

from urllib.parse import quote user = quote("admin", safe="") pwd = quote("pass@word#1", safe="") url = f"rtsp://{user}:{pwd}@192.168.1.64:554/Streaming/Channels/102"

safe="" 表示所有特殊字符全部编码,%40 代表 @、%23 代表 #。这个做法也适用于密码里带中文的情况。上线前建议先写一个「RTSP 地址可达性测试」脚本,用 OpenCV 试连 5 秒,能省大量排查时间。

5.4 OpenCV 装好了却报 module 找不到,DNN 读模型报错

现象:pip install opencv-python 成功后 import cv2 报 ModuleNotFoundError;或者 readNet 加载 ONNX 时抛 cv2.error,提示 DNN 相关错误。Windows 上常见报错路径里带 C:\Users\appveyor...,那是源码编译版本留下的痕迹。

原因:opencv-python 和 opencv-contrib-python 是两个不同包,环境里两个都装过时,pip 可能装成了不带完整模块的精简版;还有 Python 版本和 OpenCV 版本不匹配的情况。DNN 模块在 opencv-python 里是带的,但某些嵌入式平台默认源里的老版本编译时没启用 DNN。

解决:先确认版本和 import 路径:

import cv2 print(cv2.__version__) # 4.6+ 对 ONNX 支持较好 print(cv2.__file__) # 确认 import 的是哪个路径下的包

如果路径指向系统 site-packages 而不是当前虚拟环境,说明环境混了,重建虚拟环境后统一装 opencv-python。读 ONNX 报 DNN 相关错误的,直接升级到 4.6 以上,或者在源码编译时打开 WITH_DNN 和 WITH_FFMPEG。

5.5 多路 RTSP 同时拉,CPU 内存暴涨、GPU 吃不满

现象:单路跑得好好的,加到 4 路以后 CPU 占用 100%,GPU 利用率只有 30%,系统越来越卡。

原因:多路解码是 CPU 密集型操作,YOLO 推理是 GPU 密集型。H.264 1080p 解码一路大约占 2~3 个 CPU 核,4 路就吃掉 8~12 核;推理只占 GPU 30%,说明 CPU 解码是瓶颈,模型没吃饱。

解决:把解码和推理分开规划。第一种是调用支持硬解码的 OpenCV 版本,在 NVIDIA 平台上用 GStreamer 后端加 nvdec 硬解,把解码从 CPU 挪到 GPU;第二种是用 FFmpeg 的 C 库做解码,Python 只做推理;第三种最省事——如果摄像头支持子码流,直接把输入切成子码流,解码开销降一半以上。内存暴涨的另一个隐蔽原因是每路都开一个 VideoCapture,FFmpeg 缓冲叠加,注意 2.3 说的 BUFFERSIZE,还要控制每路独立队列的 maxsize。

6. 进阶:多路 RTSP 并发检测与延迟验证方法

6.1 多路并发:线程池 + 每路独立状态

多路并发不建议每个摄像头一个 Python 进程。常见做法是线程池,每个摄像头一个抓帧线程、一个推理 worker,共享同一个 GPU 模型实例。GPU 上模型常驻,多路输入交替推理,吞吐往往比进程隔离更高。

每路维护自己的帧队列和断流计数,推理 worker 从各路队列轮流取帧;GPU 推理前把多路帧拼成一个 batch 一次前向,ultralytics 的 predict 可以直接喂 batch,做到「多路共享一次推理」。我在消费级 GPU 上跑过 4 路 1080p 子码流,每路 25 帧输入、实际检测 12~15 帧每秒,CPU 占用比每路一个进程降了大约一半。

6.2 端到端延迟怎么测

不能靠眼睛估计,分钟级延迟和亚秒级延迟对告警系统完全是两个体验。测量方法:在摄像头画面前放一台手机,屏幕显示毫秒级时钟,摄像头和推理结果显示窗口同时对准,拍照读两个时钟的差值,这个差值就是端到端延迟。

重复测 10 次取中位数,比取平均值更真实,因为网络抖动会拉高均值。正常的局域网加子码流加跳帧方案应该在 300~800 毫秒;超过 2 秒就回到 5.1 查缓冲。这个方法朴素但有效,上线前测一次并记录成基准,之后每次改模型、改码率都重测一遍。

6.3 一个收尾习惯:检测结果打时间戳留存

最后分享一个我多年的习惯:所有检测结果——画的框、类别、置信度、帧时间戳——都写进一个可检索的日志文件或 SQLite,而不是只显示在窗口里。出问题时就有后悔药可吃:把「帧时间戳 + 检测框 + 图帧路径」存下来,回放时能精确复现每一个异常检测,数据还能拿去做阈值调优的离线评估。

很多团队上线告警系统后最痛苦的不是模型不准,而是「准不准」没法量化,因为推理日志没有留存。没有数据,调阈值全靠感觉,这是最烧钱的做法。希望这个习惯对你有帮助,也希望这套 yolo-python-rtsp 链路能让你少翻几次车。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询