
简介这份资源是一套基于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人学习下载。读者可从中获得可直接运行的完整项目结构、模型权重与配置、Streamlit可视化页面以及Docker部署方案便于理解Yolov5多尺度预测与DeepSort外观特征匹配的工程落地方式并在此基础上二次开发或迁移到其他监控场景。1. 从一段监控视频到实时人数这套 Yolov5 DeepSort 的 WebApp 到底能干什么手里有一段商场门口或者园区闸机的监控视频想快速知道今天到底过了多少人靠人眼盯屏幕数显然不现实。这套基于 Yolov5 和 DeepSort 的人流量监测 WebApp解决的正是这个具体问题把视频丢进去浏览器里直接看到每个人被框出来、被分配一个 ID画面角落实时跳出累计人数。它适合两类人——一类是刚学完 Yolov5 目标检测、想找个完整项目把检测和追踪串起来的开发者另一类是需要快速搭一个演示级人流统计原型、验证算法可行性的工程人员。整个项目用 Streamlit 做前端省掉了写 HTML 和 Flask 路由的功夫核心推理逻辑集中在 Yolov5 检测器和 DeepSort 追踪器上压缩包里还带了 Dockerfile 和 openh264 动态库说明作者考虑过部署环节。但要注意它不是一个开箱即用的商业级产品而是一个结构清晰、方便你改参数和换模型的工程脚手架。2. Yolov5 检测层从权重加载到后处理的参数怎么定2.1 为什么选 Yolov5 而不是 YOLOv8 或 Faster R-CNN在这个项目里检测器的任务是逐帧找出画面中所有的人输出边界框和置信度。Yolov5 虽然已经不是最新版本但它的优势在于生态成熟、源码可读性强、社区里针对人流场景的预训练权重多。相比 Faster R-CNN 这类两阶段检测器Yolov5 的单阶段推理速度快得多在普通显卡甚至 CPU 上都能跑到可用的帧率。相比 YOLOv8Yolov5 的配置文件结构更直白改 anchor、改输入尺寸、改 NMS 阈值都不需要翻太多文档。项目里常见的做法是直接用yolov5s.pt或yolov5m.pt作为起点如果画面里人比较小、比较密就换成yolov5l.pt并适当提高输入分辨率。这里有一个容易被忽略的点Yolov5 的推理代码里conf_thres和iou_thres这两个参数直接决定了送进 DeepSort 的检测框质量。conf_thres设得太低误检框会大量涌入追踪器导致 ID 频繁切换设得太高远处的人漏检追踪轨迹断裂。我一般会在人流密集场景下把conf_thres设在 0.35 到 0.45 之间iou_thres保持在 0.45 左右然后根据实际画面微调。2.2 加载模型与推理的核心代码拆解项目里通常会有一个detector.py或者直接在app.py里封装 Yolov5 的调用。下面这段代码是我从类似项目里提炼出来的典型写法你可以直接对照压缩包里的源码看import torch import cv2 import numpy as np # 加载 Yolov5 模型这里假设权重文件放在项目根目录的 weights 文件夹下 # 常见做法是用 torch.hub.load 直接拉取但离线部署时更推荐指定本地路径 model torch.hub.load(./yolov5, custom, path./weights/yolov5s.pt, sourcelocal) model.conf 0.4 # 置信度阈值低于这个值的检测框直接丢弃 model.iou 0.45 # NMS 的 IoU 阈值控制重叠框的合并程度 model.classes [0] # 只保留 person 类COCO 数据集里 person 的索引是 0 def detect(frame): # Yolov5 接受 RGB 图像OpenCV 默认读进来是 BGR必须转换 img_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results model(img_rgb) # results.xyxy[0] 的格式是 [x1, y1, x2, y2, confidence, class] detections results.xyxy[0].cpu().numpy() # 只保留 person 类并且把置信度低于阈值的再过滤一遍双保险 detections detections[(detections[:, 5] 0) (detections[:, 4] model.conf)] return detections这段代码的逻辑很直接先把 OpenCV 读到的 BGR 帧转成 RGB因为 Yolov5 的预处理是按 RGB 训练的。model.classes [0]这行很关键它让模型只输出人的检测框避免把车、包、椅子也送进追踪器。results.xyxy[0]拿到的是绝对坐标格式是左上角和右下角后面 DeepSort 需要的正是这种格式。参数方面conf和iou可以直接在模型对象上改不需要重新加载权重。如果你发现画面里人挨得特别近NMS 把旁边的人框合并掉了就把iou调低到 0.3 左右如果发现同一个人出现两个框就把iou调高到 0.5 以上。这些调整不需要重新训练属于推理期的超参数改完立刻生效。2.3 输入尺寸与批处理的取舍Yolov5 默认的推理尺寸是 640x640项目里一般不会去改这个值因为改大了显存占用飙升改小了小目标漏检严重。但有一个细节值得注意如果监控画面是 1920x1080直接缩放到 640x640 会让人物变得很小尤其是画面远端的人可能只有十几个像素高。常见做法是先把画面裁剪成几个区域或者用imgsz参数把推理尺寸提到 960 甚至 1280代价是帧率下降。在 Streamlit 里做实时展示时我一般会保持 640 的输入尺寸因为 Web 端的刷新频率本身就有限制追求过高的单帧精度反而会让体验变卡。另外Yolov5 支持批处理推理但 DeepSort 是逐帧追踪的批处理在这里意义不大反而增加显存峰值所以项目里通常是单帧串行处理。3. DeepSort 追踪层卡尔曼滤波与外观特征怎么配合3.1 追踪器的工作流程与 ID 分配逻辑DeepSort 的核心任务是在 Yolov5 给出检测框之后判断当前帧的框和上一帧的框是不是同一个人。它的流程分四步第一步用卡尔曼滤波器根据上一帧的轨迹预测当前帧的位置第二步计算预测框和当前检测框之间的马氏距离得到一个运动匹配度第三步用预训练的 ReID 模型提取每个检测框的外观特征计算特征之间的余弦距离得到外观匹配度第四步把运动匹配度和外观匹配度加权融合用匈牙利算法做分配。如果某个检测框和所有已有轨迹的匹配度都低于阈值就新建一个轨迹如果某个轨迹连续多帧没有匹配到检测框就把它标记为丢失并最终删除。项目里通常会有一个deep_sort.yaml或者类似的配置文件里面有几个参数直接决定追踪效果。max_dist控制外观匹配的最大余弦距离默认 0.2调大之后更容易把不同的人关联到同一条轨迹但也会增加 ID 混淆的风险。max_iou_distance控制运动匹配的阈值默认 0.7。max_age决定轨迹丢失后保留多少帧默认 70在人流密集场景下可以适当调低到 30 左右避免已经离开画面的人还占着 ID。3.2 把 Yolov5 的检测结果喂给 DeepSort下面这段代码展示了如何把上一节的检测结果转换成 DeepSort 需要的输入格式并完成追踪更新from deep_sort_realtime.deepsort_tracker import DeepSort # 初始化 DeepSort 追踪器 # max_age 表示轨迹丢失后最多保留多少帧n_init 表示轨迹确认前需要连续匹配多少帧 tracker DeepSort(max_age30, n_init3, max_iou_distance0.7, max_dist0.2) def track(detections, frame): # DeepSort 需要的输入格式是 [[x1, y1, w, h], confidence, class] # 注意这里用的是宽高不是右下角坐标 bboxes [] for det in detections: x1, y1, x2, y2, conf, cls det w x2 - x1 h y2 - y1 bboxes.append([[x1, y1, w, h], conf, int(cls)]) # update_tracks 返回当前帧的所有活跃轨迹 tracks tracker.update_tracks(bboxes, frameframe) # 只保留已经确认的轨迹tentative 状态的轨迹不参与计数 active_tracks [t for t in tracks if t.is_confirmed()] return active_tracks这段代码里最容易翻车的地方是坐标格式。Yolov5 输出的是[x1, y1, x2, y2]而 DeepSort 的update_tracks期望的是[x1, y1, w, h]如果不做转换直接传进去追踪框会完全错位。n_init3表示一条轨迹需要连续三帧匹配上检测框才会被确认为有效轨迹这个参数在人流稀疏时可以调低到 1 或 2让计数更快响应在人流密集、误检较多时保持 3 或更高可以过滤掉一闪而过的假阳性。frame参数必须传原始 BGR 帧因为 DeepSort 内部要用它来提取外观特征如果传了缩放后的图或者灰度图ReID 特征的质量会大幅下降。3.3 计数逻辑怎么定义“一个人经过了”追踪本身不产生人数人数来自计数规则。项目里常见的做法是在画面中间画一条虚拟线当某个轨迹的中心点从线的一侧移动到另一侧时计数器加一。这个逻辑看起来简单但有几个细节要处理第一轨迹的中心点要用边界框的底部中心而不是几何中心因为人的脚部位置更能反映实际位移第二要记录每个轨迹上一次在线哪一侧只有发生侧别切换时才计数避免同一个人在线上来回晃动导致重复计数第三对于新创建的轨迹第一帧不参与计数判断因为不知道它从哪来。下面是一个简化的计数实现# 假设画面高度为 frame_height在中间画一条水平线 line_y frame_height // 2 # 用一个字典记录每个 track_id 上一次在线的哪一侧 track_side {} count 0 def count_people(active_tracks, frame_height): global count line_y frame_height // 2 for track in active_tracks: track_id track.track_id # 取边界框底部中心作为人的位置参考点 x1, y1, x2, y2 track.to_ltrb() center_x (x1 x2) / 2 center_y y2 # 底部中心 current_side below if center_y line_y else above if track_id in track_side: last_side track_side[track_id] # 只有从一侧跨到另一侧才计数 if last_side ! current_side: count 1 track_side[track_id] current_side return count这段代码里track.to_ltrb()返回的是左上角和右下角坐标和 Yolov5 的输出格式一致。center_y y2取的是底部纵坐标这样人走过线的时候脚先过线计数更符合直觉。track_side字典会随着轨迹的删除而残留旧 ID长时间运行后可能占用内存常见做法是定期清理不在活跃轨迹里的 ID或者直接用track_id作为键、用deque限制长度。计数线不一定非要在正中间如果画面里人是从左往右走可以画一条竖直线用center_x来判断。关键是计数线要避开画面边缘否则人在边缘徘徊时容易反复触发。4. Streamlit 界面与 Docker 部署从本地跑通到容器化4.1 Streamlit 的页面布局与视频流接入Streamlit 的好处是可以用纯 Python 写交互界面不需要碰前端框架。项目里通常有一个app.py里面用st.sidebar放参数控件用st.video或者st.image展示处理后的帧。下面是一个典型的页面结构import streamlit as st import cv2 import tempfile import os st.set_page_config(page_title人流量监测, layoutwide) st.sidebar.title(参数设置) conf_thres st.sidebar.slider(置信度阈值, 0.1, 0.9, 0.4, 0.05) iou_thres st.sidebar.slider(NMS IoU 阈值, 0.1, 0.9, 0.45, 0.05) uploaded_file st.sidebar.file_uploader(上传视频, type[mp4, avi, mov]) if uploaded_file is not None: # 把上传的文件写到临时文件OpenCV 需要从磁盘读取 tfile tempfile.NamedTemporaryFile(deleteFalse) tfile.write(uploaded_file.read()) cap cv2.VideoCapture(tfile.name) stframe st.empty() # 占位符后续用来不断更新画面 count_placeholder st.empty() while cap.isOpened(): ret, frame cap.read() if not ret: break detections detect(frame) tracks track(detections, frame) total count_people(tracks, frame.shape[0]) # 在帧上画框和 ID for t in tracks: x1, y1, x2, y2 t.to_ltrb() cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fID {t.track_id}, (int(x1), int(y1)-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # Streamlit 需要 RGB 格式 stframe.image(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB), channelsRGB) count_placeholder.metric(累计人数, total) cap.release() os.unlink(tfile.name)这里有几个实操要点。stframe st.empty()创建了一个占位符每次循环用stframe.image覆盖它这样页面不会堆积大量图片。count_placeholder.metric用来实时刷新人数比每次重新渲染整个页面流畅得多。视频处理完后要记得cap.release()和删除临时文件否则磁盘会慢慢被占满。如果要用摄像头流把cv2.VideoCapture(tfile.name)换成cv2.VideoCapture(0)就行但在 Docker 容器里访问摄像头需要额外映射设备这个后面会讲。4.2 Dockerfile 里的依赖与 openh264 动态库项目里带了 Dockerfile 和openh264-1.8.0-win64.dll说明作者考虑过跨平台部署。openh264 是 OpenCV 读取某些编码格式视频时需要的动态库Windows 下直接放在项目根目录或者系统 PATH 里就行。Linux 容器里则需要安装对应的.so文件或者用apt-get install libopenh264-dev。下面是一个典型的 Dockerfile 结构FROM python:3.8-slim # 安装 OpenCV 运行所需的系统库 RUN apt-get update apt-get install -y \ libgl1-mesa-glx \ libglib2.0-0 \ libsm6 \ libxext6 \ libxrender-dev \ rm -rf /var/lib/apt/lists/* WORKDIR /app # 先拷贝依赖文件利用 Docker 缓存层 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再拷贝项目源码和权重 COPY . . EXPOSE 8501 CMD [streamlit, run, app.py, --server.port8501, --server.address0.0.0.0]这个 Dockerfile 的关键在于系统库的安装。libgl1-mesa-glx和libglib2.0-0是 OpenCV 在 Linux 下必须的缺了会报ImportError: libGL.so.1: cannot open shared object file。--server.address0.0.0.0让 Streamlit 监听所有网卡否则容器外部访问不到。如果要用 GPU 推理基础镜像要换成nvidia/cuda系列并且安装对应版本的 PyTorch这个改动比较大建议先在 CPU 上跑通再折腾 GPU。requirements.txt里通常包含torch、torchvision、opencv-python、streamlit、deep-sort-realtime这几个包版本号尽量固定避免自动升级后 API 不兼容。4.3 本地启动与容器启动的差异本地直接streamlit run app.py时Streamlit 会自动打开浏览器文件路径也是相对于当前工作目录。放到 Docker 里之后工作目录变成了/app所有相对路径都要以这个为基准。常见的一个坑是权重文件路径写成了./weights/yolov5s.pt本地跑没问题容器里因为COPY . .把整个项目拷进去了路径其实也对但如果用了-v挂载卷就要注意挂载点是否覆盖了权重目录。另一个差异是端口映射docker run -p 8501:8501把容器端口暴露到宿主机如果 8501 被占用改成-p 8502:8501即可。CPU 版容器启动后第一帧推理会明显偏慢因为 PyTorch 要初始化线程池和内存分配等几帧之后速度会稳定下来。5. 避坑与排查ID 跳变、漏检、容器报错怎么处理5.1 同一个人被分配了多个 ID现象是画面里一个人走过去ID 从 1 变成 5 又变成 12计数结果明显偏大。原因通常是 Yolov5 的检测框在相邻帧之间抖动太大或者 DeepSort 的外观特征区分度不够。解决方法是先把conf_thres提高到 0.5 以上过滤掉低置信度的抖动框然后把max_dist从 0.2 调到 0.3让外观匹配更宽容如果画面里人穿的衣服颜色相近ReID 模型本身区分度有限可以考虑换一个在行人数据集上微调过的 ReID 权重。另外n_init设得太低也会导致临时轨迹被误确认调到 3 或 4 能缓解。5.2 远处的小目标漏检严重现象是画面近处的人能框住远处的人完全没反应。原因是 Yolov5 默认 640 的输入尺寸下远处的人可能只有 10 个像素高低于模型的有效检测范围。解决办法有两个一是把推理尺寸imgsz提到 960 或 1280代价是帧率下降二是把画面切成上下两半分别推理上半部分对应远处放大后再送进模型。第二种做法更灵活但要注意切分边界处的人可能被截断需要在后处理时做框的合并。如果不想改代码也可以直接换用yolov5l.pt或yolov5x.pt大模型对小目标的敏感度更高但速度会慢不少。5.3 Docker 容器里 OpenCV 报 libGL 错误现象是容器启动后立刻退出日志里写着ImportError: libGL.so.1: cannot open shared object file。原因是python:3.8-slim基础镜像里没有 OpenCV 需要的图形库。解决办法是在 Dockerfile 里加上libgl1-mesa-glx和libglib2.0-0的安装前面第 4 章的 Dockerfile 已经包含了这两条。如果已经构建了镜像不想重建也可以进入容器手动apt-get install但下次启动又会丢失所以还是改 Dockerfile 最稳妥。另外如果用的是opencv-python-headless而不是opencv-python就不需要这些图形库但 headless 版本不支持cv2.imshow在 Streamlit 场景下反而更合适。5.4 计数线附近的人来回走动导致重复计数现象是有人在计数线附近徘徊人数一直往上涨。原因是轨迹的中心点在线上下来回穿越每次穿越都触发计数。解决办法是加一个滞回区间比如线上方 10 像素和线下方 10 像素之间不触发计数只有明确越过这个区间才计数。另一个做法是记录轨迹的移动方向只有从一侧到另一侧的净位移超过阈值才计数。代码上可以在count_people里加一个last_counted_frame字典同一个轨迹在 N 帧内只计一次N 根据帧率调整一般取 15 到 30 帧。5.5 Streamlit 页面刷新导致视频从头播放现象是每次调整侧边栏参数视频就跳回第一帧重新处理。原因是 Streamlit 的机制是参数变化触发整个脚本重新运行cv2.VideoCapture被重新初始化了。解决办法是把视频处理结果缓存起来用st.cache_data装饰器把推理结果存成列表参数变化时只重新渲染不重新推理。但视频帧数据量大缓存会占内存更实际的做法是把参数控件放在视频处理之前让用户先调好参数再点“开始处理”按钮用st.button控制流程避免边看边调。6. 进阶技巧用跟踪轨迹做停留时长与热力统计跑通基本的人流量计数之后这套代码还能顺手做两件更有价值的事停留时长统计和区域热力分析。停留时长的逻辑很简单每个track_id从第一次出现到最后一次出现之间的帧数除以帧率就是这个人在这段视频里的停留时间。如果停留时间超过某个阈值比如 10 秒就可以标记为“驻足”在商场场景里对应的是对某个展台感兴趣的人。实现上用一个字典记录每个 ID 的首次出现帧号和最后出现帧号轨迹删除时计算差值。区域热力分析则是把画面划分成网格统计每个网格内出现过的轨迹中心点数量用numpy的histogram2d就能算出来最后用cv2.applyColorMap叠加到原图上。下面是一个停留时长的简化实现# track_first_seen 和 track_last_seen 分别记录每个 ID 的首次和最后出现帧号 track_first_seen {} track_last_seen {} frame_idx 0 def update_duration(active_tracks, fps): global frame_idx frame_idx 1 for track in active_tracks: tid track.track_id if tid not in track_first_seen: track_first_seen[tid] frame_idx track_last_seen[tid] frame_idx # 计算已经消失的轨迹的停留时长 durations {} for tid in list(track_first_seen.keys()): if tid not in [t.track_id for t in active_tracks]: duration (track_last_seen[tid] - track_first_seen[tid]) / fps durations[tid] round(duration, 1) # 清理已消失的轨迹记录避免内存泄漏 del track_first_seen[tid] del track_last_seen[tid] return durations这段代码里fps从cv2.VideoCapture的get(cv2.CAP_PROP_FPS)获取如果视频文件没有正确的帧率元数据就手动传一个估计值比如 25。durations字典可以在 Streamlit 侧边栏用st.table展示或者只显示停留超过阈值的 ID 数量。热力分析那边网格大小一般取 20x20 或 30x30太细了噪声大太粗了看不出分布。我自己的习惯是每次换场景先跑一遍纯检测把conf_thres和max_age调稳再开计数和停留统计否则底层追踪不稳上层统计全是玄学。从那以后我每次部署这类 WebApp都强制先用一段 30 秒的短视频验证 ID 稳定性确认没有频繁跳变再上完整视频。希望帮到你。本文还有配套的精品资源点击获取