Goal
在本教程中,您将学习如何:
使用 OpenCV 函数 Sobel() 计算图像的导数。
使用 OpenCV 函数 Scharr() 为大小为 3⋅3 的内核计算更准确的导数
Theory
下面的解释属于 Bradski 和 Kaehler 的《Learning OpenCV》一书。
- 在上两个教程中,我们看到了卷积的应用示例。 最重要的卷积之一是计算图像中的导数(或它们的近似值)。
- 为什么图像中导数的计算可能很重要? 假设我们想要检测图像中存在的边缘。 例如:
您可以很容易地注意到,在边缘中,像素强度以一种臭名昭著的方式变化。 表达变化的一个好方法是使用导数。 梯度的高变化表明图像发生了重大变化。
- 为了更形象化,假设我们有一个一维图像。 在下图中,边缘由强度的“跳跃”表示:
- 如果我们取一阶导数,可以更容易地看到边缘“跳跃”(实际上,这里显示为最大值)
- 因此,从上面的解释中,我们可以推断出一种检测图像边缘的方法可以通过定位梯度高于其邻居(或概括地说,高于阈值)的像素位置来执行。
- 更详细的解释请参考 Bradski 和 Kaehler 的 Learning OpenCV
Sobel Operator
- Sobel算子是离散微分算子。 它计算图像强度函数梯度的近似值。
- Sobel算子结合了高斯平滑和微分。
Formulation公式
假设要操作的图像是I:
- 我们计算两个导数:
水平变化:这是通过将 I 与具有奇数大小的内核 Gx 进行卷积来计算的。 例如,对于 3 的内核大小,Gx 将被计算为:
垂直变化:这是通过将 I 与具有奇数大小的内核 Gy 进行卷积来计算的。 例如,对于 3 的内核大小,Gy 将计算为:
- 在图像的每个点,我们通过结合上述两个结果来计算该点的梯度近似值:
尽管有时会使用以下更简单的等式:
笔记
当核的大小为 3 时,上面显示的 Sobel 核可能会产生明显的不准确性(毕竟,Sobel 只是导数的近似)。 OpenCV 通过使用 Scharr() 函数解决了大小为 3 的内核的这种不准确性。 这与标准 Sobel 函数一样快但更准确。 它实现了以下内核:
您可以在 OpenCV 参考资料 - Scharr() 中查看有关此函数的更多信息。 此外,在下面的示例代码中,您会注意到在 Sobel() 函数的代码上方还注释了 Scharr() 函数的代码。 取消注释它(并且显然注释 Sobel 的东西)应该让你了解这个函数是如何工作的。
Code
- 这个程序有什么作用?
应用 Sobel 算子并生成在较暗背景上检测到的边缘明亮的图像作为输出。
- Applies the Sobel Operator and generates as output an image with the detected edges bright on a darker background.
- 教程代码如下所示。
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
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);
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);
// converting back to 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);
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;
}
Explanation
Declare variables
// 首先我们声明我们要使用的变量
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;
Load source image 加载图像
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;
}
Reduce noise 减少噪音
// Remove noise by blurring with a Gaussian filter ( kernel size = 3 )
//通过使用高斯滤波器(内核大小 = 3)进行模糊来去除噪声
GaussianBlur(image, src, Size(3, 3), 0, 0, BORDER_DEFAULT);
Grayscale
// Convert the image to grayscale 将图像转换为灰度图
cvtColor(src, src_gray, COLOR_BGR2GRAY);
Sobel Operator
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_order:x方向导数的阶数。
y_order:y方向导数的阶数。
scale、delta 和 BORDER_DEFAULT:我们使用默认值。
请注意,要计算 x 方向的梯度,我们使用:xorder=1 和 yorder=0。 我们对 y 方向进行类似的操作。
Convert output to a CV_8U image
// converting back to CV_8U
convertScaleAbs(grad_x, abs_grad_x);
convertScaleAbs(grad_y, abs_grad_y);
Gradient
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
我们尝试通过添加两个方向梯度来近似梯度(请注意,这根本不是一个精确的计算!但它对我们的目的有好处)。
Show results
imshow(window_name, grad);
char key = (char)waitKey(0);
Results
这是将我们的基本检测器应用于 lena.jpg 的输出: