1. 程式人生 > >【OpenCV入門教程之十】 形態學影象處理(一):膨脹與腐蝕

【OpenCV入門教程之十】 形態學影象處理(一):膨脹與腐蝕

本系列文章由@淺墨_毛星雲 出品,轉載請註明出處。 

寫作當前博文時配套使用的OpenCV版本: 2.4.8


本篇文章中,我們一起探究了影象處理中,最基本的形態學運算——膨脹與腐蝕。淺墨在文章開頭友情提醒,用人物照片做腐蝕和膨脹的素材圖片得到的效果會比較驚悚,毀三觀的,不建議嘗試。。。。。。。。。。

OK,開始吧,依然是先放一張截圖:


一、理論與概念講解——從現象到本質

1.1 形態學概述

形態學(morphology)一詞通常表示生物學的一個分支,該分支主要研究動植物的形態和結構。而我們影象處理中指的形態學,往往表示的是數學形態學。下面一起來了解數學形態學的概念。

數學形態學(Mathematical morphology) 是一門建立在格論和拓撲學基礎之上的影象分析學科,是數學形態學影象處理的基本理論。其基本的運算包括:二值腐蝕和膨脹、二值開閉運算、骨架抽取、極限腐蝕、擊中擊不中變換、形態學梯度、Top-hat變換、顆粒分析、流域變換、灰值腐蝕和膨脹、灰值開閉運算、灰值形態學梯度等。

簡單來講,形態學操作就是基於形狀的一系列影象處理操作。OpenCV為進行影象的形態學變換提供了快捷、方便的函式。最基本的形態學操作有二種,他們是:膨脹與腐蝕(Dilation與Erosion)。

膨脹與腐蝕能實現多種多樣的功能,主要如下:

  • 消除噪聲
  • 分割(isolate)出獨立的影象元素,在影象中連線(join)相鄰的元素。
  • 尋找影象中的明顯的極大值區域或極小值區域
  • 求出影象的梯度

我們在這裡給出下文會用到的,用於對比膨脹與腐蝕運算的“淺墨”字樣毛筆字原圖:

 

在進行腐蝕和膨脹的講解之前,首先需要注意,腐蝕和膨脹是對白色部分(高亮部分)而言的,不是黑色部分。膨脹就是影象中的高亮部分進行膨脹,“領域擴張”,效果圖擁有比原圖更大的高亮區域。腐蝕就是原圖中的高亮部分被腐蝕,“領域被蠶食”,效果圖擁有比原圖更小的高亮區域。

1.2膨脹

其實,膨脹就是求區域性最大值的操作。

按數學方面來說,膨脹或者腐蝕操作就是將影象(或影象的一部分割槽域,我們稱之為A)與核(我們稱之為B)進行卷積。

核可以是任何的形狀和大小,它擁有一個單獨定義出來的參考點,我們稱其為錨點(anchorpoint)。多數情況下,核是一個小的中間帶有參考點和實心正方形或者圓盤,其實,我們可以把核視為模板或者掩碼。

而膨脹就是求區域性最大值的操作,核B與圖形卷積,即計算核B覆蓋的區域的畫素點的最大值,並把這個最大值賦值給參考點指定的畫素。這樣就會使影象中的高亮區域逐漸增長。如下圖所示,這就是膨脹操作的初衷。


膨脹的數學表示式:


膨脹效果圖(毛筆字):

 

照片膨脹效果圖:


1.3 腐蝕

再來看一下腐蝕,大家應該知道,膨脹和腐蝕是一對好基友,是相反的一對操作,所以腐蝕就是求區域性最小值的操作。

我們一般都會把腐蝕和膨脹對應起來理解和學習。下文就可以看到,兩者的函式原型也是基本上一樣的。

原理圖:

腐蝕的數學表示式:

 

腐蝕效果圖(毛筆字):


照片腐蝕效果圖:

 

 淺墨表示這張狗狗超可愛:D

二、深入——OpenCV原始碼分析溯源

直接上原始碼吧,在…\opencv\sources\modules\imgproc\src\ morph.cpp路徑中 的第1353行開始就為erode(腐蝕)函式的原始碼,1361行為dilate(膨脹)函式的原始碼。

//-----------------------------------【erode()函式中文註釋版原始碼】---------------------------- 
//    說明:以下程式碼為來自於計算機開源視覺庫OpenCV的官方原始碼 
//    OpenCV原始碼版本:2.4.8 
//    原始碼路徑:…\opencv\sources\modules\imgproc\src\ morph.cpp 
//    原始檔中如下程式碼的起始行數:1353行 
//    中文註釋by淺墨 
//--------------------------------------------------------------------------------------------------------  
void cv::erode( InputArray src, OutputArraydst, InputArray kernel,
                Point anchor, int iterations,
                int borderType, constScalar& borderValue )
{
//呼叫morphOp函式,並設定識別符號為MORPH_ERODE
   morphOp( MORPH_ERODE, src, dst, kernel, anchor, iterations, borderType,borderValue );
}
//-----------------------------------【dilate()函式中文註釋版原始碼】---------------------------- 
//    說明:以下程式碼為來自於計算機開源視覺庫OpenCV的官方原始碼 
//    OpenCV原始碼版本:2.4.8 
//    原始碼路徑:…\opencv\sources\modules\imgproc\src\ morph.cpp 
//    原始檔中如下程式碼的起始行數:1361行 
//    中文註釋by淺墨 
//-------------------------------------------------------------------------------------------------------- 
void cv::dilate( InputArray src,OutputArray dst, InputArray kernel,
                 Point anchor, int iterations,
                 int borderType, constScalar& borderValue )
{
//呼叫morphOp函式,並設定識別符號為MORPH_DILATE
   morphOp( MORPH_DILATE, src, dst, kernel, anchor, iterations, borderType,borderValue );
}

可以發現erode和dilate這兩個函式內部就是呼叫了一下morphOp,只是他們呼叫morphOp時,第一個引數識別符號不同,一個為MORPH_ERODE(腐蝕),一個為MORPH_DILATE(膨脹)。

morphOp函式的原始碼在…\opencv\sources\modules\imgproc\src\morph.cpp中的第1286行,有興趣的朋友們可以研究研究,這裡就不費時費力花篇幅展開分析了。

三、淺出——API函式快速上手

3.1  形態學膨脹——dilate函式

erode函式,使用畫素鄰域內的區域性極大運算子來膨脹一張圖片,從src輸入,由dst輸出。支援就地(in-place)操作。

函式原型:

C++: void dilate(
	InputArray src,
	OutputArray dst,
	InputArray kernel,
	Point anchor=Point(-1,-1),
	int iterations=1,
	int borderType=BORDER_CONSTANT,
	const Scalar& borderValue=morphologyDefaultBorderValue() 
);

引數詳解:

  • 第一個引數,InputArray型別的src,輸入影象,即源影象,填Mat類的物件即可。影象通道的數量可以是任意的,但影象深度應為CV_8U,CV_16U,CV_16S,CV_32F或 CV_64F其中之一。
  • 第二個引數,OutputArray型別的dst,即目標影象,需要和源圖片有一樣的尺寸和型別。
  • 第三個引數,InputArray型別的kernel,膨脹操作的核。若為NULL時,表示的是使用參考點位於中心3x3的核。

我們一般使用函式 getStructuringElement配合這個引數的使用。getStructuringElement函式會返回指定形狀和尺寸的結構元素(核心矩陣)。

其中,getStructuringElement函式的第一個引數表示核心的形狀,我們可以選擇如下三種形狀之一:

    • 矩形: MORPH_RECT
    • 交叉形: MORPH_CROSS
    • 橢圓形: MORPH_ELLIPSE

而getStructuringElement函式的第二和第三個引數分別是核心的尺寸以及錨點的位置。

