OpenMVG(EXIF、畸变、仿射特征、特征匹配)

news2024/11/24 6:35:48

本人之前也研究过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 提取图像中的仿射特征点

什么是仿射特征点?

仿射特征点指的是图像中能够在视角变化、光照改变或其他影响下保持其特性的点。这些点具有独特的属性,使得它们在图像的不同视图中都能被识别和匹配。

为什么要提取仿射特征点?

  1. 图像匹配和识别:在不同图像之间识别相同的物体或场景时,通过比较它们的仿射特征点可以有效地找到匹配点。这在例如全景图像拼接、物体识别等任务中非常关键。

  2. 三维重建:通过从不同角度拍摄的图像中提取仿射特征点,可以计算出物体的三维结构。这是现代三维扫描技术和虚拟现实内容创建中的一个重要步骤。

  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筛选的基础上,绘制掩码图 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1445641.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux:信号的保存

文章目录 信号相关概念信号递达信号未决信号阻塞内核中的示意图 信号集的操作函数 前面对于信号的产生中对操作系统有了一个基础的认知&#xff0c;对于一个真正的操作系统来说&#xff0c;进程是由操作系统进行调度的&#xff0c;那操作系统本身也是代码&#xff0c;是由谁进行…

一键打造属于自己漏扫系统

0x01 工具介绍 本系统是对Web中间件和Web框架进行自动化渗透的一个系统,根据扫描选项去自动化收集资产,然后进行POC扫描,POC扫描时会根据指纹选择POC插件去扫描,POC插件扫描用异步方式扫描.前端采用vue技术,后端采用python fastapi。 0x02 安装与使用 1、Docker部署环境 编译…

Java String源码剖析+面试题整理

由于字符串操作是计算机程序中最常见的操作之一&#xff0c;在面试中也是经常出现。本文从基本用法出发逐步深入剖析String的结构和性质&#xff0c;并结合面试题来帮助理解。 String基本用法 在Java中String的创建可以直接像基本类型一样定义&#xff0c;也可以new一个 Str…

骑砍MOD天芒传奇-天芒使用方法

骑砍1战团mod天芒传奇-使用红色天芒碎片开P51战斗机_单机游戏热门视频 (bilibili.com)https://www.bilibili.com/video/BV1nm41197iA/ 一.黄色天芒碎片 天芒盒子 野外战斗H键-召唤徐天地 二.绿色天芒碎片 天芒盒子 野外战斗H键-站在巨人肩膀上战斗 三.蓝色天芒碎片 天芒盒…

华为问界M9:全方位自动驾驶技术解决方案

华为问界M9的自动驾驶技术采用了多种方法来提高驾驶的便利性和安全性。以下是一些关键技术&#xff1a; 智能感知系统&#xff1a;问界M9配备了先进的传感器&#xff0c;包括高清摄像头、毫米波雷达、超声波雷达等&#xff0c;这些传感器可以实时监测车辆周围的环境&#xff0…

车载电子电器架构 —— 电子电气系统功能开发

车载电子电器架构 —— 电子电气系统功能开发 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,喝完再挣扎,出门靠自己,四海皆…

几个好用的 iphone 手机模板贴图样机

整理了几个好用的 iphone 手机模板贴图&#xff0c;分享一下。 关注订阅号「设计师工作日常」&#xff0c;发送关键词 iphone mockup ,获取下载链接。 [1] 原文阅读 我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;求点赞求关注&#xff01;

huggingface学习|用dreambooth和lora对stable diffusion模型进行微调

目录 用dreambooth对stable-diffusion-v1-5模型进行微调&#xff08;一&#xff09;模型下载和环境配置&#xff08;二&#xff09;数据集准备&#xff08;三&#xff09;模型微调&#xff08;四&#xff09;运行微调后的模型 用lora对stable-diffusion-v1-5模型进行微调&#…

windows 下安装gin

go install 执行命令&#xff0c;执行不了的参考一下 https://blog.csdn.net/weixin_42592326/article/details/135946806 Golang 中没法下载第三方包解决办法-CSDN博客 go install github.com/gin-gonic/ginlatest 还是安装不了的话&#xff0c;用手机开热点&#xff0c;电…

在程序中使用日志功能

在应用中&#xff0c;需要记录程序运行过程中的一些关键信息以及异常输出等。这些信息用来排查程序故障或者其他用途。 日志模块可以自己实现或者是借用第三方库&#xff0c;之前写过一个类似的使用Qt的打印重定向将打印输出到文件&#xff1a;Qt将打印信息输出到文件_qt log输…

PyCharm2023.3.2配置conda环境

重点在于Path to conda这一步&#xff0c;需要找到conda.bat这个文件&#xff0c;PyCharm才能识别出现有的conda环境。

配置VMware实现从服务器到虚拟机的一键启动脚本

正文共&#xff1a;1666 字 15 图&#xff0c;预估阅读时间&#xff1a;2 分钟 首先祝大家新年快乐&#xff01;略备薄礼&#xff0c;18000个红包封面来讨个开年好彩头&#xff01; 虽然之前将服务器放到了公网&#xff08;成本增加了100块&#xff0c;内网服务器上公网解决方案…

c语言游戏实战(6):走迷宫之推箱子

前言&#xff1a; 在上一篇文章当中我介绍了一个走迷宫的写法&#xff0c;但是那个迷宫没什么可玩性和趣味性&#xff0c;所以我打算在迷宫的基础上加上一个推箱子&#xff0c;使之有更好的操作空间&#xff0c;从而增强了游戏的可玩性和趣味性。 1. 打印菜单 void menu() {…

【DDD】学习笔记-UML 与彩色建模

如果某个领域已经形成了稳定的分析模式&#xff0c;在设计该领域的分析模型时&#xff0c;这些模式就可以提供有价值的参考。可惜&#xff0c;分析模式需要有人来总结和提炼&#xff0c;最好的分析模式提炼者需要兼具领域知识和软件建模能力。很早以前&#xff0c;Martin Fowle…

nodejs切换版本

sudo n 18.17.0 sudo n然后键盘上下选择

Vue核心基础6:Vue内置指令、自定义指令、生命周期

1 Vue中的内置指令 <script>const vm new Vue({el: #root,data: {n: 1,m: 100,name: Vue,str: <h3>你好</h3>}})</script> 1.1 v-text <div v-text"name"></div>1.2 v-html <div v-html"str"></div> …

SpringCloud-高级篇(二十)

下面我们研究MQ的延迟性问题 &#xff08;1&#xff09;初始死信交换机 死信交换机作用一方面可以向Public的异常交换机一样做异常消息的兜底方案&#xff0c;另一方面&#xff0c;可以处理一些超时消息&#xff0c;功能比较丰富一点 &#xff08;2&#xff09;TTL 上面学习…

Java基础:值传递和引用传递

Java在给方法传递参数时&#xff0c;有值传递和引用传递两种方式。 基本概念 值传递&#xff1a;传递对象的一个副本&#xff0c;即使副本被改变&#xff0c;也不会影响源对象&#xff0c;因为值传递的时候&#xff0c;实际上是将实参的值复制一份给形参。 引用传递&#xf…

猫头虎分享已解决Bug || ValueError: Data cardinality is ambiguous

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

【Cocos入门】物理系统

物理引擎默认是关闭状态以节省资源开销。开启方法和之前的普通碰撞类似:cc.directorgetPhysicsManager().enabled true但有一个区别&#xff0c;物理引擎的开启必须放在onLoad函数内运行&#xff0c;否则不生效。 开启物理引擎后&#xff0c;游戏运行&#xff0c;会发现添加…