资讯详情

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

📅 2026/9/28 14:58:13 | 华诺云谱 👁 阅读
YOLOv5+DeepSort人流量监测实战:从检测跟踪到WebApp部署
简介这份资源面向计算机视觉入门与进阶开发者、智能监控方向的学生及工程人员提供一套可直接部署的人流量监测WebApp完整方案解决公共场所实时人流统计与轨迹追踪问题。项目以Yolov5作为目标检测器识别视频帧中的人体配合DeepSort追踪算法通过卡尔曼滤波预测与外观特征匹配实现跨帧同一人轨迹关联并借助Streamlit搭建交互式网页界面支持上传视频或接入摄像头流实时查看统计结果。压缩包共318个文件约64.37MB以163个py源码、52个yaml配置、13个md说明文档为主另含pth与pt模型权重、sh启动脚本、Dockerfile及少量cpp、cu加速代码覆盖检测、追踪、界面与部署各环节。目前已有289人学习下载。读者可获取完整工程源码、预训练权重与配置模板理解Yolov5多尺度预测、DeepSort特征提取与Streamlit可视化整合思路快速复现并二次开发智能监控应用。1. 从一段监控视频说起YOLOv5 DeepSort 做人流量监测到底在解决什么商场出入口的摄像头每天都在产生视频流但绝大多数只能做到录像存证真正想知道今天进来了多少人、哪个时段最拥挤、有没有人逆行靠人工回看根本不现实。基于 YOLOv5 和 DeepSort 的人流量监测 WebApp要解决的就是把这段视频流变成可查询、可展示的客流数据。YOLOv5 负责每一帧里人在哪DeepSort 负责跨帧把同一个人认出来并分配稳定 IDWebApp 负责把统计结果实时呈现出来。这套组合是目前落地成本最低、社区资料最全的方案之一适合有 Python 基础、想快速搭出可用原型的开发者也适合需要给门店或园区做客流看板的工程团队。它不追求学术上的 SOTA追求的是今天下午就能跑起来。2. YOLOv5 检测层模型选型、环境配置与推理参数怎么定2.1 为什么是 YOLOv5 而不是 v8 或 RT-DETR做客流监测检测器只需要认一个类person。这意味着模型容量不用太大推理速度才是第一优先级。YOLOv5 在这个场景下有三个现实优势一是工程成熟度高torch.hub一行就能加载导出 ONNX、TensorRT 的脚本都是现成的二是 n/s/m/l/x 五档模型覆盖了从树莓派到服务器的全部算力区间三是后处理逻辑简单NMS 之后直接拿 person 类的框喂给 DeepSort中间不需要任何格式转换。YOLOv8 精度确实更高但它的 API 封装更重自定义后处理比如按区域过滤、按置信度分档反而要多写胶水代码。RT-DETR 这类 Transformer 检测器在 CPU 上延迟明显不适合边缘部署。我一般会先跑 YOLOv5s如果遮挡严重再升到 YOLOv5m很少直接上 l 或 x——人流量场景里检测框稍微抖一点DeepSort 的卡尔曼滤波能兜住但帧率掉下来跟踪就连不上了。2.2 conda 环境配置与依赖版本锁定YOLOv5 对 PyTorch 和 CUDA 版本比较敏感用 conda 隔离环境是基本操作。下面这套配置在 CUDA 11.8 RTX 3060 上验证过CPU 环境把 torch 换成 CPU 版即可。# 创建独立环境Python 3.9 是 YOLOv5 兼容性最好的版本 conda create -n crowdflow python3.9 -y conda activate crowdflow # 安装 PyTorchCUDA 11.8 对应 cu118 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 克隆 YOLOv5 源码并安装依赖 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 验证环境 python -c import torch; print(torch.cuda.is_available())逻辑说明先建环境再装 torch避免 conda 自动解析依赖时把 torch 降级。requirements.txt里锁定了 numpy、opencv-python 等版本不要手动升级。参数说明python3.9是因为 3.10 以上部分依赖轮子不全cu118要和本机驱动匹配用nvidia-smi看右上角 CUDA Version驱动版本低于 11.8 就换 cu117。2.3 推理参数conf、iou 和 imgsz 的取值逻辑YOLOv5 推理时有三个参数直接决定检测质量和速度很多人直接默认值跑结果要么漏检要么框抖动。参数默认值人流量场景建议影响conf0.250.35 ~ 0.45太低会引入背景误检DeepSort 会为噪声分配 IDiou0.450.5 ~ 0.6人群密集时适当调高减少重叠框被误删imgsz640640 或 960960 提升小目标召回但显存和延迟上升约 40%conf 调到 0.4 左右是我在商场场景的常用值。原因是 DeepSort 的匹配依赖检测框质量一个置信度 0.3 的误检框会让跟踪器凭空多出一个 ID而这个 ID 可能存活几十帧才消失直接污染客流计数。宁可漏检一两帧也不要引入假阳性。import torch # 加载模型force_reload 确保用本地权重 model torch.hub.load(ultralytics/yolov5, yolov5s, force_reloadFalse) model.conf 0.4 # 置信度阈值 model.iou 0.55 # NMS IoU 阈值 model.classes [0] # 只保留 person 类COCO 里 person 是 0 # 推理 results model(frame, size640) # 提取 person 检测框格式 [x1, y1, x2, y2, conf, cls] detections results.xyxy[0].cpu().numpy()逻辑说明model.classes [0]是关键一步它让 NMS 阶段就过滤掉非人类减少后续计算量。results.xyxy[0]返回的是绝对坐标DeepSort 需要的就是这个格式。参数说明size640对应 imgsz如果视频分辨率是 1080p640 缩放后远处的人可能只有十几个像素这时候要升到 960。2.4 用自己数据集微调 YOLOv5 的触发条件COCO 预训练的 YOLOv5 对正面、侧面的人检测已经够用但有两种情况必须微调一是俯拍角度摄像头装在天花板正上方人的形态和 COCO 差异大二是特殊场景比如工地安全帽、医院白大褂需要区分人员类型。微调的数据量不用多每个场景 500 到 1000 张标注图就能明显改善。# 数据目录结构 # datasets/crowd/ # images/train/ images/val/ # labels/train/ labels/val/ # 创建 data.yaml cat data.yaml EOF path: ./datasets/crowd train: images/train val: images/val nc: 1 names: [person] EOF # 开始训练基于预训练权重 python train.py --img 640 --batch 16 --epochs 100 \ --data data.yaml --weights yolov5s.pt \ --project runs/crowd --name exp1逻辑说明--weights yolov5s.pt是迁移学习比从头训练收敛快得多。--img 640要和推理时的 imgsz 一致否则训练和推理的尺度分布不匹配。参数说明--batch 16在 8G 显存下比较稳显存不够降到 8--epochs 100配合早停实际通常 60 到 80 轮就收敛。训练完在runs/crowd/exp1/weights/best.pt拿权重替换推理时的模型路径即可。3. DeepSort 跟踪层ID 稳定性和计数逻辑的实现细节3.1 DeepSort 的匹配流程与三个关键阈值DeepSort 的核心是在卡尔曼滤波预测位置的基础上用外观特征和运动信息做两级匹配。第一级是级联匹配用余弦距离比较外观特征第二级是 IoU 匹配处理外观特征不明显的情况。整个流程有三个阈值决定跟踪质量。max_dist控制外观特征的最大余弦距离默认 0.2。这个值越小匹配越严格ID 切换越少但容易在遮挡后丢失目标。人流量场景建议 0.2 到 0.3因为人的外观在短时间遮挡前后变化不大。max_iou_distance控制 IoU 匹配阈值默认 0.7。人群密集时检测框重叠严重这个值可以适当放宽到 0.8。max_age是轨迹最大存活帧数默认 70。意思是目标消失 70 帧内还保留轨迹等待重新匹配。25fps 下约 2.8 秒对于人走出画面再回来是合理的。如果场景里人流动很快可以降到 30。from deep_sort_realtime.deepsort_tracker import DeepSort # 初始化跟踪器 tracker DeepSort( max_age30, # 轨迹存活帧数 n_init3, # 连续检测到 3 帧才确认轨迹 max_iou_distance0.8, # IoU 匹配阈值 max_dist0.25, # 外观特征余弦距离阈值 embeddermobilenet, # 外观特征提取网络 embedder_gpuTrue ) # 每帧更新 tracks tracker.update_tracks(detections, frameframe) for track in tracks: if not track.is_confirmed(): continue track_id track.track_id bbox track.to_ltrb() # left, top, right, bottom逻辑说明n_init3是防抖设计避免单帧误检直接生成轨迹。track.is_confirmed()过滤掉未确认的轨迹只对稳定目标计数。参数说明embeddermobilenet是轻量外观特征网络比默认的mobilenetv2快精度损失在人流量场景可接受。embedder_gpuTrue需要 torch 和 CUDA 可用否则设 False。3.2 越线计数的实现从轨迹到人数有了稳定 ID计数就变成判断轨迹是否跨越某条线。常见做法是在画面里画一条虚拟线当轨迹的中心点从线的一侧移动到另一侧时计数加一。这里有个坑同一个 ID 可能在线的两侧反复横跳导致重复计数。import numpy as np class LineCounter: def __init__(self, line_start, line_end): self.line_start np.array(line_start) self.line_end np.array(line_end) self.counted_ids set() # 已计数的 ID防止重复 self.total 0 def _side(self, point): # 用叉积判断点在线的哪一侧 line_vec self.line_end - self.line_start point_vec np.array(point) - self.line_start cross line_vec[0] * point_vec[1] - line_vec[1] * point_vec[0] return 1 if cross 0 else -1 def update(self, track_id, center): if track_id in self.counted_ids: return side self._side(center) # 记录每个 ID 的初始侧 if not hasattr(self, _sides): self._sides {} if track_id not in self._sides: self._sides[track_id] side elif self._sides[track_id] ! side: self.total 1 self.counted_ids.add(track_id)逻辑说明counted_ids集合是后悔药防止同一 ID 多次越线重复计数。_side用叉积判断方向比斜率法更稳定不受线段方向影响。参数说明line_start和line_end是画面里的两个点通常取画面中间水平线的两端。实际部署时这条线要避开人群停留区否则人在线附近徘徊会反复触发。3.3 跟踪 ID 跳变的三类原因和排查方法ID 跳变是人流量监测最头疼的问题表现为同一个人被分配了多个 ID计数虚高。根据我的踩坑经验原因分三类。第一类是检测框抖动。YOLOv5 在连续帧上对同一个人的框位置有轻微变化如果变化幅度超过 IoU 阈值DeepSort 会认为是新目标。解决方法是提高 conf 阈值或者在检测后加一个简单的框平滑。第二类是遮挡。人被人或物体挡住几帧外观特征中断重新出现时如果 max_age 太小轨迹已经删除。解决方法是适当增大 max_age但代价是误匹配风险上升。第三类是外观特征失效。穿同样颜色衣服的人并排走MobileNet 提取的特征区分度不够。这时候可以换更强的 ReID 模型或者引入运动方向约束——同一个人短时间内运动方向不会突变。排查时我一般会先把跟踪结果可视化把每个 ID 的轨迹画出来看跳变发生在哪些帧、哪些位置。如果是固定位置跳变多半是遮挡如果是随机跳变多半是检测抖动。4. WebApp 层从视频流到实时看板的技术选型4.1 后端框架选型Flask 还是 FastAPIWebApp 的核心需求是实时推送计数结果到前端。Flask 生态成熟但原生不支持 WebSocketFastAPI 原生支持异步和 WebSocket更适合这个场景。如果只是轮询展示Flask 也够用但轮询延迟高客流看板体验差。我一般用 FastAPI WebSocket后端跑一个视频处理线程每处理完一帧就把计数结果推给所有连接的客户端。视频流本身用 MJPEG 推送到前端 img 标签延迟在局域网内可以接受。from fastapi import FastAPI, WebSocket from fastapi.responses import StreamingResponse import cv2, asyncio, json app FastAPI() counter LineCounter([(0, 360), (1280, 360)]) # 画面中间水平线 def process_frame(frame): # YOLOv5 推理 DeepSort 更新 计数 detections model(frame).xyxy[0].cpu().numpy() tracks tracker.update_tracks(detections, frameframe) for track in tracks: if track.is_confirmed(): bbox track.to_ltrb() center ((bbox[0]bbox[2])/2, (bbox[1]bbox[3])/2) counter.update(track.track_id, center) return frame def gen_frames(): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break frame process_frame(frame) _, buffer cv2.imencode(.jpg, frame) yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n buffer.tobytes() b\r\n) app.get(/video) def video_feed(): return StreamingResponse(gen_frames(), media_typemultipart/x-mixed-replace; boundaryframe) app.websocket(/ws/count) async def count_ws(websocket: WebSocket): await websocket.accept() while True: await websocket.send_text(json.dumps({total: counter.total})) await asyncio.sleep(1)逻辑说明gen_frames是 MJPEG 流生成器每帧编码成 JPEG 后按 multipart 格式推送。count_ws每秒推送一次计数前端用 WebSocket 接收更新。参数说明cv2.VideoCapture(0)是本地摄像头换成 RTSP 地址就是网络摄像头。asyncio.sleep(1)控制推送频率太高会占带宽太低看板更新不及时。4.2 前端看板三个必须显示的指标前端不需要花哨三个指标是刚需当前画面人数、累计进入人数、分时段统计。当前画面人数直接从 tracks 里数 confirmed 的数量累计进入人数用越线计数分时段统计用后端按分钟聚合。// WebSocket 接收计数更新 const ws new WebSocket(ws://localhost:8000/ws/count); ws.onmessage (event) { const data JSON.parse(event.data); document.getElementById(total).textContent data.total; document.getElementById(current).textContent data.current; }; // MJPEG 流直接塞进 img document.getElementById(video).src /video;逻辑说明WebSocket 负责数据MJPEG 负责画面两者独立。参数说明ws://localhost:8000要换成实际部署地址生产环境用 wss。前端每收到一次消息就更新 DOM不需要轮询。4.3 部署到树莓派 5 的可行性评估树莓派 5 的算力比 4 提升明显但跑 YOLOv5s DeepSort 仍然吃力。实测在树莓派 5 上YOLOv5n 用 ONNX Runtime 推理640 分辨率下单帧约 200ms也就是 5fps。DeepSort 的 MobileNet 特征提取再加 50ms。整体 4fps 左右对于人流量不大的场景勉强可用但人群密集时会丢帧。如果要在树莓派 5 上部署建议三个优化一是用 YOLOv5n 而不是 s二是导出 ONNX 并用 ONNX Runtime 推理比 PyTorch 快 30% 左右三是 DeepSort 的 embedder 换成更轻的模型或者干脆用 IoU 匹配代替外观匹配。另外树莓派 5 的 USB 带宽有限接多个摄像头要注意。5. 避坑与排查人流量监测上线后最容易翻车的五个点5.1 计数虚高同一人被分配多个 ID现象看板显示进入 200 人实际只有 120 人左右。原因DeepSort 的 ID 跳变常见于遮挡和检测框抖动。人群密集时一个人被挡住几帧重新出现后外观特征匹配失败生成新 ID。解决先提高 YOLOv5 的 conf 到 0.45减少低质量检测框再把 DeepSort 的 max_age 从 30 调到 50给遮挡后重匹配留时间最后在计数逻辑里加去重同一个位置附近短时间内新增的 ID 只计一次。5.2 画面卡顿推理和推流抢资源现象WebApp 画面延迟越来越大计数更新滞后。原因视频处理线程和 WebSocket 推送线程共享 GILPyTorch 推理时释放 GIL 不充分导致推流阻塞。解决把推理放在独立进程而不是线程用 multiprocessing 或把推理服务拆成单独的 FastAPI 实例WebApp 通过 HTTP 调用。或者用 ONNX Runtime它的 GIL 释放比 PyTorch 好。5.3 夜间或逆光漏检现象白天计数正常傍晚或逆光时人数明显偏少。原因YOLOv5 在 COCO 上训练低照度和高对比度场景下召回下降。解决在视频预处理阶段加自适应直方图均衡CLAHE提升暗部细节。如果场景固定用该场景的夜间数据微调 YOLOv5500 张图就能明显改善。5.4 越线计数在门口反复触发现象同一个人在门口进进出出计数反复加。原因虚拟线画在了人的停留区人在线附近徘徊时中心点反复穿越。解决把线画在远离停留区的位置或者加一个冷却时间——同一个 ID 计数后 3 秒内不再计数。更稳妥的做法是用双向计数进入和离开分开统计看板显示净值。5.5 内存持续增长直到崩溃现象服务跑几个小时就 OOM。原因DeepSort 的轨迹字典没有清理已删除的轨迹 ID 还留在内存里或者 MJPEG 流的缓冲区没有释放。解决定期检查 tracker 的 tracks 数量确认 max_age 生效在 gen_frames 里确保每帧的 buffer 被回收用tracemalloc定位内存增长点。我一般会在服务里加一个定时任务每小时打印一次内存占用和轨迹数量超过阈值就重启处理进程。6. 把计数误差压到 5% 以内一个可复现的调参流程这套方案上线后计数误差主要来自 ID 跳变和越线误判。我总结了一个调参流程能把误差压到 5% 以内前提是摄像头角度固定、光照变化不大。第一步录一段 10 分钟的测试视频人工数出真实人数作为基准。第二步用默认参数跑一遍把跟踪结果可视化统计 ID 跳变次数和位置。第三步按跳变原因调参遮挡导致的跳变调大 max_age抖动导致的调高 conf外观混淆导致的换 embedder。第四步重新跑测试视频对比计数结果误差大于 5% 就回到第三步。这里有个经验值max_age 每增加 10ID 跳变减少约 15%但误匹配增加约 5%。conf 每提高 0.05误检减少约 20%但漏检增加约 10%。这两个参数要一起调单独调一个容易顾此失彼。验证方法上我习惯用 A/B 对比同一段视频用两组参数各跑一遍把计数曲线画在一起看哪组的曲线更平滑、更接近人工计数。曲线毛刺多说明 ID 跳变频繁曲线整体偏低说明漏检严重。最后说一个我自己的习惯每次调完参我都会把当天的计数结果和人工抽查结果记在一个表格里跑一周后看误差分布。如果某天的误差突然变大多半是那天的光照或人流模式变了这时候要考虑加自适应逻辑而不是继续调固定参数。这套方案不值得追求 100% 准确但把误差稳定控制在 5% 以内对于门店客流分析、园区人流管理已经足够用了。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