☰
PPYOLO+DeepSORT:行人检测跟踪项目实战与踩坑总结
2026/10/19 16:31:35 网站建设 项目流程

做行人检测跟踪这个项目,前前后后折腾了将近两个月。当时选型的时候没有太纠结,检测端直接锁定了PaddlePaddle生态里的PPYOLO,跟踪端用了经典的DeepSORT。理由是现成的:软件杯赛题要求最终能在一个统一框架里跑通训练、推理和部署,PaddlePaddle从模型库到推理引擎的链路足够完整,PPYOLO在公开数据集上的表现不输同量级的其他检测器,预训练权重也方便;DeepSORT则是多目标跟踪里被大量项目验证过的成熟方案,把它和PPYOLO接在一起,思路清楚,出问题也好定位。这篇把项目从技术选型、训练调参到系统集成整个过程中的细节和踩过的坑都梳理一遍,给后面做“检测+跟踪”类任务的同学做个参考。

1. 项目思路与技术选型:为什么是PPYOLO + DeepSORT

1.1 检测和跟踪,为什么要分成两段

行人跟踪这个需求,看起来好像一个检测器逐帧画框就够了,但真的上手之后会发现,检测只能给出“这一帧里有哪些人”,解决不了“这个人是不是上一帧那个人”的问题。单独用检测器做跟踪,框会跳、ID会乱、一旦目标被柱子挡住几帧再出现,直接就当成新人处理了。

所以我把任务拆成了两条线:检测器负责单帧目标定位,跟踪器负责跨帧身份关联。检测器的输出是每一帧的边界框和置信度,跟踪器把这些框按时间序列关联起来,维护每个人的轨迹和ID。

这个拆法最大的好处是调试起来非常清晰:检测漏检了,问题在PPYOLO那一侧;ID频繁切换,问题在DeepSORT那一侧。如果做成端到端的联合模型,检测和跟踪互相耦合,赛程又紧,出了问题根本不知道先往哪里查。两段式虽然多了个中间层,但每一段都能独立验证,项目进度反而更可控。

1.2 PPYOLO相比其他检测器的优势

当时可选的目标检测模型不算少,YOLOv5、FCOS、EfficientDet都有自己的优点。但PPYOLO在比赛场景里有几个不可替代的优势。

第一是框架一致性。PaddlePaddle的训练、评估、推理链条是配套的,模型从训练到部署不用跨框架转换。我见过很多项目用其他框架训练,最后部署又要转成另一种格式,光算子对齐就能折腾好几天,这在竞赛赛程里非常吃亏。

第二是精度和速度的平衡。PPYOLO在COCO级别的评测上,mAP和推理速度的平衡做得相当好。尤其是行人这种非刚性目标,形变和遮挡很多,PPYOLO用的可变形卷积对这类场景比较友好,小目标召回率也更好看。

第三是上手门槛低。PaddleDetection里面预训练权重和配置文件都是现成的,把数据集路径和类别数改一改就能开始训练。对一支主要精力不在检测算法本身的队伍来说,这个“省心”属性太重要了。

当然PPYOLO也不是没有代价,它对PaddlePaddle的算子生态绑定比较深,如果非要导出成其他框架格式会折腾。但既然项目本来就用这套框架,这个约束等于不存在。

1.3 DeepSORT解决的核心问题

DeepSORT的前身是SORT,核心是卡尔曼滤波加匈牙利匹配。SORT的思路很简单:用卡尔曼滤波预测每条轨迹在当前帧的位置,然后把预测框和检测框的IOU作为相似度,跑一次匈牙利算法做最优匹配。这个方案速度快,但一旦目标运动轨迹突变,或者被遮挡一段时间,关联很容易断裂,ID切换率很高。

DeepSORT在SORT基础上加了外观特征这个关键组件。对每个检测框,用一个行人重识别网络提取特征向量,再与轨迹保存的历史特征做余弦距离匹配。最终匹配代价由运动距离和外观距离加权组合而成,并且设计了级联匹配机制,优先匹配刚更新过的轨迹,再处理长时间未更新的旧轨迹。

