OpenCV图像特征提取
Camera系列文章
基于多传感器的数据整合方法被广泛应用于环境感知领域;为此我们首先介绍与Camera相关的知识体系;涵盖摄像头功能分析以及OpenCV图像识别的基础理论;随后通过结合激光雷达生成的点云数据与摄像头捕捉的画面信息实现目标跟踪与分类。
系列文章目录
1. 摄像头基础知识与校准
2. 基于Lidar的TTC估计方法
3. 基于摄像头的TTC估计方法
4. 基于OpenCV的图像特征提取技术
文章目录
- 相机系列综述
- 序论
- 基础知识介绍
- 强度梯度阐述
-
图像平滑与高斯滤波
-
强度梯度计算方法
-
二、Harris角点检测
-
- 1.特征点唯一性的度量
- 2.Harris角点检测实例
- 3. NMS non-maxima suppression
-
前言
在接下来的几节中我们将探讨识别方法以及可靠的追踪机制来跟踪连续帧中的图像特征。主要内容包括:
- 进行图像梯度计算与图像平滑处理,并准确表征特征的关键属性;
- 掌握利用强度信息定位关键点的方法,并熟悉Harris检测算法;
- 实现关键点检测:掌握广泛采用的关键点检测算法及其几何变换条件,在不同场景下的适应性表现;
- 采用描述符模型追踪连续帧中的关键点位置,并建立基于关键点的车辆运动参数模型。
本节主要覆盖前两点。
鉴于图像识别的核心知识众多,在此仅作简要概述。此外,由于当前的图像识别基础较为薄弱,在表述时可能会出现不够准确的情况,请多包涵。OpenCV提供了丰富的算法库用于检测图像的关键特征(Keypoint Detection),随后提取这些特征(Feature extraction),从而形成有效的图像描述符(descriptor)。
一、预备知识
特征定义
我们选择图像中的特定区域作为目标特征求解对象;这些区域具有重要的意义或特性;通常来说;角点和高密度区域常被视作有效的候选;而大量重复出现或低密度度较低的领域则不具备足够的代表性;边缘能够将图像分割为两个部分;因此也适合作为分割后的独立单元;斑点也是一种有意义的目标域
二、强度梯度和图像过滤- 寻找Keypoints特征点
如同上一章所述,在计算机视觉领域中

强度梯度介绍
该图像中红线范围内的强度值为Intensity。其梯度变化量为intensity gradient。观察到在这些关键点(边缘、角点等)处出现了显著的梯度变化。

下面我们看一下梯度的计算公式。

Intensity gradient for three patches and partial derivatives

Equations for gradient direction and magnitude.
在传统方法中,在实际应用中直接通过相邻像素之间的亮度差异来计算其梯度可能会导致结果容易受到图像噪声的影响。特别是在光照条件较差的情况下,在这种情况下仅凭相邻像素间的亮度变化难以获得准确的边缘检测结果。
图像去噪和高斯滤波
为了解决图像去噪问题,在实际应用中常用的一种方法是高斯滤波器。它通过高斯内核进行处理(其中核由一组加权系数构成,在计算当前像素值时考虑相邻像素点的影响;σ参数决定了权重分布范围;较大的σ值意味着越远的像素点具有更大的权重;同时卷积核的大小也反映了参与平滑运算的像素点数量)。
下图是不同偏差的高斯滤波器内核。

如图所示为图像处理公式,在周围像素点的基础上结合其权重值进行重新计算。其中k(i,j)表示该像素点所具有的权重值,并基于a_i和a_j分别代表计算点在行和列方向上的坐标位置信息

下图是7×7内核的示例。

以下代码演示高斯滤波的具体操作步骤。首先,在加载图片后生成自定义的高斯内核之后,在生成之后再调用OpenCV中的Filter2D函数来实现对图像的滤波处理过程。
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);
// STUDENTS NEET TO ENTER THIS CODE
for (int i = 0; i < 25; i++)
{
gauss_data[i] /= 273;
}
// EOF STUDENT CODE
// 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
}
强度梯度计算
通过滤波降噪处理图像后,在后续步骤中我们可以计算出图像x和y方向上的强度梯度值。其中著名的是Sobel算子和Scharr算子两种方法。以下给出Sobel算子在x方向上的3×3内核矩阵:

以下是对图像进行x方向梯度计算的代码。
// load image from file to avoid computing the operator on each color channel.
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
得到的梯度图如下,分别为①x方向梯度图;②x,y方向梯度图。


二、Harris角点检测
如上文所述,在上一节中我们已经了解到,在图像的一些特定位置(如边角点等)会显示出较大的梯度变化特性。接下来,我们将利用这些梯度信息来确定这些特征点,并详细介绍Harris角点检测算法的基本原理。
该算法的核心思想是通过分析像素之间的差异来定位图像中显著变化的区域。具体而言,在该方法中我们主要关注的是基于图像边缘信息进行计算的结果。该方法通过计算相邻像素之间的差异值来确定哪些区域的变化最为显著,并将这些位置标记为角点候选区域。
1.特征点唯一性的度量
关键点检测的原理是识别图像中可以定位特征点x、y位置的独特结构,并将这些结构称为关键点。下图展示了角点的示意图:红色表示在该方向上没有独特特征(梯度为零),而绿色表示存在独特特征。

为了确定角点的位置, 必须将红框W沿着其边缘逐一计算梯度. 通常采用的方式是在红框区域内比较相邻像素的强度差异以确定边缘. 公式如下

首先使用泰勒公式展开:

再代入方差公式。其中H为协方差矩阵。

通过可视化协方差矩阵H的形式可以看出,在垂直于该直线的方向上呈现出最大的梯度变化率,并对应着长边方向上的最大特征向量;而在平行于该直线的方向上呈现出最小的梯度变化率并对应着短边方向上的最小特征向量;从而为了检测关键点应确定其对应的最大特征向量

协方差矩阵H的特征向量计算公式如下:

Harris角点检测算法中,也使用了高斯窗w(x,y)设置权重计算强度梯度。

2.Harris角点检测实例
Harris角点检测主要依据以下公式确定每个像素的位置响应度。k因子通常取值于0.04至0.06

在这些示例中采用cornerHarris算法来进行角点检测。该函数的关键参数是apertureSize这一项指标,在Sobel梯度计算中决定了核的尺寸大小,并规定了其取值必须为介于3到31之间的奇数值。
// 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);
// Detector parameters
int blockSize = 2; // for every pixel, a blockSize × blockSize neighborhood is considered
int apertureSize = 3; // aperture parameter for Sobel operator (must be odd)
int minResponse = 100; // minimum value for a corner in the 8bit scaled response matrix
double k = 0.04; // Harris parameter (see equation for details)
// Detect Harris corners and normalize output
cv::Mat dst, dst_norm, dst_norm_scaled;
dst = cv::Mat::zeros(imgGray.size(), CV_32FC1 );
cv::cornerHarris( imgGray, dst, blockSize, apertureSize, k, cv::BORDER_DEFAULT );
cv::normalize( dst, dst_norm, 0, 255, cv::NORM_MINMAX, CV_32FC1, cv::Mat() );
cv::convertScaleAbs( dst_norm, dst_norm_scaled );
// visualize results
string windowName = "Harris Corner Detector Response Matrix";
cv::namedWindow( windowName, 4 );
cv::imshow( windowName, dst_norm_scaled );
cv::waitKey(0);
输出图像如下,像素点位置越亮,Harris角点响应越高。

