• 语义分割作为计算机视觉中一项基础任务,同时在自动驾驶/视频编辑等领域中有重要的应用,因此一直受到学术界和工业界的广泛关注。在近几年的会议中,语义分割的论文层出不穷,但是市面上一直缺乏一款能够相对公平比较各种方法的框架。为了方便研究员和工程师们,openmmlab开源了一套基于 PyTorch 实现的标准统一的语义分割框架:MMSegmentation开始:安装和运行 MMSeg — MMSegmentation 1.0.0 文档
  • 作为MM系列的新成员,MMSegmentation 保持了 MM 系列一贯的风格,拥有灵活的模块化设计和全面的高性能model zoo。大部分算法都提供了多种 setting 以及在 Cityscapes,ADE20K,Pascal VOC 2012上的训练结果(目前应该是语义分割中最大的模型库)。MMSeg 作为全新的语义分割框架,和其他的框架相比,它提供了更强更快的主流算法,统一超参的公平比较,附带丰富的配置文件和五花八门的 tricks,而且非常灵活易于拓展。
  • 在众多开源的语义分割代码库中,几乎找不到两个完全一样超参设定的代码库,而且代码库所支持方法的交集又十分有限,无法做到严格的对比实验和公平比较,甚至有时候会推导出“因为 A+B+C > D 所以 A 比 D 更好”这样的结论。因此在 MMSeg 中,所有支持的方法中,除去其自身的结构特性,所有的超参(和 trick)都保持了统一,而且清晰的配置文件使得对比更加容易
  • 常用组件
  • 在 MMSegmentation 中,将网络架构抽象为分割器,它是一个包含网络所有组件的模型。我们已经实现了编码器解码器(EncoderDecoder)和级联编码器解码器(CascadeEncoderDecoder),它们通常由数据预处理器、骨干网络、解码头和辅助头组成
  • 数据预处理器是将数据复制到目标设备并将数据预处理为模型输入格式的部分。
  • 主干网络是将图像转换为特征图的部分
  • 颈部是连接主干网络和头的部分。它对主干网络生成的原始特征图进行一些改进或重新配置。
  • 解码头是将特征图转换为分割掩膜的部分
  • 辅助头是一个可选组件,它将特征图转换为仅用于计算辅助损失的分割掩膜。
  • MMSeg 提供非常丰富的配置文件,这些配置文件整合了各种比较常见的训练 setting,以PSPNet 在 Cityscapes 数据集为例,我们提供了多种不同配置的 PSPNet,报告了速度,显存占用,单尺度/多尺度精度等指标,并提供模型以及实验记录供用户直接下载使用
  • 语义分割中一般剪切出固定大小的图片进行训练,在 Cityscapes 数据集上,主流的有769x769 和 512x1024 两种(Cityscapes 数据集所有图片大小均为1024x2048)。用户在训练的时候无论用那种设定,都可以找到合理的 baseline。在所有的提供的模型和配置文件,没有增加额外的训练 tricks,只选取了最朴素的训练方式,但是这不妨碍我们支持各种各样有趣(也可能有用的)的特性,如
  • 混合精度训练:所有实现的方法都可以无缝支持 FP16 混合精度训练,在保持性能几乎一致的情况下,可以节约40%以上的显存。
  • 难样本挖掘 (OHEM): 可以提升在难样本上在训练中所占权重,提升在难样本上的性能。MMSegmentation 中实现了像素采样器,训练时可以对特定像素进行采样,例如 OHEM(Online Hard Example Mining),可以解决样本不平衡问题, 如下例子是使用 PSPNet 训练并采用 OHEM 策略的配置:
_base_ = './pspnet_r50-d8_512x1024_40k_cityscapes.py'
model=dict(
    decode_head=dict(
        sampler=dict(type='OHEMPixelSampler', thresh=0.7, min_kept=100000)) )
  • 分层设定学习率: 可以为 backbone 和 head 设置不同的学习率。在语义分割里,一些方法会让解码头组件的学习率大于主干网络的学习率,这样可以获得更好的表现或更快的收敛。在 MMSegmentation 里面,您也可以在配置文件里添加如下行来让解码头组件的学习率是主干组件的10倍。
optim_wrapper=dict(
    paramwise_cfg = dict(
        custom_keys={
            'head': dict(lr_mult=10.)}))
  • 通过这种方式,只有置信分数在0.7以下的像素值点会被拿来训练。在训练时我们至少要保留100000个像素值点。如果 thresh 并未被指定,前 min_kept 个损失的像素值点才会被选择。
  • 类别平衡损失函数: 可以用于类别不平衡的数据集,平衡各类之间的损失。对于不平衡类别分布的数据集,可以改变每个类别的损失权重。这里以 cityscapes 数据集为例:
_base_ = './pspnet_r50-d8_512x1024_40k_cityscapes.py'
model=dict(
    decode_head=dict(
        loss_decode=dict(
            type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0,
            # DeepLab 对 cityscapes 使用这种权重
            class_weight=[0.8373, 0.9180, 0.8660, 1.0345, 1.0166, 0.9969, 0.9754,
                        1.0489, 0.8786, 1.0023, 0.9539, 0.9843, 1.1116, 0.9037,
                        1.0865, 1.0955, 1.0865, 1.1529, 1.0507])))
  • class_weight 将被作为 weight 参数,传递给 CrossEntropyLoss
  • 对于训练时损失函数的计算,我们目前支持多个损失函数同时使用。 以 unet 使用 DRIVE 数据集训练为例, 使用 CrossEntropyLoss 和 DiceLoss 的 1:3 的加权和作为损失函数。配置文件写为:
_base_ = './fcn_unet_s5-d16_64x64_40k_drive.py'
model = dict(
    decode_head=dict(loss_decode=[
        dict(type='CrossEntropyLoss', loss_name='loss_ce', loss_weight=1.0),
        dict(type='DiceLoss', loss_name='loss_dice', loss_weight=3.0)
    ]),
    auxiliary_head=dict(loss_decode=[
        dict(type='CrossEntropyLoss', loss_name='loss_ce', loss_weight=1.0),
        dict(type='DiceLoss', loss_name='loss_dice', loss_weight=3.0)
    ]),
)
  • 通过这种方式,确定训练过程中损失函数的权重 loss_weight 和在训练日志里的名字 loss_name
  • 灵活的设计可以大大降低开发人员实现新算法的门槛,MMSeg 使得模型的编码器和解码器可以自由组合,同时我们尽可能避免任何形式的 hardcode,所以大部分模型的内部结构都可以通过配置文件来调节。所有超参的接口都可以通过我们的配置文件灵活的进行调整,比如通道数,损失函数权重,池化尺度等。
  • 将模块化和继承性设计融入到我们的配置文件系统中,方便进行各种实验。如果您想查看配置文件,你可以运行 python tools/misc/print_config.py /PATH/TO/CONFIG 来查看完整的配置文件。你也可以通过传递参数 --cfg-options xxx.yyy=zzz 来查看更新的配置信息。
  • 在 config/_base_ 文件夹下面有4种基本组件类型: 数据集(dataset),模型(model),训练策略(schedule)和运行时的默认设置(default runtime)。许多模型都可以很容易地通过组合这些组件进行实现,比如 DeepLabV3,PSPNet。使用 _base_ 下的组件构建的配置信息叫做原始配置 (primitive)。
  • 对于同一个文件夹下的所有配置文件,建议只有一个对应的原始配置文件。所有其他的配置文件都应该继承自这个原始配置文件,从而保证每个配置文件的最大继承深度为 3
  • 配置文件命名风格
{algorithm name}_{model component names [component1]_[component2]_[...]}_{training settings}_{training dataset information}_{testing dataset information}
  • {algorithm name}: 算法的名称,如 deeplabv3pspnet 等。
  • {model component names}: 算法中使用的组件名称,如主干(backbone)、解码头(head)等。例如,r50-d8 表示使用ResNet50主干网络,并使用主干网络的8倍下采样输出作为下一级的输入。
  • {training settings}: 训练时的参数设置,如 batch size、数据增强(augmentation)、损失函数(loss)、学习率调度器(learning rate scheduler)和训练轮数(epochs/iterations)。例如: 4xb4-ce-linearlr-40K 意味着使用4个gpu,每个gpu4个图像,使用交叉熵损失函数(CrossEntropy),线性学习率调度程序,训练40K iterations。 一些缩写:
  • {gpu x batch_per_gpu}: GPU数量和每个GPU的样本数。bN 表示每个GPU的batch size为N,如 8xb2 为8个gpu x 每个gpu2张图像的缩写。如果未提及,则默认使用 4xb4 。
  • {schedule}: 训练计划,选项有20k40k等。20k 和 40k 分别表示20000次迭代(iterations)和40000次迭代(iterations)。
  • {training dataset information}: 训练数据集名称,如 cityscapes , ade20k 等,以及输入分辨率。例如: cityscapes-768x768  表示使用 cityscapes 数据集进行训练,输入分辨率为768x768 。
  • {testing dataset information} (可选): 测试数据集名称。当您的模型在一个数据集上训练但在另一个数据集上测试时,请将测试数据集名称添加到此处。如果没有这一部分,则意味着模型是在同一个数据集上进行训练和测试的。
  • _base_/models/pspnet_r50-d8.py是使用ResNet50V1c作为主干网络的PSPNet的基本模型配置文件。
# 模型设置
norm_cfg = dict(type='SyncBN', requires_grad=True)  # 分割框架通常使用 SyncBN
data_preprocessor = dict(  # 数据预处理的配置项,通常包括图像的归一化和增强
    type='SegDataPreProcessor',  # 数据预处理的类型
    mean=[123.675, 116.28, 103.53],  # 用于归一化输入图像的平均值
    std=[58.395, 57.12, 57.375],  # 用于归一化输入图像的标准差
    bgr_to_rgb=True,  # 是否将图像从 BGR 转为 RGB
    pad_val=0,  # 图像的填充值
    seg_pad_val=255)  # 'gt_seg_map'的填充值
model = dict(
    type='EncoderDecoder',  # 分割器(segmentor)的名字
    data_preprocessor=data_preprocessor,
    pretrained='open-mmlab://resnet50_v1c',  # 加载使用 ImageNet 预训练的主干网络
    backbone=dict(
        type='ResNetV1c',  # 主干网络的类别,更多细节请参考 mmseg/models/backbones/resnet.py
        depth=50,  # 主干网络的深度,通常为 50 和 101
        num_stages=4,  # 主干网络状态(stages)的数目
        out_indices=(0, 1, 2, 3),  # 每个状态(stage)产生的特征图输出的索引
        dilations=(1, 1, 2, 4),  # 每一层(layer)的空心率(dilation rate)
        strides=(1, 2, 1, 1),  # 每一层(layer)的步长(stride)
        norm_cfg=norm_cfg,  # 归一化层(norm layer)的配置项
        norm_eval=False,  # 是否冻结 BN 里的统计项
        style='pytorch',  # 主干网络的风格,'pytorch' 意思是步长为2的层为 3x3 卷积, 'caffe' 意思是步长为2的层为 1x1 卷积
        contract_dilation=True),  # 当空洞率 > 1, 是否压缩第一个空洞层
    decode_head=dict(
        type='PSPHead',  # 解码头(decode head)的类别。可用选项请参 mmseg/models/decode_heads
        in_channels=2048,  # 解码头的输入通道数
        in_index=3,  # 被选择特征图(feature map)的索引
        channels=512,  # 解码头中间态(intermediate)的通道数
        pool_scales=(1, 2, 3, 6),  # PSPHead 平均池化(avg pooling)的规模(scales)。 细节请参考文章内容
        dropout_ratio=0.1,  # 进入最后分类层(classification layer)之前的 dropout 比例
        num_classes=19,  # 分割前景的种类数目。 通常情况下,cityscapes 为19,VOC为21,ADE20k 为150
        norm_cfg=norm_cfg,  # 归一化层的配置项
        align_corners=False,  # 解码过程中调整大小(resize)的 align_corners 参数
        loss_decode=dict(  # 解码头(decode_head)里的损失函数的配置项
            type='CrossEntropyLoss',  # 分割时使用的损失函数的类别
            use_sigmoid=False,  # 分割时是否使用 sigmoid 激活
            loss_weight=1.0)),  # 解码头的损失权重
    auxiliary_head=dict(
        type='FCNHead',  # 辅助头(auxiliary head)的种类。可用选项请参考 mmseg/models/decode_heads
        in_channels=1024,  # 辅助头的输入通道数
        in_index=2,  # 被选择的特征图(feature map)的索引
        channels=256,  # 辅助头中间态(intermediate)的通道数
        num_convs=1,  # FCNHead 里卷积(convs)的数目,辅助头中通常为1
        concat_input=False,  # 在分类层(classification layer)之前是否连接(concat)输入和卷积的输出
        dropout_ratio=0.1,  # 进入最后分类层(classification layer)之前的 dropout 比例
        num_classes=19,  # 分割前景的种类数目。 通常情况下,cityscapes 为19,VOC为21,ADE20k 为150
        norm_cfg=norm_cfg,  # 归一化层的配置项
        align_corners=False,  # 解码过程中调整大小(resize)的 align_corners 参数
        loss_decode=dict(  # 辅助头(auxiliary head)里的损失函数的配置项
            type='CrossEntropyLoss',  # 分割时使用的损失函数的类别
            use_sigmoid=False,  # 分割时是否使用 sigmoid 激活
            loss_weight=0.4)),  # 辅助头损失的权重,默认设置为0.4
    # 模型训练和测试设置项
    train_cfg=dict(),  # train_cfg 当前仅是一个占位符
    test_cfg=dict(mode='whole'))  # 测试模式,可选参数为 'whole' 和 'slide'. 'whole': 在整张图像上全卷积(fully-convolutional)测试。 'slide': 在输入图像上做滑窗预测
  • _base_/datasets/cityscapes.py是数据集的基本配置文件。
# 数据集设置
dataset_type = 'CityscapesDataset'  # 数据集类型,这将被用来定义数据集
data_root = 'data/cityscapes/'  # 数据的根路径
crop_size = (512, 1024)  # 训练时的裁剪大小
train_pipeline = [  # 训练流程
    dict(type='LoadImageFromFile'),  # 第1个流程,从文件路径里加载图像
    dict(type='LoadAnnotations'),  # 第2个流程,对于当前图像,加载它的标注图像
    dict(type='RandomResize',  # 调整输入图像大小(resize)和其标注图像的数据增广流程
        scale=(2048, 1024),  # 图像裁剪的大小
        ratio_range=(0.5, 2.0),  # 数据增广的比例范围
        keep_ratio=True),  # 调整图像大小时是否保持纵横比
    dict(type='RandomCrop',  # 随机裁剪当前图像和其标注图像的数据增广流程
        crop_size=crop_size,  # 随机裁剪的大小
        cat_max_ratio=0.75),  # 单个类别可以填充的最大区域的比
    dict(type='RandomFlip',  # 翻转图像和其标注图像的数据增广流程
        prob=0.5),  # 翻转图像的概率
    dict(type='PhotoMetricDistortion'),  # 光学上使用一些方法扭曲当前图像和其标注图像的数据增广流程
    dict(type='PackSegInputs')  # 打包用于语义分割的输入数据
]
test_pipeline = [
    dict(type='LoadImageFromFile'),  # 第1个流程,从文件路径里加载图像
    dict(type='Resize',  # 使用调整图像大小(resize)增强
        scale=(2048, 1024),  # 图像缩放的大小
        keep_ratio=True),  # 在调整图像大小时是否保留长宽比
    # 在' Resize '之后添加标注图像
    # 不需要做调整图像大小(resize)的数据变换
    dict(type='LoadAnnotations'),  # 加载数据集提供的语义分割标注
    dict(type='PackSegInputs')  # 打包用于语义分割的输入数据
]
train_dataloader = dict(  # 训练数据加载器(dataloader)的配置
    batch_size=2,  # 每一个GPU的batch size大小
    num_workers=2,  # 为每一个GPU预读取数据的进程个数
    persistent_workers=True,  # 在一个epoch结束后关闭worker进程,可以加快训练速度
    sampler=dict(type='InfiniteSampler', shuffle=True),  # 训练时进行随机洗牌(shuffle)
    dataset=dict(  # 训练数据集配置
        type=dataset_type,  # 数据集类型,详见mmseg/datassets/
        data_root=data_root,  # 数据集的根目录
        data_prefix=dict(
            img_path='leftImg8bit/train', seg_map_path='gtFine/train'),  # 训练数据的前缀
        pipeline=train_pipeline)) # 数据处理流程,它通过之前创建的train_pipeline传递。
