2025. 6. 4. 02:17ㆍ프로젝트
OCR 인식률을 개선하기 위해 문제 단위로 이미지를 자동 분할하는 스크립트를 만들었습니다.
https://choddu.tistory.com/27
PDF 시험지에서 문제별로 이미지 자동 자르기 - 텍스트 기반 (PyMuPDF + OpenCV)
프로젝트를 진행하면서 수식이 포함된 시험 문제의 경우, 전체 페이지 단위로 OCR을 적용하면 인식 정확도가 떨어진다는 점을 알게 되었습니다.특히 수식이 많을수록 잘못 인식되거나 문장 구조
choddu.tistory.com
지난 게시물에서는 텍스트 기반 PDF를 처리하는 스크립트를 소개했는데, 이번에는 이미지 기반 PDF를 대상으로 한 스크립트를 정리했습니다.
이미지 기반 PDF는 Python에서 직접 텍스트를 추출할 수 없기 때문에, OCR 기술을 활용해 다른 방식으로 접근해야 합니다.
참고로,
- PDF에서 글자가 드래그로 복사된다면 텍스트 기반
- 복사가 되지 않는다면 이미지 기반 PDF입니다.
필요 라이브러리 설치
pip install pymupdf opencv-python tesseract
- PyMuPDF (fitz): PDF에서 텍스트와 좌표 추출
- OpenCV: 이미지 자르기 및 저장
- Tesseract : 이미지에서 글자를 인식해 텍스트로 변환해주는 오픈소스 OCR 엔진
tesseract 한글팩 설치
Tesseract에서 이미지를 한글로 인식하려면 한글 언어팩 설치가 필요합니다.
(macOS)
# 다양한 언어가 설치되는데 여기에 kor.traineddata(한글팩)도 포함됨
brew install tesseract-lang
설치가 완료되면 보통 /opt/homebrew/share/tessdata/ 경로에 kor.traineddata 파일이 저장되어 있습니다.
(Window)
https://github.com/tesseract-ocr/tessdata
GitHub - tesseract-ocr/tessdata: Trained models with fast variant of the "best" LSTM models + legacy models
Trained models with fast variant of the "best" LSTM models + legacy models - tesseract-ocr/tessdata
github.com
위 경로에서 kor.traineddata 파일을 다운로드한 후, Tesseract 설치 경로의 tessdata 폴더에 넣어주면 됩니다.
Windows에서는 보통 C:\Program Files\Tesseract-OCR\tessdata\ 경로에 위치합니다.
전체 코드
1. PDF 열기
pdf_path = "법인세법_기본문제.pdf"
output_folder = pdf_path.replace(".pdf", "")
os.makedirs(output_folder, exist_ok=True)
doc = fitz.open(pdf_path)
PyMuPDF (fitz) 라이브러리를 이용해 파이썬에서 PDF 파일을 불러옵니다.
2. PDF 페이지를 이미지로 로드
# PDF 페이지 로드 (7페이지~24페이지)
for p in range(6, 24):
page = doc.load_page(p)
# 페이지를 300dpi 해상도의 이미지(Pixmap)로 변환
full_pix = page.get_pixmap(dpi=300)
# Pixmap의 이미지 데이터를 NumPy 배열로 변환
full_img = np.frombuffer(full_pix.samples, dtype=np.uint8).reshape((full_pix.height, full_pix.width, full_pix.n))
# RGBA (Red, Green, Blue, Alpha)이미지인 경우 → RGB로 변환
# 왜냐하면 OpenCV는 RGB만 지원하기 떄문
if full_pix.n == 4:
full_img = cv2.cvtColor(full_img, cv2.COLOR_RGBA2RGB)
# 이미지 디버깅
cv2.imshow("이미지", full_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
PDF를 불러와서 이미지를 로드하는 코드입니다.
중간에 보이는 NumPy 배열로의 변환은 이미지를 (높이, 너비, 채널 수<*색상 정보 수, RGB면 3개>) 구조로 만들기 위한 것으로,
이 배열은 이후 Tesseract OCR의 입력값으로 사용되거나, OpenCV로 이미지 전처리할 때 활용됩니다.
3. 이미지 영역 조정
문제번호가 PDF의 왼쪽에 정렬돼 있는 경우가 많아서, 숫자 인식률을 높이기 위해 이미지의 좌측 부분만 잘라보도록 영역을 조정해봤습니다.
# 문제번호 인식률을 높이기 위해 문제영역 조정
clip_rect = fitz.Rect(0, 120, 150, 700)
clip_pix = page.get_pixmap(dpi=300, clip=clip_rect)
clip_img = np.frombuffer(clip_pix.samples, dtype=np.uint8).reshape((clip_pix.height, clip_pix.width, clip_pix.n))
if clip_pix.n == 4:
clip_img = cv2.cvtColor(clip_img, cv2.COLOR_RGBA2RGB)
# 이미지 디버깅
cv2.imshow("이미지1", clip_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
clip_rect = fitz.Rect(0, 120, 150, 700)
PyMuPDF의 Rect(x0, y0, x1, y1)의 형식입니다.
- x0 = 0, y0 = 120 → 좌측 상단
- x1 = 150, y1 = 700 → 우측 하단
이 좌표는 PDF 페이지 기준으로 좌측 상단에서 우측 하단 방향까지의 사각형 영역을 지정합니다.
지정한 좌표 범위대로 이미지가 잘 잘린 것을 확인할 수 있습니다.
4. 숫자 인식률 높이기
위 과정으로 얻은 이미지에 바로 OCR을 적용해봤지만, 숫자가 잘 인식되지 않았습니다.
아무래도 주변에 글자가 많다 보니, 숫자만 더 정확히 추출할 수 있는 방법이 필요하다고 느꼈습니다.
제가 생각한 방법은, 숫자의 색상 코드를 기준으로 해당 색상만 남기고 나머지 영역은 모두 흑백 처리하는 것입니다.
이를 통해 숫자 외의 배경이나 다른 텍스트의 영향을 줄이고, OCR 인식률을 높이고자 했습니다.
lower_pink = np.array([200, 0, 200])
upper_pink = np.array([255, 80, 255])
mask = cv2.inRange(clip_img, lower_pink[::-1], upper_pink[::-1])
masked_img = cv2.bitwise_and(clip_img, clip_img, mask=mask)
추출하고자 하는 색상의 하한값과 상한값을 설정한 뒤,
cv2.inRange()를 이용해 해당 색상 범위에 포함되는 픽셀만 흰색(255), 나머지는 검정색(0)으로 구성된 마스크 이미지를 생성합니다.
이 마스크를 기준으로 cv2.bitwise_and()를 적용하면, 마스크가 흰색인 부분만 남기고 나머지는 제거되어
결과적으로 숫자 색상만 남은 이미지가 만들어집니다.
5. pytesseract를 이용해 이미지에서 숫자만 추출
# Thresholding 처리
# OCR 인식을 위해 불필요한 색상정보 제거하고,
# 밝기값이 100 이상이면 흰색(255), 아니면 검정색(0) 설정
gray = cv2.cvtColor(masked_img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)
# Tesseract OCR 설정
config = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789'
ocr_data = pytesseract.image_to_data(thresh, config=config, output_type=pytesseract.Output.DICT)
print('ocr_data',ocr_data['text'])
# ocr_data['', '', '', '', '26', '', '27']
# 인식된 숫자의 y좌표 저장
positions = []
for i in range(len(ocr_data['text'])):
word = ocr_data['text'][i]
if re.fullmatch(r'\d+', word):
y_clip_px = ocr_data['top'][i]
number = int(word)
positions.append({'number': number, 'y_clip_px': y_clip_px})
print('positions',positions)
# positions[{'number': 26, 'y_clip_px': 85}, {'number': 27, 'y_clip_px': 1455}]
이 코드는 마스킹된 이미지에서 OCR을 이용해 숫자만 인식하고, 각 숫자의 y 위치 정보를 기록하는 역할을 합니다.
# Tesseract OCR 설정
config = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789'
ocr_data = pytesseract.image_to_data(thresh, config=config, output_type=pytesseract.Output.DICT)
- --oem 3 : OCR Engine Mode 3
→ OCR 엔진 종류 설정, 기존 Tesseract 엔진과 인공지능(LSTM) 기반 엔진을 혼합해서 사용해 더 정확하게 인식 - --psm 6 : Page Segmentation Mode 6
→ 글자가 어떻게 배치돼 있는지에 대한 설정, "한 덩어리에 여러 줄 텍스트가 있다"고 가정해 글자를 인식 - tessedit_char_whitelist=0123456789 :
→ 숫자(0~9)만 인식하도록 제한, 불필요한 문자 인식 방지
즉, 정확하게 줄 단위로 숫자만 인식하게 만드는 설정입니다.
결과를 보면 이미지에 있는 26, 27을 잘 추출한 것을 볼 수 있습니다.
6. OCR로 찾은 y좌표를 기준으로 문제별 이미지 영역을 나누어 잘라서 각각 이미지 파일로 저장
# 정렬
positions.sort(key=lambda x: x['y_clip_px'])
# OCR 결과로 얻은 클립 영역 내 상대 좌표(픽셀 단위)를
# 기존 PDF 페이지 전체 좌표(점 단위)로 변환하기 위해
# 사용되는 변수 선언
clip_top_pt = clip_rect.y0
clip_height_pt = clip_rect.y1 - clip_rect.y0
clip_height_px = clip_img.shape[0]
full_height_px = full_img.shape[0]
page_height_pt = page.rect.height
# 문제별 자르기
for i, q in enumerate(positions):
# OCR 상대 y → 절대 y (pt) → full_img px 변환
# 공식 : 페이지 위치 = 클립 영역 시작 위치 + (OCR 위치 ÷ 클립 이미지 높이) × 클립 영역 높이
y_pt = clip_top_pt + q['y_clip_px'] * clip_height_pt / clip_height_px
y1 = int(y_pt * full_height_px / page_height_pt)
if i + 1 < len(positions):
y_pt_next = clip_top_pt + positions[i + 1]['y_clip_px'] * clip_height_pt / clip_height_px
y2 = int(y_pt_next * full_height_px / page_height_pt)
else:
y2 = full_img.shape[0]
# 크롭 및 저장
cropped = full_img[y1-20:y2, :]
filename = f"{output_folder}/{q['number']:02d}.png"
cv2.imwrite(filename, cropped)
print(f"Saved: {filename} ({y1}px ~ {y2}px)")
'''
Saved: 법인세법_기본문제/01.png (670px ~ 2059px)
Saved: 법인세법_기본문제/02.png (2059px ~ 3188px)
Saved: 법인세법_기본문제/03.png (292px ~ 1216px)
Saved: 법인세법_기본문제/04.png (1216px ~ 2053px)
Saved: 법인세법_기본문제/05.png (2053px ~ 3188px)
'''
먼저, 인식된 숫자의 y좌표를 위에서 아래로 정렬한 뒤,
이 좌표를 기준으로 이미지에서 각 문제 영역을 순차적으로 잘라냅니다.


느낀점
OCR 인식률을 높이기 위해 텍스트 기반 PDF와 이미지 기반 PDF에서 문제 영역을 자동으로 추출하는 파이썬 스크립트를 작성했습니다.OpenCV를 처음 사용하다 보니 여러 시행착오를 겪었고, 이미지에서 데이터를 추출하는 일이 생각보다 어려웠습니다.
노이즈 제거부터 인식률 향상을 위한 다양한 방법을 고민하며 개발자로서 많이 고민하고 성장한 뜻깊은 경험이었습니다.
'프로젝트' 카테고리의 다른 글
PDF 시험지에서 문제별로 이미지 자동 자르기 - 텍스트 기반 (PyMuPDF + OpenCV) (0) | 2025.06.03 |
---|---|
PyKoSpacing으로 OCR 텍스트 후처리 : 한국어 띄어쓰기 보정 라이브러리 활용 예시 (1) | 2025.06.03 |
Mac에서 Spring Boot + JDK 17 + IntelliJ Community 개발환경 세팅 (2) | 2025.04.24 |
Git 커밋 메시지 템플릿 설정하기 (.gitmessage.txt 사용법) (0) | 2025.03.29 |
[taxpass 프로젝트] Expo로 개발 환경 세팅하기 (웹 + 모바일) (0) | 2025.03.29 |