Kannala-Brandt 鱼眼相机模型

最近在学习 ORB-SLAM3 的源代码,并模仿、重构了相机模型的实现

在学习的过程中发现针孔相机 (Pinhole) 与鱼眼相机 (Fisheye) 都有畸变参数,但是鱼眼相机无法使用 cv::undistort 函数去畸变

在对鱼眼相机的深度归一化平面进行可视化后,发现鱼眼相机真的不需要去畸变

参考文献:A generic camera model and calibration method for conventional, wide-angle, and fish-eye lenses

相机基类模型

../logging.hpp 中主要调用了 glog 库,并定义了 ASSERT(expr, msg) 宏

基类 Base 初始化时需要输入 imgSize (图像尺寸)、intrinsics (相机内参)、distCoeffs (畸变参数)

#ifndef ZJSLAM__CAMERA__BASE_HPP
#define ZJSLAM__CAMERA__BASE_HPP#include <Eigen/Core>
#include <opencv2/opencv.hpp>
#include <sophus/se3.hpp>#include "../logging.hpp"namespace camera {typedef std::vector<float> Vectorf;enum CameraType {PINHOLE, FISHEYE
};class Base {protected:cv::Size mImgSize;Vectorf mvParam;cv::Mat mMap1, mMap2;   // 畸变矫正映射public:Sophus::SE3d T_cam_imu;typedef std::shared_ptr<Base> Ptr;explicit Base(const cv::Size imgSize, const Vectorf &intrinsics, const Vectorf &distCoeffs,const Sophus::SE3d &T_cam_imu = Sophus::SE3d()) : mImgSize(imgSize), mvParam(intrinsics), T_cam_imu(T_cam_imu) {ASSERT(intrinsics.size() == 4, "Intrinsics size must be 4")mvParam.insert(mvParam.end(), distCoeffs.begin(), distCoeffs.end());}virtual CameraType getType() const = 0;// 参数读取inline void setParam(int i, float value) { mvParam[i] = value; }inline float getParam(int i) const { return mvParam[i]; }inline size_t getParamSize() const { return mvParam.size(); }Vectorf getDistCoeffs() const { return {mvParam.begin() + 4, mvParam.end()}; }// 内参矩阵 K
#define GETK(vp, K) (K << vp[0], 0.f, vp[2], 0.f, vp[1], vp[3], 0.f, 0.f, 1.f)virtual cv::Mat getK() const { return GETK(mvParam, cv::Mat_<float>(3, 3)); };virtual Eigen::Matrix3f getKEig() const { return GETK(mvParam, Eigen::Matrix3f()).finished(); };// 3D -> 2Dvirtual cv::Point2f project(const cv::Point3f &p3D) const = 0;virtual Eigen::Vector2d project(const Eigen::Vector3d &v3D) const = 0;virtual Eigen::Vector2f project(const Eigen::Vector3f &v3D) const = 0;virtual Eigen::Vector2f projectEig(const cv::Point3f &p3D) const = 0;// 2D -> 3Dvirtual cv::Point3f unproject(const cv::Point2f &p2D) const = 0;virtual Eigen::Vector3f unprojectEig(const cv::Point2f &p2D) const = 0;// 去畸变virtual void undistort(const cv::Mat &src, cv::Mat &dst) = 0;// 绘制归一化平面 (z=1)void drawNormalizedPlane(const cv::Mat &src, cv::Mat &dst);
};
}#endif

鱼眼相机模型

因为在实现 C++ 的函数多态时,需要根据不同的输入值类型设计对应的计算过程 —— 但往往计算过程都是极其相似的,这给代码维护造成了麻烦

所以本文使用宏定义实现了这些计算过程

