【CV】第 9 章:图像分割

  🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

探索U-Net 架构

执行升级

使用 U-Net 实现语义分割

探索 Mask R-CNN 架构

投资回报率对齐

Mask head

使用 Mask R-CNN 实现实例分割

预测多个类的多个实例

概括

问题


在上一章中,我们学习了检测图像中存在的对象,以及与检测到的对象对应的类。在本章中,我们将更进一步,不仅在对象周围绘制一个边界框,而且通过识别包含对象的确切像素。除此之外,在本章结束时,我们将能够挑选出属于同一类的实例/对象。

在本章中,我们将通过了解 U-Net 和 Mask R-CNN 架构来了解语义分割和实例分割。具体来说,我们将涵盖以下主题:

  • 探索 U-Net 架构
  • 使用 U-Net 实现语义分割
  • 探索 Mask R-CNN 架构
  • 使用 Mask R-CNN 实现实例分割

我们试图通过图像 分割(https://arxiv.org/pdf/1405.0312.pdf)实现的简洁图像如下:

让我们开始吧!

探索U-Net 架构

想象一个场景,你得到一张图像并被要求预测哪个像素对应于哪个对象。到目前为止,当我们一直在预测对象的类别和对象对应的边界框时,我们将图像通过网络传递,然后将图像通过主干架构(例如 VGG 或 ResNet),使输出变平在某一层,并在对类和边界框偏移进行预测之前连接额外的密集层。然而,在图像分割的情况下,输出形状与输入图像的形状相同,将卷积的输出展平然后重建图像可能会导致信息丢失。此外,

在本节中,我们将学习如何执行图像分割。

在进行分割时,我们需要牢记以下两个方面:

  • 原始图像中对象的形状和结构在分割输出中保持不变。
  • 利用一个完全卷积的架构(而不是我们扁平化某个层的结构)在这里会有所帮助,因为我们使用一个图像作为输入,另一个作为输出。

U-Net 架构帮助我们实现了这一目标。U-Net 的典型表示如下(输入图像的形状为 3 x 96 x 128,而图像中存在的类数为 21;这意味着输出包含 21 个通道):

上述架构因其“ U ”形而被称为U-Net架构。

在上图的左半部分,我们可以看到图像通过卷积层,正如我们在前几章中看到的那样,图像尺寸不断减小,而通道数不断增加。然而,在右半部分,我们可以看到我们正在放大缩小后的图像,恢复到原始的高度和宽度,但通道数与类一样多。

此外,在升级的同时,我们还利用跳过连接的左半部分相应层的信息,以便我们可以保留原始图像中的结构/对象。

通过这种方式,U-Net 架构学习保留原始图像的结构(和对象的形状),同时利用卷积的特征来预测对应于每个像素的类别。

一般来说,输出中的通道数与我们想要预测的类数一样多。

执行升级

在 U-Net 架构中,使用该方法进行升级,该方法将输入通道数、输出通道数、内核大小和步幅作为输入参数。计算示例如下: nn.ConvTranspose2dConvTranspose2d

在前面的示例中,我们采用了一个形状为 3 x 3 的输入数组(输入数组),应用了 2 的步幅,我们分配输入值以适应步幅(输入数组为步幅调整),用零填充数组(输入数组为 stride 和 padding 调整),并将填充的输入与过滤器(Filter/Kernel)卷积以获取输出数组。

通过利用 padding 和 stride 的组合,我们将 3 x 3 形状的输入放大为 6 x 6 形状的数组。虽然前面的示例仅用于说明目的,但最佳滤波器值会学习(因为在模型训练过程中对滤波器权重和偏差进行了优化)以尽可能地重建原始图像。

中的超参数nn.ConvTranspose2d如下:

为了了解如何nn.ConvTranspose2d帮助提升数组,让我们看一下以下代码:

1.导入相关包:

import torch
import torch.nn as nn

2.m使用以下方法初始化网络 , nn.ConvTranspose2d:

m = nn.ConvTranspose2d(1, 1, kernel_size=(2,2), 
                       stride=2, padding = 0)

在前面的代码中,我们指定输入通道的值为1,输出通道的值为1,内核大小为(2,2),步幅为2,填充为0。

在内部,填充计算为膨胀 * (kernel_size - 1) - 填充。

因此 1*(2-1)-0 = 1,其中我们将 1 的零填充添加到输入数组的两个维度。

3.初始化一个输入数组并将其传递给模型:

input = torch.ones(1, 1, 3, 3)
output = m(input)
output.shape

1x1x6x6如前面提供的示例图像所示,前面的代码生成 的形状。

现在我们了解了 U-Net 架构的工作原理以及如何nn.ConvTranspose2d帮助放大图像,让我们实现它,以便我们可以预测道路场景图像中存在的不同对象。

使用 U-Net 实现语义分割

在本节中,我们将利用 U-Net 架构来预测对应于图像中所有像素的类别。这种输入-输出组合的示例如下:

请注意,在上图中,属于同一类的对象(左图-输入图像)具有相同的像素值(右图-输出图像),这就是我们对像素进行分割的原因它们在语义上彼此相似。这也称为语义分割。

现在,让我们学习如何编码语义分割:

以下代码可Semantic_Segmentation_with_U_Net.ipynb在Chapter09本书的 GitHub 存储库的文件夹中找到- https://tinyurl.com/mcvp-packt该代码包含用于下载数据的 URL,并且长度适中。

1.让我们首先下载必要的数据集,安装必要的包,然后导入它们。完成后,我们可以定义设备:

import os
if not os.path.exists('dataset1'):
    !wget -q \
     https://www.dropbox.com/s/0pigmmmynbf9xwq/dataset1.zip
    !unzip -q dataset1.zip
    !rm dataset1.zip
    !pip install -q torch_snippets pytorch_model_summary

from torch_snippets import *
from torchvision import transforms
from sklearn.model_selection import train_test_split
device = 'cuda' if torch.cuda.is_available() else 'cpu'

2.定义将用于转换图像的函数 ( tfms):

tfms = transforms.Compose([ 
            transforms.ToTensor(), 
            transforms.Normalize([0.485, 0.456, 0.406], 
                                 [0.229, 0.224, 0.225]) 
        ])

3.定义数据集类 ( SegData):

  • 在方法中指定包含图像的文件夹__init__:
class SegData(Dataset):
    def __init__(self, split):
        self.items=stems(f'dataset1/images_prepped_{split}')
        self.split = split
  • 定义__len__方法:
    def __len__(self):
        return len(self.items)
  • 定义__getitem__方法:
    def __getitem__(self, ix):
        image = read(f'dataset1/images_prepped_{self.split}/\
{self.items[ix]}.png', 1)
        image = cv2.resize(image, (224,224))
        mask=read(f'dataset1/annotations_prepped_{self.split}\
/{self.items[ix]}.png')
        mask = cv2.resize(mask, (224,224))
        return image, mask

在该__getitem__ 方法中,我们调整输入 ( image) 和输出 ( mask) 图像的大小,使它们具有相同的形状。请注意,掩码图像包含介于 之间的整数[0,11]。这表明有 12 个不同的类别。

  • 定义一个函数 ( choose) 用于选择随机图像索引(主要用于调试目的):
    def select(self): return self[randint(len(self))]
  • 定义collate_fn对一批图像进行预处理的方法:
    def collat​​e_fn(self, batch): 
        ims, mask = list(zip(*batch)) 
        ims = torch.cat([tfms(im.copy()/255.)[None] \ 
                         for ims in ims]).float ().to(device) 
        ce_masks = torch.cat([torch.Tensor(mask[None]) for \ 
                            mask in mask]).long().to(device) 
        return ims, ce_masks

在前面的代码中,我们正在对所有输入图像进行预处理,以便在我们转换缩放图像后,它们具有一个通道(以便以后可以通过 CNN 传递每个图像)。请注意,这ce_masks是一个长整数张量,类似于交叉熵目标。

4.定义训练和验证数据集,以及数据加载器:

trn_ds = SegData('train') 
val_ds = SegData('test') 
trn_dl = DataLoader(trn_ds, batch_size=4, shuffle=True, \ collat​​e_fn= 
                    trn_ds.collat​​e_fn) 
val_dl = DataLoader(val_ds, batch_size=1, shuffle=True , \ 
                    collat​​e_fn=val_ds.collat​​e_fn)

5.定义神经网络模型:

  • 定义卷积块 ( conv):
def conv(in_channels, out_channels): 
    return nn.Sequential( 
        nn.Conv2d(in_channels,out_channels,kernel_size=3, \ 
                    stride=1, padding=1), 
        nn.BatchNorm2d(out_channels), 
        nn.ReLU(inplace=True) 
    )

在前面的定义中conv,我们依次执行Conv2d操作、BatchNorm2d操作和ReLU操作。

  • 定义up_conv块:
def up_conv(in_channels, out_channels): 
    return nn.Sequential( 
        nn.ConvTranspose2d(in_channels, out_channels, \ 
                           kernel_size=2, stride=2), 
        nn.ReLU(inplace=True) 
    )

ConvTranspose2d确保我们升级图像。这与Conv2d我们减少图像尺寸的操作不同。它将具有in_channels多个通道的图像作为输入通道,并生成具有out_channels多个输出通道的图像。

  • 定义网络类 ( UNet):
from torchvision.models import vgg16_bn 
class UNet(nn.Module): 
    def __init__(self, pretrained=True, out_channels=12): 
        super().__init__() 

        self.encoder = \ 
                vgg16_bn(pretrained=pretrained).features 
        self. block1 = nn.Sequential(*self.encoder[:6]) 
        self.block2 = nn.Sequential(*self.encoder[6:13]) 
        self.block3 = nn.Sequential(*self.encoder[13:20] ) 
        self.block4 = nn.Sequential(*self.encoder[20:27]) 
        self.block5 = nn.Sequential(*self.encoder[27:34]) 

        self.bottleneck = nn.Sequential(*self.encoder[ 34:]) 
        self.conv_bottleneck = conv(512, 1024) 

        self.up_conv6 = up_conv(1024, 512)
        self.conv6 = conv(512 + 512, 512) 
        self.up_conv7 = up_conv(512, 256) 
        self.conv7 = conv(256 + 512, 256) 
        self.up_conv8 = up_conv(256, 128) 
        self.conv8 = conv( 128 + 256, 128) 
        self.up_conv9 = up_conv(128, 64) 
        self.conv9 = conv(64 + 128, 64) 
        self.up_conv10 = up_conv(64, 32) 
        self.conv10 = conv(32 + 64, 32) 
        self.conv11 = nn.Conv2d(32, out_channels, \ 
                                kernel_size=1)

在前面的__init__方法中,我们定义了我们将在该forward方法中使用的所有层。

  • 定义forward方法:
    def forward(self, x):
        block1 = self.block1(x)
        block2 = self.block2(block1)
        block3 = self.block3(block2)
        block4 = self.block4(block3)
        block5 = self.block5(block4)

        bottleneck = self.bottleneck(block5)
        x = self.conv_bottleneck(bottleneck)

        x = self.up_conv6(x)
        x = torch.cat([x, block5], dim=1)
        x = self.conv6(x)

        x = self.up_conv7(x)
        x = torch.cat([x, block4], dim=1)
        x = self.conv7(x)

        x = self.up_conv8(x)
        x = torch.cat([x, block3], dim=1)
        x = self.conv8(x)

        x = self.up_conv9(x)
        x = torch.cat([x, block2], dim=1)
        x = self.conv9(x)

        x = self.up_conv10(x)
        x = torch.cat([x, block1], dim=1)
        x = self.conv10(x)

        x = self.conv11(x)

        return x

torch.cat在前面的代码中,我们通过使用适当的张量对在缩小和放大卷积特征之间建立 U 型连接。

  • 定义一个函数 ( UNetLoss) 来计算我们的损失和准确率值:
ce = nn.CrossEntropyLoss()
 def UnetLoss(preds, targets): 
    ce_loss = ce(preds, targets) 
    acc = (torch.max(preds, 1)[1] == targets).float().mean() 
    return ce_loss, acc
  • 定义一个函数,该函数将在批处理 ( train_batch) 上进行训练并在验证数据集 ( ) 上计算指标validate_batch:
def train_batch(model, data, optimizer, criterion):
    model.train()
    ims, ce_masks = data
    _masks = model(ims)
    optimizer.zero_grad()
    loss, acc = criterion(_masks, ce_masks)
    loss.backward()
    optimizer.step()
    return loss.item(), acc.item()

@torch.no_grad()
def validate_batch(model, data, criterion):
    model.eval()
    ims, masks = data
    _masks = model(ims)
    loss, acc = criterion(_masks, masks)
    return loss.item(), acc.item()
  • 定义模型、优化器、损失函数和 epoch 数:
model = UNet().to(device)
criterion = UnetLoss
optimizer = optim.Adam(model.parameters(), lr=1e-3)
n_epochs = 20

6.在越来越多的时期训练模型:

log = Report(n_epochs)
for ex in range(n_epochs):
    N = len(trn_dl)
    for bx, data in enumerate(trn_dl):
        loss, acc = train_batch(model, data, optimizer, \
                                criterion)
        log.record(ex+(bx+1)/N,trn_loss=loss,trn_acc=acc, \
                                 end='\r')

    N = len(val_dl)
    for bx, data in enumerate(val_dl):
        loss, acc = validate_batch(model, data, criterion)
        log.record(ex+(bx+1)/N,val_loss=loss,val_acc=acc, \
                                 end='\r')
        
    log.report_avgs(ex+1)

7.绘制增加时期的训练、验证损失和准确度值:

log.plot_epochs(['trn_loss','val_loss'])

上述代码生成以下输出:

8.计算新图像上的预测输出:

  • 在新图像上获取模型预测:
im, mask = next(iter(val_dl))
_mask = model(im)
  • 获取概率最高的通道:
_, _mask = torch.max(_mask, dim=1)
  • 显示原始图像和预测图像:
subplots([im[0].permute(1,2,0).detach().cpu()[:,:,0], \
          mask.permute(1,2,0).detach().cpu()[:,:,0], \
          _mask.permute(1,2,0).detach().cpu()[:,:,0]],nc=3, \
          titles=['Original image','Original mask', \
          'Predicted mask'])

上述代码生成以下输出:

从上图中可以看出,我们可以使用 U-Net 架构成功生成分割掩码。但是,同一类的所有实例都将具有相同的预测像素值。如果我们想Person在图像中分离类的实例怎么办?在下一节中,我们将了解 Mask R-CNN 架构,它有助于生成实例级掩码,以便我们可以区分实例(甚至是同一类的实例)。

探索 Mask R-CNN 架构

Mask R-CNN 架构有助于识别/突出显示图像中给定类的对象实例。当图像中存在多个相同类型的对象时,这特别方便。此外,术语Mask表示由 Mask R-CNN 在像素级别完成的分割。

Mask R-CNN 架构是 Faster R-CNN 网络的扩展,我们在上一章中了解了它。但是,对 Mask R-CNN 架构进行了一些修改,如下所示:

  • RoI Pooling 层已替换为 RoI Align 层。
  • 除了头部之外,还包括一个掩码头来预测对象的掩码,该头已经预测了最后一层中的对象类别和边界框校正。
  • 卷积网络FCN ) 用于掩码预测。

在了解每个组件的工作原理之前,让我们快速浏览一下 Mask R-CNN 中发生的事件(图片来源:https ://arxiv.org/pdf/1703.06870.pdf ):

在上图中,请注意我们从一层获取类和边界框信息,从另一层获取掩码信息。

Mask R-CNN 架构的工作细节如下:

在我们实现 Mask R-CNN 架构之前,我们需要了解它的组件。我们将从 RoI Align 开始。

投资回报率对齐

通过 Faster R-CNN,我们了解了 RoI Pooling。RoI Pooling 的缺点之一是我们在执行 RoI 池化操作时可能会丢失某些信息。这是因为在池化之前,我们可能会在图像的所有区域中均匀地表示内容。

让我们看一下我们在上一章中提供的示例:

在上图中,区域提案的形状为 5 x 7,我们必须将其转换为 2 x 2 的形状。在将其转换为 2 x 2 形状(一种称为量化的现象)时,该区域的一部分与该区域的其他部分相比具有较少的代表性。这会导致信息丢失,因为该区域的某些部分比其他部分具有更大的权重。RoI Align 可以解决这种情况。

要了解 RoI Align 的工作原理,让我们看一个简单的示例。在这里,我们尝试将以下区域(以虚线表示)转换为 2 x 2 形状:

请注意,该区域(以虚线表示)并非均匀分布在特征图中的所有单元格中。

我们必须执行以下步骤来获得 2 x 2 形状的区域的合理表示:

1.首先,将该区域划分为相等的 2 x 2 形状:

2.定义在每个 2 x 2 单元内等距分布的四个点:

请注意,在上图中,两个连续点之间的距离为 0.75。

3.根据与最近已知值的距离计算每个点的加权平均值:

4.对单元格中的所有四个点重复前面的插值步骤:

5.在单元格内的所有四个点上执行平均池化:

通过执行上述步骤,我们在执行 RoI Align 时不会丢失信息;也就是说,当我们将所有区域放在同一个形状内时。

Mask head

使用 RoI Align,我们可以更准确地表示从 Region Proposal Network 获得的区域建议。现在,我们想要为每个区域提案获得分割(掩码)输出,给定一个标准形状的 RoI Align 输出。

通常,在对象检测的情况下,我们会将 RoI Align 通过一个扁平层,以预测对象的类别和边界框偏移量。然而,在图像分割的情况下,我们预测包含对象的边界框内的像素。因此,我们现在有第三个输出(除了类和边界框偏移),它是感兴趣区域内的预测掩码。

在这里,我们正在预测掩码,它是原始图像之上的图像叠加。假设我们正在预测一个图像,而不是展平 RoI Align 的输出,我们将把它连接到另一个卷积层以获得另一个类似图像的结构(宽度 x 高度)。让我们通过看下图来了解这种现象:

在上图中,我们使用特征金字塔网络FPN ) 获得了形状为 7 x 7 x 2048 的输出,该网络现在有 2 个分支:

  • 第一个分支在展平 FPN 输出后返回对象的类和边界框。
  • 第二个分支在 FPN 的输出之上执行卷积以获得掩码。

对应于 14 x 14 输出的 ground truth 是区域建议的调整大小的图像。如果数据集中有 80 个唯一类,则区域提议的基本事实为 80 x 14 x 14 的形状。80 x 14 x 14 像素中的每一个都是 1 或 0,表示该像素是否包含对象。因此,我们在预测像素类别的同时执行二进制交叉熵损失最小化。

模型训练后,我们能够检测区域,获取类,获取边界框偏移量,并获取每个区域对应的掩码。在进行推理时,我们首先检测图像中存在的对象并进行边界框校正。然后,我们将偏移后的区域传递给蒙版头,以预测该区域中不同像素对应的蒙版。

现在我们了解了 Mask R-CNN 架构的工作原理,让我们对其进行编码,以便我们可以检测图像中的人物实例。

使用 Mask R-CNN 实现实例分割

为了帮助我们理解如何编写 Mask R-CNN 进行实例分割,我们将利用一个数据集来掩盖图像中出现的人。我们将使用的数据集是从 ADE20K 数据集的子集创建的,该数据集可在ADE20K dataset获得。我们只会使用那些为人们提供面具的图像。

我们将采用的策略如下:

  1. 获取数据集,然后从中创建数据集和数据加载器。
  2. 以 PyTorch 的 Mask R-CNN 官方实施所需的格式创建基本事实。
  3. 下载预训练的 Faster R-CNN 模型并为其附加一个 Mask R-CNN 头。
  4. 使用已标准化用于训练 Mask R-CNN 的 PyTorch 代码片段来训练模型。
  5. 通过首先执行非最大抑制然后识别与图像中的人对应的边界框和掩码来推断图像。

