이미지에 담긴 숫자를 깔끔하게 추출해서, 해당 데이터를 활용할 로직을 짜야 할 일이 생겼다. 호기롭게 python과 EasyOCR을 데리고 시작했다. 이런 건 일도 아닐 거라고 생각했다.
import easyocr
import cv2
import matplotlib.pyplot as plt
# 1. OCR 리더 초기화 (한 번만 실행)
reader = easyocr.Reader(['en'], gpu=False)
def test_ocr(image_path):
# 이미지 읽기
img = cv2.imread(image_path)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 숫자 추출 (숫자만 인식하도록 )
results = reader.readtext(image_path, allowlist='0123456789')
print(f"--- [{image_path}] 결과 ---")
for (bbox, text, prob) in results:
if prob > 0.5: # 신뢰도가 50% 이상인 것만
print(f"인식된 번호: {text} (정확도: {prob:.2f})")
# 이미지에 박스 그리기 (시각화)
(tl, tr, br, bl) = bbox
cv2.rectangle(img_rgb, (int(tl[0]), int(tl[1])), (int(br[0]), int(br[1])), (0, 255, 0), 5)
plt.text(int(tl[0]), int(tl[1]) - 10, text, color='red', fontsize=12, fontweight='bold')
plt.imshow(img_rgb)
plt.axis('off')
plt.show()
# 준비한 이미지 파일명을 여기에 넣고 실행
# test_ocr('image1.jpg')
그런데 왠걸, 예시 이미지를 찾아 구글링 했더니 예쁘게 숫자만 적힌 건 나오지도 않았다. 위에는 해당 연도와 행사 이름, 가운데 숫자만 있어야 할 영역은 거리와 이름, 하단에는 후원사까지.
알고 보니 OCR 프로젝트에서 흔히 겪는 데이터 필터링 문제였다. 실제 사용하게 되는 이미지들은 이렇게 noisy한 경우가 많다고 한다. 이럴 때 해결책은
1. 크기 기반 필터링
이렇게 한번에 추출해야 하는 값이 있다면 양식이 통일되어 있기 마련이다. 내가 추출해야 하는 이미지에서는 추출할 숫자값이 가장 크다는 원칙이 양식에 있었다. EasyOCR은 글자의 좌표(Bounding Box)를 반환하는데, 이 좌표로 면적을 계산한 뒤 가장 큰 글자를 골라내는 방법이 있다.
2. 정규표현식(Regex) 활용
마찬가지로 다른 정보값이 있다고 해도, 해당 정보값에도 양식이 있기 마련이다. 나의 경우 연도(2026)나 길이(10, 42), 그리고 내가 추출해야 하는 값(4-5자리)에 차이가 있었다.
-
길이 제한: 숫자가 3자리 미만이거나 6자리 이상이면 탈락
-
특정 숫자 제외: 해당 연도 (2026)이나 반복되는 길이(10, 42, 5) 등은 리스트에서 제외하는 로직 추가
위 과정을 거쳐 나온 코드는 아래와 같다. 주피터 환경에서 실행중이라 셀 단위로 잘랐다.
1. 환경 준비
import easyocr
import cv2
import matplotlib.pyplot as plt
import re
# OCR 엔진 초기화 (한글이 섞여있을 수 있으니 'ko'도 추가)
reader = easyocr.Reader(["ko", "en"], gpu=False)
print("OCR 엔진 준비 완료!")
2. 노이즈 제외 로직
def get_best_bib(ocr_results):
candidates = []
for bbox, text, prob in ocr_results:
# 1. 면적 계산
(tl, tr, br, bl) = bbox
width = tr[0] - tl[0]
height = br[1] - tr[1]
area = width * height
# 2. 숫자만 추출
clean_text = re.sub(r"[^0-9]", "", text)
# 3. 조건: 3~5자리 숫자이고 정확도가 0.3 이상인 것들만 후보로!
if 3 <= len(clean_text) <= 5 and prob > 0.3:
candidates.append({"text": clean_text, "area": area, "prob": prob})
# 면적이 가장 큰 순서로 정렬
candidates.sort(key=lambda x: x["area"], reverse=True)
return candidates[0]["text"] if candidates else "인식 실패"
3. 파일 한 개 테스트
# 테스트
# 파일명 여기에
image_path = "images/sample_7.jpg"
# 1. OCR 실행
raw_results = reader.readtext(image_path)
# 2. 필터링 함수로 배번만 추출
final_bib = get_best_bib(raw_results)
# 3. 시각화 및 결과 출력
img = cv2.imread(image_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
print(f"★ 최종 확정 값: {final_bib}")
plt.figure(figsize=(10, 10))
plt.imshow(img)
plt.title(f"Detected Bib: {final_bib}")
plt.axis("off")
plt.show()
4. 로직이 잘 동작하면 여러 파일 반복문 돌리기!
import os
# 1. 사진들이 들어있는 폴더 경로
image_dir = "images"
# 폴더 내 파일 목록 가져오기 (jpg, png 등 이미지 파일만)
image_files = [
f for f in os.listdir(image_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))
]
# 2. 반복문으로 하나씩 처리
for file_name in image_files:
image_path = os.path.join(image_dir, file_name)
# OCR 실행
raw_results = reader.readtext(image_path)
# 필터링 함수로 배번 추출
final_bib = get_best_bib(raw_results)
# --- 시각화 부분 ---
img = cv2.imread(image_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 새로운 도화지(Figure) 반복 생성
plt.figure(figsize=(8, 5))
plt.imshow(img)
plt.title(f"File: {file_name} | Detected: {final_bib}")
plt.axis("off")
# ★ 루프 안에서 명시적으로 보여달라고 하기
plt.show()
print(f"처리 완료: {file_name} -> {final_bib}")
print("-" * 30)
으하하 잘 작동해서 기분 좋다 !!