YOLOv5+DeepSORT+SlowFast:视频流实时动作检测与跟踪
2026/9/12 13:34:01 网站建设 项目流程

简介:这是一套基于Pytorch+YOLOv5+SlowFast的视频流实时动作检测项目源码,支持多目标跟踪检测,源自作者毕业设计(答辩98分),适合计算机、人工智能、自动化等专业学生及开发者用于课程设计、毕业设计或算法进阶。资源包共31个文件,压缩包7.21MB,核心为22个Python脚本,配合2个pbtxt配置文件、2个类别/标签txt、1个YAML配置及2个GIF演示动图,另有说明文档与readme,目录结构清晰。当前已有164人浏览学习,代码经调试测试可运行,适合新手跟着demo动图快速理解推理流程。项目整合了YOLOv5目标检测、SlowFast动作识别与DeepSort跟踪,包含ava动作标签、coco类别名等配套文件,可直接用于视频流实时检测实验;学习者可在此基础上调整模型和参数,拓展到行为分析、安全监控等场景。

1. 视频流里的动作检测,难在“同时”两个字

把一张静态图片里的人物框出来,YOLO 系检测器早就做到了;给一段短视频判断里面的人在“跑”还是“走”,SlowFast 这类动作识别模型也能完成。但把两者拼成一条对视频流实时处理的管线,麻烦立刻出现:检测器只在单帧上工作,动作识别却需要连续几十帧的时间上下文,而中间还要保证同一个人的框不会因为一两帧漏检就“消失”,否则动作识别的输入就断了。这个项目解决的就是这个衔接问题。它用 YOLOv5 做逐帧目标检测、DeepSORT 做跨帧身份关联、SlowFast 在滑动窗口里做动作分类,三个阶段各司其职,最终输出带动作标签的跟踪轨迹。对正在做毕设、课程设计或者刚接触视频理解方向的同学来说,这套代码把“检测—跟踪—识别”三段式管线完整串了起来,跑通它之后,换成自己的数据集和场景就有了清晰的抓手。

2. YOLOv5 做检测器:单帧定位的性能与精度取舍

先看检测层。YOLOv5 在整个管线里承担最基础的职责:每一帧画面里有哪些目标(主要是人),各自在什么位置。这个阶段只回答“在哪”,不回答“在做什么”,所以对检测器的要求首先是稳定——漏掉一帧,后面的跟踪和识别都会受影响。

2.1 为什么选用 YOLOv5 而不是更新的检测器

虽然 YOLOv8、RT-DETR 等新模型在精度上已有提升,但 YOLOv5 在这套项目里依然合理。一方面是 PyTorch 生态下的部署资料极其密集,换 COCO 预训练权重、调整 anchor 或做 TensorRT 加速都有大量现成案例;另一方面,v5 的模型结构在推理延迟上非常可控,s/m 系列在 GTX 1660 级别的显卡上就能跑到实时帧率。对于动作识别这种需要“持续不断喂帧”的上游模块,检测器占用推理时间越短,留给 SlowFast 的计算预算就越宽裕。

2.2 检测器核心推理代码的拆解

项目中yolo_slowfast.pyslowfast_detection.py分别对应两条运行路径,前者侧重完整流程,后者更贴近接口化调用。以常见的视频帧推理为例,YOLOv5 的调用方式如下:

import torch import cv2 # 加载预训练模型,num_classes 需与权重匹配 model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.conf = 0.4 # 检测置信度阈值 model.iou = 0.5 # NMS 的 IoU 阈值 model.classes = [0] # 只保留 person 类别(COCO 中类别 0 为 person) cap = cv2.VideoCapture('demo/input.mp4') while cap.isOpened(): ret, frame = cap.read() if not ret: break # 返回的 results 对象内含检测框、置信度、类别 results = model(frame, size=640) # 提取 [x1, y1, x2, y2, conf, class] 格式的数组 dets = results.xyxy[0].cpu().numpy() # 只保留 person 目标交给后续跟踪器 person_boxes = dets[dets[:, 5] == 0] for box in person_boxes: x1, y1, x2, y2, conf = box[:5] cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.imshow('detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这段代码里有两个参数直接影响后续管线的质量。conf=0.4是置信度阈值,调低会召回更多漏检目标,但也会把背景误检当作人传入跟踪器;调高则减少误检,但跟踪轨迹可能频繁中断。classes=[0]限定了只检测 person,这是因为动作识别模型训练时只面对人的行为类别,把其他物体框交给 SlowFast 没有意义。imgsz=640是推理分辨率,输入尺寸越大,小目标检出越好,但推理耗时线性上升。我的经验是,720p 视频流用 640 足够,1080p 且人员密集时可以试 960,前提是 GPU 显存和延迟预算许可。

2.3 检测器输出归一化与跟踪器的衔接

从 YOLOv5 拿到的边界框坐标是像素绝对值,如果送入跟踪器前不做任何处理,当输入视频分辨率变化时,跟踪器的距离度量会不一致。常见做法是在交给 DeepSORT 之前,把框坐标归一化到[0, 1]区间,或者统一缩放到固定尺寸。项目源码中selfutils目录下的工具函数就承担了这类转换工作,使用顺序一般为:检测器原始框 -> 坐标格式对齐 -> 送入 DeepSORT update()。这一步看起来很琐碎,但确实是最容易出 bug 的地方——YOLOv5 输出的中心坐标形式与 DeepSORT 需要的(x1, y1, x2, y2)不一致时,跟踪器会直接报错或产生无意义的匹配结果。

3. DeepSORT 多目标跟踪:把断帧拼成连续轨迹

检测器给出的是每一帧独立的框,没有任何帧间关联。如果直接对每个框单独做动作识别,那么目标一旦暂时被遮挡或检测丢失,身份就变了。多目标跟踪就是解决这个问题的中间层,DeepSORT 在这里负责把同一目标在不同帧的检测框串联成一条轨迹,并为每条轨迹分配稳定的 ID。

3.1 级联匹配策略与代价矩阵

DeepSORT 的核心是匈牙利算法加级联匹配。它不直接对全部轨迹和全部检测做全局匹配,而是优先匹配“最近被更新过”的轨迹,再逐步放宽条件去匹配较久未更新的轨迹。这么做是为了降低 ID Switch 的频次。匹配时的代价由两方面组成:一是马氏距离,度量检测框和轨迹预测位置的运动一致性;二是外观特征的余弦距离,由一个小型 ReID 网络提取的外观向量计算。最终的代价矩阵是两者的加权和:

代价矩阵 = 外观距离权重 × 余弦距离 + 运动距离权重 × 马氏距离

外观距离的权重通常更高,因为行人外观在短时间段内变化不大,而运动模型在摄像头抖动或目标突然转向时会明显失真。deep_sort/configs目录下的配置文件里就包含了这些权重和阈值,建议按实际视频场景调,而不是照搬默认值。

3.2 轨迹生命周期管理的关键状态

DeepSORT 里的每一条轨迹都经历三个阶段:tentative(不确定)、confirmed(确认)、deleted(删除)。新检测框先产生 tentative 轨迹,只有连续若干帧都能被匹配到才升级为 confirmed;反之,一条 confirmed 轨迹如果连续多帧没有匹配到任何检测框,会进入missed计数,超过max_age之后被删除。理解这个生命周期对调优很重要,因为动作识别只处理 confirmed 轨迹的框,误判阶段(tentative)的框往往不稳定,位置跳动大,送进 SlowFast 会引入噪声。

下面是一段典型的 DeepSORT 状态更新逻辑,代码结构在项目deep_sort目录中对应实现:

# 以 tracker.update 为核心,模拟一帧的处理流程 from deep_sort import DeepSort # 构造跟踪器,max_age 控制轨迹丢失后保留的最大帧数 deepsort = DeepSort( model_path='deep_sort/checkpoint/ckpt.t7', # ReID 特征提取权重 max_dist=0.2, # 特征最大余弦距离,超过则拒绝匹配 max_age=30, # 轨迹连续 30 帧无匹配则删除 n_init=3, # 连续 3 帧匹配才确认轨迹 nn_budget=100 # 外观特征库容量,超限则淘汰旧特征 ) # 每帧得到检测框后调用 # bboxes: [x1, y1, x2, y2] 格式的二维数组 # confs: 每个框的检测置信度 outputs = deepsort.update(bboxes, confs, frame) # outputs 中每个元素为 [x1, y1, x2, y2, track_id]

max_age是本项目中最值得调整的参数之一,它直接影响动作识别的稳定性。max_age太小时,短暂遮挡就会导致轨迹删除,重新生成的轨迹会拿到新 ID,动作识别就必须重新积累帧;max_age太大时,目标离开画面很久后轨迹仍残留,可能与新进入画面的目标错误关联。在广场、地铁站这类遮挡频繁的场景,我一般会把max_age设置在 45 到 60 之间。

3.3 与检测器之间的关键耦合点:置信度传递

检测器输出的置信度不只是用于画框的,它在 DeepSORT 中同样参与匹配。项目源码里通常会对置信度低的检测框做一个过滤——常见的做法是低于阈值的框直接丢弃,或者保留但降低其参与匹配的优先级。我在使用时会以conf = 0.4为界,低于这个值的框直接不送入跟踪器,因为低置信度框大概率是误检或严重遮挡的局部框,强行送入反而会污染轨迹的外观特征库。这样做的副作用是漏检变多,所以max_age就得相应调大,两者需要联动而非单独调优。

4. SlowFast 动作识别:理解“在做什么”的时间网络

有了稳定的跟踪轨迹,最后一个核心模块就是 SlowFast。与图像分类不同,动作识别必须同时利用空间信息和时间动态。SlowFast 的设计思路非常直接:用两条分支分别处理慢速、高分辨率的语义信息和快速、低分辨率的运动信息,再通过侧向连接融合。

4.1 双路径结构的直观理解与选型理由

Slow 路径以较低的帧率采样,比如每秒 4 帧,通道数较多,负责捕捉目标的静态外观和场景语义;Fast 路径以较高的帧率采样,比如每秒 32 帧,通道数较少(通常是 Slow 的 1/8),负责捕捉幅度较大的快速运动。Fast 路径不处理颜色信息,输入是灰度图甚至仅仅是帧差,计算量更小。侧向连接把 Fast 路径的运动特征送入 Slow 路径,让 Slow 路径感知“这个人动得快还是慢”。这个设计契合了人类视觉系统的双通道假说,在 AVA、Kinetics 等数据集上表现非常稳定。

在本项目中需要识别的是“走、跑、挥手、蹲下”这一级别的动作,SlowFast 的预训练权重已经覆盖常见类别。若想识别自定义动作,需要收集视频片段裁剪并微调,这一步推荐使用 UCF101 或自定义数据集先热热身——这也是网上“ucf101数据集实战:如何用pytorch提升视频动作分类准确率”这类教程的价值所在,训练流程与项目内的微调逻辑可以直接复用。

4.2 把轨迹盒变成 SlowFast 输入的关键操作

跟踪器输出的是每一帧的二维框,而 SlowFast 要求输入是多帧的连续视频片段。项目中的基本做法是:以当前帧为基准,维护一个长度为clip_len的队列,队列内存储同一track_id在不同帧对应位置的图像块。队列攒够帧数后,组装成一个 5 维张量[B, C, T, H, W]送入模型。这段数据流水线的代码逻辑如下:

import torch import torchvision.transforms as T # SlowFast 常用的输入预处理 mean = [0.45, 0.45, 0.45] std = [0.225, 0.225, 0.225] transform = T.Compose([ T.ToTensor(), T.Normalize(mean, std) ]) def build_clip_tensor(track_patch_queue): """ track_patch_queue: deque 存储同一 track_id 最近 N 帧的裁剪图 返回形状为 [1, 3, T, H, W] 的张量 """ T = len(track_patch_queue) # 时间维长度 H = track_patch_queue[0].shape[0] # 裁剪图高度 W = track_patch_queue[0].shape[1] # 裁剪图宽度 clip = torch.zeros(1, 3, T, H, W) for t in range(T): frame = track_patch_queue[t] tensor = transform(frame) # [3, H, W] clip[0, :, t, :, :] = tensor return clip # SlowFast 前向推理 # model 为 SlowFast 网络实例 with torch.no_grad(): # 模型输出维度为 [1, num_classes],对应每帧动作类别 preds = model(clip) action_id = torch.argmax(preds, dim=1).item()

这段代码中有几个要点容易出错。裁剪图必须按同一track_id的框坐标从原帧中截取,且所有帧的裁剪尺寸要保持一致——通常把检测框缩放为正方形,推荐224x224,因为 SlowFast 预训练权重输入就是 224。队列长度建议取 32 的倍数,SlowFast 的 Backbone 包含时域下采样,输入帧数不是 4 的倍数时会报维度错误。如果帧率是 30 FPS,32 帧约等于 1 秒的动作片段,识别“走路”这类周期性动作完全够用。

4.3 SlowFast 关键超参数速查

项目配置和模型定义中涉及以下超参数,调参时优先关注标粗的部分:

参数项目中的常见取值作用与调整依据
alpha4Fast 路径相对 Slow 路径的帧率倍率,调大增强运动感知,但增大计算量
tau8Slow 路径的采样间隔,帧率低时调小以保留足够帧数
beta8Fast 路径通道数与 Slow 路径通道数的比值,一般固定为 1/8
clip_len32输入片段总帧数,取决于动作持续时长,动作剧烈时可用 16 减少延迟
stride2滑动窗口的时间步长,步长越小识别越平滑,但推理次数增加
num_classes80动作类别数,与ava_action_list.pbtxtcoco_names.txt对应

ava_action_list.pbtxt文件是类别名称与 ID 的映射表,当模型预测结果与预期动作对不上时,首先检查这个文件中类别的排列顺序是否与训练时一致。这是一个非常隐蔽但后果很严重的问题——不同版本的 AVA 预训练模型类别顺序不完全相同,直接替换权重而不同时更新类别文件,会导致“跑”被识别成“挥手”之类的荒谬输出。

4.4 与检测器级联时的真实性能瓶颈

SlowFast 的推理耗时远高于 YOLOv5 检测器,这在实时系统中是绕不开的瓶颈。以输入clip_len=32、分辨率224x224为例,在 RTX 3060 上单条轨迹的单次推理约为 30 至 60 毫秒。如果画面里有 5 条轨迹,而每条轨迹都有独立的滑动窗口,那么一帧的总推理时间会膨胀到数百毫秒。项目代码中通常会做两个优化:一是仅对confirmed状态的轨迹进行推理,二是共享同一个 SlowFast 推理批。若你的场景中并发目标很多,建议把多个轨迹的片段拼成一个 batch 一次性前向传播,GPU 利用率会显着提升,这也是本项目中slowfast_detection.pyyolo_slowfast.py更追求工程化的原因所在。

5. 整条管线组装与实测调优技巧

把三块技术串起来之后,真正决定项目能否稳定跑完一场视频的,是帧率匹配、队列管理和验证方法。

5.1 帧率适配:让三套模型各跑各的节奏

YOLOv5 检测器理论上可以做到 30 FPS 实时推理,但 SlowFast 由于滑动窗口的存在,每个 snippet 内部存在大量重复计算——相邻两个窗口之间有 30 帧重叠时,重叠部分被反复前向传播了好几次。最直接的优化是让三套模型不要以相同帧率运行:检测器每帧都跑,DeepSORT 每帧都更新,但 SlowFast 每隔 4 帧才触发一次新片段推理。中间间隔的帧直接沿用前一个窗口的动作预测结果,这是因为人类动作在 100 毫秒级的时间尺度上通常不会发生类别跳变。

# 帧率解耦伪代码:识别器降频运行 frame_count = 0 action_results = {} while cap.isOpened(): ret, frame = cap.read() frame_count += 1 # 1. YOLOv5 每帧检测 dets = yolo_detect(frame) # 2. DeepSORT 每帧更新轨迹 tracks = deepsort.update(dets, frame) # 3. SlowFast 每隔 4 帧才刷新一次动作结果 if frame_count % 4 == 0: for track_id, track in tracks.items(): snippet = assemble_snippet(track.patch_queue) action_results[track_id] = slowfast_infer(snippet) # 4. 当前帧直接复用最近一次结果 for track_id, action in action_results.items(): if track_id in tracks: draw_action(frame, tracks[track_id].box, action)

代码中frame_count % 4 == 0就是一个典型的时间分片策略。在 30 FPS 视频中,动作结果每 4 帧刷新一次,即每 133 毫秒更新一次,在人的视觉感知中依然流畅;而 SlowFast 的实际推理次数减少到原来的 1/4。若想进一步降低延迟,可以把窗口重叠率从 0.5 降到 0.75——也就是频繁触发推理,但不建议低于 0.5,否则识别结果抖动会非常明显。

5.2 用torch.cuda.synchronize()量出真实耗时

排查性能瓶颈时,直接使用时间戳测量的结果往往不可靠。PyTorch 的 CUDA 算子默认是异步的,普通的time.time()测量得到的只是 CPU 提交任务的时间,GPU 实际计算还在后面排队。正确的测速方式是在每次前向传播后显式调用同步函数:

import time # 前面代码忽略,这里只展示计时方法 model = model.cuda().eval() frame = frame.cuda() for _ in range(10): # 预热 GPU 算子 with torch.no_grad(): _ = model(frame) torch.cuda.synchronize() t0 = time.time() with torch.no_grad(): _ = model(frame) torch.cuda.synchronize() t1 = time.time() print(f"单次推理耗时: {(t1 - t0) * 1000:.2f} ms")

注意torch.cuda.synchronize()必须在time.time()之前和之后各调用一次,否则统计区间内可能混入 GPU 处理上一帧遗留的任务。若耗时远高于预期,优先排查是否误用了 CPU 推理,或者 PyTorch 与 CUDA 版本不匹配导致算子回退到 CPU 实现。

5.3 验证方法:不要只看 GIF 演示

项目附带的demo目录里有两张动图作为演示,但那是别人跑通的结果,自己修改代码后必须构造可重复的验证步骤。先准备一段自己录制的视频,画面中包含单个人做两种以上动作,例如走几步然后挥手。运行项目后检查三点:第一,跟踪 ID 在动作切换时是否保持不变;第二,动作标签的切换是否与画面动作时间点一致;第三,按q键退出时程序是否能正常释放摄像头或视频资源——很多毕设答辩现场演示出现卡死,都是因为最后忘了cap.release()或者 GPU 显存没有释放。把这三项逐条确认后,再把自己的视频片段换成 UCF101 里的标准数据集来测试模型对不同尺度、不同光照的鲁棒性,数据集中部分类别与预训练权重覆盖的类别高度重合,很适合验证阶段做基准比对。

本文还有配套的精品资源,点击获取

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

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

立即咨询