이진 이미지와 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("픽셀 수")

전역 문턱값 처리
전역 문턱값 처리(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]
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의 결과. 값이 지나치게 낮거나 높으면 글자와 배경의 세부 정보가 함께 소실.

문턱값 처리 방식
같은 문턱값도 처리 방식에 따라 다른 결과.
| 방식 | 문턱값보다 큰 픽셀 | 나머지 픽셀 |
|---|---|---|
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)

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가 너무 작으면 작은 밝기 변화와 잡음에 민감. 너무 크면 지역 조명 차이를 충분히 반영하지 못할 수 있음. 이미지의 글자나 물체 크기에 맞춘 조정 필요.