val_dataloader = dict(
    batch_size=1,  # 每一个GPU的batch size大小
    num_workers=4,  # 为每一个GPU预读取数据的进程个数
    persistent_workers=True,  # 在一个epoch结束后关闭worker进程,可以加快训练速度
    sampler=dict(type='DefaultSampler', shuffle=False),  # 训练时不进行随机洗牌(shuffle)
    dataset=dict(  # 测试数据集配置
        type=dataset_type,  # 数据集类型,详见mmseg/datassets/
        data_root=data_root,  # 数据集的根目录
        data_prefix=dict(
            img_path='leftImg8bit/val', seg_map_path='gtFine/val'),  # 测试数据的前缀
        pipeline=test_pipeline))  # 数据处理流程,它通过之前创建的test_pipeline传递。
test_dataloader = val_dataloader
# 精度评估方法,我们在这里使用 IoUMetric 进行评估
val_evaluator = dict(type='IoUMetric', iou_metrics=['mIoU'])
test_evaluator = val_evaluator
  • _base_/schedules/schedule_40k.py训练的配置
# optimizer
optimizer = dict(type='SGD', # 优化器种类,更多细节可参考 https://github.com/open-mmlab/mmengine/blob/main/mmengine/optim/optimizer/default_constructor.py
                lr=0.01,  # 优化器的学习率,参数的使用细节请参照对应的 PyTorch 文档
                momentum=0.9,  # 动量大小 (Momentum)
                weight_decay=0.0005)  # SGD 的权重衰减 (weight decay)
optim_wrapper = dict(type='OptimWrapper',  # 优化器包装器(Optimizer wrapper)为更新参数提供了一个公共接口
                    optimizer=optimizer,  # 用于更新模型参数的优化器(Optimizer)
                    clip_grad=None)  # 如果 'clip_grad' 不是None,它将是 ' torch.nn.utils.clip_grad' 的参数。
# 学习策略
param_scheduler = [
    dict(
        type='PolyLR',  # 调度流程的策略,同样支持 Step, CosineAnnealing, Cyclic 等. 请从 https://github.com/open-mmlab/mmengine/blob/main/mmengine/optim/scheduler/lr_scheduler.py 参考 LrUpdater 的细节
        eta_min=1e-4,  # 训练结束时的最小学习率
        power=0.9,  # 多项式衰减 (polynomial decay) 的幂
        begin=0,  # 开始更新参数的时间步(step)
        end=40000,  # 停止更新参数的时间步(step)
        by_epoch=False)  # 是否按照 epoch 计算训练时间
]
# 40k iteration 的训练计划
train_cfg = dict(type='IterBasedTrainLoop', max_iters=40000, val_interval=4000)
val_cfg = dict(type='ValLoop')
test_cfg = dict(type='TestLoop')
# 默认钩子(hook)配置
default_hooks = dict(
    timer=dict(type='IterTimerHook'),  # 记录迭代过程中花费的时间
    logger=dict(type='LoggerHook', interval=50, log_metric_by_epoch=False),  # 从'Runner'的不同组件收集和写入日志
    param_scheduler=dict(type='ParamSchedulerHook'),  # 更新优化器中的一些超参数,例如学习率
    checkpoint=dict(type='CheckpointHook', by_epoch=False, interval=4000),  # 定期保存检查点(checkpoint)
    sampler_seed=dict(type='DistSamplerSeedHook'))  # 用于分布式训练的数据加载采样器
  • _base_/default_runtime.py运行时的默认设置
# 将注册表的默认范围设置为mmseg
default_scope = 'mmseg'
# environment
env_cfg = dict(
    cudnn_benchmark=True,
    mp_cfg=dict(mp_start_method='fork', opencv_num_threads=0),
    dist_cfg=dict(backend='nccl'),
)
log_level = 'INFO'
log_processor = dict(by_epoch=False)
load_from = None  # 从文件中加载检查点(checkpoint)
resume = False  # 是否从已有的模型恢复
  • 有时,可以设置_delete_=True 来忽略基本配置文件中的某些字段。用以下代码加载并解析配置文件pspnet.py:
from mmengine.config import Config

cfg = Config.fromfile('pspnet.py')
print(cfg.model)
{'type': 'EncoderDecoder',
 'pretrained': 'torchvision://resnet50',
 'backbone': {'type': 'ResNetV1c',
  'depth': 50,
  'num_stages': 4,
  'out_indices': (0, 1, 2, 3),
  'dilations': (1, 1, 2, 4),
  'strides': (1, 2, 1, 1),
  'norm_cfg': {'type': 'SyncBN', 'requires_grad': True},
  'norm_eval': False,
  'style': 'pytorch',
  'contract_dilation': True},
 'decode_head': {'type': 'PSPHead',
  'in_channels': 2048,
  'in_index': 3,
  'channels': 512,
  'pool_scales': (1, 2, 3, 6),
  'dropout_ratio': 0.1,
  'num_classes': 19,
  'norm_cfg': {'type': 'SyncBN', 'requires_grad': True},
  'align_corners': False,
  'loss_decode': {'type': 'CrossEntropyLoss',
   'use_sigmoid': False,
   'loss_weight': 1.0}}}
  • 在 MMSegmentation 中,我们提供了最方便的方式 MMSegInferencer 来使用模型。您只需 3 行代码就可以获得图像的分割掩膜。
from mmseg.apis import MMSegInferencer
# 将模型加载到内存中
inferencer = MMSegInferencer(model='deeplabv3plus_r18-d8_4xb2-80k_cityscapes-512x1024')
# 推理
inferencer('demo/demo.png', show=True)
  • 此外,也可以使用 MMSegInferencer 来处理一个包含多张图片的 list
# 输入一个图片 list
images = [image1, image2, ...] # image1 可以是文件路径或 np.ndarray
inferencer(images, show=True, wait_time=0.5) # wait_time 是延迟时间,0 表示无限
# 或输入图像目录
images = $IMAGESDIR
inferencer(images, show=True, wait_time=0.5) 
# 保存可视化渲染彩色分割图和预测结果
# out_dir 是保存输出结果的目录,img_out_dir 和 pred_out_dir 为 out_dir 的子目录
# 以保存可视化渲染彩色分割图和预测结果
inferencer(images, out_dir='outputs', img_out_dir='vis', pred_out_dir='pred')
  • 推理器有一个可选参数 return_datasamples,其默认值为 False,推理器的返回值默认为 dict 类型,包括 ‘visualization’ 和 ‘predictions’ 两个 key。 如果 return_datasamples=True 推理器将返回 SegDataSample 或其列表。
result = inferencer('demo/demo.png')
# 结果是一个包含 'visualization' 和 'predictions' 两个 key 的 `dict`
# 'visualization' 包含彩色分割图
print(result['visualization'].shape)
# (512, 683, 3)
# 'predictions' 包含带有标签索引的分割掩膜
print(result['predictions'].shape)
# (512, 683)
result = inferencer('demo/demo.png', return_datasamples=True)
print(type(result))
# <class 'mmseg.structures.seg_data_sample.SegDataSample'>
# 输入一个图片 list
results = inferencer(images)
# 输出为列表
print(type(results['visualization']), results['visualization'][0].shape)
# <class 'list'> (512, 683, 3)
print(type(results['predictions']), results['predictions'][0].shape)
# <class 'list'> (512, 683)
results = inferencer(images, return_datasamples=True)
# <class 'list'>
print(type(results[0]))
# <class 'mmseg.structures.seg_data_sample.SegDataSample'>
  • 教程3:使用预训练模型推理 — MMSegmentation 1.0.0 文档MMSegInferencer 必须使用 model 初始化,该 model 可以是模型名称或一个 Config,甚至可以是配置文件的路径。 模型名称可以在模型的元文件(configs/xxx/metafile.yaml)中找到,比如 maskformer 的一个模型名称是 maskformer_r50-d32_8xb2-160k_ade20k-512x512如果输入模型名称,模型的权重将自动下载。以下是其他输入参数:
  • weights(str,可选)- 权重的路径。如果未指定,并且模型是元文件中的模型名称,则权重将从元文件加载。默认为 None。
  • classes(list,可选)- 输入类别用于结果渲染,由于分割模型的预测结构是标签索引的分割图,classes 是一个相应的标签索引的类别列表。若 classes 没有定义,可视化工具将默认使用 cityscapes 的类别。默认为 None。
  • palette(list,可选)- 输入调色盘用于结果渲染,它是对应分类的配色列表。若 palette 没有定义,可视化工具将默认使用 cityscapes 的调色盘。默认为 None。
  • dataset_name(str,可选)- 数据集名称或别名,可视化工具将使用数据集的元信息,如类别和配色,但 classes 和 palette 具有更高的优先级。默认为 None。
  • device(str,可选)- 运行推理的设备。如果无,则会自动使用可用的设备。默认为 None。
  • scope(str,可选)- 模型的作用域。默认为 ‘mmseg’。
  • MMSegInferencer 有4个用于可视化预测的参数,您可以在初始化推理器时使用它们:
  • show(bool)- 是否弹出窗口显示图像。默认为 False。
  • wait_time(float)- 显示的间隔。默认值为 0。
  • img_out_dir(str)- out_dir 的子目录,用于保存渲染有色分割掩膜,因此如果要保存预测掩膜,则必须定义 out_dir。默认为 vis
  • opacity(int,float)- 分割掩膜的透明度。默认值为 0.8。
  • mmseg.apis.init_model,从配置文件初始化一个分割器。
  • config(str,Path 或 mmengine.Config)- 配置文件路径或配置对象。
  • checkpoint(str,可选)- 权重路径。如果为 None,则模型将不会加载任何权重。
  • device(str,可选)- CPU/CUDA 设备选项。默认为 ‘cuda:0’。
  • cfg_options(dict,可选)- 用于覆盖所用配置中的某些设置的选项。
  • 返回值:nn.Module:构建好的分割器。
from mmseg.apis import init_model
config_path = 'configs/pspnet/pspnet_r50-d8_4xb2-40k_cityscapes-512x1024.py'
checkpoint_path = 'checkpoints/pspnet_r50-d8_512x1024_40k_cityscapes_20200605_003338-2966598c.pth'
# 初始化不带权重的模型
model = init_model(config_path)
# 初始化模型并加载权重
model = init_model(config_path, checkpoint_path)
# 在 CPU 上的初始化模型并加载权重
model = init_model(config_path, checkpoint_path, 'cpu')
  • mmseg.apis.inference_model,使用分割器推理图像。
  • model(nn.Module)- 加载的分割器
  • imgs(str,np.ndarray 或 list[str/np.ndarray])- 图像文件或加载的图像
  • 返回值:SegDataSample 或 list[SegDataSample]:如果 imgs 是列表或元组,则返回相同长度的列表类型结果,否则直接返回分割结果。
  • SegDataSample 中的参数分为几个部分:
  • gt_sem_segPixelData)- 语义分割的标注。
  • pred_sem_segPixelData)- 语义分割的预测。
  • seg_logitsPixelData)- 模型最后一层的输出结果。
