定位图像中的关键点
如前一课所述,相机无法直接测量到对象的距离。然而,对于我们的避碰系统,我们可以根据图像传感器上的相对距离比来计算碰撞时间。要做到这一点,我们需要在图像平面上设置一组位置,这些位置可以作为稳定的锚来计算它们之间的相对距离。本节讨论了如何在图像中定位此类定位点或关键点。
请看下图中的三个小块,它们是从公路驾驶场景的图像中提取的。网格显示单个像素的边界。您如何描述这些小块中可以用作关键点的有意义的位置?
在最左边的补丁中,亮像素和暗像素之间有一个明显的对比,这类似于从左下到右上的一条线。中间的补丁类似于左上角一组非常暗的像素形成的一个角。最右边的补丁看起来像一个明亮的斑点,可以近似于椭圆。
为了精确定位图像中的一个关键点,我们需要一种方法在X和Y中为它们指定一个唯一的坐标。并非所有上述补丁都适合这一目标。无论是角还是椭圆,都可以在X和Y中精确定位,而最左侧图像中的线条则不能。
在下面,我们将集中精力检测图像中的角点。在后面的部分中,我们还将研究针对类斑点结构(如sift检测器)进行优化的检测器。
灰度梯度
在上面的例子中,相邻像素之间的对比度包含了我们需要的信息:为了精确定位例如中间小块中的角,我们不需要知道它的颜色,而是需要形成角的像素之间的颜色差高达p。可能。一个理想的角只包含黑白像素。
下图显示了图像中所有像素沿红线的灰度分布以及灰度梯度,这是图像灰度的导数。
可以看出,在相邻像素之间的对比度发生显著变化的位置,灰度分布迅速增加。左侧路灯下部和暗门与光墙有明显的灰度差。如果我们想给发生变化的像素指定唯一的坐标,我们可以通过观察灰度的导数来实现,这是你可以在红线下方看到的蓝色渐变轮廓。图像灰度的突然变化在梯度剖面中以明显的峰谷清晰可见。如果我们不仅要从左到右,而且从上到下寻找这样的峰,我们可以寻找在水平和垂直方向上都显示梯度峰的点,并选择它们作为x和y坐标的关键点。在上面的示例补丁中,这对角点最有效,而类似边缘的结构在所有位置都有或多或少相同的梯度,在x和y中没有明显的峰值。
基于以上观测,进入关键点检测的第一步就是梯度图像的计算。在数学上,梯度是图像灰度对x和y方向的偏导数。下图显示了三个示例补片的灰度梯度。渐变方向由箭头表示。
在方程(1)和(2)中,灰度梯度近似于相邻像素之间的灰度差除以这些像素在x和y方向上的距离。接下来,基于灰度梯度向量,我们可以计算方向和大小,如下方程所示:
计算灰度梯度的方法有很多种。最简单的方法是简单地计算相邻像素之间的灰度差。然而,这种方法对噪音非常敏感,在实践中应避免使用。在本节的后面,我们将介绍一个经过验证的标准方法,Sobel运算符。
图像滤波器和高斯平滑
在进一步讨论梯度计算之前,我们需要考虑噪声,噪声存在于所有图像中(人工图像除外),并且随着光强的增加而减小。为了抵消噪声,特别是在低光照条件下,在进行梯度计算之前,必须对图像应用平滑算子。通常,高斯滤波器用于此目的,它在图像上移动并与图像下的强度值相结合。为了正确地参数化过滤器,必须调整两个参数:
标准偏差,它控制图像平面中过滤器的空间扩展。标准偏差越大,过滤器覆盖的区域越宽。
内核大小,它定义中心位置周围有多少像素将有助于平滑操作。
下图显示了三个具有不同标准偏差的高斯滤波核。
高斯平滑的工作原理是根据高斯曲线在每个点的高度为每个像素分配一个周围像素的加权和。最大的贡献将来自中心像素本身,而像素周围的贡献将根据高斯曲线的高度而减少,从而降低其标准偏差。可以很容易地看出,当标准偏差较大时(左图),中心位置周围像素的贡献也会增加。
应用高斯滤波器(或任何其他滤波器)的工作分为四个连续步骤,如下图所示:
1.创建具有所需属性的过滤内核(例如高斯平滑或边缘检测)
2.在内核中定义锚点(通常是中心位置),并将其放置在图像的第一个像素的顶部。
3.计算核系数与下面相应图像像素值的乘积之和。
4.将结果放到输入图像中内核锚定的位置。
5. 对整个图像的所有像素重复此过程。
下图说明了将(黄色)过滤器内核逐行移动到图像上,并将二维和H(x,y)的结果分配给每个像素位置的过程。
高斯平滑的过滤核如下图所示。在(a)中,显示了一条三维高斯曲线,在(b)中,可以看到相应的离散滤波核,其中心锚点(41)对应于高斯曲线的最大值,并朝着(近似)圆形的边缘减小值。
高斯平滑——c++:
#include <iostream>
#include <numeric>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
void gaussianSmoothing1()
{
// load image from file
cv::Mat img;
img = cv::imread("../images/img1gray.png");
// create filter kernel
float gauss_data[25] = {1, 4, 7, 4, 1,
4, 16, 26, 16, 4,
7, 26, 41, 26, 7,
4, 16, 26, 16, 4,
1, 4, 7, 4, 1};
cv::Mat kernel = cv::Mat(5, 5, CV_32F, gauss_data);
// apply filter
cv::Mat result;
cv::filter2D(img, result, -1, kernel, cv::Point(-1, -1), 0, cv::BORDER_DEFAULT);
// show result
string windowName = "Gaussian Blurring";
cv::namedWindow(windowName, 1); // create window
cv::imshow(windowName, result);
cv::waitKey(0); // wait for keyboard input before continuing
}
int main()
{
gaussianSmoothing1();
}
计算灰度梯度
在稍微平滑图像以减少噪声影响后,我们现在可以计算图像在x和y方向的灰度梯度。在文献中,有几种方法可以用于梯度计算。其中最著名的是Sobel算子(1968年提出),但还有一些其他的,例如Scharr算子,它针对旋转对称进行了优化。
Sobel运算符基于在水平和垂直方向上应用小的整数值过滤器。运算符是3x3个内核,一个用于x的渐变,一个用于y的渐变。两个内核如下所示。
在下面的代码中,Sobel运算符的一个内核被应用于一个图像。请注意,它已转换为灰度,以避免计算每个颜色通道上的运算符。此代码在sobel.cpp文件的渐变中找到。您可以使用Gradient Sobel可执行文件来运行代码。
只在x方向做sobel变换——c++
// load image from file
cv::Mat img;
img = cv::imread("./img1.png");
// convert image to grayscale
cv::Mat imgGray;
cv::cvtColor(img, imgGray, cv::COLOR_BGR2GRAY);
// create filter kernel
float sobel_x[9] = {-1, 0, +1,
-2, 0, +2,
-1, 0, +1};
cv::Mat kernel_x = cv::Mat(3, 3, CV_32F, sobel_x);
// apply filter
cv::Mat result_x;
cv::filter2D(imgGray, result_x, -1, kernel_x, cv::Point(-1, -1), 0, cv::BORDER_DEFAULT);
// show result
string windowName = "Sobel operator (x-direction)";
cv::namedWindow( windowName, 1 ); // create window
cv::imshow(windowName, result_x);
cv::waitKey(0); // wait for keyboard input before continuing
生成的渐变图像如下所示。可以看出,具有灰度局部对比度的区域,如前面车辆的投影阴影,会导致滤波图像中的高值。
注意,在上面的代码中,只有Sx过滤内核应用,这就是为什么投影只在X方向显示的原因。应用Sy是对图像产生以下结果:
在x和y方向都做sobel变换——c++
#include <iostream>
#include <numeric>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
void magnitudeSobel()
{
// load image from file
cv::Mat img;
img = cv::imread("../images/img1gray.png");
// convert image to grayscale
cv::Mat imgGray;
cv::cvtColor(img, imgGray, cv::COLOR_BGR2GRAY);
// apply smoothing
cv::Mat blurred = imgGray.clone();
int filterSize = 5;
int stdDev = 2.0;
cv::GaussianBlur(imgGray, blurred, cv::Size(filterSize, filterSize), stdDev);
// create filter kernels
float sobel_x[9] = {-1, 0, +1, -2, 0, +2, -1, 0, +1};
cv::Mat kernel_x = cv::Mat(3, 3, CV_32F, sobel_x);
float sobel_y[9] = {-1, -2, -1, 0, 0, 0, +1, +2, +1};
cv::Mat kernel_y = cv::Mat(3, 3, CV_32F, sobel_y);
// apply filter
cv::Mat result_x, result_y;
cv::filter2D(blurred, result_x, -1, kernel_x, cv::Point(-1, -1), 0, cv::BORDER_DEFAULT);
cv::filter2D(blurred, result_y, -1, kernel_y, cv::Point(-1, -1), 0, cv::BORDER_DEFAULT);
// compute magnitude image
cv::Mat magnitude = imgGray.clone();
for (int r = 0; r < magnitude.rows; r++)
{
for (int c = 0; c < magnitude.cols; c++)
{
magnitude.at<unsigned char>(r, c) = sqrt(pow(result_x.at<unsigned char>(r, c), 2) +
pow(result_y.at<unsigned char>(r, c), 2));
}
}
// show result
string windowName = "Gaussian Blurring";
cv::namedWindow(windowName, 1); // create window
cv::imshow(windowName, magnitude);
cv::waitKey(0); // wait for keyboard input before continuing
}
int main()
{
magnitudeSobel();
}
在x和y方向都应用sobel变换的效果如下图所示: