1. 什么是语义分割?

语义分割是一种将图像中的每个像素划分为一类的图像分析过程。

这与人类在默认情况下一直在做的事情类似。当我们看到某样东西时,我们下意识地试图将图像的哪一部分“分割”成一个预定义的类/标签/类别。

在这里插入图片描述
上图显示了语义分割的结果。mask中的人用红色像素表示,草是浅绿色,树是深绿色,天空是蓝色。

我们可以通过简单地检查mask在该像素处是否为红色来判断哪个像素属于“person”类,但是我们不能判断两个红色的mask像素是属于同一个人还是属于不同的人。

2. 语义分割的应用

语义分割最常用的场景如下:

2.1 自动驾驶

在自动驾驶中,驾驶汽车的计算机需要对它面前的道路场景有很好的了解。把汽车、行人、车道和交通标志等物体分割开来是很重要的。我们在 Deep Learning course with PyTorch.课程中详细介绍了这个应用。

2.2 人脸分割

面部分割。
参考:https://github.com/massimomauro/FASSEG-repository/blob/master/papers/multiclass_face_segmentation_ICIP2015.pdf

人脸分割是用来将人脸的每个部分分割成语义上相似的区域——嘴唇,眼睛等。这在许多实际应用程序中都很有用。一个非常有趣的应用程序是虚拟化妆。

2.3 室内目标分割

在这里插入图片描述
Indoor Scene Segmentation
Source: http://buildingparser.stanford.edu/dataset.html

你能猜出这个用在哪里吗?在AR(增强现实)和VR(虚拟现实)中。AR应用可以分割整个室内区域,了解椅子、桌子、人、墙等类似物体的位置,从而有效地放置和操作虚拟物体。

2.4 地理遥感

在这里插入图片描述
卫星图像分割:https://www.sciencedirect.com/science/article/pii/S0924271616305305

地理遥感是一种将卫星图像中的每个像素分类的方法,这样我们就可以跟踪每个地区的土地覆盖。如果有一个地区发生严重的森林砍伐,那么就可以采取适当的措施。在卫星图像上使用语义分割的应用还有很多。

3.使用torchvision进行语义分割

我们将研究两个基于深度学习的语义分割模型——全卷积网络(FCN)和DeepLab v3。这些模型已经在COCO Train 2017数据集的子集上进行了训练,该数据集对应于PASCAL VOC数据集。这些模型总共支持20个类别。

3.1 输入和输出

在我们开始之前,让我们了解模型的输入和输出。

这些模型期望得到一个3通道图像(RGB),该图像使用Imagenet均值和标准偏差进行归一化,即:
mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225]

所以输入维数是[Ni x Ci x Hi x Wi]:
其中:

  • Ni ->批大小
  • Ci ->通道数(即3)
  • Hi->图像的高度
  • Wi ->图像的宽度

模型的输出维数为[No x Co x Ho x Wo],其中,

  • No ->是批大小(与Ni相同)
  • Co ->是数据集拥有的类的数量!
  • Ho ->图像的高度(在几乎所有情况下都与Hi相同)
  • Wo ->图像的宽度(在几乎所有情况下都与Wi相同)

注意:torchvision模型的输出是一个OrderedDict,而不是torch。在推断(.eval()模式)期间,输出是一个OrderedDict,它只有一个键-out。这个out键保存输出,对应的值是[No x Co x Ho x Wo]的形状。

3.2. FCN with Resnet-101 backbone

FCN -全卷积网络是使用神经网络完成语义分割任务的首批成功尝试之一。FCN介绍课程,详见: course on Deep Learning with PyTorch

3.2.1. 加载模型

让我们加载FCN!

from torchvision import models 
fcn = models.segmentation.fcn_resnet101(pretrained=True).eval()

现在,我们有一个使用Resnet101主干的FCN预训练模型。如果模型还没有出现在缓存中,pretrained=True标志将下载该模型。eval方法将以推理模式加载它。

3.2.2. 加载图片

接下来,让我们得到一个图像!我们直接从URL下载鸟的图像并保存它。正如您将在代码中看到的,我们使用PIL加载图像。

from PIL import Image 
import matplotlib.pyplot as plt 
import torch 
!wget -nv https://static.independent.co.uk/s3fs-public/thumbnails/image/2018/04/10/19/pinyon-jay-bird.jpg -O bird.png 
img = Image.open(./bird.png’) 
plt.imshow(img); 
plt.show()

在这里插入图片描述

3.2.3 预处理图像

