基于pytorchVideo的行人实时动作识别实践:从模型选型到性能优化
2026/9/21 1:42:27 网站建设 项目流程

简介:这是一套基于PyTorchVideo的行人实时动作识别完整工程,主要面向计算机视觉与深度学习方向的学生、研究人员及开发者,适用于智能监控、行人安全预警、人机交互等需要实时理解人体动作的场景。资源以YOLOv5完成行人的实时检测与定位,以SlowFast双流网络提取视频中的时空动作特征,并使用DeepSort多目标追踪算法保持检测目标在时序上的连续性,配合PyTorchVideo库高效完成视频的读取、预处理与推理,构成一个端到端的可运行识别系统。压缩包共47个文件,整体约184.14MB。其中22个Python源码文件覆盖检测、追踪、推理与可视化逻辑,14个pyc编译模块便于直接调用,另有pbtxt配置文件、YOLOv5权重pt、DeepSort模型t7、标签txt及yaml配置等,并附带1个mp4演示视频和1个gif效果展示,目录结构清晰,适合直接对照学习和二次开发。已有407人学习浏览,读者可据此快速掌握PyTorchVideo与YOLOv5、SlowFast、DeepSort的集成思路,理解行人动作识别从模型搭建到实际部署的完整链路。 做行人实时动作识别这个项目,刚接到需求的时候,客户只给我提了一句话:“监控画面里能不能自动判断有人奔跑、挥手、倒地”。听起来是个很普通的AI需求,真正动手才发现,动作识别本身只是最上层的一小块,真正磨人的是视频流处理、检测跟踪、模型推理速度和误报控制这一整条链路。我在对比了几个开源库之后,把主线定在了pytorchVideo上,前后折腾了大概三周,最终在RTX 3060上把单路1080P视频的端到端处理跑到了30FPS以上,行人级动作识别的Top-1准确率在自建测试集上到了87%左右。这篇文章把整个选型、实现、优化的过程完整复盘一遍,重点讲清楚每一步为什么这么做,以及那些不跑一遍根本发现不了的坑。

1. 为什么我选pytorchVideo来做“行人动作”识别

1.1 这个库在项目里的真实定位

先给还不熟悉的朋友一句话介绍:pytorchVideo是Facebook AI开源的视频理解库,跑在PyTorch生态里,里面实现了SlowFast、X3D、C2D、R3D这些主流视频分类模型,也带训练、验证、推理的完整管线。它不是那种“开箱即用”的应用级工具,更像是一个模型仓库加数据管线工具箱,核心价值是把视频模型里常用的时序采样、视频变换、模型组装这些模块做成标准件。

当时摆在我面前的有三条路:用MMAction2、用pytorchVideo、或者自己写一套模型训练和推理代码。MMAction2确实功能全,但它依赖MMCV那套全家桶,版本对齐经常让人头疼,在边缘设备上部署也不是它的强项。自己写的成本太高,光是把SlowFast这类模型的时序结构复现对,就得花不少精力对比论文。pytorchVideo卡在两者之间:模型实现直接、依赖轻量、和PyTorch本身衔接顺畅,工程上改起来也方便。我不需要训练它内置的全部分类器,只需要把预训练模型拿过来做迁移学习,再自己接解码和检测管线,这个库的定位刚刚好。

1.2 动作识别真正的难点不在模型,在整条链路

很多人第一次接触动作识别,会以为这就是“把一段视频扔进神经网络,出来一个动作类别”的事。实际上单帧图像分类已经很成熟了,动作识别的核心增量是时序建模:网络要回答的不只是“这一帧画面里有人”,而是“这几帧之间这个人经历了什么运动变化”。奔跑和快走、挥手和招手,单帧画面几乎无法区分,必须靠一段时间内的姿态和位移变化来判断。

但真实监控场景里,问题远不止时序建模。摄像头画面里通常有多个行人,每个人在画面里占的面积不大,光照、遮挡、摄像头抖动、压缩噪声都在干扰模型。如果直接把整个画面喂给动作识别模型,模型会把背景运动也学进去,行人一多就乱套。所以完整的链路应该是:先定位行人,再裁剪出每个行人的局部画面,按时间顺序累积成clip,再交给动作识别模型分类。模型只是这条链路的最后一环,前端的视频解码、行人检测、目标跟踪、时序缓冲,任何一个环节掉链子,最后识别结果都会崩。

1.3 目标场景与技术指标

