[Tutorial] Image Thresholding


Introduction

  • OpenCV와 Python으로 Image processing을 알아봅시다.
  • 이 글에서는 Image thresholding을 간단히 알아보고, 어떻게 응용되는지 Blob labeling예제를 통해 확인하겠습니다.

Import Libraries

import os
import sys
import math
from platform import python_version

import cv2
import matplotlib.pyplot as plt
import matplotlib
import numpy as np

print("Python version : ", python_version())
print("OpenCV version : ", cv2.__version__)
matplotlib.rcParams['figure.figsize'] = (4.0, 4.0)
Python version :  3.6.7
OpenCV version :  3.4.5

Data load

sample_image_path = '../image/'
sample_image = 'lena_gray.jpg'
img = cv2.imread(sample_image_path + sample_image, cv2.IMREAD_GRAYSCALE)

coin_image = 'coins.jpg'
mask = np.array([[0, 1, 0],[1, 1, 1], [0, 1, 0]], dtype=np.uint8)
coin_img = cv2.imread(sample_image_path + coin_image, cv2.IMREAD_GRAYSCALE)

ret, coin = cv2.threshold(coin_img, 240, 255, cv2.THRESH_BINARY_INV)
coin = cv2.dilate(coin, mask, iterations=1)
coin = cv2.erode(coin, mask, iterations=6)

Data description

  • 본 예제에서 사용할 데이터는 아래와 같습니다.
    • Lena : 지난 예제에서 사용한 Lena 입니다.
    • Coins : Blob labeling 예제에서 사용될 이미지 입니다.
      • blob labeling이라는 주제에 맞게 전처리가 된 이미지 입니다. (blob labeling에서 자세하게 설명합니다.)
plt.subplot(1, 2, 1)
plt.imshow(img, cmap='gray')
plt.title('Lena')
plt.subplot(1, 2, 2)
plt.imshow(coin, cmap='gray')
plt.title('Coins')
plt.show()

Binarization

  • Binarization(이진화)이란, grayscale의 이미지를 기준에 따라 0 또는 1의 값만을 갖도록 만드는 작업입니다.
    • 일반적으로 특정 픽셀 값을 기준으로 더 작은 값은 0으로, 더 큰 값은 1로 만듭니다.
    • 사용 목적에 따라 결과 값을 반전시킬 때도 있는데, 추후에 코드 예시로 알아보겠습니다.
  • 이진화 자체는 단순한 작업이나, 추후에 보다 발전된 알고리즘을 다루기 위한 기본이 됩니다.
def simple_img_binarization_npy(img, threshold):
    w, h = img.shape
    b_img = np.zeros([w, h])
    b_img[img > threshold] = 1
    return b_img
plt.figure(figsize=(8, 8))
b_img1 = simple_img_binarization_npy(img, 200)
b_img2 = simple_img_binarization_npy(img, 150)
b_img3 = simple_img_binarization_npy(img, 100)

plt.subplot(2, 2, 1)
plt.imshow(img, cmap='gray')
plt.title('Gray Lena')

plt.subplot(2, 2, 2)
plt.imshow(b_img1, cmap='gray')
plt.title('Lena over 200')

plt.subplot(2, 2, 3)
plt.imshow(b_img2, cmap='gray')
plt.title('Lena over 150')

plt.subplot(2, 2, 4)
plt.imshow(b_img3, cmap='gray')
plt.title('Lena over 100')

plt.suptitle('Lena with different threshold value (numpy)', size=15)
plt.show()

  • 위에서 numpy를 이용했다면, 이번엔 OpenCV 내장 함수를 이용하여 image threshold를 해보겠습니다.
  • OpenCV 함수를 이용하면 numpy로 작성하는 것 보다 편리하게 다양한 결과물을 만들어낼 수 있습니다.
    • cv2.threshold() 함수에 전달하는 인자를 통해 threshold 알고리즘을 다양하게 변경할 수 있습니다.
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)

plt.figure(figsize=(8, 12))
plt.subplot(3, 2, 1)
plt.imshow(thresh1, cmap='gray')
plt.title('threshold lena')

plt.subplot(3, 2, 2)
plt.imshow(thresh2, cmap='gray')
plt.title('inversed threshold lena')

plt.subplot(3, 2, 3)
plt.imshow(thresh3, cmap='gray')
plt.title('truncated threshold lena')

plt.subplot(3, 2, 4)
plt.imshow(thresh4, cmap='gray')
plt.title('zero threshold lena')

plt.subplot(3, 2, 5)
plt.imshow(thresh5, cmap='gray')
plt.title('inversed zero threshold lena')

plt.suptitle('Lena with different threshold algorithm(OpenCV)', size=15)
plt.show()

Otsu Algorithm

  • Otsu algorithm(오츄 알고리즘)은 특정 threshold값을 기준으로 영상을 둘로 나눴을때, 두 영역의 명암 분포를 가장 균일하게 할 때 결과가 가장 좋을 것이다는 가정 하에 만들어진 알고리즘입니다.
    • 여기서 균일함 이란, 두 영역 각각의 픽셀값의 분산을 의미하며, 그 차이가 가장 적게 하는 threshold 값이 오츄 알고리즘이 찾고자 하는 값입니다.
  • 위에 기술한 목적에 따라, 알고리즘에서는 특정 Threshold $T$를 기준으로 영상을 분할하였을 때, 양쪽 영상의 분산의 weighted sum이 가장 작게 하는 $T$값을 반복적으로 계산해가며 찾습니다.

    • weight는 각 영역의 넓이로 정합니다.
    • 어떤 연산을 어떻게 반복하는지에 대한 내용이 아래 수식에 자세히 나와있습니다.
      $$ \begin{align} & T = argmin_{t\subseteq {1,\cdots,L-1}} v_{within}(t) \\
      & v_{within}(t) = w_{0}(t)v_{0}(t) + w_{1}(t)v_{1}(t) \\
      \\
      & w_{0}(t) = \Sigma_{i=0}^{t} \hat h(i),\hspace{2cm} && w_{1}(t) = \Sigma_{i=t+1}^{L-1} \hat h(i)\\
      & \mu_{0}(t)=\frac{1}{w_{0}(t)}\Sigma_{i=0}^{t}i\hat h(i) && \mu_{1}(t)=\frac{1}{w_{1}(t)}\Sigma_{i=t+1}^{L-1}i\hat h(i)\\
      & v_{0}(t) = \frac{1}{w_{0}(t)}\Sigma_{i=0}^{t}i\hat h(i)(i-\mu_{0}(t))^2 && v_{1}(t) = \frac{1}{w_{1}(t)}\Sigma_{i=t+1}^{L-1}i\hat h(i)(i-\mu_{1}(t))^2\\
      \end{align} $$
  • $w_{0}(t), w_{1}(t)$는 threshold 값으로 결정된 흑색 영역과 백색 영역의 크기를 각각 나타냅니다.

  • $v_{0}(t), v_{1}(t)$은 두 영역의 분산을 뜻합니다.

  • 위 수식을 그대로 적용하면 시간복잡도가 $\Theta(L^{2})$이므로 실제로 사용하기 매우 어려워집니다.

  • 그러나, $\mu$와 $v$가 영상에 대해 한번만 계산하고 나면 상수처럼 취급된다는 사실에 착안하여 다음 알고리즘이 완성되었습니다.
    $$ \begin{align} &T = argmax_{t\subseteq{0,1,\cdots,L-1}}v_{between}(t)\\
    &v_{between}(t)=w_{0}(t)(1-w_{0}(t))(\mu_{0}(t)-\mu_{1}(t))^2\\
    &\mu = \Sigma_{i=0}^{L-1}i\hat h(i)\\
    \\
    &\text{초깃값}(t=0):w_{0}(0)=\hat h(0),\ \mu_{0}(0)=0\\
    &\text{순환식}(t>0):\\
    & \hspace{1cm} w_{0}(t)=w_{0}(t-1)+\hat h(t)\\
    & \hspace{1cm} \mu_{0}(t)=\frac{w_{0}(t-1)\mu_{0}(t-1)+t\hat h(t)}{w_{0}(t)}\\
    & \hspace{1cm} \mu_{1}(t)=\frac{\mu-w_{0}(t)\mu_{0}(t)}{1-w_{0}(t)}\\
    \end{align} $$

  • 위 순환식을 $t$에 대하여 수행하여 가장 큰$v_{between}$를 갖도록 하는 $t$를 최종 threshold $T$로 사용합니다.

  • 이와 같은 알고리즘이 OpenCV의 threshold함수에 구현되어있으며, cv2.THRESH_OTSU 파라미터를 아래 코드와 같이 사용하면 적용이 됩니다.

plt.figure(figsize=(8, 4))
ret, th = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

plt.subplot(1, 2, 1)
plt.imshow(img, cmap='gray')
plt.title('Gray Lena')

plt.subplot(1, 2, 2)
plt.imshow(th, cmap='gray')
plt.title('Otsu Lena')

plt.suptitle('Lena with Otsu threshold value', size=15)
plt.show()

Blob labeling

  • Threshold를 통해 할 수 있는 일은 그야말로 무궁무진한데, 그 중 하나로 이미지 분할(image segmentation)을 예시로 들 수 있습니다.

    • 만일 threshold 등의 알고리즘을 이용하여 특정 목적에 따라 영상을 분할할 수 있다면(e.g. 사람 손 or 도로의 차선) 1로 정해진 픽셀끼리 하나의 object라고 생각할 수 있을것이고, 우리는 이 object를 묶어서 사용하고 싶게 될 것입니다.
  • 서로 다른 object인지를 판단하기 위하여 픽셀의 연결성 [2] 을 고려한 알고리즘을 수행하고 각기 다른 label을 할당하는데, 이를 Blob labeling이라 합니다.

  • Blob labeling을 하면 개별 object에 대해 각각 접근하여 우리가 하고싶은 다양한 영상처리를 개별적으로 적용할 수 있게 되니, 활용도가 아주 높은 기능입니다.

    • 본 예제에서는 blob labeling에 대한 개념적인 소개와 OpenCV에 구현된 함수의 간단한 사용법을 확인하겠습니다. [3]
    • 직관적으로 원의 형상을 띄는 위치에 하나의 blob을 의미하는 파랑색 동그라미를 생성하는 모습입니다.
    • 잘못된 위치에 그려진 blob이 눈에 띄는데요, 이와 같은 결과를 parameter를 통해 handling하는 내용에 대해서 차후 다가올 주제인 Image Segmentation에서 확인하겠습니다.
ret, coin_img = cv2.threshold(coin, 200, 255, cv2.THRESH_BINARY_INV)

params = cv2.SimpleBlobDetector_Params()
params.minThreshold = 10
params.maxThreshold = 255
params.filterByArea = False
params.filterByCircularity = False
params.filterByConvexity = False
params.filterByInertia = False

detector = cv2.SimpleBlobDetector_create(params) # Blob detector 선언
keypoints = detector.detect(coin_img)  # Blob labeling 수행
im_with_keypoints = \
cv2.drawKeypoints(coin_img, keypoints, np.array([]), (0, 0, 255),
                  cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)  # 원본 이미지에 찾은 blob 그리기

plt.figure(figsize=(15,15))
plt.imshow(im_with_keypoints)
plt.title('Coin keypoint', size=15)
plt.show()

Conclusion

  • Image thresholding의 사용법과 다양한 응용방법, threshold 값을 선택해주는 Otsu 알고리즘을 알아보았습니다.
  • 마지막에 알아본 Blob labeling은 Image processing 분야 전반에 걸쳐 사용되는 곳이 아주 많으니 추후에 더 깊이 알아보는 시간을 갖겠습니다.

Reference

Avatar
YoungEon Kim
AI Scientist
Avatar
Whi Kwon
AI Researcher