本次笔记要整理记录一些常用的导数算子,包括一阶导数算子和二阶导数算子等,这些导数算子种类比较繁杂,统一记录起来会比较容易回顾。
- 常见一阶导数算子
对于图像求一阶导数,就是求图像灰度曲线的导数,所以在灰度曲线中变化较大的区域,在导数曲线中体现出绝对值比较大的值,而灰度曲线中变化平坦的区域,在导数曲线中体现出接近零的值,通过求一阶导数的处理,能突出图像中对象的边缘,并且还具有方向性。
常用的一阶导数算子主要有:Robert算子、prewitt算子、sobel算子。
下面分别给出这三个算子的代码实现:
Mat robertx_image;
Mat robert_x = (Mat_<int>(2, 2) << 1, 0, 0, -1); //robert_x
filter2D(image, robertx_image, CV_32F, robert_x);
convertScaleAbs(robertx_image, robertx_image); //将输入数组均转化为uchar类型
Mat roberty_image;
Mat robert_y = (Mat_<int>(2, 2) << 0, -1, 1, 0); //robert_y
filter2D(image, roberty_image, CV_32F, robert_y);
convertScaleAbs(roberty_image, roberty_image);
Mat robert_image;
add(robertx_image, roberty_image, robert_image, Mat(), CV_16S);
convertScaleAbs(robert_image, robert_image);
imshow("robert_image", robert_image);
其中,定义了robert算子的x方向和y方向,分别是Mat robert_x = (Mat_
和Mat robert_y = (Mat_
,然后通过filter2D
这个API对图像和算子分别卷积,要注意图像求梯度后存在浮点数,需要使用浮点(如CV_32F)类型,最后将x和y方向的两个结果进行相加,并且转换到CV_8UC类型,就可以显示出输出图像了。
效果如下(分别是x、y方向的梯度,以及完整图像梯度):
2.prewitt算子
//prewitt算子
Mat prewitt_x_image, prewitt_y_image, prewitt_image;
Mat prewitt_x = (Mat_<int>(3, 3) << -1, 0, 1, -1, 0, 1, -1, 0, 1);
filter2D(image, prewitt_x_image, CV_32F, prewitt_x);
convertScaleAbs(prewitt_x_image, prewitt_x_image);
Mat prewitt_y = (Mat_<int>(3, 3) << -1, -1, -1, 0, 0, 0, 1, 1, 1);
filter2D(image, prewitt_y_image, CV_32F, prewitt_y);
convertScaleAbs(prewitt_y_image, prewitt_y_image);
add(prewitt_x_image, prewitt_y_image, prewitt_image, Mat(), CV_16S);
convertScaleAbs(prewitt_image, prewitt_image);
imshow("prewitt_image", prewitt_image);
其中同样分别定义了prewitt算子的x、y方向:Mat prewitt_x = (Mat_
和Mat prewitt_y = (Mat_
,分别与图像计算卷积后再进行相加,转换数据类型后显示输出图像。
效果如下:
3.sobel算子
Mat x_grad, y_grad, image_grad;
//Sobel算子求梯度后的值为浮点型且超过[0, 255]
Sobel(image, x_grad, CV_32F, 1, 0, 3, 1, 0, 4);
Sobel(image, y_grad, CV_32F, 0, 1, 3, 1, 0, 4);
convertScaleAbs(x_grad, x_grad); //转化至CV_8UC
imshow("x_grad", x_grad);
convertScaleAbs(y_grad, y_grad); //转化至CV_8UC
imshow("y_grad", y_grad);
add(x_grad, y_grad, image_grad, Mat(), CV_16S); //两个[0,255]范围的值相加,输出深度为CV_16S(16位有符号整型)
convertScaleAbs(image_grad, image_grad);
imshow("image_grad", image_grad);
Sobel算子的优势是,相比prewitt算子能更好的抑制噪声。而且不需要我们自定义卷积核,而是OpenCV提供了一个专门的APISobel(image, x_grad, CV_32F, 1, 0, 3, 1, 0, 4)
其第一个参数是要计算梯度的输入图像;
第二个参数是处理完的输出图像;
第三个参数是输出图像的深度;
第四个参数dx,为1时表示计算x方向梯度,为0表示不计算x方向梯度;
第五个参数dy,为1时表示计算y方向梯度,为0表示不计算y方向梯度;
第六个参数ksize是计算梯度的卷积核大小;
后面几个参数分别是缩放量(输出图像相比输入图像的缩放)、增加常量(输出图像相比输入图像增加的常量)、边缘填充方式,如果没有特殊需求都选用默认值即可。
效果如下:
- 常见二阶导数算子
二阶导数算子是对图像灰度曲线的导数求导,也就是在一阶导数的基础上再次求导,对灰度值的剧烈变化敏感,能突出图像的纹理结构,并且不具有方向性。
常见的二阶导数算子主要有拉普拉斯算子和canny边缘检测算子。
1.拉普拉斯算子
//
//拉普拉斯算子
Mat gaussianBlur_image, laplacian_image_4, laplacian_image_8;
GaussianBlur(image, gaussianBlur_image, Size(), 1, 1);
Laplacian(gaussianBlur_image, laplacian_image_4, CV_32F, 1, 1, 127);
convertScaleAbs(laplacian_image_4, laplacian_image_4);
Laplacian(gaussianBlur_image, laplacian_image_8, CV_32F, 3, 1, 127);
convertScaleAbs(laplacian_image_8, laplacian_image_8);
imshow("laplacian_image_4", laplacian_image_4);
imshow("laplacian_image_8", laplacian_image_8);
拉普拉斯算子对于噪声非常敏感,所以需要先对原图进行高斯模糊后再提取梯度,在某些场合下会将高斯模糊和拉普拉斯边缘提取合并起来,即为LOG算子。
OpenCV中提供一个API实现拉普拉斯算子的运算:Laplacian(gaussianBlur_image, laplacian_image_4, CV_32F, 1, 1, 127)
第一个参数是经过高斯模糊后的图像;
第二个参数是输出图像;
第三个参数是输出图像的深度;
第四个参数是ksize,当ksize = 1 使用4邻域的拉普拉斯算子;当ksize > 1 使用8邻域的拉普拉斯算子;
第五个参数是缩放值;
第六个参数是输出图像加上的常量,因为二阶导数求得值很小,为了便于观察可以加上一个delta值来提高显示亮度。
效果如下:
左边是四邻域的拉普拉斯算子,右边是八邻域的拉普拉斯算子提取,可见使用八邻域的拉普拉斯算子的纹理提取效果比四邻域要好得多,能看得到更多的细节,轮廓也更加清晰。
2.Canny边缘检测
Canny边缘检测的主要步骤:
1、 高斯模糊 – 抑制噪声;
2、梯度提取得到候选边缘;
3、角度计算与非最大信号抑制;
4、高低阈值链接、获取完整边缘;
5. 输出边缘;
其中,第一、二步骤可以采用LOG算子实现,或使用高斯模糊加Sobel算子实现;第三步计算每个像素点的边缘幅值和角度,并进行非最大信号抑制,主要目的是实现边缘细化,使得边缘像素进一步减少(个人对非最大信号抑制的理解:对已经提取出的离散的边缘像素,与其邻域内的像素进行比较,若不是邻域的最大值则将其从边缘像素中剔除,若为最大值则保留为边缘像素);第四步是在边缘像素点集中进行筛选,将小于低阈值的像素点除去,将大于高阈值的像素点保留,对介于高低阈值区间的像素点则进行判断:该像素点是否与位于高阈值及以上的像素点相连通,若有连通域则在边缘像素点集中保留该像素点,若没有连通域则剔除该像素点;最后第五步以二值图像输出边缘。
当然了在OpenCV中已经封装好了这些功能,不需要我们自己实现,我们可以调用相关API即可,下面是代码:
Mat image_canny;
Canny(image, image_canny, 200, 500, 5, false);
imshow("image_canny", image_canny);
Mat image_and, image_add;
bitwise_and(image, image, image_and, image_canny);
imshow("image_and", image_and);
addWeighted(image, 1, image_and, 0.2, 0, image_add, CV_16S);
convertScaleAbs(image_add, image_add);
imshow("image_add", image_add);
主要APICanny(image, image_canny, 200, 500, 5, false)
第一个参数是输入图像;
第二个参数是输出图像;
第三和第四个参数:参数threshold1表示低阈值,参数threshold2表示高阈值(上限尽量不超过500);经验值是设置threshold1:threshold2 = 1:2到1:3之间效果较好
第五个参数apertureSize:进行梯度运算(Sobel算子)的窗口大小,默认值为3
第六个参数L2gradient:布尔类型,表示是否使用L2计算梯度,即使用矢量和计算;默认为false,即默认使用平方和计算梯度。
上述代码中,将canny边缘检测提取出的边缘与原图像进行求与(and)运算,得到有色彩的边缘,再将边缘和原图像按权重相加,最后的输出结果增强了原图像细节纹理,但视觉感受上存在撕裂感。效果如下:
最左边是进行canny提取后的边缘图像,中间是和原图像求与(and)运算后的彩色边缘,最右边是彩色和原图像按权重相加的结果,可见虽然纹理部分变得明显了,但是却显得很不自然。
本次关于图像导数算子的整理就到此结束啦,谢谢~
PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!