可以这样理解:运动模型告诉我们“这个目标大概在哪里”,外观模型告诉我们“这个目标是不是之前那个人”。两者配合之后,ID切换的问题比纯SORT好很多。这也是我在项目里没有选择其他更冷门跟踪算法的主要原因——DeepSORT的代码实现多、参数资料全,踩坑的时候能搜到很多现成的经验。

2. 核心原理拆解:PPYOLO的检测能力从哪来,DeepSORT怎么把框串起来

2.1 PPYOLO的结构与关键改进

PPYOLO的基本流程还是YOLOv3那套:把输入图片划分成网格,每个网格预测若干候选框,再做分类和边框回归。但在具体实现里,PPYOLO做了几处针对性很强的改进。

Backbone用的是ResNet50vd。相比普通ResNet,它在下采样和stem部分做了调整,更适合目标检测这种需要多尺度特征的任务。在backbone里还集成了可变形卷积,让卷积核的感受野能自适应目标形变。行人这种姿态多变的非刚性目标,用普通卷积很容易笼统地“看”成一个矩形,可变形卷积能把感受野贴合到行人轮廓上,特征提取质量明显更高。

Neck部分用CSPPAN替代了传统的特征金字塔。CSP结构的跨阶段部分连接能在减少计算量的同时保留更多梯度信息,让多尺度特征融合更充分。Head部分则引入了IOU Loss做边框回归,让模型优化的目标从简单的坐标差变成了预测框和真实框的重叠度,收敛更稳定。

还有一个容易被忽略的小改动是grid sensitiveness。YOLOv3在预测物体中心坐标时,对网格边界的落入位置比较敏感,PPYOLO对这个做了缓和处理,让回归值在网格边缘附近更平滑。这些改进单独看都不复杂,但叠在一起效果很显著。我们在自己的验证集上测过,相同分辨率下,PPYOLO mAP比普通YOLOv3明显高出一截,漏检率也低很多。

2.2 DeepSORT的状态估计和数据关联

DeepSORT里,每条轨迹用一个八维状态向量表示:

[u, v, s, r, dot_u, dot_v, dot_s]

其中u和v是目标中心的横纵坐标,s是包围框的面积,r是宽高比。前四个是当前观测状态,后三个带点是对应的速度信息。卡尔曼滤波假设目标做匀速运动,用上一帧的状态预测当前位置,再用检测框的测量值去修正预测结果。

数据关联是核心中的核心。DeepSORT用了两层相似度:

第一层是马氏距离,用来衡量预测状态和检测状态之间的匹配程度。马氏距离考虑了每个状态变量的方差,比单纯的欧式距离更适合表达观测不确定性。

第二层是外观特征余弦距离。每个轨迹会保存最近若干帧的ReID特征,组成一个特征池,新的检测框提取特征后,和特征池里所有特征计算余弦相似度,取最大值或平均值作为外观距离。

最终的总代价是两个距离的加权混合,然后通过匈牙利算法求全局最优匹配。级联匹配还有一条额外的规则:轨迹越久没有匹配到检测框,它在这次匹配中的优先级就越低。这样做的原因是,长时间未更新的轨迹预测误差会越来越大,如果允许它参与普通竞争,很容易抢走其他正常轨迹的目标。

理解了这套流程,就能解释实际场景里的很多现象。比如一个人被挡住五秒后重新出现,ID变了,大概率不是因为跟踪算法坏了,而是因为外观特征在低分辨率下区分度不够,或者级联匹配已经把旧轨迹的优先级降到了很低。

2.3 检测结果到跟踪器的衔接细节

检测和跟踪不是简单把框丢给DeepSORT就完事,项目里最容易翻车的恰恰是这个衔接层。

我第一次做联调的时候,把所有检测框直接送进跟踪器,结果画面里全是乱跳的误检轨迹。后来加了两个前置过滤才稳住局面。

