☰
YOLOv3+OpenCV dnn实战:RTSP流目标检测与分类归档
2026/9/28 15:25:25 网站建设 项目流程

简介:这份资源面向具备一定 Python 基础、希望将深度学习目标检测落地到实时视频流的开发者与学习者,核心解决如何通过 RTSP 协议拉取网络摄像头视频流,并借助 YOLO、OpenCV 与 Python 完成实时对象检测的问题。包内共 14 个文件,涵盖 py 主程序脚本、cfg 与 txt 模型配置及类别文件、xml 工程配置、png 效果图、mp4 示例视频以及 md 说明与 license 等,压缩包约 56.34MB,结构紧凑,便于直接运行与二次修改。资源利用 OpenCV dnn 模块加载 YOLO/DarkNet 预训练模型进行推理,并将识别出的对象按日期分类存入对应类别文件夹,方便后续用于再训练或人脸识别等场景。目前已有 6514 人学习下载,适合想快速搭建 RTSP 实时检测流水线、理解模型配置与结果归档思路的读者参考。

1. 从一路 RTSP 流到分类归档:这套 yolo-python-rtsp 到底能干什么

手里有一台支持 RTSP 的摄像头,想让它自己认出画面里的人、车、背包,还要把识别结果按日期、按类别存成文件夹,方便后面拿去做二次训练或者人脸比对——这套yolo-python-rtsp-master就是干这个的。它把 YOLOv3 的 DarkNet 权重塞进 OpenCV 的 dnn 模块,用 Python 拉 RTSP 流逐帧推理,检测框画出来,同时把命中的目标裁剪下来,按日期/类别的目录结构落盘。整包不大,核心就一个yolo_opencv.py,加上cfg目录里的yolov3.txt、yolov3.cfg、yolov3-tiny.cfg三份配置,sampledata里还塞了一段commuters.mp4当离线测试素材。依赖只有numpy、opencv-python、imageio-ffmpeg三个,Python 2.x 直接不支持。适合谁?做安防预览、工地/园区计数、零售客流统计这类需要「先跑起来看效果」的从业者,也适合拿它当 OpenCV dnn 跑 YOLO 的最小可运行样板。下面按「先立住原理、再动手复现、最后避坑」的顺序拆开讲。

2. 拆开压缩包:文件结构、依赖与 RTSP 拉流原理

2.1 目录里每个文件是干嘛的

解压yolo-python-rtsp-master.zip之后,先别急着python yolo_opencv.py,把结构看清楚能省掉后面一半的报错。整包大致长这样:

路径作用是否要改
yolo_opencv.py主程序,拉流、推理、画框、存图要改 RTSP 地址和输出路径
cfg/yolov3.txt80 类 COCO 类别名,一行一个一般不动
cfg/yolov3.cfgYOLOv3 网络结构定义不动
cfg/yolov3-tiny.cfg轻量版网络结构,速度快精度低按算力切换
sampledata/commuters.mp4离线测试视频可替换
object-detection.pngREADME 里的效果图不动
README.md/LICENSE说明和许可不动
.idea/、yolo-python.imlPyCharm 工程文件可删

注意一点:压缩包里没有.weights权重文件。YOLO 的推理必须同时有.cfg(结构)和.weights(权重),缺一不可。权重得自己去 DarkNet 官方渠道拿yolov3.weights或yolov3-tiny.weights,放到项目根目录,路径要和脚本里写的一致。这是新手第一个翻车点,后面避坑章还会展开。

2.2 为什么用 OpenCV dnn 而不是装 DarkNet

对象检测这块主流框架就三个:YOLO、SSD、Faster R-CNN。SSD 对小目标弱,Faster R-CNN 精度高但两阶段推理慢,YOLO 单阶段、速度快,最适合 RTSP 这种要连续处理的流。传统玩法是编译 DarkNet 源码再调,但那样得配 CUDA、cuDNN、OpenCV 编译链,环境成本高。

