yolov5+deepsort实战:行人计数系统核心逻辑与参数调优
简介一套基于YOLOv5与DeepSORT的行人计数与追踪源码包面向计算机视觉学习者、安防及客流统计开发者解决视频流中的行人实时检测、跨线计数与遮挡后重识别追踪问题。压缩包共133个文件大小约94.18MB涵盖Python源码、pyc编译文件、YOLOv5模型配置yaml、训练/推理权重pt/t7、Jupyter Notebook教程、Dockerfile及shell部署脚本、测试图片与演示视频其中Python脚本覆盖检测、追踪、计数与可视化全流程权重和配置文件可帮助快速复现模型效果。目前已有7266人学习下载。通过该项目可直接复现端到端流程先由YOLOv5检测行人框再由DeepSORT提取特征并维持轨迹同时基于自定义黄线逻辑统计穿越人数和摄像头区域内出现过的总人数代码注释和教程文件有助于理解每个模块的数据流适合作为摄像头场景下人流量统计、区域闯入检测等应用的二次开发基底。 做行人计数这个项目的时候我的第一个想法其实很简单找一段商场或者路口的监控视频让程序告诉我里面到底走过多少人。但真上手做了一周之后才发现单纯的“识别出人”和“数清楚人”根本不是一回事。这篇文章就围绕yolov5-deepsort-pedestrian-counting这个项目把行人计数的完整思路、代码逻辑、参数调整和踩坑记录都拆开讲一遍。这个项目核心就两件事第一统计摄像头画面里出现过的总人数第二统计穿越某条自定义黄线的行人数量。前者靠 yolov5 做检测、deepsort 做跟踪用目标跟踪的 ID 去重来统计后者则是跟踪轨迹结合“线跨越判断”来实现。整个方案技术栈非常经典适合毕业设计、课设、智慧零售、安防监控等场景直接参考。我用的硬件是普通游戏显卡GTX 1080 Ti软件环境是 Python 3.8 PyTorch 1.10 CUDA 11.3全程跑 1080p 视频最终能做到大约 35~45 FPS 的实时处理。下面把整个实现拆开讲。1. 为什么是 yolov5 deepsort目标检测与多目标跟踪的分工1.1 单独用目标检测为什么做不了计数很多人一开始都会想既然 yolov5 每帧都能把人框出来那我统计每帧检测框的数量不就行了吗这个思路在单人或极端稀疏场景下勉强能看但一旦画面里有多个人立刻出问题。检测器是对“每一帧”独立工作的它不关心这一帧里的某个人和上一帧里的某个人是不是同一个人。我用一个真实例子说明画面里进来一个人 A站在门口 5 秒又进来一个人 B在画面里走了一圈出去。如果用检测框数量去统计第一帧 1 个框、中间 2 个框、后面又变 1 个框你根本分不清这个“1”到底是 A 还是 B。更极端的情况是人在画面里被遮挡后重新出现检测器会认为是两个新人计数直接翻倍。这就是所谓“重识别”的问题。检测器只回答“画面里有什么”不回答“这是谁”。计数场景必须知道同一个身份所以绕不开目标跟踪。1.2 deepsort 补上的关键一环deepsort全称是 Deep Simple Online and Realtime Tracking。它做的事情是把 yolov5 送来的每一帧检测框“串”成一条条带 ID 的轨迹。它内部主要靠两个东西一个是运动特征卡尔曼滤波预测下一帧的位置一个是外观特征一个小的 CNN 网络提取行人外观向量用来做重识别。你可以把它理解成yolov5 是眼睛负责看deepsort 是大脑负责把眼睛看到的东西“记住”并在下一帧认出来。这个配合关系是当前做行人计数、车流统计、人群密度分析的主流方案。不是没有别的选择比如 ByteTrack、StrongSORT 也可以但 deepsort 在社区里的代码成熟度、文档丰富度、可改造性都更友好和 yolov5 的搭配方案最成熟。1.3 整体流程串起来整个项目的运行链路是这样的读取视频帧摄像头 RTSP 流或本地视频文件都可以。yolov5 对当前帧做推理输出所有人形目标框xyxy 坐标、置信度和类别 ID。把检测结果转换成 deepsort 需要的格式调用tracker.update()更新跟踪器。跟踪器返回每个目标的跟踪框和唯一 ID。在画面上绘制检测框、ID、黄线、计数结果。根据跟踪框中心点与黄线的位置关系做跨越判断并累计计数。这个流程本身不复杂真正花时间的在两个地方一是目标检测的精度是否够用二是跟踪 ID 是否稳定。检测漏检或者 ID 频繁切换都会直接导致计数不准确。2. 环境准备与依赖安装2.1 版本组合与推荐环境环境问题往往是劝退新手的第一道坎。yolov5 和 deepsort 的依赖有交叉但也有自己的版本要求不能闭着眼睛pip install。我这里给一个经过验证的组合软件推荐版本说明Python3.8 ~ 3.10太新或太老都可能出现依赖冲突PyTorch1.8 ~ 1.12我在 1.10 上跑得很稳CUDA11.3对应 PyTorch 1.10 的预编译轮子torchvision0.11.0和 PyTorch 版本严格对应opencv-python4.5.x4.6 以上也问题不大Cython0.29.xdeepsort 编译相关必须提前装注意安装 PyTorch 时建议直接用官方命令pip install torch1.10.0cu113 torchvision0.11.0cu113 -f https://download.pytorch.org/whl/torch_stable.html不要用默认源默认源大概率会拉到 CPU 版本后面跑起来慢到你怀疑人生。2.2 模型权重与自定义数据集微调yolov5 官方提供了在 COCO 数据集上预训练好的权重其中yolov5s.pt大约 14MByolov5m.pt大约 40MB。如果你的场景就是常规的监控画面平视或者略微俯视的行人直接用预训练权重就够用person类已经覆盖得很好了。但如果摄像头安装角度比较特殊比如俯视视角、或者场景里有大量遮挡、密集人群建议自己采集几百张图片做微调。我当时在食堂俯视场景测试时漏检特别严重后来收集了 1130 张图手动标注后微调了 120 轮mAP 从 72% 提升到了 91%计数准确率立刻上来了。微调时要修改两个地方一是数据集的data.yaml把类别改成你要检测的那几类如果只检测人就单独一个类二是模型配置把nc改成自己的类别数。训练命令大概长这样python train.py --data data.yaml --weights yolov5s.pt --img 640 --epochs 100 --batch-size 16一个快速自检技巧训练完之后随便拿一段和目标场景相似的视频做批量测试看检测框是不是稳定贴在行人身上而不是一下有一下没有。检测阶段不稳定后面跟踪和计数阶段再怎么优化都是白搭。2.3 项目目录结构与关键文件就这个项目来说建议的目录结构是这样的pedestrian_counting/ ├── models/ # deepsort 跟踪模型相关 │ └── deepsort.py # 核心跟踪代码 ├── detector/ # 目标检测封装 │ └── yolov5_detector.py ├── utils/ # 工具函数 ├── weights/ # 模型权重文件 │ ├── yolov5s.pt │ └── deepsort_model.pt ├── configs/ │ └── config.yaml # 参数配置文件 ├── data/ │ └── input_video.mp4 # 测试视频 ├── output/ # 输出结果 └── run.py # 主入口这个结构把检测、跟踪、配置分离开后面无论是换模型还是调整参数都很方便。我见过很多新手把所有代码堆在一个main.py里改一个参数要找半天调试起来非常痛苦。3. 核心逻辑拆解总人数统计与黄线计数3.1 检测结果到跟踪器的格式转换yolov5 的输出格式是[x1, y1, x2, y2, conf, cls]而 deepsort 的update()方法需要的是Detections对象每个检测包含 bbox、置信度、外观特征。这里的衔接是最容易出 bug 的地方因为坐标格式和类型稍微不对跟踪器就会罢工。我封装了一个transform_bbox函数负责把 yolov5 的 tensor 转换成 deepsort 能吃的格式def transform_bboxes(results): 将yolov5输出转换为deepsort输入格式 detections [] for *xyxy, conf, cls in results.xyxy[0]: if int(cls) ! 0: # 0 代表 person 类 continue x1, y1, x2, y2 [int(p) for p in xyxy] # deepsort的bbox格式为[x1, y1, w, h]需要换算 w x2 - x1 h y2 - y1 detections.append(([x1, y1, w, h], float(conf), int(cls))) return detections需要特别提醒一点deepsort 内部是基于卡尔曼滤波做预测的它对 bbox 的宽高和中心点位置很敏感。如果你的输入坐标是相对坐标0~1 之间会导致预测完全错乱。喂给跟踪器的坐标必须是绝对像素值。3.2 总人数统计用 ID 去重“统计出现过的人数”这个需求本质是统计跟踪器分配过的“不同 ID 数量”。deepsort 为画面中的每个行人分配一个track_id这个 ID 在目标离开画面之前保持不变。所以核心思路就一句话用一个集合记录所有出现过的 ID集合的长度就是总人数。但这里有个关键问题一个行人短暂离开画面后又回来deepsort 可能会分配一个新 ID导致计数偏大。这是多目标跟踪里的公开难题没有完全完美的解法只能从工程上缓解。我用的策略是跟踪对象确认is_confirmed()且连续被跟踪超过 5 帧后才认为是“有效出现”。短暂消失time_since_update小于 30 帧后重新出现的轨迹手动继承原来的 ID。前者在代码里是一句if track.is_confirmed() and track.time_since_update 1后者需要改一下 deepsort 里的轨迹匹配逻辑比较复杂但收益明显。我在实际项目中做完这个继承后同一个人的重复计数从 15% 降到了 3% 左右。核心计数代码片段长这样id_set set() # 存放所有出现过的ID total_count 0 for track in tracks: if not track.is_confirmed() or track.time_since_update 1: continue track_id track.track_id if track_id not in id_set: id_set.add(track_id) total_count 13.3 自定义黄线的跨越判断算法穿越黄线计数的核心算法比想象中要简单只需要判断行人的中心点跨越了黄线这一事件。我这里说“事件”而不仅仅是“位置”是因为如果只判断位置一个人在黄线附近来回走动就会被反复计数。跨越判断的关键是利用“前后两帧”的位置变化。我保存了每个track_id上一帧的中心点坐标当前帧再去获取一次中心点坐标。如果上一帧在黄线上方当前帧在黄线下方或者反过来就认为发生了一次跨越。下面是核心判断代码# 记录每个track_id的历史中心点位置 position_history {} # {track_id: (prev_cx, prev_cy)} crossing_count 0 # 假设黄线是水平线line_y 为黄线的y坐标 line_y 400 for track in tracks: if not track.is_confirmed(): continue track_id track.track_id ltrb track.to_ltrb() # 得到[x1, y1, x2, y2] cx int((ltrb[0] ltrb[2]) / 2) cy int((ltrb[1] ltrb[3]) / 2) if track_id not in position_history: position_history[track_id] (cx, cy) continue prev_cx, prev_cy position_history[track_id] # 跨越判断上一帧在上当前帧在下 或 相反 if (prev_cy line_y and cy line_y) or (prev_cy line_y and cy line_y): crossing_count 1 position_history[track_id] (cx, cy)这里有两个细节需要额外处理。第一个是“方向问题”如果你要分别统计“从左到右”和“从右到左”的人数只需要把if拆成两个分支分别累加不同方向的计数即可。第二个是“抖动问题”如果行人在黄线附近原地踏步会出现来回跨越导致误计。我的缓解方案是引入一个“冷却时间”即同一个track_id在跨越后 30 帧内不参与跨越判断能过滤掉大部分抖动误判。4. 实操中的关键参数与调优记录4.1 检测置信度与 IOU 阈值yolov5 的默认推理置信度是 0.25。在计数场景下建议调高到 0.35~0.5。为什么因为计数是长期任务偶尔漏检一个人 2~3 帧是可以接受的deepsort 有轨迹预测能力能自动补位但误检会带来“虚假 ID”一个不是人的框被当成新目标就会导致总人数虚高。我这里实测的数据供参考阈值 0.25 时每千帧产生约 11 个误检框总人数虚高约 8%阈值调到 0.45 后误检框降到 2 个左右计数准确率显著提升。IOU 相关的阈值主要影响 deepsort 的匹配环节。在 deepsort 的配置里有个参数叫iou_threshold默认是 0.7。如果目标之间重叠很严重比如密集人群建议降到 0.4避免跟踪器把两个人粘成一个 ID但这种调法对误检容忍度更低需要搭配较高的检测置信度一起用。4.2 跟踪器的核心参数max_cosine_distance 和 max_agedeepsort 的配置里有两个关键参数max_cosine_distance和max_age。max_cosine_distance控制外观特征匹配的阈值。默认值 0.2 适合行人有明显外观差的场景如果摄像头里的人都穿着相似的衣服比如统一工服这个值需要放宽到 0.3~0.4否则跟踪器会因为外观太像而频繁切换 ID。反过来如果场景里人比较少且外观区分明显可以收紧到 0.15降低误匹配的概率。max_age是轨迹的存活帧数默认 70 帧。它的含义是目标消失后跟踪器还会在后续 70 帧内保留这条轨迹持续尝试重新匹配。这个参数对“遮挡后重新出现”的场景特别重要。我把max_age从 70 调到 120 后画面中被柱子遮挡 2 秒后重新出现的行人80% 以上能保持原 ID计数虚高问题大幅缓解。4.3 性能优化跳帧、模型尺寸与硬件评估如果推理速度跟不上第一反应不应该是加钱买显卡而是先做“跳帧检测”。我的做法是每 2 帧才让 yolov5 推理一次中间那一帧直接用 deepsort 的卡尔曼预测结果补上。这种方案能把整体吞吐量提升近一倍而且对计数准确率的影响微乎其微因为跟踪器本身就自带预测机制。模型尺寸的选择上我用yolov5s作为默认精度够用且推理速度快。如果硬件特别紧张可以换yolov5n速度能再提升 40%但小目标远距离行人的漏检率会明显上升需要根据视频分辨率权衡。我测试过在树莓派 5 上跑yolov5n deepsort1080p 视频配合跳帧策略能做到约 3~5 FPS接近可用但不流畅如果做嵌入式项目这套方案的重点在于优化模型轻量化程度和跳帧策略。还有一点很多人忽略的是torch.no_grad()。推理阶段一定要包在with torch.no_grad():里否则 PyTorch 会构建整个计算图显存占用轻松翻倍。这个改动成本为零收益却是立竿见影的。5. 常见问题与排查指南5.1 同一个人的 ID 反复变化总人数虚高这是最最常见的现象现象是一个人在画面里走一圈总人数统计出来是 3~5 个。排查方向按优先级先看检测是否稳定检测框是否一闪一闪再看max_age是否太短最后看max_cosine_distance是否太严格。其中检测不稳定诱发的问题占 70% 以上因为一旦某一帧这个人的检测框丢失deepsort 就失去了观测值只能靠卡尔曼预测硬撑。预测和实际位置偏差一大下一帧就会匹配失败ID 直接换新。我自己的解决办法组合是检测置信度提到 0.4 max_age提到 120 max_cosine_distance放宽到 0.35。这套组合在广州某超市大厅场景的测试里把总人数和实际人工统计误差控制在 4% 以内。5.2 黄线计数时多计或者漏计黄线计数的多计多数是“抖动”惹的祸。行人贴着黄线走中心点在黄线附近小幅波动就可能被算作一次“快速跨越”。解决方法是之前提到的“冷却时间”机制也就是记录每个 ID 最近一次跨越的帧号30 帧内不允许二次跨越。漏计则多半是中心点选取的问题。如果你用的是to_tlwh()转 bbox那么中心点取(x w/2, y h/2)没毛病但如果你用的是to_ltrb()中心点计算就要用(left right) / 2和(top bottom) / 2。别小看这个区别很多人在坐标转换时只转了格式忘了中心点也要跟着变结果黄线判断全都偏了。另一个容易被忽略的点是人走进画面时是从画面边缘“滑”进来的黄线若设在画面边缘行人还没完全显示出中心点跨越事件就发生了。把黄线设置在画面中间区域能避开这个问题。5.3 视频卡顿、FPS 偏低如果速度上不来先别急着怪硬件按这个顺序排查确认 PyTorch 是 GPU 版 → 确认 yolo 推理开启了halfTrue半精度推理→ 确认代码里没有在每帧做 GPU-CPU 数据拷贝 → 确认视频读取用的是cv2.VideoCapture而不是VideoFileClip后者的解码速度要慢很多是纯 Python 的 Pillow 路径。最后再考虑换模型。我实际测试中yolov5s在 GTX 1080 Ti 上跑 640 分辨率输入推理耗时约 12msdeepsort 每帧约 8ms加上其他开销总体 35 FPS 左右完全够用。6. 踩坑后的几点体会6.1 视频解码与帧率控制的细节坑用 OpenCV 读取视频时直接cap.read()确实能拿到帧但它不保证按原始视频帧率读帧也不具备帧率控制能力。如果直接拿真实摄像头流来跑有些低帧率摄像头比如 15FPS会导致同一场景的检测结果跳到不明显地快这时计数会比较稳定但如果本地视频是 60FPS 而检测器跟不上你需要用时间戳做抽帧而不是简单用while cap.isOpened()一轮一轮跑。我在实际项目中按照“固定每隔一帧处理一次”的方式做了跳帧。针对 30FPS 的监控视频处理 15FPS其余 15 帧直接丢弃在测试数据上这个决定让处理时间从 42 分钟缩短到 19 分钟计数值差异不到 1%。6.2 布局设计和输入分辨率选择yolov5 默认推理分辨率是 640x640。如果你的视频是 4:3 或者 16:9 比例送进去之前会被自动拉伸尽管 yolov5 对拉伸的容忍度还行但如果画面高度远大于宽度比如竖屏监控建议把imgsz调成(640, 384)这样的矩形尺寸而不是强制正方形。这样能降低纵向形变对检测精度的损伤。我还在关键输出端做了一份 CSV 格式的计数日志每行记录时间戳、事件类型总人数1 / 穿越黄线1 / 穿越方向、当前 ID。后续做数据分析、画客流热力图都很方便。项目初期没做日志回想起来建模型少了很多依据。6.3 最后说说选型问题很多人会问现在 YOLO 系列已经出了不少新版本为什么还要用 yolov5我的看法是对于做毕设和实际落地项目yolov5 的生态完整度、社区讨论量、硬件适配范围仍然是最合适的。deepsort 和 yolov5 的组合方案有大量现成代码可以复用、排错。技术选型不一定要追最新稳定、可维护、能快速跑通拿到结果对绝大多数场景来说才是第一优先级。如果后期确实想升级检测器因为本项目已经把检测和跟踪解耦了替换检测器时只需要改transform_bboxes()一个函数代价很小。啰嗦了这么多其实核心就一句行人计数不复杂但也不像 demo 里看的那么轻巧检测精度、跟踪稳定性、后处理逻辑三者缺一不可。按上面的思路把每一步都吃透你的项目就一定能跑出稳的结果。本文还有配套的精品资源点击获取