为了使图像以正确的格式使用模型进行推理,我们需要对它进行预处理和标准化!因此,对于预处理步骤,我们执行以下步骤。

  • Resize the image to (256 x 256)
  • CenterCrop it to (224 x 224)
  • Convert it to Tensor – all the values in the image will be scaled so they lie between [0, 1]instead of the original, [0, 255] range.
  • Normalize it with the Imagenet specific values where mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225]

最后,我们扩展图像的尺寸,使它从[C x H x W]变成[1 x C x H x W]。这是必需的,因为我们需要一批数据进行模型推理。

# Apply the transformations needed 
import torchvision.transforms as T 
trf = T.Compose([
T.Resize(256), 
T.CenterCrop(224), 
T.ToTensor(), 
T.Normalize(mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225])]) 
inp = trf(img).unsqueeze(0)

让我们看看上面的代码做了什么。
torchvision有很多有用的功能。其中之一是Transforms,它用于预处理图像。 T.Compose是一个函数,它接受一个列表,其中每个元素都是转换类型的。这将返回一个对象,通过该对象我们可以传递一批图像,并且所有需要的转换将应用于所有图像。

让我们来看看应用在图像上的变换:

  • T.Resize(256):将图像大小调整为256 x 256
  • T.CenterCrop(224):将图像裁剪为224 x 224
  • T.ToTensor():将图像类型转换为torch,并将值缩放到[0,1]范围
  • T.Normalize(mean, std):用给定的平均值和标准偏差对图像进行normalize。

3.2.4 网络前向推理

现在我们已经有了预处理并准备好的图像,让我们将它传递给模型并获得out键。

如前所述,模型的输出是一个OrderedDict,因此我们需要从中取出out键来获得模型的输出。

# Pass the input through the net 
out = fcn(inp)[‘out’] 
print (out.shape)

torch.Size([1, 21, 224, 224])

因此,输出是模型的最终输出。正如我们所看到的,它的形状是[1 x 21 x H x W],正如前面所讨论的。由于模型被训练了21个类,输出有21个通道!

现在我们需要做的是,将这个21通道的输出变成一个2D图像或一个1通道的图像,其中图像的每个像素对应一个类!

2D图像(形状[H x W])的每个像素都对应于一个类标签。请注意,这个2D图像中的每个(x, y)像素对应于0 - 20之间的一个数字,表示一个类。

现在的问题是,我们如何从尺寸[1 x 21 xHxW]的当前图像中获得呢?

简单!我们为每个像素位置取一个最大索引,它代表类。

import numpy as np 
om = torch.argmax(out.squeeze(), dim=0).detach().cpu().numpy() 
print (om.shape)

(224, 224)

print (np.unique(om))

[0 3]

正如我们在处理后观察到的,我们现在有一个2D图像,其中每个像素对应一个类。最后要做的是将这个2D图像转换为分割图,其中每个类标签都转换为RGB颜色,从而帮助可视化。

3.2.5. Decode Output

我们将使用以下函数将该2D图像转换为RGB图像,其中每个标签都映射到其相应的颜色。

# Define the helper function
def decode_segmap(image, nc=21):
  label_colors = np.array([(0, 0, 0),  # 0=background
               # 1=aeroplane, 2=bicycle, 3=bird, 4=boat, 5=bottle
               (128, 0, 0), (0, 128, 0), (128, 128, 0), (0, 0, 128), (128, 0, 128),
               # 6=bus, 7=car, 8=cat, 9=chair, 10=cow
               (0, 128, 128), (128, 128, 128), (64, 0, 0), (192, 0, 0), (64, 128, 0),
               # 11=dining table, 12=dog, 13=horse, 14=motorbike, 15=person
               (192, 128, 0), (64, 0, 128), (192, 0, 128), (64, 128, 128), (192, 128, 128),
               # 16=potted plant, 17=sheep, 18=sofa, 19=train, 20=tv/monitor
               (0, 64, 0), (128, 64, 0), (0, 192, 0), (128, 192, 0), (0, 64, 128)])
  r = np.zeros_like(image).astype(np.uint8)
  g = np.zeros_like(image).astype(np.uint8)
  b = np.zeros_like(image).astype(np.uint8)
  for l in range(0, nc):
    idx = image == l
    r[idx] = label_colors[l, 0]
    g[idx] = label_colors[l, 1]
    b[idx] = label_colors[l, 2]
  rgb = np.stack([r, g, b], axis=2)
  return rgb

让我们看看我们在这个函数中做了什么!
首先,变量label_colors根据索引存储每个类的颜色。因此,第一个类的颜色(即background)存储在label_colors列表的第0个索引处。第二类是plane,存储在索引1中,以此类推。

现在,我们必须从已有的2D图像创建一个RGB图像。那么,我们要做的就是为这3个通道创建一个空的2D矩阵。

因此,r, g和b是数组,它们将形成最终图像的RGB通道。每个数组的形状都是[H x W] (这与2Dimage的形状相同)。

现在,我们循环遍历存储在label_colors中的每个类颜色,并获得该特定类标签所在图像中的对应索引。然后,对于每个通道,我们将其对应的颜色放到类标签所在的像素上。
最后,我们将这3个独立的通道叠加在一起,形成一个RGB图像。

rgb = decode_segmap(om) 
plt.imshow(rgb); plt.show()

在这里插入图片描述
我们已经对图像的输出进行了分割。

注意:由于在预处理步骤中对图像进行了缩放和裁剪,分割后的图像比原始图像要小

3.2.6 最终结果

接下来,让我们将所有这些内容移动到一个函数中,并使用更多的图像!

def segment(net, path):
  img = Image.open(path)
  plt.imshow(img); plt.axis('off'); plt.show()
  # Comment the Resize and CenterCrop for better inference results
  trf = T.Compose([T.Resize(256), 
                   T.CenterCrop(224), 
                   T.ToTensor(), 
                   T.Normalize(mean = [0.485, 0.456, 0.406], 
                               std = [0.229, 0.224, 0.225])])
  inp = trf(img).unsqueeze(0)
  out = net(inp)['out']
  om = torch.argmax(out.squeeze(), dim=0).detach().cpu().numpy()
  rgb = decode_segmap(om)
  plt.imshow(rgb); plt.axis('off'); plt.show()

让我们得到一个新图像!

!wget -nv https://learnopencv.com/wp-content/uploads/2021/01/horse-segmentation.jpeg -O horse.png 
segment(fcn, './horse.png')

在这里插入图片描述
在这里插入图片描述
这不是很有趣吗?现在让我们来看看语义分割中最先进的架构之一——DeepLab

3.3 使用DeepLab进行语义分割

DeepLab是由谷歌大脑衍生而来的语义分割架构。让我们看看如何使用它。

dlab = models.segmentation.deeplabv3_resnet101(pretrained=1).eval()

让我们看看如何使用这个模型在同一幅图像上执行语义分割!我们将使用上面定义的相同函数。

segment(dlab, './horse.png')

在这里插入图片描述
在这里插入图片描述
DeepLab模型几乎完美地分割了马!

3.4 多目标分割

当我们拍摄一个包含多个物体的更复杂的图像时,我们可以开始看到使用两种模型得到的结果的一些差异。

!wget -nv "https://learnopencv.com/wp-content/uploads/2021/01/person-segmentation.jpeg" -O dog-park.png
img = Image.open('./dog-park.png')
plt.imshow(img); plt.show()
print ('Segmenatation Image on FCN')
segment(fcn, path='./dog-park.png', show_orig=False)
print ('Segmenatation Image on DeepLabv3')
segment(dlab, path='./dog-park.png', show_orig=False)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
正如你所看到的,这两个模型都表现得很好!然而,在某些情况下,模型会严重失败。

4. 比较

到目前为止,我们已经看到了代码是如何工作的,以及输出是如何定性的。在本节中,我们将讨论模型的定量方面。我们还将在以下3个指标的基础上对两个模型进行比较。

  • CPU和GPU上的推理时间
  • 模型的大小。
  • 推理时使用的GPU内存。

4.1. 推理时间

我们使用了谷歌Colab来运行代码并得到这些数字。
在这里插入图片描述
在这里插入图片描述
我们可以看到DeepLab模型略慢于FCN。进入翻译页面

4.2 模型尺寸

模型大小是模型权重文件的大小。DeepLab的模型略大于FCN。
在这里插入图片描述

4.3 GPU内存需求

我们使用的是NVIDIA GTX 1080 Ti GPU,发现这两种型号的图片都需要1.2GB的内存。

我们将在下一篇文章中讨论使用PyTorch和Torchvision的其他计算机视觉问题。请继续关注!

源代码地址https://github.com/yuanxinshui/DeepLearnCV/tree/main/PyTorch-Segmentation-torchvision

参考

https://learnopencv.com/pytorch-for-beginners-semantic-segmentation-using-torchvision/

.

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