让我们编写前面的策略:

1.从 GitHub 导入相关的数据集和训练实用程序:

!wget --quiet \
 http://sceneparsing.csail.mit.edu/data/ChallengeData2017/images.tar
!wget --quiet \ http://sceneparsing.csail.mit.edu/data/ChallengeData2017/annotations_instance.tar
!tar -xf images.tar
!tar -xf annotations_instance.tar
!rm images.tar annotations_instance.tar
!pip install -qU torch_snippets
!wget --quiet \ https://raw.githubusercontent.com/pytorch/vision/master/references/detection/engine.py
!wget --quiet \ https://raw.githubusercontent.com/pytorch/vision/master/references/detection/utils.py
!wget --quiet \ https://raw.githubusercontent.com/pytorch/vision/master/references/detection/transforms.py
!wget --quiet \ https://raw.githubusercontent.com/pytorch/vision/master/references/detection/coco_eval.py
!wget --quiet \ https://raw.githubusercontent.com/pytorch/vision/master/references/detection/coco_utils.py
!pip install -q -U \
    'git+https://github.com/cocodataset/cocoapi.git#subdirectory=PythonAPI'

2.导入所有必要的包并定义device:

from torch_snippets import *

import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor

from engine import train_one_epoch, evaluate
import utils
import transforms as T
device = 'cuda' if torch.cuda.is_available() else 'cpu'