我做的这个项目面向的是园区周界、门店出入口这类相对固定的监控场景,摄像头用普通IPC,1080P分辨率25FPS,大多是侧面视角。要识别的动作定义得比较克制:站立、行走、奔跑、举手、挥手、倒地,一共6类。把“动作”限定在这个范围,不是偷懒,而是真实需求就是这些——太细的动作在低分辨率、动态模糊下根本做不准,硬撑只会把误报率拉高。

技术指标也是在项目一开始就跟客户对齐的:端到端处理延迟控制在500毫秒以内,连续视频流处理要达到实时(至少25FPS),自建测试集上的平均识别准确率不低于85%,同时误报要尽量低。这几个指标互相打架:要准确就要大模型、长clip,要实时就要轻量化、短clip。后面所有的选型,本质上都是在这些约束里找平衡。

2. 环境安装和模型选型:从SlowFast切到X3D的判断过程

2.1 装pytorchVideo前先想清楚CUDA、Python和PyTorch版本

先泼一盆冷水:pytorchVideo这个库最近几年维护频率不高,PyTorch的新版本和它之间偶尔会有兼容问题。我建议装的时候不要盲目追最新,而是先固定一套经过验证的版本组合。我用了Python 3.8、PyTorch 1.13.1、CUDA 11.7这套组合,跑下来最稳。

conda create -n ptv python=3.8 conda activate ptv pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install "pytorchvideo>=0.1.5" pip install av # 后面视频解码要用

这里有个特别容易翻车的地方:直接pip install pytorchvideo大概率会给你拉一个很新的PyTorch版本,和你机器上的CUDA驱动不一定匹配。所以顺序很重要,先把PyTorch装好,再装pytorchvideo,这样pip解析依赖的时候不会把PyTorch版本给你顶掉。如果发现装完import pytorchvideo报版本冲突,直接去GitHub拉源码用pip install -e .装,反而更省事。

2.2 SlowFast和X3D的实测对比

pytorchVideo里最出名的模型是SlowFast,SlowFast在行为识别准确率上确实高,但它设计上就是“高精度高代价”的路线:一条慢路径抽空间特征,一条快路径抽运动特征,两个分支还要做侧向连接融合。在当时的硬件条件(一块RTX 3060,还要同时跑检测和跟踪)下,用SlowFast做单路实时流水线太奢侈了。

我也不是一开始就放弃了,先拿SlowFast R50做了个快速验证,单clip推理在RTX 3060上要15毫秒左右,看着不慢,但这只是模型推理时间。整条流水线里还有解码、检测、跟踪、预处理,叠在一起帧率立刻掉到十几FPS。后面把目光转到X3D系列,发现它在精度和速度上明显更平衡。

模型Kinetics-400 Top-1输入尺寸显存占用单clip推理耗时(FP32)
SlowFast R5077.0%8x256约4GB约15ms
SlowFast R10178.7%8x256约8GB约30ms
X3D-M76.0%8x256约2GB约3ms
X3D-L77.5%8x256约3.5GB约6ms

X3D-M和SlowFast R50在Kinetics-400上差距只有1个点左右,但推理速度快了5倍,显存还少一半。实时性在这个项目里是硬指标,所以我最终选了X3D-M作为主干模型。这里也给大家一个经验:选模型不要只看公开数据集准确率,一定要结合你的输入分辨率和硬件条件做实测,很多差距在真实场景里会被放大。

2.3 预训练模型的加载与最后一层换掉

pytorchVideo提供了非常方便的加载预训练模型的方式,直接用torch.hub就能拉:

import torch model = torch.hub.load("facebookresearch/pytorchvideo", "x3d_m", pretrained=True) # 冻结主干参数,只训练分类头 for param in model.parameters(): param.requires_grad = False num_features = model.blocks[-1].proj.in_features model.blocks[-1].proj = torch.nn.Linear(num_features, 6)

这里有个细节要注意:pytorchVideo里X3D的最后一层分类头叫blocks[-1].proj,不是像ResNet那样叫fc。我第一次用的时候惯性思维去找model.fc,结果报错报得一头雾水。另外,torch.hub拉权重需要联网,内网部署的时候建议先手动把权重下载好,放到本地路径,再通过model.load_state_dict()加载,不然到了现场才发现拉不下来就尴尬了。

3. 视频流处理:把IPC画面变成模型输入张量

3.1 clip长度、帧采样步长与实时性的关系

动作识别模型的输入不是一个单帧,而是一个clip,也就是一段连续帧。X3D-M默认输入是8帧或16帧,这里有个取舍:帧数越多,时间信息越丰富,但计算量线性增长,而且连续两帧高度相似,信息冗余严重。我实测发现,在“奔跑、举手、倒地”这类动作上,8帧已经够用,16帧带来的准确率提升不到1个百分点,但推理时间几乎翻倍。

真正的技巧在帧采样步长。直接取相邻8帧,模型只能看到0.3秒内的运动,奔跑这种动作根本表现不出来。我的做法是每隔4帧取一帧,也就是stride=4,这样8帧覆盖了大约32个原始帧,在25FPS的视频里就是1.28秒的时间窗口。这个窗口长度基本能覆盖一个完整动作的关键阶段。滑动窗口每次前进4帧(约160毫秒)做一次推理,既能保持实时性,又能让检测结果有足够的更新频率。

3.2 PyAV替代OpenCV做解码

视频解码是整个流水线的第一个瓶颈。IPC的RTSP流里的编码帧是压缩过的,解码要消耗不少CPU。多数人下意识用OpenCV的cv2.VideoCapture来读,这个方法在本地文件上没问题,但在网络流、高分辨率、长连接场景下有三个毛病:一是部分IPC的编码格式支持得不好,B帧多的视频会出花屏;二是它内部是同步阻塞的,一旦网络抖动,整条流水线就卡住;三是多路视频场景下它是一种“一个线程一路流”的笨办法,很难统筹资源。

我换成了PyAV,它本质上是FFmpeg的Python绑定,解码能力和格式兼容性比OpenCV强很多:

import av container = av.open("rtsp://your_ipc_stream", options={"rtsp_transport": "tcp"}) container.streams.video[0].thread_type = "AUTO" for frame in container.decode(video=0): img = frame.to_ndarray(format="bgr24") # 这里把img交给后面的检测模块

这里有个关键选项必须强调:thread_type = "AUTO"。默认情况下PyAV是单线程解码,遇到高分辨率H.265视频CPU占用会拉满,帧率还上不去。设成AUTO之后FFmpeg会自动开多线程解码,同一路1080P视频的CPU占用能降30%以上。另外RTSP传输尽量用TCP,UDP虽然延迟低一些,但丢包环境下会出现花屏和卡顿,TCP牺牲的那点延迟对于动作识别来说完全无所谓。

3.3 预处理里最容易踩的归一化坑

视频帧从解码器出来之后,要经过一系列变换才能塞进模型。pytorchVideo用了一套组合变换API,刚开始看会觉得绕,但用顺了之后觉得很合理,它保证了所有变换都作用在“视频”这个维度上:

from torchvision.transforms import Compose, Resize, CenterCrop from pytorchvideo.transforms import ( ApplyTransformToKey, Normalize, UniformTemporalSubsample, Div255, ) transform = ApplyTransformToKey( key="video", transform=Compose([ UniformTemporalSubsample(num_samples=8), Div255(), Normalize((0.45, 0.45, 0.45), (0.225, 0.225, 0.225)), Resize((256, 256)), CenterCrop((224, 224)), ]), )

这几个变换的顺序和参数都是坑。第一,UniformTemporalSubsample要在最前面,它负责把任意长度的视频均匀抽样成8帧,注意是“均匀抽样”不是“取前8帧”,这样才不挑视频长度。第二,Normalize的均值和标准差是Kinetics数据集的标准值,如果直接拿ImageNet的归一化参数来用,模型输出会明显退化,这一点特别隐蔽。第三,ResizeCenterCrop要分开写,不要图省事直接Resize((224,224)),那样图像会变形,X3D这种3D卷积对空间形变很敏感,压缩到非等比尺寸会让精度掉两个点左右。

4. 检测+识别双流管线:行人框、跟踪器、时序状态机怎么配合

4.1 先检测后识别,而不是端到端

刚开始我也考虑过走端到端的视频动作检测方案,模型直接输出“在哪个位置发生了什么动作”,听起来很美好,但现实很骨感:这类方案的数据标注成本极高,需要在视频的每一帧标注空间位置和时间范围,自建数据集根本供不起。而且监控场景下行人小、遮挡多,端到端模型在这种条件下很难收敛好。

我最后采用的是先检测后识别的两段式管线:先用YOLOX或YOLOv5把每一帧的行人框检测出来,把行人区域裁剪出来并缩放到固定尺寸,再从每个行人的历史帧里攒出8帧clip,最后交给X3D分类。两段式的优点在于每一个阶段都能独立调优和替换。检测模型可以直接用现成的预训练权重,动作识别模型只负责时序分类,两者解耦,出问题的时候定位也快。代价是多一次模型调用,但通过后面的优化完全可以把这部分时间吃回来。

4.2 跟踪器在这里不是可选项

我一开始没上跟踪器,天真地以为“检测到人然后把历史帧喂给模型就行”。实际一跑就发现问题:检测器偶尔漏检一帧,行人的历史帧序列就断了,重新拼接出来的clip从时间上跳了一大截,动作语义直接被破坏。还有两个行人靠得近的时候,前后帧的检测框对不上号,张冠李戴,把A的帧和B的帧拼在一起,模型自然出各种离谱结果。

解决这个问题的是目标跟踪器。我在YOLOv5的检测结果后面接了一个ByteTrack,让每个检测框带上稳定的跟踪ID。有了ID之后,我为每个ID维护一个固定长度的帧队列,每次推理只从同一个ID的队列里取8帧,干净利落。ByteTrack最吸引我的地方是它对低置信度框的利用方式,行人被遮挡导致检测置信度骤降时,它还能尽量维持轨迹不丢,这对连续性要求高的人体动作识别来说是刚需。

4.3 时序平滑和状态机:压制单帧误判

即便有检测和跟踪兜底,X3D分类结果还是会有明显抖动。比如“行走”动作偶尔会连续几帧被识别成“奔跑”,“站立”偶尔会被识别成“举手”,原因可能是画面压缩噪声、行人姿态恰好处于动作临界态。直接展示原始分类结果,告警就会频繁误触发,客户一秒都忍不了。

我的做法是在分类结果外面包一层时序过滤状态机。每个跟踪ID维护一个状态,状态只在“连续N帧都是同一动作且置信度超过阈值”时才切换:

class ActionState: def __init__(self, confirm_frames=3, release_frames=10, score_threshold=0.6): self.confirm_frames = confirm_frames self.release_frames = release_frames self.score_threshold = score_threshold self.state = "unknown" self.history = [] def update(self, action, score): self.history.append((action, score)) if len(self.history) > 20: self.history.pop(0) last_actions = [a for a, s in self.history[-self.confirm_frames:]] if self.state == "unknown": if all(a == action and s >= self.score_threshold for a, s in self.history[-self.confirm_frames:]): self.state = action else: last_scores = [s for a, s in self.history[-self.release_frames:]] if len(last_scores) == self.release_frames and max(last_scores) < self.score_threshold: self.state = "unknown" return self.state

这个状态机的参数是我反复试出来的:确认帧数设3帧,释放帧数设10帧。3帧确认能拦住绝大多数的单帧误判,10帧释放则保证了动作真实结束时能及时回到unknown状态,不会让告警“粘住”不消失。这套机制加完之后,我统计到的误报数量大概下降了70%。

5. 实时性优化:从能跑到跑得快

5.1 优化的顺序是输入、模型、工程

整个流水线初步跑通之后,单路视频端到端帧率只有12FPS左右,离25FPS的目标差得远。我优化的时候给自己定了一个原则:先砍输入,再换模型,最后动工程。这个顺序很重要,因为它决定了优化的性价比。

先砍输入,指的是把送入模型的图像尺寸从256降到224,同时把检测模型输入从1080P降到640分辨率。对1080P画面来说,场景里行人的面积不大,但检测器用640分辨率已经能稳定框住,没必要在全分辨率上算。这一步做完,检测耗时直接砍半,帧率从12提到17FPS。接着把X3D-M的推理切成FP16精度,在PyTorch里开着autocast跑:

with torch.cuda.amp.autocast(): logits = model(clip.to("cuda"))

FP16对X3D这种结构来说精度损失很小,但推理速度能再快大概30%。这一步做完帧率到了22FPS左右,最后靠下面的工程优化才真正摸到30FPS。

5.2 ONNX导出与TensorRT FP16的算子坑

为了进一步压榨性能,我把X3D模型导出成了ONNX,再用TensorRT转成FP16引擎。导出过程没少折腾:

import torch model.eval() dummy_input = torch.randn(1, 3, 8, 224, 224, device="cuda") torch.onnx.export( model, dummy_input, "x3d_m.onnx", opset_version=17, input_names=["video"], output_names=["logits"], dynamic_axes={"video": {0: "batch"}, "logits": {0: "batch"}}, )

