【OpenCV影象處理】二十二、影象邊緣檢測(上)
→影象邊緣檢測的目的是檢測鄰域內灰度明顯變化的畫素,常用一階差分和二階差分來進行邊緣檢測
→數字影象中的邊緣是由鄰域內灰度值明顯變化的畫素構成,邊緣檢測主要是影象灰度的度量檢測和定位
→影象的邊緣有方向和幅值兩個屬性,沿邊緣方向畫素灰度值變化平緩或不發生變化,而垂直於邊緣方向畫素灰度值變化劇烈
→需要理解的是,邊緣是灰度值變化的產物,可以利用差分來檢測這種不連續性,邊緣檢測方法大致可以分為兩類:
(1)基於一階差分的方法
(2)基於二階差分的方法
→一階差分運算元通過尋找一階差分中的最大值來檢測邊緣,將邊緣定位在一階差分最大的方向
→二階差分運算元通過尋找二階差分過零點來定位邊緣,常用的有Laplace過零點等
在實際影象中,影象的邊緣部分通常具有一定的斜坡面,斜坡部分與邊緣模糊程度成比例
→邊緣的寬度取決於其是灰度值到終止灰度值的斜面長度,而斜面長度取決於模糊程度
灰度不連續檢測(間斷檢測)是最普遍的邊緣檢測方法:
在邊緣檢測中,梯度運算元是常用的一階差分運算元,用於檢測影象中邊緣的存在和強度,Laplace運算元是常用的二階差分運算元,二階差分在邊緣亮的一邊符號為負,在邊緣暗的一邊符號為正。而在邊緣處,二階差分穿過零點
應該注意的是,差分操作對噪聲十分敏感
→對於有噪邊緣,利用差分檢測影象邊緣將放大噪聲的影響,因此在利用差分進行邊緣檢測時,應該謹慎的考慮噪聲的影響,通常在進行邊緣檢測之前對影象進行去噪或降噪處理
→利用一階差分模板檢測影象邊緣實際上是利用一種區域性處理的方法,當某一畫素鄰域內的灰度值的一階差分大於閾設值時,則判定該畫素為邊緣畫素。
→利用一階差分模板提取的邊緣影象是由許多不連續的邊緣組成,這些邊緣畫素勾畫出各個物體的輪廓,但是不能形成影象分割所需的閉合且連通的邊界。
邊緣連線:
是指將鄰近的邊緣畫素連線起來,從而產生一條閉合且連通邊緣的過程。根據事先定義的連結準則,對這樣的邊緣畫素進行邊緣連線處理,天不由於噪聲和陰影造成的間斷。
→利用二階差分模板檢測影象邊緣實際上是尋找二階差分過零點,二階差分模板產生連續的邊緣,但是不能保證檢測的邊緣是準確的,只能檢測邊緣的大致形狀。
下面首先介紹一階差分運算元,大致可以分為兩類,分別是梯度運算元
(1)梯度運算元
梯度運算元定義在二維一階導數的基礎上,在數字影象中,由於畫素是離散的,因此常用差分來近似偏導數
→對於一幅數字影象f(x,y),在畫素(x,y)處梯度定義為:
向量
其中,Gx(x, y)和Gy(x, y)分別表示x(垂直)和y(水平)方向上的一階差分
在邊緣檢測中,一個重要的量是梯度的幅度,用公式可以表示為
上式中,mag{·}表示求幅度的函式,但是為了降低複雜度,避免進行平方和開放運算,所以求幅度的公式通常寫為如下形式:
使用上面這種絕對和計算簡單而且保持了灰度的相對變化
→但是,也導致了梯度運算元不具備各向同性→也就是不具備旋轉不變性,梯度的方向指向畫素值f(x, y)在(x,y)處增加最快的方向。
梯度關於x軸的角度為:
上式表示的是畫素(x,y)處梯度關於x軸的方向角,這一點的梯度方向和該點邊緣方向垂直
→梯度運算元檢測灰度值變化的兩個屬性是灰度值的變化率和方向,分別用幅度和方向來表示
→梯度的計算需要在每一個畫素位置計算兩個方向的一階差分,常用的一階差分運算元有Robberts,Prewitt和Sobel運算元等
在介紹各個運算元之前,先給出一個3x3畫素領域的定義,如下圖所示:
首先簡單介紹一下Robberts交叉運算元,這是最簡單的梯度運算元,
在中心畫素x(垂直)方向上,一階差分計算式為
Gx = z9 - z5
在中心畫素y(水平)方向上,一階差分計算式為
Gy = z8 - z6
需要說明的是,Robbert交叉運算元沒有固定的中心點,因此不能使用2x2的模板卷積來實現
對於原始影象f(x,y),Roberts邊緣檢測輸出影象為g(x,y),影象的Roberts邊緣檢測可以用下式來表示:
具體實現程式如下所示:
//實現Roberts邊緣檢測
#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
using namespace cv;
using namespace std;
//函式:Roberts運算元實現
Mat Roberts(Mat srcImage);
int main()
{
Mat srcImage = imread("2345.jpg",0);
if (!srcImage.data)
{
cout << "讀入圖片錯誤!" << endl;
system("pause");
return -1;
}
imshow("原影象", srcImage);
Mat dstImage = Roberts(srcImage);
imshow("Roberts邊緣檢測影象", dstImage);
waitKey();
return 0;
}
//函式:Roberts運算元實現
Mat Roberts(Mat srcImage)
{
Mat dstImage = srcImage.clone();
int rowsNum = srcImage.rows;
int colsNum = srcImage.cols;
for (int i = 0; i < rowsNum - 1; i++)
{
for (int j = 0; j < colsNum - 1; j++)
{
//根據公式進行計算
int t1 = (srcImage.at<uchar>(i, j) -
srcImage.at<uchar>(i + 1, j + 1))*
(srcImage.at<uchar>(i, j) -
srcImage.at<uchar>(i + 1, j + 1));
int t2 = (srcImage.at<uchar>(i + 1, j) -
srcImage.at<uchar>(i, j + 1))*
(srcImage.at<uchar>(i + 1, j) -
srcImage.at<uchar>(i, j + 1));
//計算對角線畫素差
dstImage.at<uchar>(i, j) = (uchar)sqrt(t1 + t2);
}
}
return dstImage;
}
程式執行後的結果如下所示:
→在實際使用中,常常使用Prewitt運算元和Sobel運算元來當做梯度運算元。
下面首先介紹利用影象差分運算進行邊緣檢測,然後再分別介紹各種邊緣檢測運算元,包括一階運算元和二階運算元
(1)影象差分運算:
對於原函式f(u),積分運算使計算f(u)對映到 f(u+a) - f(u+b)的值,差分運算分為前向差分和後向差分,一階前向差分是指Δf = f(u+1) - f(u),一階逆向差分是指Δf = f(u) - f(u-1)
二維離散影象f(x,y)在x方向的一階差分定義為Δfx = f(x+1, y) - f(x, y),y方向的一階差分定義為Δfy = f(x, y+1) - f(x, y)
差分運算通過求影象灰度變化劇烈處的一階微分運算元的極值來檢測奇異點,通過奇異點的值進一步設定閾值就可以得到邊緣二值化影象。
差分邊緣檢測中差分的水平或垂直方向都與邊緣方向正交,因此在實際應用場景中,常常將邊緣檢測分為水平邊緣,垂直邊緣和對角線邊緣,差分邊緣檢測定義方向模板如下所示:
垂直邊緣水平邊緣 對角線邊緣
利用OpenCV實現差分邊緣檢測例項如下所示:
//實現差分邊緣檢測
#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
using namespace cv;
using namespace std;
//影象差分函式
void diffOperation(const Mat srcImage, Mat &edgeXImage, Mat &edgeYImage);
int main()
{
Mat srcImage = imread("2345.jpg", 0);
if (!srcImage.data)
{
cout << "讀入圖片錯誤!" << endl;
system("pause");
return -1;
}
imshow("原影象", srcImage);
Mat edgeXImage(srcImage.size(), srcImage.type());
Mat edgeYImage(srcImage.size(), srcImage.type());
//計算差分影象
diffOperation(srcImage, edgeXImage, edgeYImage);
imshow("垂直方向差分影象", edgeXImage);
imshow("水平方向差分影象", edgeYImage);
Mat edgeImage(srcImage.size(), srcImage.type());
//將水平與垂直邊緣進行疊加
addWeighted(edgeXImage, 0.5, edgeYImage, 0.5, 0.0, edgeImage);
imshow("水平和垂直方向邊緣", edgeImage);
waitKey();
return 0;
}
void diffOperation(const Mat srcImage, Mat &edgeXImage, Mat &edgeYImage)
{
Mat tempImage = srcImage.clone();
int rowsNum = tempImage.rows;
int colsNum = tempImage.cols;
for (int i = 0; i < rowsNum - 1; i++)
{
for (int j = 0; j < colsNum - 1; j++)
{
//計算垂直邊緣
edgeXImage.at<uchar>(i, j) =
abs(tempImage.at<uchar>(i + 1, j) - tempImage.at<uchar>(i, j));
//計算水平邊緣
edgeYImage.at<uchar>(i, j) =
abs(tempImage.at<uchar>(i, j + 1) - tempImage.at<uchar>(i, j));
}
}
}
執行程式後,效果如下圖所示:
(2)非極大值抑制
在介紹其餘的邊緣檢測運算元之前,首先簡單介紹一下影象中的非極大值抑制。
影象梯度矩陣中的元素值越大,說明影象中該點的梯度值越大,但是這並不能將它判斷為該點處的邊緣。
非極大值抑制操作可以提出為偽邊緣資訊,被廣泛應用於影象邊緣檢測中,其原理是通過畫素鄰域的區域性最優值,將非極大值點所對應的灰度值設定為背景畫素點,如畫素鄰域區域滿足梯度值區域性最優值則判斷為該畫素的邊緣,對其餘非極大值的相關資訊進行抑制,利用這個準則可以剔除大部分非邊緣點。
(3)Sobel邊緣檢測運算元
Sobel運算元是應用廣泛的離散微分運算元之一,常常用於影象處理中的邊緣檢測,計算影象灰度函式的近似梯度。利用影象畫素點Sobel運算元計算出相應的地圖向量及向量的範數,基於影象卷積來實現在水平方向與垂直方向檢測對應方向的邊緣。對於原影象與奇數Sobel水平核Gx,垂直核Gy進行卷積可計算水平與垂直變換,當核心大小為3x3時,Gx與Gy為下式:
對影象中沒一點結合卷積後的結果求出近似梯度幅度G:
G= √(Gx^2 + Gy^2)
Sobel運算元在進行邊緣檢測時效率較高,當對精度要求不是很高時,是一種比較常用的邊緣檢測方法。Sobel運算元對於沿著x軸和y軸的排列的邊緣表示的較好,但是對與其他角度的表示取不夠精確,這時候可以使用Scharr濾波器。這種濾波器的水平與垂直的核因子如下:
首先給出使用OpenCV中的自帶函式庫實現Sobel邊緣檢測,
在OpenCV中實現相關功能使用函式Sobel(),函式的宣告和說明如下所示:
void Sobel( InputArray src, OutputArray dst, int ddepth,int dx, int dy, int ksize=3,
double scale=1, double delta=0, int borderType=BORDER_DEFAULT );
第一個和第二個引數分別為源影象和輸出影象
第三個引數為int型別的ddepth,為輸出影象的深度,支援src.depth()和ddepth的組合:
(·若src.depth() = CV_8U,則取ddepth = -1 / CV_16S / CV_32F / CV_64F
(·若src.depth() = CV_16U / CV_16S ,則ddepth = -1 /CV_32F / CV_64F
(·若src.depth() = CV_32F,取ddepth = -1 /CV_32F / CV_64F
(·若src.depth() = CV_64F,取ddepth = -1 / CV_64F
第四個引數為int型別的dx,為x方向上的差分階數
第五個引數為int型別的dy,為y方向上的差分階數
第六個引數為int型別的ksize,有預設值3,表示Sobel核的大小,需要注意的是,Sobel運算元的核心大小必須取 1,3,5,7。
第七個引數是double型別的scale,計算導數值是可選的縮放因子,預設值為1,表示預設的情況下不進行放縮操作。可以查閱getDeriveKernels檢視相關內容
第八個引數是double型別的delta,表示在結果存入目標圖(dst)之前可選的delta值,有預設值0
第九個引數是int型別的bordertype,有預設值BORDER_DEFAULT,在之前的文章中有過這個引數的相關介紹,在這裡不再贅述。
具體程式如下:
//使用OpenCV自帶函式實現Sobel邊緣檢測
#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");
if (!srcImage.data)
{
cout << "讀入圖片錯誤!" << endl;
system("pause");
return -1;
}
Mat srcGray;
cvtColor(srcImage, srcGray, CV_BGR2GRAY);
imshow("原影象灰度圖", srcGray);
//定義邊緣圖,水平和垂直
Mat edgeImage, edgeImageX, edgeImageY;
//求x方向Sobel邊緣
Sobel(srcGray, edgeImageX, CV_16S, 1, 0, 3, 1, 0);
//求y方向的Sobel運算元
Sobel(srcGray, edgeImageY, CV_16S, 0, 1, 3, 1);
//線性變換,轉換輸入陣列元素為8位無符號整型
convertScaleAbs(edgeImageX, edgeImageX);
convertScaleAbs(edgeImageY, edgeImageY);
//x與y方向進行疊加
addWeighted(edgeImageX, 0.5, edgeImageY, 0.5, 0, edgeImage);
imshow("Sobel邊緣影象", edgeImage);
//定義scharr邊緣影象
Mat edgeXImageS, edgeYImageS, edgeImageS;
//計算X方向的Scharr邊緣
Scharr(srcGray, edgeXImageS, CV_16S, 1, 0, 1, 0);
convertScaleAbs(edgeXImageS, edgeXImageS);
//計算Y方向的Scharr邊緣
Scharr(srcGray, edgeYImageS, CV_16S, 0, 1, 1, 0);
convertScaleAbs(edgeYImageS, edgeYImageS);
//x與y方向進行邊緣疊加
addWeighted(edgeXImageS, 0.5, edgeYImageS, 0.5,0,edgeImageS);
imshow("Scharr邊緣影象", edgeImageS);
waitKey();
return 0;
}
執行後的結果如下所示:
可以看出,Scharr能夠保持更多更好的影象邊緣部分。
下面介紹不使用OpenCV中的函式庫實現Sobel邊緣檢測
(a.非極大值抑制Sobel邊緣檢測
→非極大值抑制Sobel邊緣檢測實現步驟主要如下:
(①.將影象轉換為32位浮點型資料,定義水平或垂直方向的Sobel運算元
(②.利用filter2D完成影象與運算元的卷積操作,計算卷積結果的梯度幅值
(③.自適應計算出梯度幅度閾值,閾值設定不大於梯度幅值的均值乘以4,根據與之對水平或垂直的鄰域區域梯度進行比較。
(④.判斷當前鄰域梯度是否大於水平或垂直鄰域梯度,自適應完成邊緣檢測處二值影象的操作。
對上面這個過程進行程式實現如下所示:
//影象非極大值抑制實現Sobel豎直細化邊緣
bool SobelVerEdge(Mat srcImage, Mat &dstImage)
{
CV_Assert(srcImage.channels() == 1);
srcImage.convertTo(srcImage, CV_32FC1);
//水平方向的Sobel運算元
Mat sobelX = (Mat_<float>(3, 3) << -0.125, 0, 0.125,
-0.25, 0, 0.25,
-0.125, 0, 0.125);
Mat ConResMat;
//卷積運算
filter2D(srcImage, ConResMat, srcImage.type(), sobelX);
//計算梯度的幅度
Mat gradMagMat;
multiply(ConResMat, ConResMat, gradMagMat);
//根據梯度幅度及引數設定閾值
int scaleVal = 4;
double thresh = scaleVal*mean(gradMagMat).val[0];
Mat resultTempMat = Mat::zeros(gradMagMat.size(), gradMagMat.type());
float *pDataMag = (float*)gradMagMat.data;
float *pDataRes = (float*)resultTempMat.data;
const int rowsNum = ConResMat.rows;
const int colsNum = ConResMat.cols;
for (int i = 1; i != rowsNum - 1; ++i)
{
for (int j = 1; j != colsNum - 1; ++j)
{
//計算這一點的梯度與水平或垂直梯度值的大小並比較結果
bool b1 = (pDataMag[i*colsNum + j] > pDataMag[i*colsNum + j - 1]);
bool b2 = (pDataMag[i*colsNum + j] > pDataMag[i*colsNum + j + 1]);
bool b3 = (pDataMag[i*colsNum + j] > pDataMag[(i - 1)*colsNum + j]);
bool b4 = (pDataMag[i*colsNum + j] > pDataMag[(i + 1)*colsNum + j]);
//判斷鄰域梯度是否滿足大於水平或垂直梯度的條件
//並根據自適應閾值引數進行二值化
pDataRes[i*colsNum + j] = 255 * ((pDataMag[i*colsNum + j] > thresh)
&& ((b1 && b2) || (b3 && b4)));
}
}
resultTempMat.convertTo(resultTempMat, CV_8UC1);
dstImage = resultTempMat.clone();
return true;
}
(b.影象直接卷積實現Sobel
影象直接卷積Sobel邊緣檢測實現比較簡單,首先定義水平或垂直方向的Sobel核因子,直接對源影象進行窗遍歷,計算視窗內的鄰域梯度幅值;然後根據梯度模長進行二值化操作,完成影象水平或垂直方向的邊緣檢測
影象直接卷積Sobel邊緣實現程式碼如下所示:
//影象直接卷積實現Sobel
bool SobelEdge(const Mat &srcImage, Mat &dstImage, uchar threshold)
{
CV_Assert(srcImage.channels() == 1);
//初始化水平核因子
Mat sobelX = (Mat_<double>(3, 3) << 1, 0, -1,
2, 0, -2,
1, 0, -1);
//初始化垂直核因子
Mat sobelY = (Mat_<double>(3, 3) << 1, 2, 1,
0, 0, 0,
-1, -2, -1);
dstImage = Mat::zeros(srcImage.rows - 2, srcImage.cols - 2, srcImage.type());
double edgeX = 0;
double edgeY = 0;
double graMag = 0;
for (int k = 1; k < srcImage.rows - 1; ++k)
{
for (int n = 1; n < srcImage.cols - 1; ++n)
{
edgeX = 0;
edgeY = 0;
//遍歷計算水平與垂直梯度
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
edgeX += srcImage.at<uchar>(k + i, n + j)*
sobelX.at<double>(1 + i, 1 + j);
edgeY += srcImage.at<uchar>(k + i, n + j)*
sobelY.at<double>(1 + i, 1 + j);
}
}
//計算梯度模長
graMag = sqrt(pow(edgeY, 2) + pow(edgeX, 2));
//進行二值化
dstImage.at<uchar>(k - 1, n - 1) =
((graMag > threshold) ? 255 : 0);
}
}
return true;
}
(c.影象卷積下非極大值抑制Sobel
影象卷積下非極大值抑制Sobel邊緣檢測的實現過程與之前的a部分比較類似。但是需要說明的是,非極大值抑制雖然能夠較好的剔除虛假邊緣點,但是對於某些特定場景下的邊緣檢測並不起作用,例如無損文字字元識別
相關的實現程式碼如下所示:
//影象卷積實現Sobel非極大值抑制
bool sobelOptaEdge(const Mat &srcImage, Mat &dstImage, int flag)
{
CV_Assert(srcImage.channels() == 1);
//初始化Sobel水平核因子
Mat sobelX = (Mat_<double>(3, 3) << 1, 0, -1,
2, 0, -2,
1, 0, -1);
//初始化Sobel垂直核因子
Mat sobelY = (Mat_<double>(3, 3) << 1, 2, 1,
0, 0, 0,
-1, -2, -1);
//計算水平與垂直卷積
Mat edgeX, edgeY;
filter2D(srcImage, edgeX, CV_32F, sobelX);
filter2D(srcImage, edgeY, CV_32F, sobelY);
//根據傳入的引數確定計算水平或垂直方向的邊緣
int paraX = 0;
int paraY = 0;
switch (flag)
{
case 0:
paraX = 1;
paraY = 0;
break;
case 1:
paraX = 0;
paraY = 1;
break;
case 2:
paraX = 1;
paraY = 1;
break;
default:
break;
}
edgeX = abs(edgeX);
edgeY = abs(edgeY);
Mat graMagMat = paraX*edgeX.mul(edgeX) +
paraY*edgeY.mul(edgeY);
//計算閾值
int scaleVal = 4;
double thresh = scaleVal*mean(graMagMat).val[0];
dstImage = Mat::zeros(srcImage.size(), srcImage.type());
for (int i = 1; i < srcImage.rows - 1; i++)
{
float *pDataEdgeX = edgeX.ptr<float>(i);
float *pDataEdgeY = edgeY.ptr<float>(i);
float *pDataGraMag = graMagMat.ptr<float>(i);
//閾值化和極大值抑制
for (int j = 1; j < srcImage.cols - 1; j++)
{
//判斷當前鄰域梯度是否大於閾值與大於水平或垂直梯度
if (pDataGraMag[j]>thresh &&
(pDataEdgeX[j]>paraX*pDataEdgeY[j] &&
pDataGraMag[j] > pDataGraMag[j - 1] &&
pDataGraMag[j] > pDataGraMag[j + 1] ||
(pDataEdgeY[j] > paraY*pDataEdgeX[j] &&
pDataGraMag[j] > pDataGraMag[j - 1] &&
pDataGraMag[j] > pDataGraMag[j + 1])))
dstImage.at<uchar> = 255;
}
}
return true;
}