OpenCV 从 3.4.2 之后在 dnn 模块里加了 YOLO/DarkNet 支持,可以直接readNetFromDarknet加载.cfg+.weights,推理走blobFromImage+forward。好处是:不用装 DarkNet,不用编译,pip install opencv-python就能跑;坏处是默认 CPU 推理,帧率取决于机器。常见做法是先用 CPU 把流程跑通,确认检测逻辑没问题,再考虑换opencv-python的 CUDA 版本或者上 V100 这类卡做加速。

2.3 RTSP 拉流在代码里是怎么落地的

RTSP 是实时流协议,摄像头把 H.264/H.265 编码的流推出来,客户端用rtsp://user:pass@ip:554/...这种地址去拉。OpenCV 的VideoCapture底层走 FFmpeg,能直接吃 RTSP 地址,所以拉流这一步在代码里就是一行cv2.VideoCapture(rtsp_url)。但 RTSP 是长连接,网络抖动、摄像头重启都会让连接断掉,裸写while True: ret, frame = cap.read()一旦ret变False就会疯狂空转。所以合格的写法必须带重连和缓冲控制,这部分在第 3 章给完整代码。

2.4 环境准备:三个依赖装完就能跑

依赖就三个,Python 3.6 以上:

# 建议先建虚拟环境,避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 三个依赖一次装齐 pip install numpy opencv-python imageio-ffmpeg

numpy负责数组运算,opencv-python提供 dnn 和 VideoCapture,imageio-ffmpeg是给某些环境下 OpenCV 找不到 FFmpeg 后端时兜底的。装完用下面这段验证 dnn 模块和 FFmpeg 后端都在:

import cv2 import numpy as np print("OpenCV:", cv2.__version__) # 确认 dnn 模块可用 print("dnn available:", hasattr(cv2, "dnn")) # 确认能构建 FFmpeg 后端(RTSP 拉流依赖它) print("FFmpeg backend:", cv2.videoio_registry.getBackendName(cv2.CAP_FFMPEG))

如果dnn available是False,说明装的是精简版 OpenCV,换opencv-python完整包;如果 FFmpeg 后端报错,多半是imageio-ffmpeg没装好或者系统缺动态库。这一步过了,再往下走。

3. 把 RTSP 流接进 YOLO:推理主循环与分类落盘

3.1 加载网络与类别:三份配置怎么配合

YOLO 推理需要三样东西对齐:网络结构(.cfg)、权重(.weights)、类别名(.txt)。yolov3.txt里 80 行对应 COCO 的 80 类,顺序必须和权重训练时一致,否则会出现「框对了但标签全错」的玄学现象。加载逻辑如下:

import cv2 import numpy as np # 类别名:一行一个,读成列表 with open("cfg/yolov3.txt", "r") as f: classes = [line.strip() for line in f.readlines()] # 网络结构 + 权重,两者必须配套 net = cv2.dnn.readNet("yolov3.weights", "cfg/yolov3.cfg") # 取输出层名字,YOLOv3 有三个 yolo 输出层 layer_names = net.getLayerNames() output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]

getUnconnectedOutLayers()返回的是输出层索引,OpenCV 里索引从 0 开始但getLayerNames()返回的列表要减 1 对齐,这是 OpenCV 的老约定,写错会直接IndexError。output_layers拿到的是三个 YOLO 检测层的名字,forward时要把它们传进去,否则只拿到部分尺度的检测结果,小目标会漏。

3.2 逐帧推理:blobFromImage 的参数怎么设

每一帧进来先转成网络要的输入格式。YOLOv3 输入是 416×416,blobFromImage负责缩放、减均值、换通道:

