이진 이미지와 Thresholding
실습 준비
import cv2 as cv
import matplotlib.pyplot as plt
import numpy as np
from yeom import to_pil
이미지 불러오기
newspaper = cv.imread("newspaper.png", cv.IMREAD_GRAYSCALE)
newspaper.shape, newspaper.dtype
((512, 419), dtype('uint8'))

이진 이미지
이진 이미지(binary image)는 픽셀값이 두 종류뿐인 이미지. OpenCV에서는 일반적으로 uint8 자료형의 0과 255를 사용.
| 픽셀값 | 일반적인 의미 |
|---|---|
| 0 | 배경 또는 선택하지 않은 영역 |
| 255 | 전경 또는 선택한 영역 |
마스크 역시 이진 이미지. 색상 범위뿐 아니라 그레이스케일 밝기를 기준으로도 생성 가능.
밝기 분포와 문턱값
그레이스케일 히스토그램은 밝기별 픽셀 수를 표현. 배경과 글자의 밝기 분포가 나뉘면 두 집단 사이의 값을 문턱값(threshold)으로 선택 가능.
hist = cv.calcHist([newspaper], [0], None, [256], [0, 256])
plt.plot(hist)
plt.xlim([0, 256])
plt.xlabel("픽셀 값")
plt.ylabel("픽셀 수")
plt.show()

전역 문턱값 처리
전역 문턱값 처리(global thresholding)는 이미지 전체에 하나의 문턱값을 적용. THRESH_BINARY는 문턱값보다 큰 픽셀을 최댓값으로, 나머지를 0으로 변경.
used_threshold, binary = cv.threshold(
newspaper,
190, # 글자와 배경을 나눌 문턱값
255, # 문턱값보다 밝은 픽셀에 넣을 값
cv.THRESH_BINARY,
)
used_threshold # 실제 사용한 문턱값
190.0
np.unique(binary) # 결과 이미지의 픽셀값 종류
array([ 0, 255], dtype=uint8)
문턱값 190.0 적용 결과는 픽셀값이 0과 255뿐인 이진 이미지.
to_pil(binary)

cv.threshold()의 반환값은 실제 사용한 문턱값과 결과 이미지. 문턱값을 직접 지정한 경우 첫 번째 반환값은 입력값과 동일.
문턱값 비교
문턱값을 낮추면 흰색 픽셀이 증가하고, 높이면 검정 픽셀이 증가.
threshold_values = [80, 120, 150, 190, 210, 230] # 낮은 값부터 매우 높은 값까지 비교
binary_images = [
cv.threshold(newspaper, value, 255, cv.THRESH_BINARY)[1]
for value in threshold_values
]
comparison = cv.hconcat(binary_images)
to_pil(comparison)
왼쪽부터 문턱값 80, 120, 150, 190, 210, 230의 결과. 값이 지나치게 낮거나 높으면 글자와 배경의 세부 정보가 함께 소실.

문턱값 처리 방식
같은 문턱값도 처리 방식에 따라 다른 결과.
| 방식 | 문턱값보다 큰 픽셀 | 나머지 픽셀 |
|---|---|---|
THRESH_BINARY |
최댓값 | 0 |
THRESH_BINARY_INV |
0 | 최댓값 |
THRESH_TRUNC |
문턱값 | 원래 값 |
THRESH_TOZERO |
원래 값 | 0 |
THRESH_TOZERO_INV |
0 | 원래 값 |
THRESH_BINARY_INV는 THRESH_BINARY의 흑백을 반전. 이 신문 이미지에서는 어두운 글자가 흰색, 밝은 배경이 검정으로 변환.
_, binary_inverse = cv.threshold(
newspaper,
190, # THRESH_BINARY와 같은 문턱값으로 비교
255, # 문턱값 이하인 픽셀에 넣을 값
cv.THRESH_BINARY_INV,
)
to_pil(binary_inverse)

Otsu 문턱값 처리
문턱값을 직접 정하기 어려우면 Otsu 문턱값 처리(Otsu thresholding)로 자동 선택 가능. Otsu 알고리즘은 픽셀을 두 집단으로 나눌 때 집단 내 분산의 가중합을 최소화하는 문턱값을 선택.
THRESH_OTSU는 입력한 문턱값 대신 자동 계산값을 사용. 문턱값 인자에는 관례적으로 0을 전달하고, 실제 선택값은 첫 번째 반환값으로 확인.
otsu_threshold, binary_otsu = cv.threshold(
newspaper,
0, # Otsu가 자동으로 대체할 자리표시자
255, # 문턱값보다 밝은 픽셀에 넣을 값
cv.THRESH_BINARY | cv.THRESH_OTSU,
)
otsu_threshold # Otsu가 선택한 문턱값
169.0
신문 이미지에서 Otsu가 선택한 문턱값은 169.0.
to_pil(binary_otsu)

전역 문턱값의 한계
위치마다 조명이 다르면 같은 물체도 서로 다른 밝기. 이미지 전체에 하나의 문턱값을 적용하면 밝은 영역이나 어두운 영역 한쪽이 소실될 수 있음.
sudoku = cv.imread("sudoku.png", cv.IMREAD_GRAYSCALE)
to_pil(sudoku)

global_threshold, global_binary = cv.threshold(
sudoku,
0, # Otsu가 자동으로 대체할 자리표시자
255, # 문턱값보다 밝은 픽셀에 넣을 값
cv.THRESH_BINARY | cv.THRESH_OTSU,
)
global_threshold # Otsu가 선택한 전역 문턱값
96.0
Otsu가 선택한 문턱값은 96.0. 위치별 조명 차이로 일부 숫자와 격자가 소실.
to_pil(global_binary)


적응형 문턱값 처리
적응형 문턱값 처리(adaptive thresholding)는 각 픽셀 주변 영역에서 지역 문턱값을 계산. 조명이 고르지 않은 문서나 사진에 유용.
block_size = 15 # 숫자 획 주변의 조명 변화를 반영할 이웃 크기
c = 5 # 완만하게 어두워진 배경도 흰색으로 유지할 보정값
adaptive_mean = cv.adaptiveThreshold(
sudoku,
255, # 지역 문턱값보다 밝은 픽셀에 넣을 값
cv.ADAPTIVE_THRESH_MEAN_C,
cv.THRESH_BINARY,
block_size,
c,
)
to_pil(adaptive_mean)
block_size: 문턱값 계산에 사용할 이웃 영역 크기. 1보다 큰 홀수c: 이웃 영역에서 계산한 값에서 뺄 상수ADAPTIVE_THRESH_MEAN_C: 이웃 픽셀의 산술 평균

ADAPTIVE_THRESH_GAUSSIAN_C: 가까운 픽셀에 더 큰 가중치를 둔 평균
adaptive_gaussian = cv.adaptiveThreshold(
sudoku,
255, # 지역 문턱값보다 밝은 픽셀에 넣을 값
cv.ADAPTIVE_THRESH_GAUSSIAN_C,
cv.THRESH_BINARY,
block_size,
c,
)
to_pil(adaptive_gaussian)

block_size가 너무 작으면 작은 밝기 변화와 잡음에 민감. 너무 크면 지역 조명 차이를 충분히 반영하지 못할 수 있음. 이미지의 글자나 물체 크기에 맞춘 조정 필요.
퀴즈
이진 이미지에 관한 설명으로 맞는 것을 모두 고르세요.
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.