OpenCV C++(六)----閾值分割

6.1、方法概述

閾值分割的核心就是如何選取閾值, 選取正確的閾值是分割成功的關鍵。

1、全局閾值分割

全局閾值分割指的是將灰度值大于thresh(閾值)的像素設為白色,小于或者等于 thresh的像素設為黑色; 或者反過來, 將大于thresh的像素設為黑色, 小于或者等于thresh 的像素設為白色, 兩者的區別只是呈現形式不同。

image.png
double threshold( InputArray src, OutputArray dst,double thresh, double maxval, int type );
image.png

需要注意的是,當類型為THRESH_OTSUTHRESH_TRIANGLE時,輸入參數src只支持uchar類型, 這時thresh也是作為輸出參數的, 即通過OtsuTRIANGLE算法自動計算出來。

2、局部閾值分割

局部閾值分割的核心也是計算閾值矩陣,比較常用的是后面提到的自適應閾值算法(又稱移動平均值算法) , 是一種簡單但是高效的局部閾值算法,其核心思想就是把每一個像素的鄰域的“平均值”作為該位置的閾值。

6.2、直方圖技術法

一幅含有一個與背景呈現明顯對比的物體的圖像具有包含雙峰的直方圖,兩個峰值對應于物體內部和外部較多數目的點,兩個峰值之間的波谷對應于物體邊緣附近相對較少數目的點。

直方圖技術法就是首先找到這兩個峰值,然后取兩個峰值之 間的波谷位置對應的灰度值,就是所要的閾值。

一 種常用的方式是先對直方圖進行高斯平滑處理,逐漸增大高斯濾波器的標準差,直到能從平滑后的直方圖中得到兩個唯一的波峰和它們之間唯一的最小值。但這種方式需要手動調節,下面介紹一種規則自動選取波峰和波谷的方式。

假設輸入圖像為I, 高為H、 寬為W,histogramI代表其對應的灰度直方圖, histogramI (k) 代表灰度值等于k的像素點個數, 其中0≤k≤255。

  • 第一步: 找到灰度直方圖的第一個峰值,并找到其對應的灰度值。顯然,灰度直方圖的最大值就是第一個峰值且對應的灰度值用firstPeak表示。

  • 第二步: 找到直方圖的第二個峰值,并找到其對應的灰度值。第二個峰值不一定是直方圖的第二大值,因為它很有可能出現在第一個峰值的附近。

image.png
  • 第三步: 找到這兩個峰值之間的波谷,如果出現兩個或者多個波谷,則取左側的波 谷即可,其對應的灰度值即為閾值。
int threshTwoPeaks(const Mat &image, Mat &thresh_out)
{
    //計算灰度直方圖
    Mat histogram = calGrayHist(image);
    //找到灰度直方圖最大峰值對應的灰度值
    Point firstPeakLoc;
    minMaxLoc(histogram, NULL, NULL, NULL, &firstPeakLoc);
    int firstPeak = firstPeakLoc.x;
    //尋找灰度直方圖第二個峰值對應的灰度值
    Mat measureDists = Mat::zeros(Size(256, 1), CV_32FC1);
    for (int k = 0; k < 256; k++)
    {
        int hist_k = histogram.at<int>(0, k);
        measureDists.at<float>(0, k) = pow(float(k - firstPeak), 2)*hist_k;
    }
    Point secondPeakLoc;
    minMaxLoc(measureDists, NULL, NULL, NULL, &secondPeakLoc);
    int secondPeak = secondPeakLoc.x;

    //找到兩個之間的最小值對應的灰度值,作為閾值
    Point threshLoc;
    int thresh = 0;
    if (firstPeak < secondPeak)//第一個峰值在第二個峰值的左側
    {
        minMaxLoc(histogram.colRange(firstPeak, secondPeak), NULL, NULL, NULL, &threshLoc);
        thresh = firstPeak + threshLoc.x + 1;
    }
    else//第一個峰值在第二個峰值的右側
    {
        minMaxLoc(histogram.colRange(secondPeak, firstPeak), NULL, NULL, NULL, &threshLoc);
        thresh = secondPeak + threshLoc.x + 1;
    }
    //閾值分割
    threshold(image, thresh_out, thresh, 255, THRESH_BINARY);
    return thresh;
}

6.3、熵方法

image.png

利用熵計算閾值的步驟如下:

  • 第一步: 計算I 的累加概率直方圖,又稱零階累積矩,記為
image.png
  • 第二步: 計算各個灰度級的熵,記為
image.png
  • 第三步: 計算使f (t) =f1(t) +f2(t) 最大化的t值, 該值即為得到的閾值, 即thresh=argtmax(f (t) ), 其中
image.png

6.4、Otsu閾值處理

在對圖像進行閾值分割時,所選取的分割閾值應使前景區域的平均灰度、背景區域 的平均灰度與整幅圖像的平均灰度之間的差異最大, 這種差異用區域的方差來表示。 Otsu[2]提出了最大方差法, 該算法是在判別分析最小二乘法原理的基礎上推導得出的, 計算過程簡單, 是一種常用的閾值分割的穩定算法。

  • 第一步: 計算灰度直方圖的零階累積矩(或稱累加直方圖) 。
image.png
  • 第二步: 計算灰度直方圖的一階累積矩。
image.png
  • 第三步: 計算圖像I 總體的灰度平均值mean, 其實就是k=255時的一階累積距, 即