3.获取包含人物面具的图像,如下所示:

  • 遍历imagesandannotations_instance文件夹以获取文件名:
all_images = Glob('images/training') 
all_annots = Glob('annotations_instance/training')
  • 检查原始图像和人物实例的面具表示:
f = 'ADE_train_00014301'

im = read(find(f, all_images), 1)
an = read(find(f, all_annots), 1).transpose(2,0,1)
r,g,b = an
nzs = np.nonzero(r==4) # 4 stands for person
instances = np.unique(g[nzs])
masks = np.zeros((len(instances), *r.shape))
for ix,_id in enumerate(instances):
    masks[ix] = g==_id

subplots([im, *masks], sz=20)

上述代码生成以下输出:

从上图中,我们可以看到已经为每个人生成了一个单独的掩码。在这里,有四个Person类的实例。

在这个特定的数据集中,ground truth 实例注释以这样一种方式提供:RGB 中的红色通道对应于对象的类别,而绿色通道对应于实例编号(如果同一类中存在多个对象)图像——就像我们在这里的例子一样)。此外,Person该类被编码为 4。
  • 遍历注释并存储至少包含一个人的文件:
annots = []
for ann in Tqdm(all_annots):
    _ann = read(ann, 1).transpose(2,0,1)
    r,g,b = _ann
    if 4 not in np.unique(r): continue
    annots.append(ann)
  • 将文件拆分为训练和验证文件:
from sklearn.model_selection import train_test_split
_annots = stems(annots)
trn_items,val_items=train_test_split(_annots,random_state=2)

4.定义变换方法:

def get_transform(train): 
    transforms = [] 
    transforms.append(T.ToTensor()) 
    if train: 
        transforms.append(T.RandomHorizo​​ntalFlip(0.5)) 
    return T.Compose(transforms)

5.创建数据集类(MasksDataset),如下:

  • 定义__init__方法,将图像名称 ( items)、转换方法 ( transforms) 和要考虑的文件数 ( N) 作为输入:
class MasksDataset(Dataset):
    def __init__(self, items, transforms, N):
        self.items = items
        self.transforms = transforms
        self.N = N
  • 定义一个方法 ( get_mask),它将获取与图像中存在的实例等效的多个掩码:
    def get_mask(self, path):
        an = read(path, 1).transpose(2,0,1)
        r,g,b = an
        nzs = np.nonzero(r==4)
        instances = np.unique(g[nzs])
        masks = np.zeros((len(instances), *r.shape))
        for ix,_id in enumerate(instances):
            masks[ix] = g==_id
        return masks
  • 获取要返回的图像和相应的目标值。每个人(实例)都被视为不同的对象类;也就是说,每个实例都是不同的类。请注意,与训练 Faster R-CNN 模型类似,目标以张量字典的形式返回。让我们定义__getitem__方法:
    def __getitem__(self, ix): 
        _id = self.items[ix] 
        img_path = f'images/training/{_id}.jpg' 
        mask_path=f'annotations_instance/training/{_id}.png' 
        mask = self.get_mask( mask_path) 
        obj_ids = np.arange(1, len(masks)+1) 
        img = Image.open(img_path).convert("RGB") 
        num_objs = len(obj_ids)
  • 除了掩码本身,Mask R-CNN 还需要边界框信息。但是,这很容易准备,如下面的代码所示:
        box = [] 
        for i in range(num_objs): 
            obj_pixels = np.where(masks[i]) 
            xmin = np.min(obj_pixels[1]) 
            xmax = np.max(obj_pixels[1]) 
            ymin = np.min (obj_pixels[0]) 
            ymax = np.max(obj_pixels[0]) 
            if (((xmax-xmin)<=10) | (ymax-ymin)<=10): 
                xmax = xmin+10 
                ymax = ymin+10 
            box.append([xmin, ymin, xmax, ymax])

