简介一套融合YOLOv5与DeepSORT的车辆检测与追踪项目资源面向计算机视觉开发者、算法研究人员以及智能交通应用工程师适用于实时交通监控、停车场管理、无人驾驶环境感知等场景能有效降低目标检测与多目标跟踪的技术门槛。压缩包共含2000个文件整体约29.76MB其中1588个xml标注文件保存车辆位置信息409个txt文件多为数据划分列表与标签说明另有2个md说明文档和1个Python脚本脚本可用于自动拆分训练集与验证集方便快速进入模型训练流程。当前已有150人学习浏览。项目提供处理好的数据集以及训练好的模型免去数据采集与人工标注的繁琐工作同时保留了清晰的目录组织和可复用的工程脚本。YOLOv5负责高效准确的目标检测DeepSORT在遮挡、重叠情况下仍能稳定追踪车辆两者结合可直接迁移到智能交通、车流统计等真实任务中无论是算法验证、毕业设计还是二次开发都能在此基础上快速展开。1. 点开视频那一刻的惊喜一个YOLOv5DeepSORT车辆检测项目到底能省多少事把一段高速收费站监控扔给一个只有YOLOv5的脚本它会在每一帧画出所有车辆框但你追问“画面里这辆车到底是刚才那辆还是新来的”它答不上来。这个车辆检测项目把YOLOv5和DeepSORT接成了一条完整链路YOLOv5负责实时给出检测框DeepSORT负责把检测框变成带ID的稳定轨迹还附带了已经清洗、标注、划分好的车辆数据集和训练验证集划分脚本拿到手不用从标注工具开始熬三天。适合两类人做车辆跟踪毕业设计或工程Demo的开发者以及想对比不同检测器接DeepSORT效果的研究者。2. 项目解剖从README到car_net配置它到底给了你什么2.1 资源清单别被一堆txt吓到打开压缩包的第一印象是“东西不多”README.md、split_train_val.py、val.txt加上一排car_net_5.txt、car_net_7.txt、car_net_40.txt、car_net_141.txt、car_net_151.txt、car_net_161.txt。没有满屏的源码文件也没有一堆训练日志这份资源的重点在“配置、脚本、数据”这三个环节上。文件作用README.md项目说明、环境准备、模型与数据集的用法入口split_train_val.py把图片列表按比例划分成训练集和验证集val.txt验证集图片路径列表一行一张图片car_net_*.txtcar_net系列的模型结构配置数字代表不同规格car_net这几个txt是最容易被误会的文件。我第一次看也以为里面是车目标标注打开才发现是网络结构的参数描述。不同数字对应不同的深度、宽度、anchor组合实际训练时可以按显存和精度要求选一份。数字小的适合边缘设备数字大的代表更深的网络结构追求精度但显存消耗也更大。README里应该有对应的说明我拿到手之后先花五分钟把这几个文件的内容看一遍比到处问人“为什么报错”有用得多。2.2 数据集已经被处理成了什么样子目标检测项目最耗时间的环节不是训练而是数据准备。原始视频要抽帧、筛掉模糊的、用标注工具画框、导出YOLO格式再处理类别不平衡。这套项目里附带的数据集已经走到了最后一步图片和标签文件按YOLO惯例放在images和labels两个目录下标签文件是一个个txt每行代表一个目标。0 0.516406 0.430556 0.123438 0.259722这一行五列的含义分别是类别编号、归一化后的目标中心点x坐标、中心点y坐标、目标宽度、目标高度。类别0对应的就是“car”。坐标全部除以图片宽高做了归一化所以无论图片是1080p还是720p训练时都无需逐张处理。val.txt里保存的则是验证集图片的相对路径或绝对路径YOLOv5在训练和验证时直接读这个列表去定位图片。这种结构最大的好处是你拿到数据后不需要再做任何格式转换直接写一个data yaml指向train.txt和val.txt就能开训。2.3 split_train_val.py 脚本逐行拆项目里的split_train_val.py是标准的数据集划分脚本代码不长但思路值得抄。常见实现长这样import os import random import argparse def main(): parser argparse.ArgumentParser(descriptionSplit dataset into train/val) parser.add_argument(--image_dir, typestr, defaultimages, helpimage folder) parser.add_argument(--label_dir, typestr, defaultlabels, helplabel folder) parser.add_argument(--ratio, typefloat, default0.8, helptrain ratio) args parser.parse_args() names [f for f in os.listdir(args.image_dir) if f.endswith((.jpg, .png, .jpeg))] random.shuffle(names) split_idx int(len(names) * args.ratio) with open(train.txt, w, encodingutf-8) as f_tr, \ open(val.txt, w, encodingutf-8) as f_val: for i, name in enumerate(names): image_path os.path.join(args.image_dir, name) label_path os.path.join(args.label_dir, os.path.splitext(name)[0] .txt) if not os.path.exists(label_path): continue if i split_idx: f_tr.write(image_path \n) else: f_val.write(image_path \n) if __name__ __main__: main()这个脚本做三件事先把图片文件名全部读出来并随机打乱再按ratio算出训练集和验证集的分界下标最后写出train.txt和val.txt。需要注意的是它不复制图片只写路径这样不会产生冗余数据。参数上--image_dir是图片目录--label_dir是标注目录--ratio默认0.8表示80%数据用于训练20%用于验证。脚本里加了一个保护逻辑如果某张图片找不到同名标签文件就跳过避免把没有标注的坏样本带进训练集。这套处理思路比单纯按文件名前几位做硬划分靠谱得多因为视频抽帧出来的相邻帧内容高度相似直接顺序划分会让训练集和验证集内容重叠随机打乱能显著改善验证指标的可信度。3. 核心实现YOLOv5检测框如何变成DeepSORT的稳定ID3.1 先想清楚DeepSORT到底缺什么DeepSORT是对SORT的升级SORT只靠卡尔曼滤波预测目标位置、用匈牙利算法做检测框和轨迹的匹配缺陷是目标一旦被遮挡再出现就很容易被当成一个新目标。DeepSORT增加了一个ReID外观特征提取器每个跟踪目标会保存外观特征遮挡后靠特征相似度找回同一个目标。但DeepSORT本身不产生检测框它只是消费检测结果的跟踪器。如果你喂给它的检测框质量差后面的联动匹配全部白搭。这就解释了为什么YOLOv5和DeepSORT是常见搭配YOLOv5速度快、对小目标检测效果好能够在每一帧给出足够稳定的框DeepSORT把这些框作为观测值结合历史轨迹预测当前帧每个目标的期望位置再做匹配。两者的边界清晰检测器负责“看见”跟踪器负责“记住谁是谁”。车辆这种刚性物体运动规律强外观变化小非常适合这种组合。3.2 一个Tracker类把它们拧到一起实际工程里我不会在每一帧里零散地调用两个模型的API而是封装成一个VehicleTracker类让主循环只面对一个update方法。这个类的常见实现如下import torch from yolov5.models.experimental import attempt_load from yolov5.utils.general import non_max_suppression from deep_sort.deep_sort import DeepSort class VehicleTracker: def __init__(self, yolo_weights, reid_weights, devicecpu): self.device device self.detector attempt_load(yolo_weights, map_locationdevice) self.tracker DeepSort( reid_weights, max_dist0.2, min_confidence0.3, nms_max_overlap1.0, max_iou_distance0.7, max_age70, n_init3, nn_budget100 ) def detect(self, frame_bgr): # OpenCV读进来是BGRYOLOv5训练时用的是RGB需要先转换 img torch.from_numpy(frame_bgr[:, :, ::-1].copy()).permute(2, 0, 1).float() / 255.0 pred self.detector(img.unsqueeze(0).to(self.device), augmentFalse)[0] pred non_max_suppression(pred, conf_thres0.4, iou_thres0.45)[0] dets [] if len(pred) 0: return dets for box in pred.cpu().numpy(): x1, y1, x2, y2, conf, cls box if int(cls) 0: # 只保留车辆类别 dets.append([x1, y1, x2, y2, conf]) return dets def update(self, frame_bgr): detections self.detect(frame_bgr) return self.tracker.update(detections, frame_bgr)detect方法里做了三步关键处理颜色通道转换、归一化、NMS去重。x1/y1/x2/y2是检测框左上角和右下角在原始图片上的像素坐标conf是置信度。这里只保留类别0车辆如果你的数据集把“卡车”“轿车”分成了不同类别可以改成多个类别号。DeepSort类的几个参数有实际意义max_dist是ReID特征余弦距离阈值越小越难匹配适合遮挡严重的场景调大一点min_confidence是检测置信度下限低于这个值的框不会进入跟踪器max_age是轨迹丢失后最多保留的帧数车辆短暂被挡时靠它续命n_init是Track初始化需要的连续匹配帧数防止一个误检框就建立一条假轨迹。update返回的是一个列表每个元素是[x1, y1, x2, y2, track_id]track_id是整个视频流内唯一且尽量稳定的车辆编号。3.3 主循环视频流里保持ID稳定有了Tracker类主循环就非常简单了import cv2 cap cv2.VideoCapture(demo.mp4) tracker VehicleTracker(your_yolo_weights.pt, your_reid_weights.pt, devicecuda:0) while True: ret, frame cap.read() if not ret: break tracks tracker.update(frame) for x1, y1, x2, y2, track_id in tracks: cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fID:{track_id}, (int(x1), int(y1) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(vehicle-tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这个循环里没有手动调阈值、没有自己写IOU计算所有逻辑都在Tracker里完成了。track_id不会像单纯YOLOv5那样每次都重新生成而是由DeepSORT内部维护。车辆被前车挡住几帧后重现时卡尔曼滤波会预测被遮挡期间它大概的位移ReID特征再确认外观所以只要遮挡不超过max_ageID就能延续。实际部署时要注意如果视频分辨率高、GPU性能弱建议在进入Tracker前先对帧做等比缩放或者设置跳帧策略每两帧跑一次检测中间帧直接用上一帧的跟踪结果做插值。4. 训练与配置用项目自带的数据集重训一个car_net4.1 把car_net_5.txt变成YOLOv5能吃的配置YOLOv5训练需要两类配置data配置描述数据集路径和类别名模型配置描述网络结构。项目里的car_net_*.txt负责后者。最常见做法是在项目根目录下建一个car_net.yaml作为data配置# car_net.yaml train: ./data/car/train.txt val: ./data/car/val.txt nc: 1 names: [car]train和val指向split_train_val.py生成的txt文件nc是类别数names是类别名列表。如果项目里只有一个车辆类别nc就是1。至于car_net_5.txt这类文件训练时放在--cfg参数位置。不同的数字对应不同的网络深度与宽度组合我一般会先试car_net_5.txt因为它结构更小显存占用低迭代一轮验证集指标的时间短方便快速确认数据路数通不通。等基本流程跑通了再换更大的car_net_141或car_net_161去追精度。4.2 训练命令与关键参数确认配置无误后从YOLOv5主目录执行训练python train.py \ --data car_net.yaml \ --cfg car_net_5.txt \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 100 \ --img 640 \ --patience 10 \ --device 0--data指向刚才写的car_net.yaml--cfg指向选定的模型配置。--weights推荐用官方yolov5s.pt做迁移学习起点因为车辆检测和COCO的通用目标检测共享大量底层特征从预训练权重开始收敛快得多。--batch-size受显存约束8G显存跑yolov5s建议从16起步显存不足就降到8或4。--img是输入分辨率640是训练速度和精度比较平衡的点如果后续要部署到Jetson这类设备可以提前用448或416训练推理时会快一截。--patience是早停参数验证集mAP连续10轮不提升就自动停止避免一直空跑。首次训练建议不要上来就用超大模型和超长epoch先让项目跑通一次全流程确认数据读取、loss计算、评估环节都正常再开始调参数。4.3 跑一次验证val.txt在这里起什么作用训练结束后用val.txt做离线评估python val.py \ --data car_net.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --batch-size 8val.py会自动读取car_net.yaml里指定的val.txt对每个图片做推理然后和标签算mAP0.5、mAP0.5:0.95、Precision、Recall这几项指标。单类别车辆场景下mAP0.5反映的是“车检出率”的主要参考值mAP0.5:0.95则更严格它对框和真实框的贴合程度更敏感。如果你的项目最后要接DeepSORT我更关注Recall和检测框稳定性因为跟踪器非常讨厌“时隐时现”的检测结果——这一帧多出一个框下一帧又丢掉DeepSORT会误判目标离开产生大量虚假轨迹。5. 避坑现场五个最常见的跑不通和性能翻车点5.1 环境三连numpy版本、torch版本、OpenCV后端第一个坑出现在import阶段。现象import deep_sort时报错提示numpy.ndarray size changed或者某个方法从numpy里找不到。原因DeepSORT早期实现依赖numpy 1.x的二进制接口而新装的YOLOv5环境默认拉到了numpy 2.x二进制的ABI对不上。解决新建一个conda环境先把numpy降到1.24.x再装其他依赖顺序反过来容易白折腾。第二个坑是RTSP视频流打不开。现象本地mp4用cv2.VideoCapture读没问题换成网络摄像头地址就超时或黑屏。原因OpenCV自带的ffmpeg后端对某些RTSP编码格式兼容性差。解决确认opencv-python装了带ffmpeg的版本或者在读取端改用ffmpeg命令行拉流把流转成帧队列再喂给Tracker。第三个坑是GPU利用率很高但FPS上不去。现象显卡风扇狂转监控里显示GPU占用接近100%但画面卡顿。原因推理时没有关闭梯度计算输入也没有切半精度检测器默认走了FP32全精度反向链路。解决在detect方法外层套上torch.no_grad()模型加载后调用.half()输入张量也转成img.half()FP16推理在主流显卡上能换来接近一倍的帧率提升。5.2 数据与配置val.txt路径分隔符、car_net被误当标注第四个坑非常隐蔽。现象训练一开始就报train: No labels in ...但val.txt明明存在标签目录里也确实有txt。原因split_train_val.py在Windows上生成的路径用反斜杠分隔比如images\frame_001.jpg而Linux训练时反斜杠会被当作普通字符导致路径失效。解决写脚本时不要直接拼字符串路径用os.path.join生成后统一replace(\\, /)或者训练前在Linux下对train.txt和val.txt做一次路径替换。第五个坑是把car_net_141.txt误当成标注文件。现象把car_net_*.txt复制到labels目录后训练时标签解析全部异常loss剧烈抖动。原因文件名里带“car_net”非常像目标标注文件实际它保存的是模型结构参数不是YOLO标注。解决动手前先随便打开一个txt看一眼如果是“0 0.5 0.5 0.1 0.2”这种五列数字才是标注如果是一堆网络层描述就属于模型配置放到--cfg参数去用。这个简单的检查能省掉一晚上的排查时间。5.3 跟踪效果为什么会崩第六个坑出现在跟踪环节。现象车辆明明一直停在画面里ID却每隔几秒变一次计数系统不停重复累加。原因min_confidence设太高YOLOv5对远处车辆输出的置信度本来就偏低直接被过滤器丢掉或者DeepSORT的ReID权重没真正加载成功外观特征退化成了全零向量匹配完全退化成SORT行为。解决先把min_confidence降到0.25到0.3同时打印一次tracker初始化日志确认真实读取了reid权重文件路径。如果ID仍然跳把max_age从70调到120让轨迹在目标短暂离开后保留更久。记住一个原则检测不稳时先调检测不要急着调跟踪参数。6. 二次开发把车辆检测改成你想要的目标跟踪6.1 一个最容易上手的改造接RTSP实时流把本地mp4换成摄像头流是这套项目落地到真实场景的第一步。改动非常小import cv2 cap cv2.VideoCapture(rtsp://your_ip:554/live) cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) while True: ret, frame cap.read() if not ret: continue tracks tracker.update(frame) # 其余绘制逻辑不变关键点是设置CAP_PROP_BUFFERSIZE为较小的值否则解码器内部积压的帧会越来越多造成画面延迟越来越大。实测里几行代码就能让项目从“跑通demo”变成“跑真数据”。6.2 再做一点给轨迹加ROI计数如果业务需要统计某个区域进出车辆数可以拿track_id做去重。项目中每个track_id对应一辆被持续跟踪的车记录它第一次出现在画面时的ID放入一个set最后统计set的大小就得到总车辆数。如果要统计跨线车流只需记录每个ID上一次的框中心点和当前框中心点判断两者是否落在ROI线的两侧是则计数加一。这套改造不涉及模型训练只改后处理逻辑适合验证DeepSORT的稳定度。6.3 验证方法改完代码不要急着接真摄像头。我先拿一段录好的混合场景视频人工数一遍实际车辆数再跑算法看统计结果。对比时重点看三个数检测漏掉的车辆数、ID跳变导致的重复计数、以及车辆被完全遮挡后是否还能对得上号。我的习惯是先把检测置信度调到0.3让“漏检”和“误检”大致平衡再针对记录里偏差最大的那一段单独抽帧检查是检测器问题还是跟踪器问题。从那以后我每次接手跟踪项目都强制走一遍“数据路径检查 → 检测单帧效果确认 → 跟踪ID稳定性观察”的顺序不再上来就调一堆参数。希望帮到你。本文还有配套的精品资源点击获取