YOLOv5+DeepSORT人流量统计与轨迹追踪原理及实战解析
2026/9/11 1:05:57 网站建设 项目流程

简介:这是一份基于Python的YOLOv5-DeepSORT人群分析实战源码,面向计算机视觉学习者、安防监控与零售运营等相关开发者,解决实时视频流中的人流量统计、浏览轨迹跟踪与显示问题。项目结合YOLOv5目标检测和DeepSORT多目标跟踪,清晰展示了视频预处理、目标检测、特征提取、数据关联、轨迹绘制等完整流程,可用于商超客流分析、安防异常行为检测、交通行人管理与体育赛事分析等场景。压缩包共169个文件,包含55个py核心源码、21个yaml配置、2个pt模型权重、3个mp4演示视频及其他说明文档,整体约133.44MB,覆盖依赖声明、模型配置和运行脚本,便于直接复现和二次开发。已有1030人学习下载,适合希望系统掌握深度学习目标检测与多目标跟踪落地流程的读者,借助源码可快速梳理主线并扩展自定义功能。

1. 为什么人流量统计要同时跑YOLOv5和DeepSORT,而不是只数人头

我在调试一个商场展厅的客流分析功能时发现,只靠 YOLO 检测每一帧的人框数量来统计人流量,数据完全没法用:一个人在摄像头前多停几秒、绕一圈再走,就会被重复计数好几次;两个人前后交错时,计数更是乱成一团。真正能落地的方案,是让模型把“人”检测出来之后,再交给一个跟踪模块分配全局唯一的 ID,把同一时刻出现过的目标从第一帧到最后一帧串成一条轨迹。这就是这个项目把 YOLOv5 和 DeepSORT 放在一起的原因。YOLOv5 负责看清“这一帧里有哪些人”,DeepSORT 负责追着这些人走,输出连续 ID 和轨迹点,最终既能得到总人次,也能还原每个人在画面里的浏览路径。适合要做毕业设计、门店客流分析或安防行为识别的开发者,也适合想学习检测与跟踪如何无缝配合的入门者。

2. YOLOv5 与 DeepSORT 的协作逻辑:检测给位置,跟踪给 ID

2.1 YOLOv5 检测头输出:从特征图到人框

YOLOv5 使用 CSPDarknet 作为骨干网络,配合 PANet 做多尺度特征融合,最后通过三个不同尺度的检测头输出。在 person 这个类别上,它不只输出一个框,而是输出 x1、y1、x2、y2、confidence、class 六个维度的张量。多尺度融合在这里很关键:人流密集时,很多人身体互相遮挡,只露出头部或半身,只靠高分辨率那一层很难稳定召回;远处的小目标则需要浅层特征。YOLOv5 通过 80x80、40x40、20x20 三种特征图,把不同尺寸的人体都覆盖到。

在调用这个项目时,你拿到的最终检测结果不是原图的绝对像素,而是经过缩放和归一化后的坐标。以本项目常见的调用方式为例:

# 从 YOLOv5 的推理结果中取出人框并转成 DeepSORT 需要的格式 import numpy as np def filter_person_detections(results, conf_thres=0.4): # results.xyxy[0] 每一行是 [x1, y1, x2, y2, conf, cls] boxes = results.xyxy[0].cpu().numpy() # COCO 数据集中 person 类别的索引是 0 person_boxes = boxes[boxes[:, 5] == 0] person_boxes = person_boxes[person_boxes[:, 4] >= conf_thres] # 前 4 列是坐标,第 5 列是置信度,类别列不再需要 return person_boxes[:, :5]

这里类别 0 是 COCO 数据集定义的 person 类别。如果你训练过自己的数据集,需要把这一行改成你自己的类别索引,否则 DeepSORT 会拿其他类别的检测框去跟踪,轨迹里就会混进背包、车等目标。传回的person_boxes[:, :5]只保留坐标和置信度,因为 DeepSORT 的输入只需要位置信息,类别信息对它没有意义。

2.2 DeepSORT 的卡尔曼滤波与级联匹配

DeepSORT 并不是简单地把相邻两帧的框做 IoU 匹配,而是分成两条线索。一条用卡尔曼滤波器预测目标在当前帧的中心位置和宽高比,另一条用 ReID 特征提取器计算当前帧检测框的外观特征。级联匹配会优先匹配距离更短、特征更相似的轨迹,避免因为目标短暂遮挡导致 ID 跳变。

# DeepSORT 更新一次的典型接口 from deep_sort_pytorch.deep_sort import DeepSort deepsort = DeepSort( model_path="deep_sort/deep/checkpoint/ckpt.t7", max_dist=0.2, # 特征距离阈值,越小越严格 max_iou_distance=0.7, # 级联匹配中 IoU 距离上限 max_age=70, # 目标消失后保留 track 的帧数 n_init=3 # 必须连续匹配多少帧才确认新目标 ) # 输入需要 [cx, cy, w, h] 格式,而非 xyxy outputs, _ = deepsort.update(bbox_xywh, confs, ori_img) # outputs 是数组,每行是 [x1, y1, x2, y2, track_id]

上面的max_dist控制外观特征匹配的严格程度,值越小,越不允许同一个人的特征漂移,适合人比较多的场景;max_age控制在目标离开画面或被完全遮挡后,track 还能活多久。如果设得过大,一个人走出去很久后又走回来,可能会拿到同一个 ID,造成重复计数的假象;设得太小,遮挡几帧就会断掉。常见做法是先保持默认,再根据现场视频里遮挡的严重程度调max_age

2.3 轨迹 ID 是人流量统计的最小单元

如果只做检测,每帧的框之间没有任何关联,DeepSORT 输出的track_id是统计、画轨迹的唯一依据。一个人从画面右下角进入,走到展台前停留五秒,再左上角绕出去,这一整个过程在代码里表现为同一个track_id对应一串中心点坐标。

模块输出内容后续用途
YOLOv5x1, y1, x2, y2, conf, cls过滤人框、裁剪、传给跟踪器
DeepSORTx1, y1, x2, y2, track_id人流量计数、轨迹绘制、行为分析

人流量统计的本质,就是把这一串坐标映射到“进入”和“离开”两个事件上。所以实现时不要只看当前帧有多少个框,而是去维护一个字典,键是track_id,值是该 ID 的历史中心点列表。每来一帧就追加一个点,这个列表就是最后画轨迹的原料。

3. 从源码包到可运行环境:目录、依赖与权重文件

3.1 解压后的目录结构

项目解压后是典型的 YOLOv5 + DeepSORT 工程布局,根目录下的文件不算多,但需要看清哪些是源码、哪些是打包残留。常见结构如下:

yolov5-deepsort/ ├── yolov5/ # YOLOv5 模型源码与权重目录 ├── deep_sort/ # DeepSORT 跟踪实现 ├── deep_sort_pytorch/ # PyTorch 版 ReID 特征提取 ├── track.py # 检测+跟踪主入口 ├── train.jpg # 测试图,用于快速验证 ├── README.md # 运行说明 └── easydict-1.10.dev0-py3.8.egg # easydict 安装包
文件/目录作用
track.py主程序,读取视频,逐帧检测加跟踪
train.jpg快速验证模型是否加载成功的测试图
deep_sort/deep/checkpoint/ckpt.t7ReID 模型权重,用于行人重识别特征提取
yolov5/weights/yolov5s.ptYOLOv5 检测权重,默认使用 s 版本

根目录里的easydict-1.10.dev0-py3.8.egg是 DeepSORT 依赖的 easydict 包的一个安装文件,如果启动时报ModuleNotFoundError: No module named 'easydict',直接指定该文件安装即可,不需要去网络重新下载。MANIFEST.inDockerfile分别是打包与容器化用的,本地调试一般用不到。

3.2 创建虚拟环境并安装依赖

我一般会先用虚拟环境把这套代码和系统 Python 隔离,避免和已有的 PyTorch 版本冲突。注意 YOLOv5 的依赖和 DeepSORT 的依赖要一起装,不要只装torchopencv-python

cd yolov5-deepsort python -m venv venv source venv/bin/activate pip install -r yolov5/requirements.txt pip install easydict # 如果上面的 egg 文件存在,也可以这样装 pip install ./easydict-1.10.dev0-py3.8.egg

第一条命令创建虚拟环境,第二条激活它。yolov5/requirements.txt里包含了 torch、opencv、seaborn 等基础依赖;DeepSORT 部分的额外依赖通常是easydictscipy,所以再用pip install easydict补上。安装完成后,不要在项目根目录下再创建一个同名deep_sort文件夹存放自己的代码,容易覆盖依赖里的同名包。

