图像的特征点指的就是用图像上的一些有代表性的点来表示这张图像,这些特征点在相机发生少许的位移、旋转和尺度缩放时保持不变。图像特征点提取与匹配是单目视觉SLAM中关键的一步,同时也在很多其他的地方有应用,比如图像拼接、三维重建等等。
常见的特征点描述有:ORB、SIFT、SURF等。本文主要介绍ORB特征点,并通过实例如何使用图像处理库OpenCV进行图像的特征点提取和匹配。
1、ORB特征描述
ORB特征由关键点和描述子两部分组成。它的关键点称为"Oriented FAST",是一种改进的FAST角点;描述子称为BRIEF,用来描述关键点周围的像素。
FAST角点
FAST角点主要用来检测局部像素灰度变化明显的地方,它的思想是:如果一个像素与它领域的像素差别较大(过亮或者过暗),那么它就有可能是角点。
FAST角点的检测过程:
- 在图像中选取像素p,假设它的亮度为Ip
- 设定一个阈值T(比如Ip的20%)
- 以像素p为圆心,选取半径为3的圆上的16个像素点
- 假如选取的圆上的16个像素点中,有连续的N个像素点的亮度大于Ip+T或Ip−T,那么就认为像素p是角点(N通常取12,对应的特征点称为FAST-12)
- 循环上面的步骤1-4,对每一个像素执行相同的操作
FAST角点检测预测试: 对于每个像素,先直接检测圆上的第1、5、9、13个像素的亮度,只有当这4个像素中有3个同时大于Ip+T或Ip−T,它才有可能是一个FAST角点;否则,将该像素直接排除。
非极大值抑制避免角点扎堆: 在一定区域内保留相应极大值的角点,避免角点集中。
FAST角点的缺陷:
- 特征点数量很大且不确定(ORB对其做了改进)
- 对旋转和尺度不具有描述性
(1-2) ORB使用的改进的FAST角点
添加对尺度的描述: 对图像构建金字塔,并在金字塔的每一层上检测角点添加对方向的描述: 计算区块的灰度质心,与区块的几何质心形成方向向量,得到特征点的方向具体的过程如下:在一个小的图像块B中,定义图像块的矩为:
于是图像块的灰度质心为:
连接图像的几何中心O和质心C, 得到方向向量OC,于是特征点的方向为:
(1-3) ORB所使用的描述子BRIEF
Brief描述子是一种二进制描述子,它是由128个0/1所组成的描述向量。0/1编码了关键点附近两个像素(比如p、q)的大小关系,如果p>q,则取1;否则取0。一共取了128对这样的p、q。
原始的Brief描述子是不具有旋转不变性的,因为在提取FAST关键点的时候已经计算出来了关键点的方向,所以可以利用方向信息,计算旋转之后的"Steer Brief"特征,使用Brief具有较好的旋转不变性。
综上所述,ORB在平移、旋转、缩放的变换下都具有良好的性能。
2、ORB特征匹配
暴力匹配: 将图像1的所有特征点依次和图像2的所有特征点的描述子计算距离,然后排序,取最近的一个作为匹配点。
若描述子为浮点类型,则使用欧式距离;对于ORB使用的Brief描述子(二进制),使用汉明距离,指的是不同位数的个数。
暴力匹配的结果中含有很多的误匹配结果,在实际使用时经常要采取一些方法去除误匹配,最好用的是RANSAC算法。但是下面的例子中只是通过最小距离筛选了一次,不适用所有的情况。
3、ORB特征提取与匹配实践
#include #include #include using namespace std;void drawKeyPoints(const cv::Mat& image, const vector<:keypoint>& keypoints) {cv::Mat destImage = image.clone();for (int i = 0; i < keypoints.size(); i++) {cv::Point2f p = keypoints[i].pt;cv::circle(destImage, p, 2, cv::Scalar(0, 0, 255), 1);}cv::namedWindow("Keypoints", cv::WINDOW_NORMAL);cv::imshow("Keypoints", destImage);cv::waitKey(0);cv::destroyWindow("Keypoints");}void drawMatchKeyPoints(const cv::Mat& srcImage, const vector<:keypoint>& kps1, const cv::Mat& dstImage, const vector<:keypoint>& kps2,const vector<:dmatch>& matches) {cv::Mat srcImageCopy = srcImage.clone();cv::Mat dstImageCopy = dstImage.clone();cv::Mat matchImage(srcImageCopy.rows, srcImageCopy.cols * 2,srcImageCopy.type());cv::Mat left = cv::Mat(matchImage, cv::Rect(0, 0, srcImageCopy.cols, srcImageCopy.rows)); cv::Mat right = cv::Mat(matchImage, cv::Rect(srcImageCopy.cols, 0, dstImageCopy.cols, dstImage.rows)); srcImageCopy.copyTo(left); dstImageCopy.copyTo(right);for(int i=0; i& keypoints, cv::Mat& desc) {cv::Ptr<:featuredetector> extractor = cv::ORB::create();auto detectStart = chrono::high_resolution_clock::now();extractor->detect(image, keypoints);auto detectEnd = chrono::high_resolution_clock::now();double detectTime = chrono::duration_cast<:milliseconds>(detectEnd - detectStart).count();cout << "detectTime: " << detectTime << "ms..." << endl;cv::Ptr<:descriptorextractor> compute = cv::ORB::create();auto computeStart = chrono::high_resolution_clock::now();compute->compute(image, keypoints, desc);auto computeEnd = chrono::high_resolution_clock::now();double computeTime = chrono::duration_cast<:milliseconds>(computeEnd - computeStart).count();cout << "computeTime: " << computeTime << "ms..." << endl;}int matchKeyPoints( const cv::Mat& image1, const vector<:keypoint>& keypoint1, const cv::Mat& desc1, const cv::Mat& image2, const vector<:keypoint>& keypoint2, const cv::Mat& desc2, vector<:dmatch>& matchesNoFilter, vector<:dmatch>& matchesFilter) { // 创建使用"汉明距离"的暴力匹配器 cv::Ptr<:descriptormatcher> matcher = cv::DescriptorMatcher::create("BruteForce-Hamming"); matcher->match(desc1, desc2, matchesNoFilter);// lamda表达式auto getMinimumDistance = [](const cv::Mat& desc, const vector<:dmatch>& matches) -> double {double min_dis = 10000.;for(int i = 0; i < desc.rows; i++) {min_dis = min_dis < matches[i].distance ? min_dis : matches[i].distance;}return min_dis;};double min_dist = getMinimumDistance(desc1, matchesNoFilter);// lamda表达式auto getRightMatches = [](const double min_dis, const vector<:dmatch>& matches, vector<:dmatch>& goodMatches) -> void {for(int i = 0; i < matches.size(); i++) { //当描述子之间的距离大于两倍的最小距离时,即认为匹配有误.但有时候最小距离会非常小,设置一个经验值30作为下限.if(matches[i].distance <= max(2*min_dis, 30.0)) goodMatches.push_back(matches[i]);}};getRightMatches(min_dist, matchesNoFilter, matchesFilter); return 0;}int main() {cv::Mat image1 = cv::imread("1.png");cv::Mat image2 = cv::imread("2.png"); if(image1.empty() || image2.empty()) { cout << "Load image failed..." << endl; return -1; }vector<:keypoint> keypoint1, keypoint2;cv::Mat desc1, desc2; // 特征点提取extractKeypoints(image1, keypoint1, desc1);drawKeyPoints(image1, keypoint1); extractKeypoints(image2, keypoint2, desc2); drawKeyPoints(image2, keypoint2);// 特征点匹配vector<:dmatch> matchesNoFilter, matchesFilter;matchKeyPoints(image1, keypoint1, desc1, image2, keypoint2, desc2, matchesNoFilter, matchesFilter);drawMatchKeyPoints(image1, keypoint1, image2, keypoint2, matchesNoFilter);drawMatchKeyPoints(image1, keypoint1, image2, keypoint2, matchesFilter);return 0;}
结果:
特征点提取的结果:
特征点匹配后没有过滤误匹配的结果:
过滤了误匹配之后特征点匹配结果:
得到了正确的匹配点之后,就可以根据这些匹配的点做很多其他的事情了,比如说估计位姿、三角测量等等,这些主要是在视觉SLAM中的应用。
今天的内容就到这儿了。如果对我的推、文有兴趣,欢迎转、载分、享。也可以推荐给朋友关、注哦。只推干货,宁缺毋滥。