视觉Slam14讲笔记第7讲视觉里程计-特征提取与匹配
1.特征点法
特征检测指的是在图像中寻找具有显著性的、可重复检测的关键点(KeyPoints)。这些关键点通常是图像中具有独特性质的局部区域。
关键点类型:
角点:两条边缘的交点,如建筑物的拐角
斑点:与周围区域有明显差异的区域,如纹理丰富的区域
边缘:图像中亮度变化剧烈的区域
关键点属性:
位置 (x, y坐标)
尺度 (在不同尺度下都能检测到)
方向 (具有旋转不变性)
响应强度 (特征的显著性程度)
描述子 (Descriptor)
描述子是对关键点周围区域的数学描述,它是一个向量,用于表示关键点的特征。描述子的目的是让计算机能够比较不同图像中的关键点是否匹配。
描述子的作用
将关键点周围的视觉信息编码成数值向量
便于计算机进行相似性比较
实现不同图像间关键点的匹配
描述子特性:
独特性:不同关键点应该有不同描述子
不变性:对光照、旋转、尺度变化不敏感
紧凑性:向量长度适中,便于存储和计算
描述子的矩阵存储结构
矩阵的维度含义:
行数 (rows):等于关键点的数量
列数 (cols):等于描述子的维度(长度)
具体示例:
假设在第一张图像中检测到100个关键点,使用ORB特征(256位描述子):
descriptors_1.rows = 100(100个关键点)
descriptors_1.cols = 32(256位 = 32字节)
匹配
遍历所有描述子,求得描述子之间的最小汉明距离
opencv实现版本如下:
#include <chrono>
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/highgui/highgui.hpp>using namespace std;
using namespace cv;int main(int argc, char **argv) {if (argc != 3) {cout << "usage: feature_extraction img1 img2" << endl;return 1;}//--读取图像Mat img_1 = imread(argv[1], IMREAD_COLOR);Mat img_2 = imread(argv[2], IMREAD_COLOR);assert((img_1.data != nullptr && img_2.data != nullptr));//--初始化std::vector<KeyPoint> keypoints_1, keypoints_2;Mat descriptors_1, descriptors_2;Ptr<FeatureDetector> detector = ORB::create();Ptr<DescriptorExtractor> descriptor = ORB::create();Ptr<DescriptorMatcher> matcher =DescriptorMatcher::create("BruteForce-Hamming");//--步骤1:检测 Oriented FAST 角点位置chrono::steady_clock::time_point t1 = chrono::steady_clock::now();detector->detect(img_1, keypoints_1);detector->detect(img_2, keypoints_2);//--步骤2:根据角点位置计算BRIEF描述子descriptor->compute(img_1, keypoints_1, descriptors_1);descriptor->compute(img_2, keypoints_2, descriptors_2);chrono::steady_clock::time_point t2 = chrono::steady_clock::now();chrono::duration<double> time_used =chrono::duration_cast<chrono::duration<double>>(t2 - t1);cout << "extract ORB cost = " << time_used.count() << " seconds." << endl;Mat outimg1;drawKeypoints(img_1, keypoints_1, outimg1, Scalar::all(-1),DrawMatchesFlags::DEFAULT);imshow("ORB Features", outimg1);//--步骤3:对两幅图像中的BRIEF描述子进行匹配, 使用 Hamming 距离vector<DMatch> matches;t1 = chrono::steady_clock::now();matcher->match(descriptors_1, descriptors_2, matches);t2 = chrono::steady_clock::now();time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1);cout << "match ORB cost = " << time_used.count() << " seconds." << endl;//--步骤4:匹配点对筛选// 计算最小距离和最大距离auto min_max = minmax_element(matches.begin(), matches.end(),[](const DMatch &m1, const DMatch &m2) {return m1.distance < m2.distance;});double min_dist = min_max.first->distance;double max_dist = min_max.second->distance;printf("-- Max dist : %f \n", max_dist);printf("-- Min dist : %f \n", min_dist);// 当描述子之间的距离大于两倍的最小距离时, 则认为匹配有误.// 但有时候最小距离会非常小, 设置一个经验值30作为下限.std::vector<DMatch> good_matches;for (int i = 0; i < descriptors_1.rows; i++) {if (matches[i].distance <= max(2 * min_dist, 30.0)) {good_matches.push_back(matches[i]);}}//--步骤5:绘制匹配结果Mat img_match;Mat img_goodmatch;drawMatches(img_1, keypoints_1, img_2, keypoints_2, matches, img_match);drawMatches(img_1, keypoints_1, img_2, keypoints_2, good_matches,img_goodmatch);imshow("all matches", img_match);imshow("good matches", img_goodmatch);waitKey(0);
}
实现结果
extract ORB cost = 0.104123 seconds.
match ORB cost = 0.000353772 seconds.
-- Max dist : 94.000000
-- Min dist : 4.000000
关键点

匹配结果


二、手写ORB匹配算法
说明:
ORB_pattern的设计:
1.mean(均值)的讲究:
接近0的均值:表示这对像素点的亮度差异在统计上接近平衡
避免极端偏向:确保比较结果不是总是偏向某一方
提高区分度:平衡的对比能更好地区分不同特征
2.correlation(相关性)的讲究:
低相关性:点对之间应该尽可能独立
避免冗余:高相关性的点对提供相似信息,浪费描述子位数
最大化信息量:每个位应该提供独特的信息
3.覆盖不同尺度:
点对分布在关键点周围的不同距离上
有些点对距离较近(捕捉局部细节)
有些点对距离较远(捕捉更大范围的上下文)
4.方向多样性:
点对分布在各个方向上
确保对旋转变化具有鲁棒性
避免所有点对都集中在某个方向
5.距离分布的讲究:
观察一些点对的距离:
(8,-3)和(9,5):距离较近,捕捉细微纹理
(4,2)和(7,-12):距离较远,捕捉宏观特征
这种多尺度设计提高了特征的稳定性
如何做到旋转不变性:
通过灰度质心法计算关键点方向
将比较点对根据关键点方向进行旋转
确保描述子对图像旋转具有鲁棒性
代码:
void ComputeORB(const cv::Mat &img, std::vector<cv::KeyPoint> &keypoints,vector<DescType> &descriptors) {const int half_patch_size = 8;const int half_boundary = 16;int bad_points = 0;// 检查关键点是否离图像边界是否足够远(16像素)// 如果太靠近边界,无法提取完整的16*16像素块for (auto &kp : keypoints) {if (kp.pt.x < half_boundary || kp.pt.y < half_boundary ||kp.pt.x >= img.cols - half_boundary ||kp.pt.y >= img.rows - half_boundary) {bad_points++;descriptors.push_back({});continue;}// 通过灰度质心计算关键点的方向角度float m01 = 0, m10 = 0;for (int dx = -half_patch_size; dx < half_patch_size; ++dx) {for (int dy = -half_patch_size; dy < half_patch_size; ++dy) {uchar pixel = img.at<uchar>(kp.pt.y + dy, kp.pt.x + dx);m01 += dx * pixel;m10 += dy * pixel;}}// angle should be arc tan(m01/m10)float m_sqrt = sqrt(m01 * m01 + m10 * m10);float sin_theta = m01 / m_sqrt;float cos_theta = m10 / m_sqrt;// compute the angle of this point// 8个uint32_t共256位DescType desc(8, 0);for (int i = 0; i < 8; i++) {uint32_t d = 0;// 按照预定义模式遍历32个像素点对for (int k = 0; k < 32; k++) {int idx_pq = i * 32 + k;cv::Point2f p(ORB_pattern[idx_pq * 4], ORB_pattern[idx_pq * 4 + 1]);cv::Point2f q(ORB_pattern[idx_pq * 4 + 2], ORB_pattern[idx_pq * 4 + 3]);// rotate with thetacv::Point2f pp = cv::Point2f(cos_theta * p.x - sin_theta * p.y,sin_theta * p.x + cos_theta * p.y) +kp.pt;cv::Point2f qq = cv::Point2f(cos_theta * q.x - sin_theta * q.y,sin_theta * q.x + cos_theta * q.y) +kp.pt;// 比较两个像素点的亮度// 设置第k位为1if (img.at<uchar>(pp.y, pp.x) < img.at<uchar>(qq.y, qq.x)) {d |= 1 << k;}}desc[i] = d;}descriptors.push_back(desc);}cout << "bad/total: " << bad_points << "/" << keypoints.size() << endl;
}// brute-force matcher
void BfMatch(const vector<DescType> &desc1, const vector<DescType> &desc2,vector<DMatch> &matches) {const int d_max = 40;for (size_t i1 = 0; i1 < desc1.size(); ++i1) {if (desc1[i1].empty()) continue;cv::DMatch m{i1, 0, 256};for (size_t i2 = 0; i2 < desc2.size(); ++i2) {if (desc2[i2].empty()) continue;int distance = 0;for (int k = 0; k < 8; k++) {distance += _mm_popcnt_u32(desc1[i1][k] ^ desc2[i2][k]);}if (distance < d_max && distance < m.distance) {m.distance = distance;m.trainIdx = i2;}}if (m.distance < d_max) {matches.push_back(m);}}
}
