수민 '-'

플오그래밍

제가 작성하는 모든 글은 절대 상업적인 이용이 아니며, 그저 개인적인 공부 용도로만 사용하는 것임을 밝힙니다.

이안류 CCTV와 스타벅스 세그멘테이션 - AI Hub·COCO에서 YOLOv8까지 실습

해수욕장 안전·로고 인식을 위한 데이터셋 실습

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에 대응하는 .jpg01.원천데이터 경로에서 찾아 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 기반 모델과의 결합 등 더 복잡한 멀티모달·비전 모델로 확장해 보면 좋다.