def detect(frame, net, output_layers, conf_threshold=0.5, nms_threshold=0.4): height, width = frame.shape[:2] # 416x416 是 YOLOv3 标准输入;swapRB=True 因为 OpenCV 读的是 BGR blob = cv2.dnn.blobFromImage( frame, 1/255.0, (416, 416), swapRB=True, crop=False ) net.setInput(blob) outputs = net.forward(output_layers) boxes, confidences, class_ids = [], [], [] for output in outputs: for detection in output: scores = detection[5:] # 前 5 位是 cx,cy,w,h,obj class_id = int(np.argmax(scores)) confidence = float(scores[class_id]) if confidence > conf_threshold: # 检测框是相对坐标,乘回原图尺寸 cx, cy = int(detection[0] * width), int(detection[1] * height) w, h = int(detection[2] * width), int(detection[3] * height) x, y = int(cx - w / 2), int(cy - h / 2) boxes.append([x, y, w, h]) confidences.append(confidence) class_ids.append(class_id) # NMS 去重,否则同一目标会叠一堆框 indices = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold) return boxes, confidences, class_ids, indices

参数说明:1/255.0是归一化系数,YOLO 训练时输入就是 0~1;swapRB=True把 BGR 转成 RGB,因为 DarkNet 按 RGB 训练;conf_threshold是置信度门槛,0.5 是通用起点,漏检多就降到 0.3,误检多就升到 0.6;nms_threshold是 NMS 的 IoU 阈值,0.4 意味着重叠超过 40% 的框会被合并。NMSBoxes返回的indices在某些 OpenCV 版本里是二维数组,取用时最好np.array(indices).flatten()一下,不然遍历会报错。

3.3 分类落盘:按日期和类别建目录

摘要里说的「识别出的对象按日期存储在每个类的文件夹中」,落地就是检测到目标后裁剪 ROI,按输出根目录/日期/类别名/存图:

import os from datetime import datetime def save_crop(frame, box, class_name, root="output"): x, y, w, h = box # 边界保护,防止裁剪越界 x, y = max(0, x), max(0, y) roi = frame[y:y + h, x:x + w] if roi.size == 0: return day = datetime.now().strftime("%Y-%m-%d") save_dir = os.path.join(root, day, class_name) os.makedirs(save_dir, exist_ok=True) # 用时间戳命名,避免同秒覆盖 ts = datetime.now().strftime("%H%M%S%f") cv2.imwrite(os.path.join(save_dir, f"{ts}.jpg"), roi)

os.makedirs(..., exist_ok=True)保证目录不存在时自动建、存在时不报错。时间戳精确到微秒,避免同一秒内多个目标互相覆盖。裁剪前做max(0, ...)边界保护,因为 YOLO 的框可能超出画面边缘,直接切片会得到空数组。

3.4 主循环:拉流、重连、跳帧

把上面拼起来,主循环要处理 RTSP 断流重连和推理节流:

import time RTSP_URL = "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101" cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG) # 减小缓冲,降低延迟;设太大画面会滞后好几秒 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) frame_count = 0 while True: if not cap.isOpened(): print("流断开,3 秒后重连") time.sleep(3) cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG) continue ret, frame = cap.read() if not ret: print("读帧失败,重连") cap.release() time.sleep(1) cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG) continue frame_count += 1 # 每 3 帧推理一次,CPU 上能明显提帧率 if frame_count % 3 != 0: continue boxes, confs, ids, indices = detect(frame, net, output_layers) for i in np.array(indices).flatten(): save_crop(frame, boxes[i], classes[ids[i]]) x, y, w, h = boxes[i] cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, classes[ids[i]], (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow("RTSP-YOLO", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

CAP_PROP_BUFFERSIZE设成 1 是关键,默认缓冲会攒好几帧,导致画面延迟越来越大。跳帧推理(frame_count % 3)是在 CPU 上提帧率最直接的手段,代价是快速移动的目标可能被跳过。cv2.waitKey(1)里的 1 是毫秒,配合imshow刷新窗口,按q退出。这套跑通,你就有了一个能落盘的 RTSP 检测基线。

4. 避坑与排查:权重、延迟、编码、路径的五个血泪经验

4.1 报错Unable to load weights或检测框全乱

现象:readNet直接抛异常,或者能跑但标签和框完全对不上。原因通常是权重文件缺失、路径写错,或者.cfg和.weights版本不配套(比如用yolov3-tiny.cfg配yolov3.weights)。解决:确认根目录有yolov3.weights,且readNet第一个参数是权重、第二个是 cfg,顺序别反;tiny 模型必须配 tiny 权重。标签错乱则检查yolov3.txt行数和权重类别数是否都是 80。