第一个是置信度过滤。低于阈值的检测框直接丢弃,因为这些框通常来自背景误判、严重遮挡或模糊目标。置信度设多少要看你训练的检测器总体打分分布,我一般在0.25到0.4之间调。第二个是NMS的IOU阈值设置。PaddleDetection默认的NMS参数能用,但在行人密集的场景里,NMS的IOU阈值太高会把相邻的人合并成一个框,阈值太低则会产生大量指向同一目标的冗余框,反而干扰跟踪器的匹配。这个值需要在验证集上反复试,没有固定的“最佳值”。

还有一个细节是坐标系的统一。PPYOLO输出的是相对输入图像的坐标,而DeepSORT内部习惯使用绝对像素坐标。如果不统一,卡尔曼滤波会预测出负数坐标或超出画面范围的框,跟踪直接乱掉。我抽了专门时间写了一个坐标转换函数,把检测结果统一转成[x1, y1, x2, y2]的整数像素坐标,再喂给跟踪器,这一步对整个系统的稳定性贡献非常大。

3. 实操:环境准备、数据集与PPYOLO检测器训练

3.1 实验环境清单

我的实验环境大概是这样的:

  • 操作系统:Ubuntu 18.04,CUDA 10.2或11.2;
  • 显卡:6GB显存起步的中高端游戏卡;
  • PaddlePaddle版本:2.0以上GPU版;
  • 依赖库:OpenCV、NumPy、scikit-image,再加上PaddleDetection自带的工具链。

安装PaddlePaddle最烦人的是版本匹配问题。CUDA、cuDNN、PaddlePaddle三者必须对得上,否则训练时会出现各种底层报错。我一开始图省事装了CPU版,速度慢到完全没法训练,后来老老实实重装GPU版才正常。

PaddleDetection我强烈建议用源代码方式拉下来,而不是只pip安装。因为后面要改配置文件、看训练日志、自定义后处理,源码改起来更顺手。装完之后先别急着跑自己的数据,先在官方demo上跑通一次,确认整个环境没毛病,再开始自己的任务。

3.2 数据准备、标注与增强

行人检测的数据集有两块:检测训练集和ReID训练集。

检测部分优先用公开的行人检测数据,不用从零标注。我们先用公开数据训练出基线模型,再在自己的视频里抽帧补充少量难例做微调,效果比只用一个公共数据集好很多。PaddleDetection支持COCO和VOC两种标注格式。COCO的标注是JSON,每个annotation里有bbox和category_id;VOC是XML文件。需要注意类别编号是从0还是从1开始,PaddleDetection的配置里“num_classes”写不写背景位,一定要和数据集标注对应上,不然训练时loss会非常奇怪。

数据增强也很关键。PPYOLO配置文件里默认带了一些增强策略,比如随机翻转、随机缩放、色域扰动。训练时我额外开了Mosaic增强,它能把四张图拼成一张,相当于一次看到更多目标,对小目标检测帮助很大。代价是显存占用更高,batch size要相应缩小。

ReID数据同样重要。DeepSORT需要一个能区分不同行人的特征模型,通常是在一个包含大量行人ID的数据集上训练分类网络,然后取倒数第二层的输出作为特征向量。如果找不到合适的数据集,可以用PaddlePaddle里预训练的行人属性模型凑合,效果会差一些,但能让整个流程先跑起来。

3.3 训练PPYOLO的配置与命令

PaddleDetection里训练PPYOLO的核心是改yaml配置文件。我以ppyolo_r50vd_dcn_1x_coco.yml为例,几个关键修改如下。

  • 类别数:num_classes改成自己的类别数,检测行人时填1;
  • 数据集路径:设置TrainDataset和EvalDataset里的image_dir、anno_path、dataset_dir;
  • 训练轮数:默认是1x即12个epoch,数据量不大时可以把学习率调小,训练轮数增加到2x甚至3x;
  • batch size与学习率:batch size调小时,学习率也要等比降低,否则梯度更新不稳定。

启动命令大概长这样:

python tools/train.py -c configs/ppyolo/ppyolo_r50vd_dcn_1x_coco.yml \ -o use_gpu=true \ --eval

加--eval可以让每个epoch在验证集上跑一遍mAP,方便观察收敛情况。

训练的时候要盯着loss曲线。如果训练开始很久loss都不降,多半是数据加载出了问题,比如标注坐标越界、图片通道数不对。如果loss缓慢下降,说明模型正在正常收敛。我自己踩过的一个坑是:6GB显存直接跑完整PPYOLO,batch size只能开2,训练速度慢到让人崩溃。后来把输入分辨率从608降到416,batch size提到4,mAP只损失一点,但训练时间缩短了一大半。在竞赛场景下,这种交换非常划算。

3.4 训练效果评估

检测器训练完之后,不能只盯着mAP。行人跟踪场景里,召回率比精确率更值得关注,因为漏检会让跟踪轨迹直接断裂。当然召回率也不能拉满,否则大量误检框会把跟踪器带偏。我一般同时看mAP和F1分数,两个指标都要过线。

单帧检测指标和跟踪效果也不是线性关系。有一次我调出来的模型mAP很高,但跟踪结果一团糟,查了半天发现是小目标召回率不够,远处和半遮挡的行人经常检不出来。后来我额外统计了小目标召回率,才把问题定位清楚。

评估时最有效的办法是把预测结果可视化保存成图片或者视频,一帧一帧看。只看数字曲线,很难发现NMS阈值和置信度阈值的问题。我在训练后期养成了每个epoch结束都随机抽几个视频片段可视化的习惯,模型变化好不好,肉眼往往比单点指标更早看出来。

4. DeepSORT跟踪器的实现细节

4.1 特征提取模型的选择

DeepSORT中的ReID特征提取网络,要求是速度快、特征区分度强。我一开始试图直接用PPYOLO的backbone提取特征,后来发现检测任务训练出的特征对行人ID区分度不够,ID切换非常频繁。

换成行人重识别任务预训练的轻量网络后,情况明显改善。我用的是一个基于MobileNetV3结构的模型,输入图像裁剪成128x64,输出128维特征向量。这个模型虽然不算大,但在跟踪场景里足够用,速度也跟得上检测器。

PaddlePaddle加载预训练模型很简单:

import paddle from paddle.vision.models import mobilenet_v3_small model = mobilenet_v3_small(pretrained=True, num_classes=0) model.eval()

但要注意输入图像的预处理必须和训练时保持一致,尤其是归一化的均值和方差。如果预处理不对,特征空间的分布会整个偏移,外观匹配基本失效。

4.2 跟踪器核心参数与代码骨架

DeepSORT的参数看起来不多,但每个都会直接影响最终效果。我整理出最核心的几个:

  • max_dist:最大余弦距离阈值,超过则不匹配;
  • min_confidence:检测框的最低置信度;
  • nms_max_overlap:检测NMS的IOU阈值;
  • max_iou_distance:辅助IOU匹配的距离阈值;
  • max_age:轨迹丢失后的最大保留帧数;
  • n_init:轨迹被确认前必须连续匹配成功的帧数。

我初始配置是max_dist=0.2,min_confidence=0.3,max_age=30,n_init=3。这个组合在一般行人场景下表现稳定。

核心更新流程用伪代码描述大概是:

def update(detections): # 1. 预测所有轨迹在当前位置的状态 for track in confirmed_tracks: track.predict() # 2. 计算代价矩阵 cost_matrix = compute_cost(tracks, detections) # 3. 级联匹配,优先匹配最近更新的轨迹 matches, unmatched_trk, unmatched_det = cascade_match(...) # 4. 对未匹配的检测框,用IOU匹配补漏 matches, unmatched_trk, unmatched_det = iou_match(...) # 5. 更新匹配上的轨迹状态,管理未匹配的轨迹 # 6. 用未匹配的检测框创建新轨迹

