3 OpenCV两张图片实现稀疏点云的生成

news2024/11/25 7:51:13

前文:
1 基于SIFT图像特征识别的匹配方法比较与实现
2 OpenCV实现的F矩阵+RANSAC原理与实践

1 E矩阵

1.1 由F到E

E = K T ∗ F ∗ K E = K^T * F * K E=KTFK

E 矩阵可以直接通过之前算好的 F 矩阵与相机内参 K 矩阵获得

Mat E = K.t() * F * K;

相机内参获得的方式是一个较为复杂的方式,需要使用棋盘进行定位获得,我们这里直接使用了 OpenMVG 提供的现成的图片和 K 矩阵

1.2 直接使用函数

利用 openCV 提供的 findEssentialMat 函数可以直接得到 E 矩阵

Mat E = findEssentialMat(matchedPoints1, matchedPoints2, K, RANSAC, 0.999, 1.0, inliers);

2 相机姿态恢复

这一步可以使用 SVD 来通过 E 矩阵获取相对旋转矩阵 R平移向量 t

但是OpenCV直接提供了一个非常便捷的函数 —— recoverPose

其接受本质矩阵 E 、特征点的对应关系、相机的内参信息以及输出的相对旋转矩阵 R 和平移向量 t ;它会自动进行 SVD 分解和其他必要的计算,以恢复相对姿态信息

 //相机姿态恢复,求解R,t,投影矩阵
 Mat R, t;
 recoverPose(E, inlierPoints1, inlierPoints2, K, R, t);

3 相机投影矩阵

要构建相机的投影矩阵(也称为视图矩阵或外参矩阵),需要将旋转矩阵 R 和平移向量 t 合并到一起,投影矩阵通常表示为 3x4 的矩阵,其中旋转矩阵和平移向量都位于其中的适当位置

通常情况下,投影矩阵的形式如下:
P 1 = K ∗ [ R ∣ t ] P_1 = K * [R | t] P1=K[Rt]

P 2 = K ∗ [ I ∣ 0 ] P_2 = K * [I | 0] P2=K[I∣0]

实现代码如下:

// 创建两个相机的投影矩阵 [R T]
Mat proj1(3, 4, CV_32FC1);
Mat proj2(3, 4, CV_32FC1);

// 设置第一个相机的投影矩阵为单位矩阵 [I | 0]
proj1(Range(0, 3), Range(0, 3)) = Mat::eye(3, 3, CV_32FC1);
proj1.col(3) = Mat::zeros(3, 1, CV_32FC1);

// 设置第二个相机的投影矩阵为输入的旋转矩阵 R 和平移向量 T
R.convertTo(proj2(Range(0, 3), Range(0, 3)), CV_32FC1);
t.convertTo(proj2.col(3), CV_32FC1);

// 转换相机内参矩阵 K 为浮点型
Mat fK;
K.convertTo(fK, CV_32FC1);

// 计算投影矩阵 [K * [R|T]]
proj1 = fK * proj1;
proj2 = fK * proj2;

4 三角法得稀疏点云

4.1 三角法计算3D点

对于每对匹配的特征点,可以使用三角法来计算它们的三维坐标;这通常涉及到将两个视角下的像素坐标与相应的投影矩阵相结合,以恢复三维坐标

// 三角法求解稀疏三维点云
Mat point4D_homogeneous(4, inlierPoints1.size(), CV_64F);
triangulatePoints(proj1, proj2, inlierPoints1, inlierPoints2, point4D_homogeneous);

4.2 转换为非齐次坐标

函数 triangulatePoints 得到的 point4D_homogeneous 通常是齐次坐标,需要将它们转换为非齐次坐标,以得到真实的三维点坐标

// 将齐次坐标转换为三维坐标
Mat point3D;
convertPointsFromHomogeneous(point4D_homogeneous.t(), point3D);
cout << point3D << endl;

5 匹配颜色

将颜色信息与点云关联在一起

使用了内点(inliers)的坐标从图像中提取了颜色信息,然后将颜色信息与三维点坐标关联起来,生成了带有颜色的稀疏点云

并将其存储在 pointCloudpointColors 中;就可以根据需要进一步处理颜色信息

 // 获取特征点的颜色信息
 vector<Vec3b> colors1, colors2; // 颜色信息

 for (Point2f& inlierPoints : inlierPoints1)
 {
     int x = cvRound(inlierPoints.x); // 关键点的x坐标
     int y = cvRound(inlierPoints.y); // 关键点的y坐标
     Vec3b color = img1.at<Vec3b>(y, x);
     colors1.push_back(color);
 }

 for (Point2f& inlierPoints : inlierPoints2)
 {
     int x = cvRound(inlierPoints.x); // 关键点的x坐标
     int y = cvRound(inlierPoints.y); // 关键点的y坐标
     Vec3b color = img2.at<Vec3b>(y, x);
     colors2.push_back(color);
 }

 // 创建带颜色的点云数据结构
 vector<Point3f> pointCloud;
 vector<Vec3b> pointColors;

 // 关联颜色信息到点云
 for (int i = 0; i < point3D.rows; ++i)
 {
     Point3f point = point3D.at<Point3f>(i);
     Vec3b color1 = colors1[i];
     Vec3b color2 = colors2[i];

     // 在这里可以根据需要选择使用哪个颜色,或者进行颜色插值等处理
     Vec3b finalColor = color1; // 这里示例使用第一个相机的颜色

     pointCloud.push_back(point);
     pointColors.push_back(finalColor);
 }

6 生成 ply 文件

手动输出点云 PLY 文件,并包括了 PLY 文件的头部信息以及点云数据的写入;这是一种创建包含颜色信息的 PLY 文件的有效方法

在 PLY 文件的头部信息中,指定了点的数量以及点的属性,包括点的坐标和颜色通道(蓝色、绿色、红色)然后,你循环遍历点云数据,将点的坐标和颜色信息写入PLY文件

其会生成一个包含点云和颜色信息的PLY文件,可以将其用于保存点云以进行可视化或进一步处理

特别注意:图片是RGB还是BGR的颜色通道,这里是RGB

color[0]color[1]color[2] 分别代表蓝色,绿色,红色通道

// 手动输出点云ply文件
ofstream plyFile(PLY_SAVE_PATH);

// ply的头部信息
plyFile << "ply\n";
plyFile << "format ascii 1.0\n";
plyFile << "element vertex " << point3D.rows << "\n";
plyFile << "property float x\n";
plyFile << "property float y\n";
plyFile << "property float z\n";
plyFile << "property uchar blue\n";
plyFile << "property uchar green\n";
plyFile << "property uchar red\n";
plyFile << "end_header\n";

// 写入点云数据
for (int i = 0; i < point3D.rows; ++i)
{
    Vec3b color = pointColors[i];
    const float* point = point3D.ptr<float>(i);
    plyFile << point[0] << " " << point[1] << " " << point[2] << " "
            << static_cast<int>(color[0]) << " "
            << static_cast<int>(color[1]) << " "
            << static_cast<int>(color[2]) << endl;
}

plyFile.close();   

7 完整测试代码

关于之前的阶段可以查看我之前的文章

// 定义图像文件路径和保存结果的路径
#define IMG_PATH1 "test_img\\images\\100_7105.jpg"
#define IMG_PATH2 "test_img\\images\\100_7106.jpg"
#define PLY_SAVE_PATH "test_img\\results\\output.ply"
#define K_NUM 2905.88, 0, 1416, 0, 2905.88, 1064, 0, 0, 1 // 3*3

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <fstream>

using namespace std;
using namespace cv;

