1. 目标
在本文中,将学习如何:
2. 理论
-
笔记
下面的解释来自Bradski 和 Kaehler的《 Learning OpenCV 》一书。
-
在前2篇文章中,演示了卷积的应用示例。最重要的卷积应用之一是计算图像中的导数(或它们的近似值)。
-
为什么图像中导数的计算很重要?假设想要检测图像中存在的边缘。例如:
可以很容易地注意到,在边缘,像素强度以一种显著的变化。表达变化的一个好方法是使用导数。大的梯度变化的表明图像发生了重大变化。
-
在下图中,边缘由强度的“跳跃”表示:
-
如果取一阶导数,可以更容易地看到边缘“跳跃”(实际上,这里显示为最大值)
-
因此,从上面的解释中,可以推断出一种检测图像边缘的方法可以通过定位梯度高于其邻居(或概括地说,高于阈值)的像素位置来执行。
-
更详细的解释请参考Bradski 和 Kaehler的Learning OpenCV
Sobel算子
- Sobel算子是离散微分算子。它计算图像强度函数梯度的近似值。
- Sobel算子结合了高斯平滑和微分。
公式
假设要操作的图像是:
-
计算两个导数:
a. 水平方向:通过 计算卷积。例如,对于 内核大小3 ,将计算为:
G_x=\begin{bmatrix}
-1& 0& +1 \ -2& 0& +2 \ -1& 0& +1 \ \end{bmatrix}*I $$
b. **垂直方向**:通过$I*G_x$ 计算卷积。例如,内核大小3,$G_x$将计算为:
-
在图像的每个点,通过结合上述两个结果来计算该点的梯度近似值:
有时会使用以下更简单的等式:
-
笔记
- 当内核大小为
3时,上面显示的 Sobel 内核可能会产生明显的不准确性(毕竟,Sobel 只是导数的近似值)。OpenCV 通过使用Scharr() 函数解决了大小为 3 的内核的这种不准确性。这与标准 Sobel 函数一样快但更准确。它实现了以下内核:
- 当内核大小为
且
- 可以在 OpenCV 参考资料 - Scharr() 中查看有关此函数的更多信息。此外,在下面的示例代码中,会注意到在Sobel()** 函数的代码上方还注释了Scharr()函数的代码。取消注释它(并且显然注释 Sobel 的东西)应该让你了解这个函数是如何工作的。
3. 代码
-
这个程序有什么作用?
- 在较暗背景上应用Sobel 算子并把检测到的明亮边缘作为输出图像。
-
教程代码如下所示。
也可以从这里下载
#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;
}
4. 代码解释
声明变量
// 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;
}
减少噪音
// 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);
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_order : x方向的导数的阶数。
- y_order : y方向导数的阶数。
- scale,delta和BORDER_DEFAULT:我们使用默认值。
请注意,要计算x方向的梯度,我们使用:和. 对y方向进行类似的操作。
将输出转换为 CV_8U 图像
// 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);
5. 结果
-
这是基本检测器应用于lena.jpg的输出: