用定向滤波器检测边缘 - sumpig/OpenCV GitHub Wiki

Sobel 算子是一种典型的用于边缘检测的线性滤波器,它基于两个简单的 3×3 内核。

Sobel 算子在水平和垂直方向计算像素值的差分,得到图像梯度的近似值。

cv::Sobel 函数使用Sobel 内核来计算图像的卷积。函数的完整说明如下所示:

cv::Sobel(image, // 输入
          sobel, // 输出
          image_depth, // 图像类型
          xorder,yorder, // 内核规格
          kernel_size, // 正方形内核的尺寸
          alpha, beta); // 比例和偏移量

如果结果超出了像素值域的范围,就会进行饱和度运算。在生成最终图像之前,可以将结果缩放(相乘)alpha 倍,并加上偏移量beta。

如果xorder 和yorder 分别为1 和0,则得到水平方向Sobel 内核;如果分别是0 和1,则得到垂直方向的内核。

梯度是一个二维向量,所以它有范数和方向。梯度向量的范数表示变化的振幅,计算时通常被当作欧几里得范数(也称L2 范数)。

但是在图像处理领域,通常把绝对值之和作为范数进行计算。这称为 L1 范数,它得到的结果与 L2 范数比较接近,但计算速度快。

// 计算L1 范数
sobel= abs(sobelX) + abs(sobelY);

在检测边缘时,通常只计算范数。但如果需要同时计算范数和方向,可以使用下面的OpenCV函数:

// 计算Sobel 算子,必须用浮点数类型
cv::Sobel(image, sobelX, CV_32F, 1, 0);
cv::Sobel(image, sobelY, CV_32F, 0, 1);

// 计算梯度的L2 范数和方向
cv::Mat norm, dir;

// 将笛卡儿坐标换算成极坐标,得到幅值和角度
cv::cartToPolar(sobelX, sobelY, norm, dir);

默认情况下,得到的方向用弧度表示。如果要使用角度,只需要增加一个参数并设为true。

在convertTo 方法中使用可选的缩放参数可得到一幅图像,图像中的白色用0 表示,更黑的灰色阴影用大于0 的值表示。这幅图像可以很方便地显示Sobel 算子的范数,代码如下所示:

// 找到 Sobel 最大值
double sobmin, sobmax;
cv::minMaxLoc(sobel, &sobmin, &sobmax);

// 转换成 8 位图像
// sobelImage = -alpha*sobel + 255
cv::Mat sobelImage;
sobel.convertTo(sobelImage, CV_8U, -255./sobmax, 255);

对梯度幅值进行阈值化,可得到一个二值边缘分布图。选择合适的阈值并不容易。如果阈值太低,就会保留太多(厚)的边缘;而如果选用更严格(高)的阈值,就会留下断裂的边缘。

若想兼顾较低阈值和较高阈值的优点,有一个办法是使用滞后阈值化的概念。Canny 算子实现这个功能。

// 阈值化
cv::sobelThresholded;
cv::threshold(sobelImage, sobelThresholded, threshold, 255, cv::THRESH_BINARY);

扩展

还有其他一些梯度算子,这里将介绍其中的几个。

Prewitt 算子 用来计算某个像素位置的梯度。

Roberts 算子 基于一些简单的 2×2 内核。

Scharr 算子 可以更精确地计算梯度方向。

可以在 cv::Sobel 函数中使用 Scharr 内核,参数为CV_SCHARR:

cv::Sobel(image, sobelX, CV_16S, 1, 0, CV_SCHARR);

也可以调用cv::Scharr 函数,效果是一样的:

cv::Scharr(image, scharrX, CV_16S, 1, 0, 3);

所有这些定向滤波器都会计算图像函数的一阶导数。因此,在滤波器方向上像素强度变化大的区域将得到较大的值,较平坦的区域将得到较小的值。正因为如此,计算图像导数的滤波器被称为 高通滤波器

高斯导数

导数滤波器属于高通滤波器,因此它们往往会放大图像中的噪声和细小的高对比度细节。为了减少这些高频成分的影响,最好在应用导数滤波器之前对图像做平滑化处理。

有一个著名的数学定理:项的累加和的导数等于项的导数的累加和。

因此,可以不采取对平滑化的结果求导数,而是先对内核求导数,然后与图像卷积,这两个运算可以在像素上的同一次滤波中完成。用不同尺寸的内核调用 cv::Sobel 函数时,就采用了这种方法。