这里有两个坑必须提醒。第一个是opset_version,X3D的某些模块用到了较新的算子,opset低于17会直接导出失败。第二个是dynamic_axes,如果你确定推理时batch大小固定,最好不要开动态轴,固定形状能让TensorRT优化得更彻底。TensorRT转换的时候用trtexec --onnx=x3d_m.onnx --fp16 --saveEngine=x3d_m.trt,跑一次就能看到明显的加速。转换完之后记得做一个精度对比,我当时实测TensorRT FP16和PyTorch FP32的输出结果,最大误差在千分之一量级,完全不影acc。

5.3 多线程流水线和批量推理

模型加速完,工程的瓶颈就变成了串行处理。解码、检测、跟踪、动作识别,如果全部在一个线程里跑,任何一环卡住整个帧率都会被拖死。我把它改成了四阶段流水线:解码线程只读帧,检测线程不断消费最新帧,跟踪线程维护ID和帧队列,识别线程专门跑X3D推理。每个阶段之间用有界队列连接,队列长度为2就够,这样即使某一帧处理慢了,后面的阶段也不会无限囤积旧帧,保证系统始终处理“最新”的画面。

批量推理是另一个提效点。一个画面里通常有多个行人,如果每个人单独跑一次X3D,batch=1的GPU利用率很低。我把同一帧里所有行人的clip拼成一个batch,一次forward全部算完。在3-5个行人的典型场景下,这个优化让识别部分的平均耗时降低了近一半。两招加在一起,帧率稳定跑到了30-33FPS,终于满足了实时要求。

6. 训练自定义动作和落地复盘

6.1 数据采集、标注和增强的关键动作

pytorchVideo自带的预训练模型是在Kinetics-400上训练的,能分400类,但没有“倒地”“举手”这种细粒度行为,必须微调。数据上我踩过的最大的坑就是“以为自己能搞定数据量”。最初每个动作只录了100条左右样本,训完测试集准确率只有60%,模型在“奔跑”和“行走”之间频繁混淆。后面老老实实把每个类目的样本量提到了300条以上,其中“倒地”这类难以录制的高风险动作,从公开数据集里找相似镜头,加上时间反转增强,硬凑到了150条。

增强策略上,我的建议是时间维度和空间维度都要动。随机时间裁剪相当于模拟动作快慢变化,随机水平翻转能直接翻倍样本量,颜色抖动可以模拟不同监控摄像头的色彩差异。最好再做一个时间反转增强,动作倒放之后语义往往还能成立,对“站起来”和“蹲下”这种方向敏感的动作尤其有效。

6.2 类别不均衡和评测指标

“站立”“行走”这两类样本特别好录,数量很多;“倒地”“举手”样本少,模型天然偏向多数类。我用了两个手段:一是采样器层面做类别加权,让每个batch里少样本类的出现概率更高;二是loss层面用带权重的CrossEntropy,少样本类的loss权重调高。这个组合操作之后,少样本类的recall提升了大概8个百分点。

评测指标上我强烈建议大家不要只看整体Top-1准确率。整体准确率会被多数类拉高,掩盖少数类的糟糕表现。我最后采用的是“每类recall + 混淆矩阵 + 事件级FP/TP统计”三件套。事件级统计尤其重要,它把连续帧预测合并成一次“动作事件”,再去和人工标注的事件对比,看的是系统能不能及时告警,和真实业务场景是最对齐的。

6.3 真实场景下的边界与建议

这套系统在园区室内和户外白天的场景下表现最好,端到端准确率在87%左右,误报也控制住了。但有两个边界必须讲清楚:一是夜间红外模式下画面细节大量丢失,准确率掉到75%以下,这种场景最好在检测阶段就加置信度约束;二是逆光场景下行人轮廓模糊,检测框会在人身上乱跳,跟踪器一旦断轨,动作识别也跟着失灵。

最后给想复现这个项目的朋友几条实在建议:第一,工程上一定先把四阶段流水线搭好再调模型,否则每一步都在和“卡顿”纠缠;第二,pytorchVideo的官方文档停留在“能用”的程度,遇到问题直接翻源码,它的源码写得比文档清楚;第三,所有优化都要以实测为准,别信任何论文里的加速比,我所有数据都是在同一台RTX 3060上、同一段监控视频上对比出来的。

这个项目做完,我最大的体会是动作识别不是一个模型问题,而是一个系统工程问题。模型只负责最后一跳,前面的每一帧解码、每一个检测框、每个跟踪ID,都在为模型提供有用的上下文。把这套链路理顺了,再用pytorchVideo去调模型、调参数,其实就顺理成章了。

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

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

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

立即咨询