在前面的代码中,我们通过在边界框的 x 和 y 坐标的最小值上添加 10 个像素来调整存在可疑基本事实(Person类的高度或宽度小于 10 像素)的场景。

  • 将所有目标值转换为张量对象:
        boxes = torch.as_tensor(boxes, dtype=torch.float32)
        labels = torch.ones((num_objs,), dtype=torch.int64)
        masks = torch.as_tensor(masks, dtype=torch.uint8)
        area = (boxes[:, 3] - boxes[:, 1]) *\
                    (boxes[:, 2] - boxes[:, 0])
        iscrowd = torch.zeros((num_objs,), dtype=torch.int64)
        image_id = torch.tensor([ix])
  • 将目标值存储在字典中:
        target = {}
        target["boxes"] = boxes
        target["labels"] = labels
        target["masks"] = masks
        target["image_id"] = image_id
        target["area"] = area
        target["iscrowd"] = iscrowd
  • 指定变换方法并返回图像;即target:
        if self.transforms is not None:
            img, target = self.transforms(img, target)
        return img, target
  • 指定方法: __len__
    def __len__(self):
        return self.N
  • 定义将选择随机图像的函数:
    def select(self): 
        return self[randint(len(self))]
  • 检查输入输出组合:
x = MasksDataset(trn_items, get_transform(train=True), N=100) 
im,targ = x[0] 
inspect(im,targ) 
subplots([im, *targ['masks']], sz=10)

以下是上述代码在运行时产生的一些示例输出:

从前面的输出中,我们可以看到面具的形状是 2 x 512 x 683,表示图像中有两个人。

请注意,在该__getitem__方法中,我们在图像中拥有与图像中存在的对象(实例)一样多的掩码和边界框。此外,因为我们只有两个类(Background类和Person类),所以我们将Person类指定为 1。

到这一步结束时,我们在输出字典中有相当多的信息;也就是说,对象类、边界框、蒙版、蒙版的面积以及蒙版是否对应于人群。所有这些信息都可以在target字典中找到。对于我们将要使用的训练函数,将数据标准化为班级要求的格式非常重要。 torchvision.models.detection.maskrcnn_resnet50_fpn

6.接下来,我们需要定义实例分割模型(get_model_instance_segmentation)。我们将使用一个预训练模型,仅重新初始化头部来预测两个类别(背景和人物)。首先,我们需要初始化一个预训练模型并替换box_predictor和mask_predictor头,以便它们可以从头开始学习:

def get_model_instance_segmentation(num_classes): 
    # 加载预训练的实例分割模型
    # COCO 
    model = torchvision.models.detection\ 
                       .maskrcnn_resnet50_fpn(pretrained=True) 

    # 获取分类器的输入特征数
    in_features = model.roi_heads\ 
                       .box_predictor .cls_score.in_features 
    # 用一个新的模型替换预训练的头部
    model.roi_heads.box_predictor = FastRCNNPredictor(\ 
                                    in_features,num_classes) 
    in_features_mask = model.roi_heads\ 
                       .mask_predictor.conv5_mask.in_channels 
    hidden_​​layer = 256
    # 并用一个新的模型替换掩码预测器
    model.roi_heads.mask_predictor = MaskRCNNPredictor(\ 
                                      in_features_mask,\ 
                                   hidden_​​layer, num_classes)
    return model

FastRCNNPredictor需要两个输入—— in_features(输入通道的数量)和num_classes(类的数量)。根据要预测的类数,计算边界框预测数——这是类数的四倍。

MaskRCNNPredictor期望三个输入—— in_features_mask(输入通道数)、hidden_layer(输出中的通道数)和num_classes(要预测的类数)。

可以通过指定以下内容来获得定义模型的详细信息:

model = get_model_instance_segmentation(2).to(device)
model

模型的下半部分(即没有主干)如下所示:

请注意,Faster R-CNN 网络(我们在上一章中训练过)和 Mask R-CNN 模型之间的主要区别在于模块,它本身包含多个子模块。让我们看看他们执行了哪些任务: roi_heads

  • roi_heads: A对取自 FPN 网络的输入进行对齐并创建两个张量。
  • box_predictor:使用我们获得的输出来预测每个 RoI 的类别和边界框偏移量。
  • mask_roi_pool: RoI 然后对齐来自 FPN 网络的输出。
  • mask_head:将先前获得的对齐输出转换为可用于预测掩码的特征图。
  • mask_predictor:获取输出并预测最终的掩码。 mask_head

7.获取与训练和验证图像对应的数据集和数据加载器:

dataset = MasksDataset(trn_items, get_transform(train=True), \ 
                                                    N=3000) 
dataset_test = MasksDataset(val_items, \ 
                           get_transform(train=False), N=800) 

# 定义训练和验证数据加载器
data_loader=torch.utils.data .DataLoader(dataset,batch_size=2,\ 
                                shuffle=True, num_workers=0,\ collat​​e_fn=utils.collat 
                                 ​​e_fn) 

data_loader_test=torch.utils.data.DataLoader(dataset_test,\ 
                                batch_size=1,shuffle=False,\ 
                   num_workers=0 ,collat​​e_fn=utils.collat​​e_fn)

8.定义模型、参数和优化标准:

num_classes = 2
model = get_model_instance_segmentation(\
                        num_classes).to(device)
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005, \
                            momentum=0.9,weight_decay=0.0005)
