본문 바로가기
Computer Vision

[Computer Vision] Otsu Algorithm, 오츄 이진화 알고리즘

by se0_ing 2024. 10. 19.
반응형

 

이 블로그는 국민대학교 윤상민 교수님의 Computer Vision 교과목을 듣고 작성한 블로그입니다.

 

 

 

 

 

오늘은 중간고사 정리 겸, 이진화 영상과 Otsu 알고리즘에 대해 알아보겠다.

 

 

 

가장 먼저, 컴퓨터 비전에서 이진화란, 명암 영상을 흑과 백으로만 이루어진 이진 영상으로 변환하는 것을 말한다.

 

 

$b(j,i) = \begin{cases} 1, & \text{if } f(j,i) \geq T \\ 0, & \text{if } f(j,i) < T \end{cases}$

 

 

오츠 알고리즘(Otsu algorithm)은 이미지의 이진화(binary thresholding)를 자동으로 수행하기 위한 방법이다. 이 알고리즘은 이미지의 히스토그램을 분석하여 최적의 임계값(threshold)을 찾아 배경과 전경을 분리한다. 

 

 

 

 

 

 

 

Colab에서 실습 코드를 통해 알아보자.

 

필요한 라이브러리 및 구글 드라이브 마운트

import cv2
import os
import numpy as np
from matplotlib import pyplot as plt

from google.colab import drive
drive.mount('/content/drive')
root = '/content/drive/MyDrive/2024-2 컴퓨터비전/'

 

이미지 불러오기

img = cv2.imread(os.path.join(root, "barbara.png"), cv2.IMREAD_GRAYSCALE)
plt.imshow(img, cmap="gray")
plt.show()

 

 

Otsu algorithm 정의

# [Otsu algorithm] : 이미지에서 물체와 배경을 분리하기 위해 사용되는 이미지 이진화 기술
# 핵심 아이디어 : 이미지의 히스토그램을 분석하여 이미지의 foreground와 background를 분리하는 threshold(임계값)를 찾는 것

def otsu(img):
    # histogram and CDF(cumulative distribution function, 누적분포함수)
    # 누적 분포 함수는 랜덤 변수가 특정 값보다 작거나 같을 확률을 나타내는 함수,
    #'누적'이라는 이름은 특정 값보다 작은 값들의 확률을 모두 누적해서 구한다는 의미에서 붙여진 이름
    # ex) Fx(x) = Px(X<=x)

    # cv2.calcHist() 함수를 사용하여 이미지의 히스토그램을 계산
    # cv2.calcHist(images, channels, maskm histSize, ranges, hist=None, accumulate=None
    hist = cv2.calcHist([img], [0], None, [256], [0, 256])
    # 정규화
    hist_norm = hist.ravel() / hist.max() # 히스토그램 값 / max값 : 각 픽셀 값의 확률

    # numpy.cumsum 함수는 지정된 축을 따라 어레이 요소의 누적 합을 계산
    CDF = np.cumsum(hist_norm)

    # initialization(초기화) 정의
    bins = np.arange(256)
    fn_min = np.inf
    threshold = -1

    for i in range(1, 256):
        # 분할된 히스토그램은 각 클래스(배경 및 전경)의 확률 분포
        p1, p2 = np.hsplit(hist_norm, [i])  # probabilities, ex) np.hsplit(x, 2) : 배열을 가로(열 방향으로) 2개의 하위 배열로 분할
        q1, q2 = CDF[i], CDF[255] - CDF[i]  # cumsum of classes, 값을 사용하여 각 클래스의 가중치 및 평균을 계산

        # 0으로 나눌 수 없기 때문에 아주 작은 값으로 설정
        if q1 == 0:
            q1 = 0.00000001
        if q2 == 0:
            q2 = 0.00000001

        # 각 픽셀 값의 weights 계산을 위해 사용
        b1, b2 = np.hsplit(bins, [i])  # 이렇게 나눈 값 들은 각 클래스의 픽셀 값의 범위

        # finding means and variances
        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  # 각 클래스 내부의 픽셀 값에 대한 분산을 계산

        # calculates the minimization function
        fn = v1 * q1 + v2 * q2  # 누적 분산 계산
        if fn < fn_min: # 각 클래스의 현재 분산 값이 기존의 최소 분산 값보다 작으면 업데이트
            fn_min = fn
            threshold = i


    return threshold

 

 

 

주석이 보기 힘들텐데 수식과 함께 정리 해보겠다.

 

 

1. 히스토그램 계산

 

hist = cv2.calcHist([img], [0], None, [256], [0, 256])

 

목적: 이미지의 그레이스케일 히스토그램  $h(i)$ 를 계산한다.

수식 표현:

 

$h(i) = \text{해당 그레이스케일 값 } i \text{를 가진 픽셀의 수}$

 

여기서  $i$ = 0, 1, 2, …, 255

 

 

2. 히스토그램 정규화

 

hist_norm = hist.ravel() / hist.max()

 

목적: 히스토그램을 정규화하여 확률 분포  $P(i)$ 를 얻는다.

수식 표현:

 

$P(i) = \frac{h(i)}{\sum_{j=0}^{255} h(j)}$

 

여기서  $\sum_{j=0}^{255} P(j)$ = 1

 

 

3. 누적 분포 함수(CDF) 계산

 

CDF = np.cumsum(hist_norm)

 

목적: 누적 분포 함수  $\omega(t)$ 를 계산한다.

수식 표현:

 

$\omega(t) = \sum_{i=0}^{t} P(i)$

 

여기서  $t$ = 0, 1, 2, ..., 255

 

 

4. 임계값 탐색을 위한 초기화

 

bins = np.arange(256)

fn_min = np.inf

threshold = -1

 

설명: 임계값 후보 $t$를 1부터 255까지 순회하며 최적의 임계값을 찾는다.

 

 

5. 모든 가능한 임계값에 대해 반복

 

for i in range(1, 256):

    # ...

 

 

5-1. 히스토그램 분할

 

p1, p2 = np.hsplit(hist_norm, [i])

q1, q2 = CDF[i], CDF[255] - CDF[i]

 

목적: 현재 임계값  $t = i$ 에서 배경 클래스  $C_0$ 와 전경 클래스  $C_1$ 로 분할한다.

수식 표현:

클래스 확률:

 

$\omega_0 = \omega(t) = \sum_{i=0}^{t} P(i)$

 

 

$\omega_1 = 1 - \omega(t) = \sum_{i=t+1}^{255} P(i)$

 

확률 분포:

 

$P_0(i) = \frac{P(i)}{\omega_0}, \quad \text{for } i = 0, …, t$

 

 

$P_1(i) = \frac{P(i)}{\omega_1}, \quad \text{for } i = t+1, …, 255$

 

 

 

5-2. 0으로 나누는 것을 방지

 

if q1 == 0:

    q1 = 1e-10

if q2 == 0:

    q2 = 1e-10

 

설명: 클래스 확률 $w_0$ 또는 $w_1$가 0인 경우를 방지하기 위해 아주 작은 값으로 대체한다.

 

 

5-3. 클래스별 픽셀 값 범위 분할

 

b1, b2 = np.hsplit(bins, [i])

 

목적: 픽셀 값  $i$ 의 범위를 클래스별로 분할한다.

b1 :  $i = 0$ 부터  $t$ 까지.

b2 :  $i = t+1$ 부터 255까지.

 

 

5-4. 클래스 평균 계산

 

m1 = np.sum(p1 * b1) / q1

m2 = np.sum(p2 * b2) / q2

 

수식 표현:

배경 클래스 평균:

 

$\mu_0 = \frac{1}{\omega_0} \sum_{i=0}^{t} i \cdot P(i)$

 

전경 클래스 평균:

 

$\mu_1 = \frac{1}{\omega_1} \sum_{i=t+1}^{255} i \cdot P(i)$

 

 

 

5-5. 클래스 분산 계산

 

v1 = np.sum(((b1 - m1) ** 2) * p1) / q1

v2 = np.sum(((b2 - m2) ** 2) * p2) / q2

 

수식 표현:

배경 클래스 분산:

 

$\sigma_0^2 = \frac{1}{\omega_0} \sum_{i=0}^{t} (i - \mu_0)^2 \cdot P(i)$

 

전경 클래스 분산:

 

$\sigma_1^2 = \frac{1}{\omega_1} \sum_{i=t+1}^{255} (i - \mu_1)^2 \cdot P(i)$

 

 

 

5-6. 클래스 내부 분산의 합 계산

 

fn = v1 * q1 + v2 * q2

 

수식 표현:

 

$\sigma_w^2(t) = \omega_0 \sigma_0^2 + \omega_1 \sigma_1^2$

 

목적: 현재 임계값  $t$ 에서의 클래스 내부 분산의 합  $\sigma_w^2(t)$ 를 계산한다.

 

 

5-7. 최소 분산을 가지는 임계값 선택

 

if fn < fn_min:

    fn_min = fn

    threshold = i

 

설명: 클래스 내부 분산의 합  $\sigma_w^2(t)$ 이 최소인 임계값  $t$ 를 찾는다.

 

 

6. 최적 임계값 반환

 

return threshold

 

결과: 클래스 내부 분산의 합이 최소가 되는 최적의 임계값  $t^*$ 를 반환한다.

 

 

 

 

 

 

Otsu algorithm & OpenCV 함수

# 이미지 shape 만들기
binary_img = np.zeros((img.shape[1], img.shape[0]), np.uint8)

# Otsu algorithm으로 threshold 값 구하기
threshold = otsu(img)

# 위에서 구한 threshold 값을 기준으로 바이너리화 진행
for i in range(0, img.shape[1]):
    for j in range(0, img.shape[0]):
        if img[i][j] < threshold:
            binary_img[i][j] = 0
        else:
            binary_img[i][j] = 255

# find otsu's threshold value with OpenCV function
# cv2.threshold() 함수를 사용하여 오츠 알고리즘을 실행하고, ret에 계산된 임계값을 저장하고, otsu에는 이진화된 이미지를 저장
ret, otsu_img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

 

 

다음 코드는 OpenCV에 내장 되어있는 오츄 알고리즘이다. 

_, binary_img = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)

 


다음 코드는 최적 임계값  $t^*$ 를 이용하여 이미지를 이진화한다. 픽셀 값이  $t^*$  이상이면 255(흰색), 그렇지 않으면 0(검은색)으로 설정한다.

 

$\text{binary\_img}(x, y) =\begin{cases}255, & \text{if } \text{img}(x, y) \geq t^* \\0, & \text{otherwise}\end{cases}$

 

 

Otsu algorithm & OpenCV 함수 비교 및 시각화

print(f'직접 구한 값 : {threshold}\ncv를 이용한 값 : {ret}\n') # 비슷한 값을 가지는걸 확인

# 이미지를 1행 2열 그리드에 표시
fig, axes = plt.subplots(1, 2, figsize=(8, 4))

# 첫 번째 축에 binary 이미지 표시
axes[0].imshow(binary_img, cmap="gray")
axes[0].set_title("Binary Image")

# 두 번째 축에 cv binary 이미지 표시
axes[1].imshow(otsu_img, cmap="gray")
axes[1].set_title("CV Binary Image")

 

 

 

 

 

 

 

 

 

 

 

 

오늘은 이진화, 오츄 알고리즘에 대해 알아보았다... 시험 잘보고 싶다

 

반응형