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

8장. Threshold, Adaptive Threshold, Otsu

실습 1 — 전역 임계값으로 부품 이진화하기

밝은 작업대 위의 부품 그림(ch08_parts.png)을 cv2.threshold 로 이진화합니다. 임계값을 여러 개 바꿔 가며 검정 칸(부품으로 본 칸)의 비율을 재고, 결과를 격자로 견줍니다.

1단계. 그림 읽고 밝기 살펴보기

Python
import cv2
import numpy as np

# 부품 그림을 흑백으로 읽는다. 연습용으로 도형을 그려 만든 가상의 그림이다
gray = cv2.imread("ch08_parts.png", cv2.IMREAD_GRAYSCALE)
print("모양:", gray.shape, "/ 자료형:", gray.dtype)
print("가장 어두운 값:", int(gray.min()), "/ 가장 밝은 값:", int(gray.max()))

# 동전 한가운데(세로 60, 가로 60)와 작업대(세로 120, 가로 200)의 밝기를 견준다
print("동전 한가운데:", int(gray[60, 60]), "/ 작업대:", int(gray[120, 200]))
출력
모양: (240, 320) / 자료형: uint8
가장 어두운 값: 64 / 가장 밝은 값: 206
동전 한가운데: 78 / 작업대: 196

2단계. 임계값 127 로 이진화하기

Python
# cv2.threshold: 돌려주는 값은 (쓴 임계값, 결과 그림) 두 개
ret, binary = cv2.threshold(gray,                # src: 흑백 그림
                            127,                 # thresh: 임계값
                            255,                 # maxval: 127 보다 큰 칸에 넣을 값
                            cv2.THRESH_BINARY)   # type: 크면 255, 아니면 0
print("쓴 임계값:", ret)

# np.unique: 겹치지 않는 값만 모은다. 이진 그림이면 0 과 255 두 개뿐이다
print("결과에 있는 값:", [int(v) for v in np.unique(binary)])
# 흰 칸(작업대)과 검은 칸(부품)의 비율
print("흰 칸 비율:", round(float((binary == 255).mean()), 3))
print("검은 칸 비율:", round(float((binary == 0).mean()), 3))
출력
쓴 임계값: 127.0
결과에 있는 값: [0, 255]
흰 칸 비율: 0.825
검은 칸 비율: 0.175

3단계. 임계값을 바꿔 가며 검은 칸 비율 재기

Python
thresholds = [50, 100, 150, 200]
results = {}
for t in thresholds:
    _, out = cv2.threshold(gray, t, 255, cv2.THRESH_BINARY)
    results[t] = out
    # 검은 칸 = 임계값을 넘지 못해 부품으로 본 칸
    print(f"임계값 {t:3}: 검은 칸 비율 {round(float((out == 0).mean()), 3)}")
출력
임계값  50: 검은 칸 비율 0.0
임계값 100: 검은 칸 비율 0.167
임계값 150: 검은 칸 비율 0.177
임계값 200: 검은 칸 비율 0.906

4단계. 결과를 격자로 견주기

Python
import matplotlib.pyplot as plt

# 그래프의 한글이 깨지지 않게 맑은 고딕을 쓰고, 빼기 기호가 네모로 나오지 않게 한다
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

fig, axes = plt.subplots(2, 3, figsize=(10, 5.6))
axes[0, 0].imshow(gray, cmap="gray", vmin=0, vmax=255)
axes[0, 0].set_title("원본")
# 다섯째 칸에는 히스토그램과 임계값 네 개를 세로선으로 그린다
hist = cv2.calcHist([gray], [0], None, [32], [0, 256]).ravel()
axes[1, 2].bar(np.arange(32) * 8, hist, width=8, align="edge")
for t in thresholds:
    # axvline: 가로 위치 t 에 세로선(vertical line)을 긋는다
    axes[1, 2].axvline(t, color="black", linestyle="--")
axes[1, 2].set_title("히스토그램과 임계값")
# 나머지 네 칸에 임계값별 결과를 놓는다
for ax, t in zip([axes[0, 1], axes[0, 2], axes[1, 0], axes[1, 1]], thresholds):
    ax.imshow(results[t], cmap="gray", vmin=0, vmax=255)
    ax.set_title(f"임계값 {t}")
for ax in [axes[0, 0], axes[0, 1], axes[0, 2], axes[1, 0], axes[1, 1]]:
    ax.axis("off")
plt.tight_layout()
plt.show()
부품 그림의 원본, 임계값 50·100·150·200 의 결과, 히스토그램과 임계값 세로선을 놓은 2행 3열 결과

코드 설명

  • cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) 는 (쓴 임계값, 결과) 두 값을 돌려줍니다. 직접 127 을 주었으므로 첫 값은 127.0 이고, 결과에는 0 과 255 두 값만 있습니다.
  • 임계값 127 에서 흰 칸(작업대)이 0.825, 검은 칸(부품)이 0.175 입니다. 동전 한가운데(78)는 127 보다 작아 검정, 작업대(196)는 커서 흰색입니다.
  • 임계값 50 은 가장 어두운 값(64)보다 작아 검은 칸이 0.0, 곧 모두 흰색입니다. 100 과 150 은 0.167 과 0.177 로 비슷하고, 200 은 작업대 봉우리(184~206) 안이라 0.906 으로 작업대 대부분이 검게 칠해집니다.
  • 히스토그램의 점선 네 개를 보면 100 과 150 은 두 봉우리 사이 골짜기 쪽에, 50 은 부품 봉우리 왼쪽 바깥에, 200 은 작업대 봉우리 한가운데에 있습니다. 100 에서 동전 하나 안에 흰 고리가 보이는 것은 그 동전(밝기 92)은 100 보다 어둡지만 테두리 무늬(106)는 100 보다 밝기 때문입니다.
← 이전 절8장 · 실습 지도다음 절 →8장 · 실습 2 — 다섯 가지 임계값 방식 견주기