> 技术文档 > OpenCV (C/C++) 中使用 Sobel 算子进行边缘检测_cv::sobel

OpenCV (C/C++) 中使用 Sobel 算子进行边缘检测_cv::sobel


在 OpenCV (C++) 中使用 Sobel 算子进行边缘检测 🔪

边缘检测是图像处理和计算机视觉中的一项基础技术。它旨在识别图像中亮度发生剧烈变化或更正式地说是存在不连续性的点。这些剧烈变化通常对应于图像中物体的边界。Sobel 算子是一种广泛使用的一阶离散微分算子,用于计算图像强度函数梯度的近似值。本文将指导您如何在 C++ 中使用 OpenCV 的 Sobel 算子进行边缘检测。


理解 Sobel 算子

Sobel 算子使用两个 3×33 \\times 33×3 的卷积核来近似计算图像在 xxx 方向(水平变化)和 yyy 方向(垂直变化)的偏导数。这两个核分别是:

Sobel GxG_xGx (用于检测垂直边缘):

Gx=[−10+1−20+2−10+1]G_x = \\begin{bmatrix}-1 & 0 & +1 \\\\-2 & 0 & +2 \\\\-1 & 0 & +1\\end{bmatrix}Gx=121000+1+2+1

Sobel GyG_yGy (用于检测水平边缘):

Gy=[−1−2−1000+1+2+1]G_y = \\begin{bmatrix}-1 & -2 & -1 \\\\0 & 0 & 0 \\\\+1 & +2 & +1\\end{bmatrix}Gy=10+120+210+1

当这些核与图像进行卷积时,会产生两个梯度图像:Gx(I)G_x(I)Gx(I) 代表水平梯度(对垂直边缘敏感),Gy(I)G_y(I)Gy(I) 代表垂直梯度(对水平边缘敏感)。

每个像素点 (x,y)(x, y)(x,y) 的梯度幅值可以通过以下方式近似计算:

∣G(x,y)∣=Gx(I)2+Gy(I)2|G(x, y)| = \\sqrt{G_x(I)^2 + G_y(I)^2}G(x,y)=Gx(I)2+Gy(I)2

或者,为了计算效率,有时也使用绝对值之和:

∣G(x,y)∣=∣Gx(I)∣+∣Gy(I)∣|G(x, y)| = |G_x(I)| + |G_y(I)|G(x,y)=Gx(I)+Gy(I)

梯度的方向(边缘的朝向)可以估算为:

Θ(x,y)=arctan⁡(Gy(I)Gx(I))\\Theta(x, y) = \\arctan\\left(\\frac{G_y(I)}{G_x(I)}\\right)Θ(x,y)=arctan(Gx(I)Gy(I))

高梯度幅值表示强烈的强度变化,暗示存在边缘。


在 OpenCV (C++) 中实现 Sobel 边缘检测

OpenCV 提供了 cv::Sobel() 函数,可以方便地将 Sobel 算子应用于图像。

步骤:

  1. 包含头文件:包含必要的 OpenCV 头文件:opencv2/imgproc.hpp (用于图像处理函数如 Sobel) 和 opencv2/highgui.hpp (用于图像显示)。
  2. 加载图像:读取输入图像。通常将图像转换为灰度图是个好主意,因为边缘检测通常基于强度变化。
  3. 高斯模糊 (可选但推荐):在应用 Sobel 算子之前,通常会先对图像进行高斯模糊处理,以减少噪声对边缘检测结果的干扰。可以使用 cv::GaussianBlur()
  4. 应用 Sobel 算子:使用 cv::Sobel() 函数两次:一次用于 xxx 方向,一次用于 yyy 方向。
    • 你需要指定源图像、输出图像、输出图像的深度(通常建议使用 CV_16SCV_32F 来处理导数可能产生的负值和更大的动态范围,之后再转换类型),xxx 方向的导数阶数 (dx),以及 yyy 方向的导数阶数 (dy)。对于标准的 Sobel,GxG_xGx 对应 dx=1, dy=0GyG_yGy 对应 dx=0, dy=1
    • ksize 参数指定了 Sobel 核的大小 (例如, 3, 5, 7)。
  5. 计算梯度幅值: 为了得到一个单一的边缘图,你可以计算梯度幅值。
    • 方法一:分别计算 GxG_xGxGyG_yGy 的绝对值,然后按权重相加,例如使用 cv::convertScaleAbs() 分别处理 GxG_xGxGyG_yGy,然后用 cv::addWeighted() 合并。
    • 方法二:直接使用 GxG_xGxGyG_yGy (可能是 CV_16SCV_32F 类型) 计算精确的幅值,例如使用 cv::magnitude(grad_x, grad_y, abs_grad);,然后将结果转换为 CV_8U
  6. 转换到可显示类型: 如果你使用了 CV_16SCV_32F 作为输出深度,你可能需要将得到的梯度图像转换为 8 位无符号整数类型 (CV_8U) 以便显示,通常使用 cv::convertScaleAbs() 函数,它会取绝对值并缩放结果。
  7. 显示/保存结果: 显示原始图像和检测到的边缘。

C++ 代码示例

以下是一个使用 OpenCV 进行 Sobel 边缘检测的 C++ 代码片段:

#include #include #include int main(int argc, char** argv) { if (argc != 2) { std::cout << \"用法: \" << argv[0] << \" \" << std::endl; return -1; } // 1. 加载源图像 cv::Mat src = cv::imread(argv[1], cv::IMREAD_COLOR); if (src.empty()) { std::cerr << \"错误: 无法加载图像 \" << argv[1] << std::endl; return -1; } // 2. 转换为灰度图 cv::Mat gray; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 3. 高斯模糊 (可选,但通常推荐用于降噪) cv::Mat blurred; cv::GaussianBlur(gray, blurred, cv::Size(3, 3), 0, 0, cv::BORDER_DEFAULT); // 4. 计算 X 方向和 Y 方向的梯度 cv::Mat grad_x, grad_y; cv::Mat abs_grad_x, abs_grad_y; int scale = 1; int delta = 0; int ddepth = CV_16S; // 使用16位有符号类型,避免计算梯度时溢出 int ksize = 3; // Sobel核的大小 // X方向梯度 // cv::Sobel(src_gray, grad_x, ddepth, 1, 0, ksize, scale, delta, cv::BORDER_DEFAULT); cv::Sobel(blurred, grad_x, ddepth, 1, 0, ksize, scale, delta, cv::BORDER_DEFAULT); cv::convertScaleAbs(grad_x, abs_grad_x); // 计算绝对值并转换到8位无符号 // Y方向梯度 // cv::Sobel(src_gray, grad_y, ddepth, 0, 1, ksize, scale, delta, cv::BORDER_DEFAULT); cv::Sobel(blurred, grad_y, ddepth, 0, 1, ksize, scale, delta, cv::BORDER_DEFAULT); cv::convertScaleAbs(grad_y, abs_grad_y); // 计算绝对值并转换到8位无符号 // 5. 合并梯度 (近似梯度幅值) cv::Mat grad; cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad); // 或者,更精确的梯度幅值计算: // cv::Mat grad_magnitude; // cv::magnitude(grad_x, grad_y, grad_magnitude); // grad_x, grad_y 应该是 CV_16S 或 CV_32F // cv::convertScaleAbs(grad_magnitude, grad); // 6. 显示结果 cv::imshow(\"原始图像\", src); cv::imshow(\"灰度图\", gray); cv::imshow(\"Sobel X 梯度\", abs_grad_x); cv::imshow(\"Sobel Y 梯度\", abs_grad_y); cv::imshow(\"Sobel 合并梯度\", grad); cv::waitKey(0); cv::destroyAllWindows(); return 0;}

