iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0

一、 介紹

大津演算法(Otsu)是一種自動影像二值化方法,通過分析影像的灰度分佈,自動找到最適合的閾值,突顯出影像中的目標特徵。這個演算法在處理影像時非常有用,因為使用這個演算法可以幫助我們快速且準確的將影像二值化。

二、 原理

1. 期望值(平均值)

期望值是概率論和統計學中的一個重要概念,用來衡量隨機變數的平均值u或中心趨勢。它是在概率分佈下所有可能的值乘以其對應的概率後的加權平均。

在數學上,對於離散型隨機變數 X,期望值E[X] 的計算公式如下:
https://ithelp.ithome.com.tw/upload/images/20230920/201617323OwGk60NyX.png

當我們用一個六面骰子(六面的正方體,每個面上標有 1 到 6 的數字)投擲時,我們可以計算這個隨機事件的期望值。

假設我們想要計算骰子的點數的期望值。每個點數有等可能性(1/6)出現,因為骰子是均勻的。

因此,期望值計算如下:
https://ithelp.ithome.com.tw/upload/images/20230920/20161732Y4YAxoLpHM.png

在這個例子中,骰子的點數的期望值是 3.5。這意味著,如果我們丟了無限次的骰子,平均來說,我們可以期望得到接近 3.5 的點數。

2. 標準差(Standard Deviation)

標準差是一種統計量,用來衡量一組數據的散佈程度或變異程度。當數據中的值之間差異較大時,標準差會較大;而當數據的值彼此間的差異較小時,標準差會較小。

標準差的數學公式如下:
https://ithelp.ithome.com.tw/upload/images/20230920/20161732dU1zQ6fEVg.png

我們可以通過觀察下圖來理解這一點,這是一張平均值為0的常態分佈圖,當常態分佈的標準差(用符號σ表示)越小,這代表數據相對集中。換句話說,大部分的數據點都會集中在平均值附近,較少的數據點會偏離平均值。反之,當常態分佈的標準差越大,這代表數據相對分散,數據點會廣泛地分佈在平均值周圍。

https://ithelp.ithome.com.tw/upload/images/20230920/20161732gFW2lATmiL.jpg

3. 變異數(Variance)

變異數(Variance)是用來衡量一組數據的散佈程度或變異程度。它代表著數據點與其平均值之間的差異程度的平均值。變異數可以幫助我們瞭解數據的分佈情況,以及數據點與平均值之間的偏離程度。

變異數可以透過將標準差平方後得到。如果我們已經知道一組數據的標準差,我們可以通過對其進行平方運算,得到數據的變異數。

這種關係可以用以下的數學公式表示:
https://ithelp.ithome.com.tw/upload/images/20230920/20161732kpWWgZhvbv.png

4. 大津二值化(OTSU Thresholding)

在大津演算法中,我們的目標是找到一個最適合的閾值,以便將影像的像素分成前景和背景兩個類別。大津演算法告訴我們,我們希望讓同一類的數據更加相似,而不同類之間的數據更加不同。

在介紹大津演算法是怎麼實現之前,我們需要搞懂一些名詞:

  • 前景(Foreground): 前景通常是我們關注的物體或目標,可以想像成二值化後變成白色的部分
  • 背景(Background): 背景是相對於前景的部分,是影像中沒有引起我們注意的地方,可以想像成二值化後變成黑色的部分
  • 類內變異數(Intra-class Variance): 類內變異數衡量的是同一類別內部像素之間的差異。如果同一類別的灰階值相似,那麼類內變異數就會較小,如果不相似,就會較大。我們希望將同一類別的區域變得更加一致,所以會盡量使類內變異數減小。這意味著我們在對灰階圖作二值化時,希望同一類別(即前景)內的像素灰階值變化較小,即像素相對於平均值的變異程度較小。
  • 類間變異數(Inter-class Variance): 類間變異數衡量的是不同類別之間像素之間的差異。如果不同類別的灰階值差異很大,類間變異數就會較大。我們希望不同類別之間的差異變得更大,這樣我們可以更清楚地區分出不同的區域。這意味著我們在對灰階圖作二值化時,希望不同類別(即前景和背景)的像素灰階值差別較大,達到分類作用。
  • t:二值化閾值

https://ithelp.ithome.com.tw/upload/images/20230920/20161732vT5g9v6KGk.png

以下是大津演算法中用來計算類間變異數的公式:
https://ithelp.ithome.com.tw/upload/images/20230920/20161732S7tkGNtQ5s.png

  • σb(t) 平方:這代表類間變異數,也就是在閾值為t時不同類別之間的差異程度。
  • σ 平方:這是整個影像的變異數,代表影像中所有像素值的差異程度。
  • σw(t) 平方:這是類變異數,也就是同一類別在閾值為t時內部的像素差異程度。
  • w1(t)w2(t):這分別表示在閾值為t時的兩個類別的權重,w1(t)是背景的權重,而 w2(t)是前景的權重。
  • μ1(t)μ2(t):這分別表示在閾值為t時的兩個類別的平均像素值,μ1(t)是背景的平均像素值,而 μ2(t) 是前景的平均像素值。

可以看到,因為σ 平方是整張圖片的變異數是常數,當類內變異數最小時,類間變異數會最大只要將兩個類之間的差異(類間變異數)最大化,這兩個類分別的內部差異(類內變異數)就自動的被最小化。換句話說,我們不需要同時追求最大化類之間的差異並最小化內部差異。

大津演算法的原理是搜尋所有閾值t,找出0~255中出現最大類間變異數所在的閾值為多少,實現使同一類的數據更加相似,而不同類之間的數據更加不同的效果。

為了找到類間變異數最大值,我們需要透過上方公式求出類間變異數

我們需要先算出這張圖片灰階值的平均,假設我們今天已經將圖片的直方圖求出機率質量函數H(X=xi),H(X=xi)代表整張圖片灰階值為xi的像素個數除以圖片總像素(或是灰階值xi在這張圖片出現的機率),最大為1。

這個直方圖的期望值(或平均值)可以用數學公式表示:
https://ithelp.ithome.com.tw/upload/images/20230920/201617322cBRaywTrO.png

背景的權重 w1(t)表示的是在二值化閾值 t情況下,像素出現灰階值小於等於 t 的總和機率。反之,前景的權重w2(t) 則是像素出現灰階值大於二值化閾值 t的總和機率。因為兩個權重的總和為1,我們可以藉由w1(t)推出w2(t)的機率。這兩個函數的性質適合使用CDF函數來表示累加的範圍。
https://ithelp.ithome.com.tw/upload/images/20230920/20161732E29upMZEek.png

接下來,我們要求出背景內的平均值前景內的平均值,我們可以藉由μ1(t)期望值來推出μ2(t)期望值,以減少不必要的迴圈運算
https://ithelp.ithome.com.tw/upload/images/20230920/20161732SUoypDI2HN.png

https://ithelp.ithome.com.tw/upload/images/20230920/20161732uISx2K5M8z.jpg

大津演算法的整體步驟如下:

  1. 計算每個灰階值的直方圖並計算出現的PMF機率質量函數H(X=xi)
  2. 初始化變數 w1(0)μ1(0)
  3. 遍歷閾值 t 從 0 到 255 的範圍:
    1. 計算參數 w1(t)μ1(t)w2(t)μ2(t)
    2. 計算類間變異數 σb(t)
    3. 如果目前的類間變異數大於先前的最大值,更新最大值。
    4. 記錄當前的閾值 t
  4. 最終,使用記錄的閾值找到最大類間變異數對應的閾值。

三、 程式碼

1. 逐行解釋

這個程式碼提供了兩種運算方式,一個是使用OpenCV內建的函式,另一個是使用自己寫的大津演算法,兩者效果一樣。你可以透過設定USE_OPENCV10來決定你要使用前者還是後者的實現方式。

#define USE_OPENCV 1

1) 使用OpenCV進行大津二值化

使用OpenCV 函式庫中的 cv::threshold 函數,用於對灰階影像進行二值化。

double threshold = cv::threshold(grayImage, binaryImage, 0,255, cv::THRESH_OTSU);
printf("threshold:%d",(int)threshold);
  1. grayImage:這是輸入的灰階影像,表示你要對這張影像進行二值化。
  2. binaryImage:這是輸出的二值化影像,函數會將二值化後的影像存儲在這個變數中。
  3. 0:這是二值化的閾值,因為我們使用大津演算法,不需要提供閾值,所以輸入0。
  4. 255:這是閾值的上界,也就是將所有高於閾值的像素設為前景(白色)。
  5. cv::THRESH_OTSU:這是指定的二值化方法,使用了大津演算法來自動找到適合的閾值,以最大化類間變異數。
  6. cv::threshold 函數回傳otsu演算法的結果。

2) 使用演算法實踐大津二值化

  1. calcHist 函數用來計算一張影像的灰階直方圖。它遍歷影像的每個像素,根據像素的灰階值將對應的直方圖的計數值增加。

  2. 接下來使用otsu 函式實現了大津演算法,尋找閾值。

cv::Mat hist=calcHist(grayImage);
uchar threshold=otsu(hist);
printf("threshold:%d",threshold);
cv::threshold(grayImage, binaryImage, threshold, 255, cv::THRESH_BINARY);

otsu 函式實現了大津演算法,具體實現的步驟如下:

  1. 首先將直方圖轉換成機率質量函數 hist_pmf並除以圖片總像素進行正規化,以便進行計算。

  2. 接著計算整張影像的平均灰階值 u

  3. 在迴圈中,從閾值 0 到 255 逐個測試。對每個閾值 t,計算出在閾值左側的背景權重 w1,並計算出背景的平均灰階值 u1。同時計算在閾值右側的前景權重 w2,以及前景的平均灰階值 u2。然後計算出類間變異數 sigma_b_2

  4. 每次計算出的 sigma_b_2 與先前的最大值 max_sigma_b_2 進行比較,如果當前的 sigma_b_2 更大,則更新最大值並記錄對應的閾值 t

  5. 最終,函式返回找到的最適合的閾值。

