Skip to content

Sobel 导数

在本教程中,你将学到如何:

  • 使用 OpenCV 函数 cv::Sobel 计算图像的导数。
  • 使用 OpenCV 函数 cv::Scharr 为 3⋅33 \cdot 3 大小的核计算更精确的导数。

注意: 下面的讲解出自 Bradski 与 Kaehler 所著的 Learning OpenCV 一书。

  1. 在前两篇教程中,我们已经看到了卷积的应用示例。最重要的卷积运算之一就是计算图像中的导数(或对导数的近似)。

  2. 为什么计算图像的导数可能很重要?设想我们要检测图像中存在的边缘。例如:

    很容易注意到,在边缘处,像素强度会以明显的方式变化。表达变化的一个好办法是使用导数。梯度的大幅变化表明图像发生了显著变化。

  3. 更形象地说,假设我们有一幅一维图像。下图中强度的「跳变」就展示了一条边缘:

  4. 如果我们取一阶导数,边缘的「跳变」会看得更清楚(实际上这里表现为一个极大值)。

  5. 因此,根据上面的讲解可以推断:检测图像边缘的一种方法,可以是定位那些梯度高于其邻域的像素位置(或者更一般地,高于某个阈值的位置)。

  6. 更详细的讲解,请参阅 Bradski 与 Kaehler 所著的 Learning OpenCV。

  1. Sobel 算子(Sobel Operator)是一个离散的微分算子。它计算图像强度函数梯度的近似值。

  2. Sobel 算子结合了高斯平滑与微分。

假设要处理的图像为 II:

  1. 我们计算两个方向的导数:

    1. 水平方向变化:将 II 与一个奇数大小的核 GxG_{x} 做卷积得到。例如核大小为 3 时,GxG_{x} 的计算为:

      -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{bmatrix} * I$$
    2. 垂直方向变化:将 II 与一个奇数大小的核 GyG_{y} 做卷积得到。例如核大小为 3 时,GyG_{y} 的计算为:

      -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1 \end{bmatrix} * I$$
  2. 在图像的每一点处,我们结合上面两个结果来计算该点梯度的近似:

    G=Gx2+Gy2G = \sqrt{ G_{x}^{2} + G_{y}^{2} }

    有时也会使用下面这个更简单的公式:

    G=∣Gx∣+∣Gy∣G = |G_{x}| + |G_{y}|

注意: 当核大小为 3 时,上面展示的 Sobel 核可能产生明显的误差(毕竟 Sobel 只是导数的一种近似)。针对大小为 3 的核,OpenCV 通过 cv::Scharr 函数解决这一不精确问题。它与标准 Sobel 函数一样快,但更精确。它实现的是如下核:

-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 参考文档中查阅关于 cv::Scharr 函数的更多信息。另外,在下面的示例代码中你会注意到,cv::Sobel 函数的代码附近还有被注释掉的 cv::Scharr 函数代码。取消注释它(同时注释掉 Sobel 部分),就能让你了解这个函数的用法。 ## 代码 1. **这个程序做什么?** - 应用 *Sobel 算子*,生成一幅输出图像,检测到的*边缘*在较暗的背景上以亮色显示。 1. 本教程的代码如下所示。 **C++:** 你也可以从 [这里](https://github.com/opencv/opencv/tree/4.x/samples/cpp/tutorial_code/ImgTrans/Sobel_Demo.cpp) 下载。 ```cpp /** * @file Sobel_Demo.cpp * @brief Sample code uses Sobel or Scharr OpenCV functions for edge detection * @author OpenCV team */ # include "opencv2/imgproc.hpp" # include "opencv2/imgcodecs.hpp" # include "opencv2/highgui.hpp" # include <iostream> using namespace cv; using namespace std; /** * @function main */ int main( int argc, char** argv ) { cv::CommandLineParser parser(argc, argv, "{@input |lena.jpg|input image}" "{ksize k|1|ksize (hit 'K' to increase its value at run time)}" "{scale s|1|scale (hit 'S' to increase its value at run time)}" "{delta d|0|delta (hit 'D' to increase its value at run time)}" "{help h|false|show help message}"); cout << "The sample uses Sobel or Scharr OpenCV functions for edge detection\n\n"; parser.printMessage(); cout << "\nPress 'ESC' to exit program.\nPress 'R' to reset values ( ksize will be -1 equal to Scharr function )"; // First we declare the variables we are going to use 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"); // As usual we load our source image (src) image = imread( samples::findFile( imageName ), IMREAD_COLOR ); // Load an image // Check if image is loaded fine if( image.empty() ) { printf("Error opening image: %s\n", imageName.c_str()); return EXIT_FAILURE; } for (;;) { // Remove noise by blurring with a Gaussian filter ( kernel size = 3 ) GaussianBlur(image, src, Size(3, 3), 0, 0, BORDER_DEFAULT); // Convert the image to grayscale cvtColor(src, src_gray, COLOR_BGR2GRAY); /// Generate grad_x and grad_y Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; /// Gradient X Sobel(src_gray, grad_x, ddepth, 1, 0, ksize, scale, delta, BORDER_DEFAULT); /// Gradient Y Sobel(src_gray, grad_y, ddepth, 0, 1, ksize, scale, delta, BORDER_DEFAULT); // converting back to CV_8U convertScaleAbs(grad_x, abs_grad_x); convertScaleAbs(grad_y, abs_grad_y); /// Total Gradient (approximate) addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad); imshow(window_name, grad); char key = (char)waitKey(0); if(key == 27) { return EXIT_SUCCESS; } if (key == 'k' || key == 'K') { ksize = ksize < 30 ? ksize+2 : -1; } if (key == 's' || key == 'S') { scale++; } if (key == 'd' || key == 'D') { delta++; } if (key == 'r' || key == 'R') { scale = 1; ksize = -1; delta = 0; } } return EXIT_SUCCESS; } ``` **Java:** 你也可以从 [这里](https://github.com/opencv/opencv/tree/4.x/samples/java/tutorial_code/ImgTrans/SobelDemo/SobelDemo.java) 下载。 ```java /** * @file SobelDemo.java * @brief Sample code using Sobel and/or Scharr OpenCV functions to make a simple Edge Detector */ import org.opencv.core.*; import org.opencv.highgui.HighGui; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; class SobelDemoRun { public void run(String[] args) { // First we declare the variables we are going to use Mat src, src_gray = new Mat(); Mat grad = new Mat(); String window_name = "Sobel Demo - Simple Edge Detector"; int scale = 1; int delta = 0; int ddepth = CvType.CV_16S; // As usual we load our source image (src) // Check number of arguments if (args.length == 0){ System.out.println("Not enough parameters!"); System.out.println("Program Arguments: [image_path]"); System.exit(-1); } // Load the image src = Imgcodecs.imread(args[0]); // Check if image is loaded fine if( src.empty() ) { System.out.println("Error opening image: " + args[0]); System.exit(-1); } // Remove noise by blurring with a Gaussian filter ( kernel size = 3 ) Imgproc.GaussianBlur( src, src, new Size(3, 3), 0, 0, Core.BORDER_DEFAULT ); // Convert the image to grayscale Imgproc.cvtColor( src, src_gray, Imgproc.COLOR_RGB2GRAY ); /// Generate grad_x and grad_y Mat grad_x = new Mat(), grad_y = new Mat(); Mat abs_grad_x = new Mat(), abs_grad_y = new Mat(); /// Gradient X //Imgproc.Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, Core.BORDER_DEFAULT ); Imgproc.Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, Core.BORDER_DEFAULT ); /// Gradient Y //Imgproc.Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, Core.BORDER_DEFAULT ); Imgproc.Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, Core.BORDER_DEFAULT ); // converting back to CV_8U Core.convertScaleAbs( grad_x, abs_grad_x ); Core.convertScaleAbs( grad_y, abs_grad_y ); /// Total Gradient (approximate) Core.addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad ); HighGui.imshow( window_name, grad ); HighGui.waitKey(0); System.exit(0); } } public class SobelDemo { public static void main(String[] args) { // Load the native library. System.loadLibrary(Core.NATIVE_LIBRARY_NAME); new SobelDemoRun().run(args); } } ``` ![Sobel_Derivatives_Tutorial_Theory_0.jpg](/img/opencv/Sobel_Derivatives_Tutorial_Theory_0.jpg) **Python:** 你也可以从 [这里](https://github.com/opencv/opencv/tree/4.x/samples/python/tutorial_code/ImgTrans/SobelDemo/sobel_demo.py) 下载。 ```python """ @file sobel_demo.py @brief Sample code using Sobel and/or Scharr OpenCV functions to make a simple Edge Detector """ import sys import cv2 as cv def main(argv): # First we declare the variables we are going to use window_name = ('Sobel Demo - Simple Edge Detector') scale = 1 delta = 0 ddepth = cv.CV_16S # As usual we load our source image (src) # Check number of arguments if len(argv) < 1: print ('Not enough parameters') print ('Usage:\nmorph_lines_detection.py < path_to_image >') return -1 # Load the image src = cv.imread(argv[0], cv.IMREAD_COLOR) # Check if image is loaded fine if src is None: print ('Error opening image: ' + argv[0]) return -1 # Remove noise by blurring with a Gaussian filter ( kernel size = 3 ) src = cv.GaussianBlur(src, (3, 3), 0) # Convert the image to grayscale gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY) # Gradient-X # grad_x = cv.Scharr(gray,ddepth,1,0) grad_x = cv.Sobel(gray, ddepth, 1, 0, ksize=3, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT) # Gradient-Y # grad_y = cv.Scharr(gray,ddepth,0,1) grad_y = cv.Sobel(gray, ddepth, 0, 1, ksize=3, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT) # converting back to uint8 abs_grad_x = cv.convertScaleAbs(grad_x) abs_grad_y = cv.convertScaleAbs(grad_y) ## Total Gradient (approximate) grad = cv.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0) cv.imshow(window_name, grad) cv.waitKey(0) return 0 if __name__ == "__main__": main(sys.argv[1:]) ``` ![Sobel_Derivatives_Tutorial_Theory_Intensity_Function.jpg](/img/opencv/Sobel_Derivatives_Tutorial_Theory_Intensity_Function.jpg) ![Sobel_Derivatives_Tutorial_Theory_dIntensity_Function.jpg](/img/opencv/Sobel_Derivatives_Tutorial_Theory_dIntensity_Function.jpg) ## 说明 ### 声明变量 ```cpp // First we declare the variables we are going to use 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; ``` ### 加载源图像 ```cpp String imageName = parser.get<String>("@input"); // As usual we load our source image (src) image = imread( samples::findFile( imageName ), IMREAD_COLOR ); // Load an image // Check if image is loaded fine if( image.empty() ) { printf("Error opening image: %s\n", imageName.c_str()); return EXIT_FAILURE; } ``` ### 降噪 ```cpp // Remove noise by blurring with a Gaussian filter ( kernel size = 3 ) GaussianBlur(image, src, Size(3, 3), 0, 0, BORDER_DEFAULT); ``` ### 灰度化 ```cpp // Convert the image to grayscale cvtColor(src, src_gray, COLOR_BGR2GRAY); ``` ### Sobel 算子 ```cpp /// Generate grad_x and grad_y Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; /// Gradient X Sobel(src_gray, grad_x, ddepth, 1, 0, ksize, scale, delta, BORDER_DEFAULT); /// Gradient Y Sobel(src_gray, grad_y, ddepth, 0, 1, ksize, scale, delta, BORDER_DEFAULT); ``` - 我们分别计算 *x* 和 *y* 方向上的「导数」。为此,我们使用函数 cv::Sobel,如上所示。该函数接受以下参数: - *src_gray*:在本例中是输入图像,此处为 *CV_8U*。 - *grad_x* / *grad_y*:输出图像。 - *ddepth*:输出图像的深度。我们将其设为 *CV_16S* 以避免溢出。 - *x_order*:**x** 方向导数的阶数。 - *y_order*:**y** 方向导数的阶数。 - *scale*、*delta* 和 *BORDER_DEFAULT*:使用默认值。 注意,计算 *x* 方向的梯度时我们使用 $x_{order}= 1$ 且 $y_{order} = 0$。*y* 方向的做法类似。 ### 将输出转换为 CV_8U 图像 ```cpp // converting back to CV_8U convertScaleAbs(grad_x, abs_grad_x); convertScaleAbs(grad_y, abs_grad_y); ``` ### 梯度 ```cpp /// Total Gradient (approximate) addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad); ``` 我们通过把两个方向的梯度相加来近似*梯度*(注意,这完全不是精确计算!但对我们的目的而言已经足够)。 ### 显示结果 ```cpp imshow(window_name, grad); char key = (char)waitKey(0); ``` ## 结果 ![Sobel_Derivatives_Tutorial_Result.jpg](/img/opencv/Sobel_Derivatives_Tutorial_Result.jpg) 1. 下面是将我们的基础检测器应用到 *lena.jpg* 上的输出: (结果图像中,检测到的边缘在黑色背景上以亮色显示。)