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

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

        在上述公式中,可以了解到,真实物体转为平面之后,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、Tsflag); // 设置选项
}
//根据经验,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.mzph.cn/diannao/77076.shtml

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

相关文章

大厂不再招测试?软件测试左移开发合理吗?

&#x1f449;目录 1 软件测试发展史 2 测试左移&#xff08;Testing shift left&#xff09; 3 测试右移&#xff08;Testing shift right&#xff09; 4 自动化测试 VS 测试自动化 5 来自 EX 测试的寄语 最近两年&#xff0c;互联网大厂的招聘中&#xff0c;测试工程师岗位似…

windows10下PointNet官方代码Pytorch实现

PointNet模型运行 1.下载源码并安装环境 GitCode - 全球开发者的开源社区,开源代码托管平台GitCode是面向全球开发者的开源社区,包括原创博客,开源代码托管,代码协作,项目管理等。与开发者社区互动,提升您的研发效率和质量。https://gitcode.com/gh_mirrors/po/pointnet.pyto…

git pull 和 git fetch

关于 git pull 和 git fetch 的区别 1. git fetch 作用&#xff1a;从远程仓库获取最新的分支信息和提交记录&#xff0c;但不会自动合并或修改当前工作目录中的内容。特点&#xff1a; 它只是更新本地的远程分支引用&#xff08;例如 remotes/origin/suyuhan&#xff09;&am…