from mmseg.apis import init_model, inference_model
config_path = 'configs/pspnet/pspnet_r50-d8_4xb2-40k_cityscapes-512x1024.py'
checkpoint_path = 'checkpoints/pspnet_r50-d8_512x1024_40k_cityscapes_20200605_003338-2966598c.pth'
img_path = 'demo/demo.png'
model = init_model(config_path, checkpoint_path)
result = inference_model(model, img_path)
  • mmseg.apis.show_result_pyplot在图像上可视化分割结果。
  • model(nn.Module)- 加载的分割器。
  • img(str 或 np.ndarray)- 图像文件名或加载的图像。
  • result(SegDataSample)- SegDataSample 预测结果。
  • opacity(float)- 绘制分割图的不透明度。默认值为 0.5,必须在 (0,1] 范围内。
  • title(str)- pyplot 图的标题。默认值为 ‘’。
  • draw_gt(bool)- 是否绘制 GT SegDataSample。默认为 True
  • draw_pred(draws_pred)- 是否绘制预测 SegDataSample。默认为 True
  • wait_time(float)- 显示的间隔,0 是表示“无限”的特殊值。默认为 0
  • show(bool)- 是否展示绘制的图像。默认为 True
  • save_dir(str,可选)- 为所有存储后端保存的文件路径。如果为 None,则后端存储将不会保存任何数据。
  • out_file(str,可选)- 输出文件的路径。默认为 None
  • 返回值:np.ndarray:通道为 RGB 的绘制图像。
from mmseg.apis import init_model, inference_model, show_result_pyplot
config_path = 'configs/pspnet/pspnet_r50-d8_4xb2-40k_cityscapes-512x1024.py'
checkpoint_path = 'checkpoints/pspnet_r50-d8_512x1024_40k_cityscapes_20200605_003338-2966598c.pth'
img_path = 'demo/demo.png'
# 从配置文件和权重文件构建模型
model = init_model(config_path, checkpoint_path, device='cuda:0')
# 推理给定图像
result = inference_model(model, img_path)
# 展示分割结果
vis_image = show_result_pyplot(model, img_path, result)
# 保存可视化结果,输出图像将在 `workdirs/result.png` 路径下找到
vis_iamge = show_result_pyplot(model, img_path, result, out_file='work_dirs/result.png')
# 修改展示图像的时间,注意 0 是表示“无限”的特殊值
vis_image = show_result_pyplot(model, img_path, result, wait_time=5)
  • 注意: 如果当前设备没有图形用户界面,建议将 show 设置为 False,并指定 out_file 或 save_dir 来保存结果。如果您想在窗口上显示结果,则不需要特殊设置。
  • tools/train.py 文件提供了在单GPU上部署训练任务的方法。
python tools/train.py  ${配置文件} [可选参数]
  • --work-dir ${工作路径}: 重新指定工作路径
  • --amp: 使用自动混合精度计算
  • --resume: 从工作路径中保存的最新检查点文件(checkpoint)恢复训练
  • --cfg-options ${需更覆盖的配置}: 覆盖已载入的配置中的部分设置,并且 以 xxx=yyy 格式的键值对 将被合并到配置文件中。 比如: ‘–cfg-option model.encoder.in_channels=6’。
  • 注意: 命令行参数 --resume 和在配置文件中的参数 load_from 的不同之处:
  • --resume 只决定是否继续使用工作路径中最新的检查点,它常常用于恢复被意外打断的训练
python tools/train.py ${配置文件} --resume --cfg-options load_from=${检查点}
  • load_from 会明确指定被载入的检查点文件,且训练迭代器将从0开始,通常用于微调模型。
  • tools/test.py 文件提供了在单 GPU 上启动测试任务的方法。
python tools/test.py ${配置文件} ${模型权重文件} [可选参数]
  • --work-dir: 如果指定了路径,结果会保存在该路径下。如果没有指定则会保存在 work_dirs/{配置文件名} 路径下.
  • --show: 当 --show-dir 没有指定时,可以使用该参数,在程序运行过程中显示预测结果。
  • --show-dir: 绘制了分割掩膜图片的存储文件夹。如果指定了该参数,则可视化的分割掩膜将被保存到 work_dir/timestamp/{指定路径}.
  • --wait-time: 多次可视化结果的时间间隔。当 --show 为激活状态时发挥作用。默认为2。
  • --cfg-options: 如果被具体指定,以 xxx=yyy 形式的键值对将被合并入配置文件中。
  • 当需要保存测试输出的分割结果,用 --out 指定分割结果输出路径;或者通过配置文件定义 output_dir。例如在 configs/fcn/fcn_r50-d8_4xb4-80k_ade20k-512x512.py 添加 test_evaluator 定义:
python tools/test.py ${CONFIG_FILE} ${CHECKPOINT_FILE} --out ${OUTPUT_DIR}
test_evaluator = dict(type='IoUMetric', iou_metrics=['mIoU'], output_dir='work_dirs/format_results')
  • 以保存模型 fcn_r50-d8_4xb4-80k_ade20k-512x512 在 ADE20K 验证数据集上的结果为例:
python tools/test.py configs/fcn/fcn_r50-d8_4xb4-80k_ade20k-512x512.py ckpt/fcn_r50-d8_512x512_80k_ade20k_20200614_144016-f8ac5082.pth --out work_dirs/format_results
  • 当测试的数据集没有提供标注,评测时没有真值可以参与计算,因此需要设置 format_only=True, 同时需要修改 test_dataloader由于没有标注,需要在数据增强变换中删掉 dict(type='LoadAnnotations'),以下是一个配置示例:
test_evaluator = dict(
    type='IoUMetric',
    iou_metrics=['mIoU'],
    format_only=True,
    output_dir='work_dirs/format_results')
test_dataloader = dict(
    batch_size=1,
    num_workers=4,
    persistent_workers=True,
    sampler=dict(type='DefaultSampler', shuffle=False),
    dataset=dict(
        type = 'ADE20KDataset'
        data_root='data/ade/release_test',
        data_prefix=dict(img_path='testing'),
        # 测试数据变换中没有加载标注
        pipeline=[
            dict(type='LoadImageFromFile'),
            dict(type='Resize', scale=(2048, 512), keep_ratio=True),
            dict(type='PackSegInputs')
        ]))
  • 然后执行测试命令:
