【opencv】实时位姿估计(real_time_pose_estimation)—3D模型注册

c1c44688a044a89fc1d9fb09e33c26f7.jpeg

相机成像原理图

b833cb7b44664576465644ca4b326546.png

物体网格、关键点(局内点、局外点)图像

69b95fbbba6eb190b985bedcc22ec501.pnga26fb25c4e6f2d9bec7177b422bb77e5.png

box.ply

172d6cffa1e912109500fbd9f4092513.jpeg

resized_IMG_3875.JPG

主程序main_registration.cpp

主要实现了利用OpenCV库进行3D模型的注册。主要步骤包括加载3D网格模型、使用鼠标事件选择对应的3D点进行2D到3D的注册、利用solvePnP算法计算摄像机位姿、并将结果保存在yaml文件中。这通常用于计算机视觉中的对象识别和姿态估计。程序也包括了绘制相应点、调试文本和3D物体网格的功能,以便更好地视觉化注册过程。

// 包含C++的输入输出流库
#include <iostream>
// 包含OpenCV的核心、图像处理、相机标定和特征点检测等功能的库
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/calib3d.hpp>
#include <opencv2/features2d.hpp>
// 包含本教程的网格、模型、PNP问题、稳健匹配和模型注册以及工具等类
#include "Mesh.h"
#include "Model.h"
#include "PnPProblem.h"
#include "RobustMatcher.h"
#include "ModelRegistration.h"
#include "Utils.h"


// OpenCV和标准模板库的命名空间
using namespace cv;
using namespace std;


/**  全局变量  **/


// 注册是否完成的布尔型变量
bool end_registration = false;


// 相机的内部参数: UVC WEBCAM
const double f = 45; // 焦距,以毫米为单位
const double sx = 22.3, sy = 14.9; // 传感器尺寸,以毫米为单位
const double width = 2592, height = 1944; // 图像宽度和高度
const double params_CANON[] = { width*f/sx,   // fx
                                height*f/sy,  // fy
                                width/2,      // cx
                                height/2};    // cy


// 设置在图像中要注册的点
// 根据*.ply文件的顺序,从1开始
const int n = 8;
const int pts[] = {1, 2, 3, 4, 5, 6, 7, 8}; // 3 -> 4


/*
 * 创建模型注册对象
 * 创建网格对象
 * 创建模型对象
 * 创建PNP问题对象
 */
ModelRegistration registration;
Model model;
Mesh mesh;
PnPProblem pnp_registration(params_CANON);


/**********************************************************************************************************/
// 显示帮助信息的函数
static void help()
{
    cout
            << "--------------------------------------------------------------------------"   << endl
            << "这个程序展示了如何创建你的3D贴图模型。"                                      << endl
            << "使用方法:"                                                                    << endl
            << "./cpp-tutorial-pnp_registration"                                              << endl
            << "--------------------------------------------------------------------------"   << endl
            << endl;
}


// 鼠标事件回调函数,用于模型注册
static void onMouseModelRegistration( int event, int x, int y, int, void* )
{
    // 如果检测到鼠标左键释放事件
    if  ( event == EVENT_LBUTTONUP )
    {
        // 检查是否可以注册
        bool is_registrable = registration.is_registrable();
        if (is_registrable)
        {
            // 获取已注册的点数
            int n_regist = registration.getNumRegist();
            // 获取需要注册的顶点编号
            int n_vertex = pts[n_regist];


            // 创建2D点
            Point2f point_2d = Point2f((float)x,(float)y);
            // 获取3D点
            Point3f point_3d = mesh.getVertex(n_vertex-1);


            // 注册点
            registration.registerPoint(point_2d, point_3d);
            // 如果达到最大注册点数,结束注册
            if( registration.getNumRegist() == registration.getNumMax() ) end_registration = true;
        }
    }
}


/**  主程序  **/
int main(int argc, char *argv[])
{
    // 调用帮助信息显示函数
    help();


    // 定义命令行参数
    const String keys =
            "{help h        |      | 打印帮助信息                                                   }"
            "{image i       |      | 输入图像的路径                                                }"
            "{model         |      | 输出yml模型的路径                                             }"
            "{mesh          |      | ply网格的路径                                                 }"
            "{keypoints k   |2000  | 检测关键点的数量(仅用于ORB)                                   }"
            "{feature       |ORB   | 特征点名称 (ORB, KAZE, AKAZE, BRISK, SIFT, SURF, BINBOOST, VGG)}"
            ;
    // 创建命令行解析器
    CommandLineParser parser(argc, argv, keys);


    // 定义默认图像路径、网格文件路径和输出文件路径以及其他参数
    string img_path = samples::findFile("samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/resized_IMG_3875.JPG");  // 用于注册的图像
    string ply_read_path = samples::findFile("samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/box.ply");          // 物体网格
    string write_path = samples::findFile("samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/cookies_ORB.yml");     // 输出文件
    int numKeyPoints = 2000;
    string featureName = "ORB";


    // 根据命令行提供的参数更新路径和参数值
    if (parser.has("help"))
    {
        parser.printMessage();
        return 0;
    }
    else
    {
        img_path = parser.get<string>("image").size() > 0 ? parser.get<string>("image") : img_path;
        ply_read_path = parser.get<string>("mesh").size() > 0 ? parser.get<string>("mesh") : ply_read_path;
        write_path = parser.get<string>("model").size() > 0 ? parser.get<string>("model") : write_path;
        numKeyPoints = parser.has("keypoints") ? parser.get<int>("keypoints") : numKeyPoints;
        featureName = parser.has("feature") ? parser.get<string>("feature") : featureName;
    }


    // 打印相关路径和参数信息
    std::cout << "输入图像: " << img_path << std::endl;
    std::cout << "CAD模型: " << ply_read_path << std::endl;
    std::cout << "输出训练文件: " << write_path << std::endl;
    std::cout << "特征点: " << featureName << std::endl;
    std::cout << "ORB关键点数量: " << numKeyPoints << std::endl;


    // 使用*.ply文件路径加载网格
    mesh.load(ply_read_path);


    // 实例化RobustMatcher类:检测器、提取器、匹配器
    RobustMatcher rmatcher;
    Ptr<Feature2D> detector, descriptor;
    // 创建特征
    createFeatures(featureName, numKeyPoints, detector, descriptor);
    // 设置特征检测器和描述子提取器
    rmatcher.setFeatureDetector(detector);
    rmatcher.setDescriptorExtractor(descriptor);


    /**  第一张图像的基本真实数据  **/


    // 创建并打开窗口  创建一个保持原图像比例且大小可调的显示窗口,窗口的名字为"MODEL REGISTRATION"
    namedWindow("MODEL REGISTRATION", WINDOW_KEEPRATIO);


    // 设置鼠标事件
    setMouseCallback("MODEL REGISTRATION", onMouseModelRegistration, 0);


    // 打开要注册的图像
    Mat img_in = imread(img_path, IMREAD_COLOR);//从 img_path 指定的文件中以彩色方式读入图像,然后将读入的图像数据存储在 Mat 类型的 img_in 中。
    Mat img_vis; // 可视图像的副本


    // 如果读取图像失败
    if (img_in.empty()) {
        cout << "无法打开或找到图像" << endl;
        return -1;
    }


    // 设置要注册的点数
    int num_registrations = n;//8
    registration.setNumMax(num_registrations);


    // 提示用户点击箱子角落
    cout << "点击箱子角落..." << endl;
    cout << "等待..." << endl;


    // 定义一些基本颜色
    const Scalar red(0, 0, 255);
    const Scalar green(0,255,0);
    const Scalar blue(255,0,0);
    const Scalar yellow(0,255,255);


    // 循环直到所有点被注册
    while ( waitKey(30) < 0 )//如果在每30毫秒内没有接收到任何用户按键输入,那么就一直执行while循环中的代码。
    {
        // 刷新调试图像
        img_vis = img_in.clone();


        // 当前已注册的点
        vector<Point2f> list_points2d = registration.get_points2d();
        vector<Point3f> list_points3d = registration.get_points3d();


        // 绘制当前已注册的点  圆点+坐标文字
        drawPoints(img_vis, list_points2d, list_points3d, red);


        // 如果注册未完成,绘制我们要注册的3D点。
        // 如果注册已完成,跳出循环。
        if (!end_registration)
        {
            // 绘制调试文字
            int n_regist = registration.getNumRegist();
            int n_vertex = pts[n_regist];
            Point3f current_poin3d = mesh.getVertex(n_vertex-1);


            drawQuestion(img_vis, current_poin3d, green);//绘制当前3D点的信息
            drawCounter(img_vis, registration.getNumRegist(), registration.getNumMax(), red);//绘制计数文本 
        }
        else
        {
            // 绘制调试文字
            drawText(img_vis, "注册结束", green);
            drawCounter(img_vis, registration.getNumRegist(), registration.getNumMax(), green);
            break;
        }


        // 显示图像
        imshow("MODEL REGISTRATION", img_vis);
    }


    /** 计算相机位置 **/


    cout << "计算位置..." << endl;


    // 已注册点的列表
    vector<Point2f> list_points2d = registration.get_points2d();
    vector<Point3f> list_points3d = registration.get_points3d();


    // 根据已注册的点估计位置  通过使用潜在空间点(list_points3d)和2D图像空间点(list_points2d)的对应关系,对相机进行姿态估计
    bool is_correspondence = pnp_registration.estimatePose(list_points3d, list_points2d, SOLVEPNP_ITERATIVE);
    if ( is_correspondence )
    {
        cout << "找到对应点" << endl;


        // 计算网格的所有2D点以验证算法并绘制
        vector<Point2f> list_points2d_mesh = pnp_registration.verify_points(&mesh);
        draw2DPoints(img_vis, list_points2d_mesh, green);
    }
    else {
        cout << "没有找到对应点" << endl << endl;
    }


    // 显示图像
    imshow("MODEL REGISTRATION", img_vis);


    // 等待直到按下ESC键
    waitKey(0);


    /** 计算图像关键点的3D **/
    // 模型关键点和描述子的容器
    vector<KeyPoint> keypoints_model;
    Mat descriptors;


    // 计算关键点和描述子
    rmatcher.computeKeyPoints(img_in, keypoints_model);
    rmatcher.computeDescriptors(img_in, keypoints_model, descriptors);


    // 检查关键点是否在注册图像的表面,并添加到模型中
    //这段代码主要用于处理关键点模型中的每一个点,对于每个点,它创建
    //一个对应的2D点,并使用PnP(Perspective-n-Point)注册并尝试
    //将2D点反投影(backproject)回3D。如果反投影成功(即点在模型
    //表面上),则在模型中添加对应的2D-3D点、描述符和关键点;否则,
    //将该2D点添加到模型的离群点(outliers)中。这对于创建和调整3D模型
    //,以及进行模型匹配和识别等任务来说,非常关键。
    // 从零开始遍历关键点模型的每一个点
    for (unsigned int i = 0; i < keypoints_model.size(); ++i) {
        // 创建一个2D点,取自关键点模型的第i个点的位置
        Point2f point2d(keypoints_model[i].pt);
        // 创建一个空的3D点
        Point3f point3d;
        // 利用PnP(指视角nP点)识别并将2D点反投影到3D
        // 若点在物体表面上,on_surface则为真;否则,为假
        bool on_surface = pnp_registration.backproject2DPoint(&mesh, point2d, point3d);
        // 若该点在物体表面上
        if (on_surface)
        {
            // 在模型中添加对应的2D和3D点
            model.add_correspondence(point2d, point3d);
            // 向模型添加描述符,这是从描述符中的第i行取得的
            model.add_descriptor(descriptors.row(i));
            // 在对应模型中添加关键点,这是从关键点模型的第i个关键点取得的
            model.add_keypoint(keypoints_model[i]);
        }
        // 若该点不在物体表面上
        else
        {
            // 将该2D点添加到模型的异常值中
            model.add_outlier(point2d);
        }
    }


    // 设置训练图像路径
    model.set_trainingImagePath(img_path);
    // 保存模型到*.yaml文件
    model.save(write_path);


    // 输出图像
    img_vis = img_in.clone();


    // 模型的2D点列表
    vector<Point2f> list_points_in = model.get_points2d_in();
    vector<Point2f> list_points_out = model.get_points2d_out();


    // 绘制一些调试文本
    string num = IntToString((int)list_points_in.size());
    string text = "有 " + num + " 个内点";
    drawText(img_vis, text, green);


    // 绘制一些调试文本
    num = IntToString((int)list_points_out.size());
    text = "有 " + num + " 个外点";
    drawText2(img_vis, text, red);


    // 绘制物体网格
    drawObjectMesh(img_vis, &mesh, &pnp_registration, blue);


    // 根据是否在表面绘制找到的关键点
    draw2DPoints(img_vis, list_points_in, green);
    draw2DPoints(img_vis, list_points_out, red);


    // 显示图像
    imshow("MODEL REGISTRATION", img_vis);


    // 等待直到按下ESC键
    waitKey(0);


    // 关闭并销毁窗口
    destroyWindow("MODEL REGISTRATION");


    cout << "再见" << endl;
}

