"vscode:/vscode.git/clone" did not exist on "a21ed3afe22bbb61388d724df90206da63907905"
data_transform.md 15.5 KB
Newer Older
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 数据变换

在 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)   | 根据路径加载图像                          |
77
| [`LoadAnnotations`](TODO)      | 加载和组织标注信息,如 bbox、语义分割图等 |
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

### 数据预处理及增强

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

| 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` 类所在的文件在运行时能够被导入。

## 变换包装

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

Yining Li's avatar
Yining Li committed
153
### 字段映射(KeyMapper)
154

Yining Li's avatar
Yining Li committed
155
字段映射包装(`KeyMapper`)用于对数据字典中的字段进行映射。例如,一般的图像处理变换都从数据字典中的 `"img"` 字段获得值。但有些时候,我们希望这些变换处理数据字典中其他字段中的图像,比如 `"gt_img"` 字段。
156
157
158
159
160
161

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

```python
pipeline = [
    ...
Yining Li's avatar
Yining Li committed
162
    dict(type='KeyMapper',
163
164
165
166
        mapping={
            'img': 'gt_img',  # 将 "gt_img" 字段映射至 "img" 字段
            'mask': ...,  # 不使用原始数据中的 "mask" 字段。即对于被包装的数据变换,数据中不包含 "mask" 字段
        },
Yining Li's avatar
Yining Li committed
167
        auto_remap=True,  # 在完成变换后,将 "img" 重映射回 "gt_img" 字段
168
169
170
171
172
173
174
175
176
177
        transforms=[
            # 在 `RandomFlip` 变换类中,我们只需要操作 "img" 字段即可
            dict(type='RandomFlip'),
        ])
    ...
]
```

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

Yining Li's avatar
Yining Li committed
178
### 随机选择(RandomChoice)和随机执行(RandomApply)
179
180
181
182
183
184
185
186
187

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

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

```python
pipeline = [
    ...
    dict(type='RandomChoice',
Yining Li's avatar
Yining Li committed
188
        transforms=[
189
190
191
192
193
194
195
196
197
            [
                dict(type='Posterize', bits=4),
                dict(type='Rotate', angle=30.)
            ],  # 第一种随机变化组合
            [
                dict(type='Equalize'),
                dict(type='Rotate', angle=30)
            ],  # 第二种随机变换组合
        ],
Yining Li's avatar
Yining Li committed
198
        prob=[0.4, 0.6]  # 两种随机变换组合各自的选用概率
199
200
201
202
203
        )
    ...
]
```

Yining Li's avatar
Yining Li committed
204
205
206
207
208
209
210
211
212
213
214
215
随机执行包装(`RandomApply`)用于以指定概率随机执行数据变换组合。例如:

```python
pipeline = [
    ...
    dict(type='RandomApply',
        transforms=[dict(type='Rotate', angle=30.)],
        prob=0.3)  # 以 0.3 的概率执行被包装的数据变换
    ...
]
```

Yining Li's avatar
Yining Li committed
216
### 多目标扩展(TransformBroadcaster)
217

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

Yining Li's avatar
Yining Li committed
220
多目标扩展包装(`TransformBroadcaster`)有两个用法,一是将数据变换作用于指定的多个字段,二是将数据变换作用于某个字段下的一组目标中。
221
222
223
224
225
226
227

1. 应用于多个字段

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

   ```python
   pipeline = [
Yining Li's avatar
Yining Li committed
228
       dict(type='TransformBroadcaster',
229
           # 分别应用于 "lq" 和 "gt" 两个字段,并将二者应设置 "img" 字段
Yining Li's avatar
Yining Li committed
230
           mapping={'img': ['lq', 'gt']},
231
           # 在完成变换后,将 "img" 字段重映射回原先的字段
Yining Li's avatar
Yining Li committed
232
           auto_remap=True,
233
234
           # 是否在对各目标的变换中共享随机变量
           # 更多介绍参加后续章节(随机变量共享)
235
           share_random_params=True,
236
237
238
239
240
241
242
           transforms=[
               # 在 `RandomFlip` 变换类中,我们只需要操作 "img" 字段即可
               dict(type='RandomFlip'),
           ])
   ]
   ```

243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
   在多目标扩展的 `mapping` 设置中,我们同样可以使用 `...` 来忽略指定的原始字段。如以下例子中,被包裹的 `RandomCrop` 会对字段 `"img"` 中的图像进行裁剪,并且在字段 `"img_shape"` 存在时更新剪裁后的图像大小。如果我们希望同时对两个图像字段 `"lq"``"gt"` 进行相同的随机裁剪,但只更新一次 `"img_shape"` 字段,可以通过例子中的方式实现:

   ```python
   pipeline = [
       dict(type='TransformBroadcaster',
           mapping={
               'img': ['lq', 'gt'],
               'img_shape': ['img_shape', ...],
            },
           # 在完成变换后,将 "img" 和 "img_shape" 字段重映射回原先的字段
           auto_remap=True,
           # 是否在对各目标的变换中共享随机变量
           # 更多介绍参加后续章节(随机变量共享)
           share_random_params=True,
           transforms=[
               # `RandomCrop` 类中会操作 "img" 和 "img_shape" 字段。若 "img_shape" 空缺,
               # 则只操作 "img"
               dict(type='RandomCrop'),
           ])
   ]
   ```


266
267
268
269
270
271
2. 应用于一个字段的一组目标

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

   ```python
   pipeline = [
Yining Li's avatar
Yining Li committed
272
       dict(type='TransformBroadcaster',
273
           # 将 "images" 字段下的每张图片映射至 "img" 字段
Yining Li's avatar
Yining Li committed
274
           mapping={'img': 'images'},
275
           # 在完成变换后,将 "img" 字段下的图片重映射回 "images" 字段的列表中
Yining Li's avatar
Yining Li committed
276
           auto_remap=True,
277
           # 是否在对各目标的变换中共享随机变量
278
           share_random_params=True,
279
280
281
282
283
284
285
           transforms=[
               # 在 `RandomFlip` 变换类中,我们只需要操作 "img" 字段即可
               dict(type='RandomFlip'),
           ])
   ]
   ```

286
287
288
289

#### 装饰器 `cache_randomness`

`TransformBroadcaster` 中,我们提供了 `share_random_params` 选项来支持在多次数据变换中共享随机状态。例如,在超分辨率任务中,我们希望将随机变换**同步**作用于低分辨率图像和原始图像。如果我们希望在自定义的数据变换类中使用这一功能,需要在类中标注哪些随机变量是支持共享的。这可以通过装饰器 `cache_randomness` 来实现。
290
291
292
293

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

```python
Yining Li's avatar
Yining Li committed
294
from mmcv.transforms.utils import cache_randomness
295
296
297
298
299
300
301
302

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

Yining Li's avatar
Yining Li committed
303
    @cache_randomness  # 标注该方法的输出为可共享的随机变量
304
305
306
307
308
309
310
311
312
313
314
    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
```

315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
在上面的例子中,我们用`cache_randomness` 装饰 `do_flip`方法,即将该方法返回值 `flip` 标注为一个支持共享的随机变量。进而,在 `TransformBroadcaster` 对多个目标的变换中,这一变量的值都会保持一致。

#### 装饰器 `avoid_cache_randomness`

在一些情况下,我们无法将数据变换中产生随机变量的过程单独放在类方法中。例如,数据变换中使用了来自第三方库的模块,这些模块将随机变量相关的部分封装在了内部,导致无法将其抽出为数据变换的类方法。这样的数据变换无法通过装饰器 `cache_randomness` 标注支持共享的随机变量,进而无法在多目标扩展时共享随机变量。

为了避免在多目标扩展中误用此类数据变换,我们提供了另一个装饰器 `avoid_cache_randomness`,用来对此类数据变换进行标记:

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

@TRANSFORMS.register_module()
@avoid_cache_randomness
class MyRandomTransform(BaseTransform):

    def transform(self, results: dict) -> dict:
        ...
```

`avoid_cache_randomness` 标记的数据变换类,当其实例被 `TransformBroadcaster` 包装且将参数 `share_random_params` 设置为 True 时,会抛出异常,以此提醒用户不能这样使用。

在使用 `avoid_cache_randomness` 时需要注意以下几点:

1. `avoid_cache_randomness` 只用于装饰数据变换类(BaseTransfrom 的子类),而不能用与装饰其他一般的类、类方法或函数
2.`avoid_cache_randomness` 修饰的数据变换作为基类时,其子类将**不会继承**这一特性。如果子类仍无法共享随机变量,则应再次使用 `avoid_cache_randomness` 修饰
3. 只有当一个数据变换具有随机性,且无法共享随机参数时,才需要以 `avoid_cache_randomness` 修饰。无随机性的数据变换不需要修饰