Skip to content
Snippets Groups Projects
visualization.md 16.2 KiB
Newer Older
# 可视化 (Visualization)

## 概述

可视化可以给深度学习的模型训练和测试过程提供直观解释。在 OpenMMLab 算法库中,我们期望可视化功能的设计能满足以下需求:

- 提供丰富的开箱即用可视化功能,能够满足大部分计算机视觉可视化任务
- 高扩展性,可视化功能通常多样化,应该能够通过简单扩展实现定制需求
- 能够在训练和测试流程的任意点位进行可视化
- OpenMMLab 各个算法库具有统一可视化接口,利于用户理解和维护

基于上述需求,OpenMMLab 2.0 引入了可视化对象 Visualizer 和各个可视化存储后端 VisBackend 如 `LocalVisBackend``WandbVisBackend``TensorboardVisBackend` 等。此处的可视化不仅仅包括图片数据格式,还包括配置内容、标量和模型图等数据的可视化。
- 为了方便调用,Visualizer 提供的接口实现了绘制和存储的功能。可视化存储后端 VisBackend 作为 Visualizer 的内部属性,会在需要的时候被 Visualizer 调用,将数据存到不同的后端
- 考虑到绘制后会希望存储到多个后端,Visualizer 可以配置多个 VisBackend,当用户调用 Visualizer 的存储接口时候,Visualizer 内部会遍历的调用 VisBackend 存储接口
两者的 UML 关系图如下
<div align="center">
 <img src="https://user-images.githubusercontent.com/17425982/163327736-f7cb3b16-ef07-46bc-982a-3cc7495e6c82.png" >
</div>
## 可视化对象 Visualizer
### 接口说明
可视化对象 Visualizer 对外提供了所有接口。可以将其接口分成 3 大类,如下所示
**(1) 绘制相关接口**
- [draw_bboxes](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.draw_bboxes) 绘制单个或多个边界框
- [draw_points](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.draw_points) 绘制单个或多个点
- [draw_texts](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.draw_texts) 绘制单个或多个文本框
- [draw_lines](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.lines) 绘制单个或多个线段
- [draw_circles](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.draw_circles) 绘制单个或多个圆
- [draw_polygons](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.draw_polygons) 绘制单个或多个多边形
- [draw_binary_masks](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.draw_binary_mask) 绘制单个或多个二值掩码
- [draw_featmap](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.draw_featmap) 绘制特征图,静态方法
上述接口除了 `draw_featmap` 外都可以链式调用,因为该方法调用后可能会导致图片尺寸发生改变。为了避免给用户带来困扰, `draw_featmap` 被设置为静态方法。
当用户想先绘制边界框,在此基础上绘制文本,绘制线段的时候,可以通过链式调用实现:
```python
visualizer.set_image(image)
visualizer.draw_bboxes(...).draw_texts(...).draw_lines(...)
visualizer.show() # 可视化绘制结果
```
特征图可视化是一个常见的功能,用户通过调用 `draw_featmap` 可视化特征图,其参数定义为:
@staticmethod
def draw_featmap(featmap: torch.Tensor, # 输入格式要求为 CHW
                 overlaid_image: Optional[np.ndarray] = None, # 如果同时输入了 image 数据,则特征图会叠加到 image 上绘制
                 channel_reduction: Optional[str] = 'squeeze_mean', # 多个通道压缩为单通道的策略
                 topk: int = 10, # 可选择激活度最高的 topk 个特征图显示
                 arrangement: Tuple[int, int] = (5, 2), # 多通道展开为多张图时候布局
                 resize_shapeOptional[tuple] = None, # 可以指定 resize_shape 参数来缩放特征图
                 alpha: float = 0.5) -> np.ndarray: # 图片和特征图绘制的叠加比例
