CVAD

[VOC PASCAL 2012] Semantic segmentation 하기 - 1 본문

Segmentation

[VOC PASCAL 2012] Semantic segmentation 하기 - 1

_DK_Kim 2024. 2. 27. 14:27

PASCAL VOC dataset은 다양한 컴퓨터 비전 task를 위한 벤치마크 dataset이다. 다양한 논문에서 사용되었고, dataset을 가져오는 방법도 간단하다.

 

이번 포스팅 시리즈는 이 VOC dataset을 이용하여 다양한 모델들의 성능을 비교해보는 것과 다양한 조건들을 바꿔보았을 때 성능에 어떤 영향을 미치는지 살펴볼 것이다.


1. VOC dataset 준비하기

 

http://host.robots.ox.ac.uk/pascal/VOC/

 

The PASCAL Visual Object Classes Homepage

2006 10 classes: bicycle, bus, car, cat, cow, dog, horse, motorbike, person, sheep. Train/validation/test: 2618 images containing 4754 annotated objects. Images from flickr and from Microsoft Research Cambridge (MSRC) dataset The MSRC images were easier th

host.robots.ox.ac.uk

 

VOC dataset은 위의 링크를 통해 웹에서 다운받을 수도 있지만, 이번에는 torchvision을 사용하여 다운받아보자.

Jupyter notebook을 사용하여 아래의 코드를 실행시켜보자.

 

import torch
import cv2
import os
import PIL.Image as Image
import matplotlib.pyplot as plt
import torchvision.transforms as T
import numpy as np
from tqdm.auto import tqdm
from torchvision.datasets import VOCSegmentation

# Get PASCAL VOC 2012 Segmentation data

data_path = '../../Data'

ds_train = VOCSegmentation(root=data_path, year='2012', image_set='train', download=False)
ds_valid = VOCSegmentation(root=data_path, year='2012', image_set='val', download=False)

print(f'num of train : {len(ds_train)} || num of valid : {len(ds_valid)}')

=======================================================================================
num of train : 1464 || num of valid : 1449

 

train dataset은 1464 쌍의 이미지와 마스크 이미지로  구성되었고, validate dataset은 1449개의 쌍으로 구성되어있다.

실제 데이터를 보기 위해 train 이미지에서 한 쌍을 가져와 보자. 그리고 테스트 마스크 이미지를 한 장 저장한다.

 

# Check train dataset
img, lbl = ds_train.__getitem__(0)

plt.figure(figsize=(12,10))
plt.subplot(1,2,1)
plt.imshow(img)
plt.subplot(1,2,2)
plt.imshow(lbl)
plt.show()

lbl.save('test_image_0.png')

 

 

마스크 이미지를 보면 총 4개의 class로 표현된 것을 확인할 수 있다. 검은색은 background로, 사람은 복숭아색(?), 비행기는 빨간색, 그리고 각 object의 경계는 하얀색으로 표현되어있다.

 

VOC dataset은 1-20까지 pixel 값으로 정의된 class와 0(background), 255(boundary line)으로 구성된 class value를 갖고있다.

pixel 값에 대한 class 정보는 아래와 같다.

 

Class Background Aeroplane Bicycle Bird Boat Bottle
Value 0 1 2 3 4 5
Class Bus Car Cat Chair Cow Diningtable
Value 6 7 8 9 10 11
Class Dog Horse Motorbike Person Pottedplant Sheep
Value 12 13 14 15 16 17
Class Sofa Train TV monitor Boundary line    
Value 18 19 20 255    

 

학습을 진행하기 위해서 Boundary line의 pixel value를 21로 수정해주는 것이 좋을 것 같다.

 

이제 테스트 이미지를 다시 불러와서, 실제 class value와 일치하는지 확인해보자.

 

new_lbl = Image.open('test_image_0.png')
new_lbl = np.array(new_lbl)

print(f' mask image shape : {new_lbl.shape}')
np.unique(new_lbl)
=======================================================================================
 mask image shape : (281, 500)
array([  0,   1,  15, 255], dtype=uint8)

 

사람(15), 비행기(1), background(0), 경계선(255)가 모두 포함되어있는 걸 보면 데이터셋을 저장하고 불러오는 과정에는 문제가 없는 것으로 보인다.

 

이제 특정 경로에 데이터들을 저장해주자.

 

ds_list = [ds_train, ds_valid]
data_save_path = '../../Data/VOCseg'

for ds in ds_list:
    if ds == ds_train:
        img_save_path = data_save_path + '/train/image'
        lbl_save_path = data_save_path + '/train/label'
    else:
        img_save_path = data_save_path + '/valid/image'
        lbl_save_path = data_save_path + '/valid/label'

    if os.path.exists(img_save_path) and os.path.exists(lbl_save_path):
        pass
    else:
        os.makedirs(img_save_path)
        os.makedirs(lbl_save_path)
    
    for i in tqdm(range(len(ds)), total=len(ds)):
        img, lbl = ds.__getitem__(i)
        img.save(img_save_path + f'/image_{i}.jpg')
        lbl.save(lbl_save_path + f'/image_{i}.png')
 ======================================================================================
100%|██████████| 1464/1464 [00:13<00:00, 111.05it/s]
100%|██████████| 1449/1449 [00:17<00:00, 82.82it/s]

2. Image 분석

 

이제, Image data를 분석해보자. 여기서 살펴볼 것은 평균과 표쥰편차다. 분석에 앞서, 저장된 data들을 불러오기 위한 Dataset class를 정의해주자. python 파일을 열어 아래의 코드를 저장해주자.

 

import torch
import PIL.Image as Image
import numpy as np
import os
import re
import torchvision.transforms.functional as F

from torch.utils.data import Dataset



def sorter(text):
    num = re.findall(r'\d+', text)
    return int(num[0]) if num else 0


class _VOCdataset(Dataset):
    def __init__(self, mode, transform=None):
        self.mode = mode
        self.img_path = f'../../Data/VOCseg/{self.mode}/image'
        self.lbl_path = f'../../Data/VOCseg/{self.mode}/label'
        self.img_list = sorted(os.listdir(self.img_path), key=sorter)
        self.lbl_list = sorted(os.listdir(self.lbl_path), key=sorter)
        self.transform = transform

    def __len__(self):
        return len(self.img_list)
    
    def __getitem__(self, idx):
        img = Image.open(os.path.join(self.img_path, self.img_list[idx]))
        img = np.array(img)
        lbl = Image.open(os.path.join(self.lbl_path, self.lbl_list[idx]))
        lbl = np.array(lbl)

        # Make Numpy (H W C) => Tensor(C H W)
        img = img.transpose(2,0,1)
        img = torch.from_numpy(img)/255.0
        lbl = torch.from_numpy(lbl)
        lbl = lbl.unsqueeze(0).to(torch.float)

        sample = {'image' : img, 'label' : lbl}

        if self.transform:
            sample = self.transform(sample)
        return sample

 

다시 Jupyter Notebook으로 돌아와, 아래의 코드를 실행시켜주자.

 

import torch
from tqdm import tqdm
from Dataset import _VOCdataset

# 데이터셋을 초기화합니다.
ds_train = _VOCdataset(mode='train')
ds_valid = _VOCdataset(mode='valid')

def calculate_mean_std(ds):
    sum_rgb = torch.tensor([0.0, 0.0, 0.0])
    sum_rgb_squared = torch.tensor([0.0, 0.0, 0.0])
    n_pixels = 0
    
    for i in tqdm(range(len(ds)), desc="Calculating mean and std"):
        sample = ds.__getitem__(i)
        img = sample['image']
        sum_rgb += img.float().sum(dim=[1,2])
        sum_rgb_squared += (img.float() ** 2).sum(dim=[1,2])
        n_pixels += img.shape[1] * img.shape[2]
    
    mean = sum_rgb / n_pixels
    std = (sum_rgb_squared / n_pixels - mean ** 2) ** 0.5
    return mean, std

mean_train, std_train = calculate_mean_std(ds_train)
mean_valid, std_valid = calculate_mean_std(ds_valid)

print(f"Training set mean: {mean_train}, std: {std_train}")
print(f"Validation set mean: {mean_valid}, std: {std_valid}")
=======================================================================================
Calculating mean and std: 100%|██████████| 1464/1464 [00:03<00:00, 399.20it/s]
Calculating mean and std: 100%|██████████| 1449/1449 [00:02<00:00, 535.71it/s]
Training set mean: tensor([0.4567, 0.4425, 0.4077]), std: tensor([0.2722, 0.2690, 0.2843])
Validation set mean: tensor([0.4565, 0.4383, 0.4010]), std: tensor([0.2688, 0.2672, 0.2830])

 

Image dataset의 RGB 채널별 평균값은 0.4567, 0.4425, 0.4077정도의 값을 갖고있고 표준편차는 0.2722, 0.2690, 0.2843 정도다. validation dataset의 경우도 이 값과 거의 비슷한 값을 갖고 있다.

 

추후 학습에서 이 값을 사용해서 Normalize를 진행할 것이다.


3. Mask Image 분석

이제 Mask Image에 대해 분석해보자. 여기서 분석할 내용은 class의 분포를 알아보는 것이다.

아래의 코드를 실행하자.

 

import matplotlib.pyplot as plt

ds_list = [('train', ds_train), ('valid', ds_valid)]

class_pixel_counts = {'train': {}, 'valid': {}}

for mode, ds in ds_list:
    total_class = class_pixel_counts[mode]
    for i in tqdm(range(len(ds)), desc=f"Processing {mode} dataset"):
        _, sample = ds[i] 
        img, lbl = sample['image'], sample['label']
        uniqs, counts = torch.unique(lbl, return_counts=True)
        _uniq = torch.unique(img)

        for cls, count in zip(uniqs, counts):
            cls = cls.item()
            if cls not in total_class:
                total_class[cls] = count.item()
            else:
                total_class[cls] += count.item()

def plot_class_distributions(class_pixel_counts, title):
    classes = list(range(21)) + [255]  
    pixel_counts = [class_pixel_counts.get(cls, 0) for cls in classes]  
    x_pos = list(range(21)) + [21]  
    
    plt.figure(figsize=(10, 8))  
    plt.bar(x_pos, pixel_counts, color='skyblue', tick_label=classes)  
    plt.xlabel('Class ID')
    plt.ylabel('Pixel Count')
    plt.title(title)
    plt.xticks(x_pos, classes) 
    plt.show()

plot_class_distributions(class_pixel_counts['train'], 'Train Dataset Class Pixel Distribution')
plot_class_distributions(class_pixel_counts['valid'], 'Valid Dataset Class Pixel Distribution')

 

 

결과를 살펴보면, 0(background)의 비율이 압도적으로 높다. 그리고 경계선(255), 사람(15), 고양이(8)의 class 비율이 높은 것을 확인할 수 있다. 그리고 train과 validation의 class 분포가 비슷하다.

 

앞서, image data 분석에서도 두 dataset의 평균과 표준 편차가 비슷했었고, 분포 또한 비슷한 것으로 보아 train에서의 성능이 validation의 성능과 비슷하게 나타낼 것이라고 예측된다.


여기까지 VOC dataset을 준비하고 간단한 데이터 분석을 해보는 절차를 알아보았다.

유명한 벤치마크인만큼 dataset이 깔끔하고 사용하기 좋게 제공되어있다. 특히, train과 validation간 평균값이나 class 분포같은

값들이 비슷하다는 점은 상당히 유용한데, 앞서 언급했듯이 두 dataset값의 성질이 비슷할 수록 학습에서의 성능이 검증으로 직결될 확률이 높기 때문이다.

 

사실, 더 정밀한 분석을 위해서 각 이미지의 크기, 클래스 간의 상관 관계, background와 object 비율 분석 등 다양한 분석 절차를 진행하면 좋지만 이번 포스팅에서는 간단하게 구현하는 것이 목적이기 때문에 이런 분석은 진행하지 않았다.

 

다음 포스팅은 데이터 전처리 및 증강에 대한 내용을 다뤄보겠다.

 

포스팅에 대한 질문이나 잘못된 사항에 대한 부분 답글로 적어주시면 감사하겠습니다.

728x90