前端开发中的单引号(‘ ‘)、双引号( )和反引号( `)使用

前端开发中的单引号&#xff08;’ &#xff09;、双引号&#xff08;" "&#xff09;和反引号&#xff08; &#xff09;使用 在前端开发中&#xff0c;单引号&#xff08;’ &#xff09;、双引号&#xff08;" "&#xff09;和反引号&#xff08; &…

程序化广告行业(69/89):DMP与PCP系统核心功能剖析

程序化广告行业&#xff08;69/89&#xff09;&#xff1a;DMP与PCP系统核心功能剖析 在数字化营销浪潮中&#xff0c;程序化广告已成为企业精准触达目标受众的关键手段。作为行业探索者&#xff0c;我深知其中知识的繁杂与重要性。一直以来&#xff0c;都希望能和大家一同学习…

Amodal3R ,南洋理工推出的 3D 生成模型

Amodal3R 是一款先进的条件式 3D 生成模型&#xff0c;能够从部分可见的 2D 物体图像中推断并重建完整的 3D 结构与外观。该模型建立在基础的 3D 生成模型 TRELLIS 之上&#xff0c;通过引入掩码加权多头交叉注意力机制与遮挡感知注意力层&#xff0c;利用遮挡先验知识优化重建…

LLM面试题八

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

30天学Java第7天——IO流

概述 基本概念 输入流&#xff1a;从硬盘到内存。&#xff08;输入又叫做 读 read&#xff09;输出流&#xff1a;从内存到硬盘。&#xff08;输出又叫做 写 write&#xff09;字节流&#xff1a;一次读取一个字节。适合非文本数据&#xff0c;它是万能的&#xff0c;啥都能读…

面试可能会遇到的问题回答(嵌入式软件开发部分)

写在前面&#xff1a; 博主也是刚入社会的小牛马&#xff0c;如果下面有写的不好或者写错的地方欢迎大家指出~ 一、四大件基础知识 1、计算机组成原理 &#xff08;1&#xff09;简单介绍一下中断是什么。 ①回答&#xff1a; ②难度系数&#xff1a;★★ ③难点分析&…

层归一化详解及在 Stable Diffusion 中的应用分析

在深度学习中&#xff0c;归一化&#xff08;Normalization&#xff09;技术被广泛用于提升模型训练的稳定性和收敛速度。本文将详细介绍几种常见的归一化方式&#xff0c;并重点分析它们在 Stable Diffusion 模型中的实际使用场景。 一、常见的归一化技术 名称归一化维度应用…

深入理解Socket编程:构建简单的计算器服务器

一、Socket通信基础 1. Socket通信基本流程 服务器端流程&#xff1a; 创建Socket (socket()) 绑定地址和端口 (bind()) 监听连接 (listen()) 接受连接 (accept()) 数据通信 (read()/write()) 关闭连接 (close()) 客户端流程&#xff1a; 创建Socket (socket()) 连接…

Redis-x64-3.2.100.msi : Windows 安装包(MSI 格式)安装步骤

Redis-x64-3.2.100.msi 是 Redis 的 Windows 安装包&#xff08;MSI 格式&#xff09;&#xff0c;适用于 64 位系统。 在由于一些环境需要低版本的Redis的安装包。 Redis-x64-3.2.100.msi 安装包下载&#xff1a;https://pan.quark.cn/s/cc4d38262a15 Redis 是一个开源的 内…

4.7正则表达式

1.字符匹配 一般字符匹配自身. 匹配任意字符(换行符\n除外),一个点占一位\转义字符&#xff0c;使其后一个字符改变原来的意思(\.就是.)[......]字符集,对应的位置可以是字符集中的任意字符.字符集中的字符可以逐个列出,也可以给出范围如[abc]或[a-c] [^abc] 表示取反&#xf…

Fortran 中读取 MATLAB 生成的数据文件

在 Fortran 中读取 MATLAB 生成的数据文件&#xff0c;可以通过以下几种方法实现&#xff0c;包括使用开源工具和手动解析&#xff1a; 1. 使用开源工具&#xff1a;MATFOR MATFOR 是一个商业/开源混合工具&#xff08;部分功能免费&#xff09;&#xff0c;提供 Fortran 与 M…

压测工具开发实战篇(四)——client子窗口功能

你好&#xff0c;我是安然无虞。 文章目录 树控件添加文件补充学习: 函数定义中循环体里的局部变量补充学习: 动态添加对象属性 刷新文件上下文菜单 (右键菜单)实现右键菜单功能 编辑节点文本 在学习本篇文章之前, 建议先看一下上篇介绍MDI子窗口的文章: 压测工具开发实战篇(三…

PyTorch使用(4)-张量拼接操作

文章目录 张量拼接操作1. torch.cat 函数的使用1.1. torch.cat 定义1.2. 语法1.3. 关键规则 1.4. 示例代码1.4.1. 沿行拼接&#xff08;dim0&#xff09;1.4.2. 沿列拼接&#xff08;dim1&#xff09;1.4.3. 高维拼接&#xff08;dim2&#xff09; 1.5. 错误场景分析1.5.1. 维度…

linux命令之yes(Linux Command Yes)

linux命令之yes 简介与功能 yes 命令在 Linux 系统中用于重复输出一行字符串&#xff0c;直到被杀死&#xff08;kill&#xff09;。该命令最常见的用途是自动化控制脚本中的交互式命令&#xff0c;以便无需用户介入即可进行连续的确认操作。 用法示例 基本用法非常简单&am…

《算法笔记》10.3小节——图算法专题->图的遍历 问题 B: 连通图

题目描述 给定一个无向图和其中的所有边&#xff0c;判断这个图是否所有顶点都是连通的。 输入 每组数据的第一行是两个整数 n 和 m&#xff08;0<n<1000&#xff09;。n 表示图的顶点数目&#xff0c;m 表示图中边的数目。如果 n 为 0 表示输入结束。随后有 m 行数据…

使用Prometheus监控systemd服务并可视化

实训背景 你是一家企业的运维工程师&#xff0c;需将服务器的systemd服务监控集成到Prometheus&#xff0c;并通过Grafana展示实时数据。需求如下&#xff1a; 数据采集&#xff1a;监控所有systemd服务的状态&#xff08;运行/停止&#xff09;、资源占用&#xff08;CPU、内…

OpenCV--图像边缘检测

在计算机视觉和图像处理领域&#xff0c;边缘检测是极为关键的技术。边缘作为图像中像素值发生急剧变化的区域&#xff0c;承载了图像的重要结构信息&#xff0c;在物体识别、图像分割、目标跟踪等众多应用场景中发挥着核心作用。OpenCV 作为强大的计算机视觉库&#xff0c;提供…