# and a learning rate scheduler
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, \
                                                step_size=3, \
                                                gamma=0.1)

定义的预训练模型架构将图像和targets字典作为输入以减少损失。可以通过运行以下命令查看将从模型接收到的输出示例:

# 以下代码仅用于说明
model.eval() 
pred = model(dataset[0][0][None].to(device)) 
inspect(pred[0])

前面的代码产生以下输出:

在这里,我们可以看到一个带有边界框 ( BOXES) 的字典,对应于边界框的类 ( LABELS),对应于类预测的置信度分数 ( SCORES),以及我们的掩码实例的位置 ( MASKS)。如您所见,该模型经过硬编码以返回 100 个预测,这是合理的,因为我们不应期望典型图像中的对象超过 100 个。

要获取已检测到的实例数,我们将使用以下代码:

# 以下代码仅用于说明目的
pred[0]['masks'].shape 
# torch.Size([100, 1, 536, 559])

前面的代码为图像(以及与图像对应的尺寸)获取最多 100 个掩码实例(其中实例对应于非背景类)。对于这 100 个实例,它还将返回相应的类标签、边界框和该类的 100 个相应的置信度值。

9.在越来越多的时期训练模型:

num_epochs = 5

trn_history = []
for epoch in range(num_epochs):
    # train for one epoch, printing every 10 iterations
    res = train_one_epoch(model, optimizer, data_loader, \
                          device, epoch, print_freq=10)
    trn_history.append(res)
    # update the learning rate
    lr_scheduler.step()
    # evaluate on the test dataset
    res = evaluate(model, data_loader_test, device=device)

通过这样做,我们现在可以将我们的面具覆盖在图像中的人身上。我们可以记录我们的训练损失随时间增加的变化,如下所示:

import matplotlib.pyplot as plt
plt.title('Training Loss')
losses =[np.mean(list(trn_history[i].meters['loss'].deque)) \
            for i in range(len(trn_history))]
plt.plot(losses)

前面的代码产生以下输出:

10.在测试图像上预测:

model.eval() 
im = dataset_test[0][0] 
show(im) 
with torch.no_grad(): 
    prediction = model([im.to(device)]) 
    for i in range(len(prediction[0][ 'masks'])): 
        plt.imshow(Image.fromarray(prediction[0]['masks']\ 
                      [i, 0].mul(255).byte().cpu().numpy())) 
        plt .title('Class:'+str(prediction[0]['labels']\ 
                   [i].cpu().numpy())+' Score:'+str(\ 
                  prediction[0]['scores'] [i].cpu().numpy())) 
        plt.show()

前面的代码产生以下输出:

从上图中可以看出,我们可以成功识别出图中的四个人。此外,该模型预测图像中的多个其他片段(我们没有在前面的输出中显示),尽管这是低置信度的。

现在模型可以很好地检测实例,让我们在提供的数据集中不存在的自定义图像上运行预测。

11.对您自己的新图像进行预测:

!wget https://www.dropbox.com/s/e92sui3a4ktvb4j/Hema18.JPG 
img = Image.open('Hema18.JPG').convert("RGB") 
from torchvision import transforms 
pil_to_tensor = transforms.ToTensor()( img).unsqueeze_(0) 
Image.fromarray(pil_to_tensor[0].mul(255)\. 
                        permute(1, 2, 0).byte().numpy())

输入图像如下:

  • 获取输入图像的预测:
