8장. Threshold, Adaptive Threshold, Otsu
실습 3 — 그늘진 안내문에서 전역과 Adaptive 견주기
오른쪽에 그늘이 진 가상의 실습 안내문(ch08_shadow_doc.png)을 전역 임계값, Otsu, Adaptive Threshold 두 가지로 이진화하고, 그늘 없는 판(ch08_clean_doc.png)을 127 로 나눈 것을 정답으로 삼아 견줍니다. 가장 좋은 결과는 out_ch08_doc_binary.png 로 저장합니다.
1단계. 그늘의 크기 재기
import cv2
import numpy as np
gray = cv2.imread("ch08_shadow_doc.png", cv2.IMREAD_GRAYSCALE)
clean = cv2.imread("ch08_clean_doc.png", cv2.IMREAD_GRAYSCALE)
print("모양:", gray.shape)
# 글자가 없는 아래 여백 가까이(세로 230~239)를 가로로 네 토막 내어 종이 밝기를 잰다
for x0 in [0, 80, 160, 240]:
strip = gray[230:240, x0:x0 + 80]
print(f"가로 {x0:3}~{x0 + 79:3}: 평균 {round(float(strip.mean()), 1)}")
# 정답: 그늘 없는 판을 127 로 나눈 것(종이 흰색, 글자 검정)
_, answer = cv2.threshold(clean, 127, 255, cv2.THRESH_BINARY)
print("정답의 검은 칸(글자) 비율:", round(float((answer == 0).mean()), 3))모양: (240, 320)
가로 0~ 79: 평균 197.9
가로 80~159: 평균 163.5
가로 160~239: 평균 120.2
가로 240~319: 평균 71.2
정답의 검은 칸(글자) 비율: 0.0592단계. 전역 임계값과 Otsu
_, global127 = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# THRESH_OTSU 를 더하면 thresh(0)는 무시되고, 고른 값이 ret 으로 돌아온다
ret, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print("Otsu 가 고른 값:", int(ret))
def report(name, out):
"""정답과 같은 칸의 비율, 왼쪽 절반과 오른쪽 절반의 검은 칸 비율을 찍는다."""
agree = round(float((out == answer).mean()), 3)
left = round(float((out[:, :160] == 0).mean()), 3)
right = round(float((out[:, 160:] == 0).mean()), 3)
print(f"{name:12} 정답과 같은 칸 {agree} / 검은 칸 왼쪽 {left}, 오른쪽 {right}")
report("전역 127", global127)
report("Otsu", otsu)Otsu 가 고른 값: 137
전역 127 정답과 같은 칸 0.619 / 검은 칸 왼쪽 0.125, 오른쪽 0.754
Otsu 정답과 같은 칸 0.56 / 검은 칸 왼쪽 0.134, 오른쪽 0.8623단계. Adaptive Threshold 두 가지
# cv2.adaptiveThreshold: 칸마다 주변 15×15 의 평균에서 10 을 뺀 값을 기준으로 쓴다
mean_c = cv2.adaptiveThreshold(gray, # src
255, # maxValue: 기준보다 큰 칸에 넣을 값
cv2.ADAPTIVE_THRESH_MEAN_C, # 주변을 똑같이 더해 나눈 평균
cv2.THRESH_BINARY, # 크면 255(종이), 아니면 0(글자)
15, # blockSize: 15×15 네모
10) # C: 평균에서 뺄 값
# GAUSSIAN_C: 가운데에 가까운 칸일수록 무게를 더 준 평균
gauss_c = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, 10)
report("MEAN_C", mean_c)
report("GAUSSIAN_C", gauss_c)
report("정답", answer)MEAN_C 정답과 같은 칸 0.981 / 검은 칸 왼쪽 0.135, 오른쪽 0.019
GAUSSIAN_C 정답과 같은 칸 0.987 / 검은 칸 왼쪽 0.125, 오른쪽 0.018
정답 정답과 같은 칸 1.0 / 검은 칸 왼쪽 0.103, 오른쪽 0.0144단계. 네 결과를 견주고 저장하기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
fig, axes = plt.subplots(2, 3, figsize=(11, 5.4))
panels = [("원본", gray), ("전역 127", global127), (f"Otsu ({int(ret)})", otsu),
("정답(그늘 없는 판)", answer), ("Adaptive MEAN_C", mean_c), ("Adaptive GAUSSIAN_C", gauss_c)]
for ax, (title, im) in zip(axes.flat, panels):
ax.imshow(im, cmap="gray", vmin=0, vmax=255)
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()
# 가장 정답에 가까운 결과를 저장한다
print("저장:", cv2.imwrite("out_ch08_doc_binary.png", gauss_c))저장: True
코드 설명
- 아래 여백의 종이 밝기가 왼쪽 토막 197.9 에서 오른쪽 토막 71.2 까지 떨어집니다. 같은 종이인데 오른쪽으로 갈수록 그늘이 짙습니다.
- 전역 127 은 오른쪽 절반의 검은 칸이 0.754, Otsu(137)는 0.862 입니다. 정답의 오른쪽 검은 칸은 0.014 뿐이므로, 그늘진 종이를 통째로 글자로 본 것입니다. 정답과 같은 칸도 0.619 와 0.560 에 그칩니다.
cv2.adaptiveThreshold(gray, 255, 방법, cv2.THRESH_BINARY, 15, 10)는 칸마다 주변 15×15 의 평균에서 10 을 뺀 값을 기준으로 씁니다. 오른쪽 검은 칸이 MEAN_C 0.019, GAUSSIAN_C 0.018 로 정답(0.014)에 가깝습니다.- 정답과 같은 칸은 MEAN_C 0.981, GAUSSIAN_C 0.987 로, 이 그림에서는 GAUSSIAN_C 가 조금 더 낫습니다. 그래서 GAUSSIAN_C 결과를
out_ch08_doc_binary.png로 저장했습니다.