Opencv计算机视觉编程攻略-第十一节 三维重建

news2025/4/8 8:38:20

      此处重点讨论在特定条件下,重建场景的三维结构和相机的三维姿态的一些应用实现。下面是完整投影公式最通用的表示方式。

        在上述公式中,可以了解到,真实物体转为平面之后,s系数丢失了,因而无法会的三维坐标,而s系数其实是与直接距离Z相关,实际上,如果知道物体到相机成像平面距离,相机的内参、外参就可以获得空间坐标:

1. 相机标定

       相机标定就是设置相机各种参数(即投影公式中的内参,外参)的过程。当然也可以使用相机厂家提供的技术参数,利用正确的相机标定方法,即可得到精确的标定信息。

      相机标定的基本原理:确定场景中一系列点的三维坐标并拍摄这个场景,然后观测这些点
在图像上投影的位置。有了足够多的三维点和图像上对应的二维点,就可以根据投影方程推断出
准确的相机参数。

      (1)一种方法是对一个包含大量三维点的场景取像(一些车辆在出场会使用)。

      (2)更实用的做法是从不同的视角为一些三维点拍摄多个照片。这种方法相对比较简单,但是它除了需要计算相机本身的参数,还需要计算每个相机视图的位置,OpenCV 推荐使用国际象棋棋盘的图案生成用于标定的三维场景点的集合。

        这个图案在每个方块的角点位置创建场景点;由于图案是平面的,可以假设棋盘位于Z=0 且X 和Y 的坐标轴与网格对齐的位置。

// 1. 输出图像角点的向量
std::vector<cv::Point2f> imageCorners;
// 棋盘内部角点的数量
cv::Size boardSize(7,5);
// 获得棋盘角点
bool found = cv::findChessboardCorners(
image, // 包含棋盘图案的图像
boardSize, // 图案的尺寸
imageCorners); // 检测到的角点列表

// 2. 画出角点
cv::drawChessboardCorners(image, boardSize,
imageCorners, found); // 找到的角点

        连接角点的线条的次序,就是角点在向量中存储的次序。在进行标定前,需要指定相关的三维点。指定这些点时可自由选择单位(例如厘米或英寸),不过最简单的办法是将方块的边长指定为一个单位。这样第一个点的坐标就是(0, 0, 0)(假设棋盘的纵深坐标为Z = 0),第二个点的坐标是(1, 0, 0),最后一个点的坐标是(6, 4, 0)。这个图案共有35 个点;若要进行精确的标定,这些点是远远不够的。为了得到更多的点,需要从不同的视角对同一个标定图案拍摄更多的照片。可以在相机前移动图案,也可以在棋盘周围移动相机。从数学的角度看,这两种方法是完全等效的。OpenCV 的标定函数假定由标定图案确定坐标系,并计算相机相对于坐标系的旋转量和平移量。

// 打开棋盘图像,提取角点
int CameraCalibrator::addChessboardPoints(
const std::vector<std::string> & filelist, // 文件名列表
cv::Size & boardSize) { // 标定面板的大小
// 棋盘上的角点
std::vector<cv::Point2f> imageCorners;
std::vector<cv::Point3f> objectCorners;
// 场景中的三维点:
// 在棋盘坐标系中,初始化棋盘中的角点
// 角点的三维坐标(X,Y,Z)= (i,j,0)
for (int i=0; i<boardSize.height; i++) {
for (int j=0; j<boardSize.width; j++) {
objectCorners.push_back(cv::Point3f(i, j, 0.0f));
}
}
// 图像上的二维点:
cv::Mat image; // 用于存储棋盘图像
int successes = 0;
// 处理所有视角
for (int i=0; i<filelist.size(); i++) {
// 打开图像
image = cv::imread(filelist[i],0);
// 取得棋盘中的角点
bool found = cv::findChessboardCorners(
image, // 包含棋盘图案的图像
boardSize, // 图案的大小
imageCorners); // 检测到角点的列表
// 取得角点上的亚像素级精度
if (found) {
cv::cornerSubPix(image, imageCorners,
cv::Size(5,5), // 搜索窗口的半径
cv::Size(-1,-1),
cv::TermCriteria( cv::TermCriteria::MAX_ITER +
cv::TermCriteria::EPS,30, // 最大迭代次数
0.1)); // 最小精度
// 如果棋盘是完好的,就把它加入结果
if (imageCorners.size() == boardSize.area()) {
// 加入从同一个视角得到的图像和场景点
addPoints(imageCorners, objectCorners);
successes++;
}
}
// 如果棋盘是完好的,就把它加入结果
if (imageCorners.size() == boardSize.area()) {
// 加入从同一个视角得到的图像和场景点
addPoints(imageCorners, objectCorners);
successes++;
}
}
return successes;
}

             

    处理完足够数量的棋盘图像后(这时就有了大量的三维场景点/二维图像点的对应关系),就可以开始计算标定参数了:

