ARTICLE DETAIL

资讯详情

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

Yolov5+DeepSort人流量监测WebApp实战:从检测跟踪到部署避坑

Yolov5+DeepSort人流量监测WebApp实战:从检测跟踪到部署避坑 简介这份资源面向计算机视觉学习者与智能监控开发者提供一套基于Yolov5与DeepSort的人流量监测WebApp完整实现。项目以Yolov5作为目标检测器识别视频帧中的人体DeepSort负责跨帧追踪同一目标轨迹再借助Streamlit搭建交互式网页界面用户上传视频或接入摄像头流即可实时查看人流量统计与追踪轨迹。压缩包共318个文件约64.37MB以163个py源码、52个yaml配置、13个md与12个rst文档为主另含8个pth与5个pt模型权重、9个sh启动脚本及Dockerfile等部署文件覆盖检测、追踪、界面与容器化各环节。已有289人学习下载。资源包含训练权重、DeepSort配置、预处理与后处理脚本及Streamlit应用配置目录结构清晰便于读者快速部署运行、理解检测与追踪的衔接逻辑并在此基础上二次开发智能监控方案。1. 从一段监控视频说起Yolov5 DeepSort 的人流量监测 WebApp 到底在做什么你手头有一段商场入口、园区闸机或者食堂通道的监控视频老板想要一份按小时统计的进出人数报表最好还能在浏览器里实时看到画面和数字。这件事听起来简单做起来却卡在三个地方检测要准、跟踪要稳、结果要能被人看见。Yolov5 负责每一帧里把人框出来DeepSort 负责给每个框分配一个跨帧不变的 IDWebApp 负责把视频流、统计数字和查询接口串起来。三者缺一不可单独跑 Yolov5 只能得到一堆散乱的框单独跑 DeepSort 没有检测输入就是空转。这套方案适合谁适合已经跑通过 Yolov5 推理、想把它从脚本升级成可交互系统的人也适合做智慧安防、零售客流分析、工地安全管理的工程师需要快速搭一个能演示、能出数据、能继续迭代的原型。不适合谁不适合指望零标注数据直接上线的人也不适合把精度要求推到工业级计数比如误差小于 1%的场景那需要更重的 ReID 模型和标定流程。我见过太多人卡在“Yolov5 跑通了DeepSort 也跑通了但合在一起 ID 乱跳”的阶段。问题往往不在模型本身而在检测置信度阈值、NMS 后处理、跟踪器最大丢失帧数这几个参数的配合。这篇笔记就按“先让单帧检测稳再让跨帧跟踪稳最后让 WebApp 把结果吐出来”的顺序把每个环节的参数、代码和踩坑点讲清楚。你照着做至少能少走两星期弯路。2. Yolov5 检测层从环境配置到推理参数调优2.1 环境配置与模型选型为什么我推荐 conda yolov5sYolov5 的环境配置是第一个拦路虎。常见做法是用 conda 建一个独立环境Python 版本选 3.8 或 3.9PyTorch 版本跟 CUDA 对齐。如果你在树莓派 5 上部署那就别想 CUDA 了直接用 CPU 推理模型换成 yolov5n 或 yolov5s输入尺寸降到 320 或 416。我一般会先确认硬件有 NVIDIA 显卡就装 CUDA 版 PyTorch没有就装 CPU 版别混着装否则后面报错能查到你怀疑人生。# 创建 conda 环境Python 3.8 兼容性最好 conda create -n yolov5_env python3.8 -y conda activate yolov5_env # 安装 PyTorch以 CUDA 11.8 为例CPU 版去掉 --index-url 那行 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 克隆 Yolov5 源码并安装依赖 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt这段命令的逻辑是先隔离环境再装框架最后装项目依赖。参数说明python3.8是因为 Yolov5 对 3.10 以上版本的部分依赖兼容性不稳定cu118对应 CUDA 11.8你要根据nvidia-smi显示的版本改。树莓派 5 上装 CPU 版即可但推理速度会降到 2-5 FPS后面 WebApp 要改成抽帧处理。模型选型上yolov5s 是精度和速度的平衡点参数量约 7M在 1080Ti 上跑 640 输入能到 60 FPS 以上。yolov5n 更小适合边缘设备但小目标召回率会掉。yolov5m 及以上除非你有明确的高精度需求否则推理延迟会拖垮整个跟踪链路。2.2 推理参数怎么设conf、iou、imgsz 的联动关系Yolov5 推理时有三个参数直接决定 DeepSort 的输入质量conf置信度阈值、iouNMS 交并比阈值、imgsz输入尺寸。很多人只调 conf结果要么漏检要么误检跟踪 ID 跟着乱。我的经验是人流量监测场景下conf 设 0.35-0.45iou 设 0.45-0.5imgsz 设 640。如果画面里人很小imgsz 提到 1280但速度会掉一半。import torch # 加载模型指定设备 model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.4 # 置信度阈值低于此值的框被丢弃 model.iou 0.45 # NMS 阈值控制重叠框的合并程度 model.classes [0] # 只检测 person 类COCO 数据集中 person 的索引是 0 model.max_det 100 # 单帧最大检测数人流量大时调高 # 推理 results model(test.jpg, size640) results.print()逻辑说明classes[0]是关键人流量监测只关心人过滤掉其他 79 类能减少误检和计算量。max_det默认 1000但实际场景一帧超过 100 人的情况很少调低能减少后处理时间。参数改动后要重新验证拿一段标注好的视频统计漏检和误检conf 每降 0.05误检率大概上升 3-5%漏检率下降 2-4%自己找平衡点。后处理阶段Yolov5 默认用 NMS 去重。如果两个人挨得很近iou 设太高会导致其中一个被吞掉设太低会保留重复框。我一般会先把 iou 设 0.5 跑一遍看跟踪 ID 是否稳定不稳定再降到 0.45。2.3 训练自己的数据集标注、划分与超参数如果你要监测的场景不是标准视角比如俯拍、红外、夜间预训练模型会翻车。这时候需要训练自己的数据集。流程是用 labelImg 或 CVAT 标注导出 YOLO 格式的 txt按 8:1:1 划分训练/验证/测试集然后改 data.yaml 和 hyp.yaml。# data.yaml 示例 path: ./dataset train: images/train val: images/val test: images/test nc: 1 names: [person]# 开始训练batch size 根据显存调imgsz 跟推理保持一致 python train.py --img 640 --batch 16 --epochs 100 --data data.yaml --weights yolov5s.pt --device 0参数说明--batch 16在 8G 显存上比较稳显存不够降到 8--epochs 100是起点如果验证集 mAP 还在涨就继续--weights yolov5s.pt用预训练权重做迁移学习比从头训快很多。训练完成后用val.py看 mAP0.5人流量场景一般能到 0.85 以上就够用了。超参数方面hyp.yaml里的lr0初始学习率设 0.01lrf最终学习率因子设 0.01momentum设 0.937weight_decay设 0.0005。这些是 Yolov5 的默认值除非你有大量数据否则不用大改。数据增强里mosaic设 1.0 能提升小目标效果但人流量场景如果人本来就大可以降到 0.5。3. DeepSort 跟踪层让每个行人拥有稳定 ID3.1 DeepSort 的工作原理卡尔曼滤波 匈牙利算法 ReIDDeepSort 的核心是三件事卡尔曼滤波预测轨迹、匈牙利算法做匹配、ReID 特征做外观关联。Yolov5 每帧输出检测框DeepSort 先用卡尔曼滤波根据上一帧轨迹预测当前帧位置然后计算预测框和检测框的 IoU 代价矩阵同时提取每个检测框的 ReID 特征计算外观代价两者加权得到最终代价矩阵再用匈牙利算法做最优匹配。匹配上的轨迹更新状态没匹配上的检测框新建轨迹连续多帧没匹配上的轨迹删除。这套机制里max_age最大丢失帧数和n_init确认轨迹所需的连续匹配帧数是两个关键参数。max_age设太小人一被遮挡 ID 就断设太大人走了轨迹还留着容易跟别人匹配上。n_init设太小误检会变成轨迹设太大新人进入画面后要等好几帧才有 ID。3.2 把 Yolov5 检测结果喂给 DeepSort代码与参数DeepSort 的官方实现是deep_sort_realtime或deep-sort-pytorch我一般用deep_sort_realtime因为它封装好了 ReID 模型开箱即用。下面是把 Yolov5 和 DeepSort 串起来的核心代码。from deep_sort_realtime.deepsort_tracker import DeepSort import cv2 import torch # 初始化 DeepSort tracker DeepSort( max_age30, # 轨迹丢失 30 帧后删除 n_init3, # 连续 3 帧匹配才确认轨迹 nms_max_overlap1.0, # 不做额外 NMS因为 Yolov5 已经做过 max_cosine_distance0.2, # ReID 特征余弦距离阈值 nn_budget100 # 每个轨迹保存的特征数 ) model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.4 model.iou 0.45 model.classes [0] cap cv2.VideoCapture(test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame, size640) detections [] for *xyxy, conf, cls in results.xyxy[0]: x1, y1, x2, y2 map(int, xyxy) detections.append(([x1, y1, x2-x1, y2-y1], conf.item(), person)) tracks tracker.update_tracks(detections, frameframe) for track in tracks: if not track.is_confirmed(): continue track_id track.track_id ltrb track.to_ltrb() cv2.rectangle(frame, (int(ltrb[0]), int(ltrb[1])), (int(ltrb[2]), int(ltrb[3])), (0, 255, 0), 2) cv2.putText(frame, fID {track_id}, (int(ltrb[0]), int(ltrb[1])-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明detections的格式是([x, y, w, h], confidence, class_name)DeepSort 需要这个格式。tracker.update_tracks返回当前帧所有轨迹is_confirmed()过滤掉还没确认的轨迹。max_cosine_distance0.2是 ReID 特征的匹配阈值设太小会导致同一个人换外观比如转身后 ID 断设太大会把不同人匹配成同一个 ID。nn_budget100控制每个轨迹保存多少历史特征越大越准但越慢。参数调优上max_age我一般设 30对应 30 FPS 视频约 1 秒。如果场景遮挡严重提到 50如果人流量极大降到 20 减少误匹配。n_init设 3 是平衡点设 1 会让误检立刻变成轨迹设 5 会让新人等太久。3.3 计数逻辑越线计数与区域计数怎么选人流量监测最终要输出数字。常见做法有两种越线计数和区域计数。越线计数是在画面里画一条虚拟线轨迹从线的一侧到另一侧时计数加一区域计数是画一个多边形区域统计区域内轨迹数量。越线计数适合出入口区域计数适合统计停留人数。# 越线计数示例 line [(0, 300), (640, 300)] # 虚拟线坐标 count_in 0 count_out 0 track_history {} # 记录每个 ID 的历史位置 def is_crossing(prev_y, curr_y, line_y): return (prev_y line_y curr_y) or (prev_y line_y curr_y) for track in tracks: if not track.is_confirmed(): continue track_id track.track_id ltrb track.to_ltrb() center_y (ltrb[1] ltrb[3]) / 2 if track_id in track_history: prev_y track_history[track_id] if is_crossing(prev_y, center_y, 300): if center_y prev_y: count_in 1 else: count_out 1 track_history[track_id] center_y逻辑说明track_history保存每个 ID 上一帧的中心 y 坐标is_crossing判断是否跨越虚拟线。方向判断用center_y prev_y表示从上往下走计为进入。参数说明虚拟线的位置要选在画面中段避开边缘畸变区域track_history要定期清理否则内存会涨。区域计数用cv2.pointPolygonTest判断轨迹中心是否在多边形内逻辑类似。两种方法都要注意轨迹 ID 切换会导致重复计数所以max_age不能设太小否则同一个人断成两个 ID 会被计两次。4. WebApp 层把检测、跟踪、统计串成可访问的服务4.1 后端选型Flask OpenCV 推流 vs FastAPI WebSocketWebApp 的核心需求是浏览器能看到实时画面能查历史统计。常见做法有两种Flask MJPEG 推流或者 FastAPI WebSocket 推流。Flask 简单但 MJPEG 是长连接并发一高就卡FastAPI 的 WebSocket 更灵活但代码复杂度高。我一般先用 Flask 快速搭原型验证逻辑没问题再换 FastAPI。from flask import Flask, Response, render_template, jsonify import cv2 import threading app Flask(__name__) output_frame None lock threading.Lock() def process_video(): global output_frame cap cv2.VideoCapture(test.mp4) while True: ret, frame cap.read() if not ret: cap.set(cv2.CAP_PROP_POS_FRAMES, 0) continue # 这里调用 Yolov5 DeepSort 处理 frame # 处理后的 frame 赋值给 output_frame with lock: output_frame frame.copy() app.route(/) def index(): return render_template(index.html) app.route(/video_feed) def video_feed(): def generate(): while True: with lock: if output_frame is None: continue ret, jpeg cv2.imencode(.jpg, output_frame) yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n jpeg.tobytes() b\r\n) return Response(generate(), mimetypemultipart/x-mixed-replace; boundaryframe) app.route(/stats) def stats(): return jsonify({count_in: 100, count_out: 80}) if __name__ __main__: t threading.Thread(targetprocess_video) t.daemon True t.start() app.run(host0.0.0.0, port5000, threadedTrue)逻辑说明process_video在后台线程跑检测和跟踪output_frame是共享变量用锁保护。/video_feed返回 MJPEG 流浏览器用img src/video_feed就能显示。/stats返回 JSON 统计数字前端用 AJAX 轮询。参数说明host0.0.0.0让局域网可访问threadedTrue支持多请求并发。如果并发超过 10 路Flask 的 MJPEG 会撑不住这时候换 FastAPI WebSocket把帧编码成 base64 通过 WebSocket 推送前端用 canvas 渲染。代价是代码量翻倍但延迟和并发都好很多。4.2 前端页面实时画面 统计面板 历史查询前端不需要复杂框架一个 HTML 页面加几行 JavaScript 就够。实时画面用img标签指向/video_feed统计数字用setInterval每 2 秒请求/stats历史查询用表格展示。!DOCTYPE html html head title人流量监测/title /head body h1实时人流量监测/h1 img src/video_feed width640 height480 div p进入: span idcount_in0/span/p p离开: span idcount_out0/span/p /div script setInterval(function() { fetch(/stats) .then(response response.json()) .then(data { document.getElementById(count_in).innerText data.count_in; document.getElementById(count_out).innerText data.count_out; }); }, 2000); /script /body /html逻辑说明setInterval每 2 秒拉一次统计避免频繁请求。参数说明width640和height480要跟后端推流分辨率一致否则画面拉伸。历史查询可以加一个/history?date2024-01-01接口从 SQLite 或 CSV 里读数据返回。4.3 部署到树莓派 5性能取舍与抽帧策略树莓派 5 上部署这套系统CPU 推理 Yolov5s 大概 2-5 FPSDeepSort 再吃掉一部分实时性没法保证。我的做法是抽帧每 3 帧处理 1 帧中间帧复用上一帧的跟踪结果。这样显示还是 15 FPS但实际检测只有 5 FPS计数精度会掉一点但能跑起来。frame_count 0 process_interval 3 # 每 3 帧处理一次 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_count 1 if frame_count % process_interval 0: results model(frame, size320) # 树莓派上用 320 输入 # DeepSort 更新 tracks tracker.update_tracks(detections, frameframe) # 用上一帧的 tracks 画框 for track in tracks: # 画框逻辑 pass参数说明size320比 640 快 3-4 倍但小目标漏检率上升。process_interval3是平衡点设 5 会更卡但更省资源。树莓派 5 上还要关掉图形界面用sudo raspi-config设成命令行启动能省出 200MB 内存。5. 避坑与排查那些让我熬夜的 ID 跳变和计数重复5.1 坑一ID 频繁跳变同一个人被计多次现象视频里一个人走过去ID 从 1 变 2 再变 3计数直接翻三倍。原因max_age设太小人一被遮挡轨迹就删了重新出现时新建 ID或者max_cosine_distance设太大ReID 特征匹配错误。解决先把max_age提到 50再把max_cosine_distance降到 0.15观察 ID 是否稳定。如果还跳检查 Yolov5 的 conf 是不是太低误检导致轨迹混乱。5.2 坑二检测框抖动导致轨迹震荡现象人站着不动检测框在小范围内抖动DeepSort 的卡尔曼滤波跟不上ID 偶尔切换。原因Yolov5 的 NMS 后处理不稳定或者输入分辨率太低。解决把imgsz提到 640 以上iou设 0.5减少重叠框。如果还抖在检测后加一个简单的框平滑用上一帧的框和当前帧的框做加权平均。5.3 坑三WebApp 推流延迟越跑越高现象浏览器画面比实际慢好几秒而且越来越慢。原因Flask 的 MJPEG 流没有丢帧机制处理速度跟不上采集速度帧堆积在缓冲区。解决在process_video里加一个判断如果output_frame还没被消费就跳过当前帧。或者换 FastAPI WebSocket主动控制推流节奏。5.4 坑四树莓派上内存溢出现象跑几个小时后程序崩溃日志显示 OOM。原因track_history和 DeepSort 的nn_budget无限增长。解决定期清理track_history只保留最近 100 个 IDnn_budget设 50 而不是 100Yolov5 的max_det设 50 而不是 100。5.5 坑五夜间或逆光场景漏检严重现象白天计数正常晚上或逆光时人数少一半。原因预训练模型对低照度场景泛化差。解决用夜间数据微调 Yolov5或者加一个简单的图像增强直方图均衡化再推理。如果不想训练把 conf 降到 0.3但误检会上升需要人工复核。6. 进阶技巧用轨迹平滑和 ReID 缓存把计数误差压到 5% 以内如果你已经跑通基础版本想把计数精度再提一档我一般会做两件事轨迹平滑和 ReID 缓存。轨迹平滑是在 DeepSort 输出后加一个卡尔曼滤波或滑动平均把抖动的中心点修平越线判断就不会因为抖动反复触发。ReID 缓存是给每个确认的轨迹保存最近 10 帧的 ReID 特征当轨迹丢失又重新出现时先用缓存特征做一次匹配匹配上就复用原 ID而不是新建。from collections import deque import numpy as np # 轨迹平滑滑动平均 track_smooth {} # track_id - deque of centers smooth_window 5 def smooth_center(track_id, center): if track_id not in track_smooth: track_smooth[track_id] deque(maxlensmooth_window) track_smooth[track_id].append(center) return np.mean(track_smooth[track_id], axis0) # ReID 缓存保存每个轨迹的特征 reid_cache {} # track_id - list of features cache_size 10 def update_reid_cache(track_id, feature): if track_id not in reid_cache: reid_cache[track_id] deque(maxlencache_size) reid_cache[track_id].append(feature) def match_reid(feature): best_id None best_dist float(inf) for track_id, features in reid_cache.items(): for f in features: dist np.linalg.norm(feature - f) if dist best_dist: best_dist dist best_id track_id if best_dist 0.3: # 阈值根据场景调 return best_id return None逻辑说明smooth_center用滑动平均把中心点修平smooth_window5对应约 0.2 秒的平滑太大反应迟钝太小没效果。match_reid在轨迹丢失后重新出现时用 ReID 特征跟缓存匹配匹配上就复用 ID。参数说明cache_size10保存最近 10 帧特征best_dist 0.3是匹配阈值设太小匹配不上设太大误匹配。验证方法拿一段 10 分钟的视频人工数一遍真实人数然后跑系统统计算误差率。我实测下来基础版本误差在 10-15%加了轨迹平滑和 ReID 缓存后能压到 5% 以内。如果还不行检查虚拟线位置是不是在画面边缘边缘畸变会导致轨迹中心偏移。最后说个血泪教训别在树莓派上跑 640 输入的 Yolov5m我试过一次帧率掉到 1 FPSWebApp 直接卡死。边缘设备就老老实实用 yolov5n 320 输入 抽帧精度不够就加数据微调别跟硬件较劲。希望帮到你。本文还有配套的精品资源点击获取
返回列表