// 使用OTSU方法計算閾值
uchar otsu(cv::Mat hist)
{
    cv::Mat hist_pmf;
    hist.convertTo(hist_pmf, CV_32F);
    hist_pmf /= cv::sum(hist)[0];

    float u = 0.0f;
    for (int i = 0; i < 256; i++)
    {
        u += i * hist_pmf.at<float>(i);
    }

    float w1 = 0.0f;
    float sum1 = 0.0f;
    float max_sigma_b_2 = -1.0f;
    uchar threshold = 0;

    for (int t = 0; t < 256; t++)
    {
        //累加背景權重
        w1 += hist_pmf.at<float>(t);
        if (w1 == 0.0f)
            continue;

        //累加前景權重
        float w2 = 1.0f - w1;
        if (w2 == 0.0f)
            continue;
        sum1 += t * hist_pmf.at<float>(t);

        //計算背景平均值
        float u1 = sum1 / w1;

        float sum2 = u - sum1;

        //計算前景平均值
        float u2 = sum2 / w2;

        //計算類間變異數
        float sigma_b_2 = w1 * w2 * (u1 - u2) * (u1 - u2);

        if (sigma_b_2 > max_sigma_b_2)
        {
            max_sigma_b_2 = sigma_b_2;
            threshold = t;
        }
    }

    return threshold;
}

2. 完整程式碼

#include <iostream>
#include <inttypes.h>
#include <opencv2/opencv.hpp>
#include <opencv2/core/utils/logger.hpp>

#define USE_OPENCV 1

using namespace std;

// 函數原型
uchar otsu(cv::Mat hist);
cv::Mat calcHist(cv::Mat image);

int main()
{
    cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);

    // 讀取灰度影像
    cv::Mat grayImage = cv::imread("C:\\Users\\vince\\Downloads\\Lenna.png", cv::IMREAD_GRAYSCALE);
    cv::Mat binaryImage;

#if USE_OPENCV
    // 使用OpenCV的OTSU閾值化
    double threshold = cv::threshold(grayImage, binaryImage, 0, 255, cv::THRESH_OTSU);
    printf("OTSU threshold:%d", (int)threshold);
#else
    // 計算影像的直方圖
    cv::Mat hist = calcHist(grayImage);

    // 使用OTSU方法計算閾值
    uchar threshold = otsu(hist);
    printf("OTSU threshold:%d", threshold);

    // 根據計算得到的閾值進行二值化
    cv::threshold(grayImage, binaryImage, threshold, 255, cv::THRESH_BINARY);
#endif

    // 顯示二值化影像
    cv::imshow("Binary Image", binaryImage);
    cv::waitKey(0);
    return 0;
}

// 計算灰度影像的直方圖
cv::Mat calcHist(cv::Mat image)
{
    cv::Mat hist = cv::Mat::zeros(cv::Size(1, 256), CV_32SC1);

    for (int y = 0; y < image.rows; y++)
    {
        for (int x = 0; x < image.cols; x++)
        {
            uchar value = image.at<uchar>(y, x);
            hist.at<int>(value)++;
        }
    }
    return hist;
}

// 使用OTSU方法計算閾值
uchar otsu(cv::Mat hist)
{
    cv::Mat hist_pmf;
    hist.convertTo(hist_pmf, CV_32F);
    hist_pmf /= cv::sum(hist)[0];

    float u = 0.0f;
    for (int i = 0; i < 256; i++)
    {
        u += i * hist_pmf.at<float>(i);
    }

    float w1 = 0.0f;
    float sum1 = 0.0f;
    float max_sigma_b_2 = -1.0f;
    uchar threshold = 0;

    for (int t = 0; t < 256; t++)
    {
        //累加背景權重
        w1 += hist_pmf.at<float>(t);
        if (w1 == 0.0f)
            continue;

        //累加前景權重
        float w2 = 1.0f - w1;
        if (w2 == 0.0f)
            continue;
        sum1 += t * hist_pmf.at<float>(t);

        //計算背景平均值
        float u1 = sum1 / w1;

        float sum2 = u - sum1;

        //計算前景平均值
        float u2 = sum2 / w2;

        //計算類間變異數
        float sigma_b_2 = w1 * w2 * (u1 - u2) * (u1 - u2);

        if (sigma_b_2 > max_sigma_b_2)
        {
            max_sigma_b_2 = sigma_b_2;
            threshold = t;
        }
    }

    return threshold;
}

3. 測試結果

可以看到,即使我們沒有提供閾值,大津演算法幫我們找出了最適合這張圖片的閾值。大津演算法的這個特性使得影像處理變得更加自動化,無需事先了解影像的特性,就能夠獲得最佳的結果。這對於處理大量影像或需要快速處理影像的場景非常有用。

https://ithelp.ithome.com.tw/upload/images/20230920/20161732ldD29uorQw.png


上一篇
【Day12】OpenCV 自適應二值化(Adaptive Thresholding):降低亮度干擾
下一篇
【Day14】影像處理 空間濾波器(Spatial Filter)
系列文
圖解C++影像處理與OpenCV應用:從基礎到高階,深入學習超硬核技術!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言