DatasetMapper作为dectectron2中加载数据的中间工具,根据数据路径载入数据、进行转换处理(包括数据增强(data augmentation)、标注格式化等)并输出合适类型以匹配训练过程对DataLoader输出要求。
DatasetMapper位于detectron2.data.dataset_mapper中。
class
detectron2.data.DatasetMapper
(*args, **kwargs)基类:object
一种以Detectron2 Dataset的字典(dict)格式为输入,以模型所需格式为映射输出的callable对象。
在需要将你的数据集字典转换为训练数据时,该类为缺省选择。若构建诸如读取或变换图像的自定义逻辑,你可能需要以该类为蓝本实现。详见Dataloader。
原文:detectron2.data — detectron2 0.4 documentation
DatasetMapper并不是必须的,无论从任何角度来讲。DatasetMapper严格来说只是提供了detectron2框架下的mapper的一种模板,至于mapper的作用,就是上面一句话所概括的,对数据进行处理。不过,mapper是必须的,mapper的上层是数据集(Dataset),下层是数据加载器(DataLoader),上层只提供数据加载地址和标注信息,而下层则需要完整的数据,因此mapper必须负责将数据载入内存,而数据增强只是可选操作。
mapper要求一个callable对象,可以是函数、lambda,或者任何实现了调用器__call__的类实例(DatasetMapper就是这样的类型)。你完全可以参照DatasetMapper,另外构建自定义的数据映射函数,而无需对DatasetMapper做任何继承、复用操作。
本文从DatasetMapper的代码流程,帮助了解detectron2如何做数据增强。
Pytorch的DataLoader类
在Pytorch框架下,torch.utils.data.DataLoader是一个迭代器。依赖一个torch.utils.data.Dataset的列表对象,继承了data.Dataset的类必须重写索引器__getitem__,以便由下标获取数据,而数据增强则在索引器返回之前完成。(或者依赖一个torch.utils.data.IterableDataset的迭代对象,其子类需重写__iter__用以产生迭代器,数据增强在迭代器__next__返回之前完成。Dataset和IterableDataset只是获取数据方式不同,在结构中的地位及作用没有区别)
图:DataLoader的基本结构,图中带有斜体名称的为DataLoader初始化参数(除mapper)。当使用iter内建函数作用于DataLoader实例时,DataLoader会使用其内部的Sampler实例生成迭代器(可能会用到Generator,这与随机数生成有关),当使用next内建函数作用于迭代器实例时(若为多线程迭代器,worker_init_fn才会生效),会产生若干个索引,并从Dataset实例中获取数据,经过collate_fn校验之后返回数据。
DatasetMapper位于上图中以橙色填充的方框处,mapper并不是DataLoader结构内的成员,与DatasetMapper配套的是detectron2.data.common.MapDataset。换言之,MapDataset是Dataset的一种实现,而DatasetMapper(或mapper)是MapDataset的参数。
__init__(
self,
is_train: bool,
*,
augmentations: List[Union[T.Augmentation, T.Transform]],
image_format: str,
use_instance_mask: bool = False,
use_keypoint: bool = False,
instance_mask_format: str = "polygon",
keypoint_hflip_indices: Optional[np.ndarray] = None,
precomputed_proposal_topk: Optional[int] = None,
recompute_boxes: bool = False,
):
DatasetMapper的构造器和detectron2中许多类一样,采用configurable装饰,使其支持CfgNode类型参数包。具体可以参考detectron2中的configurable函数——Python装饰器实例。
DatasetMapper的__call__以Detectron2 Dataset格式字典为输入。在对这个承载了单个数据信息的字典进行处理之前,需要先了解Detectron2 Dataset格式字典的具体含义。
detectron2的Standard Dataset Dicts
标准数据集字典
对标准任务(实例检测,实例/语义/全景分割,关键点检测),我们以类似于COCO标注的规范将原始数据集加载到list[dict]实例中。以下是一种数据集的标准表示。
每个字典包含一张图像的完整信息。字典可能会包含以下的部分字段,所需字段根据dataloader或任务的需要而不同(详见后文)。
任务 字段 公有 file_name, height,width,image_id 实例检测/分割 annotations 语义分割 sem_seg_file_name 全景分割 pan_seg_file_name, segments_info
file_name
:图像文件的完整路径。
height
,width
:整数。图像的形状。
image_id
(str或int):检索该图像的唯一id,许多评估工具在辨识图片时需要用到,但数据集可能有其他用途。
annotations
(list[dict]):实例检测/分割或关键点检测任务必选字段。每个字典保存该图中单个实例的标注信息,可能包含以下键:
bbox
(list[float],必选):由代表实例边界框的4个数字组成的列表。
bbox_mode
(int,必选):bbox的格式。必须是structures.BoxMode的成员。目前可选:BoxMode.XYXY_ABS
(译注:即四个数字代表[xmin, ymin, xmax, ymax]
,均为绝对坐标),BoxMode.XYWH_ABS
(译注:即四个数字代表[xmin, ymin, width, height]
,均为绝对坐标)。
category_id
(int,必选):范围在[0, num_categories-1]内的整数,代表类别标签。num_categories这个值用以表示“背景”类(若需要的话)。(译注:这表示detectron2的类别标签“0”并不代表“背景”,而最大值才可能代表“背景”)
segmentation
(list[list[float]或dict]):实例分割掩码。
- 如果类型为
list[list[float]]
,则为代表多边形的列表,每个元素表示一个对象的连结边界。每个list[float]
是一个形如[x1, y1, ..., xn, yn]
(n≥3)的简单多边形。其中每点的x和y均为绝对坐标。- 如果类型为
dict
,则代表COCO的压缩RLE格式的逐像素分割掩码。该dict应当包含键"size"和"counts"。你可以使用语句pycocotools.mask.encode(np.asarray(mask,order='F'))
将该dict转换为0/1格式的uint8分割掩码。使用默认的DataLoader处理这种格式掩码,必须将cfg.INPUT.MASK_FORMAT
设置为bitmask
。
keypoints
(list[float]):格式为[x1, y1, v1, ..., xn, yn, vn]
。v[i]表示第i个关键点是否可见。n
必须等于关键点种类数。每点的x和y均为绝对实数坐标,且在范围[0, W或H]内。(注意COCO格式关键点坐标是范围在[0, W-1或H-1]的整数对,有别于我们的标准格式。Detectron2为每个COCO关键点坐标加上0.5,以将其从离散像素索引转换为浮点坐标。)
iscrowd
:0(缺省)或1。指示该实例是否等同于COCO标注中的"crowd region"。如果该标注对于你或你的数据集是未知的,请勿加入这个字段。如果
annotations
是空列表,意味着该图像被标记为不含目标实例。训练阶段这类图像默认情况下会被移除,但可以通过配置DATALOADER.FILTER_EMPTY_ANNOTATIONS
来避免。
sem_seg_file_name
(str):语义分割真值文件的完整路径。要求像素值为整型的灰度图。
pan_seg_file_name
(str):全景分割真值文件的完整路径。要求像素值为panopticapi.utils.id2rgb编码的整型id的RGB图(译注:此处所谓id实际上就是以24位rgb存储的整型。低8位为r,高八位为b,详见id2rgb源码)。id由segments_info
定义,若id未在segments_info
中定义,其对应像素将视为无标签,通常在训练&评估阶段将被忽略。
segments_info
(list[dict]):定义全景分割真值中每种id的含义。每个dict包含以下键:
id
(int):出现在真值图中的整型。category_id
(int):范围在[0, num_categories-1]的整数,代表类别标签。iscrowd
:0(缺省)或1。指示该实例是否等同于COCO标注中的"crowd region"。原文:Use Custom Datasets — detectron2 0.4 documentation
detectron2使用注册机制保存内置或用户自定义的数据集,对于每个数据集,其名称为键(key),要求对应的值(value)是一个callable对象,返回值应该是包含数据集全部数据的列表,列表元素类型为字典(dict),即上述标准数据集字典。当使用DatasetCatalog.get(dataset_name)
时(DatasetCatalog位于detectron2.data),会无参执行这个callable,并将其返回值返回。
官方文档对字典包含字段的解释较长,但根据作用来看,只有两类字段:图像与标注。在DatasetMapper中实际用到的,也只有图像与标注。file_name
记录了图像文件路径,annotations
记录了标注(语义分割的标注应为sem_seg_file_name
,全景分割的标注为pan_seg_file_name
和segments_info
)。
因此,从DatasetMapper中返回的字典,也只包含image
和instances
(实例分割会包含sem_seg
)。
注意width
和height
记录的信息与从file_name
中读取的图像会做一个相互检查,额外加入width
和height
字段只是方便某些场景计算相对坐标或恢复绝对坐标,要知道为了取得长宽信息而将一整张图像读入内存是很耗时且不必要的操作。
如DatasetMapper文档所述,其__call__实现包括三个步骤。若用户自定义mapper,也可以参考这三个步骤:
class
detectron2.data.DatasetMapper
(****args,**kwargs***)该callable对象当前执行下列操作:
- 从"file_name"读取图像
- 将裁剪/几何变换应用于图像和标注
- 将图像和标注分别转换为Tensor类和Instance类
原文:detectron2.data — detectron2 0.4 documentation
(调用器完整签名为:__call__(self, dataset_dict)
)
拷贝:此处建议对dataset_dict
深拷贝,防止篡改上层数据集变量。
dataset_dict = copy.deepcopy(dataset_dict)
读图:按照detectron2的标准,dataset_dict
的"file_name"字段保存了图像地址,可使用提供的库函数detectron2.data.dataset_utils.utils.read_image
读取,也可以自行用OpenCV或PIL读取。
image = utils.read_image(dataset_dict["file_name"], format=self.image_format)
此处可能需要考虑颜色格式,一般需要从cfg.INPUT.FORMAT
配置中读取。可根据调用的库函数约定cfg.INPUT.FORMAT
取值形式和范围,如utils.read_image要求为"BGR"或"YUV-BT.601"。
检查[可选]:此处建议调用detectron2.data.dataset_utils.utils.check_image_size
检查图像尺寸与dataset_dict中的尺寸是否一致,若dataset_dict中并无width
或height
字段,该函数会为其添加。
utils.check_image_size(dataset_dict, image)
detectron2的Augmentation和AugInput
detectron2中数据增强依赖于两种抽象类型,Augmentation和Transform(分别位于detectron2.data.transforms.augmentation
和fvcore.transforms.transform
),更进一步地,Augmentation依赖于Transform。
Augmentation具体实现类:提供一种数据增强手段。
Transform具体实现类:提供一种图像/坐标转换方式。
举个例子,“图像旋转”是一种Transform,指定一个旋转角度,即可以对输入图像进行旋转并返回处理后图像(输入坐标也可);而“随机图像旋转”是一种Augmentation,它依赖于“图像旋转”,对于输入图像,它生成一个随机数,构造“图像旋转”的Transform实例,并对图像进行处理,返回该Transform实例。(顺带一提,如果随机角度为0,会返回NoOpTransform
实例,也就是啥都不干。这种行为在那种要么做要么不做的随机增强中经常会出现,如随机镜像)
注意到Augmentation需要对传入的图像进行原地更改,因此直接传图像引用很难实现(形参是实参的引用副本,共同指向同一内存,而函数内部对形参的赋值,不会对实参引用的指向产生作用),需要对图像进行“装箱”。于是detectron2引入了AugInput类(位于detectron2.data.transforms.augmentation
),作用为打包图像、标注等实例。
上述例子调用过程可以用如下框图概括:
图:Augmentation示例。以AugInput实例调用Augmentation的__call__,Augmentation会调用get_transform生成特定的Transform实例,其间用到AugInput中何种数据,取决于get_transform的签名;然后Augmentation会根据AugInput的成员,分别调用Transform中对应的apply函数,生成转换后的数据,并覆盖AugInput的成员。
为什么Augmentation要返回Transform实例呢?实际上,AugInput仅支持image、boxes、sem_seg三种成员,分别对应Transform中的apply_image、apply_box、apply_segmentation处理函数,因此AugInput的处理场景是十分有限的。通常对于不同任务可能会需要进行特定的转换处理操作,返回Transform会方便处理。当然也可以通过继承AugInput并重写AugInput.transform函数实现(又见Extend T.AugInput)。
def transform(self, tfm: Transform) -> None:
self.image = tfm.apply_image(self.image)
if self.boxes is not None:
self.boxes = tfm.apply_box(self.boxes)
if self.sem_seg is not None:
self.sem_seg = tfm.apply_segmentation(self.sem_seg)
// TODO: 其他成员操作
以下给出detectron2中内置的Augmentation实现类介绍(如无特殊说明,插值方式为PIL插值枚举;相对值及比重取值范围为[0, 1]):
类名 | 描述 | 参数 | 依赖Transform |
---|---|---|---|
RandomApply | 给定数据增强方法(Augmentation或Transform)及概率,按概率应用增强方法 | 1.方法 2.概率 |
无 |
RandomBrightness | 随机调整图像亮度。生成范围内随机比例,图像逐像素乘上该比例。(大于1增亮、小于1暗化,若图像不属于RGB色彩,处理效果未知) | 1.范围下界 2.范围上界 |
BlendTransform |
RandomContrast | 随机调整图像对比度。生成范围内随机比例,图像逐像素与图像平均色按比例混合。(大于1提高对比度,小于1降低对比度,若图像不属于RGB色彩,处理效果未知) | 1.范围下界 2.范围上界 |
BlendTransform |
RandomCrop | 随机位置裁剪小图(不过边界)。小图大小为(float, float)元组,有4种解析方式:相对值(relative)、相对范围(relative_range)、绝对值(absolute)、绝对范围(absolute_range)。 | 1.解析方式 2.小图大小 |
CropTransform |
RandomExtent | 随机位置、尺寸裁剪小图。指定裁剪窗的边长范围(相对值)和横纵位移上界(相对值),随机缩放裁剪框(以图中心为锚点)并随机生成横纵位移(每轴均为双向位移,各向不会超过上界的一半),以裁剪窗裁剪图像。若裁剪窗越过边界,则像素以0补偿。 | 1.边长范围 2.位移上界 |
ExtentTransform |
RandomFlip | 随机水平/垂直镜像翻转。指定概率和翻转方向,按概率及方向翻转图像。 | 1.概率 2.水平 3.垂直 |
HFlipTransform VFlipTransform NoOpTransform |
RandomSaturation | 随机调整图像饱和度。生成范围内随机饱和度,调整图像饱和度。(大于1提高饱和度,小于1降低饱和度,要求输入图像颜色通道顺序为R、G、B)。 | 1.范围上界 2.范围下界 |
BlendTransform |
RandomLighting | AlexNet论文中提出的RGB通道光强调整操作。在ImageNet训练集中使用主成分分析(PCA)得到RGB协方差矩阵的3*3特征向量组及3*1特征值组,特征值组逐元素乘上均值为0、给定标准差的正态分布随机值,将特征向量组和特征值组向量相乘得到RGB增量,对图像逐像素加上该增量。该方案可能相当于掌握了自然图像的一个重要属性,即目标特征不受光强及颜色变化影响。(要求输入图像颜色通道顺序为R、G、B。) | 1.正态分布标准差 | BlendTransform |
RandomRotation | 随机旋转图像。指定插值方式、旋转中心,并生成范围内随机旋转角度(或随机选择候选角度池中的角度,由随机采样方式决定),对图像进行旋转。可设置是否拉伸画布并补0以容纳旋转后的图像边角。(注意:此处插值方式为OpenCV2插值枚举) | 1.角度(范围或池) 2.是否拉伸 3.旋转中心 4.采样方式 5.插值方式 |
RotationTransform NoOpTransform |
Resize | 调整图像大小。可指定插值方式。 | 1.尺寸 2.插值方式 |
ResizeTransform |
ResizeShortestEdge | 按短边调整图像大小。生成范围内随机短边长度(或随机选择候选短边池中的短边长度,由采样方式决定),保持纵横比调整图像大小,若长边长度超过上界,等比缩小图像直到长边等于上界。缩放可指定插值方式。 | 1.短边(范围或池) 2.长边上界 3.采样方式 4.插值方式 |
ResizeTransform NoOpTransform |
RandomCrop_ CategoryAreaConstraint |
带单类面积比重约束的随机位置图像裁剪,仅用于实例分割。类似于RandomCrop,但引入单类面积比重上界,判断随机裁剪位置中各类分割掩码面积比重,当某类比重越过上界时,重新随机(当仅含一类时,也重新随机;可设置忽略某类)。以上过程最多进行10次,仍未满足时采用第十次裁剪结果。 | 1.解析方式 2.小图大小 3.单类面积比重上界 |
CropTransform |
数据装箱:将image和其他标注信息包装进AugInput。
boxes = [BoxMode.convert(obj["bbox"], obj["bbox_mode"], BoxMode.XYXY_ABS) for obj in dataset_dict["annotations"]]
aug_input = AugInput(image, boxes=boxes)
当运行阶段不属于训练阶段时(如推理、评估),输出字典不需要包含标注。
注意到boxes需要进行格式转换,生成边框格式为XYXY_ABS
的list[list]
二维数组(第二维为4),才可以在下一步正确地被数据增强过程识别,并且此处将边框坐标(bbox字段)从标注字典中单独提取出来进行转换,后续还需要与剩余字段(bbox_mode和icategories_id等)再组合,如果标注并不仅包含边框,那么代码复杂度还会进一步提升,因此DatasetMapper并没有使用这种写法,也不建议使用这种写法,实际写法将在下一步末尾给出。
数据增强:调用Augmentation具体实现类进行数据增强。
transforms = self.augmentations(aug_input)
image, boxes = aug_input.image, aug_input.boxes
for obj, box in zip(dataset_dict["annotations"], boxes):
obj["bbox"] = box
obj["bbox_mode"] = BoxMode.XYXY_ABS
annos = dataset_dict["annotations"]
以上写法比较直观,但仅处理了标注中的边框。实际上,detectron2.data.dataset_utils.utils提供了一个处理标注的方法transform_instance_annotations
,可以使用Transform处理单个标注字典中的"bbox"、“segmentation”、"keypoints"标注信息,使用该方法进行数据增强会使代码更加简洁。
# 数据装箱
aug_input = AugInput(image)
# 数据增强
transforms = self.augmentations(aug_input)
image = aug_input.image
image_shape = image.shape[:2]
annos = [transform_instance_annotations(obj, transforms, image_shape) for obj in dataset_dict["annotations"]]
再次提醒,非训练阶段可以不对标注进行转换。以上代码仍然没有考虑语义分割和全景分割标注的情况,需要根据实际任务做出调整。
个人见解:detectron2对于AugInput的设计定位太不明确。于内,其本身处理数据增强的行为不够简洁,于外,transform_instance_annotations
可以替代它的许多功能。这使得AugInput稍显尴尬,又不得不用。
对于上一步的处理结果,图像需要按照*[C, H, W]*的维度顺序转换成Tensor类型,标注需要转换成Instance类型,分别作为结果字典的"image"、"instances"键的值。
detectron2的Instance
对于Instance类型,本文不做过多赘述。主要由于Instance的外部特性和用途均相当于字典dict,均为依据字段检索标注内容。
Instance与dict区别
d["gt_boxes"]
方式检索,而Instance实例须以inst.gt_boxes
方式检索)。Instance以逐字段保存标注信息,而不是逐实例保存标注信息
如从Instance实例中获取gt_boxes,得到的是图像中全部实例目标的边框信息。
Instance保存的值多为detectron2.structures中的类型
detectron2.structures中定义了一些管理特定标注结构的类,如Boxes(管理多个边框)、BitMasks(管理多个掩码)、Keypoints(管理多个关键点)。而Instance中gt_boxes、gt_maks、gt_keypoints等字段将存放这些类型的实例。这些类型大多表现出来的特性仅为对Tensor的简单封装,因此这里也不过多赘述。
对于结果生成,detectron2提供了非常简便的函数annotations_to_instance
(位于detectron2.data.dataset_utils.utils),支持将包含"bbox"、“segmentation”、“keypoints"的标注字典转换为Instance实例。(如果包含"segmentation”,此处可能需要指定分割标注类型mask_format
参数,可选为"polygon"或"bitmask")
instances = annotations_to_instances(annos, image_shape)
dataset_dict["instance"] = instances
return dataset_dict
这篇文章简单地总结了一下detectron2中的DatasetMapper用法和实现逻辑,及数据增强相关设计细节。文中实现逻辑依据于detectron2 v0.4源码及detectron2官方文档,除此之外没有参考任何关于detectron2的文章或书籍,具有一定程度的主观性及缺乏实验性,如有错误和理解不到之处,欢迎指出探讨。