3. NMS non-maxima suppression
基于Harris角点检测的方法我们成功地获得了多个关键点。为了准确确定这些关键点的位置,在定位每个关键点时需要考虑其邻域区域中可能存在其他潜在的关键特征这一问题。为此我们需要借助NMS(Non-Maximum Suppression)技术来消除邻域区域内可能存在的干扰特征从而精确定位出每个关键区域的核心特征即为该区域中最突出的一系列代表特征。为了确定最终的关键位置我们可以计算各候选区域的最大响应值并通过比较得出最终的关键位置坐标值即为该区域中最亮的一系列代表特征的位置坐标值。
void cornernessHarris()
{
// load image from file
cv::Mat img;
img = cv::imread("../images/img1.png");
cv::cvtColor(img, img, cv::COLOR_BGR2GRAY); // convert to grayscale
// Detector parameters
int blockSize = 2; // for every pixel, a blockSize × blockSize neighborhood is considered
int apertureSize = 3; // aperture parameter for Sobel operator (must be odd)
int minResponse = 100; // minimum value for a corner in the 8bit scaled response matrix
double k = 0.04; // Harris parameter (see equation for details)
// Detect Harris corners and normalize output
cv::Mat dst, dst_norm, dst_norm_scaled;
dst = cv::Mat::zeros(img.size(), CV_32FC1);
cv::cornerHarris(img, dst, blockSize, apertureSize, k, cv::BORDER_DEFAULT);
cv::normalize(dst, dst_norm, 0, 255, cv::NORM_MINMAX, CV_32FC1, cv::Mat());
cv::convertScaleAbs(dst_norm, dst_norm_scaled);
// visualize results
string windowName = "Harris Corner Detector Response Matrix";
cv::namedWindow(windowName, 4);
cv::imshow(windowName, dst_norm_scaled);
cv::waitKey(0);
// STUDENTS NEET TO ENTER THIS CODE (C3.2 Atom 4)
// Look for prominent corners and instantiate keypoints
vector<cv::KeyPoint> keypoints;
double maxOverlap = 0.0; // max. permissible overlap between two features in %, used during non-maxima suppression
for (size_t j = 0; j < dst_norm.rows; j++)
{
for (size_t i = 0; i < dst_norm.cols; i++)
{
int response = (int)dst_norm.at<float>(j, i);
if (response > minResponse)
{ // only store points above a threshold
cv::KeyPoint newKeyPoint;
newKeyPoint.pt = cv::Point2f(i, j);
newKeyPoint.size = 2 * apertureSize;
newKeyPoint.response = response;
// perform non-maximum suppression (NMS) in local neighbourhood around new key point
bool bOverlap = false;
for (auto it = keypoints.begin(); it != keypoints.end(); ++it)
{
double kptOverlap = cv::KeyPoint::overlap(newKeyPoint, *it);
if (kptOverlap > maxOverlap)
{
bOverlap = true;
if (newKeyPoint.response > (*it).response)
{ // if overlap is >t AND response is higher for new kpt
*it = newKeyPoint; // replace old key point with new one
break; // quit loop over keypoints
}
}
}
if (!bOverlap)
{ // only add new key point if no overlap has been found in previous NMS
keypoints.push_back(newKeyPoint); // store new keypoint in dynamic list
}
}
} // eof loop over cols
} // eof loop over rows
// visualize keypoints
windowName = "Harris Corner Detection Results";
cv::namedWindow(windowName, 5);
cv::Mat visImage = dst_norm_scaled.clone();
cv::drawKeypoints(dst_norm_scaled, keypoints, visImage, cv::Scalar::all(-1), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
cv::imshow(windowName, visImage);
cv::waitKey(0);
// EOF STUDENT CODE
}
// this function illustrates a very simple non-maximum suppression to extract the strongest corners
// in a local neighborhood around each pixel
cv::Mat PerformNMS(cv::Mat corner_img)
{
// define size of sliding window
int sw_size = 7; // should be odd so we can center it on a pixel and have symmetry in all directions
int sw_dist = floor(sw_size / 2); // number of pixels to left/right and top/down to investigate
// create output image
cv::Mat result_img = cv::Mat::zeros(corner_img.rows, corner_img.cols, CV_8U);
// loop over all pixels in the corner image
for (int r = sw_dist; r < corner_img.rows - sw_dist - 1; r++) // rows
{
for (int c = sw_dist; c < corner_img.cols - sw_dist - 1; c++) // cols
{
// loop over all pixels within sliding window around the current pixel
unsigned int max_val{0}; // keeps track of strongest response
for (int rs = r - sw_dist; rs <= r + sw_dist; rs++)
{
for (int cs = c - sw_dist; cs <= c + sw_dist; cs++)
{
// check wether max_val needs to be updated
unsigned int new_val = corner_img.at<unsigned int>(rs, cs);
max_val = max_val < new_val ? new_val : max_val;
}
}
// check wether current pixel is local maximum
if (corner_img.at<unsigned int>(r, c) == max_val)
result_img.at<unsigned int>(r, c) = max_val;
}
}
// visualize results
std::string windowName = "NMS Result Image";
cv::namedWindow(windowName, 5);
cv::imshow(windowName, result_img);
cv::waitKey(0);
return result_img;
}
