目前比较火热的图像识别技术,如车牌号识别、身份证识别、人脸识别等,都广泛运用到了图像边缘检测,今天我所介绍的就是OpenCV边缘检测,实现边缘检测有三个步骤:滤波->增强->检测,opencv中有三个常用的边缘检测算子函数:canny、sobel和laplace。
现附上一张原图:
canny算子
Canny边缘检测算子是一种多级检测算法,Canny的目标是找到一个最优的边缘检测算法,算法能够尽可能多地标识出图像中的实际边缘,标识出的边缘要与实际图像中的实际边缘尽可能接近,图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。Canny算法步骤:降噪->寻找图片的亮梯度->在图像中跟踪边缘。任何边缘检测算法都不可能在未经处理的原始数据上很好地处理,所以第一步是对原始数据与高斯平滑模板作卷积,得到的图像与原始图像相比有些轻微的模糊(blurred)。这样,单独的一个像素噪声在经过高斯平滑的图像上变得几乎没有影响。图像中的边缘可能会指向不同的方向,所以Canny算法使用4个mask检测水平、垂直以及对角线方向的边缘。原始图像与每个mask所作的卷积都存储起来。对于每个点我们都标识在这个点上的最大值以及生成的边缘的方向。这样我们就从原始图像生成了图像中每个点亮度梯度图以及亮度梯度的方向。较高的亮度梯度比较有可能是边缘,但是没有一个确切的值来限定多大的亮度梯度是边缘多大又不是,所以Canny使用了滞后阈值。滞后阈值需要两个阈值——高阈值与低阈值。假设图像中的重要边缘都是连续的曲线,这样我们就可以跟踪给定曲线中模糊的部分,并且避免将没有组成曲线的噪声像素当成边缘。所以我们从一个较大的阈值开始,这将标识出我们比较确信的真实边缘,使用前面导出的方向信息,我们从这些真正的边缘开始在图像中跟踪整个的边缘。在跟踪的时候,我们使用一个较小的阈值,这样就可以跟踪曲线的模糊部分直到我们回到起点。一旦这个过程完成,我们就得到了一个二值图像,每个点表示是否是一个边缘点。
一个获得亚像素精度边缘的改进实现是在梯度方向检测二阶方向导数的过零点:
它在梯度方向的三阶方向导数满足符号条件:
表示用高斯核平滑原始图像得到的尺度空间表示 L计算得到的偏导数。用这种方法得到的边缘片断是连续曲线,这样就不需要另外的边缘跟踪改进。滞后阈值也可以用于亚像素边缘检测。
格罗宁根大学学生的Canny算子实现边缘检测的一个web项目
/**
* canny算子 三通道
* 第一个参数,输入图像,需为单通道8位图像。
* 第二个参数,输出的边缘图,需要和源图片有一样的尺寸和类型。
* 第三个参数,第一个滞后性阈值。
* 第四个参数,第二个滞后性阈值。
* 第五个参数,表示孔径大小,其有默认值3。
* 第六个参数,计算图像梯度幅值的标识,有默认值false。
* 需要注意的是,这个函数阈值1和阈值2两者的小者用于边缘连接,
* 而大者用来控制强边缘的初始段,推荐的高低阈值比在2:1到3:1之间
*/
CV_EXPORTS_W void Canny( InputArray dx, InputArray dy,
OutputArray edges,
double threshold1, double threshold2,
bool L2gradient = false );
Mat img(h,w,CV_8UC4,pixels);
Mat outImg;
cvtColor(img,outImg,COLOR_BGR2BGR);
//滤波
blur(outImg,outImg,Size(3,3));
// //Canny算子边缘检测
Canny(outImg,outImg,3,9);
Mat out;
out = Scalar::all(0);
img.copyTo(out,outImg);
uchar *ptr = img.ptr(0);
uchar *outPtr = outImg.ptr(0);
for (int i = 0; i < w * h; ++i) {
ptr[4*i+0] = outPtr[3*i+0];
ptr[4*i+1] = outPtr[3*i+1];
ptr[4*i+2] = outPtr[3*i+2];
}
Sobel算子
它是一个离散微分算子,近似于是一个计算图像强度梯度的函数。在图像中的每个点,Sobel-Feldman算子的结果是相应的梯度向量或者这个向量的范数。Sobel算子基于将图像与水平和垂直方向上的小的可分离的整数值的滤波器进行卷积,因此在计算上相对比较节约时间。另一方面,它产生的梯度相对比较粗糙,特别是对于图像中的高频变化。
运算符使用与原始图像卷积的两个3×3内核来计算导数的近似值- 一个用于水平变化,另一个用于垂直。如果我们将A定义为源图像,则G x和G y分别是包含水平和垂直微分近似的两幅图像,计算结果如下( *这里表示二维信号处理卷积操作):
由于Sobel核可以分解为平均值和微分核的乘积,所以他们可以用平滑的方式来计算梯度。例如,G x可以写成:
该X坐标在这里被定义为向右方向增加,并y坐标被定义为向下方向增加。在图像中的每个点,可以将得到的梯度近似值结合起来,得到梯度的大小:
使用这些信息,我们也可以计算出梯度的方向:
例如,对于在右侧较亮的垂直边缘,θ是0。
/**
* sobel算子 单通道
* 第一个参数,输入图像
* 第二个参数,目标图像
* 第三个参数,输出图像的深度
使用CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F
使用CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F
使用CV_32F, 取ddepth =-1/CV_32F/CV_64F
使用CV_64F, 取ddepth = -1/CV_64F
* 第四个参数,x 方向上的差分阶数。
* 第五个参数,y方向上的差分阶数。
* 第六个参数,核的大小;必须取1,3,5或7。
* 第七个参数,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。
* 第八个参数,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
* 第九个参数,边界模式,默认值为BORDER_DEFAULT。
* 一般情况下,都是用ksize x ksize内核来计算导数的。
* 然而,有一种特殊情况——当ksize为1时,往往会使用3 x 1或者1 x 3的内核。
* 且这种情况下,并没有进行高斯平滑操作。
*/
CV_EXPORTS_W 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 );
Mat img(h,w,CV_8UC4,pixels);
Mat outImg;
cvtColor(img,outImg,COLOR_BGR2GRAY);//单通道
Mat outimgX,outimgY,outXY;
//x方向sobel梯度
Sobel(outImg,outimgX,CV_8U,1,0);
//y方向sobel梯度
Sobel(outImg,outimgY,CV_8U,0,1);
//合并梯度
addWeighted(outimgX,0.5,outimgY,0.5,0,outXY);
uchar *ptr = img.ptr(0);
uchar *outPtr = outXY.ptr(0);
for (int i = 0; i < w * h; ++i) {
ptr[4*i+0] = outPtr[i];
ptr[4*i+1] = outPtr[i];
ptr[4*i+2] = outPtr[i];
}
laplacian算子
在opencv中laplacian算子通过二阶导数来检测边缘,由于图像是2D的,所以需要在两个维度上采用导数。Opencv中使用laplacian计算图像的梯度,然后内部有调用了Sobel算子。
laplacian算子定义:
/**
* laplacian函数
* 第一个参数,输入图像,需为单通道8位图像。
* 第二个参数,输出的边缘图,需要和源图片有一样的尺寸和通道数。
* 第三个参数,目标图像的深度。
* IPL_DEPTH_8U - 无符号8位整型 0--255
* IPL_DEPTH_8S - 有符号8位整型 -128--127
* IPL_DEPTH_16U - 无符号16位整型 0--65535
* IPL_DEPTH_16S - 有符号16位整型 -32768--32767
* IPL_DEPTH_32S - 有符号32位整型 00.0--1.0
* IPL_DEPTH_64F - 双精度浮点数 0.0--1.0
* 第四个参数,用于计算二阶导数的滤波器的孔径尺寸,大小必须为正奇数,且有默认值1。
* 第五个参数,计算拉普拉斯值的时候可选的比例因子,有默认值1。放大或者缩小因子。
* 第六个参数,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
* 第七个参数,边界模式,默认值为BORDER_DEFAULT。
*/
CV_EXPORTS_W void Laplacian( InputArray src, OutputArray dst, int ddepth,
int ksize = 1, double scale = 1, double delta = 0,int borderType = BORDER_DEFAULT );
Mat img(h,w,CV_8UC4,pixels);
Mat outImg;
cvtColor(img,outImg,COLOR_BGR2GRAY);//单通道
Laplacian(outImg,outImg,CV_8U);
uchar *ptr = img.ptr(0);
uchar *outPtr = outXY.ptr(0);
for (int i = 0; i < w * h; ++i) {
ptr[4*i+0] = outPtr[i];
ptr[4*i+1] = outPtr[i];
ptr[4*i+2] = outPtr[i];
}