Skip to main content

탐지 프로젝트 — 안전장비 탐지

건설 현장이나 공장에서 작업자의 안전장비(안전모, 조끼, 안전화) 착용 여부를 자동으로 탐지하는 프로젝트입니다. YOLO와 Label Studio를 사용하여 데이터 라벨링부터 모델 학습, 평가, 추론까지 전체 파이프라인을 수행합니다.
1
프로젝트 구조 설정
2
safety-equipment-detection/
├── data/
│   ├── images/
│   │   ├── train/         # 학습 이미지
│   │   └── val/           # 검증 이미지
│   ├── labels/
│   │   ├── train/         # 학습 라벨 (YOLO TXT)
│   │   └── val/           # 검증 라벨
│   └── dataset.yaml       # YOLO 데이터셋 설정
├── labelstudio/
│   └── export/            # Label Studio 내보내기
├── runs/                  # 학습 결과
├── scripts/
│   ├── convert_labels.py  # 라벨 변환 스크립트
│   └── analyze_data.py    # 데이터 분석
└── inference.py
3
클래스 정의와 데이터 수집
4
# 클래스 정의
CLASSES = {
    0: 'helmet',        # 안전모
    1: 'vest',          # 안전 조끼
    2: 'no-helmet',     # 안전모 미착용
    3: 'no-vest',       # 조끼 미착용
}

# dataset.yaml 작성
dataset_yaml = """
path: ./data
train: images/train
val: images/val

nc: 4
names:
  0: helmet
  1: vest
  2: no-helmet
  3: no-vest
"""

with open('data/dataset.yaml', 'w') as f:
    f.write(dataset_yaml)
5
안전장비 탐지 데이터는 직접 촬영하거나, Roboflow Universe 등 공개 데이터셋을 활용할 수 있습니다. 현장 환경(조명, 각도, 거리)을 최대한 반영한 데이터가 실무 성능에 직결됩니다.
6
Label Studio로 라벨링
7
# Label Studio 설정 (Docker 실행)
# docker run -it -p 8080:8080 heartexlabs/label-studio:latest

# 라벨링 인터페이스 XML 템플릿
labeling_config = """
<View>
  <Image name="image" value="$image"/>
  <RectangleLabels name="label" toName="image">
    <Label value="helmet" background="green"/>
    <Label value="vest" background="blue"/>
    <Label value="no-helmet" background="red"/>
    <Label value="no-vest" background="orange"/>
  </RectangleLabels>
</View>
"""
8
# Label Studio COCO Export → YOLO 변환
import json
import os

def coco_to_yolo(coco_json_path, output_dir, image_width, image_height):
    """COCO 포맷 라벨을 YOLO 포맷으로 변환합니다."""
    with open(coco_json_path) as f:
        coco = json.load(f)

    # 카테고리 매핑 (COCO ID → YOLO 클래스 인덱스)
    cat_map = {}
    for cat in coco['categories']:
        cat_map[cat['id']] = cat['id'] - 1  # 0-indexed

    # 이미지별 어노테이션 그룹화
    img_annotations = {}
    for ann in coco['annotations']:
        img_id = ann['image_id']
        if img_id not in img_annotations:
            img_annotations[img_id] = []
        img_annotations[img_id].append(ann)

    os.makedirs(output_dir, exist_ok=True)

    for img_info in coco['images']:
        img_id = img_info['id']
        img_w = img_info.get('width', image_width)
        img_h = img_info.get('height', image_height)
        filename = os.path.splitext(img_info['file_name'])[0] + '.txt'

        lines = []
        for ann in img_annotations.get(img_id, []):
            cls_id = cat_map[ann['category_id']]
            x, y, w, h = ann['bbox']  # COCO: (x_min, y_min, w, h)

            # YOLO: (cx, cy, w, h) 정규화
            cx = (x + w / 2) / img_w
            cy = (y + h / 2) / img_h
            nw = w / img_w
            nh = h / img_h
            lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}")

        with open(os.path.join(output_dir, filename), 'w') as f:
            f.write('\n'.join(lines))

    print(f"변환 완료: {len(coco['images'])}개 이미지")

# 변환 실행
coco_to_yolo(
    'labelstudio/export/result.json',
    'data/labels/train',
    image_width=1920,
    image_height=1080,
)
9
데이터 분석
10
import os
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter

def analyze_dataset(label_dir, class_names):
    """데이터셋의 클래스 분포와 바운딩 박스 통계를 분석합니다."""
    class_counts = Counter()
    bbox_sizes = []
    objects_per_image = []

    for label_file in os.listdir(label_dir):
        if not label_file.endswith('.txt'):
            continue

        with open(os.path.join(label_dir, label_file)) as f:
            lines = f.readlines()

        objects_per_image.append(len(lines))

        for line in lines:
            parts = line.strip().split()
            cls_id = int(parts[0])
            w, h = float(parts[3]), float(parts[4])

            class_counts[cls_id] += 1
            bbox_sizes.append(w * h)  # 정규화된 면적

    # 클래스 분포 시각화
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))

    # 클래스별 개수
    classes = [class_names[i] for i in sorted(class_counts.keys())]
    counts = [class_counts[i] for i in sorted(class_counts.keys())]
    axes[0].bar(classes, counts, color=['green', 'blue', 'red', 'orange'])
    axes[0].set_title('클래스별 객체 수')
    axes[0].set_ylabel('개수')

    # 이미지당 객체 수
    axes[1].hist(objects_per_image, bins=20, edgecolor='black')
    axes[1].set_title('이미지당 객체 수 분포')
    axes[1].set_xlabel('객체 수')

    # 바운딩 박스 크기 분포
    axes[2].hist(bbox_sizes, bins=50, edgecolor='black')
    axes[2].set_title('바운딩 박스 크기 분포 (정규화 면적)')
    axes[2].set_xlabel('면적')

    plt.tight_layout()
    plt.savefig('outputs/data_analysis.png', dpi=150)
    print(f"총 이미지: {len(objects_per_image)}장")
    print(f"총 객체: {sum(counts)}개")
    for cls, cnt in zip(classes, counts):
        print(f"  {cls}: {cnt}개 ({cnt/sum(counts)*100:.1f}%)")

analyze_dataset('data/labels/train', CLASSES)
11
YOLO 모델 학습
12
from ultralytics import YOLO

# 모델 선택
model = YOLO('yolo11m.pt')  # Medium 모델 (균형 잡힌 성능)

# 학습 실행
results = model.train(
    data='data/dataset.yaml',
    epochs=100,
    imgsz=640,
    batch=16,
    patience=15,           # Early Stopping
    optimizer='AdamW',
    lr0=0.001,
    lrf=0.01,              # 최종 학습률 비율
    warmup_epochs=3,
    mosaic=1.0,            # Mosaic 증강
    mixup=0.1,             # MixUp 증강
    degrees=10,            # 회전 증강
    flipud=0.5,            # 상하 반전 (안전장비는 방향 중요)
    project='runs/detect',
    name='safety-equipment',
    save=True,
    plots=True,
)
13
# CLI로 학습 (동일한 설정)
# yolo detect train data=data/dataset.yaml model=yolo11m.pt \
#     epochs=100 imgsz=640 batch=16 patience=15 \
#     project=runs/detect name=safety-equipment
14
학습 결과 분석
15
# 학습 결과 확인
import os

result_dir = 'runs/detect/safety-equipment'

# 결과 디렉터리 구조
# ├── weights/
# │   ├── best.pt          # 최고 성능 모델
# │   └── last.pt          # 마지막 에포크 모델
# ├── results.csv          # 에포크별 메트릭
# ├── confusion_matrix.png # 혼동 행렬
# ├── F1_curve.png         # F1-Confidence 곡선
# ├── P_curve.png          # Precision-Confidence 곡선
# ├── R_curve.png          # Recall-Confidence 곡선
# └── PR_curve.png         # Precision-Recall 곡선

# 결과 로드
import pandas as pd
results_df = pd.read_csv(os.path.join(result_dir, 'results.csv'))
results_df.columns = results_df.columns.str.strip()

# 주요 메트릭 출력
best_epoch = results_df['metrics/mAP50(B)'].idxmax()
print(f"최고 성능 에포크: {best_epoch + 1}")
print(f"mAP@50: {results_df.loc[best_epoch, 'metrics/mAP50(B)']:.4f}")
print(f"mAP@50:95: {results_df.loc[best_epoch, 'metrics/mAP50-95(B)']:.4f}")
print(f"Precision: {results_df.loc[best_epoch, 'metrics/precision(B)']:.4f}")
print(f"Recall: {results_df.loc[best_epoch, 'metrics/recall(B)']:.4f}")
16
검증과 추론
17
from ultralytics import YOLO
import supervision as sv
import cv2

# 최고 모델 로드
model = YOLO('runs/detect/safety-equipment/weights/best.pt')

# 검증 세트 평가
metrics = model.val(data='data/dataset.yaml')
print(f"mAP@50: {metrics.box.map50:.4f}")
print(f"mAP@50:95: {metrics.box.map:.4f}")

# 클래스별 AP
for i, cls_name in CLASSES.items():
    print(f"  {cls_name}: AP@50 = {metrics.box.ap50[i]:.4f}")

# 이미지 추론 + supervision 시각화
def detect_and_visualize(image_path, model, confidence=0.5):
    """이미지에서 안전장비를 탐지하고 시각화합니다."""
    image = cv2.imread(image_path)
    results = model(image, conf=confidence)[0]
    detections = sv.Detections.from_ultralytics(results)

    # 어노테이터 설정
    box_annotator = sv.BoxAnnotator(thickness=2)
    label_annotator = sv.LabelAnnotator(text_scale=0.5)

    labels = [
        f"{results.names[int(cls)]} {conf:.2f}"
        for cls, conf in zip(detections.class_id, detections.confidence)
    ]

    # 시각화
    annotated = box_annotator.annotate(scene=image.copy(), detections=detections)
    annotated = label_annotator.annotate(scene=annotated, detections=detections, labels=labels)

    return annotated, detections

# 단일 이미지 추론
annotated, detections = detect_and_visualize('data/images/val/test.jpg', model)
cv2.imwrite('outputs/detection_result.jpg', annotated)

# 안전 위반 판정
def check_safety_violations(detections, class_names):
    """안전장비 미착용 위반을 판정합니다."""
    violations = []
    for cls_id, conf in zip(detections.class_id, detections.confidence):
        name = class_names[int(cls_id)]
        if name.startswith('no-'):
            violations.append(f"{name} (신뢰도: {conf:.2f})")

    if violations:
        print(f"⚠ 안전 위반 {len(violations)}건 탐지:")
        for v in violations:
            print(f"  - {v}")
    else:
        print("✓ 안전장비 정상 착용")

    return violations

violations = check_safety_violations(detections, CLASSES)
18
영상 실시간 추론
19
import cv2
import supervision as sv
from ultralytics import YOLO

model = YOLO('runs/detect/safety-equipment/weights/best.pt')

box_annotator = sv.BoxAnnotator(thickness=2)
label_annotator = sv.LabelAnnotator(text_scale=0.5)

def process_frame(frame):
    """프레임에서 안전장비를 탐지합니다."""
    results = model(frame, conf=0.5, verbose=False)[0]
    detections = sv.Detections.from_ultralytics(results)

    labels = [
        f"{results.names[int(cls)]} {conf:.2f}"
        for cls, conf in zip(detections.class_id, detections.confidence)
    ]

    annotated = box_annotator.annotate(scene=frame.copy(), detections=detections)
    annotated = label_annotator.annotate(scene=annotated, detections=detections, labels=labels)
    return annotated

# 영상 파일 처리
sv.process_video(
    source_path='input_video.mp4',
    target_path='output_video.mp4',
    callback=process_frame,
)
20
모델 내보내기
21
# ONNX 내보내기
model = YOLO('runs/detect/safety-equipment/weights/best.pt')
model.export(format='onnx', imgsz=640, simplify=True)

# TensorRT 내보내기 (NVIDIA GPU 환경)
model.export(format='engine', imgsz=640, half=True)

# TFLite 내보내기 (모바일/엣지)
model.export(format='tflite', imgsz=320)

프로젝트 결과 요약 예시

항목
모델YOLOv11m
클래스helmet, vest, no-helmet, no-vest (4개)
학습 데이터1,200장 (약 5,000 객체)
라벨링 도구Label Studio
mAP@5087.3%
mAP@50:9562.1%
추론 속도8.2ms (RTX 3090, FP16)
안전장비 탐지 모델을 실무에 배포할 때는 오탐(False Positive)과 미탐(False Negative)의 비용이 다릅니다. 미탐은 안전 사고로 이어질 수 있으므로, 신뢰도 임계값(Confidence Threshold)을 낮추고 Recall을 높이는 방향으로 튜닝하세요. 다만 오탐이 과도하면 운영 효율이 떨어지므로 현장 피드백을 반영하여 조정합니다.
Pre-labeling 전략을 활용하세요. (1) 소량(100200장)을 먼저 라벨링하여 초기 모델을 학습합니다. (2) 이 모델로 나머지 이미지를 자동 라벨링합니다. (3) 자동 라벨을 검수하고 수정합니다. 이 방식이 처음부터 수동 라벨링하는 것보다 23배 빠릅니다.
(1) 입력 해상도를 640에서 1280으로 높이세요. (2) SAHI(Slicing Aided Hyper Inference)를 적용하여 이미지를 패치로 나눠 추론하세요. (3) 학습 시 작은 객체가 포함된 이미지를 증강으로 보강하세요. 다만 해상도를 높이면 추론 속도가 느려지므로 트레이드오프를 고려하세요.