mmdetection详解指北 (三)

数据处理

数据处理可能是炼丹师接触最为密集的了,因为通常情况,除了数据的离线处理,写个数据类,就可以炼丹了。但本节主要涉及数据的在线处理,更进一步应该是检测分割数据的 pytorch 处理方式。虽然 mmdet 将常用的数据都实现了,而且也实现了中间通用数据格式,但,这和模型,损失函数,性能评估的实现也相关,比如你想把官网的 centernet 完整的改成 mmdet风格,就能看到 (看起来没必要)。

CustomDataset

看看配置文件,数据相关的有 data dict,里面包含了 train,val,test 的路径信息,用于数据类初始化, 有 pipeline,将各个函数及对应参数以字典形式放到列表里,是对 pytorch 原装的 transforms+compose,在检测,分割相关数据上的一次封装,使得形式更加统一。

从 builder.py 中 builddataset 函数能看到,构建数据有三种方式,ConcatDataset,RepeatDataset 和从注册器中提取。其中 datasetwrappers.py中 ConcatDataset 和 RepeatDataset 意义自明,前者继承自 pytorch 原始的ConcatDataset,将多个数据集整合到一起,具体为把不同序列( 可参考容器的抽象基类) 的长度相加__getitem 函数对应 index 替换一下。后者就是单个数据类 (序列) 的多次重复。就功能来说,前者提高数据丰富度,后者可解决数据太少使得 loading 时间长的问题 (见代码注释)。而被注册的数据类在 datasets 下一些熟知的数据名文件中。其中,基类为 custom.py 中的 CustomDataset,coco 继承自它,cityscapes 继承自 coco,xml_style 的XMLDataset 继承 CustomDataset,然后 wider_face,voc 均继承自 XMLDataset。因此这里先分析一下CustomDataset。

CustomDataset 记录数据路径等信息,解析标注文件,将每一张图的所有信息以字典作为数据结构存在 results 中,然后进入pipeline: 数据增强相关操作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@DATASETS.register_module()
class CustomDataset(Dataset):
def __init__(...):
...
self.pipeline = Compose(pipeline)

def pre_pipeline(self, results):
"""Prepare results dict for pipeline."""
results['img_prefix'] = self.img_prefix
results['seg_prefix'] = self.seg_prefix
results['proposal_file'] = self.proposal_file
results['bbox_fields'] = []
results['mask_fields'] = []
results['seg_fields'] = []

def prepare_train_img(self, idx):
"""Get training data and annotations after pipeline.

Args:
idx (int): Index of data.

Returns:
dict: Training data and annotation after pipeline with new keys \
introduced by pipeline.
"""

img_info = self.data_infos[idx]
ann_info = self.get_ann_info(idx)
# 基本信息,初始化字典
results = dict(img_info=img_info, ann_info=ann_info)
if self.proposals is not None:
results['proposals'] = self.proposals[idx]
self.pre_pipeline(results)
return self.pipeline(results) # 数据增强等

这里数据结构的选取需要注意一下,字典结构,在数据增强库 albu 中也是如此处理,因此可以快速替换为 albu 中的算法。另外每个数据类增加了各自的 evaluate 函数。evaluate 基础函数在 mmdet.core.evaluation 中,后做补充。

mmdet 的数据处理,字典结构,pipeline,evaluate 是三个关键部分。其他所有类的文件解析部分,数据筛选等,看看即可。因为我们知道,pytorch读取数据,是将序列转化为迭代器后进行 io 操作,所以在 dataset 下除了pipelines 外还有 loader 文件夹,里面实现了分组,分布式分组采样方法,以及调用了 mmcv 中的 collate 函数 (此处为 1.x 版本,2.0 版本将 loader移植到了 builder.py 中),且 build_dataloader 封装的 DataLoader 最后在train_detector 中被调用,这部分将在后面补充,这里说说 pipelines。

data_config

返回 maskrcnn 的配置文件 (1.x,2.0 看 base config),可以看到训练和测试的不同之处:LoadAnnotations,MultiScaleFlipAug,DefaultFormatBundle 和 Collect。额外提示,虽然测试没有 LoadAnnotations,根据 CustomDataset 可知,它仍需标注文件,这和 inference 的 pipeline 不同,也即这里的 test 实为 evaluate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 序列中的dict可以随意删减,增加,属于数据增强调参内容
train_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True),
dict(type='Resize', img_scale=(1333, 800), keep_ratio=True),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size_divisor=32),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']),
]
test_pipeline = [
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(1333, 800),
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size_divisor=32),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img']),
])
]

最后这些所有操作被 Compose 串联起来,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@PIPELINES.register_module()
class Compose(object):
def __init__(self, transforms):
assert isinstance(transforms, collections.abc.Sequence) #列表是序列结构
self.transforms = []
for transform in transforms:
if isinstance(transform, dict):
transform = build_from_cfg(transform, PIPELINES)
self.transforms.append(transform)
elif callable(transform):
self.transforms.append(transform)
else:
raise TypeError('transform must be callable or a dict')

def __call__(self, data):
for t in self.transforms:
data = t(data)
if data is None:
return None
return data

def __repr__(self):
format_string = self.__class__.__name__ + '('
for t in self.transforms:
format_string += '\n'
format_string += f' {t}'
format_string += '\n)'
return format_string

上面代码能看到,配置文件中 pipeline 中的字典传入 buildfromcfg 函数,逐一实现了各个增强类 (方法)。扩展的增强类均需实现 __call 方法,这和 pytorch 原始方法是一致的。有了以上认识,重新梳理一下 pipelines 的逻辑,由三部分组成,load,transforms,和 format。load 相关的 LoadImageFromFile,LoadAnnotations都是字典 results 进去,字典 results 出来。具体代码看下便知,LoadImageFromFile 增加了’filename’,’img’,’img_shape’,’ori_shape’,’pad_shape’,’scale_factor’,’img_norm_cfg’ 字段。其中 img 是 numpy 格式。LoadAnnotations 从 results[’ann_info’] 中解析出 bboxs,masks,labels 等信息。注意 coco 格式的原始解析来自 pycocotools,包括其评估方法,这里关键是字典结构 (这个和模型损失函数,评估等相关,统一结构,使得代码统一)。transforms 中的类作用于字典的 values,也即数据增强。format 中的 DefaultFormatBundle 是将数据转成 mmcv 扩展的容器类格式 DataContainer。另外 Collect 会根据不同任务的不同配置,从 results 中选取只含 keys 的信息生成新的字典,具体看下该类帮助文档。

DataContainer

那么 DataContainer 是什么呢?它是对 tensor 的封装,将 results 中的 tensor 转成 DataContainer 格式,实际上只是增加了几个 property 函数,cpu_only,stack,padding_value,pad_dims,其含义自明,以及 size,dim用来获取数据的维度,形状信息。考虑到序列数据在进入 DataLoader 时,需要以 batch 方式进入模型,那么通常的 collate_fn 会要求 tensor 数据的形状一致。但是这样不是很方便,于是有了 DataContainer。它可以做到载入 GPU 的数据可以保持统一 shape,并被 stack,也可以不 stack,也可以保持原样,或者在非 batch 维度上做 pad。当然这个也要对 default_collate进行改造,mmcv 在 parallel.collate 中实现了这个。

collate_fn 是 DataLoader 中将序列 dataset 组织成 batch 大小的函数,这里帖三个普通例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def collate_fn_1(batch) :
# 这 是 默 认 的, 明 显batch中 包 含 相 同 形 状 的img\_tensor和 label
return tuple ( zip(*batch))

def coco_collate_2(batch) :
# 传 入 的batch数 据 是 被albu增 强 后 的(字 典 结 构)
imgs = [s [ ’image’ ] for s in batch]
annots = [s [ ’bboxes’ ] for s in batch]
labels = [s [ ’category_id’ ] for s in batch]
# 以 当 前batch中 图 片annot数 量 的 最 大 值 作 为 标 记 数 据 的 第 二 维 度 值, 空 出 的 就补−1
max_num_annots = max( len (annot) for annot in annots)
annot_padded = np. ones (( len (annots) , max_num_annots, 5))*−1
if max_num_annots > 0:
for idx , (annot , lab) in enumerate( zip (annots , labels )) :
if len (annot) > 0:
annot_padded[idx,:len(annot),:4] = annot
annot_padded[idx,:len(annot),2] += annot_padded[idx,:len(annot),0]
annot_padded[idx,:len(annot),3] += annot_padded[idx,:len(annot),1]
annot_padded[idx,:len(annot),:] /= 640
annot_padded[idx,:len(annot),4] = lab
return torch.stack(imgs,0), torch.FloatTensor(annot_padded)

def detection_collate_3(batch) :
targets = []
imgs = [ ]
for _, sample in enumerate(batch) :
for _, img_anno in enumerate(sample) :
if torch.is_tensor(img_anno) :
imgs.append(img_anno)
elif isinstance(img_anno, np.ndarray) :
annos = torch.from_numpy(img_anno).float()
targets.append(annos)
return torch.stack(imgs,0), targets # 做了stack,DataContainer可以不做 stack

以上就是数据处理的相关内容。最后再用 DataLoader 封装拆成迭代器,其相关细节,sampler 等暂略。

1
2
3
4
5
6
7
8
9
data_loader = DataLoader(
dataset,
batch_size=batch_size,
sampler=sampler,
num_workers=num_workers,
collate_fn=partial(collate, samples_per_gpu=samples_per_gpu),
pin_memory=False,
worker_init_fn=init_fn,
**kwargs)