OpenCV 4.12.0
开源计算机视觉
加载中...
搜索中...
无匹配项
Sobel 导数

上一教程: 为图像添加边框
下一教程: 拉普拉斯算子

原始作者Ana Huamán
兼容性OpenCV >= 3.0

目标

在本教程中,您将学习如何

  • 使用 OpenCV 函数 Sobel() 计算图像的导数。
  • 使用 OpenCV 函数 Scharr() 计算尺寸为 \(3 \cdot 3\) 的核的更精确导数

理论

注意
以下解释摘自 Bradski 和 Kaehler 所著的 Learning OpenCV 一书。
  1. 在前面两个教程中,我们看到了卷积的应用示例。最重要的卷积之一是计算图像中的导数(或其近似值)。
  2. 为什么图像中导数的计算可能很重要?假设我们想检测图像中存在的边缘。例如

您可以很容易地注意到,在边缘处,像素强度会显著变化。表达变化的好方法是使用导数。梯度的高变化表示图像中的重大变化。

  1. 为了更形象化,假设我们有一个一维图像。边缘由下方图中强度的“跳跃”显示
  1. 如果我们取一阶导数(实际上,这里显示为一个最大值),边缘的“跳跃”会更容易看到
  1. 因此,从上面的解释我们可以推断,检测图像边缘的方法可以通过定位梯度高于其邻居(或者更普遍地说,高于阈值)的像素位置来实现。
  2. 更详细的解释,请参考 Bradski 和 Kaehler 所著的 Learning OpenCV

Sobel 算子

  1. Sobel 算子是一种离散微分算子。它计算图像强度函数的梯度的近似值。
  2. Sobel 算子结合了高斯平滑和微分。

公式

假设待操作图像为 \(I\)

  1. 我们计算两个导数

    1. 水平变化:这是通过将 \(I\) 与一个奇数尺寸的核 \(G_{x}\) 进行卷积来计算的。例如,对于尺寸为 3 的核,\(G_{x}\) 将按如下方式计算:

    \[G_{x} = \begin{bmatrix} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{bmatrix} * I\]

    1. 垂直变化:这是通过将 \(I\) 与一个奇数尺寸的核 \(G_{y}\) 进行卷积来计算的。例如,对于尺寸为 3 的核,\(G_{y}\) 将按如下方式计算:

    \[G_{y} = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1 \end{bmatrix} * I\]

  2. 在图像的每个点,我们通过结合上述两个结果来计算该点梯度的近似值

    \[G = \sqrt{ G_{x}^{2} + G_{y}^{2} }\]

    尽管有时会使用以下更简单的方程

    \[G = |G_{x}| + |G_{y}|\]

注意
当核大小为 3 时,上面显示的 Sobel 核可能会产生明显的误差(毕竟,Sobel 只是导数的近似)。OpenCV 通过使用 Scharr() 函数解决了尺寸为 3 的核的这种不准确性。它与标准 Sobel 函数一样快,但更精确。它实现了以下核:

\[G_{x} = \begin{bmatrix} -3 & 0 & +3 \\ -10 & 0 & +10 \\ -3 & 0 & +3 \end{bmatrix}\]

\[G_{y} = \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ +3 & +10 & +3 \end{bmatrix}\]

您可以在 OpenCV 参考手册 - Scharr() 中查看此函数的更多信息。此外,在下面的示例代码中,您会注意到 Sobel() 函数的代码上方也有 Scharr() 函数的注释代码。取消注释(并显然注释掉 Sobel 相关代码)应该能让您了解此函数的工作原理。

代码

  1. 此程序的作用是什么?
    • 应用Sobel 算子,并生成一张以较暗背景显示检测到的边缘的图像。
  2. 教程代码如下所示。

解释

声明变量

// 首先声明我们要使用的变量
Mat image,src, src_gray;
Mat grad;
const String window_name = "Sobel Demo - Simple Edge Detector";
int ksize = parser.get<int>("ksize");
int scale = parser.get<int>("scale");
int delta = parser.get<int>("delta");
int ddepth = CV_16S;

加载源图像

String imageName = parser.get<String>("@input");
// 像往常一样,我们加载源图像 (src)
image = imread( samples::findFile( imageName ), IMREAD_COLOR ); // Load an image
// 检查图像是否加载成功
if( image.empty() )
{
printf("Error opening image: %s\n", imageName.c_str());
return EXIT_FAILURE;
}

降噪

// Remove noise by blurring with a Gaussian filter ( kernel size = 3 )
GaussianBlur(image, src, Size(3, 3), 0, 0, BORDER_DEFAULT);

灰度化

// 将图像转换为灰度
cvtColor(src, src_gray, COLOR_BGR2GRAY);

Sobel 算子

Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
Sobel(src_gray, grad_x, ddepth, 1, 0, ksize, scale, delta, BORDER_DEFAULT);
Sobel(src_gray, grad_y, ddepth, 0, 1, ksize, scale, delta, BORDER_DEFAULT);
  • 我们计算 xy 方向的“导数”。为此,我们使用 Sobel() 函数,如下所示:该函数接受以下参数:

    • src_gray:在我们的示例中,它是输入图像。这里是 CV_8U
    • grad_x / grad_y :输出图像。
    • ddepth:输出图像的深度。我们将其设置为 CV_16S 以避免溢出。
    • x_orderx 方向导数的阶数。
    • y_ordery 方向导数的阶数。
    • scaledeltaBORDER_DEFAULT:我们使用默认值。

    请注意,为了计算 x 方向的梯度,我们使用:\(x_{order}= 1\) 和 \(y_{order} = 0\)。对于 y 方向,我们也类似处理。

将输出转换为 CV_8U 图像

// 转换回 CV_8U
convertScaleAbs(grad_x, abs_grad_x);
convertScaleAbs(grad_y, abs_grad_y);

梯度

addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);

我们尝试通过添加两个方向梯度来近似梯度(请注意,这根本不是精确计算!但对我们的目的来说已经足够了)。

显示结果

imshow(window_name, grad);
char key = (char)waitKey(0);

结果

  1. 这是将我们的基本检测器应用于 lena.jpg 的输出结果: