本文是《视觉SLAM十四讲》第7讲的个人读书笔记,为防止后期记忆遗忘写的。
本讲关注基于特征点 方式的视觉里程计算法。我们将介绍什么是特征点,如何提取和匹配特征点,以及如何根 据配对的特征点估计相机运动。
我们希望根据匹配的点对,估计相机的运动。然而,因为相机模型的不同,我们采用的方法也是不同的。
1. 当相机为单目时,问题是根据两组 2D 点估计运动。该问题用对极几何来解决。
2. 当相机为双目、RGB-D 时,或者我们通过某种方法得到了距离信息,那问题就是根据两组 3D 点估计运动,通常用 ICP 来解决。
3. 如果我们有 3D 点和它们在相机的投影位置,也能估计相机的运动。该问题通过 PnP 求解。
7.7 3D-2D: PnP
2D-2D 的对极几何方法需要八个或八个以上的点对(以八点法为例),且存在着初始化、纯旋转 和尺度的问题。PnP描述了当我们知道 n 个 3D 空间点以及它们的投影位置时,如何估计相机所在的位姿。特征点的 3D 位置可以 由三角化,或者由 RGB-D 相机的深度图确定。因此,在双目或 RGB-D 的视觉里程计中, 我们可以直接使用 PnP 估计相机运动。而在单目视觉里程计中,必须先进行初始化,然后 才能使用 PnP。3D-2D 方法不需要使用对极约束(连续两次不同位置观察产生的约束),又可以在很少的匹配点中获得较好的运 动估计,是最重要的一种姿态估计方法。
PnP 问题有很多种求解方法,这里将介绍【直接线性变换(DLT)】和【用非线性优化的方式,构建最 小二乘问题并迭代求解,也就是万金油式的 Bundle Adjustment】两种PnP求解R和t的方法。
7.7.1 直接线性变换 DLT(Pcamera=T*Pworld=[R,t]*Pworld)
某个空间点 P(世界坐标)(已知条件1),它的齐次坐标为 P = (X, Y, Z, 1)T。投影到特征 点 x1 = (u1, v1, 1)T(归一化平面齐次坐标表示)(已知条件2)。定义增广矩阵 [R|t] 为一个 3 × 4 的矩阵。不需要使用对极约束(连续两次不同位子观察产生的约束)。
那么有:--》
定义 T 的行向量: ---》
假设一 共有 N 个特征点,可以列出线性方程组:
由于 t 一共有 12 维,因此最少通过六对匹配点,即可实现矩阵 T 的线性求解,这种方 法(也)称为直接线性变换(Direct Linear Transform,DLT)。当匹配点大于六对时,(又) 可以使用 SVD 等方法对超定方程求最小二乘解。(目标变量是t1~t12,也就是R和t)
但是,对于求出的R不一定满足SO(3)的约束。所以,寻找一个最好的旋转矩阵对它进行近似。这可以由 QR 分解完成,相当于把结果从矩阵空间重新投影到 SE(3) 流形上,转换成旋转和平移两部分。
7.7.2 P3P
P3P为了求解 PnP,利用了三角形相似性质,求解投影 点 a, b, c 在相机坐标系下的 3D 坐标,最后把问题转换成一个 3D 到 3D 的位姿估计问题,最后根据 3D-3D 的点对,计算相机的运动 R, t。它仅使用三对匹配点,对数据要求较少。
A, B, C 在世界坐标系中的坐标(已知条件1),而不是在相机坐标系中的坐标。一旦 3D 点在相机坐标系 下的坐标能够算出,我们就得到了 3D-3D 的对应点,把 PnP 问题转换为了 ICP 问题。
三角形之间存在对应关系:∆Oab − ∆OAB, ∆Obc − ∆OBC, ∆Oac − ∆OAC.利用余弦定理,有:
由于我们知道 2D 点的图像位置(已知条件2),三个余弦角 cos〈a, b〉, cos〈b, c〉, cos〈a, c〉 是已知的。同时,u = BC^2/AB^2 , w = AC^2/AB^2 可以通过 A, B, C 在世界坐标系下的坐标算出,变换到相机坐标系下之后,并不改变这个比值。该式 中的 x, y 是未知的,随着相机移动会发生变化。因此,该方程组是关于 x, y 的一个二元二 次方程(多项式方程)。解析地求解该方程组是一个复杂的过程,需要用吴消元法。最终得到 A, B, C 在相机坐标系下的 3D 坐标(目标变量),最后转换为3D-3D 的点对,转变成ICP问题,计算相机的运动 R, t。
7.7.3 Bundle Adjustment
除了使用线性方法之外,我们可以把 PnP 问题构建成一个定义于李代数上的非线性 最小二乘问题。而非线性优化则是把它们都看成优化变量,放在一起优化。我们可以用它对 PnP 或 ICP 给出的结果进行优化。优化的目的是对已经估算出的R和t以及优化特征点的空间位置进行优化、减小误差。是一个最小化重投影误差(Reprojection error)的问 题。
也就是Bundle Adjustment问题归结于要解决两个优化问题:优化已经估算出的R和t以及优化特征点的空间位置。
考虑 n 个三维空间点 P(世界坐标系)(已知条件1) 和它们的投影 p(像素坐标)(已知条件2)(R和t还未知,只有估计值),它的李代数表示为 ξ。Pi = [Xi , Yi , Zi ] T,其投影的像素坐标为 ui = [ui , vi ] T。那么有:
-------》
由于相机位姿未知以及观测点的噪声,该等式存在一个误差。我们 把误差求和,构建最小二乘问题,然后寻找最好的相机位姿,使它最小化:
(实际的观测点位置 减去 通过计算出(估计的)观测位置)
重投影误差:是将像素坐标(观测到的投影位置)与 3D 点按照当前估计的位姿 进行投影得到的位置相比较得到的误差。在初始值中,P 的投影 pˆ2 与实际的 p2 之间有一定的距离。于是我们调整相机的位姿,使得这个距离变小。不过,由 于这个调整需要考虑很多个点,所以最后每个点的误差通常都不会精确为零。
回顾前几章的基础内容,对于(第1步)优化问题,需要用李代数构建无约束的优化问题。(第2步)最小二乘方法是采用不断迭代逼近最优值的过程。而这时候,R和t正是我要优化的变量。(第3步)对于 的确定,很方便地通过 G-N, L-M 等优化算法进行求解。但是在使用 G-N 和 L-M 之前,我们需要知道每个误差项关于优化变量的导数,也就是线性化:J 的形式是值得讨论的,甚至可以说是关键所在。((第4步)回忆 G-N 和 L-M的求解过程中,实际就是需要迭代求解雅可比矩阵)。
那么现在问题就是需要求解J的形式(雅可比矩阵),也就是优化R和t目标变量。
因为对于李代数的求导,需要使用扰动模型来求李代数的导数。我们对 ξ ∧ 左乘扰动量 δξ,然后考虑 e 的变化关于扰动量的导数。
因为:
-----》 ----》
,可以把这里的 u, v 与实际的测量 值比较,求差。最终求出e。
所以第一项: 。
所以第二项: 取3维得到
所以得到2 × 6 的雅可比矩阵:
这个雅可比矩阵描述了重投影误差关于相机位姿李代数的一阶变化关系。
那么现在问题就是需要优化特征点的空间位置(P ′ =RP + t.也是有误差的)。
因为:
第一项上面已经求出
第二项:
因为----》P ′ 对 P 求导后只剩下 R。所以
我们这里的R可以利用上面优化的R值带入。
所以Bundle Adjustment优化就是优化已经估算出的R和t以及优化特征点的空间位置。利用上面两个求出的雅可比矩阵,通过G-N 和 L-M 对优化变量提供梯度方向,指导优化进行不断迭代,求出最终的最优R和t以及优化特征点的空间位置.
7.8 实践:求解 PnP
7.8.1 使用 EPnP 求解位姿
我们将使用前一步的估计值作为初始值,采用 g2o实现优化。在本例子中
1. 节点(优化的目标变量):第二个相机的位姿节点 ξ ∈ se(3),以及所有特征点的空间位置 P ∈ R 3。
2. 边(节点间的联系,误差函数):每个 3D 点在第二个相机中的投影,以观测方程来描述:
g2o 提供了许多关于 BA 的节点和边,我们不必自己从头实现所有的计算。
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <Eigen/Core>
#include <Eigen/Geometry>
#include <g2o/core/base_vertex.h>
#include <g2o/core/base_unary_edge.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/solvers/csparse/linear_solver_csparse.h>
#include <g2o/types/sba/types_six_dof_expmap.h>
#include <chrono>
using namespace std;
using namespace cv;
void find_feature_matches (
const Mat& img_1, const Mat& img_2,
std::vector<KeyPoint>& keypoints_1,
std::vector<KeyPoint>& keypoints_2,
std::vector< DMatch >& matches );
// 像素坐标转相机归一化坐标
Point2d pixel2cam ( const Point2d& p, const Mat& K );
void bundleAdjustment (
const vector<Point3f> points_3d,
const vector<Point2f> points_2d,
const Mat& K,
Mat& R, Mat& t
);
int main ( int argc, char** argv )
{
if ( argc != 5 )
{
cout<<"usage: pose_estimation_3d2d img1 img2 depth1 depth2"<<endl;
return 1;
}
//-- 读取图像
Mat img_1 = imread ( argv[1], CV_LOAD_IMAGE_COLOR );
Mat img_2 = imread ( argv[2], CV_LOAD_IMAGE_COLOR );
vector<KeyPoint> keypoints_1, keypoints_2;
vector<DMatch> matches;
find_feature_matches ( img_1, img_2, keypoints_1, keypoints_2, matches );
cout<<"一共找到了"<<matches.size() <<"组匹配点"<<endl;
// 建立3D点
Mat d1 = imread ( argv[3], CV_LOAD_IMAGE_UNCHANGED ); // 深度图为16位无符号数,单通道图像
Mat K = ( Mat_<double> ( 3,3 ) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1 );
vector<Point3f> pts_3d;
vector<Point2f> pts_2d;
for ( DMatch m:matches )
{
ushort d = d1.ptr<unsigned short> (int ( keypoints_1[m.queryIdx].pt.y )) [ int ( keypoints_1[m.queryIdx].pt.x ) ];
if ( d == 0 ) // bad depth
continue;
float dd = d/1000.0;
Point2d p1 = pixel2cam ( keypoints_1[m.queryIdx].pt, K );
pts_3d.push_back ( Point3f ( p1.x*dd, p1.y*dd, dd ) );
pts_2d.push_back ( keypoints_2[m.trainIdx].pt );
}
cout<<"3d-2d pairs: "<<pts_3d.size() <<endl;
Mat r, t;
//3D点 is 世界坐标系?(unknown 似乎不是)
solvePnP ( pts_3d, pts_2d, K, Mat(), r, t, false ); // 调用OpenCV 的 PnP 求解,可选择EPNP,DLS等方法
Mat R;
cv::Rodrigues ( r, R ); // r为旋转向量形式,用Rodrigues公式转换为矩阵
cout<<"调用OpenCV 的 PnP 求解"<<endl;
cout<<"R="<<endl<<R<<endl;
cout<<"t="<<endl<<t<<endl;
cout<<"calling bundle adjustment"<<endl;
bundleAdjustment ( pts_3d, pts_2d, K, R, t );
}
void find_feature_matches ( const Mat& img_1, const Mat& img_2,
std::vector<KeyPoint>& keypoints_1,
std::vector<KeyPoint>& keypoints_2,
std::vector< DMatch >& matches )
{
//-- 初始化
Mat descriptors_1, descriptors_2;
// used in OpenCV3
Ptr<FeatureDetector> detector = ORB::create();
Ptr<DescriptorExtractor> descriptor = ORB::create();
// use this if you are in OpenCV2
// Ptr<FeatureDetector> detector = FeatureDetector::create ( "ORB" );
// Ptr<DescriptorExtractor> descriptor = DescriptorExtractor::create ( "ORB" );
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create ( "BruteForce-Hamming" );
//-- 第一步:检测 Oriented FAST 角点位置
detector->detect ( img_1,keypoints_1 );
detector->detect ( img_2,keypoints_2 );
//-- 第二步:根据角点位置计算 BRIEF 描述子
descriptor->compute ( img_1, keypoints_1, descriptors_1 );
descriptor->compute ( img_2, keypoints_2, descriptors_2 );
//-- 第三步:对两幅图像中的BRIEF描述子进行匹配,使用 Hamming 距离
vector<DMatch> match;
// BFMatcher matcher ( NORM_HAMMING );
matcher->match ( descriptors_1, descriptors_2, match );
//-- 第四步:匹配点对筛选
double min_dist=10000, max_dist=0;
//找出所有匹配之间的最小距离和最大距离, 即是最相似的和最不相似的两组点之间的距离
for ( int i = 0; i < descriptors_1.rows; i++ )
{
double dist = match[i].distance;
if ( dist < min_dist ) min_dist = dist;
if ( dist > max_dist ) max_dist = dist;
}
printf ( "-- Max dist : %f \n", max_dist );
printf ( "-- Min dist : %f \n", min_dist );
//当描述子之间的距离大于两倍的最小距离时,即认为匹配有误.但有时候最小距离会非常小,设置一个经验值30作为下限.
for ( int i = 0; i < descriptors_1.rows; i++ )
{
if ( match[i].distance <= max ( 2*min_dist, 30.0 ) )
{
matches.push_back ( match[i] );
}
}
}
Point2d pixel2cam ( const Point2d& p, const Mat& K )
{
return Point2d
(
( p.x - K.at<double> ( 0,2 ) ) / K.at<double> ( 0,0 ),
( p.y - K.at<double> ( 1,2 ) ) / K.at<double> ( 1,1 )
);
}
void bundleAdjustment (
const vector< Point3f > points_3d, const vector< Point2f > points_2d,
const Mat& K, Mat& R, Mat& t)
{
// 初始化g2o
typedef g2o::BlockSolver< g2o::BlockSolverTraits<6,3> > Block; // pose 维度为 6, landmark 维度为 3
Block::LinearSolverType* linearSolver = new g2o::LinearSolverCSparse<Block::PoseMatrixType>(); // 线性方程求解器
Block* solver_ptr = new Block ( linearSolver ); // 矩阵块求解器
g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg ( solver_ptr );
g2o::SparseOptimizer optimizer;
optimizer.setAlgorithm ( solver );
// vertex 顶点是要优化的目标变量(这里是R,t,观测点)
g2o::VertexSE3Expmap* pose = new g2o::VertexSE3Expmap(); // camera pose
Eigen::Matrix3d R_mat;
R_mat <<
R.at<double> ( 0,0 ), R.at<double> ( 0,1 ), R.at<double> ( 0,2 ),
R.at<double> ( 1,0 ), R.at<double> ( 1,1 ), R.at<double> ( 1,2 ),
R.at<double> ( 2,0 ), R.at<double> ( 2,1 ), R.at<double> ( 2,2 );
pose->setId ( 0 );
pose->setEstimate ( g2o::SE3Quat (
R_mat,
Eigen::Vector3d ( t.at<double> ( 0,0 ), t.at<double> ( 1,0 ), t.at<double> ( 2,0 ) )
) );
optimizer.addVertex ( pose );
int index = 1;
for ( const Point3f p:points_3d ) // landmarks
{
g2o::VertexSBAPointXYZ* point = new g2o::VertexSBAPointXYZ();
point->setId ( index++ );
point->setEstimate ( Eigen::Vector3d ( p.x, p.y, p.z ) );
point->setMarginalized ( true ); // g2o 中必须设置 marg 参见第十讲内容
optimizer.addVertex ( point );
}
// parameter: camera intrinsics 内联函数
g2o::CameraParameters* camera = new g2o::CameraParameters (
K.at<double> ( 0,0 ), Eigen::Vector2d ( K.at<double> ( 0,2 ), K.at<double> ( 1,2 ) ), 0
);
camera->setId ( 0 );
optimizer.addParameter ( camera );
// edges 顶点间联系->边->误差关系
index = 1;
for ( const Point2f p:points_2d )
{
g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();
edge->setId ( index );
edge->setVertex ( 0, dynamic_cast<g2o::VertexSBAPointXYZ*> ( optimizer.vertex ( index ) ) );
edge->setVertex ( 1, pose );
edge->setMeasurement ( Eigen::Vector2d ( p.x, p.y ) );
edge->setParameterId ( 0,0 );
edge->setInformation ( Eigen::Matrix2d::Identity() );
optimizer.addEdge ( edge );
index++;
}
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
optimizer.setVerbose ( true );
optimizer.initializeOptimization();
optimizer.optimize ( 100 );
chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>> ( t2-t1 );
cout<<"optimization costs time: "<<time_used.count() <<" seconds."<<endl;
cout<<endl<<"after optimization:"<<endl;
cout<<"T="<<endl<<Eigen::Isometry3d ( pose->estimate() ).matrix() <<endl;
}
我们输出了最后得 到位姿变换矩阵 T,对比之前直接做 PnP 的结果,大约在小数点后第三位发生了一些变 化。这主要是由于我们同时优化了特征点和相机位姿导致的。
7.9 3D-3D: 迭代最近点(ICP)()
设有一组配对好的 3D 点 那么有
3D-3D 位姿估计问题中,并没有出现相机模型,也就是说,仅考虑两组 3D 点之间的变换 时,和相机并没有关系。在 RGB-D SLAM 中,可以用这种方式估计相机位姿。
和 PnP 类似,ICP 的求解也分为两种方式:利用线性代数的求解(主要是 SVD),以 及利用非线性优化方式的求解(类似于 Bundle Adjustment)。
7.9.1 SVD 方法(数学推理思路)
先定义第 i 对点的误差项:
构建最小二乘问题,而R, t的值是使误差平方和达到极小时的值:
定义两组点的质心:
经过一系列推理,得到误差函数:
左边只和旋转矩阵 R 相关,而右边既有 R 也有 t,但 只和质心相关。只要我们获得了 R,令第二项为零就能得到 t。
所以重点关注 R 的计算(其中涉及到奇异分解,回忆本质矩阵)
7.9.2 非线性优化方法
非线性优化时候,需要构建无约束关系式。以李代数表达位姿时,目标函数可以写成:
单个误差项关于位姿导数已经在前面推导过了,使用李代数扰动模型即可:。
这里,我们可以回忆PnP的非线性优化(BA)内容,知识大同小异。
ICP 问题好处
ICP 问题存在唯一解或无穷多解的情况。在唯一解的情况下,只要我们能找到极小值解,那么 这个极小值就是全局最优值——因此不会遇到局部极小而非全局最小的情况。这也意味着 ICP 求解可以任意选定初始值。这是已经匹配点时求解 ICP 的一大好处。
PnP 和 ICP混合优化
在 RGB-D SLAM 中,由于一个像素的深度数据可能测量不到,所以我们可以混合着使用 PnP 和 ICP 优化:对于深度已知的特征点,用建模它们的 3D-3D 误差;对于深度未知的特征点,则建模 3D-2D 的重投影误差。
7.10 实践:求解 ICP
7.10.1 SVD 方法
使用两 个 RGB-D 图像,通过特征匹配获取两组 3D 点,最后用 ICP 计算它们的位姿变换。它的原理也并不复杂,所以 自己实现 ICP。
void pose_estimation_3d3d (
const vector<Point3f>& pts1,
const vector<Point3f>& pts2,
Mat& R, Mat& t
)
{
Point3f p1, p2; // center of mass
int N = pts1.size();
for ( int i=0; i<N; i++ )
{
p1 += pts1[i];
p2 += pts2[i];
}
p1 = Point3f( Vec3f(p1) / N);
p2 = Point3f( Vec3f(p2) / N);
vector<Point3f> q1 ( N ), q2 ( N ); // remove the center
for ( int i=0; i<N; i++ )
{
q1[i] = pts1[i] - p1;
q2[i] = pts2[i] - p2;
}
// compute q1*q2^T
Eigen::Matrix3d W = Eigen::Matrix3d::Zero();
for ( int i=0; i<N; i++ )
{
W += Eigen::Vector3d ( q1[i].x, q1[i].y, q1[i].z ) * Eigen::Vector3d ( q2[i].x, q2[i].y, q2[i].z ).transpose();
}
cout<<"W="<<W<<endl;
// SVD on W
Eigen::JacobiSVD<Eigen::Matrix3d> svd ( W, Eigen::ComputeFullU|Eigen::ComputeFullV );
Eigen::Matrix3d U = svd.matrixU();
Eigen::Matrix3d V = svd.matrixV();
cout<<"U="<<U<<endl;
cout<<"V="<<V<<endl;
//R=U*V^T t=p-R*p'
Eigen::Matrix3d R_ = U* ( V.transpose() );
Eigen::Vector3d t_ = Eigen::Vector3d ( p1.x, p1.y, p1.z ) - R_ * Eigen::Vector3d ( p2.x, p2.y, p2.z );
// convert to cv::Mat
//here R and t is P2 change to P1 that different form the result behind.
R = ( Mat_<double> ( 3,3 ) <<
R_ ( 0,0 ), R_ ( 0,1 ), R_ ( 0,2 ),
R_ ( 1,0 ), R_ ( 1,1 ), R_ ( 1,2 ),
R_ ( 2,0 ), R_ ( 2,1 ), R_ ( 2,2 )
);
t = ( Mat_<double> ( 3,1 ) << t_ ( 0,0 ), t_ ( 1,0 ), t_ ( 2,0 ) );
}
由于前面的推导是按照 pi = Rp′ i + t 进行 的,这里的 R, t 是第二帧到第一帧的变换,与前面 PnP 部分是相反的。
7.10.2 非线性优化方法
图优化不仅考虑相机的位姿,同时会优化 3D 点的空间 位置。由于 g2o/sba 中没有提供 3D 到 3D 的边(误差),而我们又想使用 g2o/sba 中李 代数实现的位姿节点,所以最好的方式是自定义一种这样的边,并向 g2o 提供解析求导方 式(雅克比矩阵)。
边类的写法,类似于前面提到的 g2o::EdgeSE3ProjectXYZ,不过观测量从 2 维变成了 3 维,内部没有相机模型,并且只关联到一个节点。雅可比矩阵给出了关于相机位姿的导数,是一个 3 × 6 的矩阵。
边类定义
// g2o edge 边是点(优化对象)之间的联系(误差)
class EdgeProjectXYZRGBDPoseOnly : public g2o::BaseUnaryEdge<3, Eigen::Vector3d, g2o::VertexSE3Expmap>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
EdgeProjectXYZRGBDPoseOnly( const Eigen::Vector3d& point ) : _point(point) {}
//点(优化对象)之间的联系(误差)
virtual void computeError()
{
const g2o::VertexSE3Expmap* pose = static_cast<const g2o::VertexSE3Expmap*> ( _vertices[0] );
// measurement is p, point is p'
_error = _measurement - pose->estimate().map( _point );
}
virtual void linearizeOplus()
{
g2o::VertexSE3Expmap* pose = static_cast<g2o::VertexSE3Expmap *>(_vertices[0]);
g2o::SE3Quat T(pose->estimate());
Eigen::Vector3d xyz_trans = T.map(_point);
double x = xyz_trans[0];
double y = xyz_trans[1];
double z = xyz_trans[2];
//求导方式 --雅可比矩阵
_jacobianOplusXi(0,0) = 0;
_jacobianOplusXi(0,1) = -z;
_jacobianOplusXi(0,2) = y;
_jacobianOplusXi(0,3) = -1;
_jacobianOplusXi(0,4) = 0;
_jacobianOplusXi(0,5) = 0;
_jacobianOplusXi(1,0) = z;
_jacobianOplusXi(1,1) = 0;
_jacobianOplusXi(1,2) = -x;
_jacobianOplusXi(1,3) = 0;
_jacobianOplusXi(1,4) = -1;
_jacobianOplusXi(1,5) = 0;
_jacobianOplusXi(2,0) = -y;
_jacobianOplusXi(2,1) = x;
_jacobianOplusXi(2,2) = 0;
_jacobianOplusXi(2,3) = 0;
_jacobianOplusXi(2,4) = 0;
_jacobianOplusXi(2,5) = -1;
}
bool read ( istream& in ) {}
bool write ( ostream& out ) const {}
protected:
Eigen::Vector3d _point;
};
主函数以及其他代码,和之前非线性优化大同小异,这里不赘述。
非线性迭代效果如下,发现只迭代一次后,总体误差就已经稳定不变,说明仅在一次迭代之后算法即已 收敛。从位姿求解的结果可以看出,它和前面 SVD 给出的位姿结果几乎一模一样,这说 明 SVD 已经给出了优化问题的解析解(所以SVD是一种很不错的可推荐广泛运用的方法)。所以,本实验中可以认为 SVD 给出的结果是相机 位姿的最优值。