视觉SLAM位姿估计(总结)

2024-05-31 22:32
文章标签 总结 slam 视觉 位姿 估计

本文主要是介绍视觉SLAM位姿估计(总结),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Overview

欢迎访问 持续更新:https://cgabc.xyz/posts/102a40b/

相关代码:

  • pose_estimation in cggos/slam_park_cg

Features Based Method

2D-2D: Epipolar Geometry

2D-2D 对极几何 主要涉及到基础矩阵、本质矩阵和单应性矩阵的求解,并从中恢复出旋转矩阵 R R R 和平移向量 t t t

  • 计算机视觉对极几何之FEH

同时,还要根据匹配的特征点和计算出的相对位姿进行三角化,恢复出 3D空间点

  • 计算机视觉对极几何之Triangulate(三角化)

在单目视觉SLAM中,以上过程主要用于SLAM的初始化:计算第二关键帧的相对位姿(假设第一帧位姿为 [ I 0 ] [I \quad 0] [I0]),并计算初始Map。

OpenCV 中相关函数:

  • findFundamentalMat
  • findEssentialMat
  • findHomography
  • recoverPose
  • decomposeEssentialMat
  • triangulatePoints

相关参考代码:

vector<Point2f> pts1;
vector<Point2f> pts2;
for ( int i = 0; i < ( int ) matches.size(); i++ ) {pts1.push_back(keypoints_1[matches[i].queryIdx].pt);pts2.push_back(keypoints_2[matches[i].trainIdx].pt);
}Mat matF = findFundamentalMat(pts1, pts2, CV_FM_8POINT);double cx = K.at<double>(0,2);
double cy = K.at<double>(1,2);
double fx = K.at<double>(0,0);
Mat matE = findEssentialMat(pts1, pts2, fx, Point2d(cx,cy));Mat matH = findHomography(pts1, pts2, RANSAC, 3);recoverPose(matE, pts1, pts2, R, t, fx, Point2d(cx,cy));

3D-2D: PnP

Perspective-n-Point is the problem of estimating the pose of a calibrated camera given a set of n 3D points in the world and their corresponding 2D projections in the image. [Wikipedia]

PnP(Perspective-n-Point) 是求解3D到2D点对运动的方法,求解PnP问题目前主要有直接线性变换(DLT)、P3P、EPnP、UPnP以及非线性优化方法。

在双目或RGB-D的视觉里程计中,可以直接使用PnP估计相机运动;而在单目视觉里程计中,必须先进行初始化,然后才能使用PnP。

在SLAM中,通常先使用 P3P或EPnP 等方法估计相机位姿,再构建最小二乘优化问题对估计值进行调整(BA)。

Bundle Adjustment

把 PnP问题 构建成一个定义于李代数上的非线性最小二乘问题,求解最好的相机位姿。

定义 残差(观测值-预测值)或 重投影误差

r ( ξ ) = u − K exp ⁡ ( ξ ∧ ) P r(\xi) = u - K \exp({\xi}^{\wedge}) P r(ξ)=uKexp(ξ)P

构建最小二乘问题
ξ ∗ = arg ⁡ min ⁡ ξ 1 2 ∑ i = 1 n ∥ r ( ξ ) ∥ 2 2 {\xi}^* = \arg \min_\xi \frac{1}{2} \sum_{i=1}^{n} {\| r(\xi) \| }_2^2 ξ=argξmin21i=1nr(ξ)22

细节可参考 应用: 基于李代数的视觉SLAM位姿优化

OpenCV 中相关函数:

  • solvePnP
  • Rodrigues

相关参考代码:

