简介:基于YOLOv5与DeepSORT的车辆目标跟踪综合应用源码,面向计算机视觉学习者、车辆监控系统开发者及交通流分析从业者,提供一套带PyQt5图形界面的完整可运行项目。系统涵盖摄像头视频播放、实时目标检测与多目标跟踪、车流量统计、车辆违停检测、车辆逆行检测等功能,可支撑智能交通场景下的算法验证与二次开发。资源共132个文件,以Python源码、YAML配置、Shell脚本为主,配套UI设计文件、模型权重、Dockerfile及项目说明文档,整体压缩包约43.69MB,目录结构清晰便于按模块学习。已有223人学习下载。代码模块化程度较高,读者可通过界面按钮快速调用检测跟踪流程,也可基于YAML参数调整模型配置,是理解YOLOv5与DeepSORT落地实践的良好参考。
1. 车辆目标跟踪从论文到桌面工具:这个 zip 里到底有什么值得你花一晚上
做视觉 AI 落地的人,大概率都经历过这种尴尬:模型在服务器上 mAP 刷得漂亮,一放到实际路口视频里,目标 ID 跳来跳去,同一个车隔几帧就换一次编号,下游的违章判断、车流统计全被这一个小问题带崩。标题里这个「基于 yolov5 与 deepsort 实现车辆目标跟踪与应用源码带 pyqt5 界面+项目说明」,本质上是把学术界最常用的检测加跟踪两段式方案,打包成了一个带图形界面的完整工程。它解决的正是从「模型能识别车」到「系统能持续跟踪同一辆车」这段落地距离。
这个方向对三类人特别值:一是要做课设或毕设的学生,需要一个能在自己电脑上跑起来、有界面能演示的完整链路;二是刚接触工业视觉的工程师,想看看 yolov5 和 deepsort 到底怎么串成一套实时管线;三是想快速验证车辆计数、逆行检测这类功能的开发者,不想从零开始搭界面和视频流。我拿到这类工程的第一反应不是直接跑,而是先拆它的目录结构和依赖关系,搞清楚检测与跟踪两个模块的接口长什么样——这决定了你后面改模型、换数据集时是顺手还是重新踩一遍坑。
2. 拆开压缩包看架构:yolov5 做检测、deepsort 做关联,两个模块的接口长什么样
2.1 先认清 yolov5 在跟踪链路里的角色:它只负责「这一帧哪里有车」
目标跟踪系统里最容易误解的一点,是认为 yolov5 承担了「跟踪」职责。实际上 yolov5 是一个纯检测器,它对每一帧图像独立推理,输出的是这一帧里所有目标的边界框、类别和置信度。在车辆跟踪场景里,它通常只保留 class 为 car、bus、truck 的检测结果,其余行人、自行车直接过滤掉,减少后续关联模块的计算压力。
检测输出的格式是整个管线的关键约定。yolov5 的 detect 输出经过非极大值抑制(NMS)后,每个目标对应一行 [x1, y1, x2, y2, conf, cls],这里 x1y1 是左上角坐标,x2y2 是右下角坐标。deepsort 的输入接口不接受这种格式,它需要的是带置信度的检测框 [x1, y1, w, h, conf],注意这里第四和第五个量是框的宽高而不是右下角坐标。我在实际工程里见过不少翻车现场,就是坐标转换没做对,跟踪器把宽高当成坐标用,导致目标运动估计完全错乱。
常见做法是把 yolov5 封装成一个检测器类,对外只暴露一个detect(frame)方法,返回统一格式的列表。这样不管后续换 yolov5s 还是 yolov5m,甚至换成其他检测器,跟踪模块的代码一行都不用改。接口设计上,检测器类内部负责加载权重、预处理、推理、NMS 和坐标转换,调用方只关心结果。这也是这类工程里最值得借鉴的设计思路——模块间耦合越松,后面换模型越省事。
class VehicleDetector: def __init__(self, weights_path, device='cpu', conf_thres=0.4, iou_thres=0.5): self.model = torch.hub.load('ultralytics/yolov5', 'custom', path=weights_path) self.model.conf = conf_thres self.model.iou = iou_thres self.model.classes = [2, 5, 7] # car, bus, truck self.device = device def detect(self, frame): results = self.model(frame, size=640) dets = [] for *xyxy, conf, cls in results.xyxy[0].tolist(): x1, y1, x2, y2 = [int(v) for v in xyxy] w, h = x2 - x1, y2 - y1 dets.append([x1, y1, w, h, conf, int(cls)]) return dets参数说明里有几个值得注意的点。conf_thres设成 0.4 是在漏检和误检之间取平衡,实际路口如果摄像头角度高、目标小,建议降到 0.3;如果场景里有大量误检,比如树影晃动被当成车,可以往上调到 0.5。classes限定为车类,这个必须和你训练或下载的模型权重匹配,coco 预训练权重里 2 是 car、5 是 bus、7 是 truck。如果用的是自己训练的车俩检测模型且只分一两类,这个列表要改成你自己的类别 ID,否则检测器会报索引越界或者输出空结果。size=640是推理分辨率,越高对小目标越友好但越慢,一般 640 够用。
2.2 deepsort 的输入输出约定:检测框进来,带 ID 的轨迹出去
deepsort 的核心是卡尔曼滤波加匈牙利算法。卡尔曼滤波负责预测每个已跟踪目标在下一帧的位置,匈牙利算法负责把当前帧的检测框和预测位置做最优匹配。匹配度由两部分组成:马氏距离描述运动相似度,余弦距离描述外观相似度。在车辆场景里,外观特征通常用一个小型 CNN 提取,也就是 deepsort 里的特征提取网络,它把每个检测框裁剪出来缩放到固定尺寸,输出一个特征向量用于重识别。
deepsort 对外的主要接口是一个update方法,输入是当前帧的检测结果和帧图像,输出是带 ID 的跟踪结果。在使用时,检测器给的是 [x1, y1, w, h, conf, cls],deepsort 内部会再做一次坐标转换,因为它的内部状态用的是以框中心点为基准的 [cx, cy, w, h, score]。实际调用中,常见做法是建立一个跟踪器实例,每帧先调检测器得到 dets,再调更新方法,最后把返回的每个目标的 ID 和坐标画到帧上。
from deep_sort.deep_sort.tracker import Tracker from deep_sort.deep_sort import nn_matching from deep_sort.deep_sort.detection import Detection def create_tracker(): metric = nn_matching.NearestNeighborDistanceMetric('cosine', max_cosine_distance=0.3, nn_budget=100) return Tracker(metric) def update_tracker(tracker, frame, dets): detections = [] for x1, y1, w, h, conf, cls in dets: if conf < 0.4: continue detections.append(Detection([x1, y1, w, h], conf, cls)) tracker.predict() tracker.update(detections) results = [] for track in tracker.tracks: if not track.is_confirmed() or track.time_since_update > 1: continue x1, y1, w, h = track.to_tlwh() results.append([int(x1), int(y1), int(w), int(h), track.track_id]) return results这段代码里有三个参数需要根据场景调。max_cosine_distance是外观特征匹配的阈值,设大了容易把两辆相似的车串成同一个 ID,设小了会导致遮挡后无法重识别,一般 0.3 是默认值,车型差异大的场景可以放宽到 0.4。nn_budget是特征库容量,它控制保存多少帧的历史特征用于匹配,100 在大多数场景够用,如果车流密度大、每帧目标多,建议加到 150 到 200,否则旧特征被过早挤出库,遮挡后容易丢 ID。time_since_update过滤条件控制输出哪些轨迹:大于 1 意味着该轨迹已经连续超过一帧没有匹配到检测框,通常是被遮挡或驶出画面,继续输出会造成轨迹残留。
2.3 帧循环与视频读写:实时管线的主循环不能只靠「读一帧处理一帧」
很多第一次接触这类工程的人,把 OpenCV 的VideoCapture读帧、检测跟踪、画框写视频三步写完就以为完事了,实际跑起来发现 CPU 占用拉满、画面卡顿。问题往往出在主循环的结构上。检测和跟踪是串行依赖的,但视频读取可以独立于处理逻辑存在。
我常用的结构是用一个队列做缓冲。主线程只负责cap.read()往队列里塞帧,工作线程从队列取帧做检测跟踪,这样即使某帧处理耗时超过帧间隔,也不会阻塞读取导致视频源断开。对于离线视频文件这不明显,但接 RTSP 摄像头流时,如果处理慢导致读帧阻塞,底层 socket 缓冲区满,摄像头端会主动断开连接,表现就是视频流跑几分钟就黑屏,重连又要好几秒。
import queue import threading frame_queue = queue.Queue(maxsize=8) def reader(cap): while True: ok, frame = cap.read() if not ok: frame_queue.put(None) break frame_queue.put(frame) def process(video_path, detector, tracker): cap = cv2.VideoCapture(video_path) t = threading.Thread(target=reader, args=(cap,), daemon=True) t.start() while True: frame = frame_queue.get() if frame is None: break dets = detector.detect(frame) tracks = update_tracker(tracker, frame, dets) draw_tracks(frame, tracks) cv2.imshow('vehicle tracking', frame) if cv2.waitKey(1) & 0xFF == ord('q'): breakqueue.maxsize设 8 是在实时性和内存占用之间取个平衡,太小容易让 reader 线程频繁阻塞,太大会引入额外延迟。实际测试里,处理一帧耗时约 80ms 的机器,这个队列能保证视频输入不丢帧、显示延迟不明显。要注意frame_queue.put(None)这个哨兵值,它通知处理线程视频读完了,避免线程永远阻塞在get()上。这属于多线程编程里典型的「退出条件缺失」问题,新手很容易在关闭程序时发现线程挂住退不出去。
3. PyQt5 界面的集成方式:把算法跑通和把工具做出来,中间隔着一个信号槽
3.1 为什么不用 OpenCV 的 imshow,而是专门写一套 PyQt5 界面
OpenCV 的imshow在调试阶段够用,但作为交付物它有几个硬伤:不能同时展示视频画面和参数控制面板,中文标题在部分环境下乱码,而且无法打包成规范的桌面应用。PyQt5 的价值在于它能把视频显示、检测置信度调节、跟踪参数设置、统计信息展示整合在一个窗口里,操作者不需要懂代码就能用。
这类工程的界面一般包含这几个区域:左侧是视频显示画布,右侧是参数面板,底部是状态栏和统计信息。参数面板里至少要暴露检测置信度阈值、IOU 阈值、最大余弦距离这三个常用调节项,这样用户不用改代码就能适配不同场景。界面与算法的交互方式,正确做法是信号槽机制:界面线程负责响应按钮点击和滑块拖动,算法线程负责跑检测跟踪,二者通过信号传递参数和视频帧,避免界面卡死。
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap class TrackWorker(QThread): frame_ready = pyqtSignal(QImage) stats_ready = pyqtSignal(dict) def __init__(self, video_path, detector, tracker, parent=None): super().__init__(parent) self.video_path = video_path self.detector = detector self.tracker = tracker self._running = True def run(self): cap = cv2.VideoCapture(self.video_path) while self._running: ok, frame = cap.read() if not ok: break dets = self.detector.detect(frame) tracks = update_tracker(self.tracker, frame, dets) annotated = draw_tracks(frame.copy(), tracks) rgb = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(QPixmap.fromImage(qimg).copy()) self.stats_ready.emit({'vehicle_count': len(set(t[4] for t in tracks))})信号槽机制在 PyQt5 里是线程安全的,frame_ready信号从工作线程发出,界面线程的槽函数接收后更新 QLabel,这个模式能保证视频显示流畅。有一个很容易踩的坑:QImage构造时用的是rgb.data,但它引用的是 numpy 数组的内存,如果数组在函数结束后被回收,图像数据就变成野指针。上面代码里通过QPixmap.fromImage(qimg).copy()做了深拷贝,这是必须的,否则界面上会出现随机花屏或程序崩溃。
3.2 PyQt5 安装与界面代码骨架:从零把界面跑起来
PyQt5 的安装现在比较省心,直接pip install PyQt5 PyQt5-tools就能拿到完整的环境。需要注意 Python 版本匹配,PyQt5 5.15 系列支持到 Python 3.9 左右,如果用的是 Python 3.10 以上,建议直接装 PyQt5 最新版或者干脆用 PyQt6。这个工程既然标题写的是 PyQt5,保持版本一致能减少不必要的兼容性排查。
界面骨架代码主要分三块:搭建控件布局、连接信号槽、启动工作线程。控件布局用 QVBoxLayout 和 QHBoxLayout 组合,左侧放 QLabel 做画布,右侧放 QGroupBox 装参数控件。按钮点击后启动 TrackWorker 线程,这是耗时任务避免阻塞 UI 的标准套路。
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QSlider, QVBoxLayout, QHBoxLayout, QWidget, QGroupBox) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('车辆目标跟踪系统') central = QWidget() self.setCentralWidget(central) main_layout = QHBoxLayout(central) self.video_label = QLabel('视频显示区') main_layout.addWidget(self.video_label, stretch=3) right_panel = QGroupBox('参数控制') right_layout = QVBoxLayout(right_panel) self.conf_slider = QSlider() self.conf_slider.setRange(10, 90) self.conf_slider.setValue(40) right_layout.addWidget(QLabel('检测置信度阈值')) right_layout.addWidget(self.conf_slider) self.start_btn = QPushButton('开始跟踪') right_layout.addWidget(self.start_btn) main_layout.addWidget(right_panel, stretch=1) self.start_btn.clicked.connect(self.start_tracking) def start_tracking(self): self.worker = TrackWorker('test.mp4', detector, tracker) self.worker.frame_ready.connect(self.update_frame) self.worker.start()这段代码里stretch=3和stretch=1控制了视频区和参数面板的宽度比例,窗口缩放时视频区按比例变大而参数区基本保持原宽。滑块的值范围设为 10 到 90 对应置信度 0.1 到 0.9,setValue(40)和检测器里的默认conf_thres=0.4保持一致,避免界面显示值与实际生效值不一致造成困惑。
3.3 界面线程与算法线程的通信:别在槽函数里做重活
PyQt5 界面卡死的根源,绝大多数是在槽函数里执行了耗时操作。槽函数运行在界面线程,如果里面直接调detector.detect(),一帧图像推理几百毫秒,界面在这期间完全无响应,鼠标点击、窗口拖动全部失效,用户体验就是「程序死了」。
正确姿势是把所有重计算都放进 QThread 的run()方法里,界面只通过信号接收结果。需要注意的一点是:如果参数滑块变化需要实时调整检测阈值,不要直接在工作线程里访问界面控件对象,那跨线程访问不安全。折中方案是滑块发出valueChanged信号时,通过一个线程安全的变量或者再发一个信号把值传递给工作线程。
class TrackWorker(QThread): conf_threshold = 0.4 def update_conf(self, value): self.conf_threshold = value / 100这里conf_threshold是工作线程自己的属性,界面通过worker.update_conf(value)去修改它,因为 Python 的 GIL 保证简单属性赋值是原子的,所以在检测循环里读取这个值不会产生脏数据。这种「界面只管传参、线程内自行取值」的设计,比跨线程直接操作控件要安全得多。
4. 训练自己的车辆数据集并替换权重:超参数怎么调,换模型后要改哪几处
4.1 自建车辆数据集的标注格式与目录结构
标题里的工程默认用的是 yolov5 官方预训练权重,它是在 coco 数据集上训练的,能检测 car、bus、truck 等常见车类。但实际场景里如果摄像头架设角度特殊,比如俯视停车场、高速卡口,或者要检测的是工程车、集装箱车这些细分类,预训练权重就不够用了。这时候必须训练自己的权重。
yolov5 训练数据采用 YOLO 格式:每张图片对应一个同名 txt 文件,每行是class_id cx cy w h,其中 cx、cy、w、h 都是相对于图片宽高的归一化值。目录结构上分为 images 和 labels 两个大文件夹,各自再划分 train 和 val 子集。很多人在这一步出错的原因是标注工具导出格式与 yolov5 要求不一致,比如用 LabelImg 导出成 Pascal VOC 格式后强行拷进 labels 目录,训练时直接报找不到标签文件。
datasets/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/一个常见做法是写个小脚本把 VOC 格式的 xml 转成 yolov5 的 txt。转换时最容易踩的坑有两个:一是 xml 里的坐标是绝对像素值,需要除以图片宽高做归一化;二是转换中心点坐标时要把 xmin 和 xmax 加起来除以 2 再除以宽,很多人写错成直接用 xmin 除以宽,训练出来的框全部偏到左上角。yolov5 的数据集配置文件是一个 yaml,里面指定 train 和 val 路径以及类别名列表,训练命令里用--data指向这个文件即可。
train: datasets/images/train val: datasets/images/val nc: 3 names: ['car', 'bus', 'truck']nc必须与标注文件里实际出现的最大类别 ID 一致,不然训练时类别数不匹配会直接报断言错误。names的顺序不是随便写的,它决定类别 ID 到类别名的映射,也决定了后面 deepsort 里你按哪个类别 ID 过滤车辆。举个例子,如果训练时names是 ['car', 'bus', 'truck'],那么检测结果的 cls 为 1 代表 bus,而不是 coco 数据集里 bus 的 ID 5。所以换自定义权重后,检测器里的classes过滤列表必须同步修改,这是替换权重后最常见的连带翻车点。
4.2 yolov5 训练超参数:哪些值得调,哪些保持默认
训练 yolov5 的默认参数对多数场景是合理的,真正值得手动调的集中在输入尺寸、batch size、学习率和数据增强这几项。--img 640是默认训练分辨率,如果你的场景里车辆在画面中占的像素很少,比如远距离卡口或者高点俯视,建议提高到 960 甚至 1280,代价是训练时间和显存占用翻倍。--batch取决于显存大小,一般 16 到 32 是常见区间,显存不足时优先降低 batch 而不是降分辨率。
--epochs是训练轮数,常规建议是 100 到 300。关键看 val 集的 mAP 曲线是否还有上升趋势,如果 150 轮后 mAP 不再变化,再训练只是浪费时间。yolov5 自带的学习率调度器会在训练后期自动降低学习率,所以不需要手动调初始学习率,保持默认的 0.01 即可。
数据增强方面,--hsv_h 0.015、--hsv_s 0.7、--hsv_v 0.4是默认的颜色增强参数,在车辆场景里不需要改太大,因为车辆颜色本身是重要特征,过度增强会让模型学到的颜色特征失真。--flipud 0.0上下翻转建议保持 0,因为车的语义和「上下颠倒」不符,强行翻转会让模型学到错误特征。--mosaic 1.0保持开启,它对小目标检测的提升非常明显。
python train.py --data data/vehicle.yaml --weights yolov5s.pt \ --img 640 --batch 16 --epochs 150 --project runs/train训练完成后权重文件在runs/train/exp/weights/best.pt,替换推理权重时只需要把检测器初始化里的weights_path指向这个文件。有个经验是训练时用 yolov5s.pt 做预训练权重比从零训练收敛快得多,因为 COCO 预训练模型已经学会了通用的纹理和边缘特征,迁移到车辆数据集只需要微调高层语义,一般 150 轮就能达到可用精度。
4.3 换模型后 deepsort 侧需要检查的三处配置
权重换完,最怕的就是检测器跑得挺好,跟踪器表现却崩了。原因往往不在 deepsort 本身,而是检测输出的类别 ID 和跟踪模块的处理逻辑对不上。第一处是类别过滤,如果自定义数据集把类别顺序改成 ['轿车', '货车'],那么原来的classes=[2, 5, 7]不再适用,正确做法是全部保留或者只保留需要的 ID。第二处是置信度阈值,自定义模型的最佳置信度阈值通常和 COCO 预训练模型不一样,要在测试视频上多试几档,观察漏检和误检的平衡点。第三处是外观特征的匹配阈值max_cosine_distance,如果你训练的数据集类别更细、车辆外形更接近,不同车的外观特征向量会更相似,阈值需要适当调低到 0.2 左右,否则容易出现不同车串 ID。
这三处配置看起来独立,实际是关联的。置信度阈值调高了,检测框变少,跟踪器匹配时可选的目标减少,容易出现轨迹中断。外观阈值调低了,遮挡后的重识别变困难,ID 切换次数反而上升。所以换权重后不要只调检测参数就完事,一定要在完整跟踪链路里重新跑一遍验证视频,重点看 ID Switch 次数。
5. 落地场景与性能调优:车辆计数的正确打开方式,以及 CPU 上怎么不卡成幻灯片
5.1 车辆计数与跨线判断:不要用「中心点是否在 ROI 内」这种笨办法
很多基于这个工程做的车辆计数,实现方式是判断每帧跟踪框的中心点是否落在某个区域内,落在区域内计数加一。这个做法在简单场景能用,但有个致命缺陷:目标在区域内停留多帧时,每帧都满足条件,计数值就会重复累加。绕开这个坑的办法是配合「轨迹跨线」判断:预先在画面上画一条虚拟线,记录每个目标上一帧的中心点坐标,对比当前帧中心点与虚拟线的位置关系,只有出现「从线的一侧变到另一侧」时才计数。
def cross_line_count(tracks, line_y): global last_positions, cross_count current_ids = set() for x1, y1, w, h, track_id in tracks: cx = x1 + w / 2 cy = y1 + h / 2 current_ids.add(track_id) if track_id in last_positions: prev_cy = last_positions[track_id][1] if (prev_cy - line_y) * (cy - line_y) < 0: cross_count += 1 last_positions[track_id] = (cx, cy) for tid in list(last_positions.keys()): if tid not in current_ids: del last_positions[tid](prev_cy - line_y) * (cy - line_y) < 0这个条件表示前后两帧的中心点分别位于线的两侧,也就是真正穿过了线。这样做不会漏计长时间停在线上再起步的车,因为只要它穿线,前后两帧中心点就会落在不同侧。同时要清理last_positions里已经消失的 ID,否则字典越积越大,内存和计算量无谓增长。实际路口录像是会一辆车占多帧的,这个方案能把计数误差控制在一个很低的范围。
5.2 CPU 推理的降帧策略:与其裁剪模型,不如让处理节奏匹配场景
CPU 上跑 yolov5s 加 deepsort,1080P 视频很难做到实时。多数人的第一反应是换更小的模型,比如 yolov5n,但这样做检测精度下降明显,小目标的召回率掉了不少。实际上在车辆跟踪场景里,合理的处理策略是隔帧推理:跟踪器对每一帧都执行predict()预测位置,但检测器每隔一帧才真正推理一次,中间帧直接用跟踪器的预测结果画框。因为车辆运动在短时间间隔内是平滑的,隔一帧检测对跟踪精度的影响远小于模型变小带来的损失。
frame_count = 0 skip_frames = 1 while cap.isOpened(): ok, frame = cap.read() frame_count += 1 if frame_count % (skip_frames + 1) == 0: dets = detector.detect(frame) tracks = update_tracker(tracker, frame, dets) else: tracker.predict() tracks = [] for track in tracker.tracks: if track.is_confirmed(): x1, y1, w, h = track.to_tlwh() tracks.append([int(x1), int(y1), int(w), int(h), track.track_id])这个改动的收益是接近翻倍的推理速度,因为省掉了检测这个最耗时的环节。skip_frames=1表示隔一帧检测一次,如果视频帧率是 25fps,实际每秒只需推理 12 到 13 次,对大多数场景足够了。要注意的是中间帧的轨迹框是纯预测结果,长时间不检测会让框逐渐漂移,所以 skip 值不建议超过 2,否则车辆快速变道或转弯时框的贴合度会明显变差。
5.3 PyQt5 界面显示的性能瓶颈:不要每帧都转 QImage
界面卡顿的另一个常见原因是每帧都执行cv2.cvtColor和QImage构造,这两个操作对 1080P 图像来说都不便宜。优化思路是降低显示分辨率:把要显示的帧先resize到宽度 960 左右再转换,人眼看不出明显差异,但耗时能减半。另一个技巧是控制刷新频率,不需要每处理完一帧就刷新界面,可以累积两到三帧再更新一次显示,维持每秒 15 帧左右的视觉流畅度即可。
display_frame = cv2.resize(annotated, (960, int(annotated.shape[0] * 960 / annotated.shape[1]))) qimg = QImage(display_frame.data, display_frame.shape[1], display_frame.shape[0], display_frame.strides[0], QImage.Format_RGB888)QImage的bytesPerLine参数如果填的是display_frame.strides[0],能避免因为图像宽度不是 4 的倍数导致的显示错位问题。这是 OpenCV 与 Qt 之间数据格式兼容性的一个经典坑:OpenCV 的图像行步长可能不等于width * channels,直接按后者构造 QImage 在某些分辨率下画面会产生斜向撕裂。用strides[0]是稳妥做法,但要注意 QImage 仍然引用的是 numpy 数组内存,把这个display_frame变量保持在槽函数作用域内,不要等函数返回后对象被回收还拿着 QImage 去用。
6. 从能跑到能交付:模型精度验证、识别准确率的量化指标,以及这个方向的性价比
验证一个跟踪系统是否合格,不能只看「画面里框跟得上车」这种主观感觉。常用的量化指标是 MOTA(多目标跟踪准确率)和 IDF1(ID F1 分数),前者综合了漏检、误检和 ID 切换的惩罚,后者侧重衡量 ID 保持的稳定性。对车辆计数场景,我更关心 ID Switch 次数,因为这个值直接决定计数有没有重复或多计。可以准备一段 5 分钟左右、包含遮挡和并行的典型路口视频,人工标注出每辆车出现的真实帧区间,然后对比跟踪结果算 ID Switch。
如果你用的是标注好的基准数据集,比如 MOT16 或 UA-DETRAC,跑一遍就能拿到指标;但实际落地场景的数据和公开数据集分布差异很大,建议复用训练集里的验证视频做人工检查。我习惯的做法是导出一份带完整轨迹的视频,重点看三个片段:车辆并排行进时 ID 有没有互相抢、大车挡住小车后重新出现时 ID 是否复用、车辆驶出画面边缘再回来时是否被当成新车。这三个片段通过,系统的稳定性基本过关。
把 yolov5 换成更轻量的模型、把 deepsort 换成更快的匹配策略,这些优化空间都存在,但在这个工程量级上,我反复吃过亏之后得出的结论是:先保证主链路数据格式正确、线程不卡死、计数逻辑严谨,再去追求精度和速度的边际提升。很多性能问题其实是配置问题,比如分辨率没降、阈值没调、坐标系搞错——修好这些,比换模型带来的收益大得多。这套工程最大的价值不是某个算法有多新颖,而是把「检测 + 跟踪 + 界面」这三层完整串通了,让你能在一个晚上看到全流程跑通的效果。替换自己的数据和权重后,它完全可以当成一个车辆跟踪系统的底座来迭代。希望这篇拆解能帮你少走几步弯路,把时间花在真正影响落地效果的算法调优和场景适配上去。
本文还有配套的精品资源,点击获取