1. 程式人生 > >【OpenCV影象處理】二十三、影象邊緣檢測(下)

【OpenCV影象處理】二十三、影象邊緣檢測(下)

(1)Prewitt邊緣檢測運算元

→prewitt邊緣檢測運算元是另一種常用的一階邊緣檢測運算元,這個運算元對於噪聲有抑制的作用。

Prewittt邊緣檢測的原理和Sobel邊緣檢測類似,都是在影象空間利用兩個方向模板與影象進行鄰域卷積來完成的,分別對水平和垂直方向邊緣進行檢測。對比其他邊緣檢測運算元,Prewitt運算元對邊緣的定位精度不如Roberts運算元,實現方法與Sobel運算元類似,但是實現功能差距很大,Sobel運算元對邊緣檢測的準確性更優於Prewitt運算元

→對於原始影象f(x,y),Prewitt邊緣檢測輸出影象為G,影象Prewitt邊緣檢測可由下式表示


對於最後輸出的邊緣影象,可以根據G = max(Gx,Gy)或 G = Gx + Gy來得到,凡是灰度值大於或等於閾值的畫素點就認為是邊緣點,也就是選擇適當的閾值T,若G≥T,則對應畫素點為邊緣點。根據上面的公式可知Prewitt運算元模板為:


Prewitt邊緣檢測程式碼如下所示:

//使用Prewitt進行邊緣檢測
#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>

using namespace cv;
using namespace std;

//prewitt邊緣檢測運算元實現
Mat prewitts(Mat srcImage, bool vecFlags);

int main()
{
	Mat srcImage = imread("2345.jpg", 0);
	if (!srcImage.data)
	{
		cout << "讀入圖片錯誤!" << endl;
		system("pause");
		return -1;
	}
	Mat dstImage = prewitts(srcImage,0);
	imshow("原影象", srcImage);
	imshow("Prewitt邊緣影象", dstImage);
	waitKey();
	return 0;
}

//prewitt邊緣檢測運算元實現
Mat prewitts(Mat srcImage, bool vecFlags = false)
{
	srcImage.convertTo(srcImage, CV_32FC1); 
	Mat prewitt_kernel = (Mat_<float>(3, 3) <<
		0.1667, 0.1667, 0.1667,
		0, 0, 0,
		-0.1667, -0.1667, -0.1667);
	//垂直邊緣
	if (vecFlags)
	{
		prewitt_kernel = prewitt_kernel.t();	//轉置操作
		Mat z1 = Mat::zeros(srcImage.rows, 1,CV_32FC1);
		Mat z2 = Mat::zeros(1, srcImage.cols, CV_32FC1);
		//將影象的四邊設定為0
		z1.copyTo(srcImage.col(0));
		z1.copyTo(srcImage.col(srcImage.cols - 1));
		z2.copyTo(srcImage.row(0));
		z2.copyTo(srcImage.row(srcImage.rows - 1));
	}
	Mat edges;
	filter2D(srcImage, edges, srcImage.type(), prewitt_kernel);
	Mat mag;	//梯度幅度
	multiply(edges, edges, mag);
	//除去垂直邊緣邊界黑邊
	if (vecFlags)
	{
		Mat black_region = srcImage < 0.03;
		Mat se = Mat::ones(5, 5, CV_8UC1);
		dilate(black_region, black_region, se);
		mag.setTo(0, black_region);
	}

	//根據模長計算出梯度的閾值
	double thresh = 4.0f*mean(mag).val[0];
	//僅在某點梯度大於水平方向或垂直方向的鄰點梯度時
	//才設該位置的輸出為255
	//並應用閾值thresh
	Mat dstImage = Mat::zeros(mag.size(), mag.type());
	float* dptr = (float*)mag.data;
	float* tptr = (float*)dstImage.data;
	int rowsNum = edges.rows;
	int colsNum = edges.cols;
	for (int i = 1; i < rowsNum - 1; i++)
	{
		for (int j = 1; j < colsNum - 1; j++)
		{
			//非極大值抑制
			bool b1 = (dptr[i*colsNum + j]>dptr[i*colsNum + j - 1]);
			bool b2 = (dptr[i*colsNum + j]>dptr[i*colsNum + j + 1]);
			bool b3 = (dptr[i*colsNum + j] > dptr[(i - 1)*colsNum + j]);
			bool b4 = (dptr[i*colsNum + j] > dptr[(i + 1)*colsNum + j]);
			tptr[i*colsNum + j] = 255 * ((dptr[i*colsNum + j] > thresh) &&
				((b1 && b2) || (b3 && b4)));
		}
	}
	dstImage.convertTo(dstImage, CV_8UC1);
	return dstImage;
}

執行程式後的結果如下所示:


(3)方向運算元

→前面提到,一節邊緣檢測運算元主要有兩種,一種是梯度運算元,另一種就是方向運算元。在這裡簡單介紹一下方向運算元。

→方向運算元使用一組方向差分模板與影象進行模板卷積。

→在影象中的同一位置計算多個方向上的一階差分,每一個模板對應某個特定方向的邊緣有最大響應。

→對於影象中的每一個畫素,方向運算元選取全部模板中最大相應幅度的方向作為該畫素的邊緣方向。

方向運算元能夠檢測多個方向的邊緣。但也同時增加了計算量。

→使用八個不同方向的一階差分模板來確定梯度的幅度和方向,方向之間夾角為45°。模板係數沿逆時針依次迴圈移位。

Krisch方向運算元如下:


→需要說明的是:

方向差分模板的邊緣檢測具有較大的靈活性,根據不同影象和不同處理目的,可以設計任意角度,任意方向的差分模板。

(3)拉普拉斯邊緣檢測運算元

→拉普拉斯運算元是最簡單的各向同性二階微分運算元,具有旋轉不變性。根據函式微分特性,該畫素點值得二階微分為0的點為邊緣點,對於二維影象函式f(x,y),影象的Laplace運算二階導數定義為:


對於而為離散影象而言,影象的Laplace可以表示為下式:


根據離散Laplace的表示式,可以得到其模板的表現形式:


G1與G2分別為離散拉普拉斯運算元的模板與擴充套件模板,利用函式模板可以將影象中的奇異點如亮點變得更亮。對於影象中灰度變化劇烈的區域,拉普拉斯運算元能夠實現其邊緣檢測。拉普拉斯運算元利用二次微分特性與峰值間的過零點來確定邊緣的位置,對奇異點或邊界點更為敏感,常應用於影象銳化處理中。

影象的銳化操作的主要目的是突出影象的細節或增強被模糊的影象細節,可以實現灰度反差增強,同事使影象變得更為清晰。微分運算可以實現影象細節的突出,積分運算或加權平均可以使影象變得模糊。針對原影象f(x,y),銳化操作可以通過拉普拉斯運算元隨源影象進行處理,進行微分運算操作後產生描述灰度圖編的影象,再將拉普拉普影象與原始影象疊加進而產生銳化影象。影象的拉普拉斯銳化可以由下式表示:


其中t為鄰域中心比較係數,拉普拉斯運算元銳化操作通過比較鄰域的中心畫素與它所在鄰域內的其他畫素的平均灰度來確定相應的變換方式。

當t≦0時,中心畫素的灰度被進一步降低;相反,t>0時,中心畫素的灰度被進一步提高。

在OpenCV中,實現Laplace邊緣檢測使用的是Laplace()函式,該函式的宣告如下所示:

void Laplacian( InputArray src, OutputArray dst, int ddepth,int ksize=1, double scale=1, 
                double delta=0, int borderType=BORDER_DEFAULT );

第一個引數為輸入影象src,使用Mat類的物件即可,而且需要時單通道的8點陣圖像。

第二個引數是輸出的邊緣影象dst,需要和源影象有一樣的尺寸和通道數。

第三個引數是int型別的ddepth,是指目標影象的深度

第四個引數是int型別的ksize,用於計算二階導數的濾波器的孔徑大小,大小必須是正奇數,而且有預設值1.

第五個引數是double型別的scale,計算拉普拉斯值得時候可以選的比例因子,有預設值1。

第六個引數是double型別的delta,表示在結果存入目標圖(第二個引數dst)之前可選的delta的值,有預設值0

第七個引數是int型別的bordertype,表示邊界模式,有預設值BORDER_DEFAULT

需要說明的是,Laplacian()函式其實主要是利用sobel運算元的運算。它通過加上sobel運算元運算出的影象x方向和y方向上的倒數,來得到輸入影象的拉普拉斯變換結果

使用Laplace邊緣檢測的相關實現如下所示:

//使用Laplace運算元實現邊緣檢測
#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>

using namespace cv;
using namespace std;

int main()
{
	Mat srcImage = imread("2345.jpg", 0);
	if (!srcImage.data)
	{
		cout << "讀入圖片錯誤!" << endl;
		system("pause");
		return -1;
	}
	//先對影象進行高斯平滑
	Mat GBdstImage;
	GaussianBlur(srcImage, GBdstImage, Size(3, 3),0,0,BORDER_DEFAULT);
	Mat dstImage;
	//拉普拉斯變換
	Laplacian(GBdstImage, dstImage, CV_16S, 3);
	convertScaleAbs(dstImage, dstImage);
	imshow("拉普拉斯變換前", GBdstImage);
	imshow("拉普拉斯變換後", dstImage);
	waitKey();
	return 0;
}

程式執行後的效果如下所示:


通過觀察輸出影象可以看出,利用拉普拉斯運算元檢測出的邊緣是雙邊緣。

需要說明的是,程式中在對影象進行拉普拉斯邊緣檢測前,首先對影象進行高斯濾波,是因為二階差分運算元對噪聲具有敏感性,高斯濾波的作用是對影象進行平滑處理,從而達到降噪的目的。高斯函式中的標準差的選取起著關鍵性的作用,對邊緣檢測結果有很大的影響。標準差取得越大,平滑能力越強,對噪聲的抑制能力越強,避免了虛假邊緣的額檢出,但是同時也模糊了影象的邊緣。

從原理上講,先進行高斯濾波再進行拉普拉斯邊緣檢測 與直接應用LOG運算元對影象進行邊緣檢測相同。

(4)Canny運算元

→Canny運算元是一種有效的邊緣檢測運算元,是滿足一定約束條件下推匯出的邊緣檢測運算元。

→具體方法可以描述為以下的4個步驟

