ARTICLE DETAIL

资讯详情

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

YOLOv5+DeepSORT多目标跟踪实战:从检测到去重计数与轨迹分析

YOLOv5+DeepSORT多目标跟踪实战:从检测到去重计数与轨迹分析 简介这份资源面向计算机视觉初学者与进阶开发者聚焦车辆与行人追踪计数这一典型落地场景提供基于YOLOv5与DeepSORT的完整项目实践代码。YOLOv5负责实时目标检测输出带置信度与分类标签的边界框DeepSORT则借助卡尔曼滤波与匈牙利算法完成多目标匹配即使目标短暂遮挡或视角变化也能保持轨迹连续。资源包共103个文件以42个py源码与51个pyc编译文件为主另含yaml配置、pt预训练权重、t7模型文件及md说明文档压缩包约79.93MB涵盖模型推理、跟踪逻辑、参数配置与辅助工具等模块。已有3289人学习下载读者可据此理解检测与跟踪的衔接方式掌握Detector类封装思路并在此基础上调整超参数、跟踪阈值与预处理流程快速搭建适应实际场景的智能监控系统。1. 从一段路口监控说起YOLOv5DeepSORT 到底能替你省掉什么手里有一段路口或园区门口的监控视频想统计一段时间内经过了多少辆车、多少个人还要把每个目标的运动轨迹画出来——如果纯靠人工盯帧十分钟的视频就能让人崩溃。这个项目要解决的正是这件事用 YOLOv5 做逐帧目标检测把画面里的车和人框出来再用 DeepSORT 给每个框分配一个稳定的 ID让同一个目标在连续帧里保持同一个编号最后按 ID 去重计数、画出轨迹。它适合做智能交通统计、园区人流车流分析、安防视频结构化这类需求也适合想入门多目标跟踪MOT的开发者拿来拆解学习。资源包里给的是推理侧代码包含yolov5m.pt权重、yolo.py、general.py、torch_utils.py、json_logger.py等文件结构偏轻量拿到手就能跑通检测加跟踪的主链路。2. 拆开推理包YOLOv5 检测与 DeepSORT 跟踪是怎么串起来的2.1 先看清目录里每个文件在干什么拿到一个推理包最忌讳上来就python main.py一把梭。先把文件职责理清楚后面出问题才知道该翻哪个文件。这个包的结构不算复杂核心文件大致是这么分工的文件/目录作用你需要关注的点yolov5m.ptYOLOv5 medium 预训练权重类别是 COCO 80 类含 person、car、bus、truck 等yolo.py模型加载与推理封装输入尺寸、置信度阈值、NMS 参数在这里general.py通用工具函数坐标换算、非极大值抑制、letterbox 预处理torch_utils.pyPyTorch 相关辅助设备选择、模型加载兼容json_logger.py结果落盘把每帧的 ID、类别、坐标写成 JSONtrain.jpg测试图用来快速验证检测链路是否通yolov5m.pt里的 m 指的是 medium是 YOLOv5 系列里速度和精度比较均衡的一档。如果你只是做车辆行人计数其实yolov5s.pt更快yolov5m.pt精度更稳选哪个取决于你的硬件和实时性要求。这个包直接给了 m 权重说明作者默认你更看重检测质量而不是极限帧率。2.2 YOLOv5 推理链路从 letterbox 到 NMSYOLOv5 的推理不是把图直接塞进网络就完事中间有几个关键步骤任何一步参数不对检测结果都会明显变差。常见做法是先做 letterbox 缩放保持长宽比把图缩到 640×640空白处填灰边这样不会把目标拉变形。推理完得到的是归一化的中心点坐标加宽高还要经过置信度过滤和 NMS 去重最后再映射回原图坐标。下面这段是我一般会写的检测调用骨架参数含义都标在注释里import torch import cv2 import numpy as np # 加载 YOLOv5 模型weights 指向包里的 yolov5m.pt # device 用 cuda 还是 cpu 取决于你机器没显卡就写 cpu model torch.hub.load(ultralytics/yolov5, custom, pathyolov5m.pt, devicecuda:0) # 只保留我们关心的类别0person, 2car, 5bus, 7truck # COCO 类别索引是固定的写错就会漏检或误检 model.classes [0, 2, 5, 7] # conf 是置信度阈值低于它的框直接丢 # iou 是 NMS 的 IoU 阈值越大保留的框越多越容易重叠 model.conf 0.4 model.iou 0.45 img cv2.imread(train.jpg) results model(img) # 推理 boxes results.xyxy[0].cpu().numpy() # 得到 [x1,y1,x2,y2,conf,cls]这里conf0.4是个经验值。调太低画面里的噪点、反光会被当成目标DeepSORT 就会凭空多出一堆短命轨迹计数直接虚高调太高远处的小目标、被遮挡的目标会漏掉轨迹断断续续。iou0.45控制 NMS 的合并力度人群密集时适当调高能减少误合并但太高会让同一个目标出现两个框。这两个参数没有万能值得拿你自己的视频片段试。2.3 DeepSORT 跟踪链路卡尔曼预测加匈牙利匹配检测给的是每一帧独立的框帧与帧之间没有身份关联。DeepSORT 要做的事就是给这些框接上身份。它的核心是两块卡尔曼滤波负责根据目标上一帧的位置和速度预测这一帧大概在哪匈牙利算法负责把当前帧的检测框和已有轨迹做匹配。匹配的代价由两部分组成一个是运动信息马氏距离一个是外观信息ReID 特征余弦距离。外观特征这块是 DeepSORT 比早期 SORT 强的地方。它用一个 ReID 网络把每个框里的图像抠出来提一个特征向量即使目标被短暂遮挡再出现只要外观特征够接近就能重新匹配回原来的 ID而不是新开一条轨迹。这就是为什么 DeepSORT 在行人跟踪上比纯运动匹配稳。跟踪器的初始化参数直接决定 ID 切换的频率下面是我常用的配置from deep_sort import DeepSort deepsort DeepSort( model_pathckpt.t7, # ReID 特征提取模型 max_dist0.2, # 外观特征余弦距离阈值越小越严格 min_confidence0.3, # 低于此置信度的检测不参与匹配 max_iou_distance0.7, # 运动匹配的 IoU 距离上限 max_age70, # 轨迹丢失多少帧后删除 n_init3 # 连续命中多少帧才确认一条新轨迹 )max_age70意味着一个目标消失后它的轨迹还会保留 70 帧等它再出现时能接回去。这个值调大抗遮挡强但目标真的离开画面后轨迹迟迟不删会占内存调小遮挡一下就断 ID。n_init3是防误检的只有连续 3 帧都匹配上才确认新轨迹能过滤掉一闪而过的假检测。这两个参数是 DeepSORT 调参里最影响计数准确度的。2.4 把检测和跟踪接起来Detector 类的封装思路项目正文里提到会封装一个 Detector 类把预处理、模型加载、推理、后处理、跟踪都收进去。这个思路是对的因为检测和跟踪的输入输出格式必须对齐——YOLO 出来的是 xyxy 加置信度加类别DeepSORT 要的是 xyxy 加置信度加类别特征中间还得做一次坐标和格式转换。封装成类之后主循环里只需要调detect()和track()两个方法。class Detector: def __init__(self, yolo_weights, reid_weights): self.model load_yolo(yolo_weights) self.tracker DeepSort(reid_weights) def detect(self, frame): # 返回 xyxy 格式的检测框已过滤类别和置信度 results self.model(frame) return results.xyxy[0].cpu().numpy() def track(self, detections, frame): # DeepSORT 需要 bbox 为 xywh 且归一化到 [0,1] bboxes [] for x1, y1, x2, y2, conf, cls in detections: bboxes.append([x1, y1, x2 - x1, y2 - y1, conf, cls]) tracks self.tracker.update(bboxes, frame) return tracks # 每条轨迹带 track_id注意track()里把 xyxy 转成了 xywh这是 DeepSORT 内部卡尔曼滤波的状态表示要求。如果你直接把 xyxy 塞进去跟踪不会报错但匹配会乱表现为 ID 频繁跳变。这个坑我第一次接的时候踩过查了半天才发现是格式问题。3. 跑通第一个计数 demo环境、参数与去重逻辑3.1 环境配置conda 建环境加依赖版本YOLOv5 对 PyTorch 和 torchvision 的版本比较敏感直接用系统 Python 装很容易出玄学问题。我一般用 conda 单独建一个环境把版本锁死。下面这套是能跑通推理的常见组合conda create -n yolo_track python3.8 -y conda activate yolo_track # 先装 PyTorchCUDA 版本按你显卡驱动选没显卡就用 cpu 版 pip install torch1.12.1 torchvision0.13.1 # 再装其余依赖 pip install opencv-python numpy scipy pyyaml tqdm pip install lap # DeepSORT 的匈牙利匹配依赖lap这个库容易被漏掉它是线性分配求解器DeepSORT 的匹配步骤依赖它。不装的话运行时会报No module named lap但报错信息不一定直观新手容易卡在这。另外opencv-python建议装 4.x 版本3.x 在部分视频编码上会读不出帧。3.2 计数逻辑为什么不能简单累加每帧的框最朴素的计数思路是每帧检测到几个就加几个但这样一段 30 秒的视频能给你数出几千个目标因为同一个目标在每一帧都被数了一次。正确的做法是按 track_id 去重维护一个已计数的 ID 集合当一条轨迹第一次被确认时把它的 ID 加进集合计数加一后续帧里同一个 ID 不再重复计数。counted_ids set() vehicle_count 0 person_count 0 for frame in video_stream: detections detector.detect(frame) tracks detector.track(detections, frame) for track in tracks: track_id track.track_id cls track.cls if track_id not in counted_ids: counted_ids.add(track_id) if cls 0: person_count 1 else: vehicle_count 1这里有个细节counted_ids只增不减如果视频很长、目标很多这个集合会一直涨。实际部署时一般会配合一个过期清理比如轨迹删除后把对应 ID 从集合里移除或者用环形缓冲区限制大小。另外如果目标走出画面又走回来DeepSORT 可能给它分配新 ID这时候会被重复计数——这是所有基于 ID 去重的计数方案都绕不开的问题后面避坑章节会细说。3.3 结果落盘json_logger 的用法json_logger.py的作用是把每帧的跟踪结果写成 JSON方便后续做轨迹回放、热力图或者对接其他系统。常见做法是每帧写一条记录包含帧号、时间戳、以及该帧所有轨迹的 ID、类别、坐标。这样即使程序崩了已经处理过的帧数据还在不用从头再跑。import json def log_frame(logger, frame_id, tracks): record { frame: frame_id, objects: [ { id: int(t.track_id), cls: int(t.cls), bbox: [float(v) for v in t.to_tlbr()] } for t in tracks ] } logger.write(json.dumps(record) \n)用 JSON Lines 格式每行一个 JSON比写一个大 JSON 数组好因为可以流式追加不用把整个文件读进内存再改。to_tlbr()返回的是左上右下坐标和 YOLO 的 xyxy 一致方便对齐。如果要做轨迹可视化直接按 ID 分组把这些坐标连起来就是运动轨迹。4. 避坑排查ID 跳变、重复计数和显存爆掉的真实原因4.1 现象同一个目标 ID 频繁跳变原因通常有三个。一是检测框抖动太大YOLO 每帧给的框位置差异明显卡尔曼预测和实际检测对不上匹配失败就新开轨迹。二是 ReID 特征质量差ckpt.t7如果不是针对你场景训练的外观特征区分度不够相似的目标会互相抢 ID。三是max_dist设得太小匹配过于严格稍微有点差异就判为不匹配。解决办法先把max_dist从 0.2 放宽到 0.3 试试观察 ID 是否稳定如果还跳检查检测框是否抖动可以在检测后加一个简单的框平滑根本方案是用你自己场景的数据微调 ReID 模型但这需要标注数据成本较高。4.2 现象计数结果比实际多很多最常见的原因是目标出画面再回来被分配了新 ID导致重复计数。其次是n_init设得太小假检测被确认为轨迹每个假检测都贡献一次计数。还有一种情况是conf阈值太低画面里的反光、阴影被当成目标。解决办法把n_init从 3 提到 5过滤掉短命假轨迹conf从 0.4 提到 0.5 以上对于出画面再回来的问题可以引入一个「冷却区」逻辑目标在画面边缘消失后短时间内不立即删除 ID给它一个重新匹配的窗口。这个逻辑需要改 DeepSORT 的轨迹管理部分不是改个参数就能解决的。4.3 现象跑几分钟后显存爆掉原因是每帧的检测结果和跟踪状态没有及时释放尤其是把每帧的 frame 或者特征张量存进了列表里没清。PyTorch 的中间变量如果一直挂在计算图上显存只增不减。解决办法推理时用with torch.no_grad():包住禁用梯度计算每帧处理完把不再用的张量显式del掉如果用了 GPU定期调torch.cuda.empty_cache()。另外检查json_logger是不是把每帧的原始图像也写进去了图像数据是大头只写坐标和 ID 就够了。4.4 现象视频读出来是黑屏或者帧率不对原因是 OpenCV 的VideoCapture对某些编码格式支持不好尤其是 H.265 或者某些监控设备导出的私有格式。表现是read()返回 False或者读出来的帧全是黑的。解决办法先用ffmpeg把视频转成 H.264 的 mp4 再喂给 OpenCV这是最稳的。命令是ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4。如果不想转码也可以换decord或者PyAV来读它们对编码格式的兼容性更好但依赖会多一层。4.5 现象CPU 占用跑满但 GPU 利用率很低原因是数据预处理和后处理都在 CPU 上做GPU 大部分时间在等数据。YOLOv5 的 letterbox、NMS、坐标映射都是 CPU 操作如果视频分辨率高这部分开销很大。解决办法把输入尺寸从 1280 降到 640预处理量直接少一半用halfTrue开启半精度推理GPU 吞吐能提升如果批量处理离线视频可以攒几帧一起推理提高 GPU 利用率。实时场景下不建议攒帧会引入延迟。5. 进阶技巧用轨迹线做越线计数和区域统计基础计数只能告诉你「一共过了多少个」但实际业务里更常问的是「有多少从东向西穿过了这条线」「某个区域里同时停留了多少人」。这就需要用到轨迹的几何信息而不是简单的 ID 去重。越线计数的思路是为每个 track_id 保存它上一帧的位置当目标从线的这一侧移动到另一侧时判定为一次穿越。这里的关键是线的方向定义和穿越判定不能只看坐标正负号因为目标可能在线的附近来回抖动。def check_cross(track_id, prev_pos, curr_pos, line): # line 用两点定义(x1,y1,x2,y2) # 用叉积判断目标在线的哪一侧 def side(p, line): x1, y1, x2, y2 line return (x2 - x1) * (p[1] - y1) - (y2 - y1) * (p[0] - x1) prev_side side(prev_pos, line) curr_side side(curr_pos, line) # 符号变化说明穿越了但要排除在线上抖动的情况 if prev_side * curr_side 0: # 加一个最小位移阈值防止抖动误判 if abs(curr_pos[0] - prev_pos[0]) abs(curr_pos[1] - prev_pos[1]) 5: return True return Falseside函数用叉积判断点在直线的哪一侧符号变化就是穿越。但光看符号变化不够目标在线的附近来回走会反复触发所以加了一个最小位移阈值。这个阈值要根据你的视频分辨率和目标速度调太小防不住抖动太大漏掉慢速目标。区域统计更简单判断目标框的中心点是否落在多边形区域内落在里面就计数。可以用cv2.pointPolygonTest做点在多边形内的判断它返回正数表示在内部负数在外部零在边界上。区域统计一般配合停留时间一起用比如「在区域内停留超过 5 秒的目标数」这需要给每个 track_id 记录进入区域的时间戳。还有一个容易被忽略的点是轨迹平滑。原始轨迹点会有抖动画出来的线毛毛躁躁做越线判定时也容易误触发。我一般会对每个 ID 的最近几帧坐标做一个简单的滑动平均窗口取 5 帧左右既能平滑又不引入明显延迟。这个操作在json_logger写盘之前做落盘的轨迹就是平滑过的后续分析也省事。从那以后我每次接新的跟踪项目都会先把检测和跟踪分开验证先单独跑 YOLO 看检测框稳不稳再单独跑 DeepSORT 看 ID 稳不稳最后才合起来调计数逻辑。跳过任何一步后面出的问题都会让你分不清是检测的锅还是跟踪的锅。希望帮到你。本文还有配套的精品资源点击获取
返回列表