model.eval() 
with torch.no_grad(): 
    prediction = model([pil_to_tensor[0].to(device)]) 
    for i in range(len(prediction[0]['masks'])): 
        plt.imshow (Image.fromarray(prediction[0]['masks']\ 
                        [i, 0].mul(255).byte().cpu().numpy())) 
        plt.title('Class: '+str( prediction[0]\ 
                              ['labels'][i].cpu().numpy())+'\ 
        Score:'+str(prediction[0]['scores'][i].cpu().numpy( ))) 
        plt.show()

前面的代码产生以下输出:

请注意,在上图中,经过训练的模型的效果不如在测试图像上的效果。这可能是由于以下原因:

  • 在训练期间,人们可能不会离得这么近。
  • 该模型可能没有在感兴趣的类别占据图像大部分的图像上进行训练。
  • 我们训练模型的数据集中的图像与被预测的图像具有不同的数据分布。

然而,即使已经检测到重复的掩码,在这些区域(从第三个掩码开始)具有较低的类分数是一个很好的指标,表明预测中可能存在重复。

到目前为止,我们已经了解了如何分割Person类的多个实例。在下一节中,我们将了解我们需要在本节中构建的代码中进行哪些调整,以分割图像中多个对象类别的多个实例。

预测多个类的多个实例

在上一节中,我们了解了对Person类进行分段。在本节中,我们将使用我们在上一节中构建的相同模型一次性学习对人员和表实例进行分割。让我们开始吧:

1.获取包含感兴趣类别的图像—— Person(类别 ID 4)和Table(类别 ID 6):

classes_list = [4,6]
annots = []
for ann in Tqdm(all_annots):
    _ann = read(ann, 1).transpose(2,0,1)
    r,g,b = _ann
    if np.array([num in np.unique(r) for num in \
                classes_list]).sum()==0: continue
    annots.append(ann)
from sklearn.model_selection import train_test_split
_annots = stems(annots)
trn_items, val_items = train_test_split(_annots, \
                                     random_state=2)

在前面的代码中,我们正在获取至少包含一个感兴趣的类 ( classes_list) 的图像。

2.修改该get_mask方法,使其返回两个掩码,以及与类中每个掩码对应的MasksDataset类:

    def get_mask(self,path):
        an = read(path, 1).transpose(2,0,1)
        r,g,b = an
        cls = list(set(np.unique(r)).intersection({4,6}))
        masks = []
        labels = []
        for _cls in cls:
            nzs = np.nonzero(r==_cls)
            instances = np.unique(g[nzs])
            for ix,_id in enumerate(instances):
                masks.append(g==_id)
                labels.append(classes_list.index(_cls)+1)
        return np.array(masks), np.array(labels)

在前面的代码中,我们正在获取图像中存在的感兴趣的类,并将它们存储在cls. 接下来,我们遍历每个已识别的类 ( ) 并将红色通道值对应于类 ( )cls的位置存储在中。接下来,我们在这些位置获取实例 ID ( )。此外,在返回 和 的 NumPy 数组之前,我们将附加到和对应于实例的类。clsnzsinstancesinstancesmaskslabelsmaskslabels

3.修改方法labels中的对象__getitem__,使其包含从get_mask方法中获得的标签,而不是用torch.ones. 以下代码的粗体部分是在上__getitem__一节中的方法上实现此更改的地方:

    def __getitem__(self, ix):
        _id = self.items[ix]
        img_path = f'images/training/{_id}.jpg'
        mask_path = f'annotations_instance/training/{_id}.png'
        masks, labels = self.get_mask(mask_path)
        #print(labels)
        obj_ids = np.arange(1, len(masks)+1)
        img = Image.open(img_path).convert("RGB")
        num_objs = len(obj_ids)
        boxes = []
        for i in range(num_objs):
            obj_pixels = np.where(masks[i])
            xmin = np.min(obj_pixels[1])
            xmax = np.max(obj_pixels[1])
            ymin = np.min(obj_pixels[0])
            ymax = np.max(obj_pixels[0])
            if (((xmax-xmin)<=10) | (ymax-ymin)<=10):
                xmax = xmin+10
                ymax = ymin+10
            boxes.append([xmin, ymin, xmax, ymax])
        boxes = torch.as_tensor(boxes, dtype=torch.float32)
        labels = torch.as_tensor(labels, dtype=torch.int64)
        masks = torch.as_tensor(masks, dtype=torch.uint8)
        area = (boxes[:, 3] - boxes[:, 1]) * 
                    (boxes[:, 2] - boxes[:, 0])
        iscrowd = torch.zeros((num_objs,), dtype=torch.int64)
        image_id = torch.tensor([ix])
        target = {}
        target["boxes"] = boxes
        target["labels"] = labels
        target["masks"] = masks
        target["image_id"] = image_id
        target["area"] = area
        target["iscrowd"] = iscrowd
        if self.transforms is not None:
            img, target = self.transforms(img, target)
        return img, target
    def __len__(self):
        return self.N
    def choose(self):
        return self[randint(len(self))]

4.在定义时指定您有三个类而不是两个model:

num_classes = 3
model=get_model_instance_segmentation(num_classes).to(device)

在训练模型时,正如我们在上一节中所做的那样,我们将看到训练损失随时间增加的变化如下:

此外,包含人和表格的样本图像的预测片段如下:

从上图中,我们可以看到我们能够使用相同的模型来预测这两个类别。作为练习,我们鼓励你增加类的数量和 epoch 的数量,看看你会得到什么结果。

概括

在本章中,我们学习了如何利用 U-Net 和 Mask R-CNN 对图像进行分割。我们了解 U-Net 架构如何使用卷积对图像执行缩小和放大,以保留图像的结构,同时仍然能够预测图像中对象周围的掩码。然后,我们使用道路场景检测练习巩固了对此的理解,我们将图像分割成多个类别。接下来,我们了解了 RoI Align,它有助于确保解决围绕图像量化的 RoI 池化问题。之后,我们了解了 Mask R-CNN 的工作原理,以便我们可以训练模型来预测图像中的人物实例,以及图像中的人物和桌子的实例。

现在我们已经对各种对象检测技术和图像分割技术有了很好的理解,在下一章中,我们将学习利用我们迄今为止所学技术的应用程序,以便我们可以扩展我们将预测的类的数量. 此外,我们还将了解 Detectron2 框架,它可以在我们构建 Faster R-CNN 和 Mask R-CNN 模型时降低代码复杂度。

问题

  1. 升级对 U-Net 架构有何帮助?
  2. 为什么我们需要在 U-Net 中有一个全卷积网络?
  3. RoI Align 如何改进 Mask-RCNN 中的 RoI 池化?
  4. U-Net 和 Mask-RCNN 用于分割的主要区别是什么?
  5. 什么是实例分割?
  • 8
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonhhxg_柒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值