mmaction 数据相关源码阅读

文章目录

    • 0. 前言
    • 1. 数据集预处理
      • 1.1. 需要进行什么预处理
      • 1.2. 预处理的结果是什么
    • 2. 构建Dataset对象
      • 2.1. 构建过程概述
      • 2.2. 配置文件详解
      • 2.3. 帧提取策略
      • 2.4. 数据预处理与数据增强
    • 3. dataloader 构建


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的相关信息。
        • 这一步是数据集相关的,如果要新增数据集就要重写一个函数
        • 每个样本信息包括了vidlabelvid表示的是样本id,一般通过样本相对路径来表示(如果样本是视频,一般不包括扩展名),label是行为标签(数字)。
      • 关联现有样本信息应有样本信息,构建目标信息,并写入文件。
        • 关联的过程也很简单,就是查看应有样本信息现有样本信息中是否存在,如果存在写入一条信息(如果是帧样本则包含的信息是vid、rgb帧数量、光流帧数量、标签,如果是视频样本则包含的信息是vid、标签)。
        • 有一点疑问,在对应的方法中连续两次调用了 random.shuffle 分别处理光流和RGB,这样可以吗?

1.2. 预处理的结果是什么

  • 其实就是为mmaction构建Dataset对象提供数据源。
  • 主要用到的就是上面说到的输入数据列表。

2. 构建Dataset对象

  • 主要内容:
    • 构建过程概述
    • 配置文件详解

2.1. 构建过程概述

  • 通过分析 tools/train_recognizer.pytools/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支持的数据库形式。
    • 本文关注的行为识别数据集,主要相关的类就是 VideoDatasetRawFramesDataset 两个类。

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对象的参数。
  • 由于 VideoDatasetRawFramesDataset 的构造器基本上相同,所以这里先同一介绍下相同的部分。
  • 配置举例(帧提取策略在后面会详细介绍):
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
    • 连续获取 new_length 帧图片,每隔 new_step 帧获取,即在视频的连续 new_length * new_step 帧图片中获取 new_length 帧图片作为输入。
      • 这种方式在除TSN外其他模型中用得比较多,参数基本上是num_segments=1, new_length>1
  • RawFramesDatasetVideoDataset中的帧提取策略都是相同的,不同之处在于获取帧的方式。
    • 前者直接读取帧图片文件,后者通过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_moderandom_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
  • 获取帧下标具体实现方式
      1. 对于训练时(test_mode=False and random_shift=True),初步获取帧下标的流程是:
      • 计算每个segment的平均帧数。
        • 这个平均帧数是通过 (num_frames - old_length + 1)// num_segments
        • 换句话说,此时提取的帧下标最大值不会超过 num_frames - old_length
      • 如果平均帧数大于1,那么每个segment中可能不止一帧,随机获取其中一帧的下标。
      • 如果平均帧数小于1,且本样本中帧的数量大于num_segmentsold_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 次。
      1. 对于验证时(test_mode=False and random_shift=False),初步获取帧下标的流程是:
      • num_frames > num_segments + old_length - 1 时,将每个样本的前num_frames - old_length平均分为 num_segments 个部分,获取每个 segement 中间帧的下标。
      • 其他时候,每次提取第一帧,提取num_segments次。
      1. 对于测试时(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
        • 可能会存在重复提取帧的情况。
      • 其他时候,每次提取第一帧,提取num_segments次。
      1. 实现 temporal jitter。
      • 通过前面三步都初步获取了 num_segments 个帧下标。
      • 这一步是对这些下标进行微调,即每个下标随机加上[0, new_step)
      • 也就是说,temporal jitter 只在new_step > 1 的时候生效。
      • 返回一个 skip_offsets,用于后面的操作。
  • 通过帧下标获取帧信息流程
      1. 由于每个segment都对应了一个帧下标,所以首先对每个segment分别进行操作。
      1. 每个segment分别提取 new_length 帧图片,帧图片下标通过当前segment的帧下标以及skip_segmetns来获取。
      • 从当前segment的帧图片id开始,每new_step获取一次图片(再加上对应的skip_segments),一共获取new_length张图片。
  • 举例:
    • 如果某个样本共有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
    • 如果某个样本共有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

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 实现。
  • 主要流程:
      1. resize/crop:如果设置了resize_crop/rescale_crop则直接执行(后面会介绍这两种方式的细节),否则就根据输入的img_scale/keep_ratio对所有图片进行resize、再执行其他几种可能的resize/crop操作(包括oversample/multiscale_crop/centercrop,后面会介绍细节)
      1. 根据flip_ratio参数水平镜像图片。
      1. 根据div_255参数将图片从[0, 255]转换为[0, 1]
      1. 根据img_norm_cfg中的mean/std/to_rgb处理输入图片。
      1. 根据size_divisor进行pad操作
      1. tranpose操作,将h, w, c转换为c, h, w
      1. stack 操作,将若干图片stack到一起,形成N, C, H, W形式图片。
  • 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

你可能感兴趣的:(PyTorch)