本人之前也研究过OpenMVS但是对于OpenMVG只是原理层次的了解,因此乘着过年期间对这个库进行详细的学习。
目录
1 OpenMVG编译与简单测试
1.1 sfm_data.json获取
1.2 计算特征
2 OpenMVG整个流程的运行测试
3 OpenMVG实战
3.1 SVG绘制
3.2 解析图片的EXIF信息
3.3 光学畸变
3.4 提取图像中的仿射特征点
3.5 对图像进行特征匹配(K-VLD)
1 OpenMVG编译与简单测试
参考文章
openMVG+openMVS对数据集的详细重建步骤!避坑!!!_lianqi1008的博客-CSDN博客
OpenMVG源码阅读小记 - 知乎 (zhihu.com)
1.1 sfm_data.json获取
-i参数是已经有的图片,-o是输出路径,创建result文件夹。-d是已经存在的txt
D:\CPlusProject\MVS_program\openMVG\src\build\Windows-AMD64-Release\Release\openMVG_main_SfMInit_ImageListing.exe
-i D:\CPlusProject\MVS_program\Data\images
-o D:\CPlusProject\MVS_program\Data\result\matches
-d D:\CPlusProject\MVS_program\openMVG\src\openMVG\exif\sensor_width_database\sensor_width_camera_database.txt
执行成功后在matches文件夹产生sfm_data.json文件 。11个图片也就是views长度为11
1.2 计算特征
D:\CPlusProject\MVS_program\openMVG\src\build\Windows-AMD64-Release\Release\openMVG_main_ComputeFeatures.exe -i D:\CPlusProject\MVS_program\Data\result\matches\sfm_data.json -o D:\CPlusProject\MVS_program\Data\result\matches
OpenMVG源码阅读小记 - 知乎 (zhihu.com)
2 OpenMVG整个流程的运行测试
openMVG中的k.txt储存的是相机的内参
增量式 SFM:
py ./SfM_SequentialPipeline.py images matches_sequential
全局式SFM:
py ./SfM_GlobalPipeline.py images matches_global
可看到在 matches_sequential 中生成了两个文件夹:matches 存储的是特征点和匹配信息;reconstruction_sequential 保存的是重建后的点云 (后缀为 .ply)。
用 Meshlab 打开其中一个稀疏点云 colorized.ply,显示如下:
3 OpenMVG实战
3.1 SVG绘制
// 包含必要的头文件
#include <iostream>
#include <cstdlib>
#define _USE_MATH_DEFINES
#include <math.h>
#include <vector>
#include "svgDrawer.hpp" // 引入SVG绘图库的头文件
using namespace svg; // 使用svg命名空间简化代码
//这段代码展示了如何使用一个简单的SVG绘图库来创建SVG(可缩放矢量图形)文件。SVG是一种基于XML的标记语言,用于描述二维矢量图形。
int main(int argc, char* argv[])
{
// 简单的使用示例:
{
svgDrawer svgSurface; // 创建SVG绘图对象
// 添加一些绘图指令
double S = 20.; // 设置一个基础尺寸
for (double i = 0; i < 3.14 * 2; i += .4) { // 循环绘制一系列的线段
// 计算线段的起点和终点坐标
const double ax = cos(i) * S + S;
const double ay = sin(i) * S + S;
const double bx = cos(i + 3.14 / 4.) * S + S;
const double by = sin(i + 3.14 / 4.) * S + S;
// 使用drawLine函数和svgAttributes设置绘制线段的属性(颜色、线宽等)
svgSurface << drawLine(ax, ay, bx, by, svgAttributes().stroke("blue", 1));
}
// 将SVG内容导出到文件
std::string sFileName = "FirstExample.svg"; // 文件名
std::ofstream svgFile(sFileName.c_str()); // 创建文件流
svgFile << svgSurface.closeSvgFile().str(); // 写入SVG内容并关闭文件
svgFile.close();
}
// 其他绘图原语的使用示例:
{
svgDrawer svgSurface(20, 20); // 创建一个新的SVG绘图对象,指定尺寸
// 添加一些绘图指令
svgSurface << drawCircle(10, 10, 4, svgAttributes().stroke("red", 1).fill("blue").tooltip("Hello"));
svgSurface << drawSquare(4, 4, 12, svgAttributes().stroke("black"));
svgSurface << drawText(8, 11, 6.f, "H", "green");
// 将SVG内容导出到文件
std::string sFileName = "SecondExample.svg";
std::ofstream svgFile(sFileName.c_str());
svgFile << svgSurface.closeSvgFile().str();
svgFile.close();
}
// 绘制心脏形状(Cardioid)使用SVG多边形线(Polyline):
{
size_t nbPoints = 120; // 点的数量
std::vector<float> vec_x(nbPoints, 0.f), vec_y(nbPoints, 0.f); // 存储点坐标的向量
double S = 20.; // 基础尺寸
for (size_t i = 0; i < nbPoints; ++i) { // 计算心脏形的每个点的坐标
const double theta = i * 2 * M_PI / nbPoints; // 角度
// 心脏形的方程
vec_x[i] = (3 * S + S * (2. * sin(theta) - (sin(2. * theta))));
vec_y[i] = (2 * S - S * (2. * cos(theta) - (cos(2. * theta))));
}
// 创建SVG绘图对象并添加心脏形多边形线
svgDrawer svgSurface(6 * S, 6 * S); // 设置尺寸
svgSurface << drawPolyline(vec_x.cbegin(), vec_x.cend(), vec_y.cbegin(), vec_y.cend(), svgAttributes().stroke("blue", 2));
// 将SVG内容导出到文件
std::string sFileName = "ThirdExample.svg";
std::ofstream svgFile(sFileName.c_str());
svgFile << svgSurface.closeSvgFile().str();
svgFile.close();
}
return EXIT_SUCCESS;
}
3.2 解析图片的EXIF信息
-i "D:\CPlusProject\MVS_program\ALLTestData\ImageDataset_SceauxCastle-master\images\100_7100.JPG"
// 包含必要的头文件
#include "openMVG/exif/exif_IO_EasyExif.hpp" // 引入OpenMVG库中处理EXIF信息的头文件
using namespace openMVG::exif; // 使用命名空间简化代码
#include "third_party/cmdLine/cmdLine.h" // 引入命令行解析工具
#include <memory> // 引入智能指针相关头文件
//这段代码是一个C++程序,用于读取一张图片的EXIF信息。EXIF(Exchangeable Image File Format)是一种标准格式,
//用于存储数字照片和音频文件中的信息,如拍摄时间、相机设置、缩略图、版权信息等。
int main(int argc, char** argv)
{
CmdLine cmd; // 创建命令行解析对象
std::string sInputImage; // 定义变量存储输入的图片文件路径
// 添加命令行参数,'-i'用于指定图片文件的路径
cmd.add(make_option('i', sInputImage, "imafile"));
// 尝试解析命令行参数
try {
if (argc == 1) throw std::string("Invalid command line parameter."); // 如果没有提供参数,则抛出异常
cmd.process(argc, argv); // 处理命令行参数
}
catch (const std::string& s) {
// 如果出现错误,显示用法信息并退出
std::cerr << "Usage: " << argv[0] << ' '
<< "[-i|--imafile path] "
<< std::endl;
std::cerr << s << std::endl; // 显示错误信息
return EXIT_FAILURE; // 返回失败状态码
}
// 显示调用信息,包括程序名和输入的图片文件路径
std::cout << " You called : " << std::endl
<< argv[0] << std::endl
<< "--imafile " << sInputImage << std::endl;
// 使用智能指针创建Exif_IO_EasyExif对象,用于读取指定图片的EXIF信息
std::unique_ptr<Exif_IO> exif_io(new Exif_IO_EasyExif(sInputImage));
// 读取并显示图片的EXIF信息,包括宽度、高度、焦距、品牌和模型
std::cout << "width : " << exif_io->getWidth() << std::endl;
std::cout << "height : " << exif_io->getHeight() << std::endl;
std::cout << "focal : " << exif_io->getFocal() << std::endl;
std::cout << "brand : " << exif_io->getBrand() << std::endl;
std::cout << "model : " << exif_io->getModel() << std::endl;
return EXIT_SUCCESS; // 程序成功执行完毕
}
3.3 光学畸变
-i D:\CPlusProject\MVS_program\ALLTestData\ImageDataset_SceauxCastle-master\images -o D:\CPlusProject\MVS_program\ALLTestData\ImageDataset_SceauxCastle-master\outputTest -f 5 -s JPG
// 引入必要的头文件
#include "openMVG/cameras/Camera_Pinhole_Radial.hpp" // 引入径向畸变的针孔相机模型
#include "openMVG/cameras/Camera_undistort_image.hpp" // 引入图像去畸变功能
#include "openMVG/image/image_io.hpp" // 引入图像输入输出功能
#include "openMVG/system/loggerprogress.hpp" // 引入进度条显示
#include "third_party/cmdLine/cmdLine.h" // 引入命令行解析工具
#include "third_party/stlplus3/filesystemSimplified/file_system.hpp" // 引入文件系统操作工具
#include <cstdlib>
#include <iostream>
#include <string>
// 使用OpenMVG库的命名空间,简化代码
using namespace openMVG;
using namespace openMVG::cameras;
using namespace openMVG::image;
//这个程序的目的是自动处理一批图像,通过去除光学畸变来改善它们的质量。
int main(int argc, char** argv)
{
CmdLine cmd; // 创建命令行解析对象
// 定义变量存储命令行参数
std::string sPath; // 输入图像目录
std::string sOutPath; // 输出图像目录
Vec2 c; // 畸变中心
Vec3 k; // 畸变系数
double f; // 焦距
std::string suffix = "JPG"; // 默认图像文件后缀
c = Vec2(1000, 1000);
k = Vec3(0.001, 0, 0);
// 添加命令行参数
cmd.add(make_option('i', sPath, "imadir"));
cmd.add(make_option('o', sOutPath, "outdir"));
cmd.add(make_option('a', c(0), "cx"));
cmd.add(make_option('b', c(1), "cy"));
cmd.add(make_option('c', k(0), "k1"));
cmd.add(make_option('d', k(1), "k2"));
cmd.add(make_option('e', k(2), "k3"));
cmd.add(make_option('f', f, "focal"));
cmd.add(make_option('s', suffix, "suffix"));
// 解析命令行参数
try {
if (argc == 1) throw std::string("Invalid command line parameter.");
cmd.process(argc, argv);
}
catch (const std::string& s) {
// 如果参数解析失败,显示用法信息并退出
std::cerr << "Usage: " << argv[0] << ' '
<< "[-i|--imadir - 输入路径]\n"
<< "[-o|--outdir - 输出JPG文件的路径]\n"
<< "[-f|--focal - 焦距]\n"
<< "[-s|--suffix - 输入文件的后缀. (默认: JPG)]\n"
<< std::endl;
std::cerr << s << std::endl;
return EXIT_FAILURE;
}
// 检查输入和输出路径是否相同
if (sOutPath == sPath)
{
std::cerr << "输入和输出路径不能相同" << std::endl;
return EXIT_FAILURE;
}
// 如果输出目录不存在,则创建它
if (!stlplus::folder_exists(sOutPath))
stlplus::folder_create(sOutPath);
// 显示使用的畸变模型参数
std::cout << "使用的Brown畸变模型参数: \n"
<< " 畸变中心: " << c.transpose() << "\n"
<< " 畸变系数 (K1,K2,K3): "
<< k.transpose() << "\n"
<< " 焦距: " << f << std::endl;
// 获取指定后缀的文件列表
const std::vector<std::string> vec_fileNames =
stlplus::folder_wildcard(sPath, "*." + suffix, false, true);
std::cout << "\n在 " << sPath
<< " 目录下找到 " << vec_fileNames.size() << " 个文件,后缀为 " << suffix;
// 为不同图像格式准备图像对象
Image<unsigned char> imageGreyIn, imageGreyU;
Image<RGBColor> imageRGBIn, imageRGBU;
Image<RGBAColor> imageRGBAIn, imageRGBAU;
// 进度条显示
system::LoggerProgress my_progress_bar(vec_fileNames.size());
for (size_t j = 0; j < vec_fileNames.size(); ++j, ++my_progress_bar)
{
// 读取图像尺寸、深度
int w, h, depth;
std::vector<unsigned char> tmp_vec;
const std::string sOutFileName =
stlplus::create_filespec(sOutPath, stlplus::basename_part(vec_fileNames[j]), "png");
const std::string sInFileName =
stlplus::create_filespec(sPath, stlplus::filename_part(vec_fileNames[j]));
const int res = ReadImage(sInFileName.c_str(), &tmp_vec, &w, &h, &depth);
// 创建相机模型对象
const Pinhole_Intrinsic_Radial_K3 cam(w, h, f, c(0), c(1), k(0), k(1), k(2));
// 根据图像深度选择相应的处理流程
if (res == 1)
{
switch (depth)
{
case 1: // 灰度图
{
imageGreyIn = Eigen::Map<Image<unsigned char>::Base>(&tmp_vec[0], h, w);
UndistortImage(imageGreyIn, &cam, imageGreyU);
WriteImage(sOutFileName.c_str(), imageGreyU);
break;
}
case 3: // RGB图
{
imageRGBIn = Eigen::Map<Image<RGBColor>::Base>((RGBColor*)&tmp_vec[0], h, w);
UndistortImage(imageRGBIn, &cam, imageRGBU);
WriteImage(sOutFileName.c_str(), imageRGBU);
break;
}
case 4: // RGBA图
{
imageRGBAIn = Eigen::Map<Image<RGBAColor>::Base>((RGBAColor*)&tmp_vec[0], h, w);
UndistortImage(imageRGBAIn, &cam, imageRGBAU);
WriteImage(sOutFileName.c_str(), imageRGBAU);
break;
}
}
}
else
{
std::cerr << "\n图像包含 " << depth << "层。不支持此深度!\n";
}
} // 结束每个文件的循环
return EXIT_SUCCESS;
}
摄影新手入门:1分钟搞懂焦距是什么?焦距与视角的关系! - 知乎 (zhihu.com)
焦距:5
焦距:100
3.4 提取图像中的仿射特征点
什么是仿射特征点?
仿射特征点指的是图像中能够在视角变化、光照改变或其他影响下保持其特性的点。这些点具有独特的属性,使得它们在图像的不同视图中都能被识别和匹配。
为什么要提取仿射特征点?
-
图像匹配和识别:在不同图像之间识别相同的物体或场景时,通过比较它们的仿射特征点可以有效地找到匹配点。这在例如全景图像拼接、物体识别等任务中非常关键。
-
三维重建:通过从不同角度拍摄的图像中提取仿射特征点,可以计算出物体的三维结构。这是现代三维扫描技术和虚拟现实内容创建中的一个重要步骤。
-
运动跟踪:在视频中跟踪特定物体或特征的运动轨迹时,识别和追踪仿射特征点可以提供准确的运动信息。
-
增强现实(AR):在增强现实应用中,将虚拟对象精准地叠加在现实世界的图像上,需要依据图像的特征点来确定正确的位置和姿态。
这段代码展示了如何在OpenMVG库中使用MSER和TBMR特征检测器来提取和可视化图像中的特征。
非极大值抑制(NMS)和最大稳定极值区域(MSER) - 知乎 (zhihu.com)
#include "openMVG/features/feature.hpp" // 引入特征提取相关的定义
#include "openMVG/features/mser/mser.hpp" // 引入MSER特征提取器的定义
#include "openMVG/features/mser/mser_region.hpp" // 引入MSER区域处理的相关定义
#include "openMVG/features/tbmr/tbmr.hpp" // 引入TBMR特征提取器的定义
#include "openMVG/image/image_io.hpp" // 引入图像输入输出功能
#include "openMVG/image/image_drawing.hpp" // 引入图像绘制功能
#include "openMVG/image/image_resampling.hpp" // 引入图像重采样功能
#include "openMVG/image/sample.hpp" // 引入图像采样工具
#include "third_party/stlplus3/filesystemSimplified/file_system.hpp" // 第三方库,用于简化文件系统操作
#include "third_party/cmdLine/cmdLine.h" // 第三方命令行解析工具
#include <unsupported/Eigen/MatrixFunctions> // 引入Eigen库的矩阵功能扩展
#include <iostream> // 引入标准输入输出流
#include <string> // 引入字符串操作
using namespace openMVG; // 使用openMVG命名空间简化代码
using namespace openMVG::image; // 使用openMVG的image命名空间
using namespace openMVG::features; // 使用openMVG的features命名空间
// 定义一个模板函数,用于将给定椭圆的一个区域规范化为指定大小的正方形补丁
template <typename Image>
void NormalizePatch
(
const Image& src_img, // 源图像
const AffinePointFeature& feat, // 仿射点特征
const int patch_size, // 补丁大小
Image& out_patch // 输出补丁图像
)
{
// 映射函数
Eigen::Matrix<double, 2, 2> A;
A << feat.a(), feat.b(),
feat.b(), feat.c();
// 逆平方根
A = A.pow(-0.5);
const float sc = 2.f * 3.f / static_cast<float>(patch_size);
A = A * sc;
const float half_width = static_cast<float>(patch_size) / 2.f;
// 计算采样网格
std::vector<std::pair<float, float>> sampling_grid;
sampling_grid.reserve(patch_size * patch_size);
for (int i = 0; i < patch_size; ++i)
{
for (int j = 0; j < patch_size; ++j)
{
// 相对于补丁中心的变换应用(假设原点在0,0,然后映射到(x,y))
Vec2 pos;
pos << static_cast<float>(j) - half_width, static_cast<float>(i) - half_width;
// 映射(即:椭圆变换)
const Vec2 affineAdapted = A * pos;
sampling_grid.emplace_back(affineAdapted(1) + feat.y(), affineAdapted(0) + feat.x());
}
}
Sampler2d< SamplerLinear > sampler;
// 采样输入图像以生成补丁
GenericRessample(
src_img, sampling_grid,
patch_size, patch_size,
sampler,
out_patch);
}
// 定义一个函数,用于从给定图像中提取MSER特征
void Extract_MSER
(
const Image<unsigned char>& img, // 输入图像
std::vector<features::AffinePointFeature>& feats_dark, // 暗区域特征
std::vector<features::AffinePointFeature>& feats_bright // 亮区域特征
)
{
using namespace openMVG::features::MSER;
// 提取亮区域MSER
{
// 反转图像
Image<unsigned char> image4(255 - img.array());
std::vector<MSERRegion> regs;
MSERExtractor extr4(2, 0.0005, 0.1, 0.5, 0.5, MSERExtractor::MSER_4_CONNECTIVITY);
extr4.Extract(image4, regs);
for (size_t i = 0; i < regs.size(); ++i)
{
double a, b, c;
regs[i].FitEllipse(a, b, c);
double x, y;
regs[i].FitEllipse(x, y);
feats_bright.emplace_back(x, y, a, b, c);
}
}
// 提取暗区域MSER
{
std::vector<MSERRegion> regs;
MSERExtractor extr8(2, 0.0005, 0.1, 0.5, 0.5, MSERExtractor::MSER_8_CONNECTIVITY);
extr8.Extract(img, regs);
for (size_t i = 0; i < regs.size(); ++i)
{
double a, b, c;
regs[i].FitEllipse(a, b, c);
double x, y;
regs[i].FitEllipse(x, y);
feats_dark.emplace_back(x, y, a, b, c);
}
}
}
// 定义一个函数,用于使用TBMR方法从图像中提取特征
void Extract_TBMR
(
const Image<unsigned char>& img, // 输入图像
std::vector<features::AffinePointFeature>& feats_dark, // 暗区域特征
std::vector<features::AffinePointFeature>& feats_bright // 亮区域特征
)
{
tbmr::Extract_tbmr(img, feats_bright, std::less<uint8_t>(), 30);
tbmr::Extract_tbmr(img, feats_dark, std::greater<uint8_t>(), 30);
}
// 程序主入口
int main(int argc, char** argv)
{
std::string sAffine_Detector_Method = "TBMR"; // 默认使用TBMR方法
CmdLine cmd; // 命令行解析对象
cmd.add(make_switch('P', "PATCH")); // 添加命令行选项,用于导出规范化的补丁
cmd.add(make_option('d', sAffine_Detector_Method, "detector")); // 添加命令行选项,用于选择特征检测器
// 打印程序使用说明
std::cout
<< "TBMR Demo:\n"
<< " Show detected Affine regions as ellipses,\n"
<< " -[P] in the command line exports square normalized patches for each ellipses.\n"
<< " -[d|detector] TBMR|MSER Detect TBMR or MSER affine regions."
<< std::endl;
try {
cmd.process(argc, argv); // 处理命令行参数
}
catch (const std::string& s) {
std::cerr << s << std::endl; // 捕获并打印处理命令行参数时的错误
return EXIT_FAILURE;
}
// 构建输入图像的路径
const std::string sInputDir =
stlplus::folder_up(std::string(THIS_SOURCE_DIR)) + "/imageData/SceauxCastle/";
const std::string jpg_filename = sInputDir + "100_7101.jpg";
Image<unsigned char> image; // 定义用于存储读入的图像的变量
ReadImage(jpg_filename.c_str(), &image); // 读取图像
std::vector<features::AffinePointFeature> feats_dark, feats_bright; // 定义存储特征的向量
if (sAffine_Detector_Method == "MSER") // 如果选择的是MSER方法
{
Extract_MSER(image, feats_dark, feats_bright); // 提取MSER特征
}
else if (sAffine_Detector_Method == "TBMR") // 如果选择的是TBMR方法
{
Extract_TBMR(image, feats_dark, feats_bright); // 提取TBMR特征
}
else // 如果输入了无效的检测器类型
{
std::cerr << "Invalid Affine detector type." << std::endl; // 打印错误信息
return EXIT_FAILURE;
}
// 特征检测器演示
{
std::cout << "#detected BRIGHT " << sAffine_Detector_Method << ": " << feats_bright.size() << std::endl; // 打印亮区域检测到的特征数量
// 显示提取的区域椭圆
Image<unsigned char> Icpy(image); // 创建图像的副本
for (size_t i = 0; i < feats_bright.size(); ++i) // 遍历所有亮区域特征
{
const AffinePointFeature& fp = feats_bright[i]; // 获取特征点
DrawEllipse(fp.x(), fp.y(), fp.l1(), fp.l2(), 255, &Icpy, fp.orientation()); // 在图像上绘制椭圆
if (cmd.used('P')) // 如果命令行中指定了导出补丁
{
// 椭圆到正方形41x41补丁的规范化
Image<unsigned char> patch;
NormalizePatch(Icpy, fp, 41, patch); // 规范化补丁
std::stringstream str;
str << "Patch_" << i << ".png"; // 构建补丁的文件名
WriteImage(str.str().c_str(), patch); // 写入补丁图像
}
}
std::ostringstream os;
os << sAffine_Detector_Method << "_BRIGHT_features.jpg"; // 构建亮区域特征图像的文件名
WriteImage(os.str().c_str(), Icpy); // 写入亮区域特征图像
std::cout << "#detected DARK " << sAffine_Detector_Method << ": " << feats_dark.size() << std::endl; // 打印暗区域检测到的特征数量
// 显示提取的区域椭圆
Icpy = image; // 重置图像副本
for (size_t i = 0; i < feats_dark.size(); ++i) // 遍历所有暗区域特征
{
const AffinePointFeature& fp = feats_dark[i]; // 获取特征点
DrawEllipse(fp.x(), fp.y(), fp.l1(), fp.l2(), 255, &Icpy, fp.orientation()); // 在图像上绘制椭圆
}
os.str("");
os << sAffine_Detector_Method << "_DARK_features.jpg"; // 构建暗区域特征图像的文件名
WriteImage(os.str().c_str(), Icpy); // 写入暗区域特征图像
}
return EXIT_SUCCESS; // 程序成功结束
}
左边是亮区域,右边是暗区域,在图像上绘制椭圆。
3.5 对图像进行特征匹配(K-VLD)
一个是模板区域,一个是搜索区域。使其两者进行特征匹配
// OpenMVG库的一部分,一个开源多视角几何C++库
#include "openMVG/features/sift/SIFT_Anatomy_Image_Describer.hpp" // SIFT特征描述器
#include "openMVG/features/svg_features.hpp" // 用于SVG特征可视化
#include "openMVG/image/image_io.hpp" // 图像输入输出
#include "openMVG/image/image_concat.hpp" // 图像拼接
#include "openMVG/matching/kvld/kvld.h" // KVLD匹配算法
#include "openMVG/matching/kvld/kvld_draw.h" // KVLD匹配结果绘制
#include "openMVG/matching/regions_matcher.hpp" // 区域匹配
#include "openMVG/matching/svg_matches.hpp" // 匹配结果的SVG可视化
#include "third_party/cmdLine/cmdLine.h" // 命令行解析
#include "third_party/stlplus3/filesystemSimplified/file_system.hpp" // 文件系统操作
#include "openMVG/vector_graphics/svgDrawer.hpp" // SVG绘图
#include <cstdlib>
#include <iostream>
#include <string>
using namespace openMVG;
using namespace openMVG::image;
using namespace openMVG::matching;
using namespace svg;
int main(int argc, char **argv) {
CmdLine cmd;
//输入参数:两个图片,一个模板一个搜索。一个输出文件夹路径
std::string sImg1 = stlplus::folder_up(std::string(THIS_SOURCE_DIR))
+ "/imageData/StanfordMobileVisualSearch/Ace_0.png";
std::string sImg2 = stlplus::folder_up(std::string(THIS_SOURCE_DIR))
+ "/imageData/StanfordMobileVisualSearch/Ace_1.png";
std::string sOutDir = "./kvldOut";
std::cout << sImg1 << std::endl << sImg2 << std::endl;
cmd.add( make_option('i', sImg1, "img1") );
cmd.add( make_option('j', sImg2, "img2") );
cmd.add( make_option('o', sOutDir, "outdir") );
if (argc > 1)
{
try {
if (argc == 1) throw std::string("Invalid command line parameter.");
cmd.process(argc, argv);
} catch (const std::string& s) {
std::cerr << "Usage: " << argv[0] << ' '
<< "[-i|--img1 file] "
<< "[-j|--img2 file] "
<< "[-o|--outdir path] "
<< std::endl;
std::cerr << s << std::endl;
return EXIT_FAILURE;
}
}
std::cout << " You called : " <<std::endl
<< argv[0] << std::endl
<< "--img1 " << sImg1 << std::endl
<< "--img2 " << sImg2 << std::endl
<< "--outdir " << sOutDir << std::endl;
if (sOutDir.empty()) {
std::cerr << "\nIt is an invalid output directory" << std::endl;
return EXIT_FAILURE;
}
// -----------------------------
// a. List images
// b. Compute features and descriptor
// c. Compute putatives descriptor matches
// d. Geometric filtering of putatives matches
// e. Export some statistics
// -----------------------------
// 是否存在文件夹,若无则创建
if (!stlplus::folder_exists(sOutDir))
stlplus::folder_create( sOutDir );
const std::string jpg_filenameL = sImg1;
const std::string jpg_filenameR = sImg2;
//读取图片
Image<unsigned char> imageL, imageR;
ReadImage(jpg_filenameL.c_str(), &imageL);
ReadImage(jpg_filenameR.c_str(), &imageR);
//--
// 检测并描述图像中的特征区域
//--
// 使用OpenMVG库的特征命名空间,以便访问特征检测和描述的相关功能
using namespace openMVG::features;
// 创建一个SIFT特征描述器对象。SIFT_Anatomy_Image_describer是SIFT特征检测和描述的一种实现。
// SIFT_Anatomy_Image_describer::Params(-1)创建了一个参数对象,-1表示使用默认参数。
std::unique_ptr<Image_describer> image_describer
(new SIFT_Anatomy_Image_describer(SIFT_Anatomy_Image_describer::Params(-1)));
// 创建一个映射,用于存储每张图像检测到的特征区域。键是图像的索引,值是特征区域的智能指针。
std::map<IndexT, std::unique_ptr<features::Regions>> regions_perImage;
// 使用刚才创建的描述器来描述两张图像中的特征区域。
// Describe函数执行特征检测,并将检测到的特征存储在regions_perImage映射中。
image_describer->Describe(imageL, regions_perImage[0]);
image_describer->Describe(imageR, regions_perImage[1]);
//左右图像的特征区域
const SIFT_Regions* regionsL = dynamic_cast<SIFT_Regions*>(regions_perImage.at(0).get());
const SIFT_Regions* regionsR = dynamic_cast<SIFT_Regions*>(regions_perImage.at(1).get());
// 从每张图像的特征区域中提取特征点的位置。
// GetRegionsPositions函数返回一个包含所有特征点位置的容器,这些位置是在图像中的坐标。
const PointFeatures
featsL = regions_perImage.at(0)->GetRegionsPositions(),
featsR = regions_perImage.at(1)->GetRegionsPositions();
// Show both images side by side
{
Image<unsigned char> concat;
ConcatH(imageL, imageR, concat);
std::string out_filename = "00_images.jpg";
WriteImage(out_filename.c_str(), concat);
}
//- Draw features on the two image (side by side)
{
Features2SVG
(
jpg_filenameL,
{imageL.Width(), imageL.Height()},
regionsL->Features(),
jpg_filenameR,
{imageR.Width(), imageR.Height()},
regionsR->Features(),
"01_features.svg"
);
}
定义一个用于存储初始匹配对的向量
std::vector<IndMatch> vec_PutativeMatches;
//-- 执行匹配 -> 寻找最近邻居,通过距离比率进行过滤
{
// 函数用于寻找两组特征之间的匹配对。
// 这个函数使用距离比率测试来过滤不可靠的匹配,增加匹配的准确性。
// 距离比率测试是一种常用的方法,用于剔除那些与最近邻居的距离
// 与次近邻居距离比值大于某个阈值(这里是0.8)的匹配,因为这样的匹配往往不够可靠。
matching::DistanceRatioMatch(
0.8, matching::BRUTE_FORCE_L2,
*regions_perImage.at(0).get(),
*regions_perImage.at(1).get(),
vec_PutativeMatches);
// Draw correspondences after Nearest Neighbor ratio filter
const bool bVertical = true;
Matches2SVG
(
jpg_filenameL,
{imageL.Width(), imageL.Height()},
regionsL->GetRegionsPositions(),
jpg_filenameR,
{imageR.Width(), imageR.Height()},
regionsR->GetRegionsPositions(),
vec_PutativeMatches,
"02_Matches.svg",
bVertical,
std::max(std::max(imageL.Width(), imageL.Height()) / float(600), 2.0f)
);
}
//K-VLD filter
Image<float> imgA (imageL.GetMat().cast<float>());
Image<float> imgB (imageR.GetMat().cast<float>());
std::vector<Pair> matchesFiltered;
std::vector<Pair> matchesPair;
//将每对匹配的索引存入matchesPair中 类似 (137,29)
for (const auto & match_it : vec_PutativeMatches)
{
matchesPair.emplace_back(match_it.i_, match_it.j_);
}
std::vector<double> vec_score;
//E矩阵用于存储每对匹配的一致性评分
openMVG::Mat E = openMVG::Mat::Ones(vec_PutativeMatches.size(), vec_PutativeMatches.size())*(-1);
// gvld-consistancy matrix, intitialized to -1, >0 consistancy value, -1=unknow, -2=false
std::vector<bool> valid(vec_PutativeMatches.size(), true);// indices of match in the initial matches, if true at the end of KVLD, a match is kept.
//执行K-VLD算法,尝试通过迭代减少inlierRate(内点率)的方法来筛选出高质量的匹配对。KVLD函数评估每对匹配的一致性,
//并更新matchesFiltered(过滤后的匹配对)、vec_score(匹配对的评分)、E和valid。如果内点率过低,则通过调整参数重新筛选。
size_t it_num=0;
KvldParameters kvldparameters; // initial parameters of KVLD
while (it_num < 5 &&
kvldparameters.inlierRate > KVLD(imgA, imgB, regionsL->Features(), regionsR->Features(),
matchesPair, matchesFiltered, vec_score,E,valid,kvldparameters)) {
kvldparameters.inlierRate /= 2;
//std::cout<<"low inlier rate, re-select matches with new rate="<<kvldparameters.inlierRate<<std::endl;
kvldparameters.K = 2;
it_num++;
}
//将K-VLD过滤后仍然有效的匹配对转换为IndMatch类型并存入vec_FilteredMatches中,这些是最终认为质量较高的匹配结果。
std::vector<IndMatch> vec_FilteredMatches;
for (std::vector<Pair>::const_iterator i_matchFilter = matchesFiltered.begin();
i_matchFilter != matchesFiltered.end(); ++i_matchFilter){
vec_FilteredMatches.push_back(IndMatch(i_matchFilter->first, i_matchFilter->second));
}
/*
打印K-VLD一致的匹配对
首先,通过svgDrawer创建一个SVG画布,其大小足以并排容纳两张输入图像。
将两张输入图像绘制到SVG画布上,一张在左侧,一张在右侧。
遍历所有匹配对,绘制那些通过K-VLD一致性检查的匹配对。具体来说,就是绘制连接一致匹配对的线段,这些线段的宽度根据视觉连通性(VLD)的长度动态调整,并用黄色高亮显示。
将绘制的结果保存为SVG文件,文件名为03_KVLD_Matches.svg,以便于后续查看和分析。
*/
//Print K-VLD consistent matches
{
svgDrawer svgStream(imageL.Width() + imageR.Width(),
std::max(imageL.Height(), imageR.Height()));
// ".svg"
svgStream << svg::drawImage(jpg_filenameL, imageL.Width(), imageL.Height());
svgStream << svg::drawImage(jpg_filenameR, imageR.Width(), imageR.Height(), imageL.Width());
for (size_t it1=0; it1<matchesPair.size()-1;it1++){
for (size_t it2=it1+1; it2<matchesPair.size();it2++){
if (valid[it1] && valid[it2] && E(it1,it2)>=0){
//(179,56)左图179 右图56
const PointFeature & l1 = featsL[matchesPair[it1].first];
const PointFeature & r1 = featsR[matchesPair[it1].second];
//(181,66)
const PointFeature & l2 = featsL[matchesPair[it2].first];
const PointFeature & r2 = featsR[matchesPair[it2].second];
// Compute the width of the current VLD segment
float L = (l1.coords() - l2.coords()).norm();
float width = 0.1;
// ".svg"
svgStream << svg::drawLine(l1.x(), l1.y(), l2.x(), l2.y(), svgAttributes().stroke("yellow", width));
svgStream << svg::drawLine(r1.x() + imageL.Width(), r1.y(), r2.x() + imageL.Width(), r2.y(), svgAttributes().stroke("yellow", width));
}
}
}
const std::string out_filename = stlplus::create_filespec(sOutDir, "03_KVLD_Matches.svg");
std::ofstream svgFile( out_filename.c_str() );
svgFile << svgStream.closeSvgFile().str();
svgFile.close();
}
{
//Print keypoints kept by K-VLD
svgDrawer svgStream(imageL.Width() + imageR.Width(),
std::max(imageL.Height(), imageR.Height()));
// ".svg"
svgStream << svg::drawImage(jpg_filenameL, imageL.Width(), imageL.Height());
svgStream << svg::drawImage(jpg_filenameR, imageR.Width(), imageR.Height(), imageL.Width());
for (size_t it=0; it<matchesPair.size();it++){
if (valid[it]){
const PointFeature & l = featsL[matchesPair[it].first];
const PointFeature & r = featsR[matchesPair[it].second];
// ".svg"
svgStream << svg::drawCircle(l.x(), l.y(), 10, svgAttributes().stroke("yellow", 2.0));
svgStream << svg::drawCircle(r.x() + imageL.Width(), r.y(), 10, svgAttributes().stroke("yellow", 2.0));
}
}
const std::string out_filename = stlplus::create_filespec(sOutDir, "04_KVLD_Keypoints.svg");
std::ofstream svgFile( out_filename.c_str() );
svgFile << svgStream.closeSvgFile().str();
svgFile.close();
}
Image <unsigned char> imageOutL = imageL;
Image <unsigned char> imageOutR = imageR;
getKVLDMask(
&imageOutL, &imageOutR,
regionsL->Features(), regionsR->Features(),
matchesPair,
valid,
E);
{
const std::string out_filename = stlplus::create_filespec(sOutDir, "05_Left-K-VLD-MASK.jpg");
WriteImage(out_filename.c_str(), imageOutL);
}
{
const std::string out_filename = stlplus::create_filespec(sOutDir, "06_Right-K-VLD-MASK.jpg");
WriteImage(out_filename.c_str(), imageOutR);
}
return EXIT_SUCCESS;
}
(1)两张图像拼接在一起,灰度化,便于后续特征匹配。
(2)每张图像进行特征检测,并用圆圈进行标注
(3)寻找两组特征之间的匹配对
(4)执行K-VLD算法筛选高质量匹配对
(5)在K-VLD筛选的基础上,绘制匹配对
(6)在K-VLD筛选的基础上,绘制关键点
(7)在K-VLD筛选的基础上,绘制掩码图