python tools/test.py configs/fcn/fcn_r50-d8_4xb4-80k_ade20k-512x512.py ckpt/fcn_r50-d8_512x512_80k_ade20k_20200614_144016-f8ac5082.pth
  • MMSegmentation 1.x 提供了简便的方式监控训练时的状态以及可视化在模型预测时的数据。在配置文件 default_runtime.py 的 vis_backend 中添加 TensorboardVisBackend
vis_backends = [dict(type='LocalVisBackend'),
                dict(type='TensorboardVisBackend')]
visualizer = dict(
    type='SegLocalVisualizer', vis_backends=vis_backends, name='visualizer')
  • 启动训练实验的命令如下
python tools/train.py configs/pspnet/pspnet_r50-d8_4xb4-80k_ade20k-512x512.py --work-dir work_dir/test_visual
  • vis_data 路径中的标量文件包括了学习率、损失函数和 data_time 等,还记录了指标结果,Tensorboard 的可视化结果使用下面的命令执行:
tensorboard --logdir work_dirs/test_visual/时间戳/vis_data
  • MMSegmentation 提供了 SegVisualizationHook ,它是一个可以用于可视化 ground truth 和在模型测试和验证期间的预测分割结果的钩子 。例如,在 _base_/schedules/schedule_20k.py 中,修改 SegVisualizationHook 配置,将 draw 设置为 True 以启用网络推理结果的存储,interval 表示预测结果的采样间隔, 设置为 1 时,将保存网络的每个推理结果。 interval 默认设置为 50:
default_hooks = dict(
    timer=dict(type='IterTimerHook'),
    logger=dict(type='LoggerHook', interval=50, log_metric_by_epoch=False),
    param_scheduler=dict(type='ParamSchedulerHook'),
    checkpoint=dict(type='CheckpointHook', by_epoch=False, interval=2000),
    sampler_seed=dict(type='DistSamplerSeedHook'),
    visualization=dict(type='SegVisualizationHook', draw=True, interval=1))
  • 它们实现了训练时所必需的功能, 在配置文件中用 default_hooks 定义传给 RunnerRunner 通过 register_default_hooks 方法注册. 钩子有对应的优先级, 优先级越高, 越早被执行器调用. 如果优先级一样, 被调用的顺序和钩子注册的顺序一致.

钩子

功能

优先级

IterTimerHook

IterTimerHook

IterTimerHook

LoggerHook

从 Runner 里不同的组件中收集日志记录, 并将其输出到终端, JSON 文件, tensorboard, wandb 等下游.

BELOW_NORMAL (60)

ParamSchedulerHook

更新优化器里面的一些超参数, 例如学习率的动量.

LOW (70)

CheckpointHook

规律性地保存 checkpoint 文件.

VERY_LOW (90)

DistSamplerSeedHook

确保分布式采样器 shuffle 是打开的.

NORMAL (50)

SegVisualizationHook

可视化验证和测试过程里的预测结果.

NORMAL (50)

  • 自定义钩子在配置通过 custom_hooks 定义, Runner 通过 register_custom_hooks 方法注册. 自定义钩子优先级需要在配置文件里设置, 如果没有设置, 则会被默认设置为 NORMAL. 下面是部分 MMEngine 中实现的自定义钩子:

钩子

钩子

EMAHook

在模型训练时使用指数滑动平均 (Exponential Moving Average, EMA).

EmptyCacheHook

在训练时释放所有没有被缓存占用的 GPU 显存.

SyncBuffersHook

在每个训练 Epoch 结束时同步模型 buffer 里的参数例如 BN 里的 running_mean 和 running_var.

  • 启动训练实验后,可视化结果将在 validation loop 存储到本地文件夹中,或者在一个数据集上启动评估模型时,预测结果将存储在本地。本地的可视化的存储结果保存在 $WORK_DIRS/vis_data 下的 vis_image 中。另外,如果在 vis_backends 中添加 TensorboardVisBackend ,如 TensorBoard 的配置,我们还可以运行下面的命令在 TensorBoard 中查看它们:
tensorboard --logdir work_dirs/test_visual/时间戳/vis_data
  • 如果想可视化单个样本数据,建议使用 SegLocalVisualizer 。
import mmcv
import os.path as osp
import torch
# `PixelData` 是 MMEngine 中用于定义像素级标注或预测的数据结构。
# 请参考下面的MMEngine数据结构教程文件:
# https://mmengine.readthedocs.io/zh_CN/latest/advanced_tutorials/data_element.html#pixeldata
from mmengine.structures import PixelData
# `SegDataSample` 是在 MMSegmentation 中定义的不同组件之间的数据结构接口,
# 它包括 ground truth、语义分割的预测结果和预测逻辑。
# 详情请参考下面的 `SegDataSample` 教程文件:
# https://github.com/open-mmlab/mmsegmentation/blob/1.x/docs/en/advanced_guides/structures.md
from mmseg.structures import SegDataSample
from mmseg.visualization import SegLocalVisualizer
out_file = 'out_file_cityscapes'
save_dir = './work_dirs'
image = mmcv.imread(
    osp.join(
        osp.dirname(__file__),
        './aachen_000000_000019_leftImg8bit.png'
    ),
    'color')
sem_seg = mmcv.imread(
    osp.join(
        osp.dirname(__file__),
        './aachen_000000_000019_gtFine_labelTrainIds.png'  # noqa
    ),
    'unchanged')
sem_seg = torch.from_numpy(sem_seg)
gt_sem_seg_data = dict(data=sem_seg)
gt_sem_seg = PixelData(**gt_sem_seg_data)
data_sample = SegDataSample()
data_sample.gt_sem_seg = gt_sem_seg
seg_local_visualizer = SegLocalVisualizer(
    vis_backends=[dict(type='LocalVisBackend')],
    save_dir=save_dir)