// 返回重投影误差
double CameraCalibrator::calibrate(cv::Size &imageSize) {
// 输出旋转量和平移量
std::vector<cv::Mat> rvecs, tvecs;
// 开始标定
return calibrateCamera(objectPoints, // 三维点
    imagePoints, // 图像点
    imageSize, // 图像尺寸
    cameraMatrix, // 输出相机矩阵
    distCoeffs, // 输出畸变矩阵
    rvecs, tvecs, // Rs、Ts
    flag); // 设置选项
}
//根据经验,10~20 个棋盘图像就足够了,但是这些图像的深度和拍摄视角必须不同

       用刚标定的相机拍摄的所有图像,在标定类中增加了一个额外畸变矫正的方法:

// 去除图像中的畸变(标定后)
cv::Mat CameraCalibrator::remap(const cv::Mat &image) {
cv::Mat undistorted;
if (mustInitUndistort) { // 每个标定过程调用一次
cv::initUndistortRectifyMap(
cameraMatrix, // 计算得到的相机矩阵
distCoeffs, // 计算得到的畸变矩阵
cv::Mat(), // 可选矫正项(无)
cv::Mat(), // 生成无畸变的相机矩阵
image.size(), // 无畸变图像的尺寸
CV_32FC1, // 输出图片的类型
map1, map2); // x 和y 映射功能
mustInitUndistort= false;
}
// 应用映射功能
cv::remap(image, undistorted, map1, map2,
cv::INTER_LINEAR); // 插值类型
return undistorted;
}


2. 相机姿态还原

       标定后,相机就可以用来构建照片与现实场景的对应关系。如果一个物体的三维结构是已知的,就能得到它在相机传感器上的成像情况。如果该方程中的大多数项目是已知的,利用若干张照片,就可以计算出其他元素(二维或三维)的值。在已知三维结构的情况下,计算出相机的姿态。

// 根据三维/二维点得到相机姿态
cv::Mat rvec, tvec;
cv::solvePnP(
objectPoints, imagePoints, // 对应的三维/二维点
cameraMatrix, cameraDistCoeffs, // 标定
rvec, tvec); // 输出姿态
// 转换成三维旋转矩阵
cv::Mat rotation;
cv::Rodrigues(rvec, rotation);

     本质是求解刚体变换(旋转和平移),这就是透视n 点定位(Perspective-n-Point,PnP)问题,把物体坐标转换到以相机为中心的坐标系上(即以焦点为坐标原点)。

     在OpenCV 中,cv::viz 是一个基于可视化工具包(Visualization Toolkit,VTK)的附加模块。它是一个强大的三维计算机视觉框架,可以创建虚拟的三维环境,并添加各种物体。它会创建可视化的窗口,用来显示从特定视角观察到的虚拟环境。

// 1  创建viz 窗口
cv::viz::Viz3d visualizer("Viz window");
visualizer.setBackgroundColor(cv::viz::Color::white());

// 2 创建一个虚拟相机
cv::viz::WCameraPosition cam(
cMatrix, // 内部参数矩阵
image, // 平面上显示的图像
30.0, // 缩放因子
cv::viz::Color::black());
// 在环境中添加虚拟相机
visualizer.showWidget("Camera", cam);

// 3 用长方体表示虚拟的长椅
cv::viz::WCube plane1(cv::Point3f(0.0, 45.0, 0.0),
cv::Point3f(242.5, 21.0, -9.0),
true, // 显示线条框架
cv::viz::Color::blue());
plane1.setRenderingProperty(cv::viz::LINE_WIDTH, 4.0);
cv::viz::WCube plane2(cv::Point3f(0.0, 9.0, -9.0),
cv::Point3f(242.5, 0.0, 44.5),
true, // 显示线条框架
cv::viz::Color::blue());
plane2.setRenderingProperty(cv::viz::LINE_WIDTH, 4.0);

