title: AIVLE TIL (‘23.03.22) 3차 미니프로젝트 4일차 date: 2023-03-23 13:35:17.360 +0000 categories: [에이블스쿨] tags: [‘aivle’] description: Object Detection을 위한 데이터 전처리 image: /assets/posts/2023-03-23-aivle-til-230322-3차-미니프로젝트-4일차/thumbnail.png
무엇을 했나?
Object Detection 전처리
- CNN Layer의 모델링을 한 차례 경험했으니, 4일차 부터는 Object Detection을 진행하였다.
- 그 중 4일차는 yolo 모델을 위한 데이터 전처리를 수행하였다.
- yolo 모델의 요구사항에 맞도록 데이터 구조를 맞추었다.
- json으로 작성된 annotation을 txt 파일로 변환하였다.
- 실무에서 데이터 전처리는 전체 딥러닝 과정의 80~95%를 차지하는 매우 중요한 과정으로, 모델에에 따라 적절하게 데이터를 나누고, lable을 생성하는 연습을 진행하였다.
데이터 분리
- 학습용 데이터와 검증용 데이터를 이미지, 레이블끼리 분류하는 과정이다.
클래스별로 분류된 이미지, 레이블 샘플링
- 가장 먼저 yolo 모델에 맞춘 디렉토리 구조를 생성하였다.
1
2
3
4
5
6
| # 구조에 맞는 디렉토리 생성
!mkdir /content/Dataset/images;
!mkdir /content/Dataset/images/train; mkdir /content/Dataset/images/val
!mkdir /content/Dataset/labels;
!mkdir /content/Dataset/labels/train; mkdir /content/Dataset/labels/val
|
1
2
3
4
5
6
7
8
9
10
11
| # Dataset metadata 입력
won_list = [디렉토리 리스트]
# 파일 담아오기
image_files = dict()
for won in won_list:
image_files[won] = glob.glob(data_path+won+'/*.jpg')
json_files = dict()
for won in won_list:
json_files[won] = glob.glob(data_path+won+'/*.json')
|
학습 데이터, 검증 데이터 분류
- 8:2로 데이터를 나눌 것이므로, random 모듈을 이용해서 추출할 파일 인덱스를 샘플링하였다.
- 클래스 별로 데이터 개수가 다르기 때문에 딕셔너리와 for loop을 이용해서 클래스별 파일 개수에 맞추어 샘플링하였다.
1
2
3
4
5
6
7
| # 랜덤 샘플링 (난수고정)
random.seed(23)
random_num = dict()
for won in won_list:
test_num = round(len(image_files[won]) * 0.2)
random_num[won] = random.sample((range(len(image_files[won]))), test_num)
|
- 먼저 샘플링된 검증용 데이터를 구조에 맞게 나눈 디렉토리로 이동시켰다.
1
2
3
4
5
| # 파일 이동
for won in won_list:
for i in random_num[won]:
shutil.move(image_files[won][i], data_path+'images/val/')
shutil.move(json_files[won][i], data_path+'labels/val/')
|
- 남아있는 파일을 모두 학습용 데이터이므로 학습용 디렉토리로 이동시켰다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # 남아있는 파일 다시 담아오기 (train data)
image_files = dict()
for won in won_list:
image_files[won] = glob.glob(data_path+won+'/*.jpg')
json_files = dict()
for won in won_list:
json_files[won] = glob.glob(data_path+won+'/*.json')
# 남아있는 파일을 전부 train으로 복사
for won in won_list:
for image in image_files[won]:
shutil.copy2(image, data_path+'images/train/')
for json in json_files[won]:
shutil.copy2(json, data_path+'labels/train/')
|
데이터 전처리
이미지 크롭
- 이미지 데이터를 살펴보았을 때, 이미지의 비율(가로 크기, 세로크기)이 일정하지 않았다. 또한 대부분의 경우 object 하나가 이미지 중앙에 위치하는 형태였으므로, 1:1의 비율을 갖도록 크롭하는 과정을 진행하였다.
- 성능 개선을 노린 것도 있지만, 이미지 데이터를 처리하는 연습을 하고 싶었다.
- pillow 라이브러리를 이용하여 이미지를 처리하였다.
1
2
3
4
5
| # 더 최신 버전이 있지만 오류가 발생할 가능성이 있어 9.0.0 버전 사용
!pip install Pillow==9.0.0
import os
from PIL import Image
|
- 이미지를 크롭하는 함수를 설정하였다.
- 간단히 가로길이, 세로길이 중 더 긴 것을 찾아 짧은 쪽과 맞추는 함수이다. 이미지가 치우치는 것을 막기 위해 양옆을 동일한 비율로 잘라주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # crop 함수 설정
"""
이미지를 1:1 비율이 되도록 자르는 함수입니다.
"""
def crop(file):
img = Image.open(file)
w, h = img.size
crop_len = abs(w - h) // 2
if w == h:
img_cropped = img
elif w > h:
img_cropped = img.crop((crop_len, 0, w-crop_len, h))
else:
img_cropped = img.crop((0, crop_len, w, h-crop_len))
return img_cropped
|
- 파일을 불러와 for loop을 이용하여 이미지를 처리하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # 파일 불러오기
train_path = '/content/Dataset/images/train/*'
test_path = '/content/Dataset/images/val/*'
train_files = glob.glob(train_path)
test_files = glob.glob(test_path)
# 이미지 처리 (기존 이미지 덮어씌움)
for file in train_files:
img_cropped = crop(file)
img_cropped.save(file)
for file in test_files:
img_cropped = crop(file)
img_cropped.save(file)
|
json에서 정보 추출
- json에 저장된 사진 정보 및 bounding box 정보를 yolo 모델에 맞는 text 파일로 바꾸어주어야 한다.
- yolo에서 요구하는 양식은 각 바운딩 박스가
class x_center y_center width, height
순서로 저장되어야 한다. 또한 0~1의 범위로 normalization이 이루어져 있어야 한다.
yolo document에서 제공하는 가이드
- 해당 양식에 맞게 데이터를 전처리하는 함수를 만들었다.
- 원래 클래스는 16개로 앞면과 뒷면을 구분하였지만 앞뒷면 구분을 없애고 8개 클래스로 만들어주었다.
- 이미지는 1:1로 crop하였으나 annotation은 그대로이므로, crop한 이미지에 맞추어서 정규화하는 작업이 다소 번거로웠다.
- 최종적으로 yolo annotation 형식에 맞는 문자열을 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| def json_to_txt(file, classes):
result = dict()
with open(file, mode='r') as f:
data = json.load(f)
# 이미지 처리를 위한 변수 설정
points = data['shapes'][0]['points']
x1, y1 = points[0]
x2, y2 = points[1]
width, height = data['imageWidth'], data['imageHeight']
# 라벨 획득 (앞면, 뒷면 구분 제거)
text = data['shapes'][0]['label']
idx = text.rfind('_')
label = text[:idx]
# crop 반영
crop_len = abs(width-height) // 2
if width > height:
width = height
x1 = max(x1-crop_len, 0)
x2 = min(x2-crop_len, width)
elif width < height:
height = width
y1 = max(y1-crop_len, 0)
y2 = min(y2-crop_len, height)
# 정규화 (0 ~ 1 min-max)
x1 = x1 / width
x2 = x2 / width
y1 = y1 / height
y2 = y2 / height
# x, y, w, h 계산
x = (x1 + x2) / 2
y = (y1 + y2) / 2
w = (x2 - x1)
h = (y2 - y1)
# 정해진 형식의 문자열로 반환
return f'{classes[label]} {x} {y} {w} {h}'
|
- 이제 처리된 문자열을 텍스트 파일로 만들어 저장하였다.
- 기존의 json 파일 이름을 이용해 txt 파일을 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| for file in train_files:
annot = json_to_txt(file, classes)
txt_file = file.replace('.json', '.txt')
with open(txt_file, mode='w') as f:
text = annot
f.write(text)
for file in test_files:
annot = json_to_txt(file, classes)
txt_file = file.replace('.json', '.txt')
with open(txt_file, mode='w') as f:
text = annot
f.write(text)
|
yaml 파일 작성
마지막으로 yolo 모델에게 파일 위치와 클래스 종류 등을 전달하는 yaml 파일을 작성하였다.
yolo document에서 알려주는 yaml파일의 형식은 다음과 같다. ```yaml
Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: ../datasets/coco128 # dataset root dir train: images/train2017 # train images (relative to ‘path’) 128 images val: images/train2017 # val images (relative to ‘path’) 128 images test: # test images (optional)
Classes (80 COCO classes)
names: 0: person 1: bicycle 2: car … 77: teddy bear 78: hair drier 79: toothbrush
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
- PyYAML 라이브러리를 이용해, 딕셔너리 형식으로 필요한 양식을 작성하였다.
```python
document = {
'path' : '/content/drive/MyDrive/my_data/220320_mini_project_money/images/',
'train' : 'train',
'val' : 'val',
'nc' : 8,
'names' : won_dict
}
# dump 함수를 이용하여 저장
with open('/content/drive/MyDrive/my_data/220320_mini_project_money/money.yaml', 'w') as f :
yaml.dump(document, f)
|
- 이제 모델 학습 및 평가를 위한 전처리 작업이 끝났다.
- 5일차에는 모델링 및 평가를 수행할 것이다.
소감
좋았던 점
- 단순히 데이터를 분리하고, annotation 파일을 만드는 것 외에 이미지 crop 등 별도의 처리를 하는 과정을 직접 시도해보았다. 많은 연습이 된 것 같다.
아쉬운 점
만약 이 모델을 실제로 사용한다면 object가 회전되어 있거나, 여러 각도에서 사진이 찍혔거나, 어느 한쪽으로 치우치고 일부만 보이는 등의 사례가 있을 것이다. 그런데 학습 데이터는 대부분 정중앙에 object 하나만 찍혀있는 형태여서 일반적인 성능이 잘 나올 수 있을지는 의문이다.
때문에 Data Augmentation을 시도해보고 싶었으나, yolo 모델 자체에 적용하는 generator는 찾을 수 없었다. 별도로 변형된 이미지를 따로 생성하면 가능하겠지만, 이는 시간 부족으로 시도하지 못했다.
train.py 파일을 뜯어보니 yolo 자체에서 augmentation을 적용하는 것을 발견하였다. 이를 적절히 조절하면 원하는 augmentation이 가능할 것으로 보인다.