【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通過以下主要步驟建立分割:
- 前景標籤(cv::GC_PR_FGD)被臨時賦予所有為標記的畫素。基於當前的分類,演算法將畫素歸類為顏色或灰度值相似的聚類。
- 通過引入背景與前景畫素的邊界進行分割。這個優化的過程嘗試將標籤相似的畫素相連線,這裡利用了在強度相對已知的區域之間對邊界畫素的(懲罰?)。這個最優化問題通過GraphCut演算法得到高效解決。
- 對獲取的分割結果產生新的畫素標籤,重複聚類過程,找到新的最優解。根據場景的複雜度,得到最佳結果,對於簡單的場景,有時只需要一次迭代。
關於GrabCut演算法,還需要進一步研究GraphCut才能深刻理解。