知识点:

c5fd7cab3efc41e9175cd0d8eadae993.png

0cf4c26b89cb966ec3b9a552ba1a94ac.png

小孔成像模型

8e52c78e9dd50f8641288230ab80cf3b.png

941ed8d09e3fc2ebdc8f80a552d82031.png

创建PNP问题对象

a9706c6c255a9e23baffba8ba4b649d1.png

74e2e35e48d26146f213ea807ca4d3c9.png

加载ply网格

e8f3db354b82b639bcb64ad4ea8b6d83.png

不同特征检测器和描述符提取器

af84ef32756b0ca97aefed33ed0f58c0.png

估计相机的位姿:即相机相对于3D模型的旋转和平移

// 给定2D/3D对应点列表和使用的方法,估计姿态的函数
bool PnPProblem::estimatePose( const std::vector<cv::Point3f> &list_points3d,
                               const std::vector<cv::Point2f> &list_points2d,
                               int flags)
{
    // 初始化畸变系数矩阵、旋转向量和平移向量
    cv::Mat distCoeffs = cv::Mat::zeros(4, 1, CV_64FC1);
    cv::Mat rvec = cv::Mat::zeros(3, 1, CV_64FC1);
    cv::Mat tvec = cv::Mat::zeros(3, 1, CV_64FC1);


    // 使用外部猜测?(暂不使用)
    bool useExtrinsicGuess = false;


    // 姿态估计
    bool correspondence = cv::solvePnP( list_points3d, list_points2d, A_matrix_, distCoeffs, rvec, tvec,
                                        useExtrinsicGuess, flags);


    // 将旋转向量转换为矩阵
    Rodrigues(rvec, R_matrix_);
    t_matrix_ = tvec;
    
    // 设置投影矩阵
    this->set_P_matrix(R_matrix_, t_matrix_);


    return correspondence;
}

9f94b91034c1e5acecd49fd6cac0a91a.png

相机的位姿估计与物体的位姿估计区别

dd7d527f132370abe36a7d515400696f.png

参考网址:

https://zhuanlan.zhihu.com/p/389653208

https://zh.wikipedia.org/wiki/%E9%87%9D%E5%AD%94%E7%9B%B8%E6%A9%9F  针孔相机

http://www.powersensor.cn/p3_demo/demo4-camIdentify.html

https://blog.51cto.com/u_14439393/5732298

The End

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在django中使用kindeditor出现转圈问题

在django中使用kindeditor出现转圈问题 【一】基础检查 【1】前端检查 确保修改了uploadJson的默认地址 该地址需要在路由层有映射关系 确认有加载官方文件 kindeditor-all-min.js确保有传递csrfmiddlewaretoken 或者后端关闭了csrf验证 <textarea name"content&qu…

无人驾驶矿卡整体解决方案(5g物联网通信方案)

​无人驾驶矿卡是智能矿山的重要组成部分,通过远程操控替代人工驾驶,可以显著提高采矿效率和作业安全性。但要实现无人驾驶矿卡,需要依赖于可靠高效的通信网络,来传输现场视频、控制指令和运行数据。以下是某大型煤矿在部署无人驾驶矿卡时,所采用的星创易联物联网整体解决方案。…

如何区分模型文件是稳定扩散模型和LORA模型

区分模型文件是否为稳定扩散模型&#xff08;Stable Diffusion Models&#xff09;或LORA模型&#xff08;LowRank Adaptation&#xff09;通常需要对模型的结构和内容有一定的了解。以下是一些方法来区分这两种模型文件&#xff1a; 1. 文件格式和结构 稳定扩散模型&#xff1…

词根词缀基础

一&#xff0e;词根词缀方法&#xff1a; 1. 类似中文的偏旁部首&#xff08;比如“休”单人旁木→一个人靠木头上休息&#xff09; 2. 把单词拆分后&#xff0c;每一个部分都有它自己的意思&#xff0c;拼凑在一起就构成了这个单词的意思 3. 一个规律&#xff0c;适用大部分…

基于nodejs+vue多媒体素材管理系统python-flask-django-php

该系统采用了nodejs技术、express 框架&#xff0c;连接MySQL数据库&#xff0c;具有较高的信息传输速率与较强的数据处理能力。包含管理员、教师和用户三个层级的用户角色&#xff0c;系统管理员可以对个人中心、用户管理、教师管理、资源类型管理、资源信息管理、素材类型管理…

论文阅读-《Lite Pose: Efficient Architecture Design for 2D Human Pose Estimation》

摘要 这篇论文主要研究了2D人体姿态估计的高效架构设计。姿态估计在以人为中心的视觉应用中发挥着关键作用&#xff0c;但由于基于HRNet的先进姿态估计模型计算成本高昂&#xff08;每帧超过150 GMACs&#xff09;&#xff0c;难以在资源受限的边缘设备上部署。因此&#xff0…

(三)Ribbon负载均衡

1.1.负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件&#xff0c;来实现负载均衡功能的。 1.2.源码跟踪 为什么我们只输入了service名称就可以访问了呢&#xff1f;之前还要获取ip和端口。 显然有人帮我们根据service名称&#xff0c;获取到了服务实例的ip和…

GitLab更新失败(Ubuntu)

在Ubuntu下使用apt更新gitlab报错如下&#xff1a; An error occurred during the signature verification.The repository is not updated and the previous index files will be used.GPG error: ... Failed to fetch https://packages.gitlab.com/gitlab/gitlab-ee/ubuntu/d…

Leetcode 3.26

Leetcode Hot 100 一级目录1.每日温度 堆1.数组中的第K个最大元素知识点&#xff1a;排序复杂度知识点&#xff1a;堆的实现 2.前 K 个高频元素知识点&#xff1a;优先队列 一级目录 1.每日温度 每日温度 思路是维护一个递减栈&#xff0c;存储的是当前元素的位置。 遍历整个…

web学习笔记(四十五)Node.js

目录 1. Node.js 1.1 什么是Node.js 1.2 为什么要学node.js 1.3 node.js的使用场景 1.4 Node.js 环境的安装 1.5 如何查看自己安装的node.js的版本 1.6 常用终端命令 2. fs 文件系统模块 2.1引入fs核心模块 2.2 读取指定文件的内容 2.3 向文件写入指定内容 2.4 创…

app自动化-Appium学习笔记

使用Appium&#xff0c;优点&#xff1a; 1、支持语言比较多&#xff0c;例如&#xff1a;Java、Python、Javascript、PHP、C#等语言 2、支持跨应用&#xff08;windows、mac、linux&#xff09; 3、适用平台Android、iOS 4、支持Native App(原生app)、Web App、Hybird App…

canvas画图写文字,有0.5像素左右的位置偏差,无解决办法,希望有知道问题的大神告知一下

提示&#xff1a;canvas画图写文字 文章目录 前言一、写文字总结 前言 一、写文字 test.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-widt…

Fragment 与 ViewPager的联合应用(2)

5.创建底部布局bottom_layout <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:orientation"horizontal"android:layout_width"match_parent"android:layout_height"55dp"android:background&qu…

【算法】求最大公约数和最小公倍数

题目 输入两个数&#xff08;空格隔开&#xff09;分2行输出他们的最大公因数和最小公倍数 原理 辗转相除法计算最大公约数 将两个数中较大的数除以较小的数&#xff0c;并将较小的数作为除数&#xff0c;较大的数作为被除数。计算余数。若余数为零&#xff0c;则较小的数即…

深入探索MySQL高阶查询语句的艺术与实践

目录 引言 一、条件查询 &#xff08;一&#xff09;比较运算符查询 1.使用匹配符号查询 2.范围查找 &#xff08;二&#xff09;逻辑运算符 二、关键字排序 三、分组与聚合函数 四、限制查询 五、别名 &#xff08;一&#xff09;设置列别名 &#xff08;二&#x…

Dockerfile和Docker-compose

一、概述 Dockerfile和Docker Compose是用于构建和管理 Docker 容器的两个工具&#xff0c;但它们的作用和使用方式不同。 Dockerfile Dockerfile 是一个文本文件&#xff0c;用于定义 Docker 镜像的构建规则。它包含一系列指令&#xff0c;如 FROM&#xff08;指定基础镜像…

python(django)之单一接口管理功能后台开发

1、创建数据模型 在apitest/models.py下加入以下代码 class Apis(models.Model):Product models.ForeignKey(product.Product, on_deletemodels.CASCADE, nullTrue)# 关联产品IDapiname models.CharField(接口名称, max_length100)apiurl models.CharField(接口地址, max_…

uniapp微信小程序_computed_计算BMI

一、computed的用法还有它是什么&#xff1f; 首先它叫计算属性&#xff0c;顾名思义他是用来计算属性&#xff0c;计算你在data模板上定义的属性&#xff08;其实在插值表达式也能直接计算但是首先太长了在{{}}里面写那么多不好看&#xff0c;还有其他特点我在下面一起说&…

jupyter notebook导出含中文的pdf(LaTex安装和Pandoc、MiKTex安装)

用jupyter notebook导出pdf时&#xff0c;因为报错信息&#xff0c;需要用到Tex nbconvert failed: xelatex not found on PATH, if you have not installed xelatex you may need to do so. Find further instructions at https://nbconvert.readthedocs.io/en/latest/install…

nacos集群搭建实战

集群结构图 初始化数据库 Nacos默认数据存储在内嵌数据库Derby中&#xff0c;不属于生产可用的数据库。官方推荐的使用mysql数据库&#xff0c;推荐使用数据库集群或者高可用数据库。 首先新建一个数据库&#xff0c;命名为nacos&#xff0c;而后导入下面的SQL&#xff08;直…