书籍介绍:视觉SLAM十四讲-从理论到实践(高翔 张涛 等著)第二版
#include
#include
#include
#include
#include
:C++ 标准库的头文件,提供了输入输出流的功能,例如 std::cout 和 std::cin。
:OpenCV 库的核心功能头文件,包含了 OpenCV 的基本数据结构和核心函数。
:OpenCV 库中用于特征检测和描述子的头文件。
:OpenCV 库中用于图像显示和用户交互的头文件。
:C++ 标准库的头文件,提供了时间和日期的处理功能
using namespace std;
using namespace cv;
包含第一个命名空间可以省去C++标准库中定义的类型和函数的std::前缀;
包含第二个命名空间也可以省去opencv库中的cv::前缀来使用这些类型和函数,例如,cv::Mat可以写成Mat,cv::imread可以写成imread
int main(int argc, char **argv) {
if (argc != 3) {
cout << "usage: feature_extraction img1 img2" << endl;
return 1;
}
argc 表示命令行参数的数量,argv 是一个指向字符数组的指针数组,其中包含实际的命令行参数
使用 return 1; 终止程序的执行,并返回一个非零值作为退出状态码,表示程序的异常终止
当没有正确指定图像文件路径会弹出该错误提醒并结束程序。
//-- 读取图像
Mat img_1 = imread(argv[1], CV_LOAD_IMAGE_COLOR);
Mat img_2 = imread(argv[2], CV_LOAD_IMAGE_COLOR);
assert(img_1.data != nullptr && img_2.data != nullptr);
imread是OpenCV库中的一个函数,位于
头文件中,用于从文件加载图像数据并将其存储为cv::Mat对象(即矩阵或图像),即读取图像文件,并将像素数据解码为OpenCV可以处理的格式。
函数原型如下:
cv::Mat imread(const cv::String& filename, int flags = cv::IMREAD_COLOR);
filename
:要读取的图像文件的路径和名称。
flags
:可选参数,用于指定图像读取的方式和格式。默认值为cv::IMREAD_COLOR,表示以彩色图像模式读取图像。其他可能的取值包括:
cv::IMREAD_GRAYSCALE:以灰度图像模式读取图像。
gray scale意思为:灰度
cv::IMREAD_UNCHANGED:读取图像的原始格式(unchanged,未变化的),包括图像的通道数和深度。
argv[1]和argv[2]是从命令行参数中获取的文件路径,可以通过这个寻找需要读入的图像文件的位置(为什么这些命令行参数能找到图片文件还不清楚)
如果将上式写为:
Mat img = imread("image.jpg", CV_LOAD_IMAGE_COLOR);
argv[1]
这部分换为"image.jpg"
,则表示从代码文件所在的目录下寻找对应名称的图片文件
CV_LOAD_IMAGE_COLOR
是OpenCV旧版本中使用的一个常量,从OpenCV 3.0版本开始,这个常量已经被废弃,取而代之的是使用IMREAD_COLOR
常量。
assert 是一个宏定义,用于在程序中进行断言检查。
void assert(int expression);
断言是一种在程序中进行条件检查的机制,用于确保某个条件为真。如果断言的条件为假,则会触发断言失败,程序会终止执行并输出相应的错误信息。
可以通过#include
或是#include
调用assert,前者为C++后者为C。
宏、函数、宏函数的区别
//-- 初始化
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");
特征点由关键点(Key-point)和描述子(Descriptor)两部分组成。
std::vector 是 C++ 标准库的一部分,而 KeyPoint 是 OpenCV 的类
Ptr 是 OpenCV 中的智能指针类,用于管理对象的生命周期,避免内存泄漏和手动释放资源的繁琐过程
包含在这个头文件中:
#include
vector(向量)
:keypoints_1 和 keypoints_2 是存储关键点的向量。
Math(矩阵)
:descriptors_1 和 descriptors_2 是存储描述子的矩阵。
detector
是特征检测器的指针,descriptor
是描述子提取器的指针,使用 ORB::create() 函数来分别创建ORB 特征检测器和ORB 描述子提取器
最后一行创建了一个描述子匹配器的指针对象 matcher,并且使用ORB::create() 函数选择了基于汉明距离(Hamming distance)的暴力匹配器(Brute-Force)作为匹配器。
汉明距离
:汉明距离是一种用于度量两个等长字符串之间的差异度量,对于二进制字符串,汉明距离表示两个二进制串对应位置不同的比特数。
基于汉明距离的暴力匹配
:
在特征匹配中,“BruteForce-Hamming” 算法会对查询图像的每个描述子与目标图像中的所有描述子进行比较,计算它们之间的汉明距离,选择距离最近的描述子作为匹配结果。
简单直接,易于理解,但是数据规模大的时候,匹配效率低,可以使用使用更高效的匹配算法,如基于近似最近邻搜索(Approximate Nearest Neighbor Search)的算法。
//-- 第一步:检测 Oriented FAST 角点位置
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
detector->detect(img_1, keypoints_1);
detector->detect(img_2, keypoints_2);
第一行
,计时:
使用now() 函数返回当前时刻的时间点,并将其赋值给 t1 变量,从而做到使用 头文件中的计时器来记录程序执行时间的起点。
第二行
,根据提供图像提取关键点存入对应向量之中
//-- 第二步:根据角点位置计算 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;
第一行、第二行
:根据关键点(keypoints)位置提取描述子(descriptors),并存储在对应向量中
第三行
:记录当前的时间(这是终点,上文的t1是起点,差值即为程序运行时间)
第四行、第五行
:计算时间差值(以秒为单位),并输出
chrono::duration_cast 是 C++ 头文件中的一个函数模板,用于进行时间单位的转换和精度截取
duration< double > 是一个时间间隔的类型,用于存储以秒为单位的时间
Mat outimg1;
drawKeypoints(img_1, keypoints_1, outimg1, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
imshow("ORB features", outimg1);
第一行
:创建输出图像的矩阵,也就是存储绘制了关键点的结果图像
第二行
:调用了 drawKeypoints 函数,将图像 img_1 中的关键点 keypoints_1 绘制到 outimg1 中。
Scalar::all(-1):关键点的颜色,这里使用 -1 表示随机颜色。
不填或者为零是黑色,填入255是白色;也可以使用RGB格式,Scalar::all(0, 0, 255) 表示纯红色,Scalar::all(255, 255, 255) 表示白色。
DrawMatchesFlags::DEFAULT:绘制关键点的标志,使用默认的绘制设置。
//-- 第三步:对两幅图像中的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;
第一行
:声明了一个存储特征匹配结果的容器,其中的元素类型为 DMatch。第三行的特征匹配的结果会存储在 matches 中,该向量由一组DMatch对象构成,每个对象表示一对匹配的特征点及其相似性度量。
第三行
:调用上文中创建的基于汉明距离的暴力特征匹配器(matcher)的 match 函数,将输入图像的描述符 descriptors_1 和目标图像的描述符 descriptors_2 进行匹配,并将匹配结果存储在 matches 中。
第二、四、五、六行
:记录时间点,计算时间差,输出程序运行耗费时间
//-- 第四步:匹配点对筛选
// 计算最小距离和最大距离
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]);
}
}
第一、二行
:寻找最小值和最大值:使用 minmax_element 函数找出匹配结果中距离的最小值和最大值,通过传入一个自定义的 lambda 表达式作为比较函数,根据 DMatch 对象中的 distance 成员变量来比较两个对象的距离
关于函数指针、Lambda表达式和std::function
第三、四行
:保存最小值和最大值对象
min_max是一个pair类型的对象,因为根据 minmax_element 函数的定义,它会在指定的范围内查找最小值和最大值,并返回一个pair 对象,其中 first 是指向最小值的迭代器,second 是指向最大值的迭代器。
distance:表示匹配结果之间的距离或相似度
第五、六行
:输出最小值和最大值
剩余行数
:选择比较适合的特征点对,循环判断特征点对的距离是否小于等于两倍最小距离和 30.0 中的较大值,如果是,则将该特征点对添加到 good_matches 中。
//-- 第五步:绘制匹配结果
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);
return 0;
}
前四行
:分别传入图像、关键点、特征点匹配结果来绘制图像,并且存入新创建的矩阵中。
第五、六行
:将绘制好的图像展示在窗口中
第七行
:通过调用 waitKey(0),程序会一直等待用户按下键盘上的任意键,直到用户关闭显示的窗口
创建诸多对象,接着对图像提取关键点,而后根据关键点计算描述子,使用描述子来直接进行特征点匹配来获取匹配结果,对匹配特征点对进行优化,末尾绘制图像并展示优化前后结果。
至此,结束。