語義分割與數據集
Semantic Segmentation and the Dataset
在目標檢測問題中,我們只使用矩形邊界框來標記和預測圖像中的對象。在這一節中,我們將對不同的語義區域進行語義分割。這些語義區域在像素級標記和預測對象。圖1顯示了一個語義分割的圖像,區域標記為“dog”、“cat”和“background”。如您所見,與目標檢測相比,語義分割使用像素級邊界標記區域,以獲得更高的精度。
Fig. 1. Semantically-segmented image, with areas labeled “dog”, “cat”, and “background”.
1. Image Segmentation and Instance Segmentation
在計算機視覺領域,語義分割有兩種重要的方法:圖像分割和實例分割。這里,我們將把這些概念與語義分割區分開來,具體如下:
圖像分割將一幅圖像分成幾個組成區域。這種方法通常利用圖像中像素之間的相關性。在訓練期間,圖像像素不需要標簽。然而,在預測過程中,這種方法不能保證分割區域具有我們想要的語義。如果輸入圖像,圖像分割可能會將狗分成兩個區域,一個覆蓋狗的嘴和眼睛,黑色是突出的顏色,另一個覆蓋狗的其余部分,黃色是突出的顏色。
實例分割也稱為同步檢測與分割。該方法嘗試識別圖像中每個對象實例的像素級區域。與語義分割不同,實例分割不僅區分語義,而且區分不同的對象實例。如果一幅圖像包含兩條狗,實例分割將區分哪些像素屬於哪只狗。
2. The Pascal VOC2012 Semantic Segmentation Dataset
在語義分割領域,一個重要的數據集是Pascal VOC2012。為了更好地理解這個數據集,我們必須首先導入實驗所需的包或模塊。
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import gluon, image, np, npx
import os
npx.set_np()
原始站點可能不穩定,因此我們從鏡像站點下載數據。該存檔文件約為2GB,因此需要一些時間來下載。解壓縮歸檔文件后,數據集位於../data/VOCdevkit/VOC2012路徑中。
#@save
d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar',
'4e443f8a2eca6b1dac8a6c57641b67dd40621a49')
voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012')
轉到../data/VOCdevkit/VOC2012查看數據集的不同部分。ImageSets/Segmentation路徑包含指定訓練和測試示例的文本文件。JPEGImages和SegmentationClass路徑分別包含示例輸入圖像和標簽。這些標簽也是圖像格式的,與它們對應的輸入圖像具有相同的尺寸。在標簽中,顏色相同的像素屬於同一語義范疇。下面定義的read_voc_images函數將所有輸入圖像和標簽讀入內存。
#@save
def read_voc_images(voc_dir, is_train=True):
"""Read all VOC feature and label images."""
txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation',
'train.txt' if is_train else 'val.txt')
with open(txt_fname, 'r') as f:
images = f.read().split()
features, labels = [], []
for i, fname in enumerate(images):
features.append(image.imread(os.path.join(
voc_dir, 'JPEGImages', '%s.jpg' % fname)))
labels.append(image.imread(os.path.join(
voc_dir, 'SegmentationClass', '%s.png' % fname)))
return features, labels
train_features, train_labels = read_voc_images(voc_dir, True)
我們繪制前五個輸入圖像及其標簽。在標簽圖像中,白色代表邊界,黑色代表背景。其他顏色對應不同的類別。
n = 5
imgs = train_features[0:n] + train_labels[0:n]
d2l.show_images(imgs, 2, n);
接下來,我們將列出標簽中的每個RGB顏色值及其標記的類別。
#@save
VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
[0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
[64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
[64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
[0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
[0, 64, 128]]
#@save
VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
'diningtable', 'dog', 'horse', 'motorbike', 'person',
'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']
在定義了上面的兩個常量之后,我們可以很容易地找到標簽中每個像素的類別索引。
#@save
def build_colormap2label():
"""Build an RGB color to label mapping for segmentation."""
colormap2label = np.zeros(256 ** 3)
for i, colormap in enumerate(VOC_COLORMAP):
colormap2label[(colormap[0]*256 + colormap[1])*256 + colormap[2]] = i
return colormap2label
#@save
def voc_label_indices(colormap, colormap2label):
"""Map an RGB color to a label."""
colormap = colormap.astype(np.int32)
idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256
+ colormap[:, :, 2])
return colormap2label[idx]
例如,在第一個示例圖像中,飛機前部的類別索引為1,背景的索引為0。
y = voc_label_indices(train_labels[0], build_colormap2label())
y[105:115, 130:140], VOC_CLASSES[1]
(array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
[0., 0., 0., 0., 0., 0., 0., 1., 1., 1.],
[0., 0., 0., 0., 0., 0., 1., 1., 1., 1.],
[0., 0., 0., 0., 0., 1., 1., 1., 1., 1.],
[0., 0., 0., 0., 0., 1., 1., 1., 1., 1.],
[0., 0., 0., 0., 1., 1., 1., 1., 1., 1.],
[0., 0., 0., 0., 0., 1., 1., 1., 1., 1.],
[0., 0., 0., 0., 0., 1., 1., 1., 1., 1.],
[0., 0., 0., 0., 0., 0., 1., 1., 1., 1.],
[0., 0., 0., 0., 0., 0., 0., 0., 1., 1.]]),
'aeroplane')
2.1. Data Preprocessing
在前面的章節中,我們縮放圖像以使它們適合模型的輸入形狀。在語義分割中,這種方法需要將預測的像素類別重新映射回原始大小的輸入圖像。要精確地做到這一點是非常困難的,尤其是在具有不同語義的分段區域中。為了避免這個問題,我們裁剪圖像以設置尺寸,而不縮放它們。具體來說,我們使用圖像增強中使用的隨機裁剪方法從輸入圖像及其標簽中裁剪出相同的區域。
#@save
def voc_rand_crop(feature, label, height, width):
"""Randomly crop for both feature and label images."""
feature, rect = image.random_crop(feature, (width, height))
label = image.fixed_crop(label, *rect)
return feature, label
imgs = []
for _ in range(n):
imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)
d2l.show_images(imgs[::2] + imgs[1::2], 2, n);
2.2. Dataset Classes for Custom Semantic Segmentation
我們使用gloon提供的繼承數據集類來定制語義分段數據集類VOCSegDataset。通過實現the __getitem__ function函數,我們可以從數據集中任意訪問索引idx和每個像素的類別索引的輸入圖像。由於數據集中的某些圖像可能小於為隨機裁剪指定的輸出尺寸,因此必須使用自定義篩選函數刪除這些示例。此外,我們定義了normalize_image函數來規范輸入圖像的三個RGB通道中的每一個。
#@save
class VOCSegDataset(gluon.data.Dataset):
"""A customized dataset to load VOC dataset."""
def __init__(self, is_train, crop_size, voc_dir):
self.rgb_mean = np.array([0.485, 0.456, 0.406])
self.rgb_std = np.array([0.229, 0.224, 0.225])
self.crop_size = crop_size
features, labels = read_voc_images(voc_dir, is_train=is_train)
self.features = [self.normalize_image(feature)
for feature in self.filter(features)]
self.labels = self.filter(labels)
self.colormap2label = build_colormap2label()
print('read ' + str(len(self.features)) + ' examples')
def normalize_image(self, img):
return (img.astype('float32') / 255 - self.rgb_mean) / self.rgb_std
def filter(self, imgs):
return [img for img in imgs if (
img.shape[0] >= self.crop_size[0] and
img.shape[1] >= self.crop_size[1])]
def __getitem__(self, idx):
feature, label = voc_rand_crop(self.features[idx], self.labels[idx],
*self.crop_size)
return (feature.transpose(2, 0, 1),
voc_label_indices(label, self.colormap2label))
def __len__(self):
return len(self.features)
2.3. Reading the Dataset
使用定制的VOCSegDataset類,我們創建訓練集和測試集實例。我們假設隨機裁剪操作會輸出形狀中的圖像
320×480個
320×480個 .
下面,我們可以看到訓練和測試集中保留的示例數。
crop_size = (320, 480)
voc_train = VOCSegDataset(True, crop_size, voc_dir)
voc_test = VOCSegDataset(False, crop_size, voc_dir)
read 1114 examples
read 1078 examples
我們將批處理大小設置為64,並為訓練集和測試集定義迭代器。打印第一個小批量的形狀。與圖像分類和對象識別不同,這里的標簽是三維數組。
batch_size = 64
train_iter = gluon.data.DataLoader(voc_train, batch_size, shuffle=True,
last_batch='discard',
num_workers=d2l.get_dataloader_workers())
for X, Y in train_iter:
print(X.shape)
print(Y.shape)
break
(64, 3, 320, 480)
(64, 320, 480)
2.4. Putting All Things Together
最后,我們下載並定義數據集加載程序。
#@save
def load_data_voc(batch_size, crop_size):
"""Download and load the VOC2012 semantic dataset."""
voc_dir = d2l.download_extract('voc2012', os.path.join(
'VOCdevkit', 'VOC2012'))
num_workers = d2l.get_dataloader_workers()
train_iter = gluon.data.DataLoader(
VOCSegDataset(True, crop_size, voc_dir), batch_size,
shuffle=True, last_batch='discard', num_workers=num_workers)
test_iter = gluon.data.DataLoader(
VOCSegDataset(False, crop_size, voc_dir), batch_size,
last_batch='discard', num_workers=num_workers)
return train_iter, test_iter
3. Summary
語義分割研究如何將圖像分割成具有不同語義類別的區域。
在語義分割領域,一個重要的數據集是Pascal VOC2012。
由於語義分割中的輸入圖像和標簽在像素級有一對一的對應關系,所以我們將它們隨機裁剪成固定的大小,而不是縮放它們。