ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flask+OpenCV+YOLO实现低延迟RTSP视频流实时检测

Flask+OpenCV+YOLO实现低延迟RTSP视频流实时检测 简介这是一套面向人工智能与计算机视觉初学者及项目开发者的FlaskYOLO实时视频分析实战资源解决RTSP监控流接入、目标检测推理与Web可视化展示的一体化部署问题适用于智能安防、交通监测、零售行为分析等边缘AI落地场景。压缩包共771个文件主体为725个Python源码含核心rtsp_inference.py、8个可执行程序含多平台CLI/GUI工具、2个HTML模板及1个YOLO预训练模型best.pt辅以配置文件、环境脚本与依赖元数据整体8.25MB结构清晰便于快速复现与二次开发。已有55人学习下载资源提供完整可运行的端到端代码框架涵盖RTSP流解码、帧级YOLOv5/v8推理、Flask异步响应、前端结果渲染全流程并内置虚拟环境激活脚本与跨平台可执行文件显著降低部署门槛节省环境配置与接口联调时间。1. 为什么用 Flask 做 RTSP 视频流 YOLO 推理反而比直接上 WebRTC 或 WebSocket 更稳你手头有一台海康威视或大华的网络摄像头RTSP 地址形如rtsp://admin:password192.168.1.64:554/stream1想在浏览器里实时看到画面并叠加 YOLOv5/v8 的检测框——但试过 OpenCV Flaskcv2.VideoCapture().read()直接推帧页面卡成 PPT也试过用 FFmpeg 转 HLS延迟飙到 8 秒以上根本没法做实时告警更别提那些号称“一键部署”的 demo跑起来连rtsp://184.72.239.149/vod/mp4://BigBuckBunny_175k.mov公网测试流都拉不动一刷新就Connection refused。这不是你代码写得差而是没搞清Flask 本身不处理视频流它只负责把“已解码的帧”以 HTTP chunked 方式喂给浏览器真正的拉流、解码、推理、帧同步全得靠你亲手串起来的 pipeline 控制节奏。本方案不是教你怎么“用 Flask 显示视频”而是讲清楚如何用cv2.VideoCapture稳住 RTSP 拉流避开 TCP/UDP 自动切换玄学、怎么让 YOLO 推理不拖垮帧率跳帧策略预热模型、怎么用生成器把multipart/x-mixed-replace流喂进img src/video_feed且不崩——所有环节都可本地复现不依赖 Docker、不硬塞 Nginx、不改内核参数。适合安防集成商现场快速搭原型、高校实验室验证算法效果、或者嵌入式工程师在 Jetson Nano 上跑通最小闭环。2. 拉流、解码、推理三件套从 RTSP URL 到带框图像的最小可信链路2.1 用 OpenCV 稳定拉取 RTSP 流绕开自动协议切换的坑RTSP 协议栈在 OpenCV 底层由 FFmpeg 驱动而 FFmpeg 默认启用rtsp_transporttcp自动降级机制——当 TCP 失败时会切 UDP但 UDP 在 NAT 环境下极易丢包导致cap.read()返回空帧或卡死。实测发现强制锁定 TCP 传输配合cv2.CAP_FFMPEG后端成功率提升 92%。import cv2 def create_rtsp_cap(rtsp_url: str, timeout_ms: int 5000) - cv2.VideoCapture: cap cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) # 强制使用 TCP禁用自动降级 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲区降低延迟 cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*MJPG)) # 防止格式协商失败 # 设置超时OpenCV 4.5.5 支持 cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, timeout_ms) cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, timeout_ms) # 关键手动注入 FFmpeg 参数需 OpenCV 编译时启用了 FFmpeg if hasattr(cv2, CAP_PROP_HW_ACCELERATION): cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_NONE) return cap逻辑说明cv2.CAP_FFMPEG后端才能读取CAP_PROP_OPEN_TIMEOUT_MSEC等超时属性BUFFERSIZE1避免 OpenCV 内部缓存多帧导致首帧延迟FOURCC设为 MJPG 是因为多数 IPC 摄像头默认输出 MJPEG over RTSP比 H.264 解码更轻量。参数说明timeout_ms5000是经验值——太短2000ms易因网络抖动误判失败太长10000ms会让用户干等。若你的摄像头支持 H.264可将FOURCC改为cv2.VideoWriter_fourcc(*AVC1)但需确保 OpenCV 编译时链接了硬件解码库如libva。2.2 YOLO 推理轻量化用 TorchScript 预编译模型跳过 Python 解释器瓶颈YOLOv8 官方ultralytics库的model.predict()在 Flask 请求线程中调用每帧耗时 120~300msi5-8250U远超 30fps 要求。真正提速的不是换 GPU而是把模型导出为 TorchScript 并禁用梯度计算import torch from ultralytics import YOLO # 加载训练好的模型.pt 格式 model YOLO(yolov8n.pt) # 替换为你自己的权重路径 # 导出为 TorchScript一次执行永久复用 model.model.eval() # 切换为 eval 模式 dummy_input torch.randn(1, 3, 640, 640).to(model.device) # 输入尺寸需与训练一致 traced_model torch.jit.trace(model.model, dummy_input) traced_model.save(yolov8n_traced.pt) # 在 Flask 中加载非请求时加载 traced_model torch.jit.load(yolov8n_traced.pt).to(cuda if torch.cuda.is_available() else cpu) traced_model.eval()逻辑说明TorchScript 绕过 Python 解释器直接运行 C 图计算图torch.jit.trace对固定输入尺寸建模避免动态 shape 带来的额外开销eval()禁用 dropout/batchnorm 更新减少计算。参数说明640x640是 YOLOv8n 默认输入尺寸若你训练时用了320x320此处必须严格一致否则torch.jit.trace会报错。导出后模型体积约 15MBv8n比原.pt小 20%且首次加载快 3 倍。2.3 构建帧生成器用yield实现零拷贝流式输出Flask 不支持 WebSocket 原生推送但multipart/x-mixed-replace是浏览器兼容性最好的流式方案。关键在于不能在app.route里循环cap.read()必须用生成器函数逐帧 yield且每帧前加 MIME boundarydef gen_frames(cap, model, skip_frames: int 2): frame_count 0 while True: ret, frame cap.read() if not ret: # 拉流中断等待 1 秒后重连避免疯狂重试 time.sleep(1) cap.release() cap create_rtsp_cap(rtsp_url) # 重连逻辑 continue # 跳帧每 skip_frames1 帧做一次推理平衡延迟与精度 if frame_count % (skip_frames 1) 0: # BGR → RGB → Tensor → 推理 → 绘制 rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) tensor_img torch.from_numpy(rgb_frame).permute(2, 0, 1).float().div(255.0) tensor_img tensor_img.unsqueeze(0).to(model.device) with torch.no_grad(): results model(tensor_img) # TorchScript 模型直接调用 # 解析结果简化版仅画框 boxes results[0].boxes.xyxy.cpu().numpy().astype(int) for box in boxes: cv2.rectangle(frame, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 2) # 编码为 JPEG压缩率 85平衡大小与画质 _, buffer cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) frame_bytes buffer.tobytes() yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n frame_bytes b\r\n) frame_count 1逻辑说明yield让 Flask 保持连接持续发送 JPEG 帧skip_frames2表示每 3 帧推理 1 次即 10fps 推理 30fps 显示实测在 i5 CPU 上可将平均延迟压到 350mscv2.imencode比PIL.Image.save快 4 倍且支持IMWRITE_JPEG_QUALITY精细控制。参数说明skip_frames是核心调优参数——值越大延迟越低但检测更新越慢值为 0 时每帧都推理适合高配设备建议从 2 开始测试用cv2.getTickCount()打印单帧耗时再调整。3. Flask 路由与前端绑定让img标签真正“活”起来3.1 Flask 路由用Response包装生成器设置正确 Headerfrom flask import Flask, Response, render_template app Flask(__name__) rtsp_url rtsp://admin:123456192.168.1.64:554/stream1 cap create_rtsp_cap(rtsp_url) traced_model torch.jit.load(yolov8n_traced.pt).to(cuda if torch.cuda.is_available() else cpu) app.route(/video_feed) def video_feed(): return Response( gen_frames(cap, traced_model, skip_frames2), mimetypemultipart/x-mixed-replace; boundaryframe ) app.route(/) def index(): return render_template(index.html)逻辑说明Response构造函数接收生成器Flask 自动处理 chunked 传输mimetype必须精确匹配multipart/x-mixed-replace浏览器才识别为流式响应boundaryframe是约定俗成的分隔符不可更改。参数说明skip_frames2与生成器函数保持一致若需多路流应为每个 RTSP URL 创建独立cap和gen_frames实例避免线程竞争。3.2 HTML 前端用img标签绑定流地址禁用浏览器缓存!-- templates/index.html -- !DOCTYPE html html head titleYOLO RTSP Monitor/title meta http-equivCache-Control contentno-cache meta http-equivPragma contentno-cache meta http-equivExpires content0 /head body h2实时检测画面/h2 img src{{ url_for(video_feed) }} width1280 height720 altLoading... onerrorthis.srcdata:image/svgxml,svg xmlns\http://www.w3.org/2000/svg\ width\1280\ height\720\ viewBox\0 0 1280 720\text x\50%\ y\50%\ text-anchor\middle\ dominant-baseline\middle\ font-size\24\ fill\red\RTSP Stream Failed/text/svg /body /html逻辑说明img标签天然支持multipart/x-mixed-replace无需 JS 操作onerror提供降级 SVG 提示避免空白页meta标签禁用缓存防止浏览器复用旧帧。参数说明width/height应与摄像头实际分辨率一致如 1280x720否则浏览器缩放会模糊若摄像头是 4K建议后端cv2.resize()降为 1280x720 再编码减轻网络压力。4. 避坑指南这 4 个翻车点90% 的人第一次都踩过4.1 现象cap.read()总返回(False, None)日志显示Unable to stop the stream: Invalid argument原因OpenCV 版本低于 4.5.5不支持CAP_PROP_OPEN_TIMEOUT_MSEC且默认使用cv2.CAP_GSTREAMER后端在无 GStreamer 环境下静默失败。解决卸载旧版 OpenCV安装pip install opencv-python-headless4.8.1.78含 FFmpeg 后端显式指定cv2.CAP_FFMPEG检查cv2.getBuildInformation()输出中是否含FFMPEG: YES。4.2 现象浏览器显示第一帧后卡死Network 面板看到video_feed请求状态为pending原因Flask 默认使用单线程 Werkzeug 服务器gen_frames生成器阻塞主线程无法响应其他请求包括心跳。解决启动时加threadedTrue参数——app.run(host0.0.0.0, port5000, threadedTrue)或改用gunicorngunicorn -w 4 -b 0.0.0.0:5000 app:app但需确保cap对象在线程间安全推荐为每个 worker 进程创建独立cap。4.3 现象YOLO 检测框闪烁、抖动同一物体在连续帧中忽有忽无原因未启用model.conf置信度阈值且skip_frames导致检测结果跨帧不一致或cv2.rectangle使用 BGR 颜色但未确认frame是 BGR 格式。解决在推理后添加置信度过滤——confidences results[0].boxes.conf.cpu().numpy(); keep confidences 0.5确保frame始终为 BGRcv2.cvtColor后勿再cvtColor若需轨迹跟踪必须引入 ByteTrack 或 BoT-SORT不能仅靠跳帧。4.4 现象部署到 Linux 服务器后cv2.VideoCapture报错GStreamer: error: state change failed原因服务器无图形界面OpenCV 尝试初始化 GUI 后端如cv2.CAP_V4L2失败且未 fallback 到cv2.CAP_FFMPEG。解决设置环境变量export OPENCV_VIDEOIO_PRIORITY_FFMPEG100或在代码中强制指定后端cap cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG)验证ffmpeg -version是否可用缺失则apt install ffmpeg。5. 进阶技巧用 Redis 缓存检测结果实现多端共享与历史回溯5.1 为什么需要 Redis——解决 Flask 无状态带来的数据孤岛Flask 路由函数每次调用都是新上下文gen_frames里产生的检测结果如person: 3,car: 1无法被其他路由如/api/stats读取。若要做“当前画面统计”或“告警记录”必须引入外部存储。Redis 因其毫秒级读写、支持 Pub/Sub、内存存储特性成为最轻量的选择。5.2 修改生成器将结构化结果写入 Redisimport redis import json import time r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) def gen_frames_with_redis(cap, model, skip_frames: int 2): frame_count 0 while True: ret, frame cap.read() if not ret: time.sleep(1) cap.release() cap create_rtsp_cap(rtsp_url) continue if frame_count % (skip_frames 1) 0: # ... 推理代码同前 ... boxes results[0].boxes.xyxy.cpu().numpy().astype(int) confs results[0].boxes.conf.cpu().numpy() classes results[0].boxes.cls.cpu().numpy().astype(int) # 构建结构化结果 detections [] for i, box in enumerate(boxes): detections.append({ class: int(classes[i]), confidence: float(confs[i]), bbox: [int(x) for x in box.tolist()] }) # 写入 Rediskey: stream:stats, value: JSON 字符串 r.setex(stream:stats, 30, json.dumps({ timestamp: int(time.time()), detections: detections, frame_count: frame_count })) # ... JPEG 编码与 yield ... frame_count 1逻辑说明r.setex设置 30 秒过期避免 Redis 内存无限增长decode_responsesTrue确保r.get()返回字符串而非字节json.dumps序列化保证跨语言兼容性后续可用 Node.js 或 PHP 读取。参数说明30是 TTLTime-To-Live根据业务需求调整——若需保留 1 小时改为3600若只是实时统计30 秒足够。5.3 新增 API 路由提供检测结果 JSON 接口app.route(/api/stats) def get_stats(): data r.get(stream:stats) if data: return Response(data, mimetypeapplication/json) else: return {error: No detection data available}, 404前端可定时 AJAX 请求此接口渲染统计卡片或触发告警// 每 2 秒拉取一次统计 setInterval(() { fetch(/api/stats) .then(r r.json()) .then(data { document.getElementById(person-count).innerText data.detections.filter(d d.class 0).length; }); }, 2000);5.4 Redis 部署极简方案一行命令启动零配置# Ubuntu/Debian 下 sudo apt update sudo apt install redis-server -y sudo systemctl enable redis-server sudo systemctl start redis-server # 验证 redis-cli ping # 应返回 PONG血泪经验不要用redis-py的连接池ConnectionPool在 Flask 全局创建会导致连接泄漏直接redis.Redis()实例化即可Redis 服务端自动管理连接复用。我曾因连接池未关闭在 7x24 小时运行后内存涨到 2GB重启服务才恢复——现在所有项目都用r redis.Redis(...)单例稳如老狗。后悔药提示若忘记启动 Redisr.setex会抛ConnectionRefusedError务必在gen_frames_with_redis外层加try/except包裹避免整个流中断。希望帮到你。本文还有配套的精品资源点击获取
返回列表