Skip to content

Commit

Permalink
[Docs] Add data transform docs. (#1723)
Browse files Browse the repository at this point in the history
* Add data transfrom docs

* Convert all Chinese paragraph in oneline to prevent some rendering bug
in Chrome/Edge. (It looks well in Firefox :/)

* Update docs

* Update docs

* Imporve docs according to comments

* Imporve docs according comments
  • Loading branch information
mzr1996 authored and zhouzaida committed Jul 31, 2022
1 parent bad822d commit 4a7a2c7
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/zh_cn/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
understand_mmcv/runner.md
understand_mmcv/io.md
understand_mmcv/data_process.md
understand_mmcv/data_transform.md
understand_mmcv/visualization.md
understand_mmcv/cnn.md
understand_mmcv/ops.md
Expand Down
274 changes: 274 additions & 0 deletions docs/zh_cn/understand_mmcv/data_transform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# 数据变换

在 OpenMMLab 算法库中,数据集的构建和数据的准备是相互解耦的。通常,数据集的构建只对数据集进行解析,记录每个样本的基本信息;而数据的准备则是通过一系列的数据变换,根据样本的基本信息进行数据加载、预处理、格式化等操作。

## 数据变换的设计

在 MMCV 中,我们使用各种可调用的数据变换类来进行数据的操作。这些数据变换类可以接受若干配置参数进行实例化,之后通过调用的方式对输入的数据字典进行处理。同时,我们约定所有数据变换都接受一个字典作为输入,并将处理后的数据输出为一个字典。一个简单的例子如下:

```python
>>> import numpy as np
>>> from mmcv.transforms import Resize
>>>
>>> transform = Resize(scale=(224, 224))
>>> data_dict = {'img': np.random.rand(256, 256, 3)}
>>> data_dict = transform(data_dict)
>>> print(data_dict['img'].shape)
(224, 224, 3)
```

数据变换类会读取输入字典的某些字段,并且可能添加、或者更新某些字段。这些字段的键大部分情况下是固定的,如 `Resize` 会固定地读取输入字典中的 `"img"` 等字段。我们可以在对应类的文档中了解对输入输出字段的约定。

MMCV 为所有的数据变换类提供了一个统一的基类 (`BaseTransform`):

```python
class BaseTransform(metaclass=ABCMeta):

def __call__(self, results: dict) -> dict:

return self.transform(results)

@abstractmethod
def transform(self, results: dict) -> dict:
pass
```

所有的数据变换类都需要继承 `BaseTransform`,并实现 `transform` 方法。`transform` 方法的输入和输出均为一个字典。在**自定义数据变换类**一节中,我们会更详细地介绍如何实现一个数据变换类。

## 数据流水线

如上所述,所有数据变换的输入和输出都是一个字典,而且根据 OpenMMLab 中 [有关数据集的约定](TODO),数据集中每个样本的基本信息都是一个字典。这样一来,我们可以将所有的数据变换操作首尾相接,组合成为一条数据流水线(data pipeline),输入数据集中样本的信息字典,输出完成一系列处理后的信息字典。

以分类任务为例,我们在下图展示了一个典型的数据流水线。对每个样本,数据集中保存的基本信息是一个如图中最左侧所示的字典,之后每经过一个由蓝色块代表的数据变换操作,数据字典中都会加入新的字段(标记为绿色)或更新现有的字段(标记为橙色)。

<div align=center>
<img src="https://user-images.githubusercontent.com/26739999/154197953-bf0b1a16-3f41-4bc7-9e67-b2b9b323d895.png" width="90%"/>
</div>

在配置文件中,数据流水线是一个若干数据变换配置字典组成的列表,每个数据集都需要设置参数 `pipeline` 来定义该数据集需要进行的数据准备操作。如上数据流水线在配置文件中的配置如下:

```python
pipeline = [
dict(type='LoadImageFromFile'),
dict(type='Resize', size=256, keep_ratio=True),
dict(type='CenterCrop', crop_size=224),
dict(type='Normalize', mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375]),
dict(type='ClsFormatBundle')
]

dataset = dict(
...
pipeline=pipeline,
...
)
```

## 常用的数据变换类

按照功能,常用的数据变换类可以大致分为数据加载、数据预处理与增强、数据格式化。在 MMCV 中,我们提供了一些常用的数据变换类如下:

### 数据加载

为了支持大规模数据集的加载,通常在 `Dataset` 初始化时不加载数据,只加载相应的路径。因此需要在数据流水线中进行具体数据的加载。

| class | 功能 |
| :---------------------------: | :---------------------------------------: |
| [`LoadImageFromFile`](TODO) | 根据路径加载图像 |
| [`LoadAnnotation`](TODO) | 加载和组织标注信息,如 bbox、语义分割图等 |

### 数据预处理及增强

数据预处理和增强通常是对图像本身进行变换,如裁剪、填充、缩放等。

| class | 功能 |
| :------------------------------: | :--------------------------------: |
| [`Pad`](TODO) | 填充图像边缘 |
| [`CenterCrop`](TODO) | 居中裁剪 |
| [`Normalize`](TODO) | 对图像进行归一化 |
| [`Resize`](TODO) | 按照指定尺寸或比例缩放图像 |
| [`RandomResize`](TODO) | 缩放图像至指定范围的随机尺寸 |
| [`RandomMultiscaleResize`](TODO) | 缩放图像至多个尺寸中的随机一个尺寸 |
| [`RandomGrayscale`](TODO) | 随机灰度化 |
| [`RandomFlip`](TODO) | 图像随机翻转 |
| [`MultiScaleFlipAug`](TODO) | 支持缩放和翻转的测试时数据增强 |

### 数据格式化

数据格式化操作通常是对数据进行的类型转换。

| class | 功能 |
| :---------------------------: | :--------------------------------: |
| [`ToTensor`](TODO) | 将指定的数据转换为 `torch.Tensor` |
| [`ImageToTensor`](TODO) | 将图像转换为 `torch.Tensor` |


## 自定义数据变换类

要实现一个新的数据变换类,需要继承 `BaseTransform`,并实现 `transform` 方法。这里,我们使用一个简单的翻转变换(`MyFlip`)作为示例:

```python
import random
import mmcv
from mmcv.transforms import BaseTransform, TRANSFORMS

@TRANSFORMS.register_module()
class MyFlip(BaseTransform):
def __init__(self, direction: str):
super().__init__()
self.direction = direction

def transform(self, results: dict) -> dict:
img = results['img']
results['img'] = mmcv.imflip(img, direction=self.direction)
return results
```

从而,我们可以实例化一个 `MyFlip` 对象,并将之作为一个可调用对象,来处理我们的数据字典。

```python
import numpy as np

transform = MyFlip(direction='horizontal')
data_dict = {'img': np.random.rand(224, 224, 3)}
data_dict = transform(data_dict)
processed_img = data_dict['img']
```

又或者,在配置文件的 pipeline 中使用 `MyFlip` 变换

```python
pipeline = [
...
dict(type='MyFlip', direction='horizontal'),
...
]
```

需要注意的是,如需在配置文件中使用,需要保证 `MyFlip` 类所在的文件在运行时能够被导入。

## 变换包装

变换包装是一种特殊的数据变换类,他们本身并不操作数据字典中的图像、标签等信息,而是对其中定义的数据变换的行为进行增强。

### 字段映射(Remap)

字段映射包装(`Remap`)用于对数据字典中的字段进行映射。例如,一般的图像处理变换都从数据字典中的 `"img"` 字段获得值。但有些时候,我们希望这些变换处理数据字典中其他字段中的图像,比如 `"gt_img"` 字段。

如果配合注册器和配置文件使用的话,在配置文件中数据集的 `pipeline` 中如下例使用字段映射包装:

```python
pipeline = [
...
dict(type='Remap',
input_mapping={'img': 'gt_img'}, # 将 "gt_img" 字段映射至 "img" 字段
inplace=True, # 在完成变换后,将 "img" 重映射回 "gt_img" 字段
transforms=[
# 在 `RandomFlip` 变换类中,我们只需要操作 "img" 字段即可
dict(type='RandomFlip'),
])
...
]
```

利用字段映射包装,我们在实现数据变换类时,不需要考虑在 `transform` 方法中考虑各种可能的输入字段名,只需要处理默认的字段即可。

### 随机选择(RandomChoice)

随机选择包装(`RandomChoice`)用于从一系列数据变换组合中随机应用一个数据变换组合。利用这一包装,我们可以简单地实现一些数据增强功能,比如 AutoAugment。

如果配合注册器和配置文件使用的话,在配置文件中数据集的 `pipeline` 中如下例使用随机选择包装:

```python
pipeline = [
...
dict(type='RandomChoice',
pipelines=[
[
dict(type='Posterize', bits=4),
dict(type='Rotate', angle=30.)
], # 第一种随机变化组合
[
dict(type='Equalize'),
dict(type='Rotate', angle=30)
], # 第二种随机变换组合
],
pipeline_probs=[0.4, 0.6] # 两种随机变换组合各自的选用概率
)
...
]
```

### 多目标扩展(ApplyToMultiple)

通常,一个数据变换类只会从一个固定的字段读取操作目标。虽然我们也可以使用 `Remap` 来改变读取的字段,但无法将变换一次性应用于多个字段的数据。为了实现这一功能,我们需要借助多目标扩展包装(`ApplyToMultiple`)。

多目标扩展包装(`ApplyToMultiple`)有两个用法,一是将数据变换作用于指定的多个字段,二是将数据变换作用于某个字段下的一组目标中。

1. 应用于多个字段

假设我们需要将数据变换应用于 `"lq"` (low-quanlity) 和 `"gt"` (ground-truth) 两个字段中的图像上。

```python
pipeline = [
dict(type='ApplyToMultiple',
# 分别应用于 "lq" 和 "gt" 两个字段,并将二者应设置 "img" 字段
input_mapping={'img': ['lq', 'gt']},
# 在完成变换后,将 "img" 字段重映射回原先的字段
inplace=True,
# 是否在对各目标的变换中共享随机变量
# 更多介绍参加后续章节(随机变量共享)
share_random_param=True,
transforms=[
# 在 `RandomFlip` 变换类中,我们只需要操作 "img" 字段即可
dict(type='RandomFlip'),
])
]
```

2. 应用于一个字段的一组目标

假设我们需要将数据变换应用于 `"images"` 字段,该字段为一个图像组成的 list。

```python
pipeline = [
dict(type='ApplyToMultiple',
# 将 "images" 字段下的每张图片映射至 "img" 字段
input_mapping={'img': 'images'},
# 在完成变换后,将 "img" 字段下的图片重映射回 "images" 字段的列表中
inplace=True,
# 是否在对各目标的变换中共享随机变量
share_random_param=True,
transforms=[
# 在 `RandomFlip` 变换类中,我们只需要操作 "img" 字段即可
dict(type='RandomFlip'),
])
]
```

`ApplyToMultiple` 中,我们提供了 `share_random_param` 选项来支持在多次数据变换中共享随机状态。例如,在超分辨率任务中,我们希望将随机变换**同步**作用于低分辨率图像和原始图像。如果我们希望在自定义的数据变换类中使用这一功能,我们需要在类中标注哪些随机变量是支持共享的。

以上文中的 `MyFlip` 为例,我们希望以一定的概率随机执行翻转:

```python
from mmcv.transforms.utils import cacheable_method

@TRANSFORMS.register_module()
class MyRandomFlip(BaseTransform):
def __init__(self, prob: float, direction: str):
super().__init__()
self.prob = prob
self.direction = direction

@cacheable_method # 标注该方法的输出为可共享的随机变量
def do_flip(self):
flip = True if random.random() > self.prob else False
return flip

def transform(self, results: dict) -> dict:
img = results['img']
if self.do_flip():
results['img'] = mmcv.imflip(img, direction=self.direction)
return results
```

通过 `cacheable_method` 装饰器,方法返回值 `flip` 被标注为一个支持共享的随机变量。进而,在 `ApplyToMultiple` 对多个目标的变换中,这一变量的值都会保持一致。

0 comments on commit 4a7a2c7

Please sign in to comment.