// 4 把虚拟物体加入到环境中
visualizer.showWidget("top", plane1);
visualizer.showWidget("bottom", plane2);

cv::Mat rotation;
// 将rotation 转换成3×3 的旋转矩阵
cv::Rodrigues(rvec, rotation);
// 移动长椅
cv::Affine3d pose(rotation, tvec);
visualizer.setWidgetPose("top", pose);
visualizer.setWidgetPose("bottom", pose);
最后用一个循环,不断显示可视化窗口。中间暂停1 毫秒,以响应鼠标事件:
// 循环显示
while(cv::waitKey(100)==-1 && !visualizer.wasStopped()) {
visualizer.spinOnce(1, // 暂停1 毫秒
true); // 重绘
}


3. 用标定相机实现三维重建

        当从多个视角观察同一个场景时,即使没有三维场景的任何信息,也可以重建三维姿态和结构。我们这次将利用不同视角下图像点之间的关系,计算出三维信息。

       相机的标定参数是能够获取到的,因此可以使用世界坐标系,还可以用它在相机姿态和对应点的位置之间建立一个物理约束。这里引入一个新的数学实体——本质矩阵。简单来说,本质矩阵就是经过标定的基础矩阵。

// 找出image1 和image2 之间的本质矩阵
cv::Mat inliers;
cv::Mat essential = cv::findEssentialMat(points1, points2,
Matrix, // 内部参数 相当于给出了内参
cv::RANSAC,
0.9, 1.0, // RANSAC 方法
inliers); // 提取到的内点

      将匹配到的同名点调用triangulate 函数,计算三角剖分点的位置:

// 根据旋转量R 和平移量T 构建投影矩阵
cv::Mat projection2(3, 4, CV_64F); // 3×4 的投影矩阵
rotation.copyTo(projection2(cv::Rect(0, 0, 3, 3)));
translation.copyTo(projection2.colRange(3, 4));
// 构建通用投影矩阵
cv::Mat projection1(3, 4, CV_64F, 0.); // 3×4 的投影矩阵
cv::Mat diag(cv::Mat::eye(3, 3, CV_64F));
diag.copyTo(projection1(cv::Rect(0, 0, 3, 3)));
// 用于存储内点
std::vector<cv::Vec2d> inlierPts1;
std::vector<cv::Vec2d> inlierPts2;
// 创建输入内点的容器,用于三角剖分
int j(0);
for (int i = 0; i < inliers.rows; i++) {
if (inliers.at<uchar>(i)) {
inlierPts1.push_back(cv::Vec2d(points1[i].x, points1[i].y));
inlierPts2.push_back(cv::Vec2d(points2[i].x, points2[i].y));
}
}
// 矫正并标准化图像点
std::vector<cv::Vec2d> points1u;
cv::undistortPoints(inlierPts1, points1u,
cameraMatrix, cameraDistCoeffs);
std::vector<cv::Vec2d> points2u;
cv::undistortPoints(inlierPts2, points2u,
cameraMatrix, cameraDistCoeffs);
// 三角剖分
std::vector<cv::Vec3d> points3D;
triangulate(projection1, projection2,
points1u, points2u, points3D);

        图中有两个相机,相对的旋转量为R,平移量为T。平移向量T 刚好连接了两个相机的投影中心点。此外,向量x 连接第一个相机的中心点与一个图像点,向量x'连接第二个相机的中心点
与对应的图像点。因为这两个相机之间的移动量是已知的,所以可以用与第二个相机的相对值来表示x 的方向,记为Rx。仔细观察图像点的几何形状,就能发现T、Rx 和x'在同一个平面上。这个关系可用数学公式表示

       由于噪声和数字化过程的影响,理想情况下应该相交的投影线在实际中一般不会相交。所以用最小二乘法就可以大致找到交点的位置。但这种方法无法重建无穷远处的点,因为它们的齐次坐标的第4 个元素为0,而不是假定的1。还有一点很重要,三维重建只受限于缩放因子。如果要测量实际尺寸,就必须预先确定至少一个长度值,例如两个相机之间的实际距离或者画面中某个物体的实际高度。