3.3 用 train.jpg 先跑通闭环

环境配好后,不要直接拿大视频测试,先用项目自带的train.jpg验证检测到跟踪的整个链路是否通。命令行入口通常长这样:

python track.py \ --source train.jpg \ --yolo_weights yolov5s.pt \ --deep_sort_weights deep_sort/deep/checkpoint/ckpt.t7

如果正常,runs/track/exp下会生成一张画有检测框和轨迹点的图。这一步能在几秒内判断环境是否通,避免直接拿大视频排错。注意--yolo_weights--deep_sort_weights是两个独立的权重路径,很多新手只改检测权重,忘掉 DeepSORT 侧还需要单独的 ReID 权重,然后发现跟踪 ID 总是跳变,就是这一步没有配对。

4. 人流统计与轨迹绘制的实战参数:从一帧到一条完整轨迹

4.1 检测后处理参数:conf-thres 与 iou-thres 怎么设

YOLOv5 的检测结果需要经过 NMS 后处理,项目中对应参数是conf_thresiou_thresconf_thres是置信度阈值,低于它的框会被丢掉;iou_thres是 NMS 阶段用来消除重叠框的 IoU 阈值。人流密集时,conf_thres不要设得过高,否则远处的人和被遮挡一半的人会被丢掉,导致跟踪轨迹中途断裂。

参数取值范围场景建议
conf_thres0.25 ~ 0.5人群密集场景建议 0.3,稀疏场景可 0.4
iou_thres0.3 ~ 0.7NMS 时建议 0.45 ~ 0.5,过高会保留大量重叠框
img_size640 / 320视频画面大但目标小,保持 640;追求速度可降到 320

在代码里,这两个值会直接影响 DeepSORT 拿到的框数量。置信度阈值过高,会导致一个人只检测到半个身体,跟踪器因为特征不足而迟迟无法确认新轨迹;过低,则会把背景噪声当成目标,出现大量短暂 ID。建议先用默认值看效果,再通过视频里人的远近缩放img_size,而不是一味调置信度。

4.2 DeepSORT 参数对轨迹连续性的影响

DeepSORT 的核心参数集中在DeepSort构造函数里。下面这组参数是我在商场客流场景调过的版本:

