统一视频数据集访问层:为EPIC和Something-Something设计PyTorch数据接口
发布时间:2026/9/2 2:29:43来源:尧图网络
简介面向计算机视觉研究者与深度学习开发者这套工具集统一封装了 Something-Something-V1、EPIC-Kitchens、HMDB-51、Diving48 等常见视频数据集的预处理与读取逻辑。它解决了视频抽帧、标注解析、训练集划分、标签及类别键输出等重复性工作适合用来快速搭建动作识别、视频分类的数据管线。资源压缩包共 41 个文件主体是 27 个 Python 脚本与 10 个 Shell 脚本另含 Markdown 说明、gitignore 等辅助文件整体仅 44KB。Python 端封装了数据集 API例如 Something-Something-V1 的类别数定义为 174Shell 端则覆盖视频切割、抽帧、从图片生成视频、目标检测对比等批量操作目录按数据集分别组织查看说明即可按需复用。目前已有 875 人学习下载适合作为视频理解实验的起步工具帮助节省数据集处理时间。 做视频理解研究的同学应该都有过这种经历换一个数据集就得重写一遍数据加载逻辑。我在 EPIC-Kitchens 和 Something-Something-V1 之间来回折腾了几轮之后终于下定决心把数据访问这层重复工作单独抽出来做成了 video_datasets_api 这个工具。它做的事情很朴素给这两个视频数据集提供统一的数据集抽象、样本元数据查询和帧读取接口让上层训练代码不必关心每个数据集“内部长什么样”。如果你平时主要跟动作识别、时序动作检测这类任务打交道并且经常需要跨数据集做实验这篇文章会讲清楚这个工具背后的设计思路、踩过的坑以及怎么把它接到 PyTorch 训练管线里。1. 先说说我为什么写这个工具两种格式隔着的不是数据是生态很多人觉得视频数据集嘛不就是一堆视频文件加几个标签。真上手之后才知道每个数据集都有自己的“脾气”。EPIC-Kitchens 和 Something-Something-V1 正好代表了两个截然不同的方向把它们并排放在一起看最能看出统一访问层的价值。1.1 EPIC-Kitchens把“科研友好”做到了极致的帧序列格式EPIC-Kitchens 是第一人称视角的厨房活动数据集视频来自头戴相机全部在真实厨房环境录制。它最大的特点是官方直接把视频拆成了 JPEG 帧序列同时附赠密集光流。每个样本由participant_id / video_id两层目录组织帧文件名类似frame_0000000100.jpg帧号直接暴露在文件名里研究者根本不需要自己调用解码器去抽帧。代价是元数据非常复杂。官方给的 CSV 里包含uid、video_id、narration_id、participant_id、start_timestamp、stop_timestamp、start_frame、stop_frame、verb、noun、verb_class、noun_class这些字段。动作标签不是简单的单标签而是“动词 名词”的组合空间比如cutonion。训练集和验证集的标签结构一致但测试集官方不公开动作标签提交结果走服务器评测。这在多数据集对比实验里非常恼人你是要按 verb 分类按 noun 分类还是按 verbnoun 联合分类不同论文的设置还不一样。1.2 Something-Something-V1标签简单但视频是“黑盒”Something-Something-V1 走的是另一条路。它是第三人称视角的短视频集合每段只有 2 到 6 秒展示人与物体的基础交互比如“Pouring water into a glass”往杯子里倒水、“Putting something on top of something”把某物放到某物上面。全部动作类别 174 个标签是平铺的单一类别元数据只用 JSON 就解决了一个labels.json做类别 id 到动作名字的映射一个划分文件列出每个视频的 id 和对应标签。听起来比 EPIC 简单多了问题出在视频本身。官方发布的是 webm 压缩视频不是现成的帧序列。你在训练时想按固定帧率采样就必须解码想随机裁剪一段也得先把视频解出来再说。decord、OpenCV、imageio 这些库在不同环境和视频编码下行为不一致光这一层就能消耗掉好几天。而且 webm 通常用 VP8/VP9 编码某些 OpenCV 发行版压根不支持硬解踩坑概率极高。两种格式一个偏“文件系统友好”一个偏“存储压缩友好”但研究者真正需要的接口其实是同一套给我一个样本告诉我它有哪些帧再把帧交给我。video_datasets_api 做的就是把这套接口固化成代码。2. 统一 API 的核心抽象样本、动作列表与帧路径生成设计这个工具的时候我反复提醒自己一句话不要把上层代码绑死在某个数据集的特殊细节上。所有跟“这个数据集特有的格式”相关的逻辑一律封装在数据集适配器内部对外只暴露最小的一致接口。2.1 样本对象把 CSV 行和 JSON 项装进同一个结构无论是 EPIC 的 CSV 行还是 Something-Something 的 JSON 项落到训练代码里最终关心的就是几个字段当前样本属于哪个视频、它的标签是什么、帧从哪里来、可用的帧范围是多大。我定义了一个统一的样本数据类大概长这样dataclass class VideoSample: dataset_name: str split: str video_id: str label: str label_id: int verb_id: Optional[int] None noun_id: Optional[int] None start_frame: Optional[int] None stop_frame: Optional[int] None video_path: Optional[str] NoneEPIC 的采样器会把start_frame和stop_frame填上video_path为空Something-Something 则反过来video_path有值start_frame是None。上层代码读到样本后不需要关心是哪种情况统一调用帧读取接口就行。2.2 动作列表与类别映射两种标签哲学的折中方案EPIC 的 verb、noun 两个维度可以自由组合Something-Something 则是单标签列表。我在 API 里提供了两套查询能力get_action_list()返回当前数据集全部平铺类别Something-Something 直接返回 174 类EPIC 默认返回 verbnoun 组合后的完整列表。get_action_space()返回(verbs, nouns)或(None, None)方便那些需要分开建模 verb 和 noun 的模型直接拿原始维度。实测下来大多数动作识别模型只需要label_id一个整数联合建模 verb/noun 的工作也有但占比小。所以样本对象里既保留组合后的label_id也保留原始verb_id/noun_id两边都不得罪。调试多数据集对比实验时这个折中能省掉大量“临时改标签映射表”的时间。3. 帧读取链路的实现选择decord、cv2 与 JPEG 帧序列的并存策略帧读取是数据集工具的核心瓶颈也是坑最多的地方。两种数据集一个从文件系统读预抽帧一个从视频容器里现解码我分了两套后端但对外暴露统一方法。3.1 Something-Something 的 webm 解码我最终选了 decord试过一圈后我最终把 decord 作为 Something-Something 的主解码器。原因很实际decord 对 webm 视频的 VP8/VP9 编码支持比较稳而且支持 GPU 解码训练时可以把解码放到 GPU 上节省 CPU 开销。具体读取逻辑这样写import decord def read_video_frames(sample: VideoSample, num_frames: int, fps: float 12.0): vr decord.VideoReader(sample.video_path, ctxdecord.cpu(0)) total_frames len(vr) # 均匀采样到目标帧数避免视频长度不一时 batch 内帧数对不齐 indices torch.linspace(0, total_frames - 1, num_frames).long().tolist() frames vr.get_batch(indices).asnumpy() return frames # shape: (num_frames, H, W, 3)有几个点值得注意。一是必须显式指定ctx否则多进程 DataLoader 下容易出现解码上下文冲突。二是decord.VideoReader不要在每个 epoch 重复初始化最好在数据集初始化时做一次否则高分辨率视频的开销会拖慢训练。三是如果是验证集单次遍历直接把vr留在数据对象里没问题但如果配合shuffleTrue做随机采样建议每次__getitem__用索引定位避免维护多进程内共享的解码器状态。3.2 EPIC 的 JPEG 帧与光流文件路径拼接和按需裁剪EPIC 读完元数据后帧读取就是个路径拼接问题。但这里有一个隐蔽的坑start_frame和stop_frame给出的是帧号而文件名里的数字是按十位补零的。我第一次写的时候直接 f-string 拼了上去结果在frame_0000000100.jpg这种边界下找不着文件。正确做法是先算好目标帧号再补零到对应宽度def frame_path_for_index(frame_root: str, frame_index: int, width: int 10): return os.path.join(frame_root, fframe_{frame_index:0{width}d}.jpg)光流部分通常是水平 U 和垂直 V 两个分量图按帧号对应存放。读取时我用 OpenCV 的cv2.imread读灰度图然后np.stack([u, v], axis-1)拼成两通道输入。训练模型时是否包含光流通道取决于模型设计但数据工具这一层把拼接逻辑处理好模型那边就能无脑取数。3.3 时间戳与帧号的换算逻辑EPIC 的 CSV 同时给了start_timestamp和start_frame两个字段其实对应同一个物理时刻。有些实验希望按时长做随机裁剪而不是按帧号裁剪这时就需要换算关系帧率近似等于(stop_frame - start_frame) / (stop_timestamp - start_timestamp)。不过官方帧是从解出的视频二次导出的时间和帧号之间存在少量取整误差。我在 API 里默认按帧号裁剪因为帧号是离散且稳定的按时间裁剪的接口保留但内部再换算回帧号宁可多做一步也避免在浮点时间上直接取整的偏差。4. 从 API 到训练管线一个完整的 PyTorch 加载器示例工具最终要接进训练流程才有价值。我给出一个最简但能直接跑的 PyTorch Dataset 封装展示 video_datasets_api 和现有训练代码怎么衔接。4.1 构造统一的 Dataset 子类import torch from torch.utils.data import Dataset from video_datasets_api import make_dataset class UnifiedVideoDataset(Dataset): def __init__(self, dataset_name, root, split, num_frames8, frame_stride2, is_trainTrue): self.ds make_dataset(dataset_name, rootroot, splitsplit) self.samples self.ds.get_samples() self.num_frames num_frames self.frame_stride frame_stride self.is_train is_train def __len__(self): return len(self.samples) def __getitem__(self, idx): sample self.samples[idx] frames self.ds.read_frames( sample, num_framesself.num_frames, strideself.frame_stride, random_offsetself.is_train ) label torch.tensor(sample.label_id, dtypetorch.long) return torch.from_numpy(frames).permute(0, 3, 1, 2).float(), label核心逻辑集中在read_frames里。对 EPIC它在start_frame到stop_frame区间内按 stride 采样或随机取起始点后连续采样对 Something-Something它调用之前的 decord 解码逻辑做均匀采样。上层完全感知不到两者的区别。4.2 DataLoader 多进程和缓存策略视频帧读取代价高num_workers开得不够会拖垮训练吞吐。以我的经验num_workers4到8是合理区间再高边际收益递减。由于每个 worker 是独立进程decord 的解码器状态也是独立的前面提到的 ctx 问题在多进程下反而更好隔离。但有一点必须注意不要在__getitem__里重复创建VideoReader否则多进程会同时打开大量文件句柄轻则变慢重则触发系统文件描述符上限。缓存方面我的建议是分两层第一层是元数据缓存这一层数据小可以直接 LRU 缓存到内存避免每次重新解析 CSV/JSON第二层是帧缓存这一步只建议用来缓存“多次会用到”的数据增强结果或高频起点附近的帧。视频整体缓存进显存并不现实除非你的视频抽样非常集中。4.3 跨数据集混合实验怎么接如果你打算做多数据集联合训练也就是一个 batch 里既有 EPIC 样本又有 Something-Something 样本建议不要在一个 Dataset 里硬拼。用torch.utils.data.ConcatDataset把两个封装好的 Dataset 拼接每个子数据集自己管理自己的帧读取训练循环不变。唯一要改的是标签空间两个数据集的类别集合完全不同要么各自维护输出头要么在采样器里做标签重映射。我倾向于后者用一个小字典把每个数据集的类别映射到统一空间再在 loss 计算时用 mask 区分。5. 踩坑记录命名、索引与数据完整性的隐性坑工具写了很多版真正让我崩溃的往往不是算法而是一些看起来极不起眼的格式细节。这里专门记录几个必须注意的点。5.1 帧号是 1-indexed 还是 0-indexed必须先确认EPIC 官方文档中帧序号是从 1 开始的但不同脚本处理时经常出现“正好差一帧”的情况。我在工具里统一约定API 内部所有帧索引以 0 为基准读 CSV 时自动减 1对外文档也明确标注避免上层用户在其他脚本里按 1 基传参。还有stop_frame是闭区间还是开区间也要理清楚。实测下来按闭区间处理就是range(start_frame, stop_frame 1)多算的可能越界报错少算的会少一帧。这个边界问题在实验里通常不影响收敛但如果你做时序边界评估差一帧会导致指标出现不可解释的小波动。严谨起见我在适配器里做了 clip保证索引落在合法范围内。5.2 Something-Something 的路径分隔符和视频损坏问题Something-Something 的划分文件里视频 ID 可能是纯数字也可能带路径层级。在不同平台上拼接路径时反斜杠和正斜杠混用会导致找不到文件。我统一用pathlib.Path处理不再手写字符串拼接。更头疼的是视频损坏。官方数据集传输过程中偶尔会有文件损坏decord 在读取到坏文件时会抛异常。最开始我没有捕获导致 DataLoader 的 worker 崩溃后整个训练中断。后来加了异常捕获和重试机制def read_video_frames(sample, num_frames, retries3): for attempt in range(retries): try: vr decord.VideoReader(sample.video_path) ... return frames except Exception: if attempt retries - 1: raise # 捕获后补一个全零帧并在日志里记录 return np.zeros((num_frames, H, W, 3), dtypenp.uint8)补零帧的做法只适合验证阶段用来跑通流程正式训练时必须人工排查损坏文件不能让模型看着全零帧去学。5.3 EPIC 的目录层级和缺失帧问题EPIC 不同下载批次目录层级可能不完全一致有的包videos前缀有的没有。我在工具里加了目录探测传入 root 后自动在可能的几层目录中寻找frame_前缀文件找到就用找不到就报错并打印当前目录树。这个小功能帮我在换机器时省了大量排查时间。另外EPIC 的帧序列里偶尔会缺少中间某几帧可能是官方导出时编码失败也可能是下载不完整。读取时如果遇到文件不存在我先检测缺失比例缺失比例低就用最近邻帧补位缺失比例高直接跳过该样本。这个策略写进 API 后大规模预训练时省心很多。5.4 光流和 RGB 的帧号是否一致不要想当然EPIC 的 RGB 帧和光流帧是两套独立文件虽然帧号通常对应但个别视频对不上尤其是视频开头和结尾边界处。稳妥的做法是交叉核对两边文件列表取并集再按帧号排序建立统一索引。video_datasets_api 的 EPIC 适配器内置了这个同步逻辑默认以 RGB 帧为准读取光流时按 RGB 帧号去查找找不到就返回空光流并打警告。6. 一点经验工具的设计要能容忍“数据集永远在变”视频数据集的格式和标签体系变动频繁EPIC 出新版本、Something-Something 出 V2、新的 benchmark 不断涌现每个都有自己的一套规则。我写 video_datasets_api 时想得很清楚不追求把全世界的视频数据集都包进来只求在两个核心数据集上做到“接入成本低、替换成本低、训练代码不感知差异”。如果你也要做类似的数据访问层我强烈建议一开始就定义好数据样本的抽象结构再把每个数据集的适配器做成独立后端。不要一上来就写“三合一”的巨型加载器那只会让所有实验都受制于一个永远改不完的文件。多数据集对比实验里真正花时间的应该是模型和训练策略的差异而不是反复调试某个数据集的帧该从第几号开始读。最后一个小技巧数据加载层一定要预留一个“dry-run”模式只输出样本数量和标签分布不真正读帧。很多数据集在下载端就出了问题但这个 bug 要等到训练启动后才会暴露排查成本极高。dry-run 模式能在两分钟之内告诉你数据是否完整值得花半天时间实现。本文还有配套的精品资源点击获取
网站建设高端定制企业官网