YOLO 객체 탐지로 푸는 번호판 인식 문제
전통 OCR의 한계를 넘어 YOLO 기반 3단계 파이프라인으로 만든 실전 번호판 인식 시스템
문제의식
전통적인 OCR은 깔끔한 문서에서는 잘 작동하지만, 번호판처럼 흐릿하고 기울어진 이미지에서는 취약합니다. 문자 간격이 불규칙하거나 일부가 가려지면 인식률이 급락합니다. 또한, 한국 자동차 번호판은 문자 배열이 다양하고 지역명과 숫자가 혼합되어 있어 일반적인 OCR 모델로는 한계가 있었습니다.
이 프로젝트는 문자를 텍스트가 아닌 객체로 보는 관점을 취했습니다. YOLO 기반 객체 탐지로 각 문자를 독립적으로 찾고, 원본과 보정 이미지 두 버전에 각각 OCR을 수행하여 이중 검증합니다. NMS와 극단점 기반 라인 피팅으로 지역명과 숫자를 분리하며, 전통 OCR이 실패하는 환경에서도 견고하게 작동합니다.
3단계 파이프라인
전체 흐름은 detect.py의 get_num() 함수 하나로 제어합니다. 세 개의 ONNX 모델이 각자의 전문 분야를 담당합니다.
단계 | 모델 | 역할 |
|---|---|---|
1 |
| 전체 이미지에서 번호판 영역 탐지 |
2 |
| 네 모서리 탐지 후 원근 보정 |
3 |
| 75개 클래스로 개별 문자 탐지 |
첫 번째 모델은 전체 이미지에서 번호판 영역 하나를 찾습니다. 여러 후보 중 신뢰도가 가장 높은 것을 선택해 크롭합니다. 두 번째 모델은 크롭된 이미지에서 네 모서리를 찾아 원근 보정을 수행합니다. 세 번째 모델은 75개 클래스로 개별 문자를 객체로 탐지합니다. 전통 OCR처럼 텍스트 라인을 읽는 것이 아니라 번호판 내에서 각 문자의 위치를 찾기 때문에 문자 간격이 불규칙하거나 일부가 가려져도 나머지는 인식할 수 있습니다.
graph LR
Input[입력 이미지] --> Plate[번호판 탐지]
Plate --> Crop[번호판 크롭]
Crop --> Vertex[모서리 탐지]
Vertex --> Warp[원근 보정]
Warp --> OCR1[warped OCR]
OCR1 --> Validate1[정규표현식 검증]
Validate1 -->|통과| Output[최종 번호]
Validate1 -->|실패| OCR2[cropped OCR]
Crop --> OCR2
OCR2 --> Validate2[정규표현식 검증]
Validate2 --> Output
검증과 Fallback: 안정성과 효율의 균형
원근 변환이 항상 완벽하지는 않습니다. 모서리 탐지가 실패하거나 기울어진 번호판을 마주할 수 있습니다.
이 프로젝트는 warped 이미지를 우선적으로 OCR하고, 정규표현식 검증을 통과하면 즉시 결과를 반환합니다. 실패할 경우에만 원본 cropped 이미지로 fallback하여 추가 OCR을 수행합니다.
def get_num(img):
cropped = plate_detector.detect_and_crop(img)
warped = vertex_detector.detect_and_warp(cropped)
# Step 1: warped 이미지 우선 OCR
if warped is not None:
res = syllable_detector.get_num_from_img(warped)
result = validate_plate_num(res, mask=False)
if result:
return result
# Step 2: fallback - cropped 이미지 OCR
if cropped is not None:
res = syllable_detector.get_num_from_img(cropped)
result = validate_plate_num(res, mask=False)
if result:
return result
return None
def validate_plate_num(plate_num, mask=True):
"""단일 OCR 결과에 대해 regex 검증 + mask 적용"""
if plate_num is None:
plate_num = ''
m = re.fullmatch(PLATE_REGEX, plate_num)
if m is None:
return None
if mask:
plate_num = re.search(OUTPUT_REGEX, plate_num).group()
return plate_num이 방식은 warping이 성공하면 OCR 1회로 처리하고, 실패할 때만 cropped 이미지로 fallback합니다. 불필요한 추론을 줄이면서도 안정성을 유지하는 설계입니다.
문자 탐지 후에는 NMS로 중복 바운딩 박스를 제거하고, 좌우 끝점을 기준으로 선을 그어 지역명과 숫자 영역을 분리합니다. 지역명은 서울, 경기, 부산 등 하드 코딩된 유효 지역 목록과 비교하여 교정합니다.
sequenceDiagram
participant Detect as detect.py
participant ONNX as ONNX Runtime
participant Validate as validate_plate_num
Detect->>ONNX: warped 이미지로 문자 인식
ONNX-->>Detect: 결과
Detect->>Validate: 정규표현식 검증
alt 검증 통과
Validate-->>Detect: 최종 번호 반환
else 검증 실패
Detect->>ONNX: cropped 이미지로 문자 인식
ONNX-->>Detect: 결과
Detect->>Validate: 정규표현식 검증
alt 검증 통과
Validate-->>Detect: 최종 번호 반환
else
Validate-->>Detect: 실패 반환
end
end사용자를 배려한 GUI 설계
배치 처리 중 일부 이미지가 검출 실패하면 어떻게 해야 할까요? 무시하고 넘어가면 데이터 품질이 떨어지고, 전체를 중단하면 효율성이 떨어집니다.
PySide6 기반 GUI는 검출 실패 시 자동으로 일시 정지하고 해당 이미지를 화면에 보여줍니다. 사용자가 직접 번호판 번호를 입력하고 확인 버튼을 누를 때까지 처리를 멈춥니다.
이는 모든 데이터에 대해서 정확한 검출이 필요한 상황을 위한 설계이며, 불완전한 부분을 임의로 보정하는 것이 아닌 사용자가 판단하도록 설계하였습니다.
트레이드오프와 설계 철학
YOLO 기반 문자 탐지는 성능상 전처리 기반 OCR보다 느리고 무겁다는 단점이 있습니다. 또한 여러 단계로 나뉘어 각기 다른 모델을 이용한다는 최적화 문제도 남아 있습니다.
하지만, 이 프로젝트의 본질은 전처리를 사용하지 않은 방식에서 인식률을 확인하는 것에 있었습니다. 추후 전처리를 유용하게 사용하고 단일 모델로 번호판 및 글자들을 인식하여 조합하는 방식을 사용한다면 인식률을 보존하거나 향상하면서 탐지 속도를 향상할 수 있을 것입니다.
마치며
이 프로젝트를 진행하면서 개인적으로 머신러닝을 처음으로 서비스에 접목한 서비스입니다. 모델을 파인 튜닝하고 새로운 방향으로 접근을 시도했다는 점에서 의미 또한 있는 프로젝트입니다.
현재의 시각으로 여러 개선점을 다시 확인하며 발견할 수 있었습니다. 향후 프로그램을 업데이트하면서 더 나은 성능과 속도를 가질 수 있도록 개선하고자 합니다.