8장. Threshold, Adaptive Threshold, Otsu
Otsu 가 잘 안 맞는 경우와 블러 먼저 하기
흐린 날 물안경 없이 수영장 바닥을 보면 타일의 경계가 흐릿합니다. 그림에 자잘한 점(노이즈)이 많이 끼면 히스토그램의 두 봉우리가 넓게 퍼져 서로 섞이고, 골짜기가 메워집니다. 그러면 Otsu 가 고른 값이 어긋나지 않더라도, 바탕의 점들이 커트라인을 넘나들어 결과에 소금 뿌린 듯한 잡티가 생깁니다.
그림처럼 이진화 전에 가우시안 블러로 칸마다 주변과 섞어 점을 줄이면, 봉우리가 다시 좁아지고 결과가 깨끗해집니다. 여기서 흐린 물속 시야가 노이즈, 물안경이 블러입니다. 블러는 6장에서 배운 필터로, 여기서는 cv2.GaussianBlur 한 줄만 씁니다.
부품 그림에 씨앗 8 로 노이즈(표준편차 35)를 더했습니다. 아래 줄 가운데의 다홍 히스토그램은 두 봉우리가 퍼져 골짜기가 거의 없습니다. 바로 Otsu 하면(위 가운데) 고른 값은 144 로 원본(136)과 크게 다르지 않지만 작업대 위에 검은 점이 가득합니다. 7×7 가우시안 블러 뒤에 Otsu 하면(위 오른쪽) 봉우리가 다시 좁아지고 결과도 깨끗합니다.
Otsu 가 힘든 경우
| 경우 | 히스토그램 | 하는 일 |
|---|---|---|
| 노이즈가 많다 | 봉우리가 퍼져 골짜기가 메워진다 | 블러 먼저 |
| 조명이 고르지 않다 | 바탕이 넓게 퍼진다 | Adaptive Threshold |
| 물체가 아주 작다 | 물체 봉우리가 거의 안 보인다 | 관심 영역만 잘라서(13장) 하거나 직접 정하기 |
| 무리가 셋 이상 | 봉우리가 셋 이상이다 | 직접 정하거나 두 번 나누기 |
짧은 예시
import cv2
import numpy as np
gray = cv2.imread("ch08_parts.png", cv2.IMREAD_GRAYSCALE)
# 씨앗을 고정한 노이즈: 평균 0, 표준편차 35 의 정규분포 숫자를 칸마다 더한다
rng = np.random.default_rng(8)
noisy = np.clip(gray + rng.normal(0, 35, gray.shape), 0, 255).astype(np.uint8)
# cv2.GaussianBlur(그림, (가로, 세로 크기), 0): 6차시에서 배운 블러. 0 은 퍼짐을 크기에 맞춰 정하라는 뜻
blur = cv2.GaussianBlur(noisy, (7, 7), 0)
for name, im in [("바로", noisy), ("블러 뒤", blur)]:
ret, _ = cv2.threshold(im, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(name, "Otsu 가 고른 값:", int(ret))
# 결과: 바로 144, 블러 뒤 138np.random.default_rng(8) 은 씨앗 8 로 난수 생성기(random number generator)를 만듭니다. 씨앗이 같으면 몇 번을 돌려도 같은 노이즈가 나옵니다.
주의할 점
- 블러를 너무 세게 하면(크기 21 이상 등) 가는 글자나 작은 물체의 테두리까지 뭉개집니다. 3, 5, 7 처럼 작은 크기부터 해 봅니다.
- 블러는 Adaptive Threshold 앞에도 쓸 수 있습니다. 블러 뒤의 이진화에도 남은 잡티는 9장의 모폴로지로 지웁니다.