int main()
{
    // 阶段一------------------------------------------------------------------------------------
    // 读取两幅图像
    Mat img1 = imread(IMG_PATH1);
    Mat img2 = imread(IMG_PATH2);
    if (img1.empty() || img2.empty())
    {
        cout << "无法读取图像" << endl;
        return -1;
    }

    // 创建SIFT对象
    Ptr<SIFT> sift = SIFT::create();
    vector<KeyPoint> keypoints1, keypoints2;
    Mat descriptors1, descriptors2;

    // 检测关键点并计算描述子
    sift->detectAndCompute(img1, noArray(), keypoints1, descriptors1);
    sift->detectAndCompute(img2, noArray(), keypoints2, descriptors2);

    // 使用FLANN进行特征匹配
    FlannBasedMatcher matcher;
    vector<vector<DMatch>> matches;
    matcher.knnMatch(descriptors1, descriptors2, matches, 2);
    vector<DMatch> good_matches;
    for (int i = 0; i < matches.size(); ++i)
    {
        const float ratio = 0.7f;
        if (matches[i][0].distance < ratio * matches[i][1].distance)
        {
            good_matches.push_back(matches[i][0]);
        }
    }
    
    
    // 阶段二------------------------------------------------------------------------------------
    // 声明用于保存匹配点对的容器
    vector<Point2f> matchedPoints1, matchedPoints2;

    for (int i = 0; i < good_matches.size(); ++i)
    {
        matchedPoints1.push_back(keypoints1[good_matches[i].queryIdx].pt);
        matchedPoints2.push_back(keypoints2[good_matches[i].trainIdx].pt);
    }
    

    // 进行基本矩阵F的估计并使用RANSAC筛选
    Mat F;
    vector<uchar> inliers;
    F = findFundamentalMat(matchedPoints1, matchedPoints2, inliers, FM_RANSAC);
    cout << F << endl;

    vector<Point2f> inlierPoints1;
    vector<Point2f> inlierPoints2;
    for (int i = 0; i < inliers.size(); ++i)
    {
        if (inliers[i])
        {
			inlierPoints1.push_back(matchedPoints1[i]);
			inlierPoints2.push_back(matchedPoints2[i]);
		}
	}

    // 相机内参矩阵K
    Mat K = (Mat_<double>(3, 3) << K_NUM);
    cout << K << endl;

    计算本质矩阵E
    //Mat E = findEssentialMat(matchedPoints1, matchedPoints2, K, RANSAC, 0.999, 1.0, inliers);
    Mat E = K.t() * F * K;
    cout << "Essential Matrix (E):" << endl;
    cout << E << endl;
    
    //相机姿态恢复,求解R,t,投影矩阵
    Mat R, t;
    recoverPose(E, inlierPoints1, inlierPoints2, K, R, t);
    cout << "recoverpose" << endl;
    cout << "R:" << R << endl;
    cout << "t:" << t << endl;
    
    // 创建两个相机的投影矩阵 [R T]
    Mat proj1(3, 4, CV_32FC1);
    Mat proj2(3, 4, CV_32FC1);
    
    // 设置第一个相机的投影矩阵为单位矩阵 [I | 0]
    proj1(Range(0, 3), Range(0, 3)) = Mat::eye(3, 3, CV_32FC1);
    proj1.col(3) = Mat::zeros(3, 1, CV_32FC1);
    
    // 设置第二个相机的投影矩阵为输入的旋转矩阵 R 和平移向量 T
    R.convertTo(proj2(Range(0, 3), Range(0, 3)), CV_32FC1);
    t.convertTo(proj2.col(3), CV_32FC1);
    
    // 转换相机内参矩阵 K 为浮点型
    Mat fK;
    K.convertTo(fK, CV_32FC1);
    
    // 计算投影矩阵 [K * [R|T]]
    proj1 = fK * proj1;
    proj2 = fK * proj2;
    
    // 三角法求解稀疏三维点云
    Mat point4D_homogeneous(4, inlierPoints1.size(), CV_64F);
    triangulatePoints(proj1, proj2, inlierPoints1, inlierPoints2, point4D_homogeneous);
   

    // 将齐次坐标转换为三维坐标
    Mat point3D;
    convertPointsFromHomogeneous(point4D_homogeneous.t(), point3D);
    cout << point3D << endl;

    // 获取特征点的颜色信息
    vector<Vec3b> colors1, colors2; // 颜色信息

    for (Point2f& inlierPoints : inlierPoints1)
    {
        int x = cvRound(inlierPoints.x); // 关键点的x坐标
        int y = cvRound(inlierPoints.y); // 关键点的y坐标
        Vec3b color = img1.at<Vec3b>(y, x);
        colors1.push_back(color);
    }

    for (Point2f& inlierPoints : inlierPoints2)
    {
        int x = cvRound(inlierPoints.x); // 关键点的x坐标
        int y = cvRound(inlierPoints.y); // 关键点的y坐标
        Vec3b color = img2.at<Vec3b>(y, x);
        colors2.push_back(color);
    }

    // 创建带颜色的点云数据结构
    vector<Point3f> pointCloud;
    vector<Vec3b> pointColors;

    // 关联颜色信息到点云
    for (int i = 0; i < point3D.rows; ++i)
    {
        Point3f point = point3D.at<Point3f>(i);
        Vec3b color1 = colors1[i];
        Vec3b color2 = colors2[i];

        // 在这里可以根据需要选择使用哪个颜色,或者进行颜色插值等处理
        Vec3b finalColor = color1; // 这里示例使用第一个相机的颜色

        pointCloud.push_back(point);
        pointColors.push_back(finalColor);
    }

    // 手动输出点云ply文件
    ofstream plyFile(PLY_SAVE_PATH);

    // ply的头部信息
    plyFile << "ply\n";
    plyFile << "format ascii 1.0\n";
    plyFile << "element vertex " << point3D.rows << "\n";
    plyFile << "property float x\n";
    plyFile << "property float y\n";
    plyFile << "property float z\n";
    plyFile << "property uchar blue\n";
    plyFile << "property uchar green\n";
    plyFile << "property uchar red\n";
    plyFile << "end_header\n";

    // 写入点云数据
    for (int i = 0; i < point3D.rows; ++i)
    {
        Vec3b color = pointColors[i];
        const float* point = point3D.ptr<float>(i);
        plyFile << point[0] << " " << point[1] << " " << point[2] << " "
                << static_cast<int>(color[0]) << " "
                << static_cast<int>(color[1]) << " "
                << static_cast<int>(color[2]) << endl;
    }

    plyFile.close();   
    return 0;
}