#ifndef ZJSLAM__CAMERA__KANNALA_BRANDT_HPP
#define ZJSLAM__CAMERA__KANNALA_BRANDT_HPP#include "base.hpp"namespace camera {// 最大视场角 (90)
#define KANNALA_BRANDT_MAX_FOV M_PI_2// 3D -> 2D
#define KANNALA_BRANDT_PROJECT_BY_XYZ(vp, p3D) \float R = this->computeR(atan2f(hypot(p3D.x, p3D.y), p3D.z)); \float phi = atan2f(p3D.y, p3D.x); \return {vp[0] * R * cosf(phi) + vp[2], vp[1] * R * sinf(phi) + vp[3]};#define KANNALA_BRANDT_PROJECT_BY_VEC3(vp, v3D) \float R = this->computeR(atan2f(hypot(v3D[0], v3D[1]), v3D[2])); \float phi = atan2f(v3D[1], v3D[0]); \return {vp[0] * R * cosf(phi) + vp[2], vp[1] * R * sinf(phi) + vp[3]};// 2D -> 3D
#define KANNALA_BRANDT_UNPROJECT_PRECISION 1e-6#define KANNALA_BRANDT_UNPROJECT_BY_XY(cache, p2D) \cv::Vec2f wxy = cache.at<cv::Vec2f>(p2D.y, p2D.x); \return {wxy[0], wxy[1], 1};class KannalaBrandt8 : public Base {protected:cv::Mat mUnprojectCache;void makeUnprojectCache();public:typedef std::shared_ptr<KannalaBrandt8> Ptr;explicit KannalaBrandt8(const cv::Size imgSize, const Vectorf &intrinsics, const Vectorf &distCoeffs,const Sophus::SE3d &T_cam_imu = Sophus::SE3d()) : Base(imgSize, intrinsics, distCoeffs, T_cam_imu), mUnprojectCache(mImgSize, CV_32FC2) {ASSERT(distCoeffs.size() == 4, "Distortion coefficients size must be 4")makeUnprojectCache();}CameraType getType() const override { return CameraType::FISHEYE; }// 3D -> 2Dfloat computeR(float theta) const;cv::Point2f project(const cv::Point3f &p3D) const override { KANNALA_BRANDT_PROJECT_BY_XYZ(mvParam, p3D) }Eigen::Vector2d project(const Eigen::Vector3d &v3D) const override { KANNALA_BRANDT_PROJECT_BY_VEC3(mvParam, v3D) }Eigen::Vector2f project(const Eigen::Vector3f &v3D) const override { KANNALA_BRANDT_PROJECT_BY_VEC3(mvParam, v3D) }Eigen::Vector2f projectEig(const cv::Point3f &p3D) const override { KANNALA_BRANDT_PROJECT_BY_XYZ(mvParam, p3D) }// 2D -> 3Dfloat solveWZ(float wx, float wy, size_t iterations = 10) const;cv::Point3f unproject(const cv::Point2f &p2D) const override { KANNALA_BRANDT_UNPROJECT_BY_XY(mUnprojectCache, p2D) }Eigen::Vector3f unprojectEig(const cv::Point2f &p2D) const override { KANNALA_BRANDT_UNPROJECT_BY_XY(mUnprojectCache, p2D) }// 去畸变void undistort(const cv::Mat &src, cv::Mat &dst) override { if (src.data != dst.data) dst = src.clone(); }
};
}#endif

与针孔类型相似的,鱼眼模型也有焦距 f_x, f_y,光心 c_x, c_y,以及畸变参数 k_1, k_2, k_3, k_4

借助这些参数,可以实现对世界坐标系下的点 (X_c, Y_c, Z_c)、像素坐标系下的点 (x, y) 实现相互变换

project (世界坐标 → 像素坐标)

\theta = \arctan(\frac{\sqrt{X_c^2+Y_c^2}}{Z_c}), \psi = \arctan(\frac{Y_c}{X_c})

R(\theta)=k_1 \theta + k_2 \theta^3 + k_3 \theta^5 + k_4 \theta^7

x = f_x R \cos(\psi) + c_x, y = f_y R \sin(\psi) + c_y

float KannalaBrandt8::computeR(float theta) const {float theta2 = theta * theta;return theta + theta2 * (mvParam[4] + theta2 * (mvParam[5] + theta2 * (mvParam[6] + theta2 * mvParam[7])));
}

unproject (像素坐标 → 世界坐标)

根据 project 的过程,可以由像素坐标计算得到 R(\theta),并反向求得 \theta

X_c= \frac{x - c_x}{f_x}, Y_c = \frac{y - c_y}{f_y}

R(X_c, Y_c) = \sqrt{X_c^2 + Y_c^2 } \in [0, \infty)

由于 \theta 的取值是有上限的 (假设为 \frac{\pi}{2}),也就是说 R_{max} = R(\frac{\pi}{2})

所以当 R(X_c, Y_c) > R_{max} 时,应当检查相机内参是否出错

使用梯度下降法使得 l(\theta)=(R(\theta) - R(X_c, Y_c))^2=0,以求解 \theta