4. 计算立体图像的深度

        人类用两只眼睛观察三维世界,装上两台相机后,机器也可以看到三维世界,这就是立体视觉。在同一个设备上安装两台相机,让它们观察同一个场景,并且两者之间有固定的基线(即相机之间的距离),就构成了一个立体视觉装置。

        两台相机之间只有水平方向的平移,因此它们的所有对极线都是水平方向的。这意味着所有关联点的y 坐标都是相同的,只需要在一维的线条上寻找匹配项即可。关联点x 坐标的差值则取决于点的深度。无穷远处的点对应图像点的坐标相同,都是(x, y),而它们离装置越近,x 坐标的差值就越大,这时计算差值x -x'(注意要除以s 以符合齐次坐标系),并分离出z 坐标,可得到:

// 1 计算单应变换矫正量
cv::Mat h1, h2;
cv::stereoRectifyUncalibrated(points1, points2,
fundamental,
image1.size(), h1, h2);

// 2 用变换实现图像矫正
cv::Mat rectified1;
cv::warpPerspective(image1, rectified1, h1, image1.size());
cv::Mat rectified2;
cv::warpPerspective(image2, rectified2, h2, image1.size());

// 3 计算视差
cv::Mat disparity;
cv::Ptr<cv::StereoMatcher> pStereo =
cv::StereoSGBM::create(0, // 最小视差
        32, // 最大视差
        5); // 块的大小
pStereo->compute(rectified1, rectified2, disparity);

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

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

相关文章

git修改已经push的commit的message

1.修改信息 2.修改message 3.强推

2026考研数学张宇武忠祥复习视频课,高数基础班+讲义PDF

2026考研数学武忠祥老师课&#xff08;网盘&#xff09;&#xff1a;点击下方链接 2026考研数学武忠祥网课&#xff08;最新网盘&#xff09; 一、基础阶段&#xff08;3-5个月&#xff09; 目标&#xff1a;搭建知识框架掌握基础题型 教材使用&#xff1a; 高数&#xff1a;…

C++使用Qt Charts可视化大规模点集

引言 数据可视化是数据分析和决策过程中的重要环节。随着数据量的不断增长&#xff0c;如何高效地可视化大规模数据集成为了一个挑战。Qt Charts 提供了一个强大的工具集&#xff0c;用于创建直观的数据可视化图表。本文将探讨如何使用 C 和 Qt Charts 可视化大规模点集&#…

质检LIMS系统在生态修复企业的实践 生态修复行业的质量管控难题

一、生态修复行业的质量管控新命题 在生态文明建设的大背景下&#xff0c;生态修复企业面临着复杂的环境治理挑战。土壤改良、水体净化、植被恢复等工程&#xff0c;均需以精准的实验数据支撑决策。传统实验室管理模式存在数据孤岛、流程非标、合规风险高等痛点&#xff0c;而…

Spring Cloud之服务入口Gateway之Route Predicate Factories

目录 Route Predicate Factories Predicate 实现Predicate接口 测试运行 Predicate的其它实现方法 匿名内部类 lambda表达式 Predicate的其它方法 源码详解 代码示例 Route Predicate Factories The After Route Predicate Factory The Before Route Predicate Fac…

《AI大模型应知应会100篇》第6篇:预训练与微调:大模型的两阶段学习方式

第6篇&#xff1a;预训练与微调&#xff1a;大模型的两阶段学习方式 摘要 近年来&#xff0c;深度学习领域的一个重要范式转变是“预训练-微调”&#xff08;Pretrain-Finetune&#xff09;的学习方式。这种两阶段方法不仅显著提升了模型性能&#xff0c;还降低了特定任务对大…

java后端对时间进行格式处理

时间格式处理 通过java后端&#xff0c;使用jackson库的注解JsonFormat(pattern "yyyy-MM-dd HH:mm:ss")进行格式化 package com.weiyu.pojo;import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Data; import …

汽车BMS技术分享及其HIL测试方案

一、BMS技术简介 在全球碳中和目标的战略驱动下&#xff0c;新能源汽车产业正以指数级速度重塑交通出行格局。动力电池作为电动汽车的"心脏"&#xff0c;其性能与安全性不仅直接决定了车辆的续航里程、使用寿命等关键指标&#xff0c;更深刻影响着消费者对电动汽车的…

【TI MSPM0】CMSIS-DSP库学习

一、什么是CMSIS-DSP库 基于Cortex微控制器软件接口标准的数字信号处理的函数库 二、页面概览 这个用户手册用来描述CMSIS-DSP软件的函数库&#xff0c;有通用的计算处理函数给Cortex-M和Cortex-A的处理器使用 三、工程学习 1.导入工程 2.样例介绍 在Q15的格式下&#xff0c…