4.2 画面延迟越跑越大,像慢动作

现象:imshow出来的画面比现实慢好几秒,且时间越久越严重。原因:OpenCV 默认缓冲会攒帧,推理速度跟不上拉流速度时,缓冲队列越积越长。解决:cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)把缓冲压到最小;再配合跳帧推理,让消费速度追上生产速度。如果还慢,说明 CPU 推理本身扛不住,得换 tiny 模型或上 GPU。

4.3 RTSP 地址能 ping 通但VideoCapture打不开

现象:网络通,但cap.isOpened()一直是False。原因:RTSP 地址格式不对,或者用户名密码里的特殊字符没转义,或者摄像头要求 TCP 传输而默认走了 UDP。解决:先用ffplay或 VLC 验证地址本身能播;地址里的@、:等字符要 URL 编码;OpenCV 可以在地址后加?tcp强制 TCP,比如rtsp://.../101?tcp,弱网下比 UDP 稳。

4.4 存图目录建了一堆空文件夹

现象:output/日期/类别/建出来了,但里面没图。原因:save_crop里 ROI 越界得到空数组,imwrite静默失败;或者indices是二维数组没flatten,循环根本没进。解决:裁剪前做边界保护并判断roi.size == 0直接返回;indices统一np.array(indices).flatten()再遍历。另外确认输出根目录有写权限。

4.5 中文路径或类别名导致imwrite失败

现象:cv2.imwrite返回False,图没存下来。原因:OpenCV 的imwrite对非 ASCII 路径支持不好,中文目录名会翻车。解决:输出路径全用英文和数字,类别名直接用yolov3.txt里的英文原词(person、car),别自己改成中文。真需要中文名,用cv2.imencode加文件流写入绕开。

5. 进阶:换 tiny 模型、调阈值与验证落盘质量

跑通基线之后,真正决定这套东西好不好用的是两个旋钮:模型和阈值。先看模型切换,把yolov3.cfg+yolov3.weights换成yolov3-tiny.cfg+yolov3-tiny.weights,代码里只改readNet的两个路径即可,其他逻辑不动。tiny 模型在 CPU 上帧率通常能翻两三倍,代价是小目标和密集场景漏检明显,适合「先看有没有人」这种粗筛场景。我一般会准备两套路径,用命令行参数切换:

import argparse parser = argparse.ArgumentParser() parser.add_argument("--model", choices=["full", "tiny"], default="full") parser.add_argument("--conf", type=float, default=0.5) parser.add_argument("--nms", type=float, default=0.4) args = parser.parse_args() if args.model == "tiny": weights, cfg = "yolov3-tiny.weights", "cfg/yolov3-tiny.cfg" else: weights, cfg = "yolov3.weights", "cfg/yolov3.cfg" net = cv2.dnn.readNet(weights, cfg)

阈值这块,conf和nms要联动调。只降conf会引入大量误检,得同时把nms从 0.4 降到 0.3 压重叠框;只升conf会漏掉遮挡目标。我的习惯是拿sampledata/commuters.mp4当固定测试集,分别跑conf=0.3/0.5/0.7三档,对比落盘图片数量和误检率,选一个平衡点再上 RTSP。验证落盘质量有个笨但有效的办法:跑十分钟后统计output/日期/下每个类别文件夹的图片数,如果某个类别的数量远超画面里实际出现的次数,基本就是误检在刷图,回去调阈值。

还有一个容易被忽略的点:blobFromImage的输入尺寸。YOLOv3 标准是 416×416,但如果你换成 608×608,小目标召回会提升,CPU 推理时间也线性上涨。改尺寸时.cfg里的width/height不用动,OpenCV 会按blobFromImage的尺寸缩放,但精度和速度的权衡得自己实测。从那以后我每次换模型或改阈值,都强制先用离线视频跑一遍对比落盘数量,确认没异常再接实时流——RTSP 上直接调参,出了问题连回放都没有,纯靠猜。希望帮到你。

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

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

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

立即咨询