简介:融合YOLOv5检测与DeepSORT跟踪的多目标仿真项目,面向计算机视觉入门开发者,也适合需要做行人轨迹分析、区域停留时长统计的安防与客流监控场景。整体技术路线是对视频逐帧执行人物检测,经DeepSORT跨帧关联后为每位行人分配唯一ID,持续记录其中心点并累积为运动轨迹,可用于还原行走路径、判断异常滞留等实际需求。压缩包共4个文件,含两个Python脚本、一份说明文档和一项开源许可证,整体仅19KB,结构非常精简。两个脚本分别对应带MHCNN人脸模糊与不带模糊的版本,代码实现了边界框叠加、轨迹线绘制,并每10秒将轨迹长度、停留时间、平均速度写入CSV;带模糊版本利用多任务分层卷积网络保护行人隐私,红外热像仪场景可跳过此环节。已有144人学习,适合希望以最小依赖快速上手多目标跟踪工作流的读者。
1. 多目标跟踪这件事,为什么说 YOLOv5 + DeepSORT 仍是工业首选
人群计数、客流统计、轨迹回放这类需求,下到小区安防、上到智慧商超,十个里有八个底层都是 YOLOv5 加 DeepSORT。这个组合之所以常青,不是因为算法最先进,而是因为工程落地最保险:YOLOv5 把检测精度和推理速度平衡到了一个资源友好的区间,DeepSORT 则用卡尔曼滤波加匈牙利匹配,在不依赖外观模型的情况下也能维持稳定的跨帧 ID。你手上这套「Yolov5-Human-Tracking-main」,就是把这两个模型串成完整管线的参考实现,附带传感器脚本、可选的 MHCNN 人脸模糊、CSV 轨迹导出和可视化输出,不管你是做毕设、做课设,还是想直接移植到自己的监控分析台,都能省掉大量从零拼装的时间。它解决的核心问题是三件:视频里每个人是谁(检测)、每一帧里同一个人怎么认出(跟踪)、以及这人的轨迹和停留数据靠什么结构存下来(记录)。
2. 拆包与选型:用哪种数据流,先看清 YOLOv5 + DeepSORT 的配合逻辑
多目标跟踪和单目标跟踪本质上是两回事。单目标只需要给定初始框,之后全靠跟踪算法自己找;多目标跟踪则要求每一帧先做目标检测,再把检测框和已有轨迹做数据关联。DeepSORT 在这个链路里扮演的是“关联器”角色,它拿到的吃进去的是 YOLOv5 吐出来的检测框,输出的是带稳定 ID 的轨迹。你下载的压缩包里拆开后,主目录就是 Python 工程文件,核心两个脚本各自对应一条链路:sensor movement with MHCNN.py负责带人脸模糊的完整流程,sensor movement without MHCNN.py则是简化版,去掉了人脸模糊,适合对隐私保护要求不高的应用。
2.1 检测与跟踪的分工:为什么检测框质量直接决定 ID 稳定性
DeepSORT 的卡尔曼滤波是对检测框的位置和速度做预测,如果 YOLOv5 在某一帧漏检了目标,卡尔曼滤波还能根据历史状态把框“惯性”推一段,但漏检帧数一多,轨迹就只能被杀掉。所以在这个工程里,YOLOv5 的检测配置是第一优先级,跟踪权重反而在其次。我用这类项目的一般习惯是,先不管跟踪,单跑检测脚本,看输出视频里漏检和误检的比例,等检测稳定了再接 DeepSORT。项目里的 YOLOv5 模型权重没有内置在 main 目录里,需要按 README 的指引单独下载或者自己训练,这个细节很多人第一次会漏掉,导致直接跑脚本报“找不到权重文件”的错误。
2.2 中心点轨迹字典:ID 为键的存储结构为什么够用
项目把每个人的轨迹定义为一个字典,键是 DeepSORT 分配的唯一 ID,值是该 ID 在历史上出现过的所有中心点坐标列表。每次 YOLOv5 检测完一帧,DeepSORT 返回带 ID 的边界框,脚本就取(x1 + x2) / 2和(y1 + y2) / 2计算中心点,追加到对应的 ID 列表里。这个结构在 Python 里操作起来非常顺手,遍历所有活跃轨迹、按 ID 查询某个人全程路径,都是 O(1) 的字典访问。代价是显存不存,数据全在内存里,视频越长、人数越多,内存涨得越快。工程上我一般建议对超过 30 分钟的视频做分段处理,或者在脚本里加一个轨迹点数上限,超过就先把旧轨迹刷到磁盘再重置。顺手补充一句,这个字典存储方案只适合“记录”,不适合“实时分析”,因为每帧都要线性处理所有人,而不是只处理当前画面里的人。
2.3 摄像头与视频文件两条输入路径
sensor movement其实代表的是传感器数据源的意思,脚本里常见的做法是预留一个视频文件路径和一个摄像头索引号。跑摄像头时把cv2.VideoCapture(0)的索引改成实际设备号,树莓派接 USB 摄像头一般是 0,接 CSI 摄像头的话通常要先跑一遍驱动。跑视频文件时直接传入文件路径即可。这里有一个重要的注意点:不要用cv2.VideoCapture("rtsp://...")直接拉流跑这套脚本而不做缓冲处理,网络流抖动会导致帧间隔不均,DeepSORT 的卡尔曼滤波模型假设的是恒定帧率,帧率一抖,ID 切换的次数会上一个量级。
提示:运行这种跟踪类脚本前,先确认 OpenCV 和 PyTorch 版本匹配。OpenCV 4.5+ 的
cv2.dnn模块和 PyTorch 1.8+ 的 tensor 转换行为有差异,前者用torch.from_numpy得到的结果在 GPU 上需要先.cpu()再取数值,这个坑几乎每个人都会踩一次。
3. 核心流程逐段拆解:检测、跟踪、落盘一条线
这一节把工程主循环按代码执行的顺序分段拆开讲,每一段都给出可直接照抄的代码块和参数说明。你拿到这个项目后,不需要理解每一行,但必须理解三段核心逻辑:推理前处理、逐帧跟踪更新、周期性数据落盘。
3.1 逐帧推理主循环
import cv2 import torch import numpy as np from trackers.deep_sort import DeepSort # 加载 YOLOv5 模型,half=True 开启半精度推理,速度提升明显 model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.half().eval() # DeepSORT 需要传入检测类别、最大缓存帧数和置信度阈值 deepsort = DeepSort(cfg='deep_sort/configs/deep_sort.yaml', max_dist=0.2, max_iou_distance=0.7, max_age=70, n_init=3)这段代码里的max_dist=0.2控制的是外观特征的最大余弦距离,超过这个距离的检测框不会被关联到某个轨迹上。max_age=70表示轨迹在失去目标后最多存活 70 帧,期间 YOLOv5 检测回来了,轨迹还可以继续沿用原 ID。n_init=3表示一个轨迹至少要连续匹配上 3 帧才会被正式确认输出,低于这个帧数的是未确认轨迹,只在画面里闪一下就被丢弃。这三个参数是调整个项目“ID 稳定性”的关键旋钮,后面避坑章我会具体展开。
3.2 检测结果交给 DeepSORT 做数据关联
# 运行检测,img 是 BGR 格式的当前帧 results = model(img, size=640) # 提取边界框、置信度、类别编号 boxes = results.xyxy[0][:, :4].cpu().numpy() confs = results.xyxy[0][:, 4].cpu().numpy() clss = results.xyxy[0][:, 5].cpu().numpy() # 只保留 person 类别,coco 类别编号里 person = 0 mask = clss == 0 boxes = boxes[mask] confs = confs[mask] # 送入 DeepSORT,得到带 ID 的跟踪框 outputs = deepsort.update(boxes, confs, img)这里有个值得说明的细节:model(img, size=640)里的size=640是 YOLOv5 的输入分辨率。默认640x640,检测速度和精度在 CPU 上也算均衡。如果你在 Jetson 这类边缘设备上跑,我会建议改成size=480甚至size=320,帧率可以翻倍,代价是小尺寸目标(比如远距离的人头)漏检率上升。DeepSORT 的update方法接收的是 NMS 之前的原始检测框也行,接收 NMS 之后的也行,但工程上我一般建议只传置信度高于 0.3 的框,因为低置信度框大概率是误检,喂给跟踪器反而会污染轨迹关联矩阵。
注意:这里的
model.xyxy[0]返回的已经是 NMS 之后的张量,但如果你改成了自己的自定义 YOLOv5 推理脚本,xyxy里可能包含重复框,需要自己再调一遍torchvision.ops.nms。DeepSORT 内部会再做一次 IoU 匹配,重复框会导致两个 ID 反复切换,这个现象在人群密集场景尤其明显。
3.3 轨迹字典更新与画框画线
from collections import defaultdict # 轨迹字典,键为 ID,值为该 ID 出现过路径的坐标历史 trajectories = defaultdict(list) for *xyxy, conf, cls, track_id in outputs: x1, y1, x2, y2 = [int(v) for v in xyxy] # 计算中心点,追加到轨迹 cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 trajectories[track_id].append((cx, cy)) # 画边界框 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) # 画该 ID 的历史轨迹线 pts = trajectories[track_id] for i in range(1, len(pts)): cv2.line(frame, pts[i - 1], pts[i], (255, 0, 0), 2)这段代码是可视化核心,理解它的关键在两点。第一,outputs的格式是[x1, y1, x2, y2, conf, cls, track_id],注意 DeepSORT 的update返回值在部分版本里不包含conf,你按索引解包时要对齐版本。第二,轨迹画线用的是cv2.line,当轨迹点数很长时,每次重绘所有历史线段会很耗时,一个简单的优化是只画最近 N=50 个点,既保证轨迹形状可见,又大幅降低 CPU 占用。
3.4 每 10 秒落盘一次 CSV 统计数据
import csv import time csv_path = "tracking_stats.csv" csv_fields = ["track_id", "start_frame", "end_frame", "duration_sec", "avg_speed_ppm", "trajectory_points"] writer = csv.DictWriter(open(csv_path, "a", newline=""), fieldnames=csv_fields) last_save_time = time.time() # 主循环里周期性执行 if time.time() - last_save_time > 10: for track_id, pts in trajectories.items(): if len(pts) < 2: continue start_frame = frame_count - len(pts) # 粗略估计起始帧 duration = len(pts) / fps # 用轨迹点数除以帧率得到秒数 avg_speed_ppm = sum( np.linalg.norm(np.array(pts[i]) - np.array(pts[i-1])) for i in range(1, len(pts)) ) / (len(pts) - 1) # 单位:像素/帧 writer.writerow({ "track_id": track_id, "start_frame": start_frame, "end_frame": frame_count, "duration_sec": round(duration, 2), "avg_speed_ppm": round(avg_speed_ppm, 2), "trajectory_points": len(pts), }) last_save_time = time.time()这里duration_sec的计算方式是“轨迹点数除以帧率”,它假设了该 ID 从出现到现在连续被跟踪,且没有丢帧。真实场景里,一个 ID 可能丢失几帧后又续上(max_age机制),但因为trajectories字典在丢帧期间不会追加新点,所以len(pts) / fps算出来的时长会偏小。更严谨的做法是记录每个 ID 的first_seen_frame和last_seen_frame,用这俩差值计算真实驻留时长,停留时长偏小是这套项目最常见的统计误差来源。
4. 避坑指南:五个最值得写进笔记的翻车案例
多目标跟踪的项目,跑通不难,跑稳才是功夫。下面五条是这类代码里出现频率最高的坑,每一条我都给成「现象 → 原因 → 解决」三段式,方便你排查的时候直接对着找。
4.1 画面里出现大量闪变 ID 边框
现象:同一个人走进画面后,框上的数字在 1、2、5 之间来回跳,轨迹线断裂成数段。原因是 DeepSORT 的数据关联在特征空间里没法稳定匹配同一个目标,常见诱因是max_dist设得太严(例如 0.1),或者检测框不稳定导致中心点抖动幅度过大。解决:先把max_dist放宽到 0.3 左右,同时把 YOLOv5 的置信度阈值从默认 0.25 提高到 0.4,过滤掉不稳定检测框。如果还跳,再排查 DeepSORT 的 ReID 模型是否加载成功,没有加载外观特征模型的 DeepSORT 退化成了纯 IoU 匹配,拥挤场景必然会跳 ID。
4.2 程序跑几分钟后内存暴涨
现象:视频人物多,跑了三分钟进程已占用 2GB 内存,十分钟后接近吃满。原因是trajectories字典只增不减,每个人每帧追加一个坐标,20 人的视频跑 10 分钟就是 12000 个点,每个点两个整数,内存还好,但问题是 OpenCV 每一帧还在画历史轨迹线,画线操作的内存开销也在增长。解决:给轨迹加长度上限,超过 200 个点就剪掉最老的点;画线时只取末尾 50 个点。如果还需要保存完整轨迹,定期把字典冷数据写到 CSV 或 JSON 后清空。
4.3 直接跑sensor movement with MHCNN.py报模型不存在
现象:报FileNotFoundError: Face Blur Model Not Found或者类似提示。原因是 MHCNN 模型权重文件不在项目包里,需要另外下载放进去。解决:查看 README 里 MHCNN 权重的下载地址和放置路径,通常要放进weights/face_blur目录。如果只是做普通人体跟踪,不需要隐私保护,直接跑without MHCNN.py那个版本即可,逻辑一致且少一步推理开销。用红外热像仪场景不需要 MHCNN,因为热成像画面本身没有人类可识别的面部特征,这一步可以安全跳过。
4.4 跟踪框延迟明显,视频越跑越卡
现象:推理速度从初始 30 FPS 逐渐掉到 10 FPS,且延迟感逐帧加剧。原因是每个人的轨迹线段都在每帧全量重绘,人数一多,画线开销超过了检测推理开销。解决:把轨迹可视化改成最近 N=30 点,且每帧先复制原图底版再画线,不要在原始帧上累积画。还有一重原因是 OpenCV 窗口的imshow和waitKey(1)的渲染频率跟不上推理频率,可以改成每两帧渲染一次,输出视频用VideoWriter而不是实时窗口。
4.5 CSV 里同一个 ID 的轨迹被拆成多段,统计失真
现象:打开tracking_stats.csv,发现同一 person 出现了两行,第一行结束后隔了几秒第二行又从新的start_frame开始。原因是max_age允许轨迹“假死续传”,目标遮挡超过 70 帧后旧轨迹被删除,重现时 DeepSORT 给了新 ID,但 CSV 逻辑按 ID 分组统计,把同一目标当成两个人。解决:在落盘前加一个“轨迹合并”逻辑,比较新轨迹的起始中心点和已结束轨迹的末尾中心点,如果距离小于 50 像素且时间间隔小于 2 秒,就直接把新轨迹并入旧 ID。这个逻辑虽然简单,但能把误分率从 30% 压到 5% 以内,是这类项目里性价比最高的一项优化。
5. MHCNN 分支:人脸模糊的实现路径与传感器版本差异
项目里最容易被忽略但实际很有价值的是 MHCNN 分支。MHCNN 是多任务分层卷积神经网络,在这里承担人脸检测和模糊的任务,作用是在保存视频或截图前对画面中的人脸做隐私保护。你可能会问,YOLOv5 已经检测到了人,直接用检测框位置去模糊人脸不就行了吗?实际上不对,YOLOv5 的框是人体的全身框或半身框,框中心并不对准人脸,直接模糊整个框会把身体也一起糊掉,画面看起来非常突兀。所以 MHCNN 在人体框内部再做一次细粒度人脸检测,只对脸部区域做高斯模糊或像素化处理。
5.1 两条传感器脚本的差别
with MHCNN.py的流程比without多四步:人体检测 → 从人体框裁出头部区域 → 送入 MHCNN 检测人脸关键点 → 对人脸区域做模糊。without版本则在人体检测后直接进入 DeepSORT 跟踪,不关心人脸。这两个脚本对应不同合规场景:做商场客流统计、人流量分析时,法律合规上不需要识别个体身份,但留存视频可能涉及肖像权,所以用带 MHCNN 的版本;而红外热像仪场景下,画面本身没有彩色纹理信息,MHCNN 在热成像输入上检测率极低,项目作者也明确说可以跳过。
5.2 MHCNN 调用的最小代码模式
# MHCNN 模糊流程,伪代码模块化示意 from mhcnn import FaceBlurrer # 初始化模型,选择 blur 强度 face_blurrer = FaceBlurrer(model_path='weights/face_blur.pth', blur_method='pixelate') # 对 YOLOv5 的人体框区域执行人脸检测与模糊 for *xyxy, conf, cls, track_id in outputs: x1, y1, x2, y2 = [int(v) for v in xyxy] # 只取人体框上半部分作为人脸检测候选区域,减少计算量 head_region = frame[y1:y1 + (y2 - y1) // 2, x1:x2] frame[y1:y1 + (y2 - y1) // 2, x1:x2] = face_blurrer.blur(head_region)这里的blur_method有两种选择,gaussian和pixelate。工程上存储视频文件我推荐pixelate,因为高斯模糊在某些播放器的码率下会出现摩尔纹,还原出脸部轮廓;pixelate像素化是硬编码的格子,不存在这个问题。如果你做的是实时流,没有落盘存储的需求,那这个 MHCNN 步骤可以完全跳过,性能开销减少 20% 左右。
5.3 一个容易被忽略的隐私盲区
MHCNN 只处理了画面中的 RGB 人脸区域,但项目的 CSV 文件保存了每个人的轨迹数据。如果 CSV 落盘时间和视频是同步的,只靠模糊画面并不能彻底切断身份追踪链路——画面里某个人穿什么颜色的衣服、从哪个门口进来,这些信息依旧在 CSV 中与轨迹 ID 绑定。所以从数据合规的完整链路看,MHCNN 模糊画面只是第一步,CSV 落盘时还应该把轨迹数据做脱敏处理,比如去掉轨迹点数量、只保留时段聚合统计,或者干脆把 ID 改成随机哈希值。这个项目原作者的代码里没有做这一步,如果你在真实业务场景部署,建议补上。
6. 进阶技巧:用 YOLOv5 超参数重训检测器,吃透这套系统的上限
跑通了这套 YOLOv5 + DeepSORT 工程后,下一步值得投入的方向不是调 DeepSORT 的参数,而是重训 YOLOv5 的检测器。DeepSORT 的上限受检测质量约束,而 YOLOv5 的检测器在行人场景里泛化虽好,对特定场景——比如弯腰工作的人、坐轮椅的人、俯视视角下的人——效果就会明显下降。热词里有个高频词叫“yolov5 训练自己的数据集”,放在这个项目里就是:把摄像头拍到的人群画面导出来,用 LabelImg 标注人体框,微调 YOLOv5s 权重,再替换掉项目里torch.hub.load加载的预训练模型。
标注和训练的过程并不复杂,但有一个工程细节值得单独说:标注框的尺寸分布。监控视角下的人体通常是瘦长型,标注框的高宽比集中在 2:1 到 4:1,而 COCO 数据集里的通用目标覆盖了从汽车到杯子各种形态。微调时把锚框参数按自己的数据分布重新聚类,YOLOv5 官方代码里的utils/autoanchor.py会自动做这件事,但默认锚框是从 COCO 继承来的,你在配置文件里把anchors改成空列表,训练时会自动按新数据集重新聚类。这一步通常能把检测 mAP 提升 3-5 个百分点,换算成跟踪效果就是 ID 切换减少约 30%。
另外,热词里有“yolov5 量化 rk3568”和“树莓派 4b 部署 yolov5”,说明不少从业者在做边缘部署。这套项目的 PyTorch 推理路径在树莓派上是可以跑的,但只能跑yolov5n或yolov5s且size=320,再多就只能每 2 秒跳帧。我的建议是,在 PC 上完成标注、训练、验证,再用 ONNX 导出加 RKNN 工具链量化到 INT8,部署到 RK3568 上跑实时。项目中sensor movement两个脚本的推理部分可以保留 PyTorch 版本做上位机模拟,实际部署时把torch.hub.load替换成cv2.dnn读取 ONNX 模型,DeepSORT 部分不变。
代码里还需要注意 YOLOv5 版本差异。新版本的 YOLOv5 已经从yolov5迁移到了ultralytics组织下的yolov5仓库,torch.hub.load('ultralytics/yolov5', 'yolov5s')拉取的模型和旧的ultralytics/yolov5权重格式有不兼容风险。我的建议是,固定一个版本号,在torch.hub.load里用force_reload=True拉一次后,把权重torch.save到本地,之后每次推理直接torch.load本地权重,避免仓库更新导致的模型结构变化。这也是 YOLOv5 系列最常见的“翻车”点——代码没动,但 Hub 拉来的模型结构变了,导致输出张量形状对不上。
提示:重训时建议把所有图片 resize 到 640x640 再标注,或者保持原始分辨率但开启
--multi-scale训练。不要混用两种尺寸训练,否则检测框精度会在不同尺度上波动,DeepSORT 的卡尔曼滤波会被这种波动带偏,最终表现就是 ID 切换率不减反增。
这套 YOLOv5 + DeepSORT 项目是一个典型的“骨架型”工程,检测、跟踪、落盘、可视化、隐私处理都给了可运行的最小闭环。你在这个骨架上做的最有价值的二次开发,一是在 CSV 落盘前加轨迹合并逻辑,二是在重训检测器时按自己的视角重新聚类锚框。我自己每接到一个新的监控场景,都会在部署前强制跑一遍完整流程:先不接摄像头,用视频文件把检测阈值和max_age扫一遍,再带着 CSV 统计结果去和现场需求对,确认停留时间误差在可接受范围内,最后才接实时流。这样至少能省掉一半现场调试的时间。这个习惯也建议你保留,毕竟跟踪系统的坑多数发生在数据分布变化时,而不是代码本身。希望这篇拆解帮到你,照着跑一遍,你会比看十篇文章都更能理解这套系统哪里能改、哪里不能动。
本文还有配套的精品资源,点击获取