# 数据集的元信息通常包括类名的 `classes` 和
# 用于可视化每个前景颜色的 `palette` 。
# 所有类名和调色板都在此文件中定义:
# https://github.com/open-mmlab/mmsegmentation/blob/1.x/mmseg/utils/class_names.py
seg_local_visualizer.dataset_meta = dict(
    classes=('road', 'sidewalk', 'building', 'wall', 'fence',
             'pole', 'traffic light', 'traffic sign',
             'vegetation', 'terrain', 'sky', 'person', 'rider',
             'car', 'truck', 'bus', 'train', 'motorcycle',
             'bicycle'),
    palette=[[128, 64, 128], [244, 35, 232], [70, 70, 70],
             [102, 102, 156], [190, 153, 153], [153, 153, 153],
             [250, 170, 30], [220, 220, 0], [107, 142, 35],
             [152, 251, 152], [70, 130, 180], [220, 20, 60],
             [255, 0, 0], [0, 0, 142], [0, 0, 70],
             [0, 60, 100], [0, 80, 100], [0, 0, 230],
             [119, 11, 32]])
# 当`show=True`时,直接显示结果,
# 当 `show=False`时,结果将保存在本地文件夹中。
seg_local_visualizer.add_datasample(out_file, image,
                                    data_sample, show=False)
  • 提供了一个脚本来导出模型到 ONNX 格式。被转换的模型可以通过工具 Netron 来可视化。除此以外,我们同样支持对 PyTorch 和 ONNX 模型的输出结果做对比。
python tools/pytorch2onnx.py \
    ${CONFIG_FILE} \
    --checkpoint ${CHECKPOINT_FILE} \
    --output-file ${ONNX_FILE} \
    --input-img ${INPUT_IMG} \
    --shape ${INPUT_SHAPE} \
    --rescale-shape ${RESCALE_SHAPE} \
    --show \
    --verify \
    --dynamic-export \
    --cfg-options \
      model.test_cfg.mode="whole"
  • config : 模型配置文件的路径
  • --checkpoint : 模型检查点文件的路径
  • --output-file: 输出的 ONNX 模型的路径。如果没有专门指定,它默认是 tmp.onnx
  • --input-img : 用来转换和可视化的一张输入图像的路径
  • --shape: 模型的输入张量的高和宽。如果没有专门指定,它将被设置成 test_pipeline 的 img_scale
  • --rescale-shape: 改变输出的形状。设置这个值来避免 OOM,它仅在 slide 模式下可以用
  • --show: 是否打印输出模型的结构。如果没有被专门指定,它将被设置成 False
  • --verify: 是否验证一个输出模型的正确性 (correctness)。如果没有被专门指定,它将被设置成 False
  • --dynamic-export: 是否导出形状变化的输入与输出的 ONNX 模型。如果没有被专门指定,它将被设置成 False
  • --cfg-options: 更新配置选项
  • 提供 tools/deploy_test.py 去评估不同后端的 ONNX 模型。安装 onnx 和 onnxruntime-gpu
python tools/deploy_test.py \
    ${CONFIG_FILE} \
    ${MODEL_FILE} \
    ${BACKEND} \
    --out ${OUTPUT_FILE} \
    --eval ${EVALUATION_METRICS} \
    --show \
    --show-dir ${SHOW_DIRECTORY} \
    --cfg-options ${CFG_OPTIONS} \
    --eval-options ${EVALUATION_OPTIONS} \
    --opacity ${OPACITY} \
  • config: 模型配置文件的路径
  • model: 被转换的模型文件的路径
  • backend: 推理的后端,可选项:onnxruntime, tensorrt
  • --out: 输出结果成 pickle 格式文件的路径
  • --format-only : 不评估直接给输出结果的格式。通常用在当您想把结果输出成一些测试服务器需要的特定格式时。如果没有被专门指定,它将被设置成 False。 注意这个参数是用 --eval 来 手动添加
  • --eval: 评估指标,取决于每个数据集的要求,例如 “mIoU” 是大多数据集的指标而 “cityscapes” 仅针对 Cityscapes 数据集。注意这个参数是用 --format-only 来 手动添加
  • --show: 是否展示结果
  • --show-dir: 涂上结果的图像被保存的文件夹的路径
  • --cfg-options: 重写配置文件里的一些设置,xxx=yyy 格式的键值对将被覆盖到配置文件里
  • --eval-options: 自定义的评估的选项, xxx=yyy 格式的键值对将成为 dataset.evaluate() 函数的参数变量
  • --opacity: 涂上结果的分割图的透明度,范围在 (0, 1] 之间
  • tools/analyze_logs.py 会画出给定的训练日志文件的 loss/mIoU 曲线,首先需要 pip install seaborn 安装依赖包。
python tools/analyze_logs.py xxx.log.json [--keys ${KEYS}] [--legend ${LEGEND}] [--backend ${BACKEND}] [--style ${STYLE}] [--out ${OUT_FILE}]
## 对 mIoU, mAcc, aAcc 指标画图
python tools/analyze_logs.py log.json --keys mIoU mAcc aAcc --legend mIoU mAcc aAcc
## 对 loss 指标画图
python tools/analyze_logs.py log.json --keys loss --legend loss
  • 为了统一模型和各功能模块之间的输入和输出的接口, 在 OpenMMLab 2.0 MMEngine 中定义了一套抽象数据结构, 实现了基础的增/删/查/改功能, 支持不同设备间的数据迁移, 也支持了如 .cpu().cuda().get() 和 .detach() 的类字典和张量的操作
  • 自定义开发新模型新增模块 — MMSegmentation 1.0.0 文档