文章目录
0. 前言
- 本文只分析行为识别数据集相关内容,不考虑其他数据集。
- 概述
- 支持的数据输入方式:视频/帧。
- 提供两种方式处理视频:mmcv(其实也就是opencv)处理以及使用了cuda的decord。
- 帧提取方式:TSN的分段提取帧,以及其他模型常用的连续提取帧。
- 数据增强方式:就提供了resize+crop。
- 支持的数据集:hmdb51、ucf101、kinetics、ava、thumos14
- 支持的数据输入方式:视频/帧。
- 本文内容包括
- 第一章:数据集预处理:将不同数据集构建成统一形式,方便后续构建Dataset对象。
- 第二章:构建 Dataset 对象:从源码角度介绍整体构建过程与相关配置文件,之后从实现功能角度分析帧提取策略、数据预处理方式与数据增强方法。
- 第三章:dataloader 构建:这部分没细看,主要内容应该是分布式相关。
1. 数据集预处理
- 主要内容:
- 需要如何进行预处理。
- 预处理的结果是什么。
1.1. 需要进行什么预处理
- 第一步:下载数据集以及对应的标签。这个没啥好说的。
- 第二步:对视频进行提取帧。
- 可以提取光流或rgb帧。
- 对应的脚本是
data_tools/build_rawframes.py
。- 可通过
flow_type
指定帧的类型,包括None
-RGB帧、tvl1
-光流。 - 光流提取主要依靠第三方库 dense_flow,提取光流可通过GPU实现。
- RGB帧提取通过CPU,具体是通过
mmcv.VideoReader
遍历所有帧分别保存。可能用ffmpeg快一点? - 总体实现通过进程池。
- 采坑:通过GPU提取光流时,注意
num_gpu
参数,如果大于已有的GPU数量会报错……
- 可通过
- 第三步:建立输入数据列表。
- 对应的脚本是
data_tools/build_file_list.py
- 主要过程就是:
- 获取
现有样本信息
(对于帧数据返回的是帧文件夹路径、RGB帧数量、光流帧数量,对于食品数据返回的是视频路径)。 - 获取
应有样本信息
,通过数据集的annotations获取,分别获取train/val/test的相关信息。- 这一步是数据集相关的,如果要新增数据集就要重写一个函数。
- 每个样本信息包括了
vid
和label
,vid
表示的是样本id,一般通过样本相对路径来表示(如果样本是视频,一般不包括扩展名),label
是行为标签(数字)。
- 关联
现有样本信息
与应有样本信息
,构建目标信息,并写入文件。- 关联的过程也很简单,就是查看
应有样本信息
在现有样本信息
中是否存在,如果存在写入一条信息(如果是帧样本则包含的信息是vid、rgb帧数量、光流帧数量、标签,如果是视频样本则包含的信息是vid、标签)。 - 有一点疑问,在对应的方法中连续两次调用了
random.shuffle
分别处理光流和RGB,这样可以吗?
- 关联的过程也很简单,就是查看
- 获取
- 对应的脚本是
1.2. 预处理的结果是什么
- 其实就是为mmaction构建
Dataset
对象提供数据源。 - 主要用到的就是上面说到的输入数据列表。
2. 构建Dataset对象
- 主要内容:
- 构建过程概述
- 配置文件详解
2.1. 构建过程概述
- 通过分析
tools/train_recognizer.py
和tools/test_recognizer.py
可以知道,mmaction中构建torch.utils.data.Dataset
对象主要是通过调用mmcv.obj_from_dict
方法。 - 该函数的定义为
def obj_from_dict(info, parent=None, default_args=None)
- 基本思路就是,
info['type']
保存了类的名称,在parent或sys._modules
中寻找名为info['type']
的类对象,然后通过info
中剩余的参数以及default_args
中的参数初始化该类。 - 在mmaction中,
parent
的取值一般是mmaction.datasets
- 该对象中保存着所有mmaction支持的数据库形式。
- 本文关注的行为识别数据集,主要相关的类就是
VideoDataset
和RawFramesDataset
两个类。
2.2. 配置文件详解
- 配置文件中数据集相关的主要就是字典对象
data
。该对象主要包括的key有:videos_per_gpu, workers_per_gpu, train, val, test
。 - 前两者在后面介绍dataloader时再介绍。
- 后面三者的形式都是一样的,这三个key对应的value也是dict,作为上面
obj_from_dict
中的info
参数。所以,也可以理解为,train/val/test 这三个字典就是构建Dataset对象的参数。 - 由于
VideoDataset
与RawFramesDataset
的构造器基本上相同,所以这里先同一介绍下相同的部分。 - 配置举例(帧提取策略在后面会详细介绍):
train=dict(
type=dataset_type, # 选定的Dataset名称,用于 obj_from_dict 中,这里一般取值就是 `RawFramesDataset` 或 `VideoDataset`
ann_file='data/kinetics400/kinetics400_train_list_rawframes.txt', # 2.2. 数据预处理中生成的 `输入数据列表`
img_prefix=data_root, # 配合 ann_file,用于指定输入数据的位置。如果输入数据都在同一根目录下,那通过该参数指定根目录路径,在ann_file中只需要指定相对路径即可。但也可以将该参数设置为``,在ann_file中直接指定参数的绝对路径。
img_norm_cfg=img_norm_cfg, # 输入图像预处理方式,主要就是指定mean/std,作为mmcv.imnormalize的参数
input_format="NCTHW", # 指定输入图片channel的分布
num_segments=1, # 帧提取策略相关,将视频分为 num_segments 然后分别提取帧
new_length=32, # 帧提取策略相关,每个 segment 提取多少帧图片(一般都会重复提取)
new_step=2, # 帧提取策略相关,配合 new_length 使用,
random_shift=True, # 帧提取策略相关
modality='RGB', # 输入数据形式,是rgb还是光流
image_tmpl='img_{:05d}.jpg', # 输入数据名称模版
img_scale=256, # 数据增强参数,
input_size=224, # 数据增强参数,图片的最终尺寸
div_255=False, # 数据增强参数,是否对输入图片向量进行 `/255.0` 操作
flip_ratio=0.5, # 数据增强参数,水平翻转概率
resize_keep_ratio=True, # 数据增强参数,resize相关参数
oversample=None, # 在测试时,如果需要提取多帧求平均,就需要这个参数
random_crop=False, # 数据增强参数,是否随机切片
more_fix_crop=False, # 数据增强参数,随机切片相关
multiscale_crop=True, # 数据增强参数,随机切片相关
scales=[1, 0.8], # 数据增强参数,
max_distort=0, # 数据增强参数,
test_mode=False), # 帧提取策略相关
2.3. 帧提取策略
- 帧提取策略是通过
Dataset
获取帧图片、作为模型输入的核心流程之一。 - 两种帧提取方式:
- 将视频平均分为若干segment,每个segment中提取一帧,一共 num_segments 帧图片作为输入。
- 这种方式用于TSN模型,参数中基本上
num_segments>1, new_length=new_step=1
- 这种方式用于TSN模型,参数中基本上
- 连续获取
new_length
帧图片,每隔new_step
帧获取,即在视频的连续new_length * new_step
帧图片中获取new_length
帧图片作为输入。- 这种方式在除TSN外其他模型中用得比较多,参数基本上是
num_segments=1, new_length>1
- 这种方式在除TSN外其他模型中用得比较多,参数基本上是
- 将视频平均分为若干segment,每个segment中提取一帧,一共 num_segments 帧图片作为输入。
RawFramesDataset
与VideoDataset
中的帧提取策略都是相同的,不同之处在于获取帧的方式。- 前者直接读取帧图片文件,后者通过
VideoReader
并通过帧图片下标获取帧。 - 前者速度明显快一些,后者就很慢了。
- 前者占硬盘空间很大(毕竟所有视频都要提取帧),后者所占硬盘空间小。
- 前者直接读取帧图片文件,后者通过
- 帧提取相关参数:
num_segments
:将视频分为num_segments
个部分。new_length
:每个segment需要提取多少帧图片。new_step
:在每个segment中连续提取帧时,提取帧的间隔。random_shift
:本质就是在获取每个segment中帧id时,是随机获取其中一帧(random_shift=True
)还是获取正中的一帧(random_shift=False
)。temporal_jitter
:在new_step>1
时有用,就是在连续提取帧时是否略微变化帧id。test_mode
:帧提取方式细节不同。
- 具体过程
- 第一步:获取帧下标。获取方式一共有三种,通过
test_mode
与random_shift
来确定获取方式。- 三种方式分别对应
_sample_indices
,_get_val_indices
,_get_test_indices
三个函数。 - 返回的参数包括对应的帧下标
offsets
(从1开始编号,shape为[num_segments,]
)以及temporal jitter参数skip_offsets
(shape为[new_length,]
)。 - 返回的这两个参数就作为第二步的输入。
- 主要参数:
num_segments, new_length, new_step, old_length=new_length * new_step
。
- 三种方式分别对应
- 第二步:根据帧下标获取对应的图片。
- 对应的函数是
_get_frames
。 - 主要输入参数就是上一部获取的帧下标
offsets
以及 temporal jitter 参数skip_offsets
。
- 对应的函数是
- 第一步:获取帧下标。获取方式一共有三种,通过
- 获取帧下标具体实现方式
-
- 对于训练时(
test_mode=False and random_shift=True
),初步获取帧下标的流程是:
- 计算每个segment的平均帧数。
- 这个平均帧数是通过
(num_frames - old_length + 1)// num_segments
。 - 换句话说,此时提取的帧下标最大值不会超过
num_frames - old_length
。
- 这个平均帧数是通过
- 如果平均帧数大于1,那么每个segment中可能不止一帧,随机获取其中一帧的下标。
- 如果平均帧数小于1,且本样本中帧的数量大于
num_segments
与old_length
(即num_segments + old_length > num_frames > max(num_segments, old_length)
时),在[0, num_frames - old_length]
范围中随机获取num_segments
帧的下标(必定会重复提取帧)。 - 如果 num_frames 的数量同时小于
num_segments, old_length
,那么就每次都提取第1帧,提取num_segments
次。
- 对于训练时(
-
- 对于验证时(
test_mode=False and random_shift=False
),初步获取帧下标的流程是:
- 当
num_frames > num_segments + old_length - 1
时,将每个样本的前num_frames - old_length
平均分为num_segments
个部分,获取每个 segement 中间帧的下标。 - 其他时候,每次提取第一帧,提取
num_segments
次。
- 对于验证时(
-
- 对于测试时(
test_mode=True
),初步获取帧下标的流程是:
- 当
num_frames > old_length - 1
时,将每个样本的前num_frames - old_length
平均分为num_segments
个部分,获取每个 segement 中间帧的下标。- 与val的不同之处在于限制条件,只要当
num_frames > old_length - 1
即可,而不用大于num_segments + old_length - 1
。 - 可能会存在重复提取帧的情况。
- 与val的不同之处在于限制条件,只要当
- 其他时候,每次提取第一帧,提取
num_segments
次。
- 对于测试时(
-
- 实现 temporal jitter。
- 通过前面三步都初步获取了
num_segments
个帧下标。 - 这一步是对这些下标进行微调,即每个下标随机加上
[0, new_step)
。 - 也就是说,temporal jitter 只在
new_step > 1
的时候生效。 - 返回一个
skip_offsets
,用于后面的操作。
-
- 通过帧下标获取帧信息流程
-
- 由于每个segment都对应了一个帧下标,所以首先对每个segment分别进行操作。
-
- 每个segment分别提取
new_length
帧图片,帧图片下标通过当前segment的帧下标以及skip_segmetns
来获取。
- 从当前segment的帧图片id开始,每
new_step
获取一次图片(再加上对应的skip_segments),一共获取new_length
张图片。
- 每个segment分别提取
-
- 举例:
- 如果某个样本共有40帧图片,假设
num_segments=8, new_length=8, new_step=2
- 每个segment的帧id是
1, 4, 7, 10, 14, 17, 20, 23
- 每个segment获取8张图片,不考虑 temporal jitter,则对应的帧id是:
1, 3, 5, 7, 9, 11, 13, 15
4, 6, 8, 10, 12, 14, 16, 18
- …
20, 22, 24, 26, 28, 30, 32, 34
23, 25, 27, 29, 31, 33, 35, 37
- 每个segment的帧id是
- 如果某个样本共有5帧图片,假设
num_segments=8, new_length=8, new_step=2
- 那么每个segment的帧id都是0.
- 每个segment获取8张图片,不考虑 temporal jitter,则对应的帧id都是:
0, 2, 4, 4, 4, 4, 4, 4
- 如果某个样本共有40帧图片,假设
2.4. 数据预处理与数据增强
- 数据预处理与数据增强是通过
Dataset
获取帧图片、作为模型输入的核心流程之一。 - 数据增强相关参数:
input_size
:第一步相关参数,设置是否进行crop,即crop_size
,用于后续各种crop中oversample
:第一步相关参数,设置是否进行three_crop/ten_crop
resize_crop
:第一步相关参数,设置是否进行resize_crop
rescale_crop
:第一步相关参数,设置是否进行rescale_crop
multiscale_crop
:第一步相关参数,设置是否进行multiscale_crop
random_crop
:第一步相关参数,multiscale_crop
参数,设置是否进行fix_crop
more_fix_crop
:第一步相关参数,multiscale_crop
参数max_distort
:第一步相关参数,multiscale_crop
参数scales
:第一步相关参数,multiscale_crop
参数resize_ratio
:第一步相关参数,multiscale_crop
参数img_scale
:第一步相关参数,在非resize_crop/rescale_crop
情况下resize参数img_scale_file
:第一步相关参数,在非resize_crop/rescale_crop
情况下resize参数resize_keep_ratio
:第一步相关参数,在非resize_crop/rescale_crop
情况下resize参数flip_ratio
:第二步相关参数div_255
:第三步相关参数img_norm_cfg
:第四步相关参数size_divisor
:目前好像没用
- 主要方法:通过
mmaction.datasets.transforms.GroupImageTransform
实现。 - 主要流程:
-
- resize/crop:如果设置了
resize_crop/rescale_crop
则直接执行(后面会介绍这两种方式的细节),否则就根据输入的img_scale/keep_ratio
对所有图片进行resize、再执行其他几种可能的resize/crop操作(包括oversample/multiscale_crop/centercrop,后面会介绍细节)
- resize/crop:如果设置了
-
- 根据
flip_ratio
参数水平镜像图片。
- 根据
-
- 根据
div_255
参数将图片从[0, 255]
转换为[0, 1]
。
- 根据
-
- 根据
img_norm_cfg
中的mean/std/to_rgb
处理输入图片。
- 根据
-
- 根据
size_divisor
进行pad操作
- 根据
-
- tranpose操作,将
h, w, c
转换为c, h, w
- tranpose操作,将
-
- stack 操作,将若干图片stack到一起,形成
N, C, H, W
形式图片。
- stack 操作,将若干图片stack到一起,形成
-
- resize/crop 操作详解(以下各类切片操作根据优先级来定义)。
oversample == 'three_crop'
:同一张图片中切取三块。- 要求crop size和输入图片的size的h或w相同。
- 切取上中下/左中右三张。
oversample == 'ten_crop'
:同一张图片切取10张。- 切取左上、右上、左下、右下、正中5张。
- 镜像上面5张。
resize_crop
:先随机crop再resize。- 根据比例进行,即需要指定面积的范围
scale
和长宽比例范围ratio
。 - 总体过程就是,先根据范围随机获取crop的面积以及长宽比例,从而计算长宽,最后看看长宽是否超出img原有的范围,如果没有超出就crop出这块区域并resize到目标的尺寸。
- 根据比例进行,即需要指定面积的范围
rescale_crop
:先resize再随机crop。- 随机获取短边的长度,按比例resize图片,然后随机切片。
- 短片长度是取一个范围。
multiscale_crop
:先crop再resize。- 首先指定scales(都<=1),作为需要crop的长/宽比例,分别计算对应的长宽。
- 构建长/宽对(pair)。注意,scales后长/宽的数量都是
len(scales)
,也可以理解为,scales后的长宽都有一个所谓的编号,通过max_distort
可以限制长宽编号插值的最大值。 - 随机选择一对长/宽。选定长宽后需要选择改crop在图像中的位置。有两种选择方式:
- 随机选择:这个没什么好说的,在可选的范围内随机选择。
- 固定位置(即设置参数
fix_crop
):如果more_fix_crop
为False,则有5个固定位置(左上、左下、右上、右下、正中),如果more_fix_crop
为True,则还要增加8个位置。
3. dataloader 构建
-
目标:通过输入
Dataset
对象以及其他参数构建Dataloader
对象。 -
主要功能:
- 设置shuffle
- 设置多GPU相关参数
- 设置集群相关参数
- 设置其他
Dataloader
参数
-
相关代码:
mmaction.datasets.loader.buil_loader.py
中的build_dataloader
函数。 -
单机多GPU、集群相关功能都是通过实现特定
Sampler
来处理的,没看细节。 -
相关参数(参数都是顾名思义,也不多解释了)
imgs_per_gpu
workers_per_gpu
num_gpus