代码解释

  1. cv::imread(argv[1], cv::IMREAD_COLOR): 加载彩色图像。
  2. cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY): 将彩色图像转换为灰度图像,因为 Sobel 通常在单通道图像上操作。
  3. cv::GaussianBlur(gray, blurred, cv::Size(3,3), 0, 0, cv::BORDER_DEFAULT): 对灰度图像进行高斯模糊,以减少噪声。cv::Size(3,3) 是高斯核的大小。
  4. cv::Sobel(blurred, grad_x, ddepth, 1, 0, ksize, scale, delta, cv::BORDER_DEFAULT):
    • blurred: 输入图像 (推荐使用模糊后的图像)。
    • grad_x: 输出的 xxx 方向梯度图像。
    • ddepth: 输出图像的深度。CV_16S 用于存储可能为负的梯度值,并且范围比 CV_8U 更大,可以防止信息丢失。
    • 1: xxx 方向的导数阶数。
    • 0: yyy 方向的导数阶数。
    • ksize: Sobel 核的大小,通常为 3。也可以是 1, 5, 7。
    • scale: 可选的缩放因子,默认为1。
    • delta: 可选的增量值,在存储之前添加到结果中,默认为0。
    • cv::BORDER_DEFAULT: 边界处理方式。
      类似的参数用于计算 grad_y (yyy 方向梯度),其中 dx=0, dy=1
  5. cv::convertScaleAbs(grad_x, abs_grad_x):
    • 此函数首先计算输入数组 (grad_x) 中每个元素的绝对值。
    • 然后将结果缩放(如果指定了 alphabeta,这里使用默认值 alpha=1, beta=0)。
    • 最后将结果转换为 8 位无符号类型 (CV_8U)。这对于显示梯度图像非常有用。
  6. cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad):
    • 这是一种近似计算总梯度幅值的方法,通过将 xxx 方向和 yyy 方向梯度的绝对值按权重(这里是各0.5)相加。
    • 0.5abs_grad_x 的权重。
    • 0.5abs_grad_y 的权重。
    • 0 是加到和上的标量 (gamma)。
    • grad 是输出的合并梯度图像。
  7. cv::imshow()cv::waitKey(): 用于显示图像并等待用户按键。

或者,使用 cv::magnitude 计算精确梯度幅值
如果你想计算更精确的梯度幅值 Gx2+Gy2\\sqrt{G_x^2 + G_y^2}Gx2+Gy2,可以这样做:

// ... 计算 grad_x 和 grad_y (类型为 CV_16S 或 CV_32F) ...cv::Mat grad_magnitude;cv::magnitude(grad_x, grad_y, grad_magnitude); // grad_x, grad_y 应该是 CV_16S 或 CV_32F 类型cv::Mat final_grad;cv::convertScaleAbs(grad_magnitude, final_grad); // 转换为 CV_8U 以便显示cv::imshow(\"Sobel 精确梯度幅值\", final_grad);

在这种情况下,grad_xgrad_y 应该是应用 cv::Sobel 后且未调用 cv::convertScaleAbs 时的结果(即 CV_16SCV_32F 类型)。


编译

要编译此 C++ 代码,你需要 g++ (或任何兼容 C++11 的编译器) 和已安装的 OpenCV。

g++ -o sobel_app sobel_edge_detection.cpp `pkg-config --cflags --libs opencv4` -std=c++11

(如果你的 OpenCV 版本较旧,或者 pkg-config 的设置不同,请使用 opencv 替换 opencv4)。

运行:

./sobel_app <你的图片路径.jpg>

总结

Sobel 算子是边缘检测中一种经典且有效的方法。通过分别计算图像在 xxxyyy 方向上的梯度,并将其组合起来,我们可以识别出图像中的边缘。OpenCV 提供了易于使用的函数来实现这一过程,使得开发者可以快速地在自己的应用中集成边缘检测功能。记住,在应用 Sobel 算子之前进行高斯模糊通常能获得更好的结果,因为它可以减少噪声的干扰。