귀퉁이 서재

컴퓨터 비전 - 8. 객체 탐지를 위한 데이터셋: Pascal VOC와 MS COCO 본문

딥러닝 컴퓨터 비전

컴퓨터 비전 - 8. 객체 탐지를 위한 데이터셋: Pascal VOC와 MS COCO

Baek Kyun Shin 2022. 5. 10. 00:00

※ 이 글은 권철민 님의 딥러닝 컴퓨터 비전 완벽 가이드 강의를 바탕으로 작성했습니다.

이번 시간에는 객체 탐지를 위한 대표적인 데이터셋(Pascal VOC와 MS-COCO)에 대해 알아보고, 그 가운데 한 가지 데이터셋(Pascal VOC)의 구조를 코드로 살펴보겠습니다.

이 글에서 사용한 코드: https://github.com/BaekKyunShin/Deep-Learning-Computer-Vision/blob/master/preliminary/pascal_voc_dataset.ipynb


객체 탐지를 위한 데이터셋

여러 객체 탐지와 세그멘테이션 딥러닝 패키지는 주로 세 가지 데이터셋을 바탕으로 사전 훈련되어 배포됩니다. 세 가지 데이터셋은 PASCAL VOC, MS COCO, 구글 Open Images입니다. 하나씩 살펴보죠.

1. PASCAL VOC 데이터셋

PASCAL VOC 데이터셋은 20개의 클래스를 포함하는 데이터셋입니다. 객체 탐지나 세그멘테이션을 위해 제공하는 데이터셋죠. 이미지와 더불어 annotation 파일도 제공합니다. annotation이란 주석이라는 뜻으로, 이미지에 관한 별도 정보라고 보시면 됩니다. 가령, 경계 박스 좌표나 객체의 클래스명, 이미지 크기 등의 정보를 말합니다. PASCAL VOC 데이터셋은 annotation을 xml 형태로 제공합니다. 

PASCAL VOC 데이터셋은 2005부터 2012까지 총 8가지 버전이 있는데 여기서는 2012 데이터셋을 사용하겠습니다. 참고로, PASCAL VOC이 제공하는 annotation에는 경계 박스 좌표가 (좌상단 x 좌표, 좌상단 y 좌표, 우하단 x 좌표, 우하단 y 좌표)로 이루어져 있습니다.

2. MS COCO 데이터셋

MS COCO 데이터셋은 총 80개의 클래스를 갖습니다. 30만 개 이미지와 150만 개 객체가 들어있죠. 한 이미지당 평균적으로 객체 5개를 포함한다는 말입니다. 한 이미지가 여러 객체를 가지고 있어 다른 데이터셋에 비해 객체 탐지 난이도가 높은 편입니다.

MS COCO 데이터셋은 annotation을 json 형태로 제공하며, PASCAL VOC와 다르게 경계 박스 좌표가 (좌상단 x 좌표, 좌상단 y 좌표, 너비, 높이)로 이루어져 있습니다.

3. 구글 Open Images 데이터셋

구글 Open Images 데이터셋은 총 600개의 클래스를 갖습니다. 이 데이터셋은 용량이 매우 커서 학습하는 데 시간이 오래 걸립니다. 참고로, annotation은 csv 형태로 제공합니다.


PASCAL VOC 2012 탐색하기 실습

세 가지 데이터셋을 알아봤는데요, 이번에는 PASCAL VOC 2012 데이터셋을 다운받아 이미지와 경계 박스를 출력하는 실습을 해보겠습니다. 먼저 데이터셋을 다운받아보죠.

실습은 구글 Colab 기반으로 설명합니다.

1. PASCAL VOC 2012 데이터셋 다운로드

구글 Colab에서는 content 디렉터리가 루트 디렉터리입니다. 먼저, PASCAL VOC 2012 데이터셋을 다운로드하기 위해 content 디렉터리 아래 data 디렉터리를 만들겠습니다.

# content 디렉터리 아래에 data 디렉터리 생성
!mkdir ./data

이어서 PASCAL VOC 2012 데이터셋을 다운로드합니다. 다운로드한 PASCAL VOC 2012 데이터는 tar 파일입니다. tar 파일은 압축 파일을 뜻하죠. 이 압출 파일을 앞서 만든 content/data 디렉터리에 압축 해제해 풀겠습니다.

# pascal voc 2012 데이터를 다운로드 후 /content/data 디렉토리에 압축 해제
# DOWNLOAD시 약 3분정도 시간 소요. 아래 디렉토리가 잘 동작하지 않으면 https://web.archive.org/web/20140815141459/http://pascallin.ecs.soton.ac.uk/challenges/VOC/voc2012/VOCtrainval_11-May-2012.tar
!wget http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar
!tar -xvf VOCtrainval_11-May-2012.tar -C /content/data

데이터셋을 잘 다운로드했는지 확인해보시죠. 왼쪽 파일 탭을 클릭해보세요. 

data 디렉터리 아래 VOCdevkit 디렉터리가 있고, 그 아래 VOC2012 디렉터리가 있습니다. 다시 그 아래 Annotations, ImageSets, JPEGImages, SegmentationsClass, SegmentationObject 디렉터리가 있습니다. 여기서 주목할 디렉터리는 Annotations와 JPEGimages 디렉터리입니다. JPEGImages 디렉터리에는 객체 탐지나 세그멘테이션에 사용할 이미지 파일이 있고, Annotations 디렉터리에는 JPEGImages 디렉터리에 있는 이미지 파일과 일대일로 매칭된 annotations 파일이 xml 형식으로 들어 있습니다.

Annotations와 JPEGImages 디렉터리가 일대일로 잘 매칭되는지 살펴보죠. 먼저 Annotations 디렉터리의 첫 다섯 개 파일을 출력해보겠습니다.

!ls /content/data/VOCdevkit/VOC2012/Annotations | head -n 5

     2007_000027.xml
     2007_000032.xml
     2007_000033.xml
     2007_000039.xml
     2007_000042.xml

이어서 JPEGImages 디렉터리에 있는 첫 다섯 개 파일을 출력합니다.

!ls /content/data/VOCdevkit/VOC2012/JPEGImages | head -n 5

     2007_000027.jpg
     2007_000032.jpg
     2007_000033.jpg
     2007_000039.jpg
     2007_000042.jpg

출력 결과를 보시죠. 확장자만 xml과 jpg로 다르고 파일 이름은 똑같습니다. 곧, 2007_000027.jpg 이미지가 갖는 annotations 정보는 2007_000027.xml에 포함되어 있다는 뜻입니다.

2. JPEGImages 디렉터리에 있는 샘플 이미지 보기

이번에는 JPEGImages 디렉터리에 있는 샘플 이미지를 출력해보겠습니다.

import cv2
import matplotlib.pyplot as plt
import os

# DATA_DIR은 /content/data로 지정하고 os.path.join()으로 상세 파일/디렉토리를 지정합니다. 
DATA_DIR = '/content/data' # 기본 경로 설정 ---① 

# 이미지 경로 설정 ---②
img_path = os.path.join(DATA_DIR, 'VOCdevkit/VOC2012/JPEGImages/2007_000032.jpg')

# 이미지 출력 ---③
img = cv2.imread(img_path)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
print('img shape:', img.shape)
plt.figure(figsize=(8, 8))
plt.imshow(img_rgb);

     img shape: (281, 500, 3)

이미지가 잘 출력됐습니다. 이미지 형상은 (281, 500, 3)이네요.

코드를 들여다보죠. ① 먼저, DATA_DIR을 /content/data로 설정하고, ② os.path.join() 메서드로 샘플 이미지 경로를 설정했습니다. os.path.join() 메서드는 입력받은 인자의 경로를 결합하는 기능을 제공합니다. ③ 샘플 이미지 경로를 cv2.imread() 메서드로 읽은 뒤 이미지를 출력했습니다.

3. JPEGImages 디렉터리에 있는 샘플 이미지와 매칭되는 annotation 파일 보기

앞서 JPEGImages 디렉터리에 있는 샘플 이미지(2007_000032.jpg) 파일을 출력해봤습니다. 그럼 이 이미지와 매칭되는 annotations 파일을 살펴보겠습니다.

!cat /content/data/VOCdevkit/VOC2012/Annotations/2007_000032.xml

     <annotation>
          <folder>VOC2012</folder>
          <filename>2007_000032.jpg</filename>
          <source>
               <database>The VOC2007 Database</database>
               <annotation>PASCAL VOC2007</annotation>
               <image>flickr</image>
          </source>
          <size>
               <width>500</width>
               <height>281</height>
               <depth>3</depth>
          </size>
          <segmented>1</segmented>
          <object>
                <name>aeroplane</name>
                <pose>Frontal</pose>
                <truncated>0</truncated>
                <difficult>0</difficult>
                <bndbox>
                     <xmin>104</xmin>
                     <ymin>78</ymin>
                     <xmax>375</xmax>
                     <ymax>183</ymax>
                </bndbox>
          </object>
            ...생략...
     </annotation>

annotations 파일 구조를 간단히 살펴보죠. <annotation>이 루트 태그입니다. 이 루트 태그를 시작으로 트리 구조로 연결된다고 보시면 됩니다. <filename> 태그에 이미지 파일명(2007_000032.jpg)이 기재돼 있습니다. <size> 태그 안에 <width>, <height>, <depth> 태그가 있는데, 각각 이미지의 너비, 높이, 깊이를 뜻합니다. <object> 태그 안에는 여러 태그가 있습니다. 가령 <name> 태그는 클래스명(레이블명)을 나타냅니다. <bndbox> 태그 안에는 <xmin>, <ymin>, <xmax>, <ymax> 태그가 있습니다. 각각 좌상단 x좌표, y좌표, 우하단 x좌표, y좌표를 나타냅니다. 만약 해당 이미지에 여러 객체가 있다면 <object> 태그도 여러 개 있습니다. 여기서는 총 4개의 <object> 태그가 있는데 지면 관계상 하나만 실었습니다.

4. SegmentationObject 디렉터리에 있는 masking 이미지 보기

이번에는 SegmentationObject 디렉터리에 있는 샘플 masking 이미지를 출력해보겠습니다.

# 세그멘테이션 마스킹 이미지 경로
img_path = os.path.join(DATA_DIR, 'VOCdevkit/VOC2012/SegmentationObject/2007_000032.png')

# 세그멘테이션 마스킹 이미지 출력
img = cv2.imread(img_path)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
print('img shape:', img.shape)
plt.figure(figsize=(8, 8))
plt.imshow(img_rgb);

    img shape: (281, 500, 3)

세그멘테이션용 마스킹 이미지, 즉 객체를 정확히 나타내는 마스킹 이미지를 출력했습니다.

5. Annotation xml 파일에 있는 요소들을 파싱하여 접근하기

Annotations 디렉터리에 있는 annotation 파일은 xml 형식이라고 했습니다. xml 파일 형식은 트리 구조로 되어있습니다. 그렇기 때문에 루트 노드부터 차례로 접근할 수 있습니다. 이번에는 xml 파일 태그들을 파싱해서 접근해보겠습니다.

먼저 Annotations 디렉터리에 있는 샘플 annotation 파일명 3개와 총 annotation 파일 개수를 출력해보겠습니다.

# 경로 설정
VOC_ROOT_DIR ="/content/data/VOCdevkit/VOC2012/"
ANNO_DIR = os.path.join(VOC_ROOT_DIR, "Annotations")

# 샘플 Annotation 파일명 및 Annotation 파일 개수 출력
anno_xml_files = os.listdir(ANNO_DIR) # Annotation 파일(xml 형태) 리스트  
print('샘플 Annotation 파일명:', anno_xml_files[:3])
print('Annotation 파일 개수:', len(anno_xml_files))

     샘플 Annotation 파일명: ['2009_001431.xml', '2008_004866.xml', '2010_005816.xml']
     Annotation 파일 개수: 17125

샘플 annotations 파일명은 2009_001431.xml, 2008_004866.xml, 2010_005816.xml입니다. 각각 2009_001431.jpg, 2008_004866.jpg, 2010_005816.jpg 이미지에 대응하는 annotation이죠. 

이어서 ElementTree 패키지를 활용해 xml 태그에 접근해보겠습니다. 샘플로 2007_000032.xml 파일을 이용합시다. 

import xml.etree.ElementTree as ET

xml_file = os.path.join(ANNO_DIR, '2007_000032.xml')

tree = ET.parse(xml_file) # xml 파일을 파싱해서 요소(Element)를 트리로 반환
root = tree.getroot() # 트리의 루트 노드(<annotattion>)

root # 루트 출력

     <Element 'annotation' at 0x7f8a6a0cef50>

ET.parse() 메서드는 전달받은 xml 파일을 파싱해 트리로 반환합니다. 이를 tree 변수에 저장했습니다. tree.getroot()를 실행하면 해당 트리의 루트 노드를 반환합니다. 앞서 살펴봤듯이 annotation xml 파일의 루트 노드는 <annotation>입니다.

이렇게 구한 루트 노드를 활용해 이미지 절대 경로와 이미지 크기를 출력해보겠습니다.

IMAGE_DIR = os.path.join(VOC_ROOT_DIR, "JPEGImages")

# image 관련 정보는 root의 자식으로 존재
image_name = root.find('filename').text # <filename> 노드의 텍스트 ---①
full_image_name = os.path.join(IMAGE_DIR, image_name) # 이미지의 절대 경로

image_size = root.find('size') # <size> 노드
# <width> 노드의 텍스트를 int로 변환 ---②
image_width = int(image_size.find('width').text) 
# <height> 노드의 텍스트를 int로 변환 ---③
image_height = int(image_size.find('height').text) 

print('이미지 절대 경로:', full_image_name)
print('이미지 크기:', (image_width, image_height))

     이미지 절대 경로: /content/data/VOCdevkit/VOC2012/JPEGImages/2007_000032.jpg
     이미지 크기: (500, 281)

① root.find('filename')은 루트 아래에 있는 <filename> 태그를 찾는 메서드입니다. 앞서 살펴본 xml 구조에서 <filename> 태그에는 2007_000032.jpg가 저장돼 있었죠? root.find('filename').text를 호출하면 '2007_000032.jpg'를 반환합니다. 바로 이미지 파일명이죠. 이미지 파일명을 image_name에 저장했습니다. IMAGE_DIR과 image_name을 활용해 이미지의 절대 경로를 full_image_name에 할당했습니다.

이어서 root.find('size') 메서드로 <size> 태그를 찾았습니다. 코드 ②에선 <size> 태그 아래에 있는 <width> 태그를 찾아 이미지의 너비 값을 구했습니다. 코드 ③에서는 <size> 태그 아래에 있는 <height> 태그를 찾아 이미지의 높이 값을 구했습니다.

다음으로 객체를 나타내는 <object> 태그에서 경계 박스 정보를 얻어보겠습니다.

objects_list = [] # <object> Element를 담을 리스트 초기화

for obj in root.findall('object'): # 모든 <object> 요소를 순회 ---①
    # <object> 요소의 자식 요소에서 <bndbox>를 찾음 ---②
    bndbox = obj.find('bndbox')
    # <bndbox> 요소의 자식 요소에서 xmin,ymin,xmax,ymax를 찾아 텍스트 추출 ---③
    x1 = int(bndbox.find('xmin').text) # 좌상단 x좌표
    y1 = int(bndbox.find('ymin').text) # 좌상단 y좌표
    x2 = int(bndbox.find('xmax').text) # 우하단 x좌표
    y2 = int(bndbox.find('ymax').text) # 우하단 y좌표
    
    bndbox_pos = (x1, y1, x2, y2) # 경계 박스 좌표
    class_name=obj.find('name').text # 클래스명(레이블명)
    object_dict={'class_name': class_name, 'bndbox_pos':bndbox_pos}
    objects_list.append(object_dict) # objects_list에 {클래스명, 경계 박스 좌표} 추가

① root.findall('object')는 루트 노드 아래에 있는 모든 <object> 태그를 반환합니다. <object> 태그는 객체 정보를 담은 태그죠. 모든 <object> 태그를 순회하면서 ② 경계 박스를 나타내는 <bndbox> 태그를 찾고, ③ <bndbox> 태그에서 좌상단 x, y좌표, 우하단 x, y좌표를 구합니다. 이어서 클래스명과 경계 박스 좌표를 딕셔너리 형태로 만들고, 이 딕셔너리를 objects_list에 추가했습니다.

objects_list에 객체 정보가 잘 담겼는지 출력해볼까요?

for object in objects_list:
    print(object)

     {'class_name': 'aeroplane', 'bndbox_pos': (104, 78, 375, 183)}
     {'class_name': 'aeroplane', 'bndbox_pos': (133, 88, 197, 123)}
     {'class_name': 'person', 'bndbox_pos': (195, 180, 213, 229)}
     {'class_name': 'person', 'bndbox_pos': (26, 189, 44, 238)}

딕셔너리가 총 네 개 출력됐네요. 이 말은 2007_000032.jpg 이미지에 객체가 총 네 개 있다는 뜻입니다. 구체적으로는 비행기(aeroplane) 두 개와 사람(person) 두 명이 있다는 말입니다. 각 객체 위치를 나타내는 경계 박스 좌표도 같이 출력했습니다.

6. 경계 박스 시각화

마지막으로 경계 박스를 시각화해보겠습니다.

img = cv2.imread(full_image_name)
img_copy = img.copy() # 복사본 ---①

green_color = (0, 255, 0)
red_color = (0, 0, 255) # OpenCV는 RGB가 아니라 BGR이므로 빨간색은 (0, 0, 255)

# 파일내에 있는 모든 object Element를 찾음.
objects_list = []
for obj in root.findall('object'): # 모든 <object> 요소를 순회
    bndbox = obj.find('bndbox') # 경계 박스
    x1 = int(bndbox.find('xmin').text) # 좌상단 x좌표
    y1 = int(bndbox.find('ymin').text) # 좌상단 y좌표
    x2 = int(bndbox.find('xmax').text) # 우하단 x좌표
    y2 = int(bndbox.find('ymax').text) # 우하단 y좌표
    class_name=obj.find('name').text # 클래스명(레이블명)
    
    # 경계 박스를 초록색으로 표시 ---②
    cv2.rectangle(img_copy, (x1, y1), (x2, y2), color=green_color, thickness=1)
    # 경계 박스 좌상단 좌표에 빨간색으로 클래스명 표시 ---③
    cv2.putText(img_copy, class_name, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, red_color, thickness=1)

img_copy = cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(10, 10))
plt.imshow(img_copy);

① 우선, img의 복사본을 만듭니다. opencv의 rectangle() 메서드는 인자로 들어온 원본 이미지 배열에 그대로 사각형을 그려줍니다. 원본 이미지를 훼손하지 않으려면 복사본을 만들어서 작업하는 게 좋겠죠?

앞서 다룬 코드와 마찬가지로 모든 <object> 태그를 순회하면서 경계 박스의 좌상단 x, y 좌표, 우하단 x, y 좌표, 그리고 클래스명을 구합니다. 코드 ②로 경계 박스를 초록색으로 표시합니다. 코드 ③으로 경계 박스 좌상단 좌표 위에 빨간색으로 클래스명을 표시합니다. 이때 텍스트 위치를 (x1, y1)이 아니라 (x1, y1 - 5)로 설정했습니다. 경계 박스보다 약간 위에 글자를 표시하려고 5만큼 뺐습니다(이미지의 좌상단이 (0, 0)이므로 y 좌표값이 작을수록 위쪽으로 간다는 점 명심하세요!).

이상으로 PASCAL VOC, MS COCO, 구글 Open Images 데이터셋을 알아보고, PASCAL VOC를 활용해 annotation 정보를 살펴본 뒤, 경계 박스를 직접 그려봤습니다.

0 Comments
댓글쓰기 폼