이미지 이진화(Binarization)
이미지 이진화(Binarization)는 이미지 분리(Image Segmentation)를 하는 가장 간단한 방법으로 이미지 내의 물체와 배경을 0과 1, 또는 그 반대로, 두 값만으로 픽셀값을 재설정하는 것이다. 이는 3채널의 RGB값을 가진 이미지가 아닌 1채널을 가지고 있는 이진화된 이미지 데이터에만 적용 가능하다. 즉, 픽셀 값을 0~255까지 가진 흑백 사진으로 변경하는 것이다. 이 이진화 작업을 수행한 후 Thresholding을 통해 임계값(Thresh) 기준 이상의 값을 255, 미만의 값은 0으로 바꾸는 것이 가능하다.
Thresholding
우선 openCV 라이브러리를 사용한다면 다음의 함수를 통해 간단하게 Thresholding을 수행할 수 있다.
cv2.threshold(src, thresh, maxval, type)
- src : gray scale의 이미지 데이터
- thresh : 임계값
- maxval: 임계값 이상이 되면 사용되는 값
- type : 임계값 함수 동작 지정
이 type에는 Simple Image Thresholding에 해당하는 flag들을 사용할 수 있다. 아래는 type에 들어갈 수 있는 여러 flag들이다.
- cv2.THRESH_BINARY: 임계값보다 크면 value이고 아니면 0으로 바꿈
- cv2.THRESH_BINARY_INV: 임계값보다 크면 0이고 아니면 value로 바꿈
- cv2.THRESH_TRUNC: 임계값보다 크면 value로 지정하고 작으면 기존의 값 그대로 사용
- cv2.THRESH_TOZERO: 임계값보다 크면 픽셀 값 그대로 작으면 0으로 할당
- cv2.THRESH_TOZERO_INV: 임계값보다 크면 0으로 작으면 그대로 할당
아래는 Simple Image Thresholding을 수행 시 임계값을 자동으로 결정해주는 flag들인데, 이 flag들도 해당 함수의 Thresholding flag들과 함께 사용할 수 있다.
- cv2.THRESH_OTSU: otsu 알고리즘으로 임계값 결정
- cv2.THRESH_TRIANGLE: 삼각 알고리즘으로 임계값 결정
이제 Simple Image Thresholding을 실험해본다. 참고로 numpy를 이용해 이진화도 가능하지만 openCV에선 불러올 때 변환하여 이미지를 load할 수 있다.
import cv2
img = cv2.imread("brokenEgg.jpeg", cv2.IMREAD_GRAYSCALE)
ret, th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
ret, th2 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
ret, th3 = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
ret, th4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
ret, th5 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)
cv2.imshow("ORIGIN", img)
cv2.imshow("BINARY", th1)
cv2.imshow("BINARY_INV", th2)
cv2.imshow("TRUNC", th3)
cv2.imshow("TOZERO", th4)
cv2.imshow("TOZERO_INV", th5)
cv2.waitKey(0)
Otus 알고리즘
위 flag 중 자주 사용되는 Otus 알고리즘은 thresh 값을 사용자 임의로 정하는 것이 아닌 알고리즘 내에서 자동으로 정해지도록 하는 방법이다. 이 알고리즘의 기본적인 전제는 이미지가 쌍봉 분포(bimodal distribution)라는 것이다. 즉, 이미지 픽셀 값들의 히스토그램은 서로 다른 두 개의 픽셀 값에서 최댓값이 나타는 double peak 분포를 가진다는 것이다.
왼쪽 클래스의 피크는 배경 값, 오른쪽 클래스의 피크는 물체의 값이다. 이 두 분포를 가장 잘 나누는 픽셀 값을 찾는 것이 오츠 알고리즘의 핵심이다. 이에 따라 다음의 수식을 세울 수 있다.
이는 전체 분포의 분산 \( σ_T \)는 두 클래스 내의(within-class) 분산(\(σ_w\))과 두 클래스 사이의(between-class) 분산(\(σ_b\))으로 이루어져 있다는 것을 뜻한다.
$$ σ^2_w(t)=w_1(t)σ^2_1()+w_2(t)σ^2_2(t) $$
두 클래스 사이의 분산 (between-class variance)은 다음과 같이 정의된다.
$$ σ^2_b(t)=w_1(t)w_2(t)[μ_1(t)-μ_2(t)]^2 $$
이때, 두 클래스의 경곗값은 \(σ_w\)을 최소화 시킴과 동시에 \(σ_b\)를 최대화시키는 값으로 정해진다. 따라서 두 클래스 분포 중 하나를 선택해서 두 클래스를 가장 잘 구분하는 최적의 임계값을 찾아낸다. 주로 두 번째 Between-class 분포를 이용한다. 즉, 두 개의 봉우리 사이의 값을 선택하여 분산을 최소로 만들어 더 효율적인 이진화를 수행한다.
import cv2
img = cv2.imread("brokenEgg.jpeg", cv2.IMREAD_GRAYSCALE)
otsu_tresh, th = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print(otsu_tresh)
cv2.imshow('otsu', th)
cv2.waitKey(0)
# 135.0
위 오츠의 이진화를 통해 찾은 임계값은 135.0이 된다. 따라서 위 Binary Thresholding은 135를 임계값으로 두고 수행된 것이다.
Adaptive Thresholding
이번엔 Adaptive Thresholding이다. 이는 앞서 설명한 Simple Image Thresholding과 다르게 지역별로 임계값을 구해줘서 적용시키는 방법이다. 개발자가 정해준 임의의 값이 아닌 bookSize*bookSize에서 구할 수 있기 때문에 더 유연하게 threshold를 적용할 수 있다. 함수는 아래와 같다.
cv2.adaptiveThreshold(src, maxval, adaptiveMethod, thresholdType, C)
- src : gray scale의 이미지 데이터
- maxval : 임계값 이상이 되면 사용되는 값
- adaptiveMethod : threshold를 구하기 위해 사용되는 알고리즘
- thresholdType(blockSize) : 임계 처리를 적용할 영역 사이즈 (적용될 픽셀이 블럭의 중심이 되어 홀수 값을 가짐)
- C: 보정 상수로 이 값이 양수이면 계산된 adaptive 임계값에서 빼고 음수면 더함
adaptiveMethod의 종류는 아래와 같다.
- cv2.ADAPTIVE_THRESH_MEAN_C : 적용할 픽셀을 중심으로 하는 영역 안에 있는 픽셀 값의 평균에서 보정값인 C를 뺀 값을 임계값으로 설정
- cv2.ADAPTIVE_THRESH_GAUSSIAN_C = 적용할 픽셀을 중심으로 하는 영역 안에 있는 Gaussian 윈도우 기반 가중치들의 합에서 보정값인 C를 뺀 값을 임계값으로 설정
아래는 python과 openCV로 구현한 Adaptive Thresholding이다.
import cv2
img = cv2.imread("brokenEgg.jpeg", cv2.IMREAD_GRAYSCALE)
mean = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 0)
gaussian = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 0)
cv2.imshow("MEAN_C", mean)
cv2.imshow("GAUSSIAN_C", gaussian)
cv2.waitKey(0)
참고 자료
https://hoony-gunputer.tistory.com/entry/opencv-python-%EC%9D%B4%EB%AF%B8%EC%A7%80-Thresholding
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=pk3152&logNo=221443089316