vector<KeyPoint> kpts_1, kpts_2;
vector<DMatch> matches;
find_feature_matches ( img_1, img_2, kpts_1, kpts_2, matches );vector<Point3f> pts_3d;
vector<Point2f> pts_2d;
for ( DMatch m : matches ) {int x1 = int(kpts_1[m.queryIdx].pt.x);int y1 = int(kpts_1[m.queryIdx].pt.y);ushort d = img_d.ptr<unsigned short>(y1)[x1];if (d == 0)   // bad depthcontinue;float dd = d / depth_scale;Point2d p1 = pixel2cam(kpts_1[m.queryIdx].pt, K);pts_3d.push_back(Point3f(p1.x * dd, p1.y * dd, dd));pts_2d.push_back(kpts_2[m.trainIdx].pt);
}Mat om, t;
solvePnP ( pts_3d, pts_2d, K, Mat(), om, t, false, SOLVEPNP_EPNP );
Mat R;
cv::Rodrigues ( om, R ); // rotation vector to rotation matrixbundle_adjustment_3d2d ( pts_3d, pts_2d, K, R, t );

3D-3D: ICP

对于3D-3D的位姿估计问题可以用 ICP(Iterative Closest Point) 求解,其求解方式分为两种:

  • 线性代数方式(主要是 SVD)
  • 非线性优化方式(类似于BA)

线性代数方式

根据ICP问题,建立第 i i i 对点的误差项