Vue3:初识Vue,Vite服务器别名及其代理配置

一、创建一个Vue3项目 创建Vue3项目默认使用Vite作为现代的构建工具&#xff0c;以下指令本质也是通过下载create-vue来构建项目。 基于NodeJs版本大于等于18.3&#xff0c;使用命令行进行操作。 1、命令执行 npm create vuelatest输入项目名称 2、选择附加功能 选择要包含的功…

Go语言类型捕获及内存大小判断

代码如下&#xff1a; 类型捕获可使用&#xff1a;reflect.TypeOf()&#xff0c;fmt.Printf在的%T。 内存大小判断&#xff1a;len()&#xff0c;unsafe.Sizeof。 package mainimport ("fmt""unsafe""reflect" )func main(){var i , j 1, 2f…

学透Spring Boot — 017. 处理静态文件

这是我的《学透Spring Boot》专栏的第17篇文章&#xff0c;了解更多内容请移步我的专栏&#xff1a; Postnull CSDN 学透 Spring Boot 目录 静态文件 静态文件的默认位置 通过配置文件配置路径 通过代码配置路径 静态文件的自动配置 总结 静态文件 以前的传统MVC的项目…

CMake实战指南一:add_custom_command

CMake 进阶&#xff1a;add_custom_command 用法详解与实战指南 在 CMake 构建系统中&#xff0c;add_custom_command 是一个灵活且强大的工具&#xff0c;允许开发者在构建流程中插入自定义操作。无论是生成中间文件、执行预处理脚本&#xff0c;还是在目标构建前后触发额外逻…

懂x帝二手车数据爬虫-涉及简单的字体加密,爬虫中遇到“口”问题的解决

#脚本如下 import requests import pprint import timeurl https://www.dongchedi.com/motor/pc/sh/sh_sku_list?aid1839&app_nameauto_web_pc headers {User-Agent: Mozilla/5.0 }font_map {58425: 0, 58700: 1, 58467: 2, 58525: 3,58397: 4, 58385: 5, 58676: 6, 58…

4.7学习总结 java集合进阶

集合进阶 泛型 //没有泛型的时候&#xff0c;集合如何存储数据 //结论: //如果我们没有给集合指定类型&#xff0c;默认认为所有的数据类型都是object类型 //此时可以往集合添加任意的数据类型。 //带来一个坏处:我们在获取数据的时候&#xff0c;无法使用他的特有行为。 //此…

Python高阶函数-eval深入解析

1. eval() 函数概述 eval() 是 Python 内置的一个强大但需要谨慎使用的高阶函数&#xff0c;它能够将字符串作为 Python 表达式进行解析并执行。 基本语法 eval(expression, globalsNone, localsNone)expression&#xff1a;字符串形式的 Python 表达式globals&#xff1a;可…

LLM面试题八

推荐算法工程师面试题 二分类的分类损失函数&#xff1f; 二分类的分类损失函数一般采用交叉熵(Cross Entropy)损失函数&#xff0c;即CE损失函数。二分类问题的CE损失函数可以写成&#xff1a;其中&#xff0c;y是真实标签&#xff0c;p是预测标签&#xff0c;取值为0或1。 …

JavaScript双问号操作符(??)详解,解决使用 || 时因类型转换带来的问题

目录 JavaScript双问号操作符&#xff08;??&#xff09;详解&#xff0c;解决使用||时因类型转换带来的问题 一、双问号操作符??的基础用法 1、传统方式的痛点 2、双问号操作符??的精确判断 3、双问号操作符??与逻辑或操作符||的对比 二、复杂场景下的空值处理 …

蓝桥杯 web 展开你的扇子(css3)

普通答案&#xff1a; #box:hover #item1{transform: rotate(-60deg); } #box:hover #item2{transform: rotate(-50deg); } #box:hover #item3{transform: rotate(-40deg); } #box:hover #item4{transform: rotate(-30deg); } #box:hover #item5{transform: rotate(-20deg); }…

聚焦楼宇自控:优化建筑性能,引领智能化管控与舒适环境

在当今建筑行业蓬勃发展的浪潮中&#xff0c;人们对建筑的要求早已超越了传统的遮风避雨功能&#xff0c;而是更加注重建筑性能的优化、智能化的管控以及舒适环境的营造。楼宇自控系统作为现代建筑技术的核心力量&#xff0c;正凭借其卓越的功能和先进的技术&#xff0c;在这几…