
📓Google Colab에서 실습하기
이 레슨은 PyTorch/GPU가 필요합니다. 노트북을 다운로드 후 Google Colab에서 열어주세요.
학습 내용
데이터 파이프라인 - 수집, 라벨링, 전처리
학습 목표
- •번호판 이미지 데이터를 체계적으로 수집하는 전략을 세운다
- •YOLO 형식과 OCR 형식의 라벨링 방법을 이해하고 적용한다
- •전처리 파이프라인을 설계하고 구현한다
- •데이터 증강으로 학습 데이터의 양과 다양성을 확보한다
데이터가 모델의 운명을 결정한다
비유: 최고급 오븐을 사도 밀가루가 상했으면 맛있는 빵을 만들 수 없습니다. AI 모델도 마찬가지입니다. 아무리 좋은 모델 구조를 설계해도, 학습 데이터의 품질이 나쁘면 성능은 바닥입니다. "Garbage In, Garbage Out"이라는 격언이 AI 분야에서도 그대로 적용됩니다.
실제 산업 현장에서 AI 프로젝트의 시간 배분을 보면 놀라운 사실을 발견합니다:
| 작업 | 시간 비중 | 설명 |
|---|---|---|
| 데이터 수집 및 정제 | 약 40% | 가장 많은 시간 소요 |
| 데이터 라벨링 | 약 20% | 노동집약적 작업 |
| 모델 설계 및 학습 | 약 25% | 실제 "AI" 작업 |
| 평가 및 배포 | 약 15% | 최종 마무리 |
데이터 관련 작업이 전체의 **60%**를 차지합니다. 그만큼 중요하다는 뜻이죠.
데이터 수집 전략
번호판 이미지를 모으는 방법은 크게 네 가지입니다. 각 방법의 장단점을 정확히 알고 조합하는 것이 핵심입니다.
수집 방법 비교
| 방법 | 장점 | 단점 | 권장 비율 |
|---|---|---|---|
| 공개 데이터셋 | 즉시 사용 가능, 라벨 포함 | 한국 데이터 부족, 품질 편차 | 30~40% |
| 직접 촬영 | 실제 환경 반영, 품질 통제 가능 | 시간 소요, 개인정보 이슈 | 20~30% |
| 웹 크롤링 | 대량 수집 가능 | 저작권/개인정보 주의 | 10~20% |
| 합성 데이터 | 무한 생성, 완벽한 라벨 | 실제 환경과 차이 | 20~30% |
1. 공개 데이터셋 활용
python# 주요 공개 데이터셋 소스 datasets = { "Roboflow Universe": "다양한 번호판 데이터셋, YOLO 형식 지원", "Kaggle": "Korean License Plate Dataset 검색", "AI Hub (한국)": "한국 차량 번호판 데이터셋 (정부 제공)", "GitHub": "오픈소스 프로젝트의 학습 데이터" } for source, desc in datasets.items(): print(f" {source}: {desc}")
2. 직접 촬영 가이드라인
촬영할 때 다양성을 확보하는 것이 가장 중요합니다:
| 조건 | 변형 항목 | 예시 |
|---|---|---|
| 시간대 | 조명 변화 | 아침, 낮, 저녁, 밤 |
| 날씨 | 환경 조건 | 맑음, 흐림, 비, 눈 |
| 거리 | 번호판 크기 | 3m, 5m, 10m, 20m |
| 각도 | 촬영 방향 | 정면, 측면 15도, 30도 |
| 차종 | 번호판 위치 | 승용차, SUV, 트럭, 오토바이 |
3. 합성 데이터 생성
실제 데이터가 부족할 때, 프로그래밍으로 번호판 이미지를 만들 수 있습니다.
참고: 실제 이미지 생성은 PIL 라이브러리가 필요합니다. 아래는 개념 시뮬레이션입니다.
pythonimport numpy as np def generate_plate_simulation(text, noise_level=10): """합성 번호판 이미지 생성 시뮬레이션""" # 400x80 크기의 흰색 배경 (RGB) img = np.ones((80, 400, 3), dtype=np.uint8) * 255 # 테두리 시뮬레이션 (상하좌우 가장자리를 검은색으로) img[0:3, :, :] = 0 # 상단 img[-3:, :, :] = 0 # 하단 img[:, 0:3, :] = 0 # 좌측 img[:, -3:, :] = 0 # 우측 # 텍스트 영역 시뮬레이션 (중앙에 어두운 영역) img[20:60, 50:350, :] = np.random.randint(0, 50, (40, 300, 3)) # 노이즈 추가 noise = np.random.normal(0, noise_level, img.shape) img = np.clip(img + noise, 0, 255).astype(np.uint8) return img # 테스트 plates = ["12A 3456", "34B 7890", "56C 1234"] print("=== Synthetic Plate Generation ===") for text in plates: img = generate_plate_simulation(text) print(f"Plate '{text}': shape={img.shape}, dtype={img.dtype}") print(f" Pixel range: {img.min()} ~ {img.max()}")
핵심 팁: 합성 데이터만으로는 실제 환경의 복잡함을 재현할 수 없습니다. 반드시 실제 데이터와 혼합해서 사용하세요.
데이터셋 규모 가이드
모델 성능과 데이터 양의 관계를 이해하는 것이 중요합니다.
| 데이터 규모 | 검출 성능 (mAP) | 인식 성능 (Accuracy) | 적합한 용도 |
|---|---|---|---|
| 100장 | 50~60% | 60~70% | 프로토타입, 파이프라인 테스트 |
| 1,000장 | 70~80% | 80~85% | 개념 증명 (PoC) |
| 5,000장 | 85~90% | 90~93% | 초기 제품 수준 |
| 10,000장+ | 90~95% | 95%+ | 상용 서비스 수준 |
비유: 시험 공부를 할 때 기출문제 10개만 풀면 감을 잡을 수 있지만, 100개를 풀어야 실전에서 안정적으로 고득점할 수 있는 것과 같습니다.
데이터 라벨링
라벨링은 "이 이미지에서 정답이 무엇인지"를 표시하는 작업입니다. 우리 프로젝트는 두 종류의 라벨이 필요합니다.
라벨링 유형 비교
| 대상 | 라벨 형식 | 용도 | 예시 |
|---|---|---|---|
| 번호판 위치 | 바운딩 박스 좌표 (YOLO 형식) | 검출 모델 학습 | 0 0.5 0.45 0.3 0.1 |
| 번호판 텍스트 | 문자열 | 인식 모델 학습 | 12가3456 |
YOLO 형식 라벨링 상세
# YOLO 라벨 형식 (labels/img001.txt)
# <class_id> <x_center> <y_center> <width> <height>
# 모든 값은 이미지 크기로 정규화 (0~1 사이)
# 예: 1920x1080 이미지에서 번호판이 (760, 440)~(960, 480)에 있을 때
# x_center = (760 + 960) / 2 / 1920 = 0.448
# y_center = (440 + 480) / 2 / 1080 = 0.426
# width = (960 - 760) / 1920 = 0.104
# height = (480 - 440) / 1080 = 0.037
0 0.448 0.426 0.104 0.037
라벨링 도구 비교
| 도구 | 유형 | 출력 형식 | 난이도 | 추천 용도 |
|---|---|---|---|---|
| LabelImg | 데스크톱 앱 | YOLO, VOC | 쉬움 | 소규모 (100장 이하) |
| CVAT | 웹 기반 | 다양한 형식 | 보통 | 중규모, 팀 작업 |
| Roboflow | 클라우드 | YOLO + 증강 | 쉬움 | 빠른 프로토타이핑 |
| Label Studio | 웹 기반 | 커스텀 가능 | 보통 | 복잡한 라벨링 작업 |
문자 인식용 라벨링
python# 라벨 파일 구조 (CSV 형식) import numpy as np # 파일명과 번호판 텍스트 매핑 labels = [ ["img001.jpg", "12가3456"], ["img002.jpg", "34나7890"], ["img003.jpg", "123가4567"], ["img004.jpg", "56다1234"], ] print("=== 라벨 데이터 예시 ===") for fname, text in labels: num_digits = sum(1 for c in text if c.isdigit()) num_hangul = sum(1 for c in text if ord(c) >= 0xAC00) print(f" {fname} -> {text} (숫자 {num_digits}개, 한글 {num_hangul}개)")
라벨링 품질 관리
경고: 라벨 하나가 잘못되면 모델이 그 잘못된 정보를 "정답"으로 학습합니다. 100장의 깨끗한 라벨이 1,000장의 지저분한 라벨보다 낫습니다.
| 검증 항목 | 체크 방법 | 흔한 실수 |
|---|---|---|
| 바운딩 박스 정확성 | 시각적 검토 | 번호판 일부만 포함, 너무 넓게 잡기 |
| 텍스트 오타 | 이중 검수 | 숫자 0과 영문 O 혼동 |
| 클래스 일관성 | 라벨 통계 확인 | 같은 문자를 다른 클래스로 라벨링 |
| 누락 검출 | 이미지당 라벨 수 확인 | 이미지에 번호판이 있는데 라벨 없음 |
데이터 전처리 파이프라인
수집한 원본 이미지를 모델이 학습할 수 있는 형태로 가공하는 과정입니다.
전처리 단계 흐름
[원본 이미지] → [크기 조정] → [색상 변환] → [정규화] → [텐서 변환] → [모델 입력]
1. 크기 조정 (Resize)
pythonimport numpy as np # YOLO 모델은 고정 크기 입력을 받습니다 # 원본 이미지가 다양한 크기이므로 통일해야 합니다 original_sizes = [(1920, 1080), (1280, 720), (640, 480), (3840, 2160)] target_size = (416, 416) # YOLOv8 기본 입력 크기 print("=== 크기 조정 예시 ===") for w, h in original_sizes: scale_w = target_size[0] / w scale_h = target_size[1] / h print(f" {w}x{h} -> {target_size[0]}x{target_size[1]}") print(f" 가로 축소 비율: {scale_w:.3f}, 세로 축소 비율: {scale_h:.3f}")
2. 정규화 (Normalization)
pythonimport numpy as np # 픽셀 값을 0~255에서 0~1로 변환 # 신경망은 작은 범위의 값에서 더 잘 학습합니다 sample_pixels = np.array([0, 64, 128, 192, 255]) normalized = sample_pixels / 255.0 print("=== 정규화 전후 비교 ===") for orig, norm in zip(sample_pixels, normalized): print(f" {orig:3d} -> {norm:.4f}") print() print(f"원래 범위: [{sample_pixels.min()}, {sample_pixels.max()}]") print(f"정규화 후: [{normalized.min():.1f}, {normalized.max():.1f}]")
3. 색상 변환
pythonimport numpy as np # OpenCV는 BGR 순서, PyTorch/YOLO는 RGB 순서 # 반드시 변환이 필요합니다! # BGR -> RGB 변환 시뮬레이션 def bgr_to_rgb(img): """BGR to RGB conversion""" return img[:, :, ::-1] # 그레이스케일 변환 def to_grayscale(img): """RGB to Grayscale: 0.299*R + 0.587*G + 0.114*B""" return np.dot(img[..., :3], [0.299, 0.587, 0.114]) # 이진화 (Otsu 방식 시뮬레이션) def threshold_otsu(gray, threshold=128): """Simple thresholding (Otsu simulation)""" return (gray > threshold).astype(np.uint8) * 255 # 테스트 sample_img = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) gray = to_grayscale(sample_img) binary = threshold_otsu(gray) print("=== Color Conversion Demo ===") print(f"Original (BGR): shape={sample_img.shape}") print(f"Grayscale: shape={gray.shape}, range=[{gray.min():.0f}, {gray.max():.0f}]") print(f"Binary: unique values={np.unique(binary)}")
4. 데이터셋 분할
pythonimport numpy as np # 전체 데이터를 학습/검증/테스트로 분할 total_images = 5000 np.random.seed(42) # 일반적인 비율: 학습 70%, 검증 15%, 테스트 15% splits = { "학습 (Train)": int(total_images * 0.70), "검증 (Validation)": int(total_images * 0.15), "테스트 (Test)": int(total_images * 0.15), } print("=== 데이터셋 분할 ===") for name, count in splits.items(): ratio = count / total_images * 100 print(f" {name}: {count}장 ({ratio:.0f}%)") print(f" 합계: {sum(splits.values())}장")
주의: 검증 세트와 테스트 세트는 학습에 절대 사용하지 않습니다. 같은 차량의 사진이 학습 세트와 테스트 세트에 동시에 들어가지 않도록 주의하세요 (데이터 누수 방지).
데이터 증강 (Data Augmentation)
비유: 한 명의 학생이 같은 수학 문제만 반복하면 그 문제는 잘 풀지만 새로운 유형에는 약합니다. 같은 문제를 숫자만 바꿔서 여러 변형을 만들면 훨씬 유연하게 대응할 수 있죠. 데이터 증강이 바로 이런 역할입니다.
번호판 검출에 유용한 증강 기법
| 기법 | 시뮬레이션 대상 | 적용 확률 | 주의사항 |
|---|---|---|---|
| 밝기/대비 변화 | 주간/야간, 역광 | 50% | 너무 극단적이면 비현실적 |
| 가우시안 노이즈 | 카메라 센서 노이즈 | 30% | 문자 가독성 유지 |
| 모션 블러 | 이동 중 촬영 | 20% | 과도하면 학습 방해 |
| 회전 | 기울어진 촬영 | 30% | 번호판은 보통 +-15도 이내 |
| 비/안개 효과 | 악천후 | 20% | 번호판이 완전히 가려지면 안됨 |
| 색상 변환 (Hue) | 다양한 조명 색온도 | 30% | 번호판 색상 왜곡 주의 |
Albumentations 증강 파이프라인
참고: 실제 구현은 albumentations 라이브러리 설치 필요. 아래는 증강 개념 시뮬레이션입니다.
pythonimport numpy as np def augment_brightness(img, factor=0.3): """Brightness adjustment""" delta = np.random.uniform(-factor, factor) return np.clip(img * (1 + delta), 0, 255).astype(np.uint8) def augment_noise(img, var=25): """Gaussian noise""" noise = np.random.normal(0, var, img.shape) return np.clip(img + noise, 0, 255).astype(np.uint8) def augment_rotate(img, angle_limit=15): """Rotation simulation (simplified)""" angle = np.random.uniform(-angle_limit, angle_limit) return img, angle # Return angle for reference # Demo np.random.seed(42) sample = np.random.randint(100, 200, (64, 64, 3), dtype=np.uint8) print("=== Data Augmentation Demo ===") print(f"Original: mean={sample.mean():.1f}") bright = augment_brightness(sample, 0.3) print(f"Brightness adjusted: mean={bright.mean():.1f}") noisy = augment_noise(sample, 25) print(f"With noise: mean={noisy.mean():.1f}, std={noisy.std():.1f}") _, angle = augment_rotate(sample, 15) print(f"Rotation angle: {angle:.1f} degrees") print() print("Augmentation pipeline (Albumentations):") print(" 1. RandomBrightnessContrast (p=0.5)") print(" 2. GaussNoise (p=0.3)") print(" 3. MotionBlur (p=0.2)") print(" 4. Rotate (limit=15, p=0.3)") print(" 5. Resize(416, 416)")
증강 전후 데이터 규모 변화
pythonimport numpy as np # 증강으로 데이터 양을 몇 배로 늘릴 수 있습니다 original_count = 1000 augmentation_factor = 5 # 원본 1장 -> 5장의 변형 augmented_count = original_count * augmentation_factor print(f"=== 데이터 증강 효과 ===") print(f"원본 데이터: {original_count}장") print(f"증강 배수: {augmentation_factor}배") print(f"증강 후 데이터: {augmented_count}장") print(f"증가량: +{augmented_count - original_count}장") # 주의: 증강 데이터는 원본의 변형일 뿐 # 실제 다양한 데이터를 대체할 수는 없습니다 print() print(f"주의: 증강은 다양성을 높이지만,") print(f"새로운 장면을 만들어내지는 않습니다!")
디렉토리 구조 설계
체계적인 디렉토리 구조는 프로젝트 관리의 기본입니다.
핵심 정리
| 단계 | 핵심 포인트 | 주의사항 |
|---|---|---|
| 수집 | 공개 데이터 + 직접 촬영 + 합성 데이터 혼합 | 다양성 확보가 최우선 |
| 라벨링 | YOLO 형식(검출) + CSV(인식) 이중 라벨 | 품질 > 수량, 이중 검수 필수 |
| 전처리 | 크기 통일, 정규화, 색상 변환 | BGR/RGB 순서 혼동 주의 |
| 증강 | 현실적인 변형만 적용 | 과도한 증강은 오히려 해로움 |
| 분할 | 70/15/15 비율, 데이터 누수 방지 | 같은 차량 사진이 세트 간 섞이지 않도록 |
- •데이터가 60%: AI 프로젝트 시간의 대부분은 데이터 작업에 쓰인다
- •혼합 수집 전략: 공개 데이터 + 직접 촬영 + 합성 데이터를 적절히 조합
- •라벨링 품질 관리: 잘못된 라벨 1개가 모델 성능을 크게 떨어뜨린다
- •현실적 증강: 실제 환경에서 일어날 수 있는 변형만 적용
- •체계적 구조: 디렉토리, 파일명, 분할 규칙을 처음부터 정하고 시작
레슨 정보
- 레벨
- Level 9: 종합 프로젝트
- 예상 소요 시간
- 60분
- 참고 영상
- YouTube 링크
💡실습 환경 안내
이 레벨은 PyTorch/GPU가 필요하여 Google Colab 사용을 권장합니다.
Colab은 무료 GPU를 제공하여 PyTorch, CNN, Transformer 등을 실행할 수 있습니다.