< 4-2. Image Thresholding >
이번에는, 간단한 thresholding, 적응성 있는 thresholding, Otsu’s의 thresholding에 대해서 배워볼 것 이다.
- cv2.threshold, cv2.adaptiveThreshold등에 대해서 볼 것 이다.
Simple Thresholding
만약에 픽셀 값이 임계값보다 크면 하나의 값(흰색일수도)이 할당되고, 그렇지 않으면 다른 값(검정색일수도)이 할당된다. 사용된 함수는 cv2.threshold이다. 첫 번째 argument는 그레이 스케일 이미지여야하는 소스 이미지이다. 두 번째 argument는 픽셀 값을 분류하는 데 사용되는 임계값이다. 세 번째 argument는 픽셀 값이 임계값보다 클 경우(혹은 그 보다 작을 경우) 주어진 값을 나타내는 maxVal이다. OpenCV는 서로 다른 유형의 임계값을 제공하며 기능의 네 번째 매개변수에 의해 결정된다. 다른 유형들은 다음과 같다.
- cv2.THRESH_BINARY
- cv2.THRESH_BINARY_INV
- cv2.THRESH_TRUNC
- cv2.THRESH_TOZERO
- cv2.THRESH_TOZERO_INV
문서에는 각 유형이 무엇을 의미하는지 명확하게 설명되어 있다. 설명서를 참조하자!
두 개의 출력을 얻는다. 첫 번째는 나중에 설명될 retval이다. 두 번째 결과는 threshold(임계) 이미지이다.
img = cv2.imread('golden.jpg',0)
ret, thresh1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
ret, thresh2 = cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV)
ret, thresh3 = cv2.threshold(img,127,255,cv2.THRESH_TRUNC)
ret, thresh4 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO)
ret, thresh5 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img,thresh1,thresh2,thresh3,thresh4,thresh5]
plt.figure(figsize=(12,8))
for i in range(6):
plt.subplot(2,3,i+1),
plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.axis('off')
plt.show()
INV는 각 버전의 inverse된 버전이다.
Adaptive Thresholding
이전 섹션에서, 글로벌 값을 임계값으로 사용했다. 그러나 이미지가 다른 영역에서 다른 조명 조건을 갖는 모든 조건에서는 좋지 않을 수 있다. 그 경우, 우리는 적응적인 임계값을 택한다. 이 경우 알고리즘은 이미지의 작은 영역에 대한 임계값을 계산한다. 그래서 우리는 동일한 이미지의 다른 영역에 대해 서로 다른 임계값을 얻었고, 그것은 다양한 밝기를 가진 영상에 대해 더 나은 결과를 제공한다.
이는 특별한 3개의 입력 매개 변수와 단 하나의 출력 인수를 가지고 있다.
Adaptive Method - 임계갑 계산 방법을 결정한다.
- cv2.ADAPTIVE_THRESH_MEAN_C : 임계값은 인접 영역의 평균이다.
- cv2.ADAPTIVE_THRESH_GAUSSIAN_C : 임계값은 가중치가 가우시안 창인 인접 값의 가중치 합이다.
Block Size : 인접 영역의 크기를 결정한다.
C : 계산된 평균 또는 가중 평균에서 빼는 상수일 뿐이다.
아래 코드는 다양한 밝기가 있는 다양한 이미지의 전역 임계값과 적응 임계값을 비교한다.
img = cv2.imread('golden.jpg',0)
img = cv2.medianBlur(img,5)
ret, th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
th2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY,11,2)
th3 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,11,2)
titles = ['Original','Global Thresholding(v=127)','Adaptive Mean Thresholding','Adaptive Gaussian Thresholding']
images = [img,th1,th2,th3]
plt.figure(figsize=(12,8))
for i in range(4):
plt.subplot(2,2,i+1)
plt.title(titles[i])
plt.imshow(images[i],'gray')
plt.axis('off')
plt.show()
Adaptive Thresholding은 각 라인만 따온 것을 볼 수 있다. Gaussian이 조금 더 선명하다.
Otsu’s Binarization
맨 처음에 retVal을 추후에 설명한다는 것을 기억할 것이다. 이는 Otsu’s Binarization에서 사용한다. 무엇일까?
전역 임계값에서는, 임의의 임계값을 사용하였다. 그렇다면, 우리가 선택한 가치가 좋은지 아닌지 어떻게 알 수 있을까? 답은 여러번 반복하는 시행착오이다. 그러나 이항 이미지를 고려한다.(단순하게 말하면, 이항 이미지는 히스토그램이 두 개의 피크를 갖는 이미지이다. ) 그 이미지를 위해, 우리는 대략 그 피크들 중간에 있는 값을 임계값으로 받아들일 수 있다. 그것이 바로 Otsu binarization이 하는 일이다. 간단한 말로, 이는 2차원 이미지에 대한 이미지 히스토그램에서 임계값을 자동으로 계산한다. (이미지가 이항이 아닐 경우, binarization은 정확하지 않다.)
이를 위해, cv2.threshold() 함수가 사용되지만, 추가 flag인 cv2.THRESH_OTSU를 전달한다. 임계값에 대해서는 0을 넘겨주면 된다. 그러면 알고리즘이 최적의 임계값을 찾아 두 번째 출력인 retVal로 반환한다. Otsu 임계값을 사용하지 않으면 retVal은 사용한 임계값과 동일하다.
아래 예제를 확인하자. 입력 이미지는 노이즈가 많은 이미지다. 첫 번째의 경우, 127의 값에 전역 임계값을 적용했다. 두 번째의 경우, Otsu의 임계값을 직접 적용했다. 세 번째의 경우, 노이즈를 제거하기 위해 5x5 가우시안 커널로 이미지를 필터링한 다음 Otsu 임계값을 적용했다. 노이즈 필터링이 어떻게 결과를 개선하는지 보자.
img = cv2.imread('circle.jpg',0)
# Global Thresholding
ret1, th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
# Otsu's Thresholding
ret2, th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# Otsu's Thresholding after Gaussian filtering
blur = cv2.GaussianBlur(img,(5,5),0)
ret3, th3 = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# 이미지랑 히스토그램 그리기
images = [img,0,th1,
img,0,th2,
blur,0,th3]
titles = ['Original Noisy Image', 'Histogram', 'Global Thresholding(v=127)',
'Original Noisy Image','Histogram',"Otsu's Thresholding",
"Gaussian filtered Image","Histogram","Otsu's Thresholding"]
plt.figure(figsize=(12,8))
for i in range(3):
plt.subplot(3,3,i*3+1), plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.axis('off')
# ravel()은 array를 하나의 []에 담아준다. 펼친다고 보면 된다.
plt.subplot(3,3,i*3+2), plt.hist(images[i*3].ravel(),256)
plt.title(titles[i*3+1]), plt.axis('off')
plt.subplot(3,3,i*3+3), plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.axis('off')
plt.show()
Gaussian blur를 추가한 이미지에 Otsu thresholding을 했을 때 이미지가 제일 깔끔해 지는 것을 볼 수 있다. 히스토 그램도 딱 두 개로 떨어지는 것을 볼 수 있다.
How Otsu's Binarization Works?
이번에는 Otsu’s binarization의 구현이 어떻게 이루어지는지 증명해볼 것이다. 흥미가 없다면 넘겨도된다 :)
이항 이미지에 대해서 작업을 해왔기에, Otsu’s 알고리즘은 관계에 의해 주어진 가중된 클래스 내 분산(weighted within-class variance)을 최소화하는 임계값(t)을 찾으려고 한다.
이는 실제로 두 클래스에 대한 분산이 최소가 되도록 두 피크 사이에 있는 t값을 찾는다. 다음과 같이 구현할 수 있다.
img = cv2.imread('circle.jpg',0)
blur = cv2.GaussianBlur(img,(5,5),0)
# 정규화된 히스토그램을 찾고, 누적분포함수에 넣는다.
## (img, channel, mask, histSize, range) 순서
hist = cv2.calcHist([blur],[0],None,[256],[0,256])
hist_norm = hist.ravel() / hist.max()
Q = hist_norm.cumsum()
bins = np.arange(256)
fn_min = np.inf
thresh = -1
for i in range(1,256):
## np.hsplit() : array를 복수의 하위 array로 수평적으로 분리한다(column-wise)
## [i]이기에 i개 만큼 앞에서부터 뽑고 나머지는 다른 array로 분리한다.
p1,p2 = np.hsplit(hist_norm,[i]) # 확률
q1,q2 = Q[i], Q[255]-Q[i] # 클래스의 누적
b1,b2 = np.hsplit(bins,[i]) # 가중치
# 평균과 분산 찾기
m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2
v1,v2 = np.sum(((b1-m1)**2)*p1)/q1, np.sum(((b2-m2)**2)*p2)/q2
# 최소화 함수를 계산하기
fn = q1*v1 + q2*v2
if fn < fn_min:
fn_min = fn
thresh = i
# Otsu's threshold 값을 OpenCV 함수로 찾기
ret, otsu = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
print(thresh), print(ret)
-> 129, 128.0
사진 출처 : https://j07051.tistory.com/364
위의 그래프와 계산식을 보면 쉽게 이해갈 것이다.