29.边缘检测
1、边缘检测原理
图像的边缘指的是图像中像素灰度值突然发生变化的区域,如果将图像的每一行像素和每一列像素都描述成一个关于灰度值的函数,那么图像的边缘对应在灰度值函数中是函数值突然变大的区域。函数值的变化趋势可以用函数的导数描述。当函数值突然变大时,导数也必然会变大,而函数值变化较为平缓区域,导数值也比较小,因此可以通过寻找导数值较大的区域去寻找函数中突然变化的区域,进而确定图像中的边缘位置。图5-27给出一张含有边缘的图像,图像每一行的像素灰度值变化可以用图中下方的曲线表示。
通过像素灰度值曲线可以看出图像边缘位于曲线变化最陡峭的区域,对灰度值曲线求取一阶导数可以得到图5-28中所示的曲线,通过曲线可以看出曲线的最大值区域就是图像中的边缘。
由于图像是离散的信号,我们可以用临近的两个像素差值来表示像素灰度值函数的导数:
图像的边缘有可能是由高像素值变为低像素值,也有可能是由低像素值变成高像素值,因此为了在图像中同时表示出这两种边缘信息,需要将计算的结果求取绝对值。
代码清单5-22 convertScaleAbs()函数函数原型 void cv::convertScaleAbs(InputArray src, OutputArray dst, double alpha = 1, double beta = 0 )
- src:输入矩阵。
- dst:计算绝对值后输入矩阵。
- alpha:缩放因子,默认参数为只求取绝对值不进行缩放。
- beta:在原始数据上添加的偏值,默认参数表示不增加偏值。
该函数可以求取矩阵中所有数据的绝对值。函数前两个参数分别为输入、输出矩阵,两个参数可以是相同的变量。函数第三个和第四个参数为对绝对值的缩放和原始数据上的偏移。函数的计算原理如式:
图像的边缘包含X方向的边缘和Y方向的边缘,因此分别求取两个方向的边缘后,对两个方向的边缘求取并集就是整幅图像的边缘,即将图像两个方向边缘结果相加得到整幅图像的边缘信息。为了验证这种滤波方式对于图像边缘提取的效果,在代码清单5-23中给出了利用filter2D()函数实现图像边缘检测的算法,检测的结果在图5-29中给出。需要说明的是,由于求取边缘的结果可能会有复数,不在原始图像的CV_8U的数据类型内,因此滤波后的图像数据类型不要用“-1”,而应该改为CV_16S。
代码清单5-23 myEdge.cpp图像边缘检测 #include <opencv2\opencv.hpp> #include <iostream> using namespace cv; using namespace std; int main() { //创建边缘检测滤波器 Mat kernel1 = (Mat_<float>(1, 2) << 1, -1); //X方向边缘检测滤波器 Mat kernel2 = (Mat_<float>(1, 3) << 1, 0, -1); //X方向边缘检测滤波器 Mat kernel3 = (Mat_<float>(3, 1) << 1, 0, -1); //X方向边缘检测滤波器 Mat kernelXY = (Mat_<float>(2, 2) << 1, 0, 0, -1); //由左上到右下方向边缘检测滤波器 Mat kernelYX = (Mat_<float>(2, 2) << 0, -1, 1, 0); //由右上到左下方向边缘检测滤波器 //读取图像,黑白图像边缘检测结果较为明显 Mat img = imread("equalLena.png", IMREAD_ANYCOLOR); if (img.empty()) { cout << "请确认图像文件名称是否正确" << endl; return -1; } Mat result1, result2, result3, result4, result5, result6; //检测图像边缘 //以[1 -1]检测水平方向边缘 filter2D(img, result1, CV_16S, kernel1); convertScaleAbs(result1, result1); //以[1 0 -1]检测水平方向边缘 filter2D(img, result2, CV_16S, kernel2); convertScaleAbs(result2, result2); //以[1 0 -1]'检测由垂直方向边缘 filter2D(img, result3, CV_16S, kernel3); convertScaleAbs(result3, result3); //整幅图像的边缘 result6 = result2 + result3; //检测由左上到右下方向边缘 filter2D(img, result4, CV_16S, kernelXY); convertScaleAbs(result4, result4); //检测由右上到左下方向边缘 filter2D(img, result5, CV_16S, kernelYX); convertScaleAbs(result5, result5); //显示边缘检测结果 imshow("result1", result1); imshow("result2", result2); imshow("result3", result3); imshow("result4", result4); imshow("result5", result5); imshow("result6", result6); waitKey(0); return 0; }
2、Laplacian算子
Laplacian算子具有各方向同性的特点,能够对任意方向的边缘进行提取,具有无方向性的优点,因此使用Laplacian算子提取边缘不需要分别检测X方向的边缘和Y方向的边缘,只需要一次边缘检测即可。Laplacian算子是一种二阶导数算子,对噪声比较敏感,因此常需要配合高斯滤波一起使用。
代码清单5-30 Laplacian()函数原型 1.void cv::Laplacian(InputArray src, 2. OutputArray dst, 3.int ddepth, 4.int ksize = 1, 5.double scale = 1, 6.double delta = 0, 7.int borderType = BORDER_DEFAULT 8. )
- src:输入原图像,可以是灰度图像或彩色图像。
- dst:输出图像,与输入图像src具有相同的尺寸和通道数
- ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。
- ksize:滤波器的大小,必须为正奇数。
- scale:对导数计算结果进行缩放的缩放因子,默认系数为1,表示不进行缩放。
- delta:偏值,在计算结果中加上偏值。
- borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
该函数利用Laplacian算子提取图像中的边缘信息,与Soble()函数相同,函数的前两个参数分别为输入图像和输出图像,第三个参数为输出图像的数据类型,这里需要注意由于提取边缘信息时有可能会出现负数,因此不要使用CV_8U数据类型的输出图像,否则会使得图像边缘提取不准确。函数第四个参数是滤波器尺寸的大小,必须是正奇数,当该参数的值大于1时,该函数通过Sobel算子计算出图像X方向和Y方向的二阶导数,将两个方向的导数求和得到Laplacian算子,其计算公式如式所示。
3、Canny算法
Canny算法不容易受到噪声的影响,能够识别图像中的弱边缘和强边缘,并结合强弱边缘的位置关系,综和给出图像整体的边缘信息。Canny边缘检测算法是目前最优越的边缘检测算法之一,该方法的检测过程分为以下5个步骤:
-
Step1:使用高斯滤波平滑图像,减少图像中噪声。一般情况下使用式(5.23)所示的5×5的高斯滤波器。
-
Step2:计算图像中每个像素的梯度方向和幅值。首先通过Sobel算子分别检测图像X方向的边缘和Y方向的边缘,之后利用式(5.24)计算梯度的方向和幅值。
为了简便,梯度方向常取值0°、45°、90°和135°这个四个角度之一。
-
Step3:应用非极大值抑制算法消除边缘检测带来的杂散响应。首先将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较, 如果当前像素的梯度强度与另外两个像素梯度强度相比最大,则该像素点保留为边缘点,否则该像素点将被抑制。
-
Step4:应用双阈值法划分强边缘和弱边缘。将边缘处的梯度值与两个阈值进行比较,如果某像素的梯度幅值小于较小的阈值,则会被去除掉;如果某像素的梯度幅值大于较小阈值但小于较大阈值,则将该像素标记为弱边缘;如果某像素的梯度幅值大于较大阈值,则将该像素标记为强边缘。
-
Step5:消除孤立的弱边缘。在弱边缘的8邻域范围寻找强边缘,如果8邻域内存在强边缘,则保留该弱边缘,否则将删除弱边缘,最终输出边缘检测结果。
代码清单5-32 Canny()函数原型 1. void cv::Canny(InputArray image, 2. OutputArray edges, 3. double threshold1, 4. double threshold2, 5. int apertureSize = 3, 6. bool L2gradient = false 7. )
-
image:输入图像,必须是CV_8U的单通道或者三通道图像。
-
edges:输出图像,与输入图像具有相同尺寸的单通道图像,且数据类型为CV_8U。
-
threshold1:第一个滞后阈值。
-
threshold2:第二个滞后阈值。
-
apertureSize:Sobel算子的直径。
-
L2gradient:计算图像梯度幅值方法的标志,幅值的两种计算方式如式(5.25)所示。
该函数利用Canny算法提取图像中的边缘信息。第一个参数是需要提取边缘的输入图像,目前只支持数据类型为CV_8U的图像,输入图像可以是灰度图像或者彩色图像。第二个参数是边缘检测结果的输出图像,图像是数据类型为为CV_8U的单通道灰度图像。函数第三个和第四个参数是Canny算法中用于区分强边缘和弱边缘的两个阈值,两个参数不区分较大阈值和较小阈值,函数会自动比较区分两个阈值的大小,不过一般情况下,较大阈值与较小阈值的比值在2:1到3:1之间。函数最后一个参数是计算梯度幅值方法的选择标志,无特殊需求的情况下,使用默认值即可。
代码清单5-33 myCanny.cpp利用Canny算法提取图像边缘 1. #include <opencv2\opencv.hpp> 2. #include <iostream> 3. 4. using namespace cv; 5. using namespace std; 6. 7. int main() 8. { 9. //读取图像,黑白图像边缘检测结果较为明显 10. Mat img = imread("equalLena.png", IMREAD_ANYDEPTH); 11. if (img.empty()) 12. { 13. cout << "请确认图像文件名称是否正确" << endl; 14. return -1; 15. } 16. Mat resultHigh, resultLow, resultG; 17. 18. //大阈值检测图像边缘 19. Canny(img, resultHigh, 100, 200, 3); 20. 21. //小阈值检测图像边缘 22. Canny(img, resultLow, 20, 40, 3); 23. 24. //高斯模糊后检测图像边缘 25. GaussianBlur(img, resultG, Size(3, 3), 5); 26. Canny(resultG, resultG, 100, 200, 3); 27. 28. //显示图像 29. imshow("resultHigh", resultHigh); 30. imshow("resultLow", resultLow); 31. imshow("resultG", resultG); 32. waitKey(0); 33. return 0; 34. }