由于 l(\theta) 是一个凹函数,所以只要保证迭代量正负号正确即可

当求得 \theta 时,便可以得到 Z_c

Z_c =R(X_c, Y_c) / \tan(\theta)

而由于单目相机的深度没有什么意义,把 (X_c / Z_c, Y_c / Z_c, 1) 作为对应的世界坐标

(这里使用缓存的方式实现 unproject)

void KannalaBrandt8::makeUnprojectCache() {float wx, wy, wz;for (int r = 0; r < mImgSize.height; ++r) {wy = (r - mvParam[3]) / mvParam[1];for (int c = 0; c < mImgSize.width; ++c) {wx = (c - mvParam[2]) / mvParam[0];wz = this->solveWZ(wx, wy);mUnprojectCache.at<cv::Vec2f>(r, c) = {wx / wz, wy / wz};}}
}float KannalaBrandt8::solveWZ(float wx, float wy, size_t iterations) const {// wz = lim_{theta -> 0} R / tan(theta) = 1float wz = 1.f;float R = hypot(wx, wy);float maxR = this->computeR(KANNALA_BRANDT_MAX_FOV);if (R > KANNALA_BRANDT_UNPROJECT_PRECISION) {float theta = KANNALA_BRANDT_MAX_FOV;if (R < maxR) {// 最小化损失: (poly(theta) - R)^2int i = 0;float e;for (; i < iterations; i++) {float theta2 = theta * theta, theta4 = theta2 * theta2, theta6 = theta4 * theta2, theta8 = theta6 * theta2;float k0_theta2 = mvParam[4] * theta2, k1_theta4 = mvParam[5] * theta4,k2_theta6 = mvParam[6] * theta6, k3_theta8 = mvParam[7] * theta8;e = theta * (1 + k0_theta2 + k1_theta4 + k2_theta6 + k3_theta8) - R;if (abs(e) < R * KANNALA_BRANDT_UNPROJECT_PRECISION) break;// 梯度下降法: g = (poly(theta) - R) / poly'(theta)theta -= e / (1 + 3 * k0_theta2 + 5 * k1_theta4 + 7 * k2_theta6 + 9 * k3_theta8);}if (i == iterations) LOG(WARNING) << "solveWZ(" << wx << ", " << wy << "): relative error " << abs(e) / R;}wz = R / tanf(theta);}return wz;
}

绘制深度归一化平面

深度归一化平面,即世界坐标点在 Z_c = 1 平面上的投影,也就是一幅图像

基本思路就是,通过 unproject 获取深度归一化平面的边界,然后通过 project 获取平面上各个点对应图像中的位置

void Base::drawNormalizedPlane(const cv::Mat &src, cv::Mat &dst) {undistort(src, dst);cv::Mat npMap1 = cv::Mat(mImgSize, CV_32FC1), npMap2 = npMap1.clone();// 获取归一化平面边界 (桶形畸变)float x, y, w, h, W = mImgSize.width - 1, H = mImgSize.height - 1;x = this->unproject({0, H / 2}).x, y = this->unproject({W / 2, 0}).y,w = this->unproject({W, H / 2}).x - x, h = this->unproject({W / 2, H}).y - y;LOG(INFO) << "Normalized plane: " << cv::Vec4f(x, y, x + w, y + h);// 计算畸变矫正映射for (int r = 0; r < H; ++r) {for (int c = 0; c < W; ++c) {cv::Point2f p2D = this->project(cv::Point3f(w * c / W + x, h * r / H + y, 1));npMap1.at<float>(r, c) = p2D.x;npMap2.at<float>(r, c) = p2D.y;}}cv::remap(dst, dst, npMap1, npMap2, cv::INTER_LINEAR);
}

本文使用了 TUM-VI 数据集进行实验,Kannala-Brandt 相机的参数如下:

resolution: [512, 512]

intrinsics: [190.97847715128717, 190.9733070521226, 254.93170605935475, 256.8974428996504]

dist_coeffs: [0.0034823894022493434, 0.0007150348452162257, -0.0020532361418706202, 0.00020293673591811182]

(下面这段代码用了我自己写的其它东西,仅作参考)

void fisheye_test() {// 加载 TUM-VI 数据集 相机参数dataset::TumVI tumvi("/home/workbench/data/dataset-corridor4_512_16/dso");YAML::Node cfg = tumvi.loadCfg();auto cam(camera::fromYAML<camera::KannalaBrandt8>(cfg["cam0"]));// 加载图像列表, 读取第一张图像GrayLoader loader;dataset::Timestamps vTimestamps;dataset::Filenames vFilename;tumvi.loadImage(vTimestamps, vFilename);cv::Mat img = loader(vFilename[0]), dst1;// 显示原始图像, 以及去畸变后的图像cv::imshow("Origin", img);cam->drawNormalizedPlane(img, img);cv::imshow("NormalizedPlane", img);cv::waitKey(0);
}

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

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

相关文章

SQL 注入神器:SQLMap 简单使用

前言 SQLMap 是一款用于自动化 SQL 注入检测与渗透测试的开源工具&#xff0c;其主要功能是检测和利用 Web 应用程序中的 SQL 注入漏洞。以下是 SQLMap 的主要特点和功能&#xff1a; 自动化检测&#xff1a;SQLMap 可以自动发现 Web 应用程序中的 SQL 注入漏洞&#xff0c;包…

QT5之windowswidget_菜单栏+工具栏_核心控件_浮动窗口_模态对话框_标准对话框/文本对话框

菜单栏工具栏 新建工程基类是QMainWindow 1、 2、 3、 点.pro文件&#xff0c;添加配置 因为之后用到lambda&#xff1b; 在.pro文件添加配置c11 CONFIG c11 #不能加分号 添加头文件 #include <QMenuBar>//菜单栏的头文件 主窗口代码mainwindow.cpp文件 #include &q…

Hive大数据任务调度和业务介绍

目录 一、Zookeeper 1.zookeeper介绍 2.数据模型 3.操作使用 4.运行机制 5.一致性 二、Dolphinscheduler 1.Dolphinscheduler介绍 架构 2.架构说明 该服务内主要包含: 该服务包含&#xff1a; 3.FinalShell主虚拟机启动服务 4.Web网页登录 5.使用 5-1 安全中心…

局域网唤醒平台:UpSnap

简介&#xff1a;UpSnap是一个简单的唤醒局域网网络应用程序。UpSnap为每个用户、每个设备提供了唯一的访问权限。虽然管理员拥有所有权限&#xff0c;但他们可以为用户分配特定的权限&#xff0c;如显示/隐藏设备、访问设备编辑、删除和打开/关闭设备电源。 历史攻略&#xf…

给Ollama套个WebUI,方便使用

Ollama 基本的安装使用参考前文 https://xugaoxiang.com/2024/05/01/ollama-offline-deploy/&#xff0c;前文使用的模型是 llama2&#xff0c;本篇将使用 llama3&#xff0c;因此在启动时&#xff0c;命令是 ollama run llama3。 Ollama Llama3 Llama3 是 Meta 发布的大语言模…

Optional学习记录

Optional出现的意义 在Java中&#xff0c;我们经常遇到的一种异常情况&#xff1a;空指针异常&#xff0c;在原本的编程中&#xff0c;为了避免这种异常&#xff0c;我们通常会向对象进行判断&#xff0c;然而&#xff0c;过多的判断语句会让我们的代码显得臃肿不堪。 所以在J…

用LangChain打造一个可以管理日程的智能助手

存储设计定义工具创建llm提示词模板创建Agent执行总结 众所周知&#xff0c;GPT可以认为是一个离线的软件的&#xff0c;对于一些实时性有要求的功能是完全不行&#xff0c;比如实时信息检索&#xff0c;再比如我们今天要实现个一个日程管理的功能&#xff0c;这个功能你纯依赖…

拼多多关键词怎么推广

拼多多上的关键词推广可以通过以下步骤进行&#xff1a; 拼多多推广可以使用3an推客。3an推客&#xff08;CPS模式&#xff09;给商家提供的营销工具&#xff0c;由商家自主设置佣金比例&#xff0c;激励推广者去帮助商家推广商品链接&#xff0c;按最终有效交易金额支付佣金…

定子的检查和包扎及转子的检查

线圈接好后 用摇表测试 线圈和外壳之间的绝缘性&#xff01; 测试通过后进行焊接&#xff01;&#xff0c;焊接的工具在后面的文章中会介绍&#xff01; 焊接好后&#xff0c;包绝缘管。 焊接完成后 进行星型连接&#xff0c;或者三角形连接&#xff01; 白扎带进行绑扎&…

室外巡检机器人——A2型高防护轮式巡检机器人

在科技日新月异的时代&#xff0c;室外巡检机器人犹如一位无畏的守护者&#xff0c;悄然出现在我们的视野之中。它迈着坚定的步伐&#xff0c;穿梭于各种复杂的室外环境&#xff0c;承担着重要的巡检任务。它是科技与智慧的结晶&#xff0c;是保障安全与稳定的前沿力量。让我们…

【Python基础】进程

文章目录 [toc]程序与进程的区别与联系同步任务示例 并行任务示例进程调度的“随机性” 进程属性与方法process_object.start()方法process_object.join()方法process_object.daemon属性没有设置守护进程的情况设置守护进程的情况 process_object.current_process()方法 进程通…

数仓开发:DIM层数据处理

一、了解DIM层 这个就是数仓开发的分层架构 我们现在是在DIM层&#xff0c;从ods表中数据进行加工处理&#xff0c;导入到dwd层&#xff0c;但是记住我们依然是在DIM层&#xff0c;而非是上面的ODS和DWD层。 二、处理维度表数据 ①先确认hive的配置 -- 开启动态分区方案 -- …

c++:优先级队列(priority queue)使用及底层详解,附带仿函数初步使用

文章目录 优先级队列的使用大堆小堆**注意** 优先级队列的模拟实现pushpopsizeemptytop 仿函数仿函数是什么pushpop 仿函数结合优先级队列的优势 优先级队列的使用 优先级队列本质是就是完全二叉树,是个堆.我们可以用优先级队列来取出一段序列中的前N个最大值. priority_queue…

Postman的一些使用技巧

Postman 是一个流行的 API 开发工具&#xff0c;用于设计、开发、测试、发布和监控 API。在现代web开发中使用非常广泛。后端开发必备而且必会的工具。 目录 1.配置环境变量 2.动态变量 3.脚本 4.测试 5.模拟 6.监控 7.集合运行器 8.响应保存 9.请求历史 10.同步请求…

【论文阅读笔记】关于“二进制函数相似性检测”的调研(Security 22)

个人博客链接 注&#xff1a;部分内容参考自GPT生成的内容 [Security 22] 关于”二进制函数相似性检测“的调研&#xff08;个人阅读笔记&#xff09; 论文&#xff1a;《How Machine Learning Is Solving the Binary Function Similarity Problem》&#xff08;Usenix Securi…

面试算法-链表-反转链表(golang、c++)

目录 1、题目 2、解题思路 2.1 遍历、迭代 2.2 递归 3、源代码 3.1 c 3.2 golang 4、复杂度分析 4.1 遍历、迭代法 4.2 迭代法 1、题目 链表是一种常用的数据结构&#xff0c;链表的特点是插入、删除节点的效率非常高&#xff0c;因为他不需要移动其他任何元素&…

Linux——守护进程化(独立于用户会话的进程)

目录 前言 一、进程组ID与会话ID 二、setsid() 创建新会话 三、daemon 守护进程 前言 在之前&#xff0c;我们学习过socket编程中的udp通信与tcp通信&#xff0c;但是当时我们服务器启动的时候&#xff0c;都是以前台进程的方式启动的&#xff0c;这样很不优雅&#xff0c…

数据分析:基于DESeq2的转录组功能富集分析

介绍 DESeq2常用于识别差异基因&#xff0c;它主要使用了标准化因子标准化数据&#xff0c;再根据广义线性模型判别组间差异&#xff08;组间残差是否显著判断&#xff09;。在获取差异基因结果后&#xff0c;我们可以进行下一步的富集分析&#xff0c;常用方法有基于在线网站…

银行智能化数据安全分类分级实践分享

文章目录 前言一、数据安全智能分类分级平台建设背景二、数据安全分类分级建设思路和实践1、做标签– 数据安全标签体系2、打标签– 鹰眼智能打标平台 3.03、用标签– 全行统一“数据安全打标签结果”服务提供前言 随着国家对数据安全的高度重视,以及相关法律法规的出台,数据…

python数据分析中数据可视化简单入门

1.折线图表 首先引入相关包pyecharts&#xff0c;如果没下载可以先下载 pip install pyecharts from pyecharts.charts import Lineline Line() # 添加x轴 line.add_xaxis([呱了个呱,羊村,牟多,蜂地,喵帕斯]) # 添加y轴 line.add_yaxis("GDP",[50,30,40,34,63,22])…