最终效果:

特别提醒:用 meshlab 打开后记得在右侧的设置框中将 shading 改为None !!!这样才能看到真正的颜色,也可以把点调大一点好看些

image-20230925090744151
image-20230925090905231 100_7103

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

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

相关文章

spring6-IOC容器

IOC容器 1、IoC容器1.1、控制反转&#xff08;IoC&#xff09;1.2、依赖注入1.3、IoC容器在Spring的实现 2、基于XML管理Bean2.1、搭建子模块spring6-ioc-xml2.2、实验一&#xff1a;获取bean①方式一&#xff1a;根据id获取②方式二&#xff1a;根据类型获取③方式三&#xff…

Zilliz@阿里云:大模型时代下Milvus Cloud向量数据库处理非结构化数据的最佳实践

大模型时代下的数据存储与分析该如何处理?有没有已经落地的应用实践? 为探讨这些问题,近日,阿里云联合 Zilliz 和 Doris 举办了一场以《大模型时代下的数据存储与分析》为主题的技术沙龙,其中,阿里云对象存储 OSS 上拥有海量的非结构化数据,Milvus(Zilliz)作为全球最有…

华为摄像头智能安防监控解决方案

云时代来袭&#xff0c;数字化正在从园区办公延伸到生产和运营的方方面面&#xff0c;智慧校园&#xff0c;柔性制造&#xff0c;掌上金融和电子政务等&#xff0c;面对各种各样的新兴业态的涌现&#xff0c;企业需要构建一张无所不联、随心体验、业务永续的全无线网络&#xf…

多线程锁-线程锁知识概述、乐观锁和悲观锁

3. 说说Java"锁"事 3.1 从轻松的乐观锁和悲观锁开讲 悲观锁&#xff1a; 认为自己在使用数据的时候一定有别的线程来修改数据&#xff0c;因此在获取数据的时候会先加 锁&#xff0c;确保数据不会被别的线程修改&#xff0c;synchronized和Lock的实现类都是悲观锁…

【通意千问】大模型GitHub开源工程学习笔记(2)

使用Transformers来使用模型 如希望使用Qwen-chat进行推理,所需要写的只是如下所示的数行代码。请确保你使用的是最新代码,并指定正确的模型名称和路径,如Qwen/Qwen-7B-Chat和Qwen/Qwen-14B-Chat 这里给出了一段代码 from transformers import AutoModelForCausalLM, Aut…

正点原子lwIP学习笔记——WebServer实验

1. WebServer简介 Web Server就是提供Web服务的Server&#xff0c;主要功能是&#xff1a;存储、处理和传递网页给客户端&#xff0c;他只需支持HTTP协议、HTML文档格式以及URL&#xff0c;与客户端的网络浏览器配套。 其中&#xff0c;HTTP的协议就是基于TCP进一步实现的&…

零代码编程:用ChatGPT批量修改文件夹名称中的大小写

一个文件夹下面有很多个子文件夹&#xff0c;要把文件夹中的大写数字全部重命名为小写数字&#xff0c;比如将二 三 四&#xff0c;改成&#xff1a; 2 34 在ChatGPT中输入提示词如下&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个文件夹重命名的任务。具体步骤如…

芯片测试方案之如何测试芯片EN输入阈值?

在电源管理芯片的设计中&#xff0c;除了常规的VIN、VOUT以及GND端口之外&#xff0c;还会有SW、EN、FB等芯片独有的特殊端口引脚&#xff0c;这些引脚或负责电源开关的输入&#xff0c;或负责电路的反馈电压/电流&#xff0c;这些引脚在芯片的工作中有着极其重要的作用&#x…

CUDA学习笔记0929

一、GPU缓存和变量作用域 1. 缓存类型 &#xff08;1&#xff09;GPU缓存是非可编程存储区域 &#xff08;2&#xff09;GPU包含4类缓存&#xff1a; L1缓存&#xff0c;每个流处理器一个 L2缓存&#xff0c;全部流处理器共享一个 L1和L2都可用于存储本地和全局内存中的数…

了解vtk显示的原理

文章目录 目标:知识补充:1.什么是图元?2.最让我不解的是:官方讲的是:mapper讲polydata转换为可渲染的图元数据,然后actor是将polydata映射为可渲染的图元???既然mapper就已经将其解析为图元数据,为什么actor还要进一步解析呢?3.那polydata不是也获得了一些数据,这些数据是…

快速将iPhone大量照片快速传输到电脑的办法!

很多使用iPhone 的朋友要将照片传到电脑时&#xff0c;第一时间都只想到用iTunes 或iCloud&#xff0c;但这2个工具真的都非常难用&#xff0c;今天小编分享牛学长苹果数据管理工具的照片传输功能&#xff0c;他可以快速的将iPhone照片传输到电脑上&#xff0c;并且支持最新的i…

C语言中自定义类型讲解

前言&#xff1a;C语言中拥有三种自定义类型&#xff0c;这三种自定义类型是怎么运用呢&#xff1f;在内存中又是怎么存储的呢&#xff1f;通过这篇文章我们来逐个讲解讲解。 三种类型分别是&#xff1a; 1.结构体 – 通俗的来讲就是可以把不同类型的变量放在一个集合中 2.枚举…

计算机图像处理-直方图均衡化

直方图均衡化 直方图均衡化是图像灰度变换中有一个非常有用的方法。图像的直方图是对图像对比度效果上的一种处理&#xff0c;旨在使得图像整体效果均匀&#xff0c;黑与白之间的各个像素级之间的点分布更均匀一点。通过这种方法&#xff0c;亮度可以更好地在直方图上分布。 …

将外包jar包导入到本地Maven仓库中

文章目录 1.问题描述2.方法如下 1.问题描述 有时候我们需要引入阿里云或者mvnRespository网上没有对于的jar。需要下载别人的jar的包&#xff0c;然后放到自己的项目的libs目录下。这样很不方便。因此需要把外包的jar,导入到本地maven仓库中。这样再pom.xml文件中直接按三要素…

项目进展(四)-双电机均可驱动,配置模拟SPI,调平仪功能初步实现!

一、前言 截止到今天&#xff0c;该项目也算实现基本功能了&#xff0c;后续继续更新有关32位ADC芯片相关的内容&#xff0c;今天对驱动芯片做一个总结&#xff0c;也对模拟SPI做一点总结吧 二、模拟SPI 由于模拟SPI还是得有四种模式(CPOL和CPHA组合为四种)&#xff0c;下面…

虚拟DOM详解

面试题&#xff1a;请你阐述一下对vue虚拟dom的理解 什么是虚拟dom&#xff1f; 虚拟dom本质上就是一个普通的JS对象&#xff0c;用于描述视图的界面结构 在vue中&#xff0c;每个组件都有一个render函数&#xff0c;每个render函数都会返回一个虚拟dom树&#xff0c;这也就意味…

基于视频技术与AI检测算法的体育场馆远程视频智能化监控方案

一、方案背景 近年来&#xff0c;随着居民体育运动意识的增强&#xff0c;体育场馆成为居民体育锻炼的重要场所。但使用场馆内的器材时&#xff0c;可能发生受伤意外&#xff0c;甚至牵扯责任赔偿纠纷问题。同时&#xff0c;物品丢失、人力巡逻成本问题突出&#xff0c;体育场…

系统集成|第十九章(笔记)

目录 第十九章 风险管理19.1 风险管理的概述及相关概念19.2 主要过程19.2.1 规划风险管理19.2.2 识别风险19.2.3 实施定性风险分析19.2.4 实施定量风险分析19.2.5 规划风险应对19.2.6 控制风险 上篇&#xff1a;第十八章、安全管理 下篇&#xff1a;第二十章、收尾管理 第十九…

什么是密码管理,密码管理的重要性

密码管理是通过遵守一套可持续的做法&#xff0c;在从创建到关闭的整个生命周期中保护和管理密码的过程。这是在存储特权的密码管理器的帮助下实现的 使用内置加密保管库的凭据。 随着 IT 环境的扩展&#xff0c;密码激增&#xff0c;并且随着需要保护的密码越来越多&#xff…

SpringMVC 学习(八)整合SSM

10. 整合 SSM (1) 新建数据库 CREATE DATABASE SSM;USE SSM;DROP TABLE IF EXISTS BOOKS;CREATE TABLE BOOKS (BOOK_ID INT(10) NOT NULL AUTO_INCREMENT COMMENT 书ID,BOOK_NAME VARCHAR(100) NOT NULL COMMENT 书名,BOOK_COUNTS INT(11) NOT NULL COMMENT 数量,DETAIL VARCH…