1. 程式人生 > >【OpenCV學習筆記 023】兩種影象分割方法比較

【OpenCV學習筆記 023】兩種影象分割方法比較

此次研究兩種影象分割法,分別是基於形態學的分水嶺演算法和基於圖割理論的GrabCut演算法。OpenCV均提供了兩張演算法或其變種。鑑於研究所需,記錄一些知識點,開發平臺為OpenCV2.4.9+Qt5.3.2。

一、使用分水嶺演算法進行影象分割

分水嶺變換是一種常用的影象處理演算法,在網上很容易搜到詳細的原理分析。簡單來說,這是一種基於拓撲理論的數學形態學的影象分割方法,其基本思想是把影象看作是測地學上的拓撲地貌,影象中每一點畫素的灰度值表示該點的海拔高度,每一個區域性極小值及其影響區域稱為集水盆,而集水盆的邊界則形成分水嶺。分水嶺的概念和形成可以通過模擬浸入過程來說明。在每一個區域性極小值表面,刺穿一個小孔,然後把整個模型慢慢浸入水中,隨著浸入的加深,每一個區域性極小值的影響域慢慢向外擴充套件,在兩個集水盆匯合處構築大壩,即形成分水嶺。

分水嶺演算法簡單,因此存在一些缺陷,如容易導致影象的過度分割。分水嶺演算法對微弱邊緣具有良好的響應,影象中的噪聲、物體表面細微的灰度變化,都會產生過度分割的現象。

為消除分水嶺演算法產生的過度分割,有兩種常規的處理方法,一是利用先驗知識去除無關邊緣資訊。二是修改梯度函式使得集水盆只響應想要探測的目標。

OpenCV提供了該演算法的改進版本,使用預定義的一組標記來引導對影象的分割,該演算法是通過cv::watershed函式來實現的。

要實現分水嶺演算法,首先新建一個類WaterShedSegmentation,在watershedsegmentation.h中新增:

#ifndef WATERSHEDSEGMENTATION_H
#define WATERSHEDSEGMENTATION_H #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> class WaterShedSegmentation { public: void setMarkers(const cv::Mat &markerImage); // 將原影象轉換為整數影象 cv::Mat process(const cv::Mat &image); // // 分水嶺演算法實現
// 以下是兩種簡化結果的特殊方法 cv::Mat getSegmentation(); cv::Mat getWatersheds(); private: cv::Mat markers; // 用於非零畫素點的標記 }; #endif // WATERSHEDSEGMENTATION_H
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

接著,在watershedsegmentation.cpp中新增:

#include "watershedsegmentation.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

void WaterShedSegmentation::setMarkers(const cv::Mat &markerImage) // 該函式將原影象轉換為整數影象
{
    markerImage.convertTo(markers,CV_32S);
}

cv::Mat WaterShedSegmentation::process(const cv::Mat &image)
{
  // 使用分水嶺演算法
  cv::watershed(image,markers);
  return markers;
}

// 以下是兩種簡化結果的特殊方法
// 以影象的形式返回分水嶺結果
cv::Mat WaterShedSegmentation::getSegmentation()
{
  cv::Mat tmp;
  // 所有畫素值高於255的標籤分割均賦值為255
  markers.convertTo(tmp,CV_8U);
  return tmp;
}

cv::Mat WaterShedSegmentation::getWatersheds()
{
  cv::Mat tmp;
  markers.convertTo(tmp,CV_8U,255,255);
  return tmp;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

main函式修改如下:

#include <QCoreApplication>
#include "watershedsegmentation.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 輸入待處理的影象
    cv::Mat image= cv::imread("c:/Fig4.41(a).jpg");
    if (!image.data)
        return 0;

    cv::namedWindow("Original Image");
    cv::imshow("Original Image",image);

    // 輸入影象,將其轉化為二值影象
    cv::Mat binary;
    binary= cv::imread("c:/Fig4.41(a).jpg",0);

    // 顯示二值影象
    cv::namedWindow("Binary Image");
    cv::imshow("Binary Image",binary);

    // 移除噪點
    cv::Mat fg;
    cv::erode(binary,fg,cv::Mat(),cv::Point(-1,-1),6);

    // 顯示前景影象
    cv::namedWindow("Foreground Image");
    cv::imshow("Foreground Image", fg);

    // 識別背景影象,生成的黑色畫素對應背景畫素
    cv::Mat bg;
    cv::dilate(binary,bg,cv::Mat(),cv::Point(-1,-1),6);
    cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);

    // 顯示背景影象
    cv::namedWindow("Background Image");
    cv::imshow("Background Image", bg);

    // 顯示標記影象
    cv::Mat markers(binary.size(), CV_8U,cv::Scalar(0));
    markers= fg + bg;
    cv::namedWindow("Markers");
    cv::imshow("Markers", markers);

    // 以下進行分水嶺演算法
    WaterShedSegmentation segmenter;

    segmenter.setMarkers(markers);
    segmenter.process(image);

    // 以下是兩種處理結果,顯示分割結果
    cv::namedWindow("Segmentation");
    cv::imshow("Segmentation", segmenter.getSegmentation());

    cv::namedWindow("Watersheds");
    cv::imshow("Watersheds",segmenter.getWatersheds());

    return a.exec();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

效果1:演算法識別出屬於前景和背景的畫素(有誤差)。

這裡寫圖片描述

效果2:組合前景和背景圖,形成標記圖形,這是分水嶺的輸入引數。

這裡寫圖片描述

效果3:分割結果中,標記影象得到更新。

這裡寫圖片描述

效果4:顯示邊界影象。

這裡寫圖片描述

可以看出,分水嶺演算法對微弱邊緣具有良好的響應,是得到封閉連續邊緣的保證的。但對於不同質量的影象其分割效果不盡相同,但總的來說效果仍需要改進。

二、使用GrabCut演算法分割影象

GrabCut是另一種同樣較為流行的影象分割演算法。GrabCut是在GraphCut基礎上改進的一種影象分割演算法,它並非基於影象形態學,而是基於圖割理論(參考:http://www.cnblogs.com/tornadomeet/archive/2012/11/06/2757585.html)。在使用GrabCut時,需要人工給定一定區域的目標或者背景,然後演算法根據設定的引數來進行分割。GrabCut在計算時比分水嶺演算法更加複雜,尤其適合從靜態影象中提取前景照片的應用。
OpenCV中提供了cv::grabcut函式,因此只需提供影象並標記背景畫素和前景畫素,基於區域性的標記,演算法即可將影象中的畫素進行分割。在這裡使用的區域性標記方法是定義一個矩形。cv::grabcut的函式定義如下:

    cv::grabCut(image,    // 輸入影象
                result,   // 分割輸出結果
                rectangle,// 包含前景物體的矩形
                bgModel,fgModel, // 模型
                1,        // 迭代次數
                cv::GC_INIT_WITH_RECT); // 使用矩形進行初始化
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在main函式新增:

    // GrabCut演算法
    cv::Mat image= cv::imread("c:/Fig8.04(a).jpg");

    // 設定矩形
    cv::Rect rectangle(50,70,image.cols-150,image.rows-180);

    cv::Mat result; // 分割結果 (4種可能取值)
    cv::Mat bgModel,fgModel; // 模型(內部使用)
    // 進行GrabCut分割
    cv::grabCut(image, result, rectangle, bgModel, fgModel, 1, cv::GC_INIT_WITH_RECT); 
    // 得到可能為前景的畫素
    cv::compare(result, cv::GC_PR_FGD, result,cv::CMP_EQ);
    // 生成輸出影象
    cv::Mat foreground(image.size(), CV_8UC3, cv::Scalar(255,255,255));
    image.copyTo(foreground, result); // 不復制背景資料

    // 包含矩形的原始影象
    cv::rectangle(image, rectangle, cv::Scalar(255,255,255),1);
    cv::namedWindow("Orginal Image");
    cv::imshow("Orginal Image", image);

    // 輸出前景影象結果
    cv::namedWindow("Foreground Of Segmented Image");
    cv::imshow("Foreground Of Segmented Image", foreground);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

效果:
這裡寫圖片描述

在函式cv::grabCut中,最後一個引數表示我們使用的是包圍盒模式,而該演算法支援的輸入/輸出分割影象可以有四種數值,如函式cv::compare函式中的引數:
cv::GC_BGD:確定屬於背景的畫素;
cv::GC_FGD:確定屬於前景的元素;
cv::GC_PR_BGD:可能屬於背景的元素;
cv::GC_PR_FGD:可能屬於前景的元素。

在上圖中,GrabCut演算法通過指定方框區域來提取前景物體。同時,也可將數值cv::GC_BGD和cv::GC_FGD賦予分割影象的某些特定畫素,並且把這類分割影象作為cv::grabcut函式的第二個引數(此時需要指定GC_INIT_WITH_MASK作為輸入模式)。

基於這些資訊,GrabCut通過以下主要步驟建立分割:

  1. 前景標籤(cv::GC_PR_FGD)被臨時賦予所有為標記的畫素。基於當前的分類,演算法將畫素歸類為顏色或灰度值相似的聚類。
  2. 通過引入背景與前景畫素的邊界進行分割。這個優化的過程嘗試將標籤相似的畫素相連線,這裡利用了在強度相對已知的區域之間對邊界畫素的(懲罰?)。這個最優化問題通過GraphCut演算法得到高效解決。
  3. 對獲取的分割結果產生新的畫素標籤,重複聚類過程,找到新的最優解。根據場景的複雜度,得到最佳結果,對於簡單的場景,有時只需要一次迭代。

關於GrabCut演算法,還需要進一步研究GraphCut才能深刻理解。