8장. Threshold, Adaptive Threshold, Otsu
Adaptive Threshold
학교가 반마다 커트라인을 따로 정한다고 해 봅시다. 시험이 어려웠던 반은 커트라인을 낮추고, 쉬웠던 반은 높입니다. 그러면 반마다 "그 반에서 잘한 학생"을 고를 수 있습니다. Adaptive Threshold 는 칸마다 주변 동네(블록)의 평균 밝기를 보고 커트라인을 따로 정합니다. 그늘진 동네는 그늘진 동네끼리 견주는 셈입니다.
그림의 네 단계를 칸 하나하나에 되풀이합니다. 지금 정할 칸을 고르고, 그 칸을 가운데에 둔 blockSize × blockSize 네모의 평균을 내고, 평균에서 C 를 빼 기준을 만들고, 칸의 밝기가 기준보다 크면 흰색입니다. 여기서 반 하나가 블록이고, 반마다의 커트라인이 "주변 평균 − C"입니다.
가운데의 전역 임계값은 오른쪽 그늘을 검게 칠했지만, 오른쪽 Adaptive 결과(blockSize 15, C 10)는 그늘 속 글자까지 또렷합니다. 칸마다 기준이 주변 종이의 밝기를 따라 내려가기 때문입니다.
인자 여섯 개
| 순서 | 이름 | 보통 값 | 뜻 |
|---|---|---|---|
| 1 | src | gray | 흑백 그림(uint8) |
| 2 | maxValue | 255 | 기준을 넘은 칸에 넣을 값 |
| 3 | adaptiveMethod | cv2.ADAPTIVE_THRESH_GAUSSIAN_C | 주변 평균을 내는 법(MEAN_C 또는 GAUSSIAN_C) |
| 4 | thresholdType | cv2.THRESH_BINARY | BINARY 나 BINARY_INV 둘 가운데 하나 |
| 5 | blockSize | 15 | 둘러볼 네모의 한 변. 1 보다 큰 홀수 |
| 6 | C | 10 | 주변 평균에서 뺄 값 |
cv2.threshold 와 달리 결과 그림 하나만 돌려줍니다. 칸마다 기준이 다르니 "쓴 임계값" 하나를 돌려줄 수 없기 때문입니다.
손으로 계산해 보기
blockSize 3, C 10, ADAPTIVE_THRESH_MEAN_C 로 가운데 칸 하나를 정해 봅니다. 밝은 곳의 글자 칸, 그늘 속 글자 칸, 그늘 속 종이 칸 세 경우입니다.
| 경우 | 주변 3×3 의 밝기 | 평균 | 기준(평균 − 10) | 가운데 칸 | 결과 |
|---|---|---|---|---|---|
| 밝은 곳의 글자 | 180, 190, 200, 185, 120, 195, 190, 200, 180 | 1640 ÷ 9 = 182.2 | 172.2 | 120 | 기준보다 작다 → 0 |
| 그늘 속 글자 | 60, 70, 65, 62, 30, 68, 64, 66, 70 | 555 ÷ 9 = 61.7 | 51.7 | 30 | 기준보다 작다 → 0 |
| 그늘 속 종이 | 60, 70, 65, 62, 66, 68, 64, 66, 70 | 591 ÷ 9 = 65.7 | 55.7 | 66 | 기준보다 크다 → 255 |
그늘 속 종이(66)는 전역 임계값 127 이면 검정이 되지만, 주변 평균에 맞춘 기준(55.7)보다는 커서 흰색이 됩니다.
짧은 예시
import cv2
import numpy as np
# 위 표의 둘째, 셋째 경우를 3×3 그림으로 만든다
text = np.array([[60, 70, 65], [62, 30, 68], [64, 66, 70]], dtype=np.uint8)
paper = np.array([[60, 70, 65], [62, 66, 68], [64, 66, 70]], dtype=np.uint8)
for name, block in [("그늘 속 글자", text), ("그늘 속 종이", paper)]:
out = cv2.adaptiveThreshold(block, # src
255, # maxValue
cv2.ADAPTIVE_THRESH_MEAN_C, # 주변을 똑같이 더해 나눈 평균
cv2.THRESH_BINARY, # 기준보다 크면 255
3, # blockSize: 3×3 네모
10) # C: 평균에서 10 을 뺀다
print(name, "가운데 칸:", int(out[1, 1]))
# 결과: 그늘 속 글자 0, 그늘 속 종이 255가장자리 칸은 그림 밖의 이웃이 없어서 OpenCV 가 안쪽 칸을 거울처럼 비춰 채운 뒤 평균을 냅니다. 그래서 여기서는 이웃 여덟 칸이 모두 있는 가운데 칸만 읽습니다.
MEAN_C 와 GAUSSIAN_C
왼쪽 초록 상자의 MEAN_C 는 블록 안의 칸을 모두 똑같이 더해 나눕니다. 오른쪽 다홍 상자의 GAUSSIAN_C 는 가운데에 가까운 칸일수록 무게를 더 줍니다. 반 평균을 낼 때 모든 학생을 똑같이 세는 것이 MEAN_C, 내 짝꿍과 앞뒤 자리 학생을 더 크게 세는 것이 GAUSSIAN_C 입니다.
| 견줄 것 | ADAPTIVE_THRESH_MEAN_C | ADAPTIVE_THRESH_GAUSSIAN_C |
|---|---|---|
| 평균 내는 법 | 블록 안의 칸을 똑같이 | 가운데에 가까울수록 무게를 더 |
| 결과 | 블록 경계에서 조금 거칠 수 있다 | 조금 더 매끈하다 |
| 속도 | 조금 빠르다 | 조금 느리다 |
| 고르는 법 | 먼저 둘 다 해 보고 결과가 좋은 쪽을 쓴다 | 같다 |
주의할 점
- thresholdType 에는
THRESH_BINARY와THRESH_BINARY_INV만 줄 수 있습니다. TRUNC 나 TOZERO 를 주면 오류가 납니다. - 그늘이 없는 고른 그림에서도 Adaptive 를 쓸 수 있지만, 넓은 빈 바탕에 자잘한 점이 생길 수 있습니다. 다음 절의 C 로 줄입니다.