OpenCV实战(24)——相机姿态估计

news2024/12/29 10:57:21

OpenCV实战(24)——相机姿态估计

    • 0. 前言
    • 1. 相机姿态估计
    • 2. 3D 可视化模块 cv::Viz
    • 3. 完整代码
    • 小结
    • 系列链接

0. 前言

校准相机后,就可以将捕获的图像与物理世界联系起来。如果物体的 3D 结构是已知的,那么就可以预测物体如何投影到相机的传感器上,图像形成的过程由投影方程描述。当方程的大部分项已知时,就可以通过观察一些图像来推断其他元素 (2D3D) 的值。相机姿态估计就是通过几个已知坐标的特征点,以及这些点在照片中的成像位置,求解出相机位于坐标系内的坐标与旋转角度。在本节中,我们将研究观察已知 3D 结构时的相机姿态估计问题。

1. 相机姿态估计

我们考虑一个简单的对象——公园里的长凳,使用我们在上一节中校准的相机和镜头系统拍摄了以下图像,并在长凳上手动识别了 8 个不同的图像点,用于相机姿态估计 (camera pose estimation):

带有标记点的图像
通过访问该对象,可以进行许多 3D 物理测量。长凳由一个 242.5cmx53.5cmx9cm 的座椅和一个 242.5cmx24cmx9cm 的靠背组成,靠背固定在座椅上方 12cm 处。使用此信息,我们可以轻松推导出以对象为中心的参考系中 8 个识别点的 3D 坐标(将原点固定在两个平面之间的交点的左端)。

(1) 在算法中要做的第一件事是创建一个包含以上对象坐标的 cv::Point3f 向量:

// 输入图像点
std::vector<cv::Point2f> imagePoints;
imagePoints.push_back(cv::Point2f(207, 60));
imagePoints.push_back(cv::Point2f(670, 103));
imagePoints.push_back(cv::Point2f(637, 240));
imagePoints.push_back(cv::Point2f(183, 156));
imagePoints.push_back(cv::Point2f(185, 179));
imagePoints.push_back(cv::Point2f(633, 273));
imagePoints.push_back(cv::Point2f(541, 332));
imagePoints.push_back(cv::Point2f(73, 213));

(2) 现在的问题是在拍摄以上照片时,相机相对于这些点的位置。由于这些已知点在 2D 图像平面上的图像坐标也是已知的,那么使用 cv::solvePnP 函数就很容易估计相机位置。这里,我们手动建立了 3D2D 点之间的对应关系,但是我们也可以自动获取此信息:

// 输入对象点
std::vector<cv::Point3f> objectPoints;
objectPoints.push_back(cv::Point3f(0, 45, 0));
objectPoints.push_back(cv::Point3f(242.5, 45, 0));
objectPoints.push_back(cv::Point3f(242.5, 21, 0));
objectPoints.push_back(cv::Point3f(0, 21, 0));
objectPoints.push_back(cv::Point3f(0, 9, -9));
objectPoints.push_back(cv::Point3f(242.5, 9, -9));
objectPoints.push_back(cv::Point3f(242.5, 9, 44.5));
objectPoints.push_back(cv::Point3f(0, 9, 44.5));
// 根据 3D/2D 点计算相机姿态
cv::Mat rvec, tvec;
cv::solvePnP(objectPoints, imagePoints,         // 对应的 3D/2D 点
                cameraMatrix, cameraDistCoeffs, // 标定
                rvec, tvec);                    // 输出姿态
cv::Mat rotation;
// 将向量 rvec 转换为 3x3 旋转矩阵 
cv::Rodrigues(rvec, rotation);

