(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);
}
}
}
}