特征图可视化功能较多,目前不支持 Batch 输入,其功能可以归纳如下
- 输入的 Tensor 一般是包括多个通道的,channel_reduction 参数可以将多个通道压缩为单通道,然后和图片进行叠加显示
  - `squeeze_mean` 将输入的 C 维度采用 mean 函数压缩为一个通道,输出维度变成 (1, H, W)
  - `select_max` 从输入的 C 维度中先在空间维度 sum,维度变成 (C, ),然后选择值最大的通道
  - `None` 表示不需要压缩,此时可以通过 topk 参数可选择激活度最高的 topk 个特征图显示
- 在 channel_reduction 参数为 None 的情况下,topk 参数生效,其会按照激活度排序选择 topk 个通道,然后和图片进行叠加显示,并且此时会通过 arrangement 参数指定显示的布局
  - 如果 topk 不是 -1,则会按照激活度排序选择 topk 个通道显示
  - 如果 topk = -1,此时通道 C 必须是 1 或者 3 表示输入数据是图片,否则报错提示用户应该设置 `channel_reduction`来压缩通道。
- 考虑到输入的特征图通常非常小,函数支持输入 `resize_shape` 参数,方便将特征图进行上采样后进行可视化。
**(2) 存储相关接口**
- [add_config](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.writer.BaseWriter.add_config) 写配置到特定存储后端
- [add_graph](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.writer.BaseWriter.add_graph) 写模型图到特定存储后端
- [add_image](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.writer.BaseWriter.add_image) 写图片到特定存储后端
- [add_scalar](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.writer.BaseWriter.add_scalar) 写标量到特定存储后端
- [add_scalars](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.writer.BaseWriter.add_scalars) 一次性写多个标量到特定存储后端
- [add_datasample](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.writer.BaseWriter.add_datasample) 各个下游库绘制 datasample 数据的抽象接口

以 add 前缀开头的接口表示存储接口。datasample 是 OpenMMLab 2.0 架构中设计的各个下游库统一的抽象数据接口,而 `add_datasample` 接口可以直接处理该数据格式,例如可视化预测结果、可视化 Dataset 或者 DataLoader 输出、可视化中间预测结果等等都可以直接调用下游库重写的 `add_datasample` 接口。

所有下游库都必须要继承 Visualizer 并实现 `add_datasample` 接口。以 MMDetection 为例,应该继承并通过该接口实现目标检测中所有预置任务的可视化功能,例如目标检测、实例分割、全景分割任务结果的绘制和存储。

**(3) 其余功能性接口**

- [set_image](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.set_image) 设置原始图片数据,默认输入图片格式为 RGB
- [get_image](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.get_image) 获取绘制后的 Numpy 格式图片数据,默认输出格式为 RGB
- [show](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.show) 可视化
- [get_backend](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.get_backend) 通过 name 获取特定存储后端
- [close](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.Visualizer.close) 关闭所有已经打开的资源,包括 VisBackend
### 使用样例
**(1) 在任意位置获取 visualizer**
为了确保可视化对象 Visualizer 能够在任何地方被调用,设计上将其继承自 `ManagerMixin` 类,转变为全局唯一对象,用户初始化 `Visualizer` 时必须要调用 `visualizer.get_instance()` 方法才能使实例对象具备全局唯一性。一旦实例化完成,后续可以在任意代码位置通过 `Visualizer.get_current_instance()` 来获取可视化对象。
以 MMDetection 为例,假设 `DetLocalVisualizer` 类继承自 `Visualizer`,并实现了 `add_datasample` 接口。配置文件写法为:
vis_backends = [dict(type='LocalVisBackend')]
visualizer = dict(
    type='DetLocalVisualizer', vis_backends=vis_backends, name='visualizer')
```
```python
# 内部会调用 get_instance() 进行全局唯一实例化
VISUALIZERS.build(cfg.visualizer)
通过上述代码实例化后,可以在任意位置调用 `get_current_instance` 方法来获取 visualizer
# 任意代码位置获取 visualizer
visualizer = Visualizer.get_current_instance()
如果用户直接使用了 MMEngine 或者下游库中的 Runner,则无需进行额外的实例化,因为在 Runner 的初始化函数中会自动创建全局唯一的 visualizer。

