1. 程式人生 > 其它 >Learn OpenCV ---- 大津法(Otsu‘s)閾值

Learn OpenCV ---- 大津法(Otsu‘s)閾值

技術標籤:Opencvopencvpython計算機視覺

閱讀原文

大津法(Otsu's)閾值

什麼是影象閾值

影象閾值化是一種基於畫素強度大影象二值化方法。這種方法的輸入通常是一個灰度圖和一個閾值,輸出是一個二值影象。
如果影象中某個畫素的強度大於閾值,則該畫素被標記為白色(前景);如果影象中的某個畫素的強度小於或者等於閾值,則輸出畫素位置被標記為黑色(背景)。
影象閾值通常作為預處理步驟,用於使得更為關注的資訊突出化。
簡單的影象閾值可以通過手動來設定閾值,進而得到一個好的影象閾值表示。但是這對於大量的影象來說,這是及其乏味和無效的。

為了克服這一缺點,大津閾值演算法被提出,它是一種根據影象來自動確定閾值的演算法。在深入瞭解大津演算法之前,我們先了解影象閾值設定與影象分割之間的關係。

影象閾值與影象分割

影象分割是指將影象分割成不同區域或畫素組的一類演算法。因此,在某種意義上,影象閾值是影象分割的一種簡單形式,即將影象分成兩組,一組為前景,一組為背景。下圖給出來影象分割的一個小家族。
在這裡插入圖片描述

影象閾值可以分為區域性閾值演算法和全域性閾值演算法。對於全域性閾值演算法而言,是使用某個固定的閾值來對整個影象進行處理;而區域性閾值演算法可以利用影象的區域性區域資訊來選擇不同的閾值。值得注意的是,大津演算法屬於全域性閾值演算法。

大津(Otsu)演算法

全自動的全域性閾值演算法通常由下述幾步組成:

  1. 預處理輸入影象。
  2. 獲取影象直方圖(畫素分佈)。
  3. 計算閾值 T T T
  4. 將影象中畫素大於 T T T的區域替換成白色,其餘部分替換成黑色。

可以看到,最主要的步驟在第三步,因此,不同的演算法在第三步中有很大的不同。大津法在處理影象直方圖後,通過最小化每個類上的方差來分割目標。通常,這種技術為雙峰影象產生適當的結果。該類影象的直方圖中有兩個表達清晰的峰值,代表不同強度值的範圍。其核心思想是將影象直方圖劃分為兩個類,並定義一個閾值,使這些類的加權方差 σ w 2 ( t ) \sigma^2_w(t) σw2(t)最小。該加權方差的計算公式為: σ w 2 ( t ) = w 1 ( t ) σ 1 2 ( t ) + w 2 ( t ) σ 2 2 ( t ) \sigma^2_w(t)=w_1(t)\sigma^2_1(t)+w_2(t)\sigma^2_2(t)

σw2(t)=w1(t)σ12(t)+w2(t)σ22(t),其中,兩個權值是兩個類別的概率除以閾值 t t t,該閾值在[0, 255]範圍內取值,計算公式如下:
在這裡插入圖片描述
在這裡插入圖片描述
其中, P ( i ) P(i) P(i)的計算公式如下:‘
在這裡插入圖片描述
其中, n n n為畫素的強度。接著,我們需要求公式中的兩個方差,這兩個方差的計算公式如下:
在這裡插入圖片描述
在這裡插入圖片描述
可以看到,如果我們想要求得兩個方差,我們需要先獲得 μ \mu μ的值:
在這裡插入圖片描述
在這裡插入圖片描述
自此,我們就可以得到了所需的加權方差。其實,還有另一種計算方法,這種方法也是更為常用,如下:
在這裡插入圖片描述
具體程式碼如下:

import cv2
import numpy as np


def otsu_implementation(img_title="boat.jpg", is_normalized=False, is_reduce_noise=False):
    # 使用灰度模式讀取影象
    image = cv2.imread(img_title, 0)

    # 是否使用高斯模糊
    # 這裡不使用
    if is_reduce_noise:
        image = cv2.GaussianBlur(image, (5, 5), 0)

    # 設定直方圖bin的數量
    bins_num = 256

    # 獲取影象的直方圖
    hist, bin_edges = np.histogram(image, bins=bins_num)

    # 是否對影象進行正則化
    # 這裡不進行
    if is_normalized:
        hist = np.divide(hist.ravel(), hist.max())

    # 計算bin的中心
    bin_mids = (bin_edges[:-1] + bin_edges[1:]) / 2.

    # 迭代累加得到權值
    weight1 = np.cumsum(hist)
    weight2 = np.cumsum(hist[::-1])[::-1]

    # 計算得到均值
    mean1 = np.cumsum(hist * bin_mids) / weight1
    mean2 = (np.cumsum((hist * bin_mids)[::-1]) / weight2[::-1])[::-1]

    # 計算最終方差
    inter_class_variance = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2

    # 取最大值對應的下標
    index_of_max_val = np.argmax(inter_class_variance)
    # 從bin中取得閾值
    threshold = bin_mids[:-1][index_of_max_val]
    print("Otsu's algorithm implementation thresholding result: ", threshold)
    return threshold


def main():
    otsu_implementation()


if __name__ == "__main__":
    main()

當然,Opencv也提供了大津演算法來進行影象閾值的計算,因此,我們在此也給出使用了Opencv介面的程式碼:

import cv2
from matplotlib.ticker import FuncFormatter
from matplotlib import pyplot as plt
from otsu_implementation import otsu_implementation


def call_otsu_threshold(img_title="boat.jpg", is_reduce_noise=False):
    # 讀取二值影象
    image = cv2.imread(img_title, 0)

    if is_reduce_noise:
        image = cv2.GaussianBlur(image, (5, 5), 0)

    # 現實影象的直方圖
    plt.hist(image.ravel(), 256)
    plt.xlabel('Colour intensity')
    plt.ylabel('Number of pixels')
    plt.savefig("image_hist.png")
    plt.show()
    plt.close()

    # 影象二值化,其中THRESH_OTSU就為大津演算法
    otsu_threshold, image_result = cv2.threshold(
        image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU,
    )
    print("Obtained threshold: ", otsu_threshold)

    # 顯示兩類大概率的直方圖
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.hist(image_result.ravel(), 256)
    ax.set_xlabel('Colour intensity')
    ax.set_ylabel('Number of pixels')
    ax.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: ('%1.1fM') % (x*1e-6)))
    plt.savefig("image_hist_result.png")
    plt.show()
    plt.close()

    # 將二值化後的影象顯示
    cv2.imshow("Otsu's thresholding result", image_result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def main():
    call_otsu_threshold()
    otsu_implementation()


if __name__ == "__main__":
    main()