我們一般在呼叫erode以及dilate函式之前,先定義一個Mat型別的變數來獲得getStructuringElement函式的返回值。對於錨點的位置,有預設值Point(-1,-1),表示錨點位於中心。且需要注意,十字形的element形狀唯一依賴於錨點的位置。而在其他情況下,錨點只是影響了形態學運算結果的偏移。

getStructuringElement函式相關的呼叫示例程式碼如下:

 int g_nStructElementSize = 3; //結構元素(核心矩陣)的尺寸
 
//獲取自定義核
Mat element = getStructuringElement(MORPH_RECT,
	Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),
	Point( g_nStructElementSize, g_nStructElementSize ));

呼叫這樣之後,我們便可以在接下來呼叫erode或dilate函式時,第三個引數填儲存了getStructuringElement返回值的Mat型別變數。對應於我們上面的示例,就是填element變數。

  • 第四個引數,Point型別的anchor,錨的位置,其有預設值(-1,-1),表示錨位於中心。
  • 第五個引數,int型別的iterations,迭代使用erode()函式的次數,預設值為1。
  • 第六個引數,int型別的borderType,用於推斷影象外部畫素的某種邊界模式。注意它有預設值BORDER_DEFAULT。
  • 第七個引數,const Scalar&型別的borderValue,當邊界為常數時的邊界值,有預設值morphologyDefaultBorderValue(),一般我們不用去管他。需要用到它時,可以看官方文件中的createMorphologyFilter()函式得到更詳細的解釋。

使用erode函式,一般我們只需要填前面的三個引數,後面的四個引數都有預設值。而且往往結合getStructuringElement一起使用。

呼叫範例:

       	//載入原圖 
       	Mat image = imread("1.jpg");
	//獲取自定義核
       	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       	Mat out;
       	//進行膨脹操作
       	dilate(image, out, element);

用上面核心程式碼架起來的完整程式程式碼:

//-----------------------------------【標頭檔案包含部分】---------------------------------------
//     描述:包含程式所依賴的標頭檔案
//----------------------------------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <iostream>
 
//-----------------------------------【名稱空間宣告部分】---------------------------------------
//     描述:包含程式所使用的名稱空間
//----------------------------------------------------------------------------------------------- 
using namespace std;
using namespace cv;
 
//-----------------------------------【main( )函式】--------------------------------------------
//     描述:控制檯應用程式的入口函式,我們的程式從這裡開始
//-----------------------------------------------------------------------------------------------
int main(  )
{
 
       //載入原圖 
       Mat image = imread("1.jpg");
 
       //建立視窗 
       namedWindow("【原圖】膨脹操作");
       namedWindow("【效果圖】膨脹操作");
 
       //顯示原圖
       imshow("【原圖】膨脹操作", image);
 
//獲取自定義核
       Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       Mat out;
//進行膨脹操作
       dilate(image,out, element);
 
       //顯示效果圖
       imshow("【效果圖】膨脹操作", out);
 
       waitKey(0);
 
       return 0;
}

 執行截圖:


3.2 形態學腐蝕——erode函式

erode函式,使用畫素鄰域內的區域性極小運算子來腐蝕一張圖片,從src輸入,由dst輸出。支援就地(in-place)操作。

看一下函式原型:

C++: void erode(
	InputArray src,
	OutputArray dst,
	InputArray kernel,
	Point anchor=Point(-1,-1),
	int iterations=1,
	int borderType=BORDER_CONSTANT,
	const Scalar& borderValue=morphologyDefaultBorderValue()
 );

引數詳解:

  • 第一個引數,InputArray型別的src,輸入影象,即源影象,填Mat類的物件即可。影象通道的數量可以是任意的,但影象深度應為CV_8U,CV_16U,CV_16S,CV_32F或 CV_64F其中之一。
  • 第二個引數,OutputArray型別的dst,即目標影象,需要和源圖片有一樣的尺寸和型別。
  • 第三個引數,InputArray型別的kernel,腐蝕操作的核心。若為NULL時,表示的是使用參考點位於中心3x3的核。我們一般使用函式 getStructuringElement配合這個引數的使用。getStructuringElement函式會返回指定形狀和尺寸的結構元素(核心矩陣)。(具體看上文中淺出部分dilate函式的第三個引數講解部分)
  • 第四個引數,Point型別的anchor,錨的位置,其有預設值(-1,-1),表示錨位於單位(element)的中心,我們一般不用管它。
  • 第五個引數,int型別的iterations,迭代使用erode()函式的次數,預設值為1。
  • 第六個引數,int型別的borderType,用於推斷影象外部畫素的某種邊界模式。注意它有預設值BORDER_DEFAULT。
  • 第七個引數,const Scalar&型別的borderValue,當邊界為常數時的邊界值,有預設值morphologyDefaultBorderValue(),一般我們不用去管他。需要用到它時,可以看官方文件中的createMorphologyFilter()函式得到更詳細的解釋。

同樣的,使用erode函式,一般我們只需要填前面的三個引數,後面的四個引數都有預設值。而且往往結合getStructuringElement一起使用。

呼叫範例:

       	//載入原圖 
       	Mat image = imread("1.jpg");
	//獲取自定義核
       	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       	Mat out;
       	//進行腐蝕操作
       	erode(image,out, element);

用上面核心程式碼架起來的完整程式程式碼:

//-----------------------------------【標頭檔案包含部分】---------------------------------------
//     描述:包含程式所依賴的標頭檔案
//----------------------------------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <iostream>
 
//-----------------------------------【名稱空間宣告部分】---------------------------------------
//     描述:包含程式所使用的名稱空間
//----------------------------------------------------------------------------------------------- 
using namespace std;
using namespace cv;
 
//-----------------------------------【main( )函式】--------------------------------------------
//     描述:控制檯應用程式的入口函式,我們的程式從這裡開始
//-----------------------------------------------------------------------------------------------
int main(  )
{
       //載入原圖 
       Matimage = imread("1.jpg");
 
        //建立視窗 
       namedWindow("【原圖】腐蝕操作");
       namedWindow("【效果圖】腐蝕操作");
 
       //顯示原圖
       imshow("【原圖】腐蝕操作", image);
 
        
//獲取自定義核
       Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       Mat out;
 
//進行腐蝕操作
       erode(image,out, element);
 
       //顯示效果圖
       imshow("【效果圖】腐蝕操作", out);
 
       waitKey(0);
 
       return 0;
}

執行結果:

 

四、綜合示例——在實戰中熟稔

依然是每篇文章都會配給大家的一個詳細註釋的博文配套示例程式,把這篇文章中介紹的知識點以程式碼為載體,展現給大家。

這個示例程式中的效果圖視窗有兩個滾動條,顧名思義,第一個滾動條“腐蝕/膨脹”用於在腐蝕/膨脹之間進行切換;第二個滾動條”核心尺寸”用於調節形態學操作時的核心尺寸,以得到效果不同的影象,有一定的可玩性。廢話不多說,上程式碼吧:

 
//-----------------------------------【程式說明】----------------------------------------------
//            程式名稱::《【OpenCV入門教程之十】形態學影象處理(一):膨脹與腐蝕  》 博文配套原始碼
//            開發所用IDE版本:Visual Studio 2010
//          開發所用OpenCV版本: 2.4.8
//            2014年4月14日 Create by 淺墨
//            淺墨的微博:@淺墨_毛星雲
//------------------------------------------------------------------------------------------------
 
//-----------------------------------【標頭檔案包含部分】---------------------------------------
//            描述:包含程式所依賴的標頭檔案
//----------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <iostream>
 
//-----------------------------------【名稱空間宣告部分】---------------------------------------
//            描述:包含程式所使用的名稱空間
//-----------------------------------------------------------------------------------------------
using namespace std;
using namespace cv;
 
 
//-----------------------------------【全域性變數宣告部分】--------------------------------------
//            描述:全域性變數宣告
//-----------------------------------------------------------------------------------------------
Mat g_srcImage, g_dstImage;//原始圖和效果圖
int g_nTrackbarNumer = 0;//0表示腐蝕erode, 1表示膨脹dilate
int g_nStructElementSize = 3; //結構元素(核心矩陣)的尺寸
 
 
//-----------------------------------【全域性函式宣告部分】--------------------------------------
//            描述:全域性函式宣告
//-----------------------------------------------------------------------------------------------
void Process();//膨脹和腐蝕的處理函式
void on_TrackbarNumChange(int, void *);//回撥函式
void on_ElementSizeChange(int, void *);//回撥函式
 
 
//-----------------------------------【main( )函式】--------------------------------------------
//            描述:控制檯應用程式的入口函式,我們的程式從這裡開始
//-----------------------------------------------------------------------------------------------
int main( )
{
       //改變console字型顏色
       system("color5E"); 
 
       //載入原圖
       g_srcImage= imread("1.jpg");
       if(!g_srcImage.data ) { printf("Oh,no,讀取srcImage錯誤~!\n"); return false; }
      
       //顯示原始圖
       namedWindow("【原始圖】");
       imshow("【原始圖】", g_srcImage);
      
       //進行初次腐蝕操作並顯示效果圖
       namedWindow("【效果圖】");
       //獲取自定義核
       Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),Point( g_nStructElementSize, g_nStructElementSize ));
       erode(g_srcImage,g_dstImage, element);
       imshow("【效果圖】", g_dstImage);
 
       //建立軌跡條
       createTrackbar("腐蝕/膨脹", "【效果圖】", &g_nTrackbarNumer, 1, on_TrackbarNumChange);
       createTrackbar("核心尺寸", "【效果圖】",&g_nStructElementSize, 21, on_ElementSizeChange);
 
       //輸出一些幫助資訊
       cout<<endl<<"\t嗯。執行成功,請調整滾動條觀察影象效果~\n\n"
              <<"\t按下“q”鍵時,程式退出~!\n"
              <<"\n\n\t\t\t\tby淺墨";
 
       //輪詢獲取按鍵資訊,若下q鍵,程式退出
       while(char(waitKey(1))!= 'q') {}
 
       return 0;
}
 
//-----------------------------【Process( )函式】------------------------------------
//            描述:進行自定義的腐蝕和膨脹操作
//-----------------------------------------------------------------------------------------
void Process()
{
       //獲取自定義核
       Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),Point( g_nStructElementSize, g_nStructElementSize ));
 
       //進行腐蝕或膨脹操作
       if(g_nTrackbarNumer== 0) {   
              erode(g_srcImage,g_dstImage, element);
       }
       else{
              dilate(g_srcImage,g_dstImage, element);
       }
 
       //顯示效果圖
       imshow("【效果圖】", g_dstImage);
}
 
 
//-----------------------------【on_TrackbarNumChange( )函式】------------------------------------
//            描述:腐蝕和膨脹之間切換開關的回撥函式
//-----------------------------------------------------------------------------------------------------
void on_TrackbarNumChange(int, void *)
{
       //腐蝕和膨脹之間效果已經切換,回撥函式體內需呼叫一次Process函式,使改變後的效果立即生效並顯示出來
       Process();
}
 
 
//-----------------------------【on_ElementSizeChange( )函式】-------------------------------------
//            描述:腐蝕和膨脹操作核心改變時的回撥函式
//-----------------------------------------------------------------------------------------------------
void on_ElementSizeChange(int, void *)
{
       //核心尺寸已改變,回撥函式體內需呼叫一次Process函式,使改變後的效果立即生效並顯示出來
       Process();
}


放出一些效果圖吧。原始圖:

 

膨脹效果圖:

 



腐蝕效果圖:




腐蝕和膨脹得到的圖,都特有喜感,但千變萬變,還是原圖好看:


OK,就放出這些吧,具體更多的執行效果大家就自己下載示例程式回去玩吧。

本篇文章到這裡就基本結束了,最後放出文章配套示例程式的打包下載地址。

本篇文章的配套原始碼請點選這裡下載:

OK,今天的內容大概就是這些,我們下篇文章見:)