cv::solvePnP 函数计算刚性变换(即旋转和平移分量),将对象坐标带入以相机为中心的参考系(即以焦点为原点的参考系),此函数计算的旋转是以 3D 矢量的形式给出的。其中要应用的旋转由单位向量(即旋转轴)描述,对象围绕该单位向量旋转某个角度,这种表示方式也称为罗德里格斯旋转公式 (Rodrigues' rotation formula)。在 OpenCV 中,旋转角度对应于输出旋转向量的范数,后者与旋转轴对齐。可以使用 cv::Rodrigues 函数获得出现在投影方程中的 3D 旋转矩阵。

(3) 相机姿势恢复过程十分简单,为了得知我们是否获得了正确的相机和物体姿势信息,可以使用 cv::viz 模块直观地评估结果,cv::viz 模块能够可视化 3D 信息。利用该模块显示对象和相机的简单 3D 表示:

3D 表示
仅通过查看此图像可能难以判断姿势恢复的质量,但可以选择使用鼠标在 3D 中移动观察,以更好地了解所得到的解。
在本节中,我们假设对象的 3D 结构、对象点集和图像点集之间的对应关系是已知的,相机的内在参数也可以通过校准得知(即第一个矩阵的元素);根据投影方程,这意味着有一些点的坐标是已知的,例如 ( X , Y , Z ) (X, Y, Z) (X,Y,Z) ( x , y ) (x, y) (x,y)。只有第二个矩阵未知,这是一个包含相机的外在参数(即相机/物体姿态信息)的矩阵。因此,我们的目标是从 3D 场景点的观察中恢复这些未知参数。此问题称为 PnP (Perspective-n-Point) 问题。
旋转具有三个自由度(可以围绕三个轴旋转),平移同样也是如此,因此,共有六个未知数。对于每个物点和像点的对应关系,投影方程给出三个代数方程,但由于射影方程有一个比例因子,因此只有两个独立的方程。因此,求解该方程组至少需要三个点,更多的点可以得到更可靠的估计。
OpenCVcv::solvePnP 函数中实现了多种不同算法。默认方法基于优化重投影误差,最大限度地减少此类误差是从相机图像中获取准确 3D 信息的最佳策略。在 PnP 问题中,对应于找到最佳相机位置,以最小化投影 3D 点(通过应用投影方程获得)和输入观察图像点之间的 2D 距离。
OpenCV 还提供了 cv::solvePnPRansac 函数使用随机抽样一致算法 (RANdom SAmple Consensus, RANSAC) 来解决 PnP 问题。这意味着某些对象点和图像点的对应关系可能是错误的,该函数返回被识别为异常值的对应点,这对于自动获取的对应关系非常有用。

2. 3D 可视化模块 cv::Viz

在处理 3D 信息时,通常很难验证获得的解。为此,OpenCV 提供了一个功能强大的可视化模块,以便于 3D 视觉算法的开发和调试。它允许在虚拟 3D 环境中插入点、线、相机和其他对象,还可以从不同的角度进行交互可视化。
cv::VizOpenCV 库的一个模块,它构建在可视化工具包 (Visualization Toolkit, VTK) 之上,是一个用于 3D 计算机图形的强大框架。使用 cv::viz,可以创建一个 3D 虚拟环境,可以在其中添加各种对象。创建一个可视化窗口,从给定的角度显示环境。在本节中,学习如何在 cv::viz 窗口中显示内容,此窗口内可以响应鼠标事件(通过旋转和平移),本节将介绍 cv::viz 模块的基本用法。

(1) 首先要创建可视化窗口,例如,使用白色背景:

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

(2) 接下来,创建虚拟对象并将它们插入到场景中。OpenCV 中包含多种预定义对象,其中包含用于创建虚拟针孔相机的工具:

// 创建虚拟相机
cv::viz::WCameraPosition cam(cMatrix,   // 内在矩阵 
                        image,          // 图像平面
                        30.0,           // 缩放因子
                        cv::viz::Color::black());
visualizer.showWidget("Camera", cam);

(3) cMatrix 变量是 cv::Matx33d 实例,即 cv::Matx<double,3,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);
// 在环境中添加虚拟对象
visualizer.showWidget("top", plane1);
visualizer.showWidget("bottom", plane2);

(4) 虚拟长凳也是在原点添加的,然后将它移动到以相机为中心(利用 cv::solvePnP 函数)的位置。setWidgetPose 方法可以用于添加操作,该函数应用了估计运动的旋转和平移分量:

cv::Mat rotation;
// 将向量 rvec 转换为 3x3 旋转矩阵 
cv::Rodrigues(rvec, rotation);
// 移动长凳
cv::Affine3d pose(rotation, tvec);
visualizer.setWidgetPose("top", pose);
visualizer.setWidgetPose("bottom", pose);

(5) 最后创建一个不断显示可视化窗口的循环,并在循环中进行 1ms 暂停以侦听鼠标事件:

// 可视化虚拟环境
while(cv::waitKey(100)==-1 && !visualizer.wasStopped())
{
    visualizer.spinOnce(1,      // 暂停 1ms 
                        true);  // 重绘
}

当可视化窗口关闭或在 OpenCV 图像窗口上按下某个按键时,循环将停止。在此循环中将运动应用于对象就可以创建动画。

3. 完整代码

完整代码 cameraPose.cpp 如下所示:

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/viz.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <iostream>

int main() {
    // 读取相机标定参数
    cv::Mat cameraMatrix;
    cv::Mat cameraDistCoeffs;
    cv::FileStorage fs("calib.xml", cv::FileStorage::READ);
    fs["Intrinsic"] >> cameraMatrix;
    fs["Distortion"] >> cameraDistCoeffs;
    std::cout << " Camera intrinsic: " << cameraMatrix.rows << "x" << cameraMatrix.cols << std::endl;
    std::cout << cameraMatrix.at<double>(0, 0) << " " << cameraMatrix.at<double>(0, 1) << " " << cameraMatrix.at<double>(0, 2) << std::endl;
    std::cout << cameraMatrix.at<double>(1, 0) << " " << cameraMatrix.at<double>(1, 1) << " " << cameraMatrix.at<double>(1, 2) << std::endl;
    std::cout << cameraMatrix.at<double>(2, 0) << " " << cameraMatrix.at<double>(2, 1) << " " << cameraMatrix.at<double>(2, 2) << std::endl << std::endl;
    cv::Matx33d cMatrix(cameraMatrix);
    // 输入图像点
    std::vector<cv::Point2f> imagePoints;
    imagePoints.push_back(cv::Point2f(207, 60));
    imagePoints.push_back(cv::Point2f(670, 103));
    imagePoints.push_back(cv::Point2f(637, 240));
    imagePoints.push_back(cv::Point2f(183, 156));
    imagePoints.push_back(cv::Point2f(185, 179));
    imagePoints.push_back(cv::Point2f(633, 273));
    imagePoints.push_back(cv::Point2f(541, 332));
    imagePoints.push_back(cv::Point2f(73, 213));
    // 输入对象点
    std::vector<cv::Point3f> objectPoints;
    objectPoints.push_back(cv::Point3f(0, 45, 0));
    objectPoints.push_back(cv::Point3f(242.5, 45, 0));
    objectPoints.push_back(cv::Point3f(242.5, 21, 0));
    objectPoints.push_back(cv::Point3f(0, 21, 0));
    objectPoints.push_back(cv::Point3f(0, 9, -9));
    objectPoints.push_back(cv::Point3f(242.5, 9, -9));
    objectPoints.push_back(cv::Point3f(242.5, 9, 44.5));
    objectPoints.push_back(cv::Point3f(0, 9, 44.5));
    // 读取图像
    cv::Mat image = cv::imread("example_1.jpg");
    // 绘制图像点
    for (int i = 0; i < 8; i++) {
        cv::circle(image, imagePoints[i], 3, cv::Scalar(0, 0, 0),2);
    }
    cv::imshow("An image of a bench", image);
    // 创建 viz 窗口
    cv::viz::Viz3d visualizer("Viz window");
    visualizer.setBackgroundColor(cv::viz::Color::white());
    // 创建虚拟相机
    cv::viz::WCameraPosition cam(cMatrix,   // 内在矩阵 
                            image,          // 图像平面
                            30.0,           // 缩放因子
                            cv::viz::Color::black());
    // 使用长方体创建虚拟长凳
    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);
    // 在环境中添加虚拟对象
    visualizer.showWidget("top", plane1);
    visualizer.showWidget("bottom", plane2);
    visualizer.showWidget("Camera", cam);
    // 根据 3D/2D 点计算相机姿态
    cv::Mat rvec, tvec;
    cv::solvePnP(objectPoints, imagePoints,         // 对应的 3D/2D 点
                    cameraMatrix, cameraDistCoeffs, // 标定
                    rvec, tvec);                    // 输出姿态
    std::cout << " rvec: " << rvec.rows << "x" << rvec.cols << std::endl;
    std::cout << " tvec: " << tvec.rows << "x" << tvec.cols << std::endl;
    cv::Mat rotation;
    // 将向量 rvec 转换为 3x3 旋转矩阵 
    cv::Rodrigues(rvec, rotation);
    // 移动长凳
    cv::Affine3d pose(rotation, tvec);
    visualizer.setWidgetPose("top", pose);
    visualizer.setWidgetPose("bottom", pose);
    // 可视化虚拟环境
    while(cv::waitKey(100)==-1 && !visualizer.wasStopped())
    {
        visualizer.spinOnce(1,      // 暂停 1ms 
                            true);  // 重绘
    }
    cv::waitKey();
    return 0;
}

小结

相机位姿估计就是通过几个已知坐标的特征点,以及这些点在照片中的成像位置,求解出相机位于坐标系内的坐标与旋转角度,本节使用 cv::solvePnP 函数估计相机位置,并介绍了如何在 OpenCV 中可视化 3D 场景。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定

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

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

相关文章

【Shell脚本】Linux安装Nexus的两种方式以及开机自启

目录 一、Linux安装Nexus的两种方式1、直接把下载好的安装包上传到服务器①、打开Nexus页面后&#xff0c;登录时会出现以下提示&#xff0c;根据路径提示可找到初始密码②、找到初始登录Nexus的初始密码 2、通过wget安装Nexus①、修改Nexus端口号②、默认的端口号为8081&#…

华为OD机试真题 Java 实现【找数字】【2023Q2 100分】

一、题目描述 给一个二维数组nums&#xff0c;对于每一个元素nums[i]&#xff0c;找出距离最近的且值相等的元素&#xff0c;输出横纵坐标差值的绝对值之和&#xff0c;如果没有等值元素&#xff0c;则输出-1。 例如 输入数组nums为 0 3 5 4 2 2 5 7 8 3 2 5 4 2 4 对于 n…

Java每日一练(20230517) 重复元素、链表重复元素、旋转数组

目录 1. 存在重复元素 &#x1f31f; 2. 删除排序链表中的重复元素 &#x1f31f; 3. 旋转数组 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 存在重…

让孩子们零基础也能学习人工智能,这家科技企业是这样做的

在偏远地区的孩子&#xff0c;即便没有任何人工智能知识和理论基础&#xff0c;也可以一步步迈入人工智能科技的殿堂&#xff1f; 你没有看错&#xff0c;这就是亚马逊云科技推出的“AI在未来”公益计划项目&#xff0c;如今已经进入了第二个学年。 “AI在未来”公益计划走进宁…

案例分享|地弹现象导致DCDC电源芯片工作不正常

很多读者都应该听过地弹&#xff0c;但是实际遇到的地弹的问题应该很少。本案例就是一个DCDC电源芯片的案例。 1. 问题描述 如下图1 &#xff0c;产品其中一个供电是12V转3.3V的电路&#xff0c;产品发货50K左右以后&#xff0c;大约有1%的产品无法启动&#xff0c;经过解耦定…

【Java8新特性】史上最全Optional实战教程,太厉害了!

目录 一、前置基础 二、什么是Optional 2.1理论拓展 三、为什么要用Optional 3.1俄罗斯式套娃判空详解 四、Optional基本知识 4.1API的思考 五、工作中如何正确使用Optional 5.1 orElseThrow 5.2 filter 5.3 orElse和orElseGet 5.4 map和flatMap 5.5 项目实战 实战…

16位单片机去哪儿了?

关注星标公众号&#xff0c;不错过精彩内容 作者 | strongerHuang 微信公众号 | strongerHuang 最近网友问了一个问题&#xff1a;为什么现在很少看见16位单片机了&#xff1f; 你是不是也有这样的疑问&#xff1a;现在市面上大多都是32位Arm Coretx-M内核的单片机&#xff0c;…

〖技术人职业规划白宝书 - 职业规划篇①〗- 大学生选择职业前的自我认知与剖析

历时18个月&#xff0c;采访 850 得到的需求。 不管你是在校大学生、研究生、还是在职的小伙伴&#xff0c;该专栏有你想要的职业规划、简历、面试的答案。说明&#xff1a;该文属于 技术人职业规划白宝书 专栏&#xff0c;购买任意白宝书体系化专栏可加入TFS-CLUB 私域社区&am…

【JS】1684- 重学 JavaScript API - Resize Observer API

❝ 前期回顾&#xff1a; 1. Page Visibility API 2. Broadcast Channel API 3. Beacon API ❞ &#x1f3dd; 什么是 Resize Observer API Resize Observer API[1] 可以帮助我们监听元素尺寸的变化&#xff0c;并在尺寸变化时执行一些操作。例如&#xff0c;我们可以使用 Resi…

突发!骨灰级程序员心梗离世!

大家注意&#xff1a;因为微信最近又改了推送机制&#xff0c;经常有小伙伴说错过了之前被删的文章&#xff0c;比如前阵子冒着风险写的爬虫&#xff0c;再比如一些限时福利&#xff0c;错过了就是错过了。 所以建议大家加个星标&#xff0c;就能第一时间收到推送。&#x1f44…

什么是鉴权?这些postman鉴权方式你又知道多少?

一、什么是鉴权&#xff1f; 鉴权也就是身份认证&#xff0c;就是验证您是否有权限从服务器访问或操作相关数据。发送请求时&#xff0c;通常必须包含相应的检验参数以确保请求具有访问权限并返回所需数据。通俗的讲就是一个门禁&#xff0c;您想要进入室内&#xff0c;必须通过…

PostgreSQL 为什么PG 的适用性很强(译)

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

ChatGPT常用提问技巧

上篇文章《ChatGPT万能提问技巧》中提到的万能提问技巧-RPEP提问法&#xff0c;家人们用ChatGPT能够轻松应对大部分的对话场景&#xff0c;获得更加出色的回答了吧&#xff01;今天&#xff0c;我们在提供两种常用的提问模式&#xff0c;让家人们使用ChatGPT都很轻松获得需要的…

Hive on Spark调优(大数据技术3)

3.1 Executor配置说明 3.1.1 Executor CPU核数配置 单个Executor的CPU核数&#xff0c;由spark.executor.cores参数决定&#xff0c;建议配置为4-6&#xff0c;具体配置为多少&#xff0c;视具体情况而定&#xff0c;原则是尽量充分利用资源。 此处单个节点共有16个核可供Exec…

时尚达人的深度学习:非洲服装检测

介绍 时尚在医疗、教育和农业等不同领域的人工智能领域并没有受到太多关注&#xff0c;包括机器学习、深度学习。这是因为时尚不被认为是一个关键领域. 时尚和文化在 AI 中并没有得到公正的对待。这是一个令人兴奋的项目&#xff0c;我们将在 8 种当地非洲服装之间建立一个分类…

科大讯飞版ChatGPT,厉害了!

前几天科大讯飞的星火认知大模型发布了&#xff0c;我刚好有朋友在科大讯飞工作&#xff0c;于是就第一时间体验了一波。 一番体验下来确实比我预想的效果要好&#xff0c;没想到国产模型的效果还不错&#xff0c;我试了很多方面&#xff0c;比如通用常识功能、写作功能、学习能…

2023 开源之夏来啦!报名 MegEngine 项目,赢取万元奖金!

“开源软件供应链点亮计划-暑期2023”是一项面向高校学生的暑期活动&#xff0c;为高校学生提供了绝佳的、友好开放的交流平台。使学生可以真正投身于开源软件的开发维护&#xff0c;得到资深开源软件开发者指导的机会&#xff0c;获得丰硕的活动奖金&#xff0c;并帮助学生获取…

【送书】前端系列14集-Vue3-setup

送书活动&#xff1a;挑选1名粉丝同学哦 免费包邮送。截止时间&#xff1a;2023/5/18号 19 : 00参与底部评论区说说&#xff1a;请在评论中分享你的阅读收获。 前端工程化&#xff1a;基于Vue.js 3.0的设计与实践实战 页面浏览量&#xff08;Page View&#xff0c;PV&#xff…

【004】C++数据类型之字符类型(char)详解

C数据类型之字符类型详解 引言一、背景知识二、字符常量三、字符变量四、键盘&#xff08;输入设备&#xff09;给字符变量赋值五、字符常量和字符串常量的区别六、案例&#xff1a;字符大小写转换总结 引言 &#x1f4a1; 作者简介&#xff1a;专注于C/C高性能程序设计和开发&…

知识变现海哥|为什么你无法将学到的知识变现

秋叶大叔在公众号里发了这样一篇文章《为什么今天的年轻人更要读大学&#xff1f;》&#xff0c;文章里提到这样一个观点&#xff1a;一个人的专业能力要获得优质的回报&#xff0c;还需要学一样「变现技能」。 我不由得想问一个问题&#xff1a;你真的理解变现能力吗&#xf…