ARTICLE DETAIL

资讯详情

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

YOLOv5+DeepSort人流量监测WebApp开发实战

YOLOv5+DeepSort人流量监测WebApp开发实战 简介面向智能监控与计算机视觉开发者的人流量监测WebApp项目包基于Yolov5完成人体目标检测再以DeepSort实现跨帧持续追踪用于园区、商超、景区等场景的实时人流统计。资源共318个文件压缩包约64.37MB核心代码以Python脚本为主配合yaml/yml配置、pth和pt模型权重、Docker部署文件以及mp4演示视频和图片样例目录涵盖检测、追踪、推理与应用界面等模块便于直接复现和二次开发。已有289人浏览学习。资源提供了完整的工程化实践从模型权重、追踪配置到Streamlit可视化页面均能快速衔接用户可上传视频或接入摄像头流实时查看检测框、轨迹和流量统计结果适合需要落地智能监控方案、学习Yolov5与DeepSort联合使用的人员。1. 人流量监测 WebApp 不是“模型能出框”就完事麻烦全在跟踪和管道上如果你在商场、车站或展厅做过视频客流统计多半遇到过这两个尴尬事检测框画得漂漂亮亮报表上还是一团乱麻人一多就重数漏数好不容易算出一个数字后端同事又追问这是进门数还是停留人数口径都对不上。这套基于 YOLOv5 加 DeepSort 的人流量监测 WebApp把目标检测、多目标跟踪、统计逻辑和 Web 可视化串成一条完整管线前端打开一个地址就能看到实时画面和计数后端把每一帧的检测框交给 DeepSort 维护 ID再把中心点轨迹喂给越线算法最后用 Flask 把带框的视频流和服务状态推出去。这套方案适合三种人只跑通过 YOLOv5 检测、但没接过多目标跟踪的入门者想给自己的数据集做个“视频级”落地 Demo 的算法工程师以及需要快速交付客流统计原型给老板看的一线开发。难点不在模型本身而在检测层与跟踪层怎么配合、统计逻辑怎么定义、Web 层怎么不把服务器拖垮。2. 用 YOLOv5 训练自己的行人数据集conda 环境、标签与四个超参数2.1 conda 环境配置与数据集目录结构labels 和 images 必须一一对应工欲善其事先建环境。我一般习惯用 conda 单独建一个 Python 3.8 环境跑 YOLOv5避免和系统 Python 互相污染尤其当你机器上还装了其他深度学习框架时这一步能省掉大量“装完 PyTorch 崩了 OpenCV”的血泪时间。YOLOv5 官方仓库对 Python 版本不挑3.8 到 3.10 都能跑但 PyTorch 版本要和 CUDA 匹配建议先装 PyTorch再装 requirements顺序反了容易遇到 torch 和 torchvision 版本对不上的玄学问题。conda create -n yolov5 python3.8 -y conda activate yolov5 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txtpip 安装 PyTorch 时带上--index-url是让 conda 环境内的 pip 从 PyTorch 官方源拉对应 CUDA 11.7 的预编译包比在 conda 里硬解依赖快得多。如果机器没有独立显卡把cu117换成cpu即可推理还能跑训练速度会感人到让你想换台机器。数据集目录是 YOLOv5 最容易踩坑的地方。它要求 images 和 labels 两个目录严格一一对应且每张图片的标注文件是同名.txt不是 xml 也不是 json。我通常直接用 labelImg 标注导出 YOLO 格式后手动整理成下面的结构datasets/ person/ images/ train/ img_0001.jpg val/ img_0002.jpg labels/ train/ img_0001.txt val/ img_0002.txt每个.txt文件里每行代表一个目标class_id cx cy w h前两个是中心点坐标后两个是宽高全部归一化到 0~1。最容易翻车的地方是class_id必须从 0 开始编号且和data.yaml里的names列表一一对应。你训行人检测names就写成[person]nc: 1别把行人当第 1 类YOLO 从第 0 类开始数。2.2 用 yolov5s 起步train.py 命令、超参数与导出部署格式数据准备好了就开训。这里不要上来就选 yolov5x训练时间是小模型的好几倍收益在行人检测这种类别单一的任务上很有限。yolov5s 是速度和精度的甜点先跑通流程再回头换 m 或 l。python train.py --data data.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --device 0--img 640是训练时喂给网络的输入尺寸也是推理时图像被 resize 到的尺寸训练和推理不一致会导致精度打折后面写推理代码时务必保持 640。--batch 16取决于显存8GB 显卡跑 16 有点紧张显存不够就先调成 8batch 减半学习率最好也相应减半否则收敛会抖。YOLOv5 的默认超参数文件是data/hyps/hyp.scratch-low.yaml新手不用大改但有两个参数值得动。第一个是lr0初始学习率默认 0.01小数据集上 0.02 反而收敛更快第二个是mosaic默认 1.0即每张训练图都由四张图拼成行人小目标多时这个增强非常有用但最后 10 个 epoch 建议关掉让模型适应真实构图。训练完成后runs/train/exp目录下会有训练曲线和最优权重best.pt这就是后续所有环节的输入。接着把权重导出成 TorchScript 格式部署时不必依赖 YOLOv5 完整的 Python 环境加载更快踩的包冲突也少python export.py --weights runs/train/exp/weights/best.pt --include torchscript导出成功后生成best.torchscript推理脚本里用torch.jit.load加载输入输出格式和原模型一致但少了 Python 侧的前处理灵活性因此图像的归一化和 letterbox 操作必须在外部手动做这个后面详细说。2.3 推理参数 conf_thres 与 iou_thres直接决定后端计数质量的“玄学”开关训练只是第一步部署推理时有两个参数比模型本身更影响人流量统计结果那就是conf_thres和iou_thres。它们在不同光照、不同人流密度下最合适的取值完全不一样调的时候请按场景实测别直接沿用 COCO 预训练权重的默认值。import cv2 import torch model torch.jit.load(best.torchscript) model.eval() model.cuda() def letterbox(img, new_shape640): h, w img.shape[:2] r min(new_shape / h, new_shape / w) new_unpad int(round(w * r)), int(round(h * r)) dw, dh (new_shape - new_unpad[0]) / 2, (new_shape - new_unpad[1]) / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value(114, 114, 114)) return img, r, dw, dh cap cv2.VideoCapture(stream_url) while True: ret, frame cap.read() if not ret: break img, ratio, dw, dh letterbox(frame) blob img[:, :, ::-1].transpose(2, 0, 1) blob torch.from_numpy(blob).float() / 255.0 blob blob.unsqueeze(0).cuda() with torch.no_grad(): preds model(blob)[0] # preds 形状为 [N, 6]每行是 cx, cy, w, h, conf, class_id dets preds[preds[:, 4] 0.25] # 注意这里只是示例实际还需要把坐标从 letterbox 缩放回原图conf_thres设太低比如 0.1检测框会多到让人怀疑人生——远处的人影、椅背、广告牌上的人形图案全被当成行人跟踪器被大量假目标填满ID 切换毫无规律。conf_thres设太高比如 0.7漏检率上升人走到近处才被框上越线统计的触发时间延后在人流量大的时刻误差能到 10% 以上。我的做法是先拿目标场景的 200 张截图跑一遍推理统计置信度的分布再把阈值定在分布低谷处。行人检测一般 0.35 到 0.45 之间属于合理区间安全帽等小目标数据则要放宽到 0.25 附近这也是热词里“yolov5 安全帽数据集”踩坑最集中的地方。iou_thres影响的是 NMS 对重叠框的抑制力度两个行人挨在一起时这个参数尤为关键。默认 0.45 偏保守重叠大的框会被误删一个调高到 0.6 能保留更多重叠框但同一目标出现双框的概率也上升。和 conf 一样没有万能值只有针对你的场景反复试出来的“够用值”。3. DeepSort 跟踪层从卡尔曼滤波到三个核心参数3.1 为什么人流量监测不能只依赖单帧检测如果只把 YOLOv5 的检测框画出来人流量 WebApp 只能算一个“会画框的播放器”因为统计必须回答一个跨帧问题这一秒画面里的人和上一秒画面里的人是不是同一个人单帧检测没有这个概念它只看当前画面人走到哪里、是否被遮挡、是否出画再入画这些信息全部丢失计数自然一片混乱。DeepSort 在这里的做法是经典的两段式卡尔曼滤波做运动预测匈牙利算法做检测框和已有轨迹的最优匹配。每个跟踪目标在内存里保存一段“外观特征”本质是经过特征提取网络得到的 128 维向量新一帧的检测框会先被预测位置筛选再和轨迹特征做距离匹配距离小于阈值的才认为是同一个人。这套机制让人流量监测在遮挡、短暂消失、交叉行走这些常见干扰下依然能维持 ID 的连续性。市面上很多人讲 DeepSort 上来就贴结构图但实际操作中真正影响你项目效果的反而是它的“性格”它对检测质量极其敏感。检测帧率抖一下卡尔曼预测的位置就偏检测框时有时无ID 就分分钟换人。所以在做 DeepSort 改进或调参之前先确保检测层输出足够干净再谈跟踪效果顺序不能反。3.2 max_dist、max_age、min_hits 怎么改才不丢 IDDeepSort 默认参数文件deep_sort/configs/deep_sort.yaml里有几个直接影响统计效果的参数大部分人只调这三个就够。第一个是max_dist控制检测框和轨迹之间外观特征距离的上限默认 0.2。这个值在行人场景里偏小当分辨率不够高、行人穿着相近时同一个人连续两帧的特征距离可能超过 0.2导致匹配失败 ID 切换。我在实际项目里常用 0.3效果明显改善。max_dist: 0.3 max_iou_distance: 0.7 max_age: 60 n_init: 3 nn_budget: 100max_iou_distance是 IoU 距离的阈值和外观特征配合使用检测框和预测位置的 IoU 太小说明两者空间上离得远匹配概率降低。默认 0.7 适用于一般监控场景如果相机角度比较俯视行人框之间本来就挨得近这个值要降到 0.5 附近否则两个相邻行人会被错误关联。max_age是轨迹丢失后保留的最大帧数默认 70这意味着一目标连续 70 帧没有匹配到检测框后才会被永久删除。人流量监测里max_age要调大人到遮挡物后面走几秒钟是常态默认值会让人刚出画就“失忆”再出现时当成新人计数。我常用的值是 60 到 90帧率 30 时大概对应 2 到 3 秒的容忍窗口。n_init类似 min_hits指新轨迹在确认前需要连续匹配到的帧数默认 3。它的作用是过滤单帧误检一个假目标如果只出现一帧就永远升不了级不会被计数。但反向坑也随之而来一个人从画面边缘快速穿过只被检测到两帧就无法形成轨迹计数直接漏掉。快速通行场景把n_init改为 1 或 2代价是假目标变多需要用conf_thres一起兜底。3.3 把检测结果喂给 DeepSort 的最小 Python 流程DeepSort 的使用不复杂但要搞清楚每个步骤在干什么。常见做法是加载一个预先训练好的ckpt.t7权重文件实例化DeepSort对象然后在视频循环里依次做检测、坐标格式转换、update最后拿到带 ID 的跟踪结果再把坐标还原到原图。from deep_sort.deep_sort.deep_sort import DeepSort # 将 YOLOv5 的 xywh 归一化坐标转换为 DeeoSort 要求的 xyxy 像素坐标 def xywh_to_xyxy(det, w, h): cx, cy, bw, bh det[:4] cx, cy cx * w, cy * h bw, bh bw * w, bh * h return [cx - bw / 2, cy - bh / 2, cx bw / 2, cy bh / 2] deepsort DeepSort(deep_sort/deep/checkpoint/ckpt.t7, max_dist0.3, max_iou_distance0.7, max_age60, n_init3, nn_budget100) # 假设 dets 已经过滤过低置信度每行为 cx, cy, w, h xyxys [xywh_to_xyxy(d, frame_w, frame_h) for d in dets] confidences dets[:, 4].tolist() class_ids dets[:, 5].tolist() outputs deepsort.update(xyxys, confidences, class_ids, frame) for x1, y1, x2, y2, track_id, cls in outputs: # x1 y1 x2 y2 已映射回原图坐标 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, fID:{track_id}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)update是 DeepSort 对当前帧做全部状态更新的入口它内部会先做运动预测再用级联匹配把检测框和轨迹关联起来。xyxys、confidences、class_ids三个列表的长度必须一致否则索引错位会让跟踪逻辑拿到错配的数据。输出里的track_id就是后面统计进出的身份证它一旦跳变前面所有的技术栈都会在报表层面上功亏一篑。这段代码跑通后建议把frame_w和frame_h的获取放在视频流初始化之后不要用检测图尺寸替代原图尺寸否则坐标会有 letterbox 和缩放偏差越线判断会整体偏一个身位。4. 人流量统计与 WebApp 串起来越线计数、状态缓冲与视频流4.1 越线脉冲计数 vs 区域驻留人数用一张表选定统计口径把跟踪 ID 拿到手之后人流量统计就变成纯粹的工程问题了。两种最常见的口径越线计数和区域驻留人数。它们对应完全不同的业务需求选错口径会让你做的 WebApp 被客户一眼看穿“这东西不顶用”。统计口径输出结果业务含义典型实现坑点越线脉冲计数“今日进店 1256 人出店 1189 人”人流通过某条线中心点轨迹跨线判定人来回走会重复计数区域驻留人数“当前店内 67 人”某个区域实时人数跟踪 ID 在区域内出现即标记区域边界抖动会误计越线计数适合商场出入口、场馆闸机核心在于统计“穿过”动作而不是“在场”状态区域驻留适合展厅、门店内部能直接算出停留时长。有些项目两个都要那就在后端同时维护两组状态一个共享轨迹缓冲两套计数器互不干扰。需要提醒的是统计口径要在项目启动时就定死并写进接口文档否则前后端联调时一定会吵翻。4.2 用中心点轨迹做跨线判定叉积检测与滞回逻辑越线判定的具体算法不复杂但有两个细节比算法本身更值得上心一是用检测框底部中心点而不是框中心点因为人在地面上的脚才是确定跨线时刻的关键二是必须加滞回逻辑即目标跨越后再回到线同一侧不会立刻触发第二次计数。prev_pts {} # track_id - 上一帧的中心点坐标 def cross_line(p1, p2, line_start, line_end, hysteresis_dist15): # p1 上一帧中心点, p2 当前帧中心点, line_start/line_end 是线段端点 # 先用叉积判断 p1 和 p2 分别在线段的哪一侧 def side(p): return (line_end[0] - line_start[0]) * (p[1] - line_start[1]) - \ (line_end[1] - line_start[1]) * (p[0] - line_start[0]) s1, s2 side(p1), side(p2) # 异号代表穿越了线段所在的直线 if s1 * s2 0: # 计算穿越点到线段端点的距离低于阈值才真正触发 cp line_start if abs(s1) abs(s2) else line_end dist ((p2[0] - cp[0]) ** 2 (p2[1] - cp[1]) ** 2) ** 0.5 return dist hysteresis_dist or True return False叉积判断的数学原理是对线段 AB 和点 P(B-A) × (P-A)的符号代表 P 在 AB 的哪一侧连续两帧异号说明目标穿过了直线。但直线穿越不等于真正“越过线段”如果人流从线段延长线附近经过叉积也会报异号所以我把line_start到line_end的距离也纳入判断并用hysteresis_dist给一个缓冲。滞回逻辑写进prev_pts维护里只有在目标完成一次完整跨线后才记录本次跨越状态目标在线附近来回抖时不会重复计数。完成一次计数后要把它从实时统计状态中清除让它重新成为一个“无状态”目标否则它会带着旧状态再次触发计数。这个状态机的实现并不复杂但漏了它你可能在高峰期看到计数每帧涨十几次的奇观。4.3 Flask 后端最小骨架跟踪循环放在线程里用状态对象缓冲输出WebApp 最常见的错误做法是让 Flask 路由里直接跑cap.read()和模型推理。这样只要有一个浏览器打开页面视频流就启动一套完整的检测跟踪线程打开两个浏览器就跑了双份服务器很快被 GPU 显存和 CPU 占满卡成幻灯片。更克制且可扩展的做法是视频流处理只启动一个后台线程读取到的每一帧实时更新一个全局状态对象Flask 路由只负责把这个状态的画面或数据拿出去。import cv2 import threading from flask import Flask, Response, jsonify app Flask(__name__) class TrackerState: def __init__(self): self.frame None self.count_in 0 self.count_out 0 self.lock threading.Lock() state TrackerState() def video_loop(): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break # 这里调用检测 DeepSort 越线统计更新 count_in / count_out with state.lock: state.frame frame time.sleep(0.03) # 控制处理帧率省 CPU threading.Thread(targetvideo_loop, daemonTrue).start() app.route(/metrics) def metrics(): with state.lock: return jsonify(instate.count_in, outstate.count_out) app.run(host0.0.0.0, port5000)视频循环线程和 Flask 主线程之间通过state.lock共享数据这是防止多线程环境里读到半帧状态的关键保护。daemonTrue的作用是让这个线程随主进程退出而终止不然终端 CtrlC 之后进程还在后台偷偷跑下次启动端口被占用排查半天才发现是昨天那个线程没杀掉。0.0.0.0监听地址意味着局域网内其他机器也能访问。如果你只在本地调试写成127.0.0.1更安全否则在公司网络里相当于把摄像头画面裸奔给全公司看。这个细节看需求不是什么金科玉律。4.4 前端播放视频流MJPEG 推流的实现与代价带检测框的视频要推到浏览器最省事的方式是 MJPEG 流也就是 Flask 把每一帧 JPEG 编码后连成一串multipart/x-mixed-replace响应前端一个img标签就能播放。它不像 WebRTC 要建复杂的信令和媒体协商也不像 HLS 有秒级延迟适合这种只想快速看到效果的内部系统。app.route(/stream) def stream(): def generate(): while True: with state.lock: if state.frame is None: continue ret, jpeg cv2.imencode(.jpg, state.frame) if not ret: continue frame_bytes jpeg.tobytes() yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n frame_bytes b\r\n) return Response(generate(), mimetypemultipart/x-mixed-replace) app.run(host0.0.0.0, port5000, threadedTrue)前端只需要一个img srchttp://服务器IP:5000/stream剩下的交给浏览器自己解析。这里有两个代价必须接受JPEG 编码是 CPU 密集操作imencode每帧都执行分辨率 1080P 下会吃掉一个 CPU 核的相当一部分算力MJPEG 没有音频同步也没有自适应码率网络差时延迟会累积。所以我在实际项目里通常把传给前端的帧先压缩到 640 宽既保住画质又省一半编码时间后端对人的跟踪仍然在原始分辨率上进行互不影响。5. 避坑排查从检测、跟踪到 WebApp 的三层“翻车”现场5.1 检测层踩坑训练集里只有“行人”镜头一变就漏检现象是模型在训练集验证集上 mAP 很高换到真实摄像头场景后远处行人漏检严重坐姿、俯视视角、逆光下的行人经常完全检测不到。原因有两层一是训练数据的地域和场景分布太单一全是白天平视视角的街拍图二是预处理里没有对光照变化做足够的随机增强。解决方式也分两步。第一步数据层面做「场景补充困难样本挖掘」从目标现场录 20~30 分钟视频抽帧用当前模型跑一遍把置信度在 0.2~0.5 之间的框全部人工复核误检和漏检的样本补充进数据集重训一版。第二步算法层面把hyp.scratch-low.yaml里的hsv_h、hsv_s、hsv_v增强幅度调大逆光场景下模型能更好地适应亮度偏移。这个坑几乎每个做真实落地的人都会踩早踩早修数据别再靠调阈值硬扛。5.2 跟踪层踩坑ID 换得比人走得还快现象是人流一密集画面上同一个人的 ID 在几秒内跳变多次越线计数乱成一锅粥。原因是 DeepSort 的级联匹配里外观特征max_dist设得太严加上检测框本身在人群遮挡时忽大忽小卡尔曼预测的框位和实际检测框 IoU 不达标匹配失败后系统认为是新目标。解决时先用排除法判断是检测抖动还是跟踪参数问题把跟踪结果输出成视频慢放观察如果 ID 跳变总是发生在目标被遮挡或检测框明显缩小之后说明检测不稳定优先把iou_thres从 0.45 提到 0.6让重叠框保留得更充分如果检测框明明稳定但 ID 依然跳那就把max_dist从 0.2 放宽到 0.3同时把max_iou_distance从 0.7 调到 0.6。调参之后看 ID Switch 数量是否明显下降衡量标准不是感觉而是下一章要说的实测指标。5.3 WebApp 层踩坑浏览器越放越卡服务器内存爆掉现象是打开页面后内存占用随运行时间线性增长几个小时后服务器卡到连视频都推不动。原因基本是检测跟踪循环里每一帧都在创建新的torch.Tensor、cv2.MatPython 的垃圾回收来不及释放叠加 Flask 的threadedTrue模式下每次断连的生成器没有正确关闭旧连接残留。解决方式按优先级来第一在generate函数里把yield包进try/finally连接断开时显式退出循环并释放当前帧引用第二每帧处理完主动将大对象置空不让上一帧的 Tensor 滞留在变量作用域里第三push 到浏览器的帧做分辨率降采样大幅减少 JPEG 缓冲的内存占用。这类问题看不出“报错”只能观察内存曲线建议在服务器上挂一个简单的psutil监控脚本内存超过某个阈值自动报警别等客户打电话来说页面打不开。6. 验收与调优从 FPS、ID Switch 到边缘设备部署的实测尺子6.1 三个指标量化系统健康度移植完成后必须用数字说话不能靠“感觉还行”。我习惯在每一版系统上记录三个指标FPS 代表处理速度ID SwitchIDSW代表跟踪稳定性计数误差代表业务指标准确性。测量方法很简单FPS 在视频循环里统计每秒实际处理的帧数IDSW 需要把同一段测试视频的跟踪轨迹可视化人工数出 ID 切换次数或者到 MOT 数据集的标准评测工具里跑一遍计数误差则用人工数人数和系统计数的差值百分比表示。指标测量方式健康范围FPS视频循环内每秒实际处理帧数实时相机场景需 ≥ 12离线处理 ≥ 25ID Switch100 帧内同目标 ID 切换次数轻人流 ≤ 2 次中高人流 ≤ 5 次计数误差|系统计数 - 人工计数| / 人工计数≤ 5% 以内可接受这三个指标是互相制约的FPS 高了检测跳帧可能让 IDSW 恶化IDSW 改善了计数误差却不一定同幅度下降因为计数逻辑本身的滞后也可能漏人。我通常按“业务指标优先”的顺序调先保证计数误差达标再回头优化速度。6.2 边缘部署的降级路线这套系统如果想部署到树莓派 5 这类边缘设备上原模原样跑权重肯定不够用降级思路有三个层次。第一是换轻量主干把 YOLOv5s 的 backbone 换成更小的网络结构或者直接用蒸馏后的模型第二是降低输入分辨率行人检测 416 分辨率下 mAP 损失有限FPS 能翻倍第三是改用 TensorRT 或 NCNN 做推理加速DeepSort 的特征提取网络也可以量化成 INT8。这三步组合下来树莓派 5 上通常能跑到 10~15 FPS对非实时的人流统计已经够用。最后提一个我的个人习惯每个版本上线前保留一份“版本状态表”记录模型文件、conf_thres、max_dist、max_age、实测 FPS 和 IDSW。三个版本跑下来你就能清楚看到哪种改动真正提升了效果而不是被某次偶然的调参结果带偏方向。希望你早日养出自己的一套调参手感也少走几趟我走过的弯路。本文还有配套的精品资源点击获取
返回列表