从FFmpeg抽帧到SlowFast训练:自定义手势识别全流程实战

发布时间:2026/9/16 19:57:57
从FFmpeg抽帧到SlowFast训练:自定义手势识别全流程实战 SlowFast这套东西在动作识别里算是绕不开的一个方案。原因不复杂它在视频动作识别任务上的效果确实好而且Facebook官方开源了完整的训练推理代码社区资料相对丰富。但SlowFast的问题也很明显——它对数据集的要求不低训练流程比普通图像分类繁琐光是把自定义视频变成模型能吃的样本就能卡住一大批人。这篇文章我顺着自己的实操经历从FFmpeg抽帧到SlowFast训练把整个流程完整走一遍。适合刚接触视频动作识别、想跑通自定义手势数据集的读者也适合对SlowFast感兴趣但一直没找到完整落地路径的人。我默认你有基础的Python和PyTorch环境命令行操作也基本熟悉。如果这两个条件不满足建议先补一下基础再看。1. 手势识别为什么值得用SlowFast而不是普通2D CNN很多人在做手势识别时第一反应是直接每帧都过一个ResNet不就行了这种思路框架上没错但忽略了一个核心问题——动作识别的关键信息不仅仅存在于每一帧的画面里还存在于帧和帧之间的时序变化上。手势尤其明显一个挥手的动作单看某一帧就是一个手臂的静态画面识别模型根本判断不了你是在挥手、举手还是在挠头。它必须看到连续几帧之间的运动趋势才能下结论。这正是SlowFast设计的基本动机用两条分支分别处理空间上是谁/是什么和时间上发生了什么变化。Slow分支用大帧间隔、慢速率来处理静态画面信息Fast分支用小帧间隔、高速率捕捉运动变化。两条分支在多个层进行侧向连接lateral connection最终融合出完整的时空特征。有一个误区得说清楚Fast分支的输入是经过降采样后的低分辨率帧所以它并不是单纯地在重复计算而是用较小的计算代价去补齐时间维度的信息。整体算下来SlowFast的FLOPs并没有比单纯的大模型高出多少但准确率和时序敏感度都明显更好。在Kinetics-400、AVA这些公开数据集上SlowFast都进了第一梯队。要说在手势识别这类短期动作任务上的表现SlowFast尤其合适。手势动作普遍持续0.5到2秒运动幅度集中在上肢和手部时序信息的重要性远大于场景信息。SlowFast的双路径设计正好匹配这种任务特性。相比之下TSN这类时序稀疏采样方案虽然轻量但精度上限不高TSM靠移位操作模拟时序卷积训练速度快但长序列建模能力一般3D-ResNet则是时序空间一起算参数量和计算开销大且没有像SlowFast这样明确区分时间与空间的采样策略。当然这不代表SlowFast是唯一选项。如果你只是做基础的手势识别DemoMediaPipe已经能吃上很大的红利——它的手部关键点检测方案用普通摄像头就能实时跑而且不需要任何训练。我自己的建议是如果目标是产品化、且场景固定比如固定的摄像头、固定的机位MediaPipe更快但要真是研究性质的项目、想上自己的数据集或者是场景多样化SlowFast是更可控、可定制性更强的方向。如果你决定用SlowFast那接下来要解决的第一件事就是数据集从哪来。你不可能一开始就用完整的多类别手势数据集去训练而自己录的视频第一步绕不开的就是抽帧。2. FFmpeg抽帧实操命令、参数和隐藏坑首先明确一个概念SlowFast训练时真正吃进模型的是视频帧不是视频文件本身。整个训练流程中数据管线按索引读取的是每一帧的图像文件。所以从视频到训练样本第一步就是把视频按一定帧率抽成图片序列。FFmpeg是这一步最趁手的工具几乎所有操作系统都支持下载安装不复杂。Windows用户我建议直接下gyan.dev提供的essentials build版本解压后把bin目录加入PATH就行Linux用户直接用包管理器macOS的话brew install ffmpeg。装完后终端执行ffmpeg -version验证是否成功。2.1 精准抽帧的命令模板抽帧最常用的命令如下ffmpeg -i input.mp4 -q:v 2 -r 25 %06d.jpg这行命令的意思是读取input.mp4每秒钟抽取25帧输出为000001.jpg、000002.jpg这样递增的序列文件。-q:v 2是JPEG质量参数数值越小画质越高一般2到5之间2已经足够训练用了。但我要多说一句实际使用中不要真的对每个视频都无脑用-r 25。视频本身的帧率可能是25、30、60。如果原视频60fps你以25fps抽帧每秒钟就要丢掉35帧动作细节可能就丢了如果原视频只有15fps你还强行按25fps抽FFmpeg会重复已有帧来做插值结果就是数据冗余。更稳的用法是先用ffprobe查看原视频帧率再根据动作的剧烈程度决定采样率。ffprobe -v error -select_streams v:0 -show_entries streamr_frame_rate -of csvp0 input.mp4输出类似60/1就是60fps。然后根据任务难度设置采样率。举个例子手势识别我一般用25fps或30fps抽帧就够了因为一个手势动作如果持续1秒25帧足够捕捉从起始到结束的运动过程。如果动作非常快比如手指快速弹动可以提升到60fps但训练数据量也会随之翻倍取舍要自己权衡。2.2 不要忽视起始偏移和抽帧比例有些视频在开头或结尾带了花屏帧、黑帧、Logo动画这些无效帧混进数据集里不仅浪费存储还可能扰乱模型。我习惯先把每个视频截掉开头和结尾的0.3到0.5秒再开始抽帧ffmpeg -i input.mp4 -ss 00:00:00.3 -to 00:00:03.5 -q:v 2 -r 25 %06d.jpg-ss放在-i前是快速定位放在-i后则是逐帧解码后丢弃效率差很多优先把-ss写在-i前面。另外一个容易踩的坑是如果你要按比例抽帧而不是按时间均匀抽比如视频2秒抽50帧1秒抽25帧直接写-r会有问题。FFmpeg的-r在输出端是按帧率复制或丢弃帧它不会智能地根据视频总时长去平均分布。要想按比例抽帧得先探明时长和总帧数再计算步长用select过滤器来实现比如每3帧抽1帧ffmpeg -i input.mp4 -vf selectnot(mod(n,3)) -q:v 2 %06d.jpg这个命令的语法可以记一下mod(n,3)计算帧序号对3取余等于0时才保留实际效果就是每3帧留下1帧。2.3 批量抽帧的脚本思路真实项目里你面对的是成百上千个视频不可能一个个敲命令。我自己习惯写一个bash脚本批量处理逻辑很简单遍历目录下所有mp4以视频文件名建子目录往里面抽帧。#!/bin/bash mkdir -p frames for video in videos/*.mp4; do name$(basename $video .mp4) mkdir -p frames/$name ffmpeg -y -i $video -q:v 2 -r 25 frames/$name/%06d.jpg done这里有个细节值得注意-y参数表示如果输出文件已存在就自动覆盖。批量抽帧时如果中间断了重跑这个参数能避免反复确认。但也要小心它同样会覆盖你之前抽过的帧如果你已经手动筛选过某些帧重跑前记得把目录移走或改名。2.4 抽帧后的质量检查和目录规范抽帧完成不等于数据就绪。我见过不少人抽完帧直接拿去训练结果训练到一半发现数据有严重问题。抽帧后务必抽查几组输出是不是有些视频的帧数明显偏少比如同样是2秒视频大部分抽到50帧某几个只有5帧检查原视频是否损坏。帧的顺序是否正确连续翻几张看看是否出现跳帧、乱序、重复帧。分辨率是否统一如果原始视频有横屏有竖屏后面做crop和resize时会很痛苦最好在抽帧阶段就统一用scale或crop过滤器。我一般会在抽帧脚本里把缩放也一起做了ffmpeg -y -i $video -vf scale320:256 -q:v 2 -r 25 frames/$name/%06d.jpg分辨率这里不用太高SlowFast训练时默认会把图片缩放到256x256再随机裁剪成224x224你抽帧时直接存720p甚至1080p的大图纯粹是浪费磁盘。存成320x256或256x256完全够用训练速度还快不少。3. 制作手势识别数据集标注格式与目录结构抽帧只是把视频拆成了图片序列距离能训练的格式还有一步——组织目录结构、标注类别。SlowFast加载数据时依赖一套自定义的标注格式在官方仓库中一般用的是JSON文件来记录每个样本的路径和标签。3.1 目录结构推荐的两种方案我的习惯是每个视频对应一个独立文件夹里面放该视频抽出来的所有帧文件夹名就是视频名。这跟你用FFmpeg抽帧时按视频名建目录的做法刚好对上。data/gesture/ ├── videos_25fps/ │ ├── gesture_thumbs_up_001/ │ │ ├── 000001.jpg │ │ ├── 000002.jpg │ │ ├── 000003.jpg │ │ └── ... │ ├── gesture_thumbs_up_002/ │ │ └── ... │ └── gesture_thumbs_down_001/ │ └── ... ├── train.json └── val.jsontrain.json和val.json是SlowFast训练时直接读取的标注文件。每个视频在json里占一条记录包含视频帧所在路径、帧数、类别标签等信息。3.2 train.json和val.json怎么写SlowFast官方在读取JSON标注时大致期望以下字段结构具体以你clone的仓库版本为准但核心字段基本一致[ { id: gesture_thumbs_up_001, frame_dir: videos_25fps/gesture_thumbs_up_001, num_frames: 75, label: 0, fps: 30 }, { id: gesture_thumbs_down_001, frame_dir: videos_25fps/gesture_thumbs_down_001, num_frames: 68, label: 1, fps: 30 } ]id样本唯一标识随便起别重复就行。frame_dir存放帧的目录路径注意是相对路径基准是train.json所在的目录。num_frames这个视频一共抽了多少帧从000001.jpg到最后一帧的数量。label类别索引整数从0开始需要维护一个类别到索引的映射关系。fps抽帧时用的原始帧率会参与时序采样逻辑。构建这类JSON不建议手动写自己写个Python脚本自动生成更靠谱还能顺便校验目录是否存在、帧数是否对得上。我写过一个简单的生成脚本核心代码如下import json, os, glob samples [] label_map {thumbs_up: 0, thumbs_down: 1, ok: 2} video_dirs sorted(glob.glob(videos_25fps/*)) for video_dir in video_dirs: video_name os.path.basename(video_dir) label_name _.join(video_name.split(_)[:-1]) if label_name not in label_map: continue frames sorted(glob.glob(os.path.join(video_dir, *.jpg))) if len(frames) 30: continue samples.append({ id: video_name, frame_dir: video_dir, num_frames: len(frames), label: label_map[label_name], fps: 30 }) with open(train.json, w) as f: json.dump(samples, f)注意协调好类名与目录名的对应关系。我的目录命名规则是类别名_序号比如thumbs_up_001、ok_002、thumbs_down_003这样脚本里通过_分割就能得到标签名。3.3 类别映射与数量均衡做数据集时最容易被忽视但又直接影响训练结果的问题就是类别不平衡。手势识别场景下特别典型像点赞手势容易录得多竖中指这种不想录或不好录的类别就少导致模型对高频手势过拟合、对低频手势欠拟合。数据显示平均每类至少500到1000个样本在SlowFast上能训练出基本可用的效果。500个样本对应每个样本1秒到2秒、每秒25帧就是大约1.2万到2.5万帧对自采数据的压力还可以。如果类别确实少可以先做迁移学习用预训练权重初始化模型然后冻结除最后分类层以外的部分进行微调。另外视频样本的时长也要尽量一致。如果有的样本8秒有的只有0.8秒模型会无所适从。我一般会让每个手势样本控制在2到3秒之间太短的补拍太长的裁掉。4. 环境搭建与代码跑通SlowFast有两个主流的跑法一是用Facebook官方的pySlowFast仓库二是用MMAction2。这两个我都用过体验差别比较大。如果你的项目需要重度定制比如改网络结构、加模块建议用pySlowFast源码更直接如果你想快速跑通、方便切换多种模型MMAction2更省心。这里以pySlowFast为主来讲。4.1 环境版本怎么选pySlowFast对版本很敏感我踩过最惨的一次就是PyTorch版本和CUDA版本不匹配编译slowfast/utils模块时直接报错排查了一整天。以下是我验证能稳定跑的版本组合组件推荐版本Python3.8PyTorch1.10 or 1.12torchvision匹配PyTorch版本CUDA11.3或11.6GCC7以上slowfast官方最新master安装命令大概是conda create -n slowfast python3.8 conda activate slowfast conda install pytorch1.12.1 torchvision0.13.1 cudatoolkit11.6 -c pytorch git clone https://github.com/facebookresearch/SlowFast.git cd SlowFast python setup.py build develop这里有个细节SlowFast仓库里的setup.py会编译一些自定义算子执行build develop前最好把机器上的CUDA路径暴露出来并且确认nvidia-smi显示的驱动版本和PyTorch要求的CUDA版本兼容。驱动版本太老而CUDA工具包太新编译也会失败。4.2 目录结构和配置文件的关系pySlowFast加载数据时会参考configs目录下的YAML配置文件。你不需要改动所有配置但有几个关键路径必须改对DATASET: TRAIN_FILE: /path/to/train.json VAL_FILE: /path/to/val.json TEST_FILE: /path/to/val.json SLOWFAST: ALPHA: 8 BETA_INV: 8 FUSION_KERNEL_SIZE: 5 DATA: NUM_FRAMES: 32 SAMPLING_RATE: 2 TRAIN_CROP_SIZE: 224 TEST_CROP_SIZE: 256 TRAIN: BATCH_SIZE: 8 LR: 0.000125这里有几个参数要单独解释一下因为它们直接影响训练效果和显存占用。ALPHA是帧率比Fast分支的帧率是Slow分支的alpha倍官方默认8BETA_INV是通道比Slow分支的通道数是Fast分支的beta_inv倍官方默认也是8。这两个参数在大多数数据集上都比较通用不建议新手乱改。NUM_FRAMES和SAMPLING_RATE决定了模型一次看到多少帧、间隔多大。NUM_FRAMES32表示每个样本取32帧SAMPLING_RATE2表示每2帧采1帧实际跨越原始视频64帧。如果你的视频是25fps这相当于覆盖了大概2.56秒的动作。如果手势动作很短可以减小SAMPLING_RATE。4.3 用预训练权重做初始化直接从零训练一个SlowFast模型需要的数据量极为庞大普通手势数据集根本不够。正确做法是加载在Kinetics-400上预训练过的权重然后把最后的全连接层改成你需要的类别数。pySlowFast在checkpoint目录下提供了多个预训练模型下载后修改配置TRAIN: CHECKPOINT_FILE_PATH: /path/to/SLOWFAST_8x8_R50.pkl AUTO_RESUME: False这里的pkl文件是全量checkpoint包含backbone和head的权重。加载后如果类别数不一致训练时会自动忽略分类层的权重并重新初始化。AUTO_RESUME设为False是为了避免它自动去找上次训练的断点而产生干扰。4.4 首次跑通小规模冒烟测试第一次训练别直接上全量数据。我强烈建议你先建一个冒烟测试子集——从每类中挑3到5个样本把迭代次数改小先跑10个迭代看看流程能不能走通。TRAIN: BATCH_SIZE: 2 MAX_EPOCH: 1 LOG_PERIOD: 1确保日志正常输出、loss在下降哪怕下降得很慢、验证流程能执行、tensorboard能记录再放开到完整数据集。这一步能帮你过滤掉90%的配置问题。5. 训练过程中真正影响效果的参数调整训练跑起来之后很多人以为万事大吉其实真正的调试才刚刚开始。我把自己在训练手势识别数据时用得最多的调节方向总结一下。5.1 Batch Size和Learning Rate的联动SlowFast每次推理需要的显存比普通分类网络大得多。当输入是32帧224x224分辨率时单卡训练batch size基本在8到16之间取决于显卡。我自己的经验batch size调小后如果不改学习率训练很容易发散因为梯度噪声变大、单batch的方差上去了。一个经验法则是线性缩放如果你把官方默认配置的batch size从8减到4学习率也应对应减半如果增加到16学习率可以适当增加但不要超过2倍。SlowFast初始学习率一般是0.000125相当低配合warmup策略使用。我的建议是先用0.0001起步看loss变化再微调。5.2 帧采样策略对训练的影响SlowFast在训练时对帧序列的采样有两个关键行为一是从视频的帧序列中随机选起点二是根据SAMPLING_RATE抽帧。这里有个经常被忽视的点如果视频时长比NUM_FRAMES乘以SAMPLING_RATE要短代码会报错或循环补帧。我之前遇到过训练中突然抛错排查半天发现是某个视频只有20帧但配置要求采样32帧乘2根本不够用。解决方式有两种要么在数据集制作阶段把太短的视频过滤掉min_frames NUM_FRAMES * SAMPLING_RATE samples [s for s in samples if s[num_frames] min_frames]要么配置循环采样让短的视频从头再来。但我更推荐前者因为循环采样会人为制造重复帧对时序学习不友好。5.3 数据增强的开与关SlowFast默认带了随机水平翻转、随机裁剪、色彩抖动等增强。手势识别任务里水平翻转要特别小心——因为左右手语义不一样翻过去之后类别可能就变了比如左手比赞翻成右手比赞。如果你的手势识别是方向敏感的建议关闭水平翻转DATA: HORIZONTAL_FLIP: False颜色抖动和随机裁剪则建议保留它们能增加模型对光照变化和位置变化的鲁棒性。5.4 Loss不降或过拟合时的排查思路训练过程中最常见的问题就是loss一直下不去。我的排查顺序是先看数据有没有错。用可视化工具随机画几个样本确认帧序列确实对应了正确的标签。重点检查标注文件里label和帧内容是否对应很多时候是脚本解析目录名时把类别搞反了。确认类别均衡。如果有一个类别样本特别少loss会被多数类主导表现就是majority类学好了minority类完全学不动。可以从混淆矩阵里看出端倪。调整学习率。如果loss持续震荡不降把学习率降到原来的五分之一试试如果降低后有效说明初始学习率过大。检查过拟合。训练集loss降但验证集loss升就是过拟合信号。这时候可以增加dropout比例、增大数据增强强度、或者提前停止。有一个具体的经验可以参考SlowFast训练手势识别这种短期动作样本时通常会先经历一段loss下降不明显的平台期大约3到5个epoch然后突然开始显著下降。如果你在第1个epoch就盯着loss焦虑那太早了。我一般至少看10个epoch再下结论。6. 验证、可视化与导出推理模型训练完成不代表任务结束你还需要一套可靠的验证流程来确认模型是真的学到了手势语义而不只是背下了训练集的帧。这部分操作虽然偏工程但决定了模型能不能真正落地。6.1 混淆矩阵是最直接的效果度量精度和损失函数是宏观指标混淆矩阵才能告诉你具体哪些手势之间容易混淆。比如你可能会发现点赞和比OK经常被搞混因为两者都涉及拇指和食指的接触或者手掌张开和五指并拢之间的区分度不够。这些信息用于决定是否增加样本量、是否换数据增强、是否修改类别定义都非常有价值。pySlowFast验证完之后测试集结果会打印在日志里。如果你想得到混淆矩阵可以在val_epoch函数里加一段sklearn的confusion_matrix调用把预测结果和真实标签收集起来最后画图保存。这个改动不难但价值很大。6.2 对验证视频做可视化推理模型训练好之后不要直接就拿去做性能测试先找一个没见过的视频抽帧跑一次推理把预测结果以可视化形式输出——比如把类别名和置信度写到图片上再合成一个带标签的视频。这样你能直观看到模型在不同时序点的预测是否正确、置信度高不高。推理阶段最简单的做法是写一个Python脚本使用slowfast.utils.checkpoint加载训练好的权重对测试视频逐段滑动窗口预测import torch, cv2, json from slowfast.utils import checkpoint as cu from slowfast.models import build_model from slowfast.utils import misc from slowfast.datasets import transform import slowfast.utils.logging as logging logger logging.get_logger(__name__) def init_model(cfg, ckpt_path): model build_model(cfg) cu.load_test_checkpoint(cfg, model) model.eval() return model注意加载checkpoint时要把配置里的NUM_CLASSES改成你训练时的实际类别数否则最后全连接层尺寸对不上。6.3 导出成可部署的ONNX或TensorRT训练完成后往往需要落地比如放进Web服务或移动端App。这时候建议把PyTorch模型导出成ONNX或TensorRT格式。SlowFast导出ONNX稍有一点麻烦因为它的lateral connection涉及一些自定义算子但PyTorch1.10以上版本整体支持得还行。dummy_input torch.randn(1, 3, 32, 224, 224) torch.onnx.export(model, dummy_input, slowfast.onnx, opset_version11)导出后最好先用onnxruntime跑一遍验证输入输出形状和值是否和PyTorch一致避免在部署阶段才发现问题。如果你的部署环境支持TensorRTONNX转TensorRT还能进一步提升推理速度。当然这是一个复杂的工程本文不展开后续有时间可以单独写一篇部署相关的文章。6.4 在验证过程中发现类别混淆后怎么迭代我在做手势数据时碰到过一种很常见的情况某个手势类别之间差异本来就很小比如数字1和数字2的手势它们都只涉及一到两个手指的伸展模型特别容易学混。这时候单纯堆样本量效果也不明显更有效的做法是检查你的采样率是不是太低手指的细微运动在高帧率下才能体现。考虑缩小Fast分支的间隔或者把ALPHA调大让时间分辨率更高。把这几个难分的手势合并成一个大类减少类别间的语义重叠。第三步听起来像作弊但实际在工程里是常态。产品需求如果不需要区分特别细的手势就没必要让模型做它做不好的事。7. 回顾整个流程时遇到的几个经典疑难最后把我自己以及周围人在复现、迁移这个方法时反复遇到的几个问题集中列出来算是给后来者提个醒。7.1 FFmpeg抽帧环节的命名对齐问题FFmpeg输出的帧序是从000001开始的但SlowFast在读取帧时是从000001开始递增查找的。这个看似简单的命名规则如果中间有缺失帧或者命名从000000开始都会导致读取错位。排查方法是看日志里报的frame path是否存在检查文件名第一位是否对齐比如000001.jpg在SlowFast代码中对应索引1还是0。7.2 训练时显存溢出SlowFast默认的8x8配置8帧采样间隔、8帧输入对显存要求大约12GB以上。如果你只有8GB显存可以按这个顺序缩小配置把BATCH_SIZE降到2或1。把NUM_FRAMES降到16或8。把TRAIN_CROP_SIZE从224降到160。使用混合精度训练开启AMP。我自己的经验是在保持效果可接受的前提下优先降BATCH_SIZE其次降NUM_FRAMESTRAIN_CROP_SIZE尽量不要低于160否则手势细节可能丢失。7.3 数据集和预训练模型领域差距大Kinetics-400预训练模型学的是通用人类动作而手势识别的视觉特征与它有差异。如果你发现迁移学习效果不理想可以尝试用一些中间阶段的视频数据集做二次预训练让模型先学习更多动作语义再用你的手势数据进行精调。这个过程不长但通常能显著提升小数据集上的精度。7.4 评估指标不要只看top-1手势识别里top-1准确率很容易被类别语义接近的问题拉低。比如学数字1和伸食指之间如果用Top-2的指标或者更宽容的评估标准模型的表现其实并不差。多维度评估能让你对模型的实际能力有一个更客观的认识。我在实际项目里最满意的一次结果是用大约1500个自采手势样本通过SlowFast微调预训练模型在8个手势类别上取得了接近90%的准确率。这个成绩不算顶尖但它验证了整个流程的可行性——从FFmpeg抽帧到JSON标注再到训练微调每一步都不难每一步都有细节。把这些细节处理好你的自定义手势识别就能平稳地从数据集走到一个可用的模型。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询