这个骨架看起来不复杂,但每一个细节都可能出问题。卡尔曼滤波的误差协方差矩阵初始化不合适,预测位置会明显偏移;代价矩阵的归一化方式不对,匈牙利算法匹配结果也不对。所以我的建议是先跑通一版再一点点调,不要一上来就追求完美。

4.3 在线关联流程逐步走一遍

我用一个实际帧的例子来说明。假设这一帧有三个检测框,上一帧有两条已经确认的轨迹。

第一步,两条轨迹分别通过卡尔曼滤波预测在当前帧的位置,得到两个预测框。第二步,计算两条轨迹和三个检测框之间的两两距离,构成一个2x3的代价矩阵。第三步,用匈牙利算法求最优匹配,比如轨迹1匹配检测2,轨迹2匹配检测3。检测1没有匹配到任何轨迹,判定为新出现的目标,创建一个新轨迹。

第四步,匹配成功的轨迹用检测框更新卡尔曼状态,并把当前检测框的ReID特征加入到轨迹的特征池里。第五步,新轨迹先进入“未确认”状态,每连续匹配一帧就累计一次,直到连续匹配帧数达到n_init才变成确认轨迹,此时ID才稳定输出。

如果中途这个人被柱子挡住,检测框消失,这条轨迹就会进入保留状态,max_age逐帧递减。三帧之后重新出现,因为外观特征还保留着,大概率还能和旧轨迹对上。但如果超过max_age还没出现,轨迹被删除,人再次出现时就会被分配新的ID。

这个生死逻辑就是DeepSORT跟踪的核心。实际项目里的ID切换,大部分发生在长时间遮挡后的恢复阶段,很少发生在正常行走过程中。所以有一段时间我专门去优化遮挡场景下的轨迹保留策略,效果立竿见影。

5. 常见问题、调参经验与性能优化

5.1 高频问题排查速查表

把项目里反复出现的问题整理成一张表,后面调起参数来很快。

现象可能原因解决思路
检测框频繁抖动置信度阈值太低,误检框太多调高min_confidence,先单独看检测效果
ID在遮挡后切换ReID特征区分度不够换成更强的行人重识别模型,适当降低max_dist
跟踪速度很慢特征提取网络太大,或全分辨率推理缩小输入尺寸,开启半精度推理
轨迹被错误关联NMS阈值过大,行人被合并调小NMS的IOU阈值
视频开头漏掉目标n_init过高,新轨迹确认太慢适当降为2,但不能太低
检测框超出画面坐标未裁剪,卡尔曼预测不受边界约束强制将坐标clip到图像边界

这张表是在项目最后几天总结出来的。每次效果不理想,先按表里的方向定位,而不是拍脑袋乱调参。

5.2 参数调整心得

想特别说说max_dist。这个参数控制外观相似度的容忍度。设太小了,一个人稍微转个身或者分辨率低一点,特征余弦距离一超阈值,就会被当成新人;设太大,不同路人之间也可能匹配上。我在密集人群场景里最终取的是0.2到0.35,稀疏场景可以放宽到0.5。

max_age也不是越大越好。旧轨迹保留时间太长,卡尔曼预测框会漂移到离谱的位置,反而更容易和其他检测框错误匹配。我自己的经验是max_age设在20到40比较合理,配合级联匹配的低优先级,才能避免旧轨迹抢占新目标。

还有一个容易忽略的组合是n_init和min_confidence的配合。如果min_confidence设太高,半遮挡的行人检测不出来,新轨迹永远确认不了;设太低,误检框全被当成目标,轨迹全是碎片。我的做法是先单独跑检测器,用视频可视化确定一个合理置信度区间,再回去调跟踪参数。

5.3 性能优化三板斧

比赛现场演示最怕卡顿。我们的性能优化思路可以总结成三板斧。

第一,降低推理分辨率。行人跟踪不需要把1080p的原图全部塞进模型。把输入尺寸缩到720p甚至608x608,检测速度能提升将近一倍,精度损失完全在可接受范围内。

