프로젝트 소개 및 도매인 이해
쿨루프는 도시열섬 현상의 주요원인으로 지목되는 건물 옥상 등에 햇빛과 열의 반사 및 방사효과가 있는 밝은 색 도료 등을 시공하여 열기가 지붕에 축적되는 것을 줄이는 공법으로 옥상 바닥 온도는 10도, 건물 실내온도는 4~5℃정도 낮아지는 가장 효과적인 기후변화대책 중 하나로 알려져 있다.
우리나라에서는 쿨루프 캠페인, 사회적 협동조합 등이 있으며 각 지역에서 쿨루프 프로젝트를 진행중이다.
쿨루프 옥상 표면은 15~30도, 실내 온도는 3~4도 감소하는 효과가 있다. 이러한 실내온도 감소에 따른 냉방비는 20% 절약할 수 있고, 온실가스 배출량은 6% 감축하는 효과가 있다.
이러한 쿨루프 사업이 진행될 때 사업 대상 모집부터 시행까지의 시간이 너무 오래 걸린다고 한다. 담당 공무원들이 사업 대상 건물을 일일히 조사해야 하기 때문이다.
따라서 인공위성 데이터를 사용해 1차적으로 쿨루프 시공 대상 여부를 식별할 수 있는 분류 모델을 만드는 프로젝트를 진행했다.
데이터셋
> cool_roof_images
인공위성 옥상 이미지
> cool_roof_yolo_labels
>> obj_train_data
<class> <x> <y> <width> <height> 형식으로 되어있는 각 이미지의 label
• Class : 객체의 클래스를 나타내는 숫자
• x, y : bounding box의 중심 좌표를 이미지의 너비와 높이에 대한 비율로 표현한 값
• width, height : bounding box의 크기를 이미지의 너비와 높이에 대한 비율로 표현한 값
>> obj.names
클래스 이름
>> obj.data
>> train.txt
1. Data Preprocessing
!pip install keras --upgrade
import os
os.environ['KERAS_BACKEND'] = 'tensorflow'
import gdown, zipfile
import os, glob, shutil
from tqdm import tqdm
import numpy as np
import matplotlib.pyplot as plt
import os
import random
from PIL import Image
from sklearn.model_selection import train_test_split
import keras
import shutil
1) 데이터셋 다운로드 및 압축 해제
- cool_roof_image.zip : 이미지 데이터 압축 파일
- cool_roof_yolo_labels.zip : YOLO 모델 형식에 맞게 가공된 레이블 파일
def dataset_extract(file_name):
# 파일 경로에서 확장자를 제외한 기본 이름 추출
base_name = os.path.basename(file_name)
folder_name = os.path.splitext(base_name)[0]
# 추출된 폴더 경로 설정
extract_folder_path = f'/content/{folder_name}/'
with zipfile.ZipFile(file_name, 'r') as zip_ref:
file_list = zip_ref.namelist()
# 폴더가 이미 존재하는지 확인
if os.path.exists(extract_folder_path):
print(f'데이터셋 폴더가 이미 존재합니다: {extract_folder_path}')
return
else:
# 폴더가 존재하지 않으면 파일 추출
for f in tqdm(file_list, desc='Extracting', unit='files'):
zip_ref.extract(member=f, path=extract_folder_path)
from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive
dataset_extract('/content/drive/MyDrive/Colab Notebooks/KT_MP_4_2/cool_roof_images.zip')
Extracting: 100%|██████████| 200/200 [00:04<00:00, 47.27files/s]
dataset_extract('/content/drive/MyDrive/Colab Notebooks/KT_MP_4_2/cool_roof_yolo_labels.zip')
Extracting: 100%|██████████| 204/204 [00:00<00:00, 615.76files/s]
2) 폴더 생성
- YOLO 모델에서 요구하는 폴더의 형식에 맞춰 폴더 생성
# yolov8/data 폴더 확인 및 생성
yolov8_data_dir = 'yolov8/data'
os.makedirs(yolov8_data_dir, exist_ok=True)
# 구조에 맞는 디렉토리 생성
!mkdir /content/yolov8/data/images;
!mkdir /content/yolov8/data/images/train; mkdir /content/yolov8/data/images/val
!mkdir /content/yolov8/data/labels;
!mkdir /content/yolov8/data/labels/train; mkdir /content/yolov8/data/labels/val
3) Data Augmentation
이 부분은 모델 학습까지 모두 완료 하고 분석한 후 성능을 높이기 위해 추가한 부분이다.
인공위성 옥상 이미지에서 옥상의 사각형이 반듯하게 되어있지 않고 살짝 기울어져 있는 경우 탐지를 잘 하지 못하는 것 같아서 다양한 각도로 회전을 한 data를 추가적으로 만들어 학습했다.
angel 변수에 원하는 각도를 넣어주며 데이터를 증강할 수 있다. 약간의 수작업이 필요하다..
이 부분은 구현이 힘들어서 gpt의 도움을 약간 받았다.
데이터 증강
import os
from PIL import Image
import math
def rotate_point(x, y, angle, cx, cy):
"""
주어진 점 (x, y)를 (cx, cy)를 중심으로 angle도 회전시킨 후의 좌표를 반환.
"""
angle_rad = math.radians(angle)
x_new = cx + math.cos(angle_rad) * (x - cx) - math.sin(angle_rad) * (y - cy)
y_new = cy + math.sin(angle_rad) * (x - cx) + math.cos(angle_rad) * (y - cy)
return x_new, y_new
def rotate_bbox(bbox, angle, image_width, image_height):
"""
바운딩 박스를 주어진 각도만큼 회전시키는 함수.
"""
x_center, y_center, width, height = bbox
cx, cy = x_center * image_width, y_center * image_height # 절대 좌표로 변환
# 바운딩 박스의 네 꼭짓점 계산
points = [
(cx - width * image_width / 2, cy - height * image_height / 2), # 좌상단
(cx + width * image_width / 2, cy - height * image_height / 2), # 우상단
(cx - width * image_width / 2, cy + height * image_height / 2), # 좌하단
(cx + width * image_width / 2, cy + height * image_height / 2) # 우하단
]
# 각 점 회전
rotated_points = [rotate_point(x, y, angle, cx, cy) for x, y in points]
# 회전된 점들을 포함하는 최소 바운딩 박스 계산
min_x = min(x for x, y in rotated_points)
max_x = max(x for x, y in rotated_points)
min_y = min(y for x, y in rotated_points)
max_y = max(y for x, y in rotated_points)
# 새로운 바운딩 박스의 중심, 너비, 높이 계산
x_center_new = (min_x + max_x) / 2 / image_width
y_center_new = (min_y + max_y) / 2 / image_height
width_new = (max_x - min_x) / image_width
height_new = (max_y - min_y) / image_height
# 새로운 바운딩 박스 반환
bbox = (x_center_new, y_center_new, width_new, height_new)
return bbox
def process_image_and_bbox(image_path, bbox_file_path, output_image_path, output_bbox_path, angle):
# 이미지 회전 및 저장
image = Image.open(image_path)
rotated_image = image.rotate(angle, expand=True)
rotated_image.save(output_image_path)
image_width, image_height = rotated_image.size
# 바운딩 박스 파일 처리
new_bbox_lines = []
with open(bbox_file_path, 'r') as file:
lines = file.readlines()
for line in lines:
parts = line.strip().split()
class_id = parts[0]
bbox = tuple(map(float, parts[1:]))
rotated_bbox = rotate_bbox(bbox, angle, image_width, image_height)
# 소수점 아래 6자리로 제한하여 문자열 포맷팅
new_bbox_line = f"{class_id} {' '.join(format(coord, '.6f') for coord in rotated_bbox)}\n"
new_bbox_lines.append(new_bbox_line)
# 새 바운딩 박스 파일 저장
with open(output_bbox_path, 'w') as file:
file.writelines(new_bbox_lines)
# 경로 설정
image_folder = '/content/cool_roof_images'
bbox_folder = '/content/cool_roof_yolo_labels/obj_train_data'
output_image_folder = 'rotated_img_10'
output_bbox_folder = 'rotated_label_10'
angle = 10
# 폴더 생성
os.makedirs(output_image_folder, exist_ok=True)
os.makedirs(output_bbox_folder, exist_ok=True)
# 모든 이미지 처리
for image_name in os.listdir(image_folder):
if not image_name.endswith('.jpg'):
continue
base_name = os.path.splitext(image_name)[0]
image_path = os.path.join(image_folder, image_name)
bbox_file_path = os.path.join(bbox_folder, base_name + '.txt')
output_image_path = os.path.join(output_image_folder, base_name + '_rotated_10.jpg')
output_bbox_path = os.path.join(output_bbox_folder, base_name + '_rotated_10.txt')
process_image_and_bbox(image_path, bbox_file_path, output_image_path, output_bbox_path, angle)
증강한 이미지, 라벨 데이터들 모두 각 폴더에 저장
# 원본 및 회전된 이미지가 저장된 폴더 목록
source_folders = [
'/content/cool_roof_images',
'/content/rotated_img_10',
'/content/rotated_img_20',
'/content/rotated_img_30',
'/content/rotated_img_40'
]
# 모든 이미지를 저장할 폴더
output_folder = 'cool_roof_images_all'
os.makedirs(output_folder, exist_ok=True)
# 각 폴더에서 이미지를 읽어서 img_all 폴더로 복사
for folder in source_folders:
for image_name in os.listdir(folder):
if image_name.endswith('.jpg'):
source_path = os.path.join(folder, image_name)
destination_path = os.path.join(output_folder, image_name)
shutil.copy(source_path, destination_path)
print(f" '{output_folder}'에 모든 이미지 저장 완료")
# 원본 및 회전된 바운딩 박스 정보가 저장된 폴더 목록
source_folders = [
'/content/cool_roof_yolo_labels/obj_train_data',
'/content/rotated_label_10',
'/content/rotated_label_20',
'/content/rotated_label_30',
'/content/rotated_label_40'
]
# 모든 .txt 파일을 저장할 폴더
output_folder = 'cool_roof_labels_all'
os.makedirs(output_folder, exist_ok=True)
# 각 폴더에서 .txt 파일을 읽어서 label_all 폴더로 이동
for folder in source_folders:
for txt_name in os.listdir(folder):
if txt_name.endswith('.txt'):
source_path = os.path.join(folder, txt_name)
destination_path = os.path.join(output_folder, txt_name)
# 파일 이동
shutil.move(source_path, destination_path)
print(f" '{output_folder}'에 모든 라벨 저장 완료")
4) 데이터 split, 파일 이동
train dataset : validation dataset = 80 : 20
# 기본 경로 설정
img_dir = '/content/cool_roof_images_all' # 모든 이미지 폴더
label_dir = '/content/cool_roof_labels_all' # 모든 레이블 폴더
# 분할 후 이미지 및 레이블 디렉토리 경로
img_train_dir = '/content/yolov8/data/images/train'
img_val_dir = '/content/yolov8/data/images/val'
label_train_dir = '/content/yolov8/data/labels/train'
label_val_dir = '/content/yolov8/data/labels/val'
# 이미지 파일 목록 로드 및 분할
img_files = [f for f in os.listdir(img_dir) if f.endswith('.jpg')]
train_img_files, val_img_files = train_test_split(img_files, test_size=0.2, random_state=2024)
# 이미지 이동
for f in train_img_files:
shutil.move(os.path.join(img_dir, f), img_train_dir)
for f in val_img_files:
shutil.move(os.path.join(img_dir, f), img_val_dir)
# 레이블 이동
for f in train_img_files:
label_file = f.replace('.jpg', '.txt')
shutil.move(os.path.join(label_dir, label_file), label_train_dir)
for f in val_img_files:
label_file = f.replace('.jpg', '.txt')
shutil.move(os.path.join(label_dir, label_file), label_val_dir)
print("데이터 분할 및 이동 완료!")
확인
directory = '/content/yolov8/data/labels/train'
file_count = len([name for name in os.listdir(directory) if os.path.isfile(os.path.join(directory, name))])
print("train dataset:", file_count)
directory = '/content/yolov8/data/labels/val'
file_count = len([name for name in os.listdir(directory) if os.path.isfile(os.path.join(directory, name))])
print("validation dataset:", file_count)
train dataset: 800 validation dataset: 200
2. Object Detection
!pip install ultralytics
from ultralytics import YOLO, settings
1) YOLO 모델에 적용할 YAML 생성하기
settings['datasets_dir'] = '/content/'
settings.update()
settings
# YOLO 학습을 위한 YAML 파일 생성 스크립트
# 필요한 정보 읽기
obj_names_path = '/content/cool_roof_yolo_labels/obj.names' # 클래스 이름 파일 경로
obj_data_path = '/content/olo_labels/obj.data' # 데이터셋 설정 파일 경로
# 클래스 이름 로드
with open(obj_names_path, 'r') as f:
class_names = [line.strip() for line in f.readlines()]
# YAML 파일 내용 작성
yaml_content = f"""
# 데이터셋 경로
path: /content/yolov8/data # Adjust if necessary
train: /content/yolov8/data/images/train # 훈련 이미지 경로
val: /content/yolov8/data/images/val # 검증 이미지 경로
# 클래스 정보
nc: {len(class_names)} # 클래스 수
names: {class_names} # 클래스 이름
"""
# YAML 파일 생성
yaml_file = '/content/yolov8/data/dataset.yaml'
with open(yaml_file, 'w') as f:
f.write(yaml_content.strip())
print(f"{yaml_file}에 YAML 파일이 생성되었습니다.")
/content/yolov8/data/dataset.yaml에 YAML 파일이 생성되었습니다.
2) YOLO v8 모델 불러와서 학습
※ 모델 크기 변경하는 방법!
model='yolov8n.pt' 에서 yolov8 뒤의 n은 nano 모델을 의미하는 것으로 s, m, l 등으로 바꾸면 각각 small, midium, large 모델로 변경된다.
- Nano Model
n_model = YOLO(model='yolov8n.pt', task='detect')
n_model.train(data='/content/yolov8/data/dataset.yaml',
epochs=100,
patience=10,
pretrained=True,
verbose=True,
seed=1234,
)
mAP50이 0.97, mAP50-95가 0.757로 높은 성능을 보인다.
3. Test
직접 수집한 인공위성 이미지로 test 해보기
# test 폴더 확인 및 생성 후 test 할 이미지 넣어주기!
dir = 'yolov8/data/test'
os.makedirs(dir, exist_ok=True)
results = n_model.predict(source='/content/yolov8/data/test/*',
save=True,
line_width=2,
stream=True
)
for r in results :
r_bbox = r.boxes
바운딩박스는 일반적으로 매우 잘 그리고, cool roof로 구분하는 경우가 많이 없다.
위의 labels 결과를 통해서도 알 수 있듯이 cool roof 데이터가 상대적으로 많이 없기에 데이터 불균형 문제가 생긴 것으로 보인다. 따라서 cool roof를 잘 구별하지 못할 수 있다고 생각한다. 하지만, 우리의 목적은 generic roof 즉, 쿨루프 공사를 해야하는 대상(일반 옥상)을 찾는 것이며, 2차적으로 직접적인 확인 절차를 거쳐야 하므로 옥상의 바운딩박스를 잘 그리고, generic roof로 잘 찾는다면 오히려 좋 수도 있다고 생각한다.
정리
- 당연하지만 학습을 많이 시킬수록 정확도가 높아진다.
- 이것도 당연한 얘기지만, 학습 Data가 많을수록 정확도가 높아진다. data augmentation 회전을 적용해서 데이터 개수를 200장에서 1000장으로 늘렸더니 성능 훨씬 좋아졌다.
- 환경이 되지 않아서 더 큰 모델이나 epochs로 돌려보지는 못했으나, 성능이 더 좋아질 여지가 있다.
- YOLO는 실시간 탐지 모델이다. 이 문제에서는 실시간으로 처리할 필요는 없기 때문에 다른 모델로 실험 해 볼 수 있다.
전체 코드는 깃허브에서 볼 수 있다.
https://github.com/suetudy/Sues_Projects
'프로젝트' 카테고리의 다른 글
차량 공유 업체의 차량 파손 여부 분류하기(3) - Transfer Learning (0) | 2024.04.14 |
---|---|
차량 공유 업체의 차량 파손 여부 분류하기(2) - CNN 모델링 (0) | 2024.04.09 |
차량 공유 업체의 차량 파손 여부 분류하기(1) - 데이터 확인 및 전처리 (0) | 2024.04.09 |
YOLOv8와 Roboflow를 활용한 Object Detection - 클라이밍 이미지에서 얼굴, 손, 발 detection(2) (0) | 2024.04.08 |
YOLOv8와 Roboflow를 활용한 Object Detection - 클라이밍 이미지에서 얼굴, 손, 발 detection(1) (0) | 2024.04.08 |