简介本资源是一套面向计算机视觉研究者与算法工程师的离线测试平台聚焦YOLOv7目标检测与多目标跟踪算法的横向对比适用于视频监控、自动驾驶、机器人导航等场景的算法选型与性能评估。平台集成SORT、DeepSORT、ByteTrack、Bot-SORT等多种跟踪算法并基于VisDrone2019无人机航拍数据集进行统一测试便于在相同条件下比较各算法在遮挡、低分辨率等复杂环境中的表现。压缩包共393个文件约229.98MB以134个py源码与204个pyc编译文件为主体辅以yaml配置、png结果图、mp4演示视频、txt说明及md文档另含Dockerfile与ui界面文件结构完整便于二次开发。已有107人学习下载。读者可获得完整的检测跟踪对比方案、算法运行脚本、可视化结果与操作说明快速复现实验并定位各算法优劣为项目选型与优化提供参考。1. 无人机视角下的多目标跟踪为什么你的 YOLOv7 检测框总在跳把 YOLOv7 和 SORT、DeepSORT、ByteTrack、Bot-SORT 放在同一个离线测试平台上跑 VisDrone2019这件事的价值不在于“集成了多少算法”而在于你能在一个可控环境里回答一个很具体的问题同一段无人机视频同一套检测框换不同跟踪器ID Switch 到底差多少。VisDrone2019 的航拍视角决定了它的难点和地面监控完全不同——目标像素少、密度高、遮挡频繁、相机自身还在动。很多人在 MOT17 上跑得好好的跟踪器换到 VisDrone 上 IDF1 直接掉十几个点这不是玄学是检测质量和运动模型同时退化导致的。这套离线测试平台要解决的核心诉求是把检测和跟踪解耦固定 YOLOv7 的检测输出让 SORT、DeepSORT、ByteTrack、Bot-SORT 在完全相同的输入下比较。适合谁用做无人机视觉感知的工程师、需要选跟踪器落地到边缘设备的开发者、以及想理解“检测器换跟踪器到底影响多大”的研究者。下面从数据准备、检测推理、各跟踪器接入、参数调优到避坑按可复现的路径讲清楚。2. VisDrone2019 数据准备与 YOLOv7 检测输出固化2.1 VisDrone2019 的目录结构与标注格式转换VisDrone2019 的 MOT 子集标注格式和 YOLO 训练格式不一样它用的是帧级 txt每行包含 frame_index、target_id、bbox_left、bbox_top、bbox_width、bbox_height、score、object_category、truncation、occlusion。做跟踪比较时你需要的不是检测训练标签而是检测推理结果和跟踪评测用的 GT。常见做法是先把 VisDrone 的 MOT 标注转成 MOTChallenge 格式方便后续用 motmetrics 或 TrackEval 评测。import os import pandas as pd # VisDrone2019-MOT 标注转 MOTChallenge 格式 # 输入: VisDrone 原始标注文件, 每行 10 个字段 # 输出: MOTChallenge 格式 gt.txt, 每行 9 个字段 def visdrone_to_motchallenge(src_txt, dst_txt): rows [] with open(src_txt, r) as f: for line in f: parts line.strip().split(,) if len(parts) 8: continue frame_id int(parts[0]) target_id int(parts[1]) left float(parts[2]) top float(parts[3]) w float(parts[4]) h float(parts[5]) score float(parts[6]) category int(parts[7]) # MOTChallenge: frame, id, x, y, w, h, conf, -1, -1 # 只保留行人类别(VisDrone 中 category1 为 pedestrian) if category ! 1: continue rows.append([frame_id, target_id, left, top, w, h, 1, -1, -1]) df pd.DataFrame(rows) df.to_csv(dst_txt, indexFalse, headerFalse) # 参数说明: # src_txt: VisDrone 原始标注路径, 如 annotations/0000001_1.txt # dst_txt: 输出 MOTChallenge 格式路径, 如 gt/0000001.txt # category1 表示行人, 做行人跟踪比较时只保留这一类这段转换的关键在于类别过滤。VisDrone 包含行人、车辆、自行车等多类目标如果你要比较的是行人跟踪必须把 category 限定为 1否则 GT 里混入车辆会导致 ID 指标虚高。另一个坑是 VisDrone 的 target_id 在跨帧时可能不连续转换后要检查是否有 ID 跳变。2.2 用 YOLOv7 生成统一的检测结果文件跟踪器比较的前提是检测输入完全一致。我一般会先用 YOLOv7 对 VisDrone2019 的测试序列做一次离线推理把每帧的检测框存成 txt格式为 frame, x1, y1, x2, y2, score, class。这样后面换跟踪器时不需要重复跑检测也避免了不同跟踪器内部预处理导致的差异。# YOLOv7 离线推理 VisDrone2019 测试序列 # 输出每帧检测结果到 det/ 目录 python detect.py \ --weights yolov7_visdrone.pt \ --source /data/VisDrone2019-MOT/test/sequences/ \ --img-size 1280 1280 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --save-txt \ --save-conf \ --project runs/detect \ --name visdrone_det参数上--img-size建议设成 1280VisDrone 的小目标在 640 下召回率会明显下降。--conf-thres设 0.25 是一个折中太低会引入大量误检跟踪器会把噪声当目标跟太高则漏检严重ID Switch 反而增加。--save-txt和--save-conf必须同时开否则后面跟踪器拿不到置信度ByteTrack 这类依赖 score 分层的算法就没法工作。检测结果固化后建议做一次统计每帧平均检测框数量、置信度分布、小目标占比。如果平均每帧检测框少于 10 个说明置信度阈值偏高如果超过 80 个说明误检太多需要先调检测器再谈跟踪。2.3 检测结果与跟踪输入的格式对齐不同跟踪器对检测输入的格式要求不同。SORT 和 ByteTrack 通常接受[x1, y1, x2, y2, score]DeepSORT 还需要额外的特征向量Bot-SORT 则可能要求[x1, y1, x2, y2, score, class]。在平台里最好统一成一种中间格式再用适配器转换。# 统一检测格式适配器 # 输入: YOLOv7 输出的 txt 行 # 输出: 各跟踪器需要的 numpy 数组 import numpy as np def parse_yolo_txt(line): # YOLOv7 save-txt 格式: class x_center y_center w h conf (归一化) parts line.strip().split() cls int(parts[0]) xc, yc, w, h map(float, parts[1:5]) conf float(parts[5]) if len(parts) 5 else 1.0 return cls, xc, yc, w, h, conf def to_xyxy(xc, yc, w, h, img_w, img_h): x1 (xc - w / 2) * img_w y1 (yc - h / 2) * img_h x2 (xc w / 2) * img_w y2 (yc h / 2) * img_h return [x1, y1, x2, y2] # 参数说明: # img_w, img_h: 推理时的输入尺寸, 必须和 detect.py 的 --img-size 一致 # 如果 detect.py 用了 --img-size 1280, 这里就填 1280这里最容易翻车的地方是归一化坐标还原。YOLOv7 的 save-txt 输出是相对于推理尺寸归一化的如果你用 1280 推理但按 1920 还原所有框都会偏。建议在平台里把推理尺寸写进配置检测解析时直接读取不要硬编码。3. SORT 与 DeepSORT 在 VisDrone 上的接入与参数调优3.1 SORT 的最小实现与卡尔曼滤波参数SORT 的核心是卡尔曼滤波做运动预测加匈牙利算法做关联。在 VisDrone 这种相机运动的场景下SORT 的匀速模型假设经常不成立但它的优势是快适合做基线。接入时最关键的是卡尔曼滤波的过程噪声和观测噪声参数。from filterpy.kalman import KalmanFilter import numpy as np def create_kalman_filter(): kf KalmanFilter(dim_x7, dim_z4) # 状态: [x, y, s, r, vx, vy, vs] # 观测: [x, y, s, r] kf.F np.array([ [1,0,0,0,1,0,0], [0,1,0,0,0,1,0], [0,0,1,0,0,0,1], [0,0,0,1,0,0,0], [0,0,0,0,1,0,0], [0,0,0,0,0,1,0], [0,0,0,0,0,0,1] ], dtypefloat) kf.H np.array([ [1,0,0,0,0,0,0], [0,1,0,0,0,0,0], [0,0,1,0,0,0,0], [0,0,0,1,0,0,0] ], dtypefloat) # 过程噪声: 位置小, 速度大 kf.Q[4:,4:] * 0.01 kf.Q[:4,:4] * 0.01 # 观测噪声: VisDrone 检测框抖动大, R 要适当放大 kf.R[2:,2:] * 10.0 kf.P[4:,4:] * 1000.0 kf.P * 10.0 return kf # 参数说明: # Q 越大, 模型越信任观测, 跟踪框跟随检测框更紧, 但容易被误检带偏 # R 越大, 模型越信任预测, 跟踪框更平滑, 但遮挡后恢复慢 # VisDrone 建议 R 比 MOT17 大 2-5 倍, 因为航拍检测框抖动更明显SORT 的max_age和min_hits是两个必须调的参数。max_age控制目标丢失后保留轨迹的帧数VisDrone 遮挡频繁设太小会导致 ID 频繁切换设太大则会在目标离开后产生幽灵轨迹。我一般从 30 开始试根据序列帧率和目标运动速度调整。min_hits控制轨迹确认前需要连续匹配的帧数设 3 可以过滤掉大部分误检产生的短轨迹。3.2 DeepSORT 的外观特征提取与级联匹配DeepSORT 比 SORT 多了一个外观特征分支用 ReID 模型提取每帧检测框的特征向量在关联时同时考虑运动距离和外观距离。在 VisDrone 上外观特征的作用比较微妙一方面航拍目标像素少ReID 特征区分度有限另一方面相机运动导致同一目标在不同帧的背景变化大外观特征反而可能引入噪声。import torch import torchvision.transforms as T from torchvision.models import resnet50 class FeatureExtractor: def __init__(self, model_pathNone): self.model resnet50(pretrainedFalse) self.model.fc torch.nn.Identity() if model_path: self.model.load_state_dict(torch.load(model_path)) self.model.eval() self.transform T.Compose([ T.ToPILImage(), T.Resize((128, 64)), T.ToTensor(), T.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225]) ]) torch.no_grad() def extract(self, image, boxes): features [] for box in boxes: x1,y1,x2,y2 map(int, box[:4]) crop image[y1:y2, x1:x2] if crop.size 0: features.append(np.zeros(2048)) continue tensor self.transform(crop).unsqueeze(0) feat self.model(tensor).cpu().numpy().flatten() # L2 归一化, 保证余弦距离在 [0,2] feat feat / (np.linalg.norm(feat) 1e-6) features.append(feat) return np.array(features) # 参数说明: # 输入 crop 尺寸 128x64 是 DeepSORT 原论文设置 # 特征维度 2048 来自 ResNet50 全局池化层 # 如果显存紧张, 可以换 ResNet18 或 MobileNet, 但区分度会下降DeepSORT 的级联匹配里max_dist控制外观距离阈值max_iou_distance控制运动距离阈值。VisDrone 上我一般把max_dist从默认的 0.2 调到 0.3因为小目标外观特征本身就不稳定阈值太严会导致匹配失败。另外nn_budget控制每个轨迹保留的特征数量设 100 足够太大占内存且对结果提升有限。3.3 两个跟踪器在 VisDrone 上的实测差异在同一段 VisDrone 测试序列上固定 YOLOv7 检测结果SORT 和 DeepSORT 的差异主要体现在遮挡恢复和 ID 保持上。SORT 在目标密集区域容易出现 ID Switch因为纯运动匹配在目标交叉时无法区分。DeepSORT 在外观特征有效时能减少一部分 ID Switch但代价是推理速度下降约 30% 到 50%具体取决于 ReID 模型大小和 batch 策略。实测中还有一个反直觉的点DeepSORT 在 VisDrone 上的 MOTA 不一定比 SORT 高。因为 MOTA 主要受检测质量影响跟踪器带来的提升有限。真正拉开差距的是 IDF1 和 ID Switch 指标。如果你的应用更关心 ID 稳定性DeepSORT 值得上如果只关心帧级检测覆盖SORT 足够。4. ByteTrack 与 Bot-SORT 的低分检测利用和轨迹管理4.1 ByteTrack 的两阶段关联逻辑ByteTrack 的核心思想是把检测框按置信度分成高分组和低分组先用高分组做第一次关联再用低分组和未匹配轨迹做第二次关联。这个设计在 VisDrone 上特别有用因为航拍小目标的检测置信度普遍偏低如果直接卡 0.5 阈值大量真实目标会被丢掉。def bytetrack_associate(detections, trackers, high_thresh0.5, low_thresh0.1): # 第一阶段: 高分检测 所有轨迹 high_dets [d for d in detections if d[4] high_thresh] low_dets [d for d in detections if low_thresh d[4] high_thresh] # 用 IoU 距离做第一次匈牙利匹配 matched, unmatched_tracks, unmatched_dets linear_assignment( iou_distance(trackers, high_dets), thresh0.8 ) # 第二阶段: 低分检测 未匹配轨迹 # 只对第一次没匹配上的轨迹做二次关联 matched2, unmatched_tracks2, unmatched_dets2 linear_assignment( iou_distance(unmatched_tracks, low_dets), thresh0.5 ) return matched matched2, unmatched_tracks2, unmatched_dets2 # 参数说明: # high_thresh: 高分检测阈值, VisDrone 建议 0.4-0.5 # low_thresh: 低分检测下限, 建议 0.1, 再低会引入大量背景噪声 # 第一次 IoU 阈值 0.8 偏严, 第二次 0.5 偏松, 这是 ByteTrack 的默认策略ByteTrack 在 VisDrone 上的调参重点是track_buffer也就是轨迹丢失后保留的帧数。VisDrone 的无人机视频帧率通常 25 到 30目标被遮挡后重新出现的时间可能在 1 到 2 秒所以track_buffer设 30 到 60 比较合理。设太小会导致遮挡后 ID 切换设太大则会在目标离开画面后产生长尾幽灵轨迹。4.2 Bot-SORT 的轨迹重识别与相机运动补偿Bot-SORT 在 ByteTrack 基础上增加了两个模块相机运动补偿和轨迹重识别。VisDrone 是无人机视角相机自身运动是常态不做补偿的话卡尔曼滤波的预测位置会严重偏离。相机运动补偿通常用 GMCGlobal Motion Compensation实现通过帧间特征点匹配估计仿射变换矩阵。import cv2 import numpy as np class GMC: def __init__(self, methodsparseOptFlow, downscale2): self.method method self.downscale downscale self.prev_img None def apply(self, img, detections): # img: 当前帧 BGR 图像 # 返回: 补偿后的检测框 if self.prev_img is None: self.prev_img self._preprocess(img) return detections curr_img self._preprocess(img) if self.method sparseOptFlow: # 稀疏光流估计帧间变换 prev_pts cv2.goodFeaturesToTrack( self.prev_img, maxCorners1000, qualityLevel0.01, minDistance1 ) curr_pts, status, _ cv2.calcOpticalFlowPyrLK( self.prev_img, curr_img, prev_pts, None ) # 用 RANSAC 估计仿射矩阵 H, _ cv2.estimateAffinePartial2D(prev_pts, curr_pts) else: H np.eye(2, 3) self.prev_img curr_img if H is None: return detections # 把变换应用到检测框 warped [] for det in detections: x1,y1,x2,y2 det[:4] pts np.array([[x1,y1],[x2,y1],[x2,y2],[x1,y2]], dtypenp.float32) pts cv2.transform(pts.reshape(1,-1,2), H).reshape(-1,2) nx1,ny1 pts.min(axis0) nx2,ny2 pts.max(axis0) warped.append([nx1,ny1,nx2,ny2] list(det[4:])) return np.array(warped) def _preprocess(self, img): h, w img.shape[:2] return cv2.resize(img, (w//self.downscale, h//self.downscale)) # 参数说明: # downscale2 降低分辨率加速光流, 对精度影响可接受 # maxCorners1000 控制特征点数量, VisDrone 纹理丰富可以适当加大 # estimateAffinePartial2D 比 estimateAffine3D 更稳, 适合无人机场景Bot-SORT 的轨迹重识别模块会在轨迹丢失后用外观特征在后续帧中重新匹配。这个机制在 VisDrone 上对长时遮挡有效但前提是 ReID 特征足够可靠。如果检测框本身只有十几个像素ReID 特征基本是噪声这时候重识别反而会引入错误匹配。我的经验是VisDrone 上 Bot-SORT 的 ReID 权重不要设太高让运动补偿和 IoU 匹配占主导。4.3 四种跟踪器的速度与精度权衡在离线测试平台上速度对比必须在同一硬件、同一 batch 策略下做。SORT 最快单帧处理通常在 1 到 3 毫秒ByteTrack 因为两阶段匹配大约 3 到 8 毫秒DeepSORT 加上 ReID 推理可能到 15 到 30 毫秒Bot-SORT 加上 GMC 和 ReID通常 20 到 40 毫秒。这些数字随目标数量和硬件变化很大但相对关系稳定。精度上VisDrone 行人跟踪的 IDF1 通常呈现 ByteTrack 和 Bot-SORT 领先、DeepSORT 次之、SORT 垫底的格局。但差距没有想象中大如果检测质量本身不高换跟踪器带来的提升可能只有 2 到 5 个点。所以平台的价值在于让你用数据决定值不值得为跟踪器付出额外算力。5. 避坑与常见问题排查5.1 检测框坐标还原错误导致跟踪全乱现象跟踪框整体偏移或者框的大小和检测框明显不匹配。原因通常是 YOLOv7 输出的是归一化坐标而跟踪器按像素坐标处理中间缺少还原步骤或者还原时用的图像尺寸和推理尺寸不一致。解决方法是把推理尺寸写进配置文件检测解析时统一读取并在可视化时叠加检测框和跟踪框做对比一眼就能看出是否对齐。5.2 低置信度检测引发轨迹碎片化现象同一个目标在连续帧里 ID 频繁变化轨迹长度很短。原因可能是检测置信度阈值设得太高导致目标在部分帧漏检跟踪器只能不断新建轨迹。解决方法是降低检测阈值同时启用 ByteTrack 这类低分检测利用机制或者增大max_age/track_buffer让轨迹在短暂漏检时保持。5.3 相机运动补偿参数不当引入额外抖动现象开启 GMC 后跟踪框反而比不开更抖。原因是光流特征点匹配错误估计出的仿射矩阵不稳定。解决方法是限制 GMC 的变换范围对估计出的矩阵做合理性检查比如平移量超过图像尺寸 10% 就丢弃该帧的补偿。另外可以降低特征点数量提高质量阈值让参与估计的点更可靠。5.4 ReID 特征维度不匹配导致 DeepSORT 崩溃现象DeepSORT 初始化时报维度错误或者匹配时距离矩阵形状不对。原因是 ReID 模型输出维度和跟踪器配置里的feature_dim不一致。解决方法是先跑一次特征提取打印输出维度再同步修改跟踪器配置。如果换过 ReID 模型这个维度必须重新确认。5.5 评测指标计算时 GT 与预测帧号错位现象MOTA 和 IDF1 异常低但可视化看起来跟踪正常。原因是 GT 和预测的帧号起始不一致VisDrone 有些序列从 1 开始有些从 0 开始而 MOTChallenge 格式通常要求从 1 开始。解决方法是在转换和推理输出时统一帧号偏移评测前先检查两边帧号范围是否一致。6. 用 TrackEval 做离线评测与参数扫描的实操技巧6.1 TrackEval 的配置与 VisDrone 序列适配TrackEval 是目前比较通用的 MOT 评测工具支持 MOTChallenge 格式。用它评测 VisDrone 时需要把 GT 和跟踪结果按序列组织成它要求的目录结构。每个序列一个 txtGT 放在gt/mot_challenge/VisDrone/seqmap.txt对应的目录下跟踪结果放在trackers/mot_challenge/你的跟踪器名/data/下。# TrackEval 评测命令 python scripts/run_mot_challenge.py \ --BENCHMARK VisDrone \ --SPLIT_TO_EVAL test \ --TRACKERS_TO_EVAL sort bytetrack deepsort botsort \ --METRICS HOTA CLEAR Identity \ --USE_PARALLEL True \ --NUM_PARALLEL_CORES 4 # 参数说明: # BENCHMARK: 对应 gt/mot_challenge/ 下的数据集名 # TRACKERS_TO_EVAL: 多个跟踪器用空格分隔, 一次跑完 # METRICS: HOTA 综合指标, CLEAR 含 MOTA, Identity 含 IDF1 # USE_PARALLEL: 多序列并行, 序列多时明显加速TrackEval 的 seqmap 文件决定了评测哪些序列。VisDrone 测试集序列多建议先在小规模子集上验证流程确认指标计算无误后再全量跑。另外 TrackEval 对帧号连续性有要求如果某个序列中间缺帧需要在 seqmap 里正确配置否则指标会异常。6.2 参数扫描用网格搜索找每个跟踪器的最优配置固定检测结果后每个跟踪器都有几个关键参数可以扫。SORT 扫max_age和iou_thresholdByteTrack 扫track_buffer和high_threshDeepSORT 扫max_dist和nn_budgetBot-SORT 扫track_high_thresh和proximity_thresh。我一般用粗扫加细扫两轮第一轮每个参数取 3 到 4 个值找到大致范围后再细化。import itertools import subprocess import json # 参数网格扫描示例: ByteTrack param_grid { track_buffer: [30, 45, 60], high_thresh: [0.4, 0.5, 0.6], low_thresh: [0.1, 0.15] } results [] for combo in itertools.product(*param_grid.values()): params dict(zip(param_grid.keys(), combo)) # 写入临时配置 with open(tmp_config.json, w) as f: json.dump(params, f) # 跑跟踪 subprocess.run([python, run_tracker.py, --config, tmp_config.json]) # 跑评测 out subprocess.run( [python, scripts/run_mot_challenge.py, --BENCHMARK, VisDrone, --TRACKERS_TO_EVAL, bytetrack, --METRICS, HOTA], capture_outputTrue, textTrue ) # 解析 HOTA 分数, 这里简化处理 hota parse_hota(out.stdout) results.append({**params, HOTA: hota}) # 按 HOTA 排序输出最优参数组合 results.sort(keylambda x: x[HOTA], reverseTrue) print(results[:5]) # 参数说明: # track_buffer: 轨迹保留帧数, VisDrone 建议 30-60 # high_thresh: 高分检测阈值, 影响第一阶段匹配数量 # low_thresh: 低分检测下限, 影响第二阶段补充匹配扫描时要注意评测的随机性。如果跟踪器里有随机初始化或者 GPU 非确定性操作同一组参数跑两次结果可能略有差异。建议对每组参数至少跑一次完整测试集不要用单序列结果做决策。另外 HOTA 是综合指标如果应用更关心 ID 保持可以同时看 IDF1 和 AssA。6.3 从评测结果反推检测与跟踪的瓶颈拿到四个跟踪器的评测结果后不要只看总分。把指标拆开看DetA 低说明检测质量是瓶颈AssA 低说明关联策略有问题LocA 低说明定位精度不够。如果四个跟踪器的 DetA 都差不多但 AssA 差距大那跟踪器选型就很重要如果 DetA 普遍偏低应该先回去优化 YOLOv7 的检测换跟踪器收益有限。我自己的习惯是每次跑完评测把每个序列的指标单独列出来找出最差的三个序列然后逐帧可视化这些序列的跟踪结果。十有八九问题出在特定场景密集人群、严重遮挡、或者相机剧烈运动。针对这些场景调参比全局调参有效得多。最后说一个血泪经验离线测试平台最大的价值不是跑出一个漂亮数字而是让你能快速复现问题。把检测结果、跟踪结果、GT、可视化脚本都固化下来每次改参数只重跑跟踪和评测不要重跑检测。这样一轮参数扫描从几小时压缩到几十分钟迭代效率完全不一样。希望帮到你。本文还有配套的精品资源点击获取