OpenCV 비전 실무8장 · Threshold, Adaptive Threshold, Otsu19 / 21← 이전목차다음 →MetaContents

8장. Threshold, Adaptive Threshold, Otsu

프로젝트 1 — 영수증 글자 살리기

강사가 "영수증 글자 인식" 수업을 준비하며 가상의 영수증 사진 세 장을 만들었습니다. 그런데 한 장은 아래로 갈수록 어둡고, 한 장은 오른쪽 위에 휴대폰 그림자가 졌고, 한 장은 가운데에 둥근 얼룩이 있습니다. 글자 인식 프로그램에 넣기 전에 글자만 흰색으로 남기는 이진화를 하려고 합니다. 전역 임계값, Otsu, Adaptive Threshold 세 방법으로 이진화하고, 미리 알고 있는 정답 마스크와 견줘 가장 좋은 방법을 골라 보세요.

데이터는 ch08_receipts/ 에 있습니다. 가게 이름(DEMO CAFE, PRACTICE MART, SAMPLE BAKERY), 품목, 금액, 날짜는 모두 지어낸 것이고, 실제 가게나 사람과 관계없습니다.

파일내용
receipt_1.png아래로 갈수록 어두워지는 영수증 (180×240, 흑백)
receipt_2.png오른쪽 위에 짙은 그림자가 진 영수증
receipt_3.png가운데에 둥근 얼룩이 있고 전체가 조금 어두운 영수증
receipt_N_mask.png영수증 N 의 정답 마스크. 그림을 만들 때 글자 자리를 따로 저장했다. 글자 칸 255, 바탕 0

요구사항

  1. 영수증 세 장과 정답 마스크를 흑백으로 읽고, 영수증마다 글자 칸의 비율을 찍습니다.
  2. 글자가 흰색(255)이 되도록 세 방법 모두 _INV 방식을 씁니다. 전역 임계값은 127, Adaptive 는 ADAPTIVE_THRESH_GAUSSIAN_C, blockSize 15, C 10 으로 합니다.
  3. 영수증마다, 방법마다 정확도(정답 마스크와 같은 칸의 비율)와 글자 찾은 비율(정답의 글자 칸 가운데 결과도 흰색인 칸의 비율)을 재어 표로 찍습니다. 소수 셋째 자리까지 적습니다.
  4. 방법마다 세 장의 평균 정확도를 내고, 가장 높은 방법을 찍습니다.
  5. 줄은 영수증, 칸은 원본·정답·세 방법인 격자 그림을 out_ch08_receipts.png 로 저장하고 화면에 띄웁니다.

힌트

  1. _INV 와 Otsu 를 함께 쓰려면 cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU 처럼 더합니다.
  2. 정확도는 (out == mask).mean(), 글자 찾은 비율은 ((out == 255) & (mask == 255)).sum() / (mask == 255).sum() 입니다. & 는 두 조건이 모두 참인 칸만 남깁니다.
  3. 방법 세 가지를 "이름: 함수" 사전으로 만들어 두면 반복문 하나로 모두 돌릴 수 있습니다.
  4. 표는 pandas 의 DataFrame 으로 만들고, groupby("방법")["정확도"].mean() 으로 방법마다의 평균을 냅니다.
  5. 격자는 plt.subplots(3, 5) 로 만들고, plt.savefig 를 plt.show() 보다 먼저 부릅니다.

채점 기준

항목기준점수
읽기영수증과 정답 마스크를 짝지어 읽고 글자 비율을 찍는다15
세 방법세 방법을 정해진 값과 _INV 방식으로 쓴다25
재기정확도와 글자 찾은 비율을 영수증·방법마다 표로 찍는다25
고르기방법마다의 평균을 내고 가장 좋은 방법을 찍는다15
격자 그림원본·정답·세 결과를 격자로 저장하고 띄운다20

코드의 흐름

읽기, 세 방법, 견주기, 표, 격자 저장의 다섯 단계를 이은 흐름도

그림의 다섯 단계를 예시 풀이의 단계와 같은 순서로 짰습니다. 셋째 단계에서 정확도 하나만 보지 않고 글자 찾은 비율을 함께 보는 까닭은, 글자가 그림의 6% 남짓이라 모든 칸을 검정으로 칠해도 정확도가 0.94 쯤 나오기 때문입니다.

실제 데이터로 해 보려면

실제 영수증이나 문서 사진으로 해 보려면 AI Hub(aihub.or.kr)처럼 문서·글자 이미지를 공개한 곳에서 내려받습니다. 내려받기 전에 데이터마다 이용 약관(상업적 이용 가능 여부, 재배포 금지 등)을 확인합니다. 영수증에는 카드 번호, 이름, 전화번호 같은 개인정보가 찍혀 있을 수 있으므로, 직접 찍은 영수증을 쓸 때도 그 부분을 가리거나 지운 뒤 씁니다. 실제 사진에는 정답 마스크가 없으므로, 몇 장만 골라 사람이 글자 자리를 칠해 정답을 만들거나 결과 그림을 눈으로 견줘 고릅니다.

결과 보기 (예시 풀이)

1단계. 영수증과 정답 마스크 읽기

Python
import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

names = ["receipt_1", "receipt_2", "receipt_3"]
images, masks = {}, {}
for name in names:
    # 요구사항 1: 영수증과 정답 마스크를 짝지어 흑백으로 읽는다
    images[name] = cv2.imread(f"ch08_receipts/{name}.png", cv2.IMREAD_GRAYSCALE)
    masks[name] = cv2.imread(f"ch08_receipts/{name}_mask.png", cv2.IMREAD_GRAYSCALE)
    text_ratio = round(float((masks[name] == 255).mean()), 3)
    print(f"{name}: 모양 {images[name].shape}, 글자 칸 비율 {text_ratio}")
출력
receipt_1: 모양 (240, 180), 글자 칸 비율 0.058
receipt_2: 모양 (240, 180), 글자 칸 비율 0.059
receipt_3: 모양 (240, 180), 글자 칸 비율 0.06

2단계. 세 방법 정하기

Python
# 요구사항 2: 글자(어두운 칸)가 흰색이 되도록 세 방법 모두 _INV 를 쓴다
def global127(gray):
    return cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)[1]

def otsu(gray):
    return cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

def adaptive(gray):
    return cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                 cv2.THRESH_BINARY_INV, 15, 10)

methods = {"전역 127": global127, "Otsu": otsu, "Adaptive": adaptive}

# Otsu 가 영수증마다 고른 값도 찍어 둔다
for name in names:
    ret, _ = cv2.threshold(images[name], 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    print(f"{name}: Otsu 가 고른 값 {int(ret)}")
출력
receipt_1: Otsu 가 고른 값 148
receipt_2: Otsu 가 고른 값 162
receipt_3: Otsu 가 고른 값 142

3단계. 정답과 견줘 표 만들기

Python
rows, outputs = [], {}
for name in names:
    mask = masks[name]
    for method, func in methods.items():
        out = func(images[name])
        outputs[(name, method)] = out
        # 요구사항 3: 정확도와 글자 찾은 비율
        accuracy = (out == mask).mean()
        found = ((out == 255) & (mask == 255)).sum() / (mask == 255).sum()
        rows.append({"영수증": name, "방법": method,
                     "정확도": round(float(accuracy), 3),
                     "글자 찾은 비율": round(float(found), 3)})

report = pd.DataFrame(rows)
print(report.to_string(index=False))

# 요구사항 4: 방법마다 세 장의 평균 정확도
mean_acc = report.groupby("방법")["정확도"].mean().round(3)
print()
print("[방법마다의 평균 정확도]")
for method in methods:
    print(f"  {method}: {float(mean_acc[method])}")
print("가장 좋은 방법:", mean_acc.idxmax())
출력
      영수증       방법   정확도  글자 찾은 비율
receipt_1   전역 127 0.724     0.996
receipt_1     Otsu 0.606     1.000
receipt_1 Adaptive 0.991     0.984
receipt_2   전역 127 0.737     0.972
receipt_2     Otsu 0.708     1.000
receipt_2 Adaptive 0.992     0.987
receipt_3   전역 127 0.867     1.000
receipt_3     Otsu 0.847     1.000
receipt_3 Adaptive 0.992     0.986

[방법마다의 평균 정확도]
  전역 127: 0.776
  Otsu: 0.72
  Adaptive: 0.992
가장 좋은 방법: Adaptive

4단계. 격자 그림으로 저장하기

Python
fig, axes = plt.subplots(len(names), 5, figsize=(11, 8.4))
for i, name in enumerate(names):
    panels = [("원본", images[name]), ("정답", masks[name])]
    panels += [(method, outputs[(name, method)]) for method in methods]
    for j, (title, im) in enumerate(panels):
        axes[i, j].imshow(im, cmap="gray", vmin=0, vmax=255)
        # 맨 위 줄에만 칸 제목을, 맨 왼쪽 칸에는 영수증 이름을 적는다
        if i == 0:
            axes[i, j].set_title(title)
        axes[i, j].set_xticks([])
        axes[i, j].set_yticks([])
    axes[i, 0].set_ylabel(name)
plt.tight_layout()
# 요구사항 5: 저장을 먼저 하고 띄운다
plt.savefig("out_ch08_receipts.png", dpi=100)
print("저장: out_ch08_receipts.png")
plt.show()
출력
저장: out_ch08_receipts.png
영수증 세 장의 원본, 정답 마스크, 전역 127, Otsu, Adaptive 결과를 3행 5열로 놓은 격자

표를 보면 Adaptive 가 세 장 모두에서 정확도 0.991~0.992 로 가장 높고, 평균 정확도도 0.992 로 전역 127(0.776), Otsu(0.72)를 크게 앞섭니다. 전역 127 과 Otsu 는 글자 찾은 비율이 0.972~1.000 으로 높은데도 정확도가 낮습니다. 격자 그림에서 보듯 영수증 1의 아래쪽, 영수증 2의 오른쪽 위 그림자, 영수증 3의 둥근 얼룩을 통째로 흰색(글자)으로 칠했기 때문입니다. 글자를 빠짐없이 찾았어도 그늘까지 글자로 본 것입니다. 특히 Otsu 는 글자가 그림의 6% 남짓이라 글자와 종이 대신 "밝은 종이"와 "그늘진 종이"를 두 무리로 나누어 148, 162, 142 처럼 높은 임계값을 골랐고, 그래서 전역 127 보다도 그늘을 더 많이 칠했습니다. Adaptive 는 글자 찾은 비율이 0.984~0.987 로 조금 낮은데, 글자 획의 가장자리 몇 칸을 놓친 것입니다. 이 영수증들처럼 그늘과 얼룩이 있는 문서에는 Adaptive Threshold 가 알맞습니다. 결과에 남은 작은 끊김과 잡티는 9장의 모폴로지로 다듬습니다.

← 이전 절8장 · 실습 5 — Otsu 로 자동 임계값 정하기다음 절 →8장 · 정리