해수욕장 안전·로고 인식을 위한 데이터셋 실습
AI Hub의 이안류 CCTV 데이터와 스타벅스 로고·텍스트가 라벨링된 COCO 형식 세그멘테이션 데이터를 활용해, YOLOv8로 객체 탐지·세그멘테이션 모델을 학습하는 과정을 정리한다. 이 글에서는 이안류 데이터셋 개요와 JSON→YOLO 변환, train/val/test 분리, ripcurrent.yaml 구성, YOLOv8 학습·평가·추론, 그리고 COCO 세그멘테이션을 YOLO Seg 포맷으로 변환해 스타벅스 로고·텍스트 분할 모델을 만드는 흐름까지 다룬다.
(이안류 CCTV 데이터셋 - 류지 프로젝트, Segmentation - 류지 프로젝트를 기반으로 재구성)
1. 이안류 CCTV 데이터셋 소개
AI Hub의 '이안류 CCTV 데이터'는 국내 주요 해수욕장(해운대, 송정, 대천, 중문, 낙산)의 CCTV 영상을 이미지로 변환해, 이안류 발생 여부와 위치를 모니터링할 수 있도록 구축된 학습용 데이터셋이다.
- 목적: 해수욕장 안전 관리 및 이안류 탐지·예측 시스템 개발
- 구성: 원천 이미지(
01.원천데이터) + 라벨 JSON(02.라벨링데이터) - 라벨: 이안류가 발생한 영역을 사각형 형태로 표시 (좌상·우상·우하·좌하 좌표)
이안류는 해안에서 먼 바다로 빨리 빠져나가는 좁은 물살로, 기상 상태가 좋아 보여도 초보자가 쉽게 휩쓸릴 수 있어 실시간 탐지 모델이 특히 중요하다.
2. 라벨 JSON → YOLO 바운딩 박스 변환
라벨은 JSON에 4개 꼭지점 좌표(좌상·우상·우하·좌하)가 들어 있고, 이미지 해상도(image_info.resolution)는 "width,height" 문자열로 주어진다. YOLO가 요구하는 포맷은 (x_center, y_center, width, height)를 0~1 범위로 정규화한 값이다.
2-1. json_to_yolo_bbox 함수
import json
def json_to_yolo_bbox(bbox, w, h):
# bbox[0] = 좌상, bbox[1] = 우상, bbox[3] = 좌하
x_center = ((bbox[0][0] + bbox[1][0]) / 2) / w
y_center = ((bbox[0][1] + bbox[3][1]) / 2) / h
width = (bbox[1][0] - bbox[0][0]) / w
height = (bbox[3][1] - bbox[0][1]) / h
return [x_center, y_center, width, height]
2-2. 파일 하나를 예제로 변환
file_list = glob.glob(f'{file_root}/02.라벨링데이터/*.json')
file = file_list[226] # 예: NS_NSBE1_20190712_144625.json
result = set()
with open(file, 'r') as f:
json_data = json.load(f)
width, height = list(map(int, json_data['image_info']['resolution'].split(',')))
cls = 0 # 이안류 클래스 하나만 사용
if json_data['annotations'].get('drawing'):
for b in json_data['annotations']['drawing']:
yolo_bbox = json_to_yolo_bbox(b, width, height)
bbox_string = ' '.join([str(x) for x in yolo_bbox])
result.add(f'{cls} {bbox_string}')
result = list(result)
if result:
with open(file.replace('json', 'txt'), 'w', encoding='utf-8') as t:
t.write('\n'.join(result))
print(file)
- YOLO 라벨 형식:
class x_center y_center width height set()으로 중복 박스를 제거하고, JSON과 같은 이름의.txt를 생성한다.
2-3. 모든 JSON 일괄 변환
for file in tqdm(file_list):
result = set()
with open(file, 'r') as f:
json_data = json.load(f)
width, height = list(map(int, json_data['image_info']['resolution'].split(',')))
cls = 0
num_b = json_data['annotations']['bounding_count']
if num_b > 0:
for b in json_data['annotations']['drawing']:
yolo_bbox = json_to_yolo_bbox(b, width, height)
bbox_string = ' '.join([str(x) for x in yolo_bbox])
result.add(f'{cls} {bbox_string}')
result = list(result)
if result:
with open(file.replace('json', 'txt'), 'w', encoding='utf-8') as t:
t.write('\n'.join(result))
3. train/val/test 분리 및 이미지 복사
*.txt 라벨 파일을 무작위 셔플한 뒤 비율에 따라 train/val/test로 나눈다. 각 .txt에 대응하는 .jpg를 01.원천데이터 경로에서 찾아 images 폴더로 복사한다.
random.seed(2026)
file_list = glob.glob(f'{file_root}/02.라벨링데이터/*.txt')
random.shuffle(file_list)
test_ratio = 0.1
num_file = len(file_list)
test_list = file_list[:int(num_file * test_ratio)]
valid_list = file_list[int(num_file * test_ratio):int(num_file * test_ratio) * 2]
train_list = file_list[int(num_file * test_ratio) * 2:]
train_root = f'{data_root}/{pjt_name}/train'
valid_root = f'{data_root}/{pjt_name}/valid'
test_root = f'{data_root}/{pjt_name}/test'
for folder in [train_root, valid_root, test_root]:
os.makedirs(folder, exist_ok=True)
for s in ['images', 'labels']:
os.makedirs(f'{folder}/{s}', exist_ok=True)
# 테스트 데이터
for i in tqdm(test_list):
txt_name = i.split('/')[-1]
shutil.copyfile(i, f'{test_root}/labels/{txt_name}')
img_path = i.replace('02.라벨링데이터', '01.원천데이터').replace('txt', 'jpg')
img_name = img_path.split('/')[-1]
shutil.copyfile(img_path, f'{test_root}/images/{img_name}')
# 검증 데이터
for i in tqdm(valid_list):
txt_name = i.split('/')[-1]
shutil.copyfile(i, f'{valid_root}/labels/{txt_name}')
img_path = i.replace('02.라벨링데이터', '01.원천데이터').replace('txt', 'jpg')
img_name = img_path.split('/')[-1]
shutil.copyfile(img_path, f'{valid_root}/images/{img_name}')
# 학습 데이터
for i in tqdm(train_list):
txt_name = i.split('/')[-1]
shutil.copyfile(i, f'{train_root}/labels/{txt_name}')
img_path = i.replace('02.라벨링데이터', '01.원천데이터').replace('txt', 'jpg')
img_name = img_path.split('/')[-1]
shutil.copyfile(img_path, f'{train_root}/images/{img_name}')
4. ripcurrent.yaml 구성과 YOLOv8 학습
4-1. YAML 설정 파일 만들기
이안류는 이안류 있음(yes) / 없음으로 하나의 객체 클래스만 사용하므로 nc=1, names=['yes']로 둔다.
pjt_root = f'{data_root}/ripcurrent'
data = dict()
data['train'] = train_root
data['val'] = valid_root
data['test'] = test_root
data['nc'] = 1
data['names'] = ['yes']
with open(f'{pjt_root}/ripcurrent.yaml', 'w') as f:
yaml.dump(data, f)
4-2. YOLOv8 학습
from ultralytics import YOLO
%cd {pjt_root}
model = YOLO('yolov8s.pt')
result = model.train(
data='ripcurrent.yaml',
epochs=10,
batch=8,
imgsz=224,
device=0,
workers=2,
amp=False,
patience=5,
name='ripcurrent_s'
)
학습이 끝나면 runs/detect/ripcurrent_s/weights/best.pt에 최적 모델이 저장된다.
4-3. 검증과 mAP 확인
result_folder = f'{pjt_root}/runs/detect/ripcurrent_s'
model = YOLO(f'{result_folder}/weights/best.pt')
metrics = model.val(split='val')
print(metrics.box.map) # mAP@0.5:0.95
print(metrics.box.map50) # mAP@0.5
- mAP@0.5:0.95: IoU 0.5~0.95(0.05 간격)에서 AP를 평균 낸 값
- 0.3 이하: 낮음
- 0.4~0.6: 보통
- 0.6~0.8: 좋음
- 0.8 이상: 매우 좋음
- mAP@0.5: IoU ≥ 0.5만 본 값으로, 조금 더 느슨한 기준
4-4. 이안류 박스 시각화
test_file_list = glob.glob(f'{test_root}/images/*')
random.shuffle(test_file_list)
model = YOLO(f'{result_folder}/weights/best.pt')
color_dict = [(0, 0, 255)] # 빨간 박스
# 단일 이미지
test_img = cv2.imread(test_file_list[2])
img_src = cv2.cvtColor(test_img, cv2.COLOR_BGR2RGB)
results = model(img_src)[0]
annotator = Annotator(img_src)
boxes = results.boxes
for box in boxes:
b = box.xyxy[0]
cls = box.cls
annotator.box_label(b, model.names[int(cls)], color_dict[int(cls)])
img_src = annotator.result()
plt.imshow(img_src)
plt.show()
여러 장에 대한 결과도 반복문으로 그릴 수 있다.
5. 세그멘테이션(Segmentation) 개념
Segmentation(세그멘테이션) 은 이미지를 픽셀 단위로 분할해 각 영역이 무엇인지 구분하는 기술이다. 크게 두 가지로 나뉜다.
- Semantic Segmentation: 같은 종류의 객체는 하나의 클래스로 묶음 (예: 모든 사람 픽셀 → person)
- Instance Segmentation: 같은 클래스라도 개별 객체를 구분 (예: 사람 A, 사람 B 따로 구분)
의료 영상 분석, 자율주행, 위성 이미지처럼 정밀한 영역 구분이 필요한 곳에서 많이 쓰이며, 대표 모델로 U-Net, DeepLab, Mask R-CNN 등이 있다. (Segmentation - 류지 프로젝트 참고)
6. 스타벅스 COCO 세그멘테이션 데이터 이해하기
스타벅스 예제에서는 COCO 형식의 세그멘테이션 라벨(폴리곤 좌표)이 들어 있는 instances_default.json을 사용한다. 이미지에는 스타벅스 로고와 텍스트가 라벨링되어 있고, 각 객체는 카테고리 ID와 함께 segmentation 리스트로 다각형 좌표를 갖는다.
data_root = '/content/drive/MyDrive/…/data/starbucks'
data_list = glob.glob(f'{data_root}/*.jpg') + glob.glob(f'{data_root}/*.jpeg')
def load_coco_annotations(json_path):
with open(json_path, 'r') as f:
data = json.load(f)
return data
coco_data = load_coco_annotations(f'{data_root}/instances_default.json')
6-1. COCO 어노테이션 시각화
def load_image(image_path):
image = cv2.imread(image_path)
if image is None:
raise FileNotFoundError(f'이미지를 찾을 수 없음: {image_path}')
return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
def draw_annotations(image, annotations, image_id):
for ann in annotations:
if ann['image_id'] == image_id and 'segmentation' in ann:
segmentation = ann['segmentation']
for seg in segmentation:
if isinstance(seg, list) and len(seg) >= 6: # 삼각형 이상
points = np.array(seg).reshape(-1, 2).astype(np.int32)
cv2.polylines(image, [points], isClosed=True, color=(0, 255, 0), thickness=2)
return image
def visualize_coco(json_path, image_folder, image_id):
coco_data = load_coco_annotations(json_path)
image_info = next((img for img in coco_data['images'] if img['id'] == image_id), None)
if not image_info:
raise ValueError(f'Image Id {image_id} not found in COCO JSON file')
image_path = os.path.join(image_folder, image_info['file_name'])
image = load_image(image_path)
annotated_image = draw_annotations(image, coco_data['annotations'], image_id)
plt.figure(figsize=(8, 6))
plt.imshow(annotated_image)
plt.axis('off')
plt.title(f'Image Id: {image_id} with Annotations')
plt.show()
visualize_coco(f'{data_root}/instances_default.json', f'{data_root}', 8)
7. COCO Segmentation → YOLO 세그멘테이션 포맷 변환
YOLOv8 세그멘테이션은 한 줄에 클래스 ID + (x, y) 정규화 좌표 쌍들을 적는 형식을 사용한다.
<class_id> x1 y1 x2 y2 x3 y3 ...
COCO의 segmentation 리스트를 순회하면서 이미지 크기로 나누어 0~1 범위로 정규화하고, 각 이미지마다 .txt 파일로 저장한다.
coco_json_path = f'{data_root}/instances_default.json'
yolo_output_folder = f'{data_root}'
with open(coco_json_path, 'r') as f:
coco_data = json.load(f)
image_dict = {img['id']: img for img in coco_data['images']}
for ann in coco_data['annotations']:
image_id = ann['image_id']
category_id = ann['category_id'] - 1 # 클래스 인덱스를 0부터 시작하도록 조정
segmentation = ann['segmentation']
image_info = image_dict.get(image_id)
if not image_info:
continue
img_width, img_height = image_info['width'], image_info['height']
yolo_label_path = os.path.join(yolo_output_folder, f"{os.path.splitext(image_info['file_name'])[0]}.txt")
yolo_lines = []
for seg in segmentation:
if isinstance(seg, list) and len(seg) >= 6:
normalized_points = [
(seg[i] / img_width, seg[i + 1] / img_height) for i in range(0, len(seg), 2)
]
yolo_line = f'{category_id} ' + ' '.join([f'{x:.6f} {y:6f}' for x, y in normalized_points])
yolo_lines.append(yolo_line)
if yolo_lines:
with open(yolo_label_path, 'w') as f:
f.write('\n'.join(yolo_lines))
print('coco -> yolo 변환 완료!')
category_id - 1: COCO 카테고리 ID는 1부터 시작하는 경우가 많아, 0부터 시작하도록 맞춘다.- 각
seg는[x1, y1, x2, y2, ...]형태로 들어 있다.
8. 스타벅스 세그멘테이션 데이터 분리와 YAML 설정
8-1. train/val/test 분리 및 복사
이미지(*.jpg, *.jpeg) 목록을 셔플한 뒤, 20% test, 20% val, 60% train으로 나눈다. 각 세트 폴더에 이미지와 같은 이름의 .txt(세그멘테이션 라벨)도 함께 복사한다.
random.seed(2026)
data_list = glob.glob(f'{data_root}/*.jpg') + glob.glob(f'{data_root}/*.jpeg')
random.shuffle(data_list)
test_ratio = 0.2
num_data = len(data_list)
test_list = data_list[:int(num_data * test_ratio)]
valid_list = data_list[int(num_data * test_ratio):int(num_data * test_ratio) * 2]
train_list = data_list[int(num_data * test_ratio) * 2:]
file_root = f'{data_root}'
train_root = f'{data_root}/train'
valid_root = f'{data_root}/valid'
test_root = f'{data_root}/test'
for folder in [train_root, valid_root, test_root]:
os.makedirs(folder, exist_ok=True)
def copy_files(file_list, dest_folder):
for file_path in file_list:
file_name = os.path.basename(file_path)
file_base, ext = os.path.splitext(file_name)
dest_path = os.path.join(dest_folder, file_name)
shutil.copy(file_path, dest_path)
txt_file_path = os.path.join(os.path.dirname(file_path), f'{file_base}.txt')
if os.path.exists(txt_file_path):
dest_txt_path = os.path.join(dest_folder, f'{file_base}.txt')
shutil.copy(txt_file_path, dest_txt_path)
copy_files(train_list, train_root)
copy_files(valid_list, valid_root)
copy_files(test_list, test_root)
Colab·드라이브 환경용 절대 경로는 아래처럼 다시 정의해 두었다.
train_root = '/content/drive/MyDrive/…/data/starbucks/train'
valid_root = '/content/drive/MyDrive/…/data/starbucks/valid'
test_root = '/content/drive/MyDrive/…/data/starbucks/test'
8-2. starbucks.yaml 생성
클래스는 logo, text 두 개이며, YOLO 세그멘테이션 학습을 위해 YAML을 구성한다.
import yaml
data = dict()
data['train'] = train_root
data['val'] = valid_root
data['test'] = test_root
data['nc'] = 2
data['names'] = ['logo', 'text']
with open(f'{data_root}/starbucks.yaml', 'w') as f:
yaml.dump(data, f)
9. YOLOv8 세그멘테이션 학습·추론
9-1. 세그멘테이션 모델 학습
YOLOv8의 세그멘테이션용 가중치(yolov8s-seg.pt)를 사용해 스타벅스 로고·텍스트를 분할하도록 학습한다.
from ultralytics import YOLO
model = YOLO('yolov8s-seg.pt')
results = model.train(
data=f'{data_root}/starbucks.yaml',
epochs=10,
batch=4,
imgsz=224,
device=0,
workers=2,
amp=False,
name='starbucks_s'
)
9-2. 세그멘테이션 결과 예측·저장
model = YOLO('/content/runs/segment/starbucks_s/weights/best.pt')
results = model.predict(
source='/content/drive/MyDrive/…/data/starbucks/test',
imgsz=224,
conf=0.3,
device=0,
save=True,
save_conf=True
)
source: 테스트 이미지 폴더save=True: 결과 이미지를 저장 (runs/segment/...)save_conf=True: 마스크에 Confidence도 함께 저장
YOLO 세그멘테이션은 객체 경계선을 따라 마스크를 그려 주기 때문에, 로고·텍스트처럼 구체적인 영역 분할에 유리하다.
마치며
- 이안류 CCTV 데이터셋은 국내 해수욕장에서 위험한 이안류를 감지하기 위한 AI Hub 데이터로, JSON 라벨을 YOLO 포맷으로 변환하고 YOLOv8으로 학습·평가·시각화하는 전 과정을 실습할 수 있다.
- 스타벅스 세그멘테이션 데이터는 COCO 형식의 폴리곤 라벨을 YOLO 세그멘테이션 텍스트 포맷으로 변환하고, YOLOv8 세그멘테이션 모델로 로고·텍스트 영역을 픽셀 단위로 분할하는 실습에 적합하다.
- 두 예제 모두 데이터 구조 파악 → 어노테이션 변환 → train/val/test 분리 → YAML 설정 → YOLOv8 학습·평가·추론이라는 공통 패턴을 갖고 있어, 이후 다른 객체 탐지·세그멘테이션 프로젝트에도 그대로 응용할 수 있다.
다음에는 차량 파손 데이터셋이나 ViT 기반 모델과의 결합 등 더 복잡한 멀티모달·비전 모델로 확장해 보면 좋다.
'AI·머신러닝 > 딥러닝·비전' 카테고리의 다른 글
| timm ViT로 멀티 브랜치 분류기 만들기 - 4자리 숫자 이미지 분류 (0) | 2026.01.27 |
|---|---|
| Vision Transformer(ViT) - 개념과 원리, 패치부터 분류까지 (0) | 2026.01.26 |
| Object Detection - 개념, 전통 기법, YOLO, Pascal VOC 2007, mAP (0) | 2026.01.07 |
| 컴퓨터 비전과 OCR - 정의, 프레임워크, 데이터셋, 어노테이션, Tesseract·EasyOCR (0) | 2026.01.06 |
| OpenCV 크로마키·ROI·이진화·기하 변환 - 투시 변환과 명함 스캐너 실습 (0) | 2026.01.05 |