**(2) 将数据写入至特定后端**
在获取到 visualizer 后,可以调用 `add_xxx` 接口将各类数据写入到特定后端
# 绘制 datasample,并保存到本地存储后端
visualizer.add_datasample('demo_image', image, gt_sample, pred_sample, step=1)
# 直接本地窗口显示,而无需存储
visualizer.add_datasample('demo_image', image, gt_sample, pred_sample, show=True)
# 写图片
visualizer.add_image('demo_image', image, step=1)
# 写模型精度值
visualizer.add_scalar('mAP', 0.9, step=1)
visualizer.add_scalars({'loss': 1.2, 'acc': 0.8}, step=1)
# 写配置文件
visualizer.add_config(cfg)
# 写模型图
visualizer.add_graph(model, data_batch)
**(3) 特征图可视化**
通过 `channel_reduction` 参数压缩或者选择特征图,并显示到本地窗口
featmap = ... # CHW shape 的 tensor

# 压缩
feat_img = visualizer.draw_featmap(featmap, channel_reduction='squeeze_mean')
visualizer.show(feat_img)

# 选择激活度最高的通道显示
feat_img = visualizer.draw_featmap(featmap, channel_reduction='select_max')
visualizer.show(feat_img)
叠加图片显示

```python
featmap = ... # CHW shape 的 tensor
img = ... # 如果 featmap 和 img 空间尺寸不一致,内部会对 featmap 进行插值
# 压缩
feat_img = visualizer.draw_featmap(featmap, img, channel_reduction='squeeze_mean')
visualizer.show(feat_img)
# 选择激活度最高的通道显示
feat_img = visualizer.draw_featmap(featmap, img, channel_reduction='select_max')
visualizer.show(feat_img)
```
通过 `topk` 参数选择指定个数的通道显示,并显示到本地窗口
```python
featmap= ... # CHW shape 的 tensor
# topk,并以 2 行 5 列模式显示
feat_img = visualizer.draw_featmap(featmap, channel_reduction=None, topk=10, arrangement=(2, 5))
visualizer.show(feat_img)
# topk,并以 5 行 2 列模式显示
feat_img = visualizer.draw_featmap(featmap, channel_reduction=None, topk=10, arrangement=(5, 2))
visualizer.show(feat_img)
```
通过 `resize_shape` 缩放显示的特征图
featmap = ... # CHW shape 的 tensor

# 压缩
feat_img = visualizer.draw_featmap(featmap, channel_reduction='squeeze_mean', resize_shape=(224, 224))
visualizer.show(feat_img)
存储特征图到可视化后端
featmap = ... # CHW shape 的 tensor
# 压缩
feat_img = visualizer.draw_featmap(featmap, channel_reduction='squeeze_mean', resize_shape=(224, 224))
# 存储
visualizer.add_image('feat_image', feat_img)
**(4) 远程窗口显示**
用户可以指定 Wandb 、Tensorboard 或者自定义具备远程窗口显示的后端来保存数据,然后在浏览器上显示。以 Wandb 为例,典型配置为:
```python
vis_backends = [dict(type='WandbVisBackend')]
visualizer = dict(
    type='DetWandbVisualizer', vis_backends=vis_backends, name='visualizer')
```
使用方法和上面完全一致。需要特别注意的是由于 Wandb 绘制的数据无法和 `LocalVisBackend` 后端兼容,所以当 `vis_backends` 存在多个可视化存储后端时候只有 `WandbVisBackend` 才是有效的。
## 可视化存储后端 VisBackend
在绘制后可以将绘制后的数据存储到多个可视化存储后端中。为了统一接口调用,MMEngine 提供了统一的抽象类 `BaseVisBackend`,和一些常用的 VisBackend 如 `LocalVisBackend``WandbVisBackend``TensorboardVisBackend`
### 接口说明
BaseVisBackend 定义了对外调用的接口规范,主要接口和属性如下:
- [add_config](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.vis_backend.BaseVisBackend.add_config) 写配置到特定存储后端
- [add_graph](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.vis_backend.BaseVisBackend.add_graph) 写模型图到特定后端
- [add_image](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.vis_backend.BaseVisBackend.add_image) 写图片到特定后端
- [add_scalar](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.vis_backend.BaseVisBackend.add_scalar) 写标量到特定后端
- [add_scalars](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.vis_backend.BaseVisBackend.add_scalars) 一次性写多个标量到特定后端
- [close](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.vis_backend.BaseVisBackend.close) 关闭已经打开的资源
- [experiment](https://mmengine.readthedocs.io/zh/latest/api.html#mmengine.visualization.vis_backend.BaseVisBackend.experiment) 写后端对象,例如 Wandb 对象和 Tensorboard 对象
`BaseVisBackend` 定义了 5 个常见的写数据接口,考虑到某些写后端功能非常强大,例如 Wandb,其具备写表格,写视频等等功能,针对这类需求用户可以直接获取 experiment 对象,然后调用写后端对象本身的 API 即可。而 `LocalVisBackend``WandbVisBackend``TensorboardVisBackend` 等都是继承自 `BaseVisBackend`,并根据自身特性实现了对应的存储功能。
### 使用案例
一般情况下用户无需操作 VisBackend 对象,只有在当前可视化存储无法满足需求时候,用户会希望直接操作存储后端。以 Wandb 为例,其提供了非常丰富的存储格式,例如存储表格、存储权重等等接口。为了所有后端能够统一接口,我们并没有提供这类常用接口,此时用户可以直接获取 Wandb 对象进行自定义存储。
vis_backends = [dict(type='WandbVisBackend')]
visualizer = dict(
    type='DetWandbVisualizer', vis_backends=vis_backends, name='visualizer')
```
```python
# 内部会调用 get_instance() 进行全局唯一实例化
VISUALIZERS.build(cfg.visualizer)
# 任意代码位置获取 visualizer
visualizer = Visualizer.get_current_instance()

# 扩展 add 功能,例如利用 Wandb 对象绘制表格
wandb = visualizer.get_backend('WandbVisBackend').experiment
val_table = wandb.Table(data=my_data, columns=column_names)
wandb.log({'my_val_table': val_table})
```

一个 visualizer 对象可以接入任意多个 VisBackend。为了方便用户获取任意的 VisBackend,在不指定 name 参数情况下,可以通过类名获取

```python
vis_backends = [dict(type='LocalVisBackend'), dict(type='WandbVisBackend')]
visualizer = dict(
    type='DetLocalVisualizer', vis_backends=vis_backends, name='visualizer')
```

```python
# 内部会调用 get_instance() 进行全局唯一实例化
VISUALIZERS.build(cfg.visualizer)
# 任意代码位置获取 visualizer
visualizer = Visualizer.get_current_instance()

local_vis_backend = visualizer.get_backend('LocalVisBackend')
wandb_vis_backend = visualizer.get_backend('WandbVisBackend')
```

当存在多个同名的 VisBackend 时候,用户必须指定唯一的 name 参数,后续可以通过 name 字符串来获取

```python
vis_backends = [dict(type='LocalVisBackend', name='local_vis_backend_1'), dict(type='LocalVisBackend', name='local_vis_backend_2')]
visualizer = dict(
    type='DetLocalVisualizer', vis_backends=vis_backends, name='visualizer')
```

```python
# 内部会调用 get_instance() 进行全局唯一实例化
VISUALIZERS.build(cfg.visualizer)
# 任意代码位置获取 visualizer
visualizer = Visualizer.get_current_instance()
local_vis_backend_1 = visualizer.get_backend('local_vis_backend_1')
local_vis_backend_2 = visualizer.get_backend('local_vis_backend_2')