(① 高斯影象平滑

→為了抑制噪聲,利用高斯函式對影象進行平滑處理,用公式可以描述為:


(②基於梯度的邊緣檢測

→利用Sobel運算元計算每一個畫素(x, y)處的區域性梯度幅度▽g(x,y),

幅度表示為:


以及梯度的方向角為:


(③梯度幅度的非極大值抑制

→追蹤梯度幅度▽g(x,y)中所有脊的頂部並將所有不再脊頂部的畫素置為0,保留區域性梯度最大值點。而一直非極大值,從而形成但畫素寬的邊緣

→關於非極大值抑制的實現方法:

→→利用梯度的方向,將梯度方向角量化到如下圖所示的4個扇區,4個扇區的標號為0~3,分別對應3x3鄰域內可能的4中畫素的組合

 

→→遍歷梯度影象中每一個畫素,將該畫素與鄰域內沿著梯度方向的2個畫素進行比較,若該畫素梯度幅度的2個畫素進行比較,若該畫素梯度幅度不大於鄰域內沿梯度方向的兩個相鄰畫素的梯度幅度,則將其置為0

(④雙閾值法邊緣檢測和連線

→雙閾值法設定兩個不同的閾值T1和T2,其中T1<T2,幅度大於T2位強邊緣,在T1,T2之間為若邊緣,分別使用這兩個閾值對影象進行二值化,高閾值的二值影象幾乎沒有虛假邊緣,但是邊緣會出現間斷,不完整,低閾值的二值影象邊緣完整,但是有較多的虛假邊緣。

→→所以,雙閾值演算法是在高閾值的二值影象中將強邊緣連線成為輪廓,當到達間斷點時,在地獄之影象中的8鄰域內尋找可以連線強邊緣的若邊緣畫素,直到將強邊緣連線起來為止。

上面的步驟用程式可以分別進行實現,下面給出程式具體的進行介紹

//實現canny邊緣檢測
#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>

using namespace cv;
using namespace std;

void nonMaximumSuppression(Mat &magnitudeImage, Mat &directionImage); 
void followEdge(int x, int y, Mat &magnitude, int tUpper, int tLower, Mat &Edges);
void edgeDetect(Mat &magnitude, int tUpper, int tLower, Mat &edges);

int main()
{
	Mat srcImage = imread("2345.jpg", 0);
	if (!srcImage.data)
	{
		cout << "讀入圖片錯誤!" << endl;
		system("pause");
		return -1;
	}
	//首先進行高斯濾波,進行消除噪聲
	Mat GaussImage;
	GaussianBlur(srcImage, GaussImage, Size(3, 3), 1.5);
	//使用Sobel計算相應的梯度幅值以及方向
	Mat magX = Mat(srcImage.rows, srcImage.cols, CV_32F);
	Mat magY = Mat(srcImage.rows, srcImage.cols, CV_32F);
	Sobel(GaussImage, magX, CV_32F, 1, 0, 3);
	Sobel(GaussImage, magY, CV_32F, 0, 1, 3);
	//計算斜率
	Mat slopes = Mat(GaussImage.rows, GaussImage.cols, CV_32F);
	//矩陣逐元素的除法,注意與A/B相區別
	divide(magY, magX, slopes);
	//計算每個點的梯度
	Mat sum = Mat(GaussImage.rows, GaussImage.cols, CV_64F);
	Mat prodX = Mat(GaussImage.rows, GaussImage.cols, CV_64F);
	Mat prodY = Mat(GaussImage.rows, GaussImage.cols, CV_64F);
	multiply(magX, magX, prodX);
	multiply(magY, magY, prodY);
	sum = prodX + prodY;
	sqrt(sum, sum);
	Mat magnitudeS = sum.clone();
	Mat edges;
	nonMaximumSuppression(magnitudeS, slopes);
	edgeDetect(magnitudeS, 150,30, edges);
	imshow("邊緣影象", edges);
	waitKey();
	return 0;
}

//非極大值抑制是進行Canny邊緣檢測的重要步驟。
//通俗意義上是指尋找畫素點區域性的最大值,將非極大值點對應的灰度值設定為背景畫素點
//畫素鄰域區域滿足梯度值得區域性最優值判斷為該畫素的邊緣
//對其餘非極大值的相關資訊進行抑制
//利用這個準則可以剔除大部分非邊緣點
//這一部分主要是排除非邊緣畫素,僅僅儲存候選影象邊緣
void nonMaximumSuppression(Mat &magnitudeImage, Mat &directionImage)
{
	Mat checkImage(magnitudeImage.rows, magnitudeImage.cols, CV_8U);
	//迭代器進行初始化
	MatIterator_<float>itMag = magnitudeImage.begin<float>();
	MatIterator_<float>itDirection = directionImage.begin<float>();
	MatIterator_<uchar>itRet = checkImage.begin<uchar>();
	MatIterator_<float>itEndMag = magnitudeImage.end<float>();
	//進行遍歷,計算對應的方向
	for (; itMag != itEndMag; ++itDirection, ++itRet, ++itMag)
	{
		//將方向進行劃分,對每個方向進行幅值判斷
		const Point pos = itRet.pos();
		float currenttDirection = atan(*itDirection)*(180 / CV_PI);
		while (currenttDirection < 0)
			currenttDirection += 180;
		*itDirection = currenttDirection;
		//邊界限定,對應方向進行判斷
		if (currenttDirection > 22.5 && currenttDirection <= 67.5)
		{
			//判斷鄰域位置極值
			if (pos.y > 0 && pos.x > 0 && *itMag <=
				magnitudeImage.at<float>(pos.y - 1, pos.x - 1))
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
			if (pos.y < magnitudeImage.rows - 1 && pos.x < magnitudeImage.cols - 1
				&& *itMag <= magnitudeImage.at<float>(pos.y + 1, pos.x + 1))
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
		}
		else if (currenttDirection > 67.5 && currenttDirection < 112.5)
		{
			//判斷鄰域位置極值
			if (pos.y >0 && *itMag <= magnitudeImage.at<float>
				(pos.y - 1, pos.x))
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
			if (pos.y < magnitudeImage.rows - 1 && *itMag <=
				magnitudeImage.at<float>(pos.y + 1, pos.x));
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
		}
		else if (currenttDirection >112.5 && currenttDirection <= 157.5)
		{
			//判斷鄰域位置極值
			if (pos.y > 0 && pos.x < magnitudeImage.at<float>
				(pos.y - 1, pos.x + 1))
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
			if (pos.y < magnitudeImage.rows - 1 && pos.x >0 &&
				*itMag <= magnitudeImage.at<float>
				(pos.y - 1, pos.x - 1))
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
		}
		else
		{
			//判斷鄰域位置極值
			if (pos.x > 0 && *itMag <= magnitudeImage.at<float>
				(pos.y, pos.x - 1))
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
			//判定極值點
			if (pos.x < magnitudeImage.cols - 1 && * itMag <=
				magnitudeImage.at<float>(pos.y, pos.x + 1))
			{
				magnitudeImage.at<float>(pos.y, pos.x) = 0;
			}
		}
	}

}


//滯後閾值邊緣連線
//因為上一步得到的疑似邊緣中存在著偽邊緣,由於單閾值方法處理邊緣操作較難
//Canny演算法中減少偽邊緣的方法是採用滯後閾值法
//滯後閾值需要高閾值和低閾值,在進行邊緣檢測時依據下面的步驟:
//1、如果某一畫素位置的幅值超過高閾值,則這個畫素被保留為邊緣畫素
//2、如果某一畫素位置的幅值小於低閾值,該畫素被排除
//3、如果某一畫素位置的幅值在兩個閾值之間,該畫素僅僅在連線到一個高閾值的畫素時被保留
//這種方法使得檢測出的影象去除了大部分噪聲,但是也損失了有用的邊緣資訊
//低閾值檢測得到的影象則保留著較多的邊緣資訊,推薦的高與低閾值比在2:1到3:1之間

void followEdge(int x, int y, Mat &magnitude, int tUpper, int tLower, Mat &Edges)
{
	Edges.at<float>(y, x) = 255;
	//進行滑窗遍歷
	for (int i = -1; i < 2; i++)
	{
		for (int j = -1; j < 2; j++)
		{
			//邊界限制
			if ((i != 0) && (j != 0) && (x + i >= 0) &&
				(y + j) >= 0 && (x + i <= magnitude.cols) &&
				(y + j) <= magnitude.rows)
			{
				//梯度幅值邊緣判斷及連線
				if ((magnitude.at<float>(y + j, x + i)>tLower) &&
					(Edges.at<float>(y + j, x + i) != 255))
				{
					//巢狀呼叫,完成邊緣連結
					followEdge(x + i, y + j, magnitude, tUpper, tLower, Edges);
				}
			}
		}
	}
}

//邊緣檢測
void edgeDetect(Mat &magnitude, int tUpper, int tLower, Mat &edges)
{
	//獲取梯度幅值相關資訊
	int rows = magnitude.rows;
	int cols = magnitude.cols;
	edges = Mat(magnitude.size(), CV_32F, 0.0);
	for (int x = 0; x < cols; x++)
	{
		for (int y = 0; y < rows; y++)
		{
			//判斷梯度幅值
			if (magnitude.at<float>(y, x) >= tUpper)
			{
				//雙閾值進行邊緣連線
				followEdge(x, y, magnitude, tUpper, tLower, edges);
			}
		}
	}
}