deepsort = DeepSort( model_path="deep_sort/deep/checkpoint/ckpt.t7", max_dist=0.2, # 外观特征余弦距离阈值 min_confidence=0.3, # 检测置信度低于该值的框不参与跟踪 max_iou_distance=0.7, # 卡尔曼预测框与检测框的 IoU 距离上限 max_age=50, # track 在丢失目标后存活的最大帧数 n_init=3, # 目标显示 3 帧且特征匹配成功才确认 ID nn_budget=100 # 特征库大小,控制内存占用 )

max_dist越小,越要求同一个人前后外观一致,适合人穿相同颜色工作服的场景;如果现场光照变化大,人的外观特征会漂移,应该适当调大到 0.3。max_age控制目标离开画面或被完全遮挡后,track 还能活多久。设置过大,一个人走出去很久又走回来,会被当作同一个人,造成重复计数假象;设置过小,遮挡几帧就会断掉。项目默认值通常是 50 到 70,如果现场没有严重遮挡,可以降到 30。

4.3 人浏览统计的两种计数方案

得到稳定的track_id后,统计人流量直接从“第一帧到最后一帧”这个维度做,而不是每帧人数求和。最常见的是中线交叉法:在画面中画一条虚拟线,当一个 track 的中心点从线的一侧移动到另一侧,就记一次进入或离开。

# 中线计数:track_id -> 最近一次中心点的 Y 坐标 line_y = 400 in_count, out_count = 0, 0 last_pos = {} for track in outputs: x1, y1, x2, y2, track_id = track cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0 if track_id in last_pos: prev_y = last_pos[track_id] if prev_y <= line_y and cy > line_y: in_count += 1 elif prev_y >= line_y and cy < line_y: out_count += 1 last_pos[track_id] = cy

这段代码用last_pos字典保存每个 ID 上一次的中心点 Y 坐标。只有当目标跨过line_y且是从上到下或从下到上,才累加计数,避免目标在线的附近来回小幅摆动造成重复计数。实际项目中,我会把line_y换成一条线段去做向量叉积判断,以支持倾斜的虚拟线;还要给每个 track 加一个“已计数”标志,防止同一个 ID 来回跨线被多次累加。

另一种是 ROI 区域法:当目标中心点进入指定多边形区域时,记录进入时间,离开时记录离开时间。这种方法更适合统计某个展台前停留了多久,实现时用cv2.pointPolygonTest判断点是否在多边形内,逻辑比中线法更贴近“浏览”这个概念。

4.4 轨迹绘制:坐标点管理与降噪

轨迹显示的原始数据是所有track_id的历史中心点。绘制时不能把每一帧的点都直接连起来,否则人在原地静止时,轨迹线会堆成一团。我一般会先做降采样,每 N 帧取一个点,再用最近 M 个点连成短线,防止旧轨迹干扰当前目标位置。

import cv2 trajectories = {} line_len = 30 # 最多画多少个历史点 skip_frames = 2 # 每隔几帧取一个点 for track in outputs: x1, y1, x2, y2, track_id = track cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2) if track_id not in trajectories: trajectories[track_id] = [] trajectories[track_id].append((cx, cy)) trajectories[track_id] = trajectories[track_id][-line_len:] # 每隔 skip_frames 取一个点,避免线下密集重影 pts = trajectories[track_id][::skip_frames] if len(pts) >= 2: for i in range(1, len(pts)): cv2.line(frame, pts[i - 1], pts[i], (0, 255, 0), 2)

line_len控制轨迹的显示长度,25 到 40 比较合适;太长的话,目标早已离开的区域还会留着旧线。skip_frames的作用是把密集的点抽稀,让画出来的轨迹更接近真实移动方向。这段代码直接放在track.py的推理循环里,每帧都会更新trajectories字典,并把轨迹线画到当前帧上。注意这里用的是中心点而不是头部点,因为检测框的底部往往被遮挡,中心点稳定得多。

5. 验证与提速技巧:把自己的视频跑出稳定帧率

5.1 先图片后视频,再固定首帧基准

视频调试前,一定要先用静态图片确认检测和跟踪能同时输出。图片没有时间维度,DeepSORT 只能输出一个未确认的 track,但至少能看出检测框是否正常。确认后,再用一段 10 秒左右的短视频跑完整流程,把首帧的检测结果与手动标记的人数对比,作为每次改参数后的基准。

python track.py \ --source shop.mp4 \ --yolo_weights yolov5s.pt \ --deep_sort_weights deep_sort/deep/checkpoint/ckpt.t7 \ --conf_thres 0.35 \ --iou_thres 0.45 \ --save-txt

--save-txt会输出每帧目标的位置和 track_id,方便你统计共有多少个 ID 出现过。用这个数据和实际视频里的人对比,能快速判断计数是否偏多或偏少。

5.2 CPU 上提速的三招

这套代码在 CPU 上跑 1080p 视频通常只有个位数帧率。常见处理是三个组合拳:缩小推理尺寸、抽帧处理、把结果缓存起来。

# 推理尺寸降到 320x320,帧率通常能提升一倍以上 results = model(frame, size=320) # 每隔 2 帧做一次检测跟踪,中间帧用上一帧的坐标 frame_idx = 0 for frame in video_stream: if frame_idx % 3 == 0: outputs = deepsort_update(frame) last_outputs = outputs else: outputs = last_outputs frame_idx += 1

降尺寸到 320 对密集型人群检测精度有一定损失,但在 CPU 场景下比调conf_thres更有效。抽帧则会让轨迹看起来略有跳变,这时要把轨迹绘制也放在跳变帧上,不要用旧坐标去猜新坐标。

5.3 把人流量统计结果导出 CSV

最后的统计结果不要只停留在画面上的数字,建议每隔一段时间把每个 track 的进入时间、离开时间、中心点采样写入 CSV,方便后续用 pandas 做小时级客流分析。导出时注意把帧号换算成实际时间,否则后续无法对齐监控录像。

import csv with open("flow.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["track_id", "frame", "x", "y"]) for track_id, pts in trajectories.items(): for frame_idx, (x, y) in enumerate(pts): writer.writerow([track_id, frame_idx, x, y])

这里的frame_idx是当前轨迹点所在的帧索引,你可以在track.py里用一个全局变量把帧号传进来。CSV 里同时保存x, y而不是只存轨迹线,是为了后续能重新绘制或计算每个目标的移动距离,这也是把检测、跟踪和统计分析分离之后最直接的好处。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询