image.png
  • 第四步: 計算每一個灰度級作為閾值時, 前景區域的平均灰度、 背景區域的平均灰 度與整幅圖像的平均灰度的方差。 對方差的衡量采用以下度量:
image.png
  • 第五步: 找到上述最大的σ2(k), 然后對應的k即為Otsu自動選取的閾值, 即
image.png
int otsu(const Mat &image, Mat &OtsuThreshImage)
{
    //計算灰度直方圖
    Mat histogram = calGrayHist(image);
    //歸一化灰度直方圖
    Mat normHist;
    histogram.convertTo(normHist, CV_32FC1, 1.0 / (image.rows*image.cols), 0.0);
    //計算累加直方圖(零階累積距)和一階累積距
    Mat zeroCumuMoment = Mat::zeros(Size(256, 1), CV_32FC1);
    Mat oneCumuMoment = Mat::zeros(Size(256, 1), CV_32FC1);
    for (int i = 0; i < 256; i++)
    {
        if (i == 0)
        {
            zeroCumuMoment.at<float>(0, i) = normHist.at<float>(0, i);
            oneCumuMoment.at<float>(0, i) = normHist.at<float>(0, i);
        }
        else
        {
            zeroCumuMoment.at<float>(0, i) = normHist.at<float>(0, i) + zeroCumuMoment.at<float>(0, i - 1);
            oneCumuMoment.at<float>(0,i)= normHist.at<float>(0, i) + oneCumuMoment.at<float>(0, i - 1);
        }
    }
    //計算類間方差
    Mat variance = Mat::zeros(Size(256, 1), CV_32FC1);
    //總平均值
    float mean = oneCumuMoment.at<float>(0, 255);
    for (int j = 0; j < 256; j++)
    {
        if (zeroCumuMoment.at<float>(0, j) == 0 || zeroCumuMoment.at<float>(0, j) == 1)
        {//分母不能為零
            variance.at<float>(0, j) = 0;
        }
        else
        {
            variance.at < float>(0, j) = pow(mean*zeroCumuMoment.at<float>(0, j) - oneCumuMoment.at<float>(0, j), 2) / zeroCumuMoment.at<float>(0, j)*(1.0 - zeroCumuMoment.at<float>(0, j));
        }
    }
    //找到最值作為閾值
    Point maxLoc;
    minMaxLoc(variance, NULL, NULL, NULL, &maxLoc);
    int thresh = maxLoc.x;
    //閾值處理
    threshold(image, OtsuThreshImage, thresh, 255, THRESH_BINARY);
    return thresh;
}

6.5、自適應閾值

在不均勻照明或者灰度值分布不均的情況下,如果使用全局閾值分割, 那么得到的分割效果往往會很不理想。那么想到的策略是針對每一個位置的灰度值 設置一個對應的閾值, 而該位置閾值的設置也和其鄰域有必然的關系。

在對圖像進行平滑處理時,均值平滑、高斯平滑、中值平滑用不同規則計算出以當前像素為中心的鄰域內的灰度“平均值”, 所以可以使用平滑處理后的輸出結果作為每個 像素設置閾值的參考值,如用均值濾波后的結果乘以某個比例系數作為最后的閾值矩陣。

平滑算子的寬度必須大于被識別物體的寬度,平滑算子的尺寸越大,平滑后的結果越能更好地作為每個像素的閾值的參考,當然也不能無限大。

  • 第一步: 對圖像進行平滑處理, 平滑結果記為fsmooth(I) ,其中fsmooth可以代表 均值平滑、高斯平滑、中值平滑。
  • 第二步: 自適應閾值矩陣Thresh=(1-ratio) *fsmooth(I) ,一般令ratio=0.15
  • 第三步: 利用局部閾值分割的規則進行閾值分割。
Mat adaptiveThresh(Mat I, int radius, float radio, METHOD method)
{
    //step1:對圖像矩陣進行平滑處理
    Mat smooth;
    switch (method)
    {
    case MEAN:
        boxFilter(I, smooth, CV_32FC1, Size(2 * radius + 1, 2 * radius + 1));
        break;
    case GAUSS:
        GaussianBlur(I, smooth, Size(2 * radius + 1, 2 * radius + 1), 0, 0);
        break;
    case MEDIAN:
        medianBlur(I, smooth, 2 * radius + 1);
        break;
    default:
        break;
    }
    //step2:平滑結果乘于比例系數,然后圖像矩陣與其做差
    I.convertTo(I, CV_32FC1);
    smooth.convertTo(smooth, CV_32FC1);
    Mat diff = I - (1.0 - radio)*smooth;
    //step3:閾值處理,當≥0,輸出值為255,反之,輸出值為0
    Mat out = Mat::zeros(diff.size(), CV_8UC1);
    for (int r = 0; r < I.rows; r++)
    {
        for (int c = 0; c < I.cols; c++)
        {
            if (diff.at<float>(r, c) > 0)
            {
                out.at<uchar>(r, c) = 255;
            }
        }
    }
    return out;
}

就可以理解OpenCV提供的自適應閾值函數:

void adaptiveThreshold( InputArray src, OutputArray dst,
                                     double maxValue, int adaptiveMethod,
                                     int thresholdType, int blockSize, double C );

6.6、二值圖的邏輯運算

OpenCV提供的兩個函數bitwise_andbitwise_or分別實現了兩 個矩陣之間的與運算和或運算,它們本質上完成的是兩個矩陣對應位置數值的邏輯運算。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容