OpenCV 4.11.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. 水平变化:这是通过使用奇数大小的内核 \(G_{x}\) 与 \(I\) 进行卷积来计算的。例如,对于大小为 3 的内核,\(G_{x}\) 将计算为

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

    1. 垂直变化:这是通过使用奇数大小的内核 \(G_{y}\) 与 \(I\) 进行卷积来计算的。例如,对于大小为 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 演示 - 简单边缘检测器";
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 ); // 加载图像
// 检查图像是否加载成功
if( image.empty() )
{
printf("图像打开错误: %s\n", imageName.c_str());
return EXIT_FAILURE;
}

减少噪声

// 使用高斯滤波器去除噪声(内核大小 = 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);
  • 我们计算 *x* 和 *y* 方向的“导数”。为此,我们使用如下所示的Sobel()函数:该函数采用以下参数

    • src_gray:在我们的示例中,输入图像。这里它是CV_8U
    • grad_x / grad_y:输出图像。
    • ddepth:输出图像的深度。我们将其设置为CV_16S以避免溢出。
    • x_orderx方向的导数阶数。
    • y_ordery方向的导数阶数。
    • scale, deltaBORDER_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的输出结果