传感器融合是将多个传感器采集的数据进行融合处理,以更好感知周围环境;这里首先介绍Camera的相关内容,包括摄像头及图像知识基本介绍,OpenCV图像识别(特征提取,目标分类等),融合激光点云和图像进行TTC估计。
系列文章目录
1. 摄像头基础及校准
2. Lidar TTC估计
3. Camera TTC估计
4. OpenCV图像特征提取
后续这几节中我们将讨论如何识别并可靠稳定的跟踪连续帧中的图像特征。主要内容包括:
本节主要覆盖前两点。
考虑到图像识别知识点很多,在这里只是简单的把关键内容放上。另外,由于图像识别知识储备相对薄弱,如有描述不合理的地方还请指正。OpenCV提供了很多算法库用于检测图像的主要特征(Keypoint Detection),然后提取这些特征(Feature extraction),使其称为图像描述符(descriptor)
特征定义
我们将图像中的某个特别的区域作为一个特征。特征是图像中有意义的图像区域,该区域具有独特特性或易于识别性。角点和高密度区域是很好的特征,而大量重复的区域或低密度区域则不是很好的特征。边缘可将图像分为两个区域,因此也可作为好的特征。斑点也是有意义的特征。
特征提取
将原始特征转换为一组具有明显物理意义、几何特征(角点,不变量)、纹理(LBP HOG)、统计意义或核的特征;
特征选择
从特征几何中挑选一组最具统计意义的特征,达到降维的目的。通过合适的特征选择可以减少数据存储和输入数据带宽,减少冗余,发现更有意义的潜在变量。由于某些算法在识别和提取某种类型特征的时候有较好的效果,所以输入图像是何种类型特征很重要,有利于选择最合适的特征检测算法。
如前一章所述,摄像头无法直接测距,计算TTS是基于目标物在图像中投影的尺寸比。因此我们需要定位特征点keypoints,比如以下从之前图片提取的图块,存在类似的特征点。
下图展示的是红线内图像的强度Intensity,强度梯度intensity gradient,可以看到,在特征点(边,角等)处存在明显的梯度变化。
通常直接通过相邻像素点的强度差计算强度梯度(Intensity Gradient)会受到图像噪点(如白噪声等)的影响,尤其是低照度环境下,因此我们需要对图像进行滤波,比如高斯滤波,对图像进行去噪,再进行梯度计算。
为了消除噪声的影响,需要使用平滑算子对图像进行滤波。通常用的是高斯滤波器,使用高斯内核对图像进行处理(核是一组权重,通过相邻像素点来计算当前像素点的像素值;偏差 σ \sigma σ 代表权重分布,偏差越大,周围像素点的权重越大;核大小表示使用多少周围像素点来进行平滑过滤)。
下图是不同偏差的高斯滤波器内核。
下图是滤波计算公式,通过周围像素点的像素值及权重重新计算像素值。 k ( i , j ) k(i,j) k(i,j)为该像素点的权重, a i a_i ai, a j a_j aj为计算点在像素中的坐标。
下图是7×7内核的示例。
以下代码展示高斯滤波如何操作。加载图片后首先生成自定义的高斯内核,再使用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 x x, y y y方向的强度梯度。比较有名的是Sobel算子和Scharr算子。
以下是Sobel算子 x x x, y y y方向的3×3内核。
以下是对图像进行 x x 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 x方向梯度图;② x x x, y y y方向梯度图。
由上一节可知,图像边角点等位置具有较大强度梯度,下面我们将使用这些梯度等信息来定义这些特征点,并介绍最常用的Harris角点检测算法。
Harris角点检测算法的基本原理是通过对比与周围像素点的差异来寻找图像中明显变化的区域,以图像梯度作为输入,输出在x、y方向梯度都比较大的位置点。
关键点检测的想法是检出图像中可以定位特征点x、y位置的独特结构,如角点。下图是角点的示意图,红色代表在该方向没有独特的特征(梯度为0),绿色代表有独特特征。
为定位角点,我们需要移动红框W并逐个计算梯度。常用的方法是计算红框内相邻点的平方差,公式如下。
首先使用泰勒公式展开:
再代入方差公式。其中H为协方差矩阵。
以下对协方差矩阵H进行可视化,由此可知,垂直于直线方向梯度变化率最大,对应长边的特征向量。沿着直线方向梯度变化率最小,对应短边特征向量。因此,为检出检点,需要找出H的特征向量极大值。
协方差矩阵H的特征向量计算公式如下:
Harris角点检测算法中,也使用了高斯窗 w ( x , y ) w(x,y) w(x,y)设置权重计算强度梯度。
Harris角点检测主要基于以下公式计算每个像素点位置的角点响应值。k因子通常取值为0.04 - 0.06。
以下例子使用cornerHarrisl来识别角点。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角点检测我们获取了一系列的亮点。考虑到角点附近局部区域可能存在的亮点,要定位角点,还需要使用NMS非最大抑制提取角点,选出局部区域最亮的点作为角点。
代码如下(示例):
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;
}