在图像处理中,导数和梯度的概念有点类似,或者说有些关联。梯度在图像处理中非常重要,可以有说,几乎所有的图像处理算法都与梯度有关。梯度包括方向和幅值两部分。最常见的利用梯度的算法就是边缘检测算法。如sobel算法等。在OpenCV中,可以利用sobel、Scharr计算一阶导数或梯度,利用Laplacian计算二阶导数或梯度。
1、Sobel算子
用于表示微分的最常用算子是Sobel算子。 Sobel算子存在任何导数阶以及混合偏导数(例如,∂2/∂x∂y)。sobel算子如下。
void cv::Sobel(
cv::InputArray src,
cv::OutputArray dst,
int ddepth,
int xorder,
int yorder,
cv::Size ksize = 3,
double scale = 1,
double delta = 0,
int borderType = cv::BORDER_DEFAULT
);
src和dst是图像的输入和输出。参数ddepth允许选择输出的深度(例如,CV_32F)。如果src是一个8位图像,那么dst的深度应至少为CV_16S以避免溢出。xorder和yorder是导数的阶数。通常,使用0,1或至多2;0值表示该方向上没有导数。ksize参数应该是奇数,是要使用的滤波器的宽度(和高度)。borderType参数的功能与其他卷积操作的描述完全相同。
Sobel算子具有可以为任意大小的内核定义的性质,而且这些内核可以快速迭代构建。较大的内核可以更好地逼近导数,因为它们对噪声不太敏感。但是,如果导数不能在空间上保持不变,那么显然一个太大的内核将不再提供有用的结果。
必须认识到,Sobel算子不是一个真正的导数,因为它是在离散空间上定义的。Sobel算子实际表示的是对多项式的拟合。也就是说,x方向二阶Sobel算子不是二阶导数,它是一种适合于抛物线功能的局部特征。这解释了为什么人们可能想要使用更大的内核:更大的内核计算适合大量像素。
2、 Scharr算子
事实上,在离散网格的情况下,有许多方法来逼近导数。用于Sobel算子的近似值的缺点是它对于小内核来说不太准确。对于大的内核,在近似中使用更多的点。这种不准确性并不直接针对Sobel()中使用的X和y滤波,因为它们与x轴和y轴完全对齐。当想要进行近似于方向导数的图像时(即通过使用两个方向滤波器响应的比率y / x的反正切来反映图像梯度的方向)将会出现困难。
为了说明这一点,可能想要通过组合对象周围的梯度直方图来收集来自对象的形状信息。这样的直方图是许多常见形状分类器被训练和操作的基础。在这种情况下,不准确的梯度角测量会降低分类器的识别性能。
对于3×3 的Sobel滤波器,梯度角度与水平或垂直角度越远,误差越明显。OpenCV通过在Sobel()函数中使用特殊的ksize值cv :: SCHARR来解决小但快速3×3的Sobel导数滤波器的这种不准确性。Scharr滤波器与Sobel滤波器一样快但精度更高,因此如果想使用3×3滤波器进行图像检测,应使用它。Scharr滤波器的滤波器系数如图1所示
图1。 使用标志为cv :: SCHARR的3×3 Scharr滤波器
3、 拉普拉斯算子
OpenCV拉普拉斯函数实现了拉普拉斯算子的离散逼近:
由于拉普拉斯算符可以用二阶导数来定义,所以可能会认为离散的实现类似于二阶Sobel导数。事实上,拉普拉斯算子的OpenCV实现直接在其计算中使用Sobel算子:
void cv::Laplacian(
cv::InputArray src,
cv::OutputArray dst,
int ddepth,
cv::Size ksize = 3,
double scale = 1,
double delta = 0,
int borderType = cv::BORDER_DEFAULT
);
Laplacian()函数与Sobel()函数具有相同的参数。该ksize与Sobel完全相同,并且实际上给出了在计算二阶导数时对像素进行采样的区域的大小。在实际实现中,对于ksize除1以外的其他值,拉普拉斯算子直接根据相应的Sobel算子的总和计算。在ksize = 1的特殊情况下,拉普拉斯算子通过与图2所示的单个内核进行卷积运算。
图2。 当ksize = 1时Laplacian()使用的单个内核
拉普拉斯算子可用于各种情况。一个常见的应用是检测“斑点”。拉普拉斯算子的形式是沿着x和y的二阶导数的总和。这意味着单个点或任何由较高值包围的小斑点将倾向于最大化该函数。相反,被较低值包围的点或小块会趋向于最大化该函数的负值。
考虑到这一点,拉普拉斯算子也可以用作一种边缘检测器。为了理解这是怎么完成的,考虑函数的一阶导数,在函数快速变化的任何地方它都会很大。同样重要的是,当接近边缘状的不连续点时,它将迅速增长,而当越过不连续点时,它将迅速缩小。因此,在这个范围内,导数将处于某个地方的最大值。因此,可以看到二阶导数的这个局部极大值的位置。原始图像中的边缘将为拉普拉斯算子的0。但是,拉普拉斯算子的实质和有意义的边都将为0,但这不是问题,因为可以简单地滤除也具有一阶(Sobel)导数的那些像素。
例1 Sobel、Scharr、Laplacian算子使用示例
#include "iostream"
#include "opencv2opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char ** argv)
{
Mat src = imread("E:/ 1.jpg", 1);
namedWindow("原图", 0);
imshow("原图", src);
Mat srcGray;
cvtColor(src, srcGray, COLOR_BGR2GRAY);
namedWindow("原图转成灰度图", 0);
imshow("原图转成灰度图", srcGray);
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y, sobelDst;
//求 X方向梯度
Sobel(srcGray, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);
convertScaleAbs(grad_x, abs_grad_x);
namedWindow("Sobel_x", 0);
imshow("Sobel_x", abs_grad_x);
//求Y方向梯度
Sobel(srcGray, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);
convertScaleAbs(grad_y, abs_grad_y);
namedWindow("Sobel_y", 0);
imshow("Sobel_y", abs_grad_y);
//合并梯度(近似)
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, sobelDst);
namedWindow("整体Sobel", 0);
imshow("整体Sobel", sobelDst);
Mat schDst;
Scharr(srcGray, grad_x, CV_16S,1, 0);
convertScaleAbs(grad_x, abs_grad_x);
namedWindow("Scharr_x", 0);
imshow("Scharr_x", abs_grad_x);
Scharr(srcGray, grad_y, CV_16S, 0, 1);
convertScaleAbs(grad_y, abs_grad_y);
namedWindow("Scharr_y", 0);
imshow("Scharr_y", abs_grad_y);
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, schDst);
namedWindow("整体sch", 0);
imshow("整体sch", schDst);
Mat LapDst;
// 拉普拉斯变换
Laplacian(src, LapDst, CV_16S, 3);
convertScaleAbs(LapDst, LapDst);
namedWindow("Laplacian", 0);
cv::imshow("Laplacian", LapDst);
waitKey(0);
return 0;
}
图3运算结果