ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

YOLOv10+StrongSORT+OSNet多目标跟踪实战:原理到部署

2026/9/8 10:26:25 拓冰建站 浏览量
YOLOv10+StrongSORT+OSNet多目标跟踪实战:原理到部署 简介面向目标跟踪与重识别方向的开发者资源提供完整的YOLOv10结合StrongSORT与OSNet的工程代码可直接用于视频中的多目标检测、跟踪与行人重识别适合作为项目基座或算法学习参考。压缩包为zip格式约115.19MB内含Python源码与模型权重文件便于本地复现验证。目前已有513人学习下载常用于智能监控、自动驾驶感知及学术实验等场景。工程实现高度模块化基于argparse命令行动态解析模型权重、视频路径、置信度阈值、IOU阈值等参数自动选择图形处理器或中央处理器作为运行设备通过Detector类同时管理YOLOv10检测器与StrongSORT跟踪器为每个目标维护独立轨迹视频处理逻辑自动获取帧率与尺寸并适配多种编码输出每一帧的检测结果直接送入跟踪器以更新目标标识与位置轨迹绘制利用不同颜色区分类别并依据最大轨迹长度自动截断保证显示效果始终清晰流畅。整体代码结构完整、模块间耦合度低特别适合需要快速集成目标跟踪能力的二次开发场景。1. 项目概述与适用场景1.1 这套组合到底在解决什么问题目标跟踪这个方向这几年我前后换过好几套技术栈。最早用DeepSORT后面换过ByteTrack最近把YOLOv10和StrongSORTOSNet整套搭起来跑通了效果确实比之前稳不少。先说清楚这套东西能干什么给定一段视频它能框出目标人、车、或者你自定义的类别给每个目标分配一个固定ID整个视频里这个ID始终跟着同一个目标走。哪怕目标短暂出画、被遮挡、再回来ID不会跳变身份不会丢。重识别ReID就是干这件事的——靠外观特征把同一个目标认回来。这套架构适用面很广线下门店客流统计、安防监控里的重点人员轨迹还原、智慧交通里的车辆连续跟踪都能直接套用。我做这套组合的动机很简单YOLOv10把检测精度和速度又往上推了一截而StrongSORT在ID SwitchID跳变的控制上比DeepSORT强OSNet则给ReID提供了轻量但足够好用的特征提取能力这三个一拼长视频跟踪的稳定性直接拉满。1.2 适合谁来参考如果你是刚接触目标跟踪的入门者这篇能帮你把检测器和跟踪器之间的关系彻底理顺如果你已经跑通过DeepSORT这篇能让你快速知道StrongSORT升级了什么、OSNet网络是怎么接入的。如果你只关心怎么把跟踪结果用起来代码里也单独抽出了调用接口拿来改改就能接自己的业务。我后面所有代码都基于Ubuntu 20.04 Python 3.8 PyTorch 1.10跑通显卡用的是RTX 3090。你用Windows也能跑个别路径和依赖有小差别我会在对应位置提醒。2. 技术选型拆解为什么是YOLOv10 StrongSORT OSNet2.1 YOLOv10相比v5/v8在跟踪任务里的真实优势选检测器时不能只盯着mAP看跟踪任务里检测器有两个额外要求漏检要少、框的定位要稳。漏检一次跟踪器就可能多一次轨迹新建框抖一下卡尔曼滤波就得跟着“猜”一遍。YOLOv10最大的变化是去掉了NMS非极大值抑制这个改动在跟踪场景里非常实用。传统YOLO的NMS后处理会带来额外延迟而且当两个人紧挨着走时NMS阈值得反复调调小了漏检调大了同一个目标产生两个框跟踪器会措手不及。YOLOv10用“无NMS”的训练策略让每个目标最多只出一个框不仅省掉了后处理时间密集人群下的检测质量也更稳定。我在实际测试里对比过同样一段拥挤地铁站的视频YOLOv8需要把NMS阈值调到0.6以下才能少漏检但伴随的是大量重复框YOLOv10默认参数下几乎没有重复框给下游省了很多麻烦。另一个实际感受是YOLOv10的模型导出和推理更干净。强M个框输入到跟踪器后跟踪器不再需要对低置信度框做大量过滤特征提取压力也随之减小。如果只是跑检测v8和v10差距感受不明显一旦接上跟踪器做长时间推理v10的稳定性和帧率优势就体现出来了。2.2 StrongSORT与DeepSORT的升级点DeepSORT是绕不开的经典方案它用卡尔曼滤波预测位置 匈牙利算法做框和轨迹的匹配。DeepSORT最头疼的问题是长时间遮挡后再出现外观特征和运动预测容易对不上ID切换频繁。StrongSORT主要改了三个地方每一个都直接对应我踩过的坑第一它加入了一种更鲁棒的运动特征融合方式。DeepSORT是用马氏距离衡量预测框和检测框的接近程度遮挡久了马氏距离惩罚很重容易把同一目标的新框判成新目标。StrongSORT引入了基于运动特征和外观特征的自适应融合权值在运动特征不可信时依靠外观特征兜底。第二它把轨迹的“出生”和“死亡”判断改成了一种带双阈值策略的方式。DeepSORT里轨迹连续多少帧没匹配就删除阈值调小了目标短暂出画就灭掉调大了轨迹残留很久导致ID错乱。StrongSORT设计了更细致的生命周期管理实际跑下来同样的视频ID Switch数量比DEepSORT下降了大约30%到40%。第三它集成了相机运动补偿模块ECC。如果摄像头固定不动卡尔曼预测就够了如果摄像头有轻微晃动框的坐标整体偏移卡尔曼预测会一直出错。StrongSORT里加了ECC全局运动估计先把帧间全局位移算出来再喂给卡尔曼滤波器这个功能对云台摄像机、手持拍摄的视频极其重要。2.3 OSNet为什么适合当ReID特征提取器ReID网络负责把检测框裁剪出来压缩成一个固定长度的特征向量。选网络要看三点特征判别力强不强、向量维度大不大、前向速度够不够快。OSNetOmni-Scale Network是比较新的轻量级ReID网络它设计了一套多尺度特征提取结构一个人穿着相似衣服、背不同包或者换个角度深度学习特征仍然能拉出明显差异。OSNet默认输出的特征维度是512维在多数跟踪场景里512维是性价比很好的选择——比1024维快很多又比256维更能抗外观变化。我试过用ResNet50配合更复杂的高维特征准确率小幅提升但耗时明显增加OSNet的平衡点更适合实时跟踪。接入时需要注意ReID模型对输入图像尺寸比较敏感。YOLOv10给出的检测框宽高比各种各样我在使用中会先把检测框resize到256×128高度256、宽度128这是ReID社区比较通用的尺寸。直接用原图尺寸扔给OSNet特征质量会明显下降这一点在代码里已经处理好了。3. 环境准备与项目结构3.1 依赖安装与版本血泪教训建议直接用conda新建环境Python版本不要选太新很多视觉库对3.10及以上的适配还有些小毛病。我用的配置是conda create -n yolo_track python3.8 conda activate yolo_track pip install torch1.10.0 torchvision0.11.0 --extra-index-url https://download.pytorch.org/whl/cu113 pip install opencv-python4.5.5.64 pip install scipy1.7.3 pip install lap0.4.0 pip install filterpy1.4.5 pip install ultralytics8.1.34 pip install Cython meson这里有几条避坑心得ultralytics包版本别追最新YOLOv10刚发布那阵子新版ultralytics经常把接口改掉8.1.34是我测过最稳定的版本。scipy版本必须低于1.9因为StrongSORT源码里用了旧的线性分配接口新版本scipy把这些函数挪了位置会直接ImportError。lap库用来做匈牙利匹配直接用pip默认源装可能因为编译问题失败先装Cython再装lap能减少很多编译坑。OpenCV不要用最新版部分较新版本对视频解码器的兼容性反而回落我这里固定到4.5.5。3.2 项目目录的粗暴但实用组织方式网上很多开源项目的目录层次特别深跑起来容易迷路。我习惯把一个跟踪项目拆成五个部分目录结构如下yolov10_strongsort_osnet/ ├── configs/ │ ├── yolo10n.yaml │ └── strongsort_config.yaml ├── detector/ │ ├── detect_yolo10.py │ └── tracker_configs.py ├── tracker/ │ ├── strongsort.py │ ├── matching.py │ └── kalman.py ├── reid/ │ ├── osnet_reid.py │ └── osnet_weights.pth ├── run_tracker.py ├── utils/ │ ├── video_io.py │ └── visualize.py └── sample_videos/ └── campus_demo.mp4这个结构的好处是检测、跟踪、重识别三个部分天然解耦出问题时能快速定位是检测的锅、特征的锅还是匹配的锅。如果你想把结果封装成API接口只需要在run_tracker.py里把输入换成视频帧生成器输出改成JSON格式的轨迹数据即可。3.3 YOLOv10的yaml配置文件从哪来很多新手卡在这里下载YOLOv10权重后找不到配套的yaml文件。这是个正常现象因为YOLOv10在ultralytics里是直接用COCO预训练权重加载的仓库里没有单独放一份定义文件。你需要自己手工创建yolo10n.yaml里面定义模型的背骨、颈部、头部结构。如果你不打算从零改模型结构可以直接在代码里用字符串初始化from ultralytics import YOLO model YOLO(yolov10n.pt)这时ultralytics会自动用默认的YOLOv10结构定义去加载你完全不需要手动写yaml。但如果想自定义类别数量比如做行人检测以外的任务就需要自己写yaml了。最简单的做法是从ultralytics内置的yolov8.yaml复制一份把backbone和head部分替换成YOLOv10的C2fUC和SCDown模块。这里给一份精简的行人检测yaml写法只保留关键结构部分# yolo10n_person.yaml nc: 1 scales: {n: [0.33, 0.25, 1024]} backbone: - [-1, 1, Conv, [64, 3, 2]] - [-1, 1, Conv, [128, 3, 2]] - [-1, 3, C2fUC, [128, True]] ... head: - [-1, 1, SCDown, [256, 3, 2]] - [-1, 3, C2fUC, [256, True]] ...写完后用model YOLO(yolo10n_person.yaml)加载它会按yaml结构初始化网络再套用预训练权重。4. 核心代码拆解与实操实现4.1 检测器封装从YOLOv10到框列表跟踪器需要的检测结果不是图片也不是pyTorch Tensor而是标准化后的框坐标。封装检测器时推荐把所有业务逻辑收敛到一个类里后面调用时不用关心内部实现。import cv2 import torch import numpy as np from ultralytics import YOLO class YOLOv10Detector: def __init__(self, weights_path, conf_thresh0.35, classesNone): self.model YOLO(weights_path) self.conf_thresh conf_thresh self.classes classes # 例如 [0] 表示只检测person def detect(self, frame_bgr): results self.model.predict( frame_bgr, confself.conf_thresh, classesself.classes, verboseFalse ) boxes [] scores [] for result in results: if result.boxes is None: continue # xyxy格式左上角x, 左上角y, 右下角x, 右下角y xyxy result.boxes.xyxy.cpu().numpy().astype(np.float64) conf result.boxes.conf.cpu().numpy().astype(np.float64) boxes.extend(xyxy) scores.extend(conf) return np.array(boxes) if boxes else np.empty((0, 4)), np.array(scores) if scores else np.empty((0,))调用这段代码时有个容易忽视的点YOLOv10返回的框坐标是相对于输入图像的原始分辨率而后续卡尔曼滤波器计算用的是像素坐标。这本身没问题但一旦你做了多尺度推理或者把大图切块检测就一定要把检测框缩放回原图坐标否则跟踪位置会整体偏移。我对置信度阈值的建议是设在0.3到0.4之间。太低了大量弱检测框会干扰关联太高了漏检率上升一旦遮挡出现目标检测不到跟踪就断了。0.35是我在多数场景里试出的平衡点。4.2 StrongSORT核心匹配流程跟踪器的匹配逻辑是整个项目的中枢。StrongSORT里维护一组“轨迹”每条轨迹包含历史位置、速度估计、外观特征库。每一帧做四件事用卡尔曼预测轨迹当前位置、计算轨迹和检测框的IoU/余弦相似度、构建代价矩阵、用匈牙利算法完成匹配。核心代价矩阵的计算分为两个矩阵from scipy.spatial.distance import cdist def compute_cost_matrix(detections, tracks, appearance_feats): # 运动代价基于马氏距离 track_positions np.array([t.predict_state() for t in tracks]) motion_cost cdist(detections[:, :2], track_positions[:, :2], metriceuclidean) # 外观代价基于余弦距离。1 - 余弦相似度 if appearance_feats is not None and len(appearance_feats) 0: app_cost cdist(appearance_feats, np.array([t.get_best_feature() for t in tracks]), metriccosine) else: app_cost np.ones_like(motion_cost) * 1e6 cost 0.7 * motion_cost 0.3 * app_cost return cost这个0.7/0.3的比例是我一开始拍脑袋设的后来做了实验运动特征越准加权应该越偏向运动当目标被长时间遮挡导致运动预测漂移时外观权重需要相应提升。StrongSORT里有一个根据连续未匹配帧数动态调整权重的机制实际效果比固定权重好不少。建议不要直接抄我的比例先用固定权重跑一遍带遮挡的测试视频再根据ID Switch数量做调整。匹配完成后成功匹配的检测框会用来更新轨迹坐标、更新卡尔曼状态、更新外观特征队列。未匹配的检测框会尝试新建轨迹连续三帧都有匹配的检测框才正式标记为确认轨迹。未被匹配的轨迹先保留一段时间保留期内持续没有匹配就删除。4.3 OSNet重识别特征提取与缓存OSNet从FastReID项目里抽取出来单独用不想改太多依赖的话可以直接用FastReID提供torchscript模型。但为了灵活性和轻量我用的是timm搭建的OSNet推理封装import torch import torch.nn as nn from torchvision import transforms class OSNetReID(nn.Module): def __init__(self, model_path, feature_dim512): super().__init__() self.model self._build_osnet() state torch.load(model_path, map_locationcpu) self.model.load_state_dict(state) self.model.eval() self.transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((256, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def _build_osnet(self): from torchreid.utils import load_pretrained_weights from torchreid.models import build_model model build_model(nameosnet_x1_0, num_classes1000, pretrainedTrue) return model torch.no_grad() def extract(self, patch_bgr): if patch_bgr.size 0: return torch.zeros(512) img self.transform(patch_bgr).unsqueeze(0) feat self.model(img).cpu().numpy().flatten() norm np.linalg.norm(feat) return feat / (norm 1e-12)如果把OSNet当成一个函数直接每次调用速度很快但重复计算太多。同一目标在连续帧里会被检测到好多次每次提取特征完全没必要。我建议在跟踪器内加一个特征缓存机制每个轨迹保存最近20帧的外观特征列表计算外观代价时取列表均值或最近一次特征即可。这样ReID推理数量能从每帧全量降为仅处理新增检测框推理耗时可以减少七成以上。4.4 主流程run_tracker.py是怎么把所有环节串起来的import cv2 from detector.detect_yolo10 import YOLOv10Detector from reid.osnet_reid import OSNetReID from tracker.strongsort import StrongSORT def run(video_path, det_weight, reid_weight): cap cv2.VideoCapture(video_path) detector YOLOv10Detector(det_weight, conf_thresh0.35, classes[0]) reid OSNetReID(reid_weight) tracker StrongSORT(reidreid, max_age30, n_init3) while True: ret, frame cap.read() if not ret: break det_boxes, det_scores detector.detect(frame) tracked_objects tracker.update(det_boxes, det_scores, frame) for obj in tracked_objects: x1, y1, x2, y2, track_id, _ obj 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)-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(YOLOv10 StrongSORT OSNet, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() if __name__ __main__: run(sample_videos/campus_demo.mp4, weights/yolov10n.pt, weights/osnet_x1_0.pth)整个主流程只需要调三个类逻辑非常清楚。StrengSORT的update方法内部接收检测框、置信度、原始帧返回跟踪结果。把max_age30理解为目标最多能连续30帧不出现仍然保留轨迹超过30帧它还没回来轨迹就会删除ID会被回收。遮挡严重的场景建议把max_age调到50甚至更高。我在实际调参中发现n_init用3是合理的短时间噪声框如果连续3帧都有检测结果才会确认为正式轨迹能挡住大量误检。5. 参数校准与实验结果5.1 动手前先想清楚评测指标怎么看跟踪效果不是靠肉眼“感觉”的得用指标说话。MOTA多目标跟踪准确率、IDF1ID F1分数和MOTP多目标跟踪精度是最常用的三个指标。MOTA主要看整体漏检、误报和ID切换的加权结果IDF1更关注ID分配的准确性简而言之IDF1越高说明“身份保持”越好这正是重识别的核心目标。我用的测试视频是自己标的一段校园道路场景包含三个人并行、两个人交叉相遇、一个人被树干遮挡后重新出现时长约90秒。跑完后的结果是MOTA: 0.72IDF1: 0.78ID Switch: 4次。作为对比同一份视频用DeepSORT跑ID Switch是7次。注意这里只说我的环境下的相对差异不同场景结论可能不同但遮挡场景下StrongSORT的ID稳定性优势是真实存在的。5.2 调参顺序和参考范围调参有个口诀先调检测、再调跟踪的阈值、最后调ReID特征保留策略。先确认YOLOv10的conf阈值合适——检测框如果抖动太大后面的参数全白调。然后是StrongSORT里的max_age和IoU阈值我通常把IoU阈值设在0.25附近再低就会让不相干的目标强行匹配。最后是外观特征队列特征队列越长对外观变化的容忍度越高但目标换衣服后旧特征会干扰匹配队列长度20到30帧是比较好的折中。如果发现ID切换特别频繁优先排查两种可能一是ReID网络输入尺寸不对回头检查是否resize到256×128二是ossible特征没有做L2归一化。归一化直接影响余弦相似度计算没归一化等于没算对距离。5.3 YOLOv10在跟踪链路里的速度表现我在RTX 3090上测了320×320输入下YOLOv10n检测单帧约3msOSNet每100帧的特征提取平均约19msStrongSORT的匹配计算本身几乎不耗时1ms以内。整体跑640×640输入时帧率能稳定在25到30 fps。如果部署到Jetson设备上建议用TensorRT导出YOLOv10和OSNet的engine文件把输入分辨率压到320帧率也能保持实时。6. 常见问题速查与避坑记现象根因解决方案运行即报错ImportError: linear_assignmentscipy版本过高降到scipy 1.7.xYOLOv10权重加载报结构不匹配自作主张改了yaml但没配对用仓库自带默认网络结构或保存完整模型而非只存权重跟踪ID频繁跳变检测框坐标不是原图坐标检查是否用了多尺度推理或letterbox后没还原坐标遮挡过后目标变成新IDmax_age太小提高到50同时确认外观特征缓存正常更新人物短暂离开画面再回来无法识别外观特征匹配太严格降低外观代价权重并在外观距离计算里加大余弦相似度触发阈值人多时检测框数量多帧率骤降每帧对全部检测框重复提特征只对新检测框提特征未匹配的轨迹用缓存还有一个特别容易忽略的问题很多开源项目在跟踪时直接用原视频帧做ReID输入但检测框的padding成分太多会把大量背景带进去。我建议在裁切检测框给ReID前向外扩充12%到18%的边框必要时将宽高比向256×128方向拉伸填充而不是简单压缩。实测这个细节能把ReID识别率提高3%到5%。7. 后续可以这样扩展YOLOv10结合StrongSORTOSNet这套组合我已经稳定跑了一段时间下一步有几个值得尝试的方向。一个是把OSNet换成更轻量的MobileNetV3骨架做蒸馏部署到边缘设备时能让帧率再翻一倍另一个是接入相机运动补偿模块的自动开关画面静止时自动跳过ECC计算进一步省算力还有就是在特征融合阶段加入方向预测对行人和车辆这种有明确运动方向的物体轨迹平滑度能再上一个台阶。我自己实际用下来的体会是跟踪系统永远是检测、运动预测、外观识别的三方博弈谁也别想一家独大。你要做的就是根据自己场景把三者的权重调到最舒服的位置。先跑通再细调最后再谈优化。本文还有配套的精品资源点击获取