e i = P i − ( R ⋅ P ′ i + t ) e_i = P_i - (R \cdot {P'}_i + t) ei=Pi(RPi+t)

构建最小二乘问题,求使误差平方和达到极小的 R , t R, t R,t

min ⁡ R , t J = 1 2 ∑ i = 1 n ∥ e i ∥ 2 2 \min_{R,t} J = \frac{1}{2} \sum_{i=1}^{n} {\| e_i \|}_2^2 R,tminJ=21i=1nei22

对目标函数处理,最终为

min ⁡ R , t J = 1 2 ∑ i = 1 n ( ∥ P i − P c − R ( P ′ i − P ′ c ) ∥ 2 + ∥ P c − R P ′ c − t ∥ 2 ) \min_{R,t} J = \frac{1}{2} \sum_{i=1}^{n} ( {\| P_i - P_c - R({P'}_i-{P'}_c) \|}^2 + {\| P_c - R{P'}_c - t \|}^2 ) R,tminJ=21i=1n(PiPcR(PiPc)2+PcRPct2)

根据上式,可以先求解 R R R,再求解 t t t

  1. 计算两组点的质心 P c P_c Pc P ′ c {P'}_c Pc
  2. 计算每个点的去质心坐标 Q i = P i − P c Q_i = P_i -P_c Qi=PiPc Q ′ i = P ′ i − P ′ c {Q'}_i = {P'}_i - {P'}_c Qi=PiPc
  3. 定义矩阵 W = ∑ i = 1 n Q i Q ′ i T W = \sum_{i=1}^{n} Q_i {Q'}_i^T W=i=1nQiQiT,再SVD分解 W = U Σ V T W = U {\Sigma} V^T W=UΣVT
  4. W W W 满秩时, R = U V T R = UV^T \quad R=UVT
  5. 计算 t = P c − R ⋅ P ′ c t = P_c - R \cdot {P'}_c t=PcRPc

相关参考代码:

cv::Point3f p1, p2;
int N = pts1.size();
for (int i = 0; i < N; i++) {p1 += pts1[i];p2 += pts2[i];
}
p1 /= N;
p2 /= N;std::vector<cv::Point3f> q1(N), q2(N);
for (int i = 0; i < N; i++) {q1[i] = pts1[i] - p1;q2[i] = pts2[i] - p2;
}Eigen::Matrix3d W = Eigen::Matrix3d::Zero();
for (int i = 0; i < N; i++) {Eigen::Vector3d v3q1 = Eigen::Vector3d(q1[i].x, q1[i].y, q1[i].z);Eigen::Vector3d v3q2 = Eigen::Vector3d(q2[i].x, q2[i].y, q2[i].z);W += v3q1 * v3q2.transpose();
}double determinantW =W(0,0)*W(1,1)*W(2,2) + W(0,1)*W(1,2)*W(2,0) + W(0,2)*W(1,0)*W(2,1) -
(W(0,0)*W(1,2)*W(2,1) + W(0,1)*W(1,0)*W(2,2) + W(0,2)*W(1,1)*W(2,0));assert(determinantW>1e-8);// SVD on W
Eigen::JacobiSVD<Eigen::Matrix3d> svd(W, Eigen::ComputeFullU | Eigen::ComputeFullV);
Eigen::Matrix3d U = svd.matrixU();
Eigen::Matrix3d V = svd.matrixV();R = U * (V.transpose());
t = Eigen::Vector3d(p1.x, p1.y, p1.z) - R * Eigen::Vector3d(p2.x, p2.y, p2.z);

Optical Flow

光流是一种描述像素随时间在图像之间运动的方法,计算部分像素的称为 稀疏光流,计算所有像素的称为 稠密光流。稀疏光流以 Lucas-Kanade光流 为代表,并可以在SLAM中用于跟踪特征点位置。

LK光流

灰度不变假设

I ( x + d x , y + d y , t + d t ) = I ( x , y , t ) I(x+dx, y+dy, t+dt) = I(x, y, t) I(x+dx,y+dy,t+dt)=I(x,y,t)

对左边一阶泰勒展开

I ( x + d x , y + d y , t + d t ) = I ( x , y , t ) + ∂ I ∂ x d x + ∂ I ∂ y d y + ∂ I ∂ t d t I(x+dx, y+dy, t+dt) = I(x, y, t) + \frac{\partial I}{\partial x} dx + \frac{\partial I}{\partial y} dy + \frac{\partial I}{\partial t} dt I(x+dx,y+dy,t+dt)=I(x,y,t)+xIdx+yIdy+tIdt

∂ I ∂ x d x + ∂ I ∂ y d y + ∂ I ∂ t d t = 0 \frac{\partial I}{\partial x} dx + \frac{\partial I}{\partial y} dy + \frac{\partial I}{\partial t} dt = 0 xIdx+yIdy+tIdt=0

整理得

∂ I ∂ x d x d t + ∂ I ∂ y d y d t = − ∂ I d t \frac{\partial I}{\partial x} \frac{dx}{dt} + \frac{\partial I}{\partial y} \frac{dy}{dt} = - \frac{\partial I}{dt} xIdtdx+yIdtdy=dtI

简写为

I x u + I y v = − I t I_x u + I_y v = -I_t Ixu+Iyv=It

[ I x I y ] [ u v ] = − I t \begin{bmatrix} I_x & I_y \end{bmatrix} \begin{bmatrix} u \\ v \end{bmatrix} = -I_t [IxIy][uv]=It

在LK光流中,假设某个窗口(w x w)内的像素具有相同的运动

[ I x I y ] k [ u v ] = − I t k , k = 1 , … , w 2 {\begin{bmatrix} I_x & I_y \end{bmatrix}}_k \begin{bmatrix} u \\ v \end{bmatrix} = -{I_t}_k, \quad k=1, \dots, w^2 [IxIy]k[uv]=Itk,k=1,,w2

简写为

A [ u v ] = − b A \begin{bmatrix} u \\ v \end{bmatrix} = -b A[uv]=b

从而得到图像间的运动速度或者某块像素的位置。

分类

增量方式ForwardInverse
AdditiveFAIAIAFA
CompositionalFCIAICIA

Forward-Additive 光流:

min ⁡ Δ x i , Δ y i ∑ W ∥ I 1 ( x i , y i ) − I 2 ( x i + Δ x i , y i + Δ y i ) ∥ 2 2 \min_{\Delta x_i, \Delta y_i} \sum_{W} {\| I_1(x_i,y_i) - I_2(x_i+\Delta x_i, y_i+\Delta y_i) \|}_2^2 Δxi,ΔyiminWI1(xi,yi)I2(xi+Δxi,yi+Δyi)22

在迭代开始时,Gauss-Newton 的计算依赖于 I 2 I_2 I2 ( x i , y i ) (x_i, y_i) (xi,yi) 处的梯度信息。然而,角点提取算法仅保证了 I 1 ( x i , y i ) I_1(x_i,y_i) I1(xi,yi) 处是角点(可以认为角度点存在明显梯度),但对于 I 2 I_2 I2,我们并没有办法假设 I 2 I_2 I2 ( x i , y i ) (x_i,y_i) (xi,yi) 处亦有梯度,从而 Gauss-Newton 并不一定成立。

反向的光流法(inverse) 则做了一个巧妙的技巧,即用 I 1 ( x i , y i ) I_1(x_i,y_i) I1(xi,yi) 处的梯度,替换掉原本要计算的 I 2 ( x i + Δ x i , y i + Δ y i ) I_2(x_i+\Delta x_i, y_i+\Delta y_i) I2(xi+Δxi,yi+Δyi) 的梯度。 I 1 ( x i , y i ) I_1(x_i,y_i) I1(xi,yi) 处的梯度不随迭代改变,所以只需计算一次,就可以在后续的迭代中一直使用,节省了大量计算时间。

讨论

  • 在光流中,关键点的坐标值通常是浮点数,但图像数据都是以整数作为下标的。在光流中,通常的优化值都在几个像素内变化,所以用浮点数的像素插值(双线性插值)。
  • 光流法通常只能估计几个像素内的误差。如果初始估计不够好,或者图像运动太大,光流法就无法得到有效的估计(不像特征点匹配那样)。但是,使用图像金字塔,可以让光流对图像运动不那么敏感
  • 多层金字塔光流

OpenCV 中相关函数:

  • calcOpticalFlowPyrLK

相关参考代码:

for (int index = 0; index < count; index++) {color = colorImgs[index];depth = depthImgs[index];if (index == 0) {vector<cv::KeyPoint> kps;cv::Ptr<cv::FastFeatureDetector> detector =cv::FastFeatureDetector::create();detector->detect(color, kps);for (auto kp:kps)keypoints.push_back(kp.pt);last_color = color;continue;}if (color.data == nullptr || depth.data == nullptr)continue;vector<cv::Point2f> next_keypoints;vector<cv::Point2f> prev_keypoints;for (auto kp:keypoints)prev_keypoints.push_back(kp);vector<unsigned char> status;vector<float> error;cv::calcOpticalFlowPyrLK(last_color, color, prev_keypoints, next_keypoints, status, error);int i = 0;for (auto iter = keypoints.begin(); iter != keypoints.end(); i++) {if (status[i] == 0) {iter = keypoints.erase(iter);continue;}* iter = next_keypoints[i];iter++;}last_color = color;
}

Direct Method

根据使用像素的数量,直接法分为以下三种

  • 稀疏直接法:使用稀疏关键点,不计算描述子
  • 半稠密直接法:只使用带有梯度的像素点,舍弃像素梯度不明显的地方
  • 稠密直接法:使用所有像素

利用直接法计算相机位姿,建立优化问题时,最小化的是 光度误差(Photometric Error)

r ( ξ ) = I 1 ( p ) − I 2 ( p ′ ) = I 1 ( p ) − I 2 ( K exp ⁡ ( ξ ∧ ) P ) r(\xi) = I_1(p) - I_2(p') = I_1(p) - I_2( K \exp({\xi}^{\wedge}) P ) r(ξ)=I1(p)I2(p)=I1(p)I2(Kexp(ξ)P)

构建最小二乘问题
ξ ∗ = arg ⁡ min ⁡ ξ 1 2 ∑ i = 1 n ∥ r ( ξ ) ∥ 2 2 {\xi}^* = \arg \min_\xi \frac{1}{2} \sum_{i=1}^{n} {\| r(\xi) \| }_2^2 ξ=argξmin21i=1nr(ξ)22

更具体地,稀疏直接法

ξ ∗ = arg ⁡ min ⁡ ξ 1 N ∑ i = 1 N ∑ W i ∥ I 1 ( p i ) − I 2 ( π ( exp ⁡ ( ξ ∧ ) π − 1 ( p i ) ) ) ∥ 2 2 {\xi}^* = \arg \min_\xi \frac{1}{N} \sum_{i=1}^N \sum_{W_i} {\| I_1(p_i) - I_2 (\pi(\exp({\xi}^{\wedge}) \pi_{-1}(p_i))) \|}_2^2 ξ=argξminN1i=1NWiI1(pi)I2(π(exp(ξ)π1(pi)))22

其雅克比矩阵的计算,可参见 视觉SLAM位姿优化时误差函数雅克比矩阵的计算

相关参考代码:

bool pose_estimation_direct(const vector<Measurement>& measurements,cv::Mat* gray, Eigen::Matrix3f& K, Eigen::Isometry3d& Tcw) {typedef g2o::BlockSolver<g2o::BlockSolverTraits<6, 1>> DirectBlock;DirectBlock::LinearSolverType * linearSolver =new g2o::LinearSolverDense<DirectBlock::PoseMatrixType>();DirectBlock * solver_ptr = new DirectBlock(linearSolver);g2o::OptimizationAlgorithmLevenberg * solver =new g2o::OptimizationAlgorithmLevenberg(solver_ptr);g2o::SparseOptimizer optimizer;optimizer.setAlgorithm(solver);optimizer.setVerbose(true);g2o::VertexSE3Expmap * pose = new g2o::VertexSE3Expmap();pose->setEstimate(g2o::SE3Quat(Tcw.rotation(), Tcw.translation()));pose->setId(0);optimizer.addVertex(pose);int id = 1;for (Measurement m: measurements) {EdgeSE3ProjectDirect * edge =new EdgeSE3ProjectDirect(m.pos_world, K(0, 0), K(1, 1), K(0, 2), K(1, 2), gray);edge->setVertex(0, pose);edge->setMeasurement(m.grayscale);edge->setInformation(Eigen::Matrix<double, 1, 1>::Identity());edge->setId(id++);optimizer.addEdge(edge);}optimizer.initializeOptimization();optimizer.optimize(30);Tcw = pose->estimate();
}

参考文献

  • 《视觉SLAM十四讲》
  • Lucas-Kanade 20 Years On: A Unifying Framework
  • 相机位姿求解问题?(知乎)

这篇关于视觉SLAM位姿估计(总结)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1019222

相关文章

Python中实现进度条的多种方法总结

《Python中实现进度条的多种方法总结》在Python编程中,进度条是一个非常有用的功能,它能让用户直观地了解任务的进度,提升用户体验,本文将介绍几种在Python中实现进度条的常用方法,并通过代码... 目录一、简单的打印方式二、使用tqdm库三、使用alive-progress库四、使用progres

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

Java向kettle8.0传递参数的方式总结

《Java向kettle8.0传递参数的方式总结》介绍了如何在Kettle中传递参数到转换和作业中,包括设置全局properties、使用TransMeta和JobMeta的parameterValu... 目录1.传递参数到转换中2.传递参数到作业中总结1.传递参数到转换中1.1. 通过设置Trans的

C# Task Cancellation使用总结

《C#TaskCancellation使用总结》本文主要介绍了在使用CancellationTokenSource取消任务时的行为,以及如何使用Task的ContinueWith方法来处理任务的延... 目录C# Task Cancellation总结1、调用cancellationTokenSource.

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

git使用的说明总结

Git使用说明 下载安装(下载地址) macOS: Git - Downloading macOS Windows: Git - Downloading Windows Linux/Unix: Git (git-scm.com) 创建新仓库 本地创建新仓库:创建新文件夹,进入文件夹目录,执行指令 git init ,用以创建新的git 克隆仓库 执行指令用以创建一个本地仓库的

二分最大匹配总结

HDU 2444  黑白染色 ,二分图判定 const int maxn = 208 ;vector<int> g[maxn] ;int n ;bool vis[maxn] ;int match[maxn] ;;int color[maxn] ;int setcolor(int u , int c){color[u] = c ;for(vector<int>::iter

整数Hash散列总结

方法:    step1  :线性探测  step2 散列   当 h(k)位置已经存储有元素的时候,依次探查(h(k)+i) mod S, i=1,2,3…,直到找到空的存储单元为止。其中,S为 数组长度。 HDU 1496   a*x1^2+b*x2^2+c*x3^2+d*x4^2=0 。 x在 [-100,100] 解的个数  const int MaxN = 3000