9.9. 语义分割和数据集

在前几节讨论的目标检测问题中,我们一直使用方形边界框来标注和预测图像中的目标。本节将探讨语义分割(semanticsegmentation)问题,它关注如何将图像分割成属于不同语义类别的区域。值得一提的是,这些语义区域的标注和预测都是像素级的。图9.10展示了语义分割中图像有关狗、猫和背景的标签。可以看到,与目标检测相比,语义分割标注的像素级的边框显然更加精细。

语义分割中图像有关狗、猫和背景的标签

图 9.10 语义分割中图像有关狗、猫和背景的标签

9.9.1. 图像分割和实例分割

计算机视觉领域还有2个与语义分割相似的重要问题,即图像分割(imagesegmentation)和实例分割(instancesegmentation)。我们在这里将它们与语义分割简单区分一下。

  • 图像分割将图像分割成若干组成区域。这类问题的方法通常利用图像中像素之间的相关性。它在训练时不需要有关图像像素的标签信息,在预测时也无法保证分割出的区域具有我们希望得到的语义。以图9.10的图像为输入,图像分割可能将狗分割成两个区域:一个覆盖以黑色为主的嘴巴和眼睛,而另一个覆盖以黄色为主的其余部分身体。
  • 实例分割又叫同时检测并分割(simultaneous detection andsegmentation)。它研究如何识别图像中各个目标实例的像素级区域。与语义分割有所不同,实例分割不仅需要区分语义,还要区分不同的目标实例。如果图像中有两只狗,实例分割需要区分像素属于这两只狗中的哪一只。

9.9.2. Pascal VOC2012语义分割数据集

语义分割的一个重要数据集叫作Pascal VOC2012[1]。为了更好地了解这个数据集,我们先导入实验所需的包或模块。

  1. In [1]:
  1. %matplotlib inline
  2. import d2lzh as d2l
  3. from mxnet import gluon, image, nd
  4. from mxnet.gluon import data as gdata, utils as gutils
  5. import os
  6. import sys
  7. import tarfile

我们下载这个数据集的压缩包到../data路径下。压缩包大小是2GB左右,下载需要一定时间。解压之后的数据集将会放置在../data/VOCdevkit/VOC2012路径下。

  1. In [2]:
  1. # 本函数已保存在d2lzh包中方便以后使用
  2. def download_voc_pascal(data_dir='../data'):
  3. voc_dir = os.path.join(data_dir, 'VOCdevkit/VOC2012')
  4. url = ('http://host.robots.ox.ac.uk/pascal/VOC/voc2012'
  5. '/VOCtrainval_11-May-2012.tar')
  6. sha1 = '4e443f8a2eca6b1dac8a6c57641b67dd40621a49'
  7. fname = gutils.download(url, data_dir, sha1_hash=sha1)
  8. with tarfile.open(fname, 'r') as f:
  9. f.extractall(data_dir)
  10. return voc_dir
  11.  
  12. voc_dir = download_voc_pascal()

进入../data/VOCdevkit/VOC2012路径后,我们可以获取数据集的不同组成部分。其中ImageSets/Segmentation路径包含了指定训练和测试样本的文本文件,而JPEGImagesSegmentationClass路径下分别包含了样本的输入图像和标签。这里的标签也是图像格式,其尺寸和它所标注的输入图像的尺寸相同。标签中颜色相同的像素属于同一个语义类别。下面定义read_voc_images函数将输入图像和标签全部读进内存。

  1. In [3]:
  1. # 本函数已保存在d2lzh包中方便以后使用
  2. def read_voc_images(root=voc_dir, is_train=True):
  3. txt_fname = '%s/ImageSets/Segmentation/%s' % (
  4. root, 'train.txt' if is_train else 'val.txt')
  5. with open(txt_fname, 'r') as f:
  6. images = f.read().split()
  7. features, labels = [None] * len(images), [None] * len(images)
  8. for i, fname in enumerate(images):
  9. features[i] = image.imread('%s/JPEGImages/%s.jpg' % (root, fname))
  10. labels[i] = image.imread(
  11. '%s/SegmentationClass/%s.png' % (root, fname))
  12. return features, labels
  13.  
  14. train_features, train_labels = read_voc_images()

我们画出前5张输入图像和它们的标签。在标签图像中,白色和黑色分别代表边框和背景,而其他不同的颜色则对应不同的类别。

  1. In [4]:
  1. n = 5
  2. imgs = train_features[0:n] + train_labels[0:n]
  3. d2l.show_images(imgs, 2, n);

../_images/chapter_computer-vision_semantic-segmentation-and-dataset_7_0.png

接下来,我们列出标签中每个RGB颜色的值及其标注的类别。

  1. In [5]:
  1. # 该常量已保存在d2lzh包中方便以后使用
  2. VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
  3. [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
  4. [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
  5. [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
  6. [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
  7. [0, 64, 128]]
  8. # 该常量已保存在d2lzh包中方便以后使用
  9. VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
  10. 'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
  11. 'diningtable', 'dog', 'horse', 'motorbike', 'person',
  12. 'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']

有了上面定义的两个常量以后,我们可以很容易地查找标签中每个像素的类别索引。

  1. In [6]:
  1. colormap2label = nd.zeros(256 ** 3)
  2. for i, colormap in enumerate(VOC_COLORMAP):
  3. colormap2label[(colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i
  4.  
  5. # 本函数已保存在d2lzh包中方便以后使用
  6. def voc_label_indices(colormap, colormap2label):
  7. colormap = colormap.astype('int32')
  8. idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256
  9. + colormap[:, :, 2])
  10. return colormap2label[idx]

例如,第一张样本图像中飞机头部区域的类别索引为1,而背景全是0。

  1. In [7]:
  1. y = voc_label_indices(train_labels[0], colormap2label)
  2. y[105:115, 130:140], VOC_CLASSES[1]
  1. Out[7]:
  1. (
  2. [[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
  3. [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
  4. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  5. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  6. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  7. [0. 0. 0. 0. 1. 1. 1. 1. 1. 1.]
  8. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  9. [0. 0. 0. 0. 0. 1. 1. 1. 1. 1.]
  10. [0. 0. 0. 0. 0. 0. 1. 1. 1. 1.]
  11. [0. 0. 0. 0. 0. 0. 0. 0. 1. 1.]]
  12. <NDArray 10x10 @cpu(0)>, 'aeroplane')

9.9.2.1. 预处理数据

在之前的章节中,我们通过缩放图像使其符合模型的输入形状。然而在语义分割里,这样做需要将预测的像素类别重新映射回原始尺寸的输入图像。这样的映射难以做到精确,尤其在不同语义的分割区域。为了避免这个问题,我们将图像裁剪成固定尺寸而不是缩放。具体来说,我们使用图像增广里的随机裁剪,并对输入图像和标签裁剪相同区域。

  1. In [8]:
  1. # 本函数已保存在d2lzh包中方便以后使用
  2. def voc_rand_crop(feature, label, height, width):
  3. feature, rect = image.random_crop(feature, (width, height))
  4. label = image.fixed_crop(label, *rect)
  5. return feature, label
  6.  
  7. imgs = []
  8. for _ in range(n):
  9. imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)
  10. d2l.show_images(imgs[::2] + imgs[1::2], 2, n);

../_images/chapter_computer-vision_semantic-segmentation-and-dataset_15_0.png

9.9.2.2. 自定义语义分割数据集类

我们通过继承Gluon提供的Dataset类自定义了一个语义分割数据集类VOCSegDataset。通过实现getitem函数,我们可以任意访问数据集中索引为idx的输入图像及其每个像素的类别索引。由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本需要通过自定义的filter函数所移除。此外,我们还定义了normalize_image函数,从而对输入图像的RGB三个通道的值分别做标准化。

  1. In [9]:
  1. # 本类已保存在d2lzh包中方便以后使用
  2. class VOCSegDataset(gdata.Dataset):
  3. def __init__(self, is_train, crop_size, voc_dir, colormap2label):
  4. self.rgb_mean = nd.array([0.485, 0.456, 0.406])
  5. self.rgb_std = nd.array([0.229, 0.224, 0.225])
  6. self.crop_size = crop_size
  7. features, labels = read_voc_images(root=voc_dir, is_train=is_train)
  8. self.features = [self.normalize_image(feature)
  9. for feature in self.filter(features)]
  10. self.labels = self.filter(labels)
  11. self.colormap2label = colormap2label
  12. print('read ' + str(len(self.features)) + ' examples')
  13.  
  14. def normalize_image(self, img):
  15. return (img.astype('float32') / 255 - self.rgb_mean) / self.rgb_std
  16.  
  17. def filter(self, imgs):
  18. return [img for img in imgs if (
  19. img.shape[0] >= self.crop_size[0] and
  20. img.shape[1] >= self.crop_size[1])]
  21.  
  22. def __getitem__(self, idx):
  23. feature, label = voc_rand_crop(self.features[idx], self.labels[idx],
  24. *self.crop_size)
  25. return (feature.transpose((2, 0, 1)),
  26. voc_label_indices(label, self.colormap2label))
  27.  
  28. def __len__(self):
  29. return len(self.features)

9.9.2.3. 读取数据集

我们通过自定义的VOCSegDataset类来分别创建训练集和测试集的实例。假设我们指定随机裁剪的输出图像的形状为

9.9. 语义分割和数据集 - 图4 。下面我们可以查看训练集和测试集所保留的样本个数。

  1. In [10]:
  1. crop_size = (320, 480)
  2. voc_train = VOCSegDataset(True, crop_size, voc_dir, colormap2label)
  3. voc_test = VOCSegDataset(False, crop_size, voc_dir, colormap2label)
  1. read 1114 examples
  2. read 1078 examples

设批量大小为64,分别定义训练集和测试集的迭代器。

  1. In [11]:
  1. batch_size = 64
  2. num_workers = 0 if sys.platform.startswith('win32') else 4
  3. train_iter = gdata.DataLoader(voc_train, batch_size, shuffle=True,
  4. last_batch='discard', num_workers=num_workers)
  5. test_iter = gdata.DataLoader(voc_test, batch_size, last_batch='discard',
  6. num_workers=num_workers)

打印第一个小批量的形状。不同于图像分类和目标识别,这里的标签是一个三维数组。

  1. In [12]:
  1. for X, Y in train_iter:
  2. print(X.shape)
  3. print(Y.shape)
  4. break
  1. (64, 3, 320, 480)
  2. (64, 320, 480)

9.9.3. 小结

  • 语义分割关注如何将图像分割成属于不同语义类别的区域。
  • 语义分割的一个重要数据集叫作Pascal VOC2012。
  • 由于语义分割的输入图像和标签在像素上一一对应,所以将图像随机裁剪成固定尺寸而不是缩放。

9.9.4. 练习

  • 回忆“图像增广”一节中的内容。哪些在图像分类中使用的图像增广方法难以用于语义分割?

9.9.5. 参考文献

[1] PascalVOC2012数据集。http://host.robots.ox.ac.uk/pascal/VOC/voc2012/