第二,使用推理引擎加速。训练完的模型先导出为静态图推理模型,再走Paddle Inference或者TensorRT优化,比直接调动态图训练模型快很多。如果GPU支持半精度,FP16也能带来明显加速,但要注意有些算子不一定支持,需要逐个benchmark。

第三,减少重复计算。ReID特征提取可以一次把所有检测框的裁剪图拼成一个batch,做一次前向推理,而不是每个框都调一次模型。检测框数量多的时候,这个优化能省下大量时间。

我们最终在演示机器上把输入分辨率控制在608,开启半精度后,FPS从最初的不到10提升到接近25,已经足够流畅。

6. 从模型到Demo:系统集成与交付细节

6.1 整体主循环与模块划分

整个系统的集成比想象中费时间。我的主循环框架大致是:

cap = cv2.VideoCapture(input_video) tracker = DeepSORT(...) detector = PPYOLOInference(...) while cap.isOpened(): ret, frame = cap.read() if not ret: break detections = detector.predict(frame) tracker.update(detections) results = tracker.get_bboxes_and_ids() visualized = draw(frame, results) writer.write(visualized)

模块划分清楚之后,每一步都能单独替换。想换检测器就只改动detector模块,想换跟踪策略就只改tracker模块。因为软件杯这种比赛后期需求变动多,模块化程度直接决定了修改效率。

我在集成时把检测结果转换、跟踪器参数、可视化逻辑都抽成了独立配置项,这样换视频、换场景时不用改代码,改配置文件就行。这一点强烈建议保留,后来评审老师现场提出要看另一个视频,我只需要改路径,几秒钟就能跑起来。

6.2 可视化与输出标准化

可视化看着简单,实际上有很多细节。框的颜色、ID的位置、轨迹线要不要画,都会影响评审观感。我给每个ID分配了一个固定颜色,同时画了最近几帧的历史轨迹线。这样即使画面里人很多,评委也能一眼看出某个人的运动轨迹。

输出标准化同样重要。竞赛通常要求提交特定格式的结果文件,比如每一帧的目标坐标和ID列表。我们专门写了一个输出模块,把跟踪结果写成了通用的文本格式,便于自动评分。这里有一个坑:视频帧率要和标注时间轴对齐,否则离线评估会错位,分数会难看。

另外一个容易被忽略的是log日志。集成阶段我加了非常详细的分级日志:每一帧检测到几个目标、多少轨迹被更新、多少被新建。排查问题的时候,有日志和没日志的效率差距是数量级的。

6.3 评审现场的经验

评审现场和实验室跑代码是完全两种场景。实验室里可以容忍模型加载慢、参数慢慢调,现场不行。

我们提前做了一次完整的彩排,把模型权重、配置文件、测试视频全部放在一个U盘里,确保拔掉外设也能离线跑。还准备了一份精简的启动脚本,一键启动Demo,不需要打开终端敲命令。这些细节看似琐碎,但现场一旦出问题,能救命的往往就是这些准备。

还有一条经验:演示视频不要整段放,挑一到两个有代表性的片段,最好包含遮挡、人群密集、目标进出的场景。这样既能展示检测效果,也能突出DeepSORT的跟踪能力,比放一整段平淡无奇的监控录像更有说服力。


最后说点个人体会。做这个项目最大的收获不是把PPYOLO和DeepSORT跑通,而是真正理解了检测和跟踪之间微妙的配合关系。PPYOLO给过来的检测质量,直接决定了DeepSORT的上限;但DeepSORT的参数设置,又反过来影响最终交付的效果。两段式方案最忌讳的就是把两端割裂开来看。如果你也想做类似的行人跟踪项目,建议一开始就盯着漏检率和ID切换这两个综合指标,不要过分追求单帧精度。还有一个实用的小技巧:所有参数调整都要对着可视化视频来看,数字指标再漂亮,不如自己的眼睛准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询