之前的坑少程序后面工作后接触到在补例程,我还是重点学习工作要用的吧,比如边缘检测。这个帖子费时有点久,所有东西本人都亲自过了一遍。
1.基本概念
边缘检测是图像处理与计算机视觉中的重要技术之一,其目的是检测识别出图像中亮度变化剧烈的像素点构成的集合。图像边缘的正确检测有利于分析目标检测、定位及识别,通常目标物体形成边缘存在以下几种情形:
<1>目标物呈现在图像的不同物体平面上,深度不连续
<2>目标物本身平面不同,表面方向不连续
<3>目标物材料不均匀,表面反射光不同
<4>目标物受外部场景光影响不一
根据边缘形成的原因,对图像的各像素点进行求微分或二阶微分可以检测出灰度变化明显的点。边缘检测大大减少了源图像的数据量,剔除了与目标不相干的信息,保留了图像重要的结构属性。边缘检测算子是利用图像边缘的突变性质来检测边缘的,通常情况下将边缘检测分为以下三个类型:
<1>一阶微分为基础的边缘检测,通过计算图像的梯度值来检测图像边缘,如Sobel算子、Prewitt算子及差分边缘检测。
<2>二阶微分为基础的边缘检测,通过寻求二阶导数中的过零点来检测边缘,如拉普拉斯算子、高斯拉普拉斯算子、Canny算子边缘检测。
<3>混合一阶与二阶微分为基础的边缘检测,综合利用一阶微分与二阶微分特征,如Marr-Hidreth算子
2.梯度算子与差分运算
由于数字图像是离散的,故处理图像时采用差分来代替微分运算。对于数字图像的简单一阶微分运算,由于其具有固定的方向性,只能检测特定的某一方向的边缘,所以不具有普遍性。为了检测克服一阶导数的缺点,我们定义图像的梯度为梯度算子,它是图像处理中最常用的一阶微分算法。图像梯度最重要的性质是梯度的方向是在图像灰度最大变化率上,它恰好可以反映出图像边缘上的灰度变化。梯度算子总是指向变换最剧烈的方向,在图像处理中,梯度向量总是与边缘正交,梯度方向为:
PS:这里是以滤波器左上角为原点建立的一个横轴为Y向右为正方向,竖轴为X向下为正方向。 (跟第五章建系不同)
<1>一阶微分算子
一阶微分算子利用了图像的边缘处的阶跃性,即图像梯度在边缘处取得极大值的特性来进行边缘检测。对于一幅二维的数字图像f(x,y)而言,需要完成x,y两个方向上的微分。
对于原函数f(u),差分运算是计算f(u)映射到f(u+a)-f(u+b)的值,差分运算分为前向差分与逆向差分,一阶前向差分是指,一阶逆向差分是指。二维离散图像函数f(x,y)在x方向的一阶差分定义为,y方向的一阶差分定义为。差分运算通过求图像灰度变化剧烈变化剧烈处的一阶微分算子的极值来检测奇异点,通过奇异点的值进一步设定阈值就可以得到边缘二值化的图像。差分边缘检测中差分的水平或垂直方向都与边缘方向正交,因此在实际应用场景,常常将边缘检测分为水平边缘、垂直边缘及对角线边缘,差分边缘检测定义方向模版如下所示:
对于原函数f(u),差分运算是计算f(u)映射到f(u+a)-f(u+b)的值,差分运算分为前向差分与逆向差分,一阶前向差分是指,一阶逆向差分是指。二维离散图像函数f(x,y)在x方向的一阶差分定义为,y方向的一阶差分定义为。差分运算通过求图像灰度变化剧烈变化剧烈处的一阶微分算子的极值来检测奇异点,通过奇异点的值进一步设定阈值就可以得到边缘二值化的图像。差分边缘检测中差分的水平或垂直方向都与边缘方向正交,因此在实际应用场景,常常将边缘检测分为水平边缘、垂直边缘及对角线边缘,差分边缘检测定义方向模版如下所示:
下面是参考历程:PS:处理之前一定要转换成灰度图
e.g:
#include
#include
#include
using namespace cv;
void diffOperation(Mat srcImage, Mat edgeXImage,Mat edgeYImage)
{
Mat tempImage = srcImage.clone();
int nRows = tempImage.rows;
int nCols = tempImage.cols;
for (int i = 0; i < nRows-1; i++)
{
for( int j = 0; j < nCols - 1;j++ )
{
//计算垂直边边缘
edgeXImage.at(i, j) = abs(tempImage.at(i + 1, j) - tempImage.at(i, j));
//计算水平边边缘
edgeYImage.at(i, j) = abs(tempImage.at(i , j + 1) - tempImage.at(i, j));
}
}
}
int main(void)
{
Mat srcImage = imread("girl4.jpg");
if (!srcImage.data)
{
return -1;
}
imshow("srcImage", srcImage);
cvtColor(srcImage , srcImage ,CV_RGB2GRAY ,0 );
Mat edgeXImage(srcImage.size(), srcImage.type());
Mat edgeYImage(srcImage.size(), srcImage.type());
//计算差分图像
diffOperation(srcImage, edgeXImage, edgeYImage);
imshow("edgeXImage", edgeXImage);
imshow("edgeYImage", edgeYImage);
Mat edgeImage(srcImage.size(), srcImage.type());
//水平与垂直边缘图像叠加
addWeighted(edgeXImage,0.5, edgeYImage,0.5,0.0, edgeImage);
imshow("edgeImage", edgeImage);
waitKey(0);
return 0;
}
<2>二阶微分算子
图像的边缘是图像的一阶微分极大值的像素点值,根据函数微分特性,该像素点值的二阶微分为零。图像边缘检测中根据二阶微分在边缘处出现零点这个特性来实现边缘检测。
(1)sobel——非极大值抑制
sobel算子是应用广泛的离散微分算子之一,常用于图像处理中的边缘检测,计算图像灰度函数的近似梯度。利用图像像素点Sobel算子计算出相应的梯度向量及向量的范数,基于图像卷积来实现水平方向与垂直方向检测对应方向上的边缘。对于源图像与奇数Sobel水平核、垂直核进行卷积可计算水平与垂直交换,与为下式:
对于图像中每一点结合卷积后的结果求出近似梯度幅度G:
这里有一个很重要的约等思想:结合交叉差分:PS:No.8-No.5=No.9-No.5+No.8-No.6+……
PS:由于基础核具有关于0,0,0所在的中轴正负对称,所以通过对基础核的旋转,和图像做卷积,可以获得灰度图的边缘图,同时消去旋转角方向+180°上的边缘,迭代多个方向即可消去多个方向的边缘,但是为消去的边缘会加倍。 结果图如下,按0°,45°,90°,135°,180°,225°,270°排序
sobel算子在进行边缘检测时效率较高,当对精度要求不是很高时,是一种较为常用的边缘检测方法。Sobel算子对于沿x轴和y轴的排列表示得较好,但是对于其他角度的表示却不够精确,这时候我们可以使用scharr滤波器,其水平与垂直核因子如下:
在Opencv中提供了Sobel函数来计算图像边缘:
void Sobel(Mat src,Mat dst,int ddepth,int dx,int dy,int ksize,double scale,double delta,int borderTpye);
其中参数src表示源图像;dst表示输出图像;ddepth表示输出图像的深度;dx为x方向的导数运算参数;dy为y方向导数运算参数;ksize为Sobel内核的大小,设置为奇数,默认参数为3;scale为缩放因子;delta为可选的增量常数,borderType用于判断图像边界的模式。
PS:对于scharr参数和Sobel算子一致,不过,scharr函数与Sobel的区别在于,Scharr仅作用于大小为3的内核。具有和sobel算子一样的速度,但结果更为精确。
这个程序是错误示范:
#include
#include
#include
#include
using namespace cv;
int main(void)
{
Mat srcImage = imread("picture3.jpg");
if (!srcImage.data)
{
return -1;
}
imshow("srcImage",srcImage);
Mat srcGray;
cvtColor(srcImage, srcGray, CV_RGB2GRAY);
Mat edgeSobelMat,edgeScharrMat;
//求Sobel边缘
Sobel(srcGray, edgeSobelMat, CV_16S,1,1);
//cvConvertScaleAbs(edgeSobelMat, edgeSobelMat);
imshow("Sobel", edgeSobelMat);
//求Scharr边缘
//Scharr(srcGray, edgeScharrMat, CV_16U, 1, 1);//有兴趣可以把scharr调用一下
//imshow("Scharr", edgeScharrMat);
waitKey(0);
return 0;
}
结果第一次接触Sobel和scharr就被坑了:
PS:Sobel算子有点坑,如果python版本的话可以x方向和y方向同时赋值,C++版本就老老实实一个一个方向合成吧,如果要是这么玩Scharr的话是直接报错的
现在给出正确程序:
#include
#include
#include
#include
using namespace cv;
int main(void)
{
Mat srcImage = imread("picture3.jpg");
if (!srcImage.data)
{
return -1;
}
imshow("srcImage",srcImage);
Mat srcGray;
cvtColor(srcImage, srcGray, CV_RGB2GRAY);
Mat edgeSobelXMat, edgeSobelYMat,edgeSobelMat,edgeScharrXMat, edgeScharrYMat,edgeScharrMat;
//求Sobel边缘
Sobel(srcGray, edgeSobelXMat, CV_16S,1,0,3,1,0,BORDER_DEFAULT);//x方向
Sobel(srcGray, edgeSobelYMat, CV_16S, 0, 1, 3, 1, 0, BORDER_DEFAULT);//y方向
convertScaleAbs(edgeSobelXMat, edgeSobelXMat);//图像增强
convertScaleAbs(edgeSobelYMat, edgeSobelYMat);
addWeighted(edgeSobelXMat,0.5, edgeSobelYMat,0.5,0, edgeSobelMat);
imshow("Sobel", edgeSobelMat);
//求Scharr边缘
Scharr(srcGray, edgeScharrXMat, CV_16S, 1, 0,1,0, BORDER_DEFAULT);//x方向
Scharr(srcGray, edgeScharrYMat, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT);//y方向
addWeighted(edgeScharrXMat, 0.5, edgeScharrYMat, 0.5, 0, edgeScharrMat);
imshow("Scharr", edgeScharrMat);
waitKey(0);
return 0;
}
运行结果如下:
(2)Laplace算子
拉普拉斯算子是最简单的各向同性二阶微分算子,具有旋转不变性。根据函数微分特性,该像素点值的二阶微分为零的点为边缘点。对于二维图像函数f(x,y),图像的Laplace运算二阶导数定义为:
对于二维离散图像而言,图像的Laplace可表示为下式:
根据离散Laplace的表示式,可以得到其模版的表现形式:
与分别为离散拉普拉斯算子的模板与扩展模版,利用函数模板可将图像中的奇异点如亮点变得更亮。对于图像中灰度变化剧烈的区域,拉普拉斯算子能实现其边缘检测。拉普拉斯算子利用二次微分特性与峰值间的过零点来确定边缘的位置,对奇异点或边界点更为敏感,常用于图像锐化处理中。
图像的锐化操作的主要目的是突出图像的细节或增强被模糊的图像细节,可实现灰度反差增强,同时使图像变得清晰。微分运算可实现图像细节的突出,积分运算或加权平均可使图像变得模糊。针对原始图像f(x,y),锐化操作可通过拉普拉斯算子对原图像进行处理,进行微分运算操作后产生描述灰度突变的图像,再将拉普拉斯图像与原始图像叠加进而产生锐化图像。图像的Laplace锐化可由下式表示:
其中t为邻域中心比较系数,拉普拉斯算子锐化操作通过比较邻域的中心像素与它所在邻域内的其他像素的平均灰度来确定相应的变换方式。当时,中心像素的灰度被进一步降低;相反,当时,中心像素的灰度被进一步提高。
下面我们一起来推导Laplace算子:
由于其为二阶导数,单个相邻像素的距离为1,在x方向上,f(x,y)对其右侧的导数是
f(x,y)对其左侧的导数是
那么二阶导数为
,y方向同理。故
由于拉普拉斯是利用检测极值的原理,也就是,故矩阵之和为0。其导数运算模板采用Sobel算子。
PS:还有改良后的高斯拉普拉斯算子的推导
也就是在高斯核函数矩阵的基础上进行二阶求导(这个计算比较简单,就不给过程了),有下式:
故有:
其基本模版是
Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。
void Laplacian(InputArray src,OutputArray dst, int ddepth, int ksize=1, double scale=1, double delta=0, intborderType=BORDER_DEFAULT );
第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和通道数。
第三个参数,int类型的ddept,目标图像的深度。
第四个参数,int类型的ksize,用于计算二阶导数的滤波器的孔径尺寸,大小必须为正奇数,且有默认值1。
第五个参数,double类型的scale,计算拉普拉斯值的时候可选的比例因子,有默认值1。
第六个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
第七个参数, int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate()处得到更详细的信息。
下面给出拉普拉斯检测的程序:
#include
#include
#include
#include
using namespace cv;
int main(void)
{
Mat srcImage = imread("picture3.jpg");
if (!srcImage.data)
{
return -1;
}
imshow("srcImage", srcImage);
Mat srcGray;
cvtColor(srcImage, srcGray, CV_RGB2GRAY);
Mat edgeSobelXMat, edgeSobelYMat, edgeSobelMat,edgeLaplaceMat;
//求Sobel边缘
Sobel(srcGray, edgeSobelXMat, CV_16S, 1, 0, 3, 1, 0, BORDER_DEFAULT);//x方向
Sobel(srcGray, edgeSobelYMat, CV_16S, 1, 0, 3, 1, 0, BORDER_DEFAULT);//y方向
//图像增强
convertScaleAbs(edgeSobelXMat, edgeSobelXMat);
convertScaleAbs(edgeSobelYMat, edgeSobelYMat);
addWeighted(edgeSobelXMat, 0.5, edgeSobelYMat, 0.5, 0, edgeSobelMat);
imwrite("Sobel.jpg", edgeSobelMat);
imshow("Sobel", edgeSobelMat);
//求Laplace边缘
Laplacian(srcGray, edgeLaplaceMat, CV_16S, 3, 1, 0, BORDER_DEFAULT);
//图像增强
convertScaleAbs(edgeLaplaceMat, edgeLaplaceMat);
imshow("Laplace", edgeLaplaceMat);
waitKey(0);
return 0;
}
程序运行结果如下:
大部分程序为拉普拉斯额外做了一步高斯滤波,现在我们看看没加高斯滤波和加了高斯滤波的区别
PS:我感觉这个没啥区别
(3)Roberts算子
这个比较简单,就不提了,上面已经讲到了。
(4)Prewitt算子
该算子对噪声有抑制作用。
先给一个单方向求解的程序:
cv::Mat Prewitt(cv::Mat img, bool verFlag = false , bool a = 0)
{
img.convertTo(img, CV_32FC1);
cv::Mat prewitt_kernel = (cv::Mat_(3, 3) << 0.1667, 0.1667, 0.1667, 0, 0, 0, -0.1667, -0.1667, -0.1667);;
switch (a)
{
case 0: prewitt_kernel = (cv::Mat_(3, 3) << 0.1667, 0.1667, 0.1667, 0, 0, 0, -0.1667, -0.1667, -0.1667);break;
case 1:prewitt_kernel = (cv::Mat_(3, 3) << -0.1667, 0, 0.1667, -0.1667, 0, 0.1667, -0.1667, 0, 0.1667); break;
}
//垂直边缘
if (verFlag)
{
prewitt_kernel = prewitt_kernel.t();
cv::Mat z1 = cv::Mat::zeros(img.rows,1,CV_32FC1);
cv::Mat z2 = cv::Mat::zeros(1, img.cols, CV_32FC1);
//将图像的四边设为0
z1.copyTo(img.col(0));
z1.copyTo(img.col(img.cols - 1));
z2.copyTo(img.row(0));
z2.copyTo(img.row(img.rows - 1));
}
cv::Mat edges;
cv::filter2D(img, edges, img.type(), prewitt_kernel);
cv::Mat mag;
cv::multiply(edges, edges, mag);
if (verFlag)
{
cv::Mat black_region = img < 0.03;
cv::Mat se = cv::Mat::ones(5, 5, CV_8UC1);
cv::dilate(black_region, black_region, se);
mag.setTo(0, black_region);
}
double thresh = 4.0f * cv::mean(mag).val[0];
cv::Mat dstImage = cv::Mat::zeros(mag.size(), mag.type());
float* dptr = (float*)mag.data;
float* tptr = (float*)dstImage.data;
int r = edges.rows, c = edges.cols;
for (int i=1; i != r - 1; ++i)
{
for (int j = 1; j != c - 1; ++j)
{
bool b1 = (dptr[ i * c + j] > dptr[i * c + j - 1]);
bool b2 = (dptr[i * c + j] > dptr[i * c + j + 1]);
bool b3 = (dptr[i*c + j] > dptr[i - 1] * c + j);
bool b4 = (dptr[i*c + j] > dptr[(i + 1)*c + j]);
tptr[i*c + j] = 255 * ((dptr[i*c + j] > thresh) && ((b1 && b2) || (b3 && b4)));
}
}
dstImage.convertTo(dstImage, CV_8UC1);
return dstImage;
}
再给一个合成的函数:(直接调用就好了,这两段代码)
void Image_prewitt(cv::Mat a, cv::Mat& b)
{
cv::Mat a1, a2;
a1 = Prewitt(a, false, 0);
a2 = Prewitt(a, false, 1);
addWeighted(a1, 1, a2, 1, 0, a);
}
(5)Canny算子
Canny算子检测的原理是通过图像信号函数的极大值来判定图像的边缘像素点。最优边缘检测主要以下面三个参数为评判标准:
<1>低错误率 <2>高定位性 <3>最小响应
其实现步骤如下:<1>消除噪声 <2>计算梯度幅度与方向 <3>非极大值抑制 <4>用滞后阈值算法求解图像边缘
一般第一步采用高斯核滤波模糊,第二部采用Sobel算子计算导数。
void Canny(InputArray image,OutputArray edges,double threshold1,double threshold2,int apertureSize=3,bool L2gradient =false)
image表示输入函数,edges表示输出图像,threshold1为第一滞后过程阈值,threshold第二滞后过程阈值,apertureSIze为索贝尔操作的尺寸因子,L2gradient为标志位,表示是否使用L2范数来计算图像梯度大小。
(6)Marr-Hildreth
Marr-Hildreth算子用于解决边缘检测的核心问题——定位精度与抑制噪声。该算子以高斯函数为平滑算子,结合拉普拉斯算子提取二阶导数的零交叉理论进行边缘检测。