1. 程式人生 > >OpenCV學習筆記(14):形態學濾波對影象進行邊緣及角點檢測

OpenCV學習筆記(14):形態學濾波對影象進行邊緣及角點檢測

</pre><pre name="code" class="cpp">#include "stdafx.h"
#include<opencv2/opencv.hpp>
using namespace cv;

class MorphoFeatures{

private:
	//用於生成二值影象的閾值
	int threshold;
	//角點檢測用到的結構元素
	Mat cross;
	Mat diamond;
	Mat square;
	Mat x;

public:
	//檢測角點
	//opencv沒有直接實現它,需要定義四種不同的結構元素,
	//包括:菱形,方形,十字形,x形。並在建構函式中完成
	MorphoFeatures() :threshold(-1),
		cross(5, 5, CV_8U, Scalar(0)),
		diamond(5, 5, CV_8U, Scalar(1)),
		square(5, 5, CV_8U, Scalar(1)),
		x(5, 5, CV_8U, Scalar(0)){

		//建立十字形元素
		for (int i = 0; i < 5; i++)
		{
			cross.at<uchar>(2, i) = 1;
			cross.at<uchar>(i, 2) = 1;
		}
		//建立菱形
		diamond.at<uchar>(0, 0) = 0;
		diamond.at<uchar>(0, 1) = 0;
		diamond.at<uchar>(1, 0) = 0;
		diamond.at<uchar>(4, 4) = 0;
		diamond.at<uchar>(3, 4) = 0;
		diamond.at<uchar>(4, 3) = 0;
		diamond.at<uchar>(4, 0) = 0;
		diamond.at<uchar>(4, 1) = 0;
		diamond.at<uchar>(3, 0) = 0;
		diamond.at<uchar>(0, 4) = 0;
		diamond.at<uchar>(0, 3) = 0;
		diamond.at<uchar>(1, 4) = 0;

		//建立x形
		for (int i = 0; i < 5; i++)
		{
			x.at<char>(i, i) = 1;
			x.at<char>(4 - i, i) = 1;
		}
	}

	Mat getEdges(const Mat &image)
	{
		//得到梯度圖
		Mat result;
		//morphology函式配上合適的濾波器就可以實現直線檢測
		morphologyEx(image, result, MORPH_GRADIENT, Mat());
		applyThreshold(result);
		return result;
	}
	void setThreshold(int t)
	{
		threshold = t;
	}
	void applyThreshold(Mat & result)
	{
		if (threshold > 0)
			cv::threshold(result, result, threshold, 255, THRESH_BINARY);
	}

	//需要連線使用這些結構元素,得到最終的角點對映圖
	Mat getCorners(const Mat&image)
	{
		Mat result;
		//十字形膨脹
		dilate(image, result, cross);
		//菱形腐蝕
		erode(result, result, diamond);

		Mat result2;
		//x形膨脹
		dilate(image, result2, x);
		//方形腐蝕
		erode(result2, result2, square);

		//通過對兩張影象做差值,得到角點影象
		absdiff(result2, result, result);
		//閾值化得到二值影象
		applyThreshold(result);
		return result;
	}
	//為了視覺化,在在二值影象中每個監測點繪製一個圓
	void drawOnImage(const Mat & binary, Mat & image)
	{
		Mat_<uchar>::const_iterator it = binary.begin<uchar>();
		Mat_<uchar>::const_iterator itend = binary.end<uchar>();

		//遍歷每個畫素
		for (int i = 0; it != itend; ++i, ++it)
		{
			if (!*it)
				circle(image, Point(i%image.step, i / image.step), 5, Scalar(0, 255, 0));
				
		}
	}
};
int _tmain(int argc, _TCHAR* argv[])
{
	//直線檢測
	Mat image = imread("building.jpg", 0);
	//imshow("d", image);
	if (!image.data)
		return -1;
	MorphoFeatures morpho;
	morpho.setThreshold(40);
	Mat edges;
	edges = morpho.getEdges(image);
	imshow("d", edges);

	//角點檢測

	morpho.setThreshold(-1);
	Mat corners;
	corners = morpho.getCorners(image);
	morphologyEx(corners, corners, MORPH_TOPHAT, Mat());
	threshold(corners, corners, 40, 255, THRESH_BINARY_INV);
	//imshow("角點",corners);  

	//展示圖片上的角點  
	morpho.drawOnImage(corners, image);
	imshow("jj", image);
	waitKey(0);
	return 0;
}


首先如何理解對灰度影象進行形態學操作?

一種比較形象的方法是將灰度影象看做是“等高線”:亮的區域代表山峰,而暗的區域代表山谷,影象的邊沿就對應於峭壁。如果腐蝕一幅影象,會導致山谷被擴充套件,而峭壁減少了。相反的,如果膨脹一幅影象,峭壁則會增加。但是這兩種情況下,中間的部分(大片的谷底和高原)基本保持不變。

在上述理解的基礎上,如果我們對影象的腐蝕和膨脹的結果做差,就能提取影象的邊界:因為邊界區域,二者完全不同。(實際上,我們也可以用腐蝕或者膨脹的結果與源影象做差得出類似結果,但提取的邊界會比較細)。可以看出,結構元越大,邊界越粗。在OpenCV中,將形態學操作函式morphologyEx 的第4個引數設為MORPH_GRADIENT,就能完成上述工作。

開運算可以 用來消除小物體、在纖細點處分離物體、平滑較大物體的邊界的同時並不明顯改變其面積。

閉運算能夠排除小型黑洞(黑色區域)

形態學梯度(Morphological Gradient)為膨脹圖與腐蝕圖之差,對二值影象進行這一操作可以將團塊(blob)的邊緣突出出來。我們可以用形態學梯度來保留物體的邊緣輪廓

頂帽運算(Top Hat)為原影象與“開運算“的結果圖之差,因為開運算帶來的結果是放大了裂縫或者區域性低亮度的區域,因此,從原圖中減去開運算後的圖,得到的效果圖突出了比原圖輪廓周圍的區域更明亮的區域,且這一操作和選擇的核的大小相關。頂帽運算往往用來分離比鄰近點亮一些的斑塊。當一幅影象具有大幅的背景的時候,而微小物品比較有規律的情況下,可以使用頂帽運算進行背景提取。

黑帽(Black Hat)運算為”閉運算“的結果圖與原影象之差,黑帽運算後的效果圖突出了比原圖輪廓周圍的區域更暗的區域,且這一操作和選擇的核的大小相關。所以,黑帽運算用來分離比鄰近點暗一些的斑塊。

void cv::morphologyEx( InputArray _src, OutputArray _dst, int op,
                       InputArray kernel, Point anchor, int iterations,
                       int borderType, const Scalar& borderValue )
{
    Mat src = _src.getMat(), temp;
    _dst.create(src.size(), src.type());
    Mat dst = _dst.getMat();

    switch( op )
    {
    case MORPH_ERODE:
        erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case MORPH_DILATE:
        dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case MORPH_OPEN:
        erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
        dilate( dst, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case CV_MOP_CLOSE:
        dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
        erode( dst, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case CV_MOP_GRADIENT:
        erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
        dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
        dst -= temp;
        break;
    case CV_MOP_TOPHAT:
        if( src.data != dst.data )
            temp = dst;
        erode( src, temp, kernel, anchor, iterations, borderType, borderValue );
        dilate( temp, temp, kernel, anchor, iterations, borderType, borderValue );
        dst = src - temp;
        break;
    case CV_MOP_BLACKHAT:
        if( src.data != dst.data )
            temp = dst;
        dilate( src, temp, kernel, anchor, iterations, borderType, borderValue );
        erode( temp, temp, kernel, anchor, iterations, borderType, borderValue );
        dst = temp - src;
        break;
    default:
        CV_Error( CV_StsBadArg, "unknown morphological operation" );
    }
}

  • 第一個引數,InputArray型別的src,輸入影象,即源影象,填Mat類的物件即可。影象位深應該為以下五種之一:CV_8U, CV_16U,CV_16S, CV_32F 或CV_64F。
  • 第二個引數,OutputArray型別的dst,即目標影象,函式的輸出引數,需要和源圖片有一樣的尺寸和型別。
  • 第三個引數,int型別的op,表示形態學運算的型別,可以是如下之一的識別符號:
    • MORPH_OPEN – 開運算(Opening operation)
    • MORPH_CLOSE – 閉運算(Closing operation)
    • MORPH_GRADIENT -形態學梯度(Morphological gradient)
    • MORPH_TOPHAT - “頂帽”(“Top hat”)
    • MORPH_BLACKHAT - “黑帽”(“Black hat“)
  • 第四個引數,InputArray型別的kernel,形態學運算的核心。若為NULL時,表示的是使用參考點位於中心3x3的核。我們一般使用函式 getStructuringElement配合這個引數的使用。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或morphologyEx 函式時,kernel引數填儲存getStructuringElement返回值的Mat型別變數。對應於我們上面的示例,就是填element變數。

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