졸업작품의 데이터셋이 너무 작아 Augmentation을 주어 크기를 키워 훈련 효율을 늘려야 하는 상황에 봉착했다. 또한 나무처럼 정확하게 구간을 정하기 힘든 라벨들이 회귀에 어려움을 주고 있었다.
우리 프로젝트는 도로 인식이 주 목표이기 때문에 데이터 이미지의 하단부분만 잘라내어 사용하기로 계획을 변경했다. 또한 Augmentation으로 데이터셋을 확장하기 위해 잘라낸 부분을 다시 여러 각도로 돌렸다.
Labelme의 어떤 이미지 파일에 대한 Label JSON 구조는 다음과 같다.
{
"version": "4.5.9",
"flags": {},
"shapes": [
{
"label": "_background_",
"points": [
[
200,
315
],
[
200,
400
],
[
639,
400
],
[
639,
315
]
],
"group_id": null,
"shape_type": "polygon",
"flags": {}
}, ...],
"imagePath": "1_taken-00720.jpg",
"imageData": "BLOB 데이터",
"imageHeight": 480,
"imageWidth": 640
}
이미지를 잘라내면서 없어지는 Label들에 대한 처리, 잘라낸 후 남아있는 지점들에 대해 새 이미지 크기에 따라 변하는 Point의 좌표들을 수정해야한다. 우리가 필요로한 부분은
shapes 아래의
label과
points들이다. Label의 이름은
label로, 그 Label의 각 점은
points에 X, Y 값으로 보관된다. 여기서 유의할
점은 Points 들의 시작
점이 이미지의 좌측 상단부터 시작한다는 점이다.
def crop_process_labelme(path: str, x1: float, y1: float, x2: float, y2: float):
'''
path - Directory that contains labelme JSONs
x1 - the cropping point of left top width from image
y1 - the cropping point of left top height from image
x2 - the cropping point of bottom right width from image
y2 - the cropping point of bottom right height from image
'''
processed_dir = os.path.join(path, 'processed')
paths = glob(os.path.join(path, '*.json'))
if not os.path.exists(processed_dir):
os.mkdir(processed_dir)
for path in paths:
with open(path, 'r') as f:
print('Processing : ', os.path.basename(path))
tmp = json.load(f)
i = 0
while True:
try:
shape = tmp['shapes'][i]
min_x = tmp['imageWidth'] + 1
max_x = -1
min_y = tmp['imageHeight'] + 1
max_y = -1
points = shape['points']
for point in points:
min_x = min(point[0], min_x)
max_x = max(point[0], max_x)
min_y = min(point[1], min_y)
max_y = max(point[1], max_y)
left = (x1 - max_x) > 0
right = (x2 - min_x) > 0
top = (y1 - max_y) > 0
bottom = (y2 - min_y) > 0
if top and bottom or left and right:
# if every points of a shape is out of cutting range,
# then it will not appear in a cropped picture. we don't need this.
print(tmp['shapes'][i]['label'], max_x, min_x, max_y, min_y)
del tmp['shapes'][i]
else:
print('Cliping : ', i)
for point in points:
# Clipping points
if int(point[0]) >= x2:
point[0] = x2
if int(point[1]) >= y2:
point[1] = y2
if int(point[0]) <= x1:
point[0] = x1
if int(point[1]) <= y1:
point[1] = y1
i += 1
except IndexError:
break
with open(os.path.join(processed_dir, os.path.basename(path)), 'w') as f:
json.dump(tmp, f, indent=2)
x1, y1, x2, y2는 잘라낼 구간의 좌표 변수이다.
shape의 모든 X, Y
points의 최대, 최소 값을 구한다.
shape의 x, y 최대/최소값이 x1, y1, x2, y2 (잘라낼 구간)에 들어오지 않는다면 해당 shape을 삭제한다. 남은
shape의
points들은 자르고자 하는 범위 내 값으로 Clipping한다.
Labelme가 제공하는
VOC 데이터셋 생성하는 스크립트는 JSON 안에 있는 이미지 메타데이터(imageData, imageHeight, imageWidth)를 이용하여 Label을 생성한다. 그래서 다음과 같이 원래 이미지의 해상도에서, 자른 구간만이 포함된 label 이미지가 생성된다.
|
새롭게 나온 Label |
이제 Pillow를 사용해 필요한 부분만을 가져온다.
def crop_image_voc(path: str, x1: float, y1: float, x2: float, y2: float):
'''
path - VOC Dataset root dir that generated by labelme2voc
x1 - the point of left top width from image
y1 - the point of left top height from image
x2 - the point of bottom right width from image
y2 - the point of bottom right height from image
'''
processed_dir = os.path.join(path, 'processed')
filenames = glob(os.path.join(path, 'SegmentationClassPNG', '*.png'))
filenames.extend(glob(os.path.join(path, 'JPEGImages', '*.jpg')))
img = None
if not os.path.exists(processed_dir):
os.mkdir(processed_dir)
for filename in filenames:
category = os.path.split(os.path.split(filename)[0])[-1]
if not os.path.exists(os.path.join(processed_dir, category)):
os.mkdir(os.path.join(processed_dir, category))
img = Image.open(filename)
img = img.crop((x1, y1, x2, y2))
img.save(os.path.join(os.path.join(processed_dir, category), os.path.basename(filename)))
잘라낸 이미지를 OpenCV로 회전시킨다.
def rotate_images_voc(path: str, angles: list):
'''
path - VOC Dataset root dir that generated by labelme2voc
angles - degrees of image rotation
'''
def rotate_image_cv2(image, angle, flag):
# https://stackoverflow.com/questions/9041681/opencv-python-rotate-image-by-x-degrees-around-specific-point
image_center = tuple(np.array(image.shape[1::-1]) / 2)
rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0)
result = cv2.warpAffine(image, rot_mat, image.shape[1::-1], flags=flag)
return result
label_root = os.path.join(path, 'SegmentationClassPNG')
image_root = os.path.join(path, 'JPEGImages')
label_processed_dir = os.path.join(label_root, 'processed')
image_processed_dir = os.path.join(image_root, 'processed')
if not os.path.exists(label_processed_dir):
os.mkdir(label_processed_dir)
if not os.path.exists(image_processed_dir):
os.mkdir(image_processed_dir)
labels = glob(os.path.join(label_root, '*.png'))
images = glob(os.path.join(image_root, '*.jpg'))
for path in labels:
for a in angles:
lbl = Image.open(path).convert("P")
palette = lbl.getpalette()
lbl = np.array(lbl)
rotated_lbl = rotate_image_cv2(lbl, a, cv2.INTER_NEAREST)
new_name = os.path.basename(path).split('.')
new_name[0] = new_name[0] + '_' + str(a)
new_name = '.'.join(new_name)
rotated_lbl = Image.fromarray(rotated_lbl)
rotated_lbl = rotated_lbl.convert("P")
rotated_lbl.putpalette(palette)
rotated_lbl.save(os.path.join(label_processed_dir, new_name))
for path in images:
for a in angles:
img = cv2.imread(path)
rotated_img = rotate_image_cv2(img, a, cv2.INTER_LINEAR)
new_name = os.path.basename(path).split('.')
new_name[0] = new_name[0] + '_' + str(a)
new_name = '.'.join(new_name)
cv2.imwrite(os.path.join(image_processed_dir, new_name), rotated_img)
label_root와
image_root는 스크립트로 생성된 Label 이미지와, 원본 이미지의 위치다. Palette 데이터가 손실되지 않도록 Interpolation을 Nearest로 선택한다. Label 데이터인 경우 회전 후 저장 시 Labelme에 의해 생성된 Palette 데이터를 복구한 후 저장한다.
혹여나 회전하면서 생기는 검은 뒷배경이 나오지 않도록, 회전 후 이미지의 중앙 부분만을 잘라서 저장하는 방법이 필요로 하다면
다음 링크를 참고하면 될 것 같다.