# 使用非线性优化方法求解ICP
# 前情提要
在迭代最近点算法ICP及SVD求解 (opens new window)中介绍了ICP
问题及使用SVD
分解求解ICP
的方法。除了SVD
,还可以使用非线性优化的方法来求解ICP
。
# ICP问题回顾
还记得,ICP
优化的目标函数为:
我们知道旋转和平移可以写成齐次变换的形式,
ICP
优化的目标函数可以写成:
这里要优化的变量是
# 对矩阵变量求导数
根据《视觉SLAM十四讲》第4讲的介绍,
李群局部正切空间上的表示是李代数,李代数相对于李群,类似于导数对函数的意义。
其中,
在
矩阵的指数运算显然没有定义,如何计算呢?这里可以用泰勒公式,
将
当
当在李群中完成两个矩阵的乘法时,也就是依次进行两个变换时,李代数上对应会发生怎样的变化呢?
两个李代数指数映射乘积的完整形式,由Baker-Campbell-Hausdorff(BCH)
公式给出,
[,]
是李括号,表示二元运算。
以旋转矩阵组成的特殊正交群
考虑
上面公式中第一种相当于是对原旋转矩阵左乘一个微小变换矩阵,第二种相当于是对原旋转矩阵右乘一个微小变换矩阵。乘以微小变换矩阵相当于是发生了微小的位移和姿态变化。
左乘微小变换矩阵的近似雅可比矩阵
上面公式中,如果左乘的微小旋转矩阵对应的李代数带雅可比矩阵,则上式就变为(以左乘为例):
由上,李代数上的加法,可近似为李群上带左或右雅可比的乘法:
因为旋转矩阵组成的李群各个变换之间只定义了有意义的乘法而没有定义有意义的加法,因此不能直接在李群上定义导数。
而考虑李群与李代数之间的关系,以旋转矩阵为例对
由于
上面推导过程中1到2用到了BCH
近似公式,2到3用到了泰勒公式,4到5用到了反对称矩阵的性质
通过转换成李代数求导得到的最终的导数包含一个
该式的求导可写成,
相比于对李代数直接求导,省去了一个雅可比
# ICP问题的非线性解法
考虑前面ICP
优化的目标函数:
单个误差项关于位姿的导数,使用李代数的扰动模型可得,
对于n
对三维空间中的点
写成矩阵的形式为:
上式隐含了一次齐次坐标往三维坐标的转换。
对于n
对空间点和对应的图像中投影的观测点,要估计相机的位姿,目标函数就是最小化两者之间的误差,
记通过相机外参变换到相机坐标系下
根据相机模型关于
展开有:
重投影误差的左乘扰动求导数为:
根据目标函数和相机模型的公式有:
而第二项为变换后的点关于李代数的导数,
根据
最后将两者相乘可以求得
# 代码示例
使用G2O
求解非线性优化问题,节点就是待求解变量,边是观测数据点。
前面推导的是相机模型的点与三维空间点的PnP
问题的求解,对于ICP
直接是成对三维点之间的优化,相对于PnP
更简单。
定义G2O
的顶点和边,
class VertexPose : public g2o::BaseVertex<6, Sophus::SE3d> {
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
// 设置初始化的更新值
virtual void setToOriginImpl() override {
_estimate = Sophus::SE3d();
}
// left multiplication on SE3
virtual void oplusImpl(const double *update) {
Eigen::Matrix<double, 6, 1> update_eigen;
update_eigen << update[0], update[1], update[2],
update[3], update[4], update[5];
_estimate = Sophus::SE3d::exp(update_eigen) * _estimate;
}
virtual bool read(std::istream &in) override {return true;}
virtual bool write(std::ostream &out) const override { return true;}
};
class EdgeProjectXYZRGBDPoseOnly : public g2o::BaseUnaryEdge<3, Eigen::Vector3d, bcv::VertexPose>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
EdgeProjectXYZRGBDPoseOnly(const Eigen::Vector3d &point) : _point(point) {}
virtual void computeError() override {
const VertexPose* p = static_cast<const VertexPose*> (_vertices[0]);
_error = _measurement - p->estimate() * _point;
}
virtual void linearizeOplus() override {
VertexPose *p = static_cast<VertexPose*> (_vertices[0]);
Sophus::SE3d T = p->estimate();
Eigen::Vector3d xyz_trans = T * _point;
_jacobianOplusXi.block<3, 3>(0, 0) = -Eigen::Matrix3d::Identity();
_jacobianOplusXi.block<3, 3>(0, 3) = Sophus::SO3d::hat(xyz_trans);
}
bool read(std::istream &in) { return true; }
bool write(std::ostream &out) const { return true; }
protected:
Eigen::Vector3d _point;
};
定义求解器:
void ICPSolver::NLOSolver(std::vector<cv::Point3f> &pts1,
std::vector<cv::Point3f> &pts2,
cv::Mat &R, cv::Mat &t)
{
typedef g2o::BlockSolverX BlockSolverType;
typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType;
auto solver = new g2o::OptimizationAlgorithmGaussNewton(
g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>())
);
g2o::SparseOptimizer optimizer; // graph model
optimizer.setAlgorithm(solver); // set solver
optimizer.setVerbose(true); // print info
bcv::VertexPose *p = new VertexPose();
p->setId(0);
p->setEstimate(Sophus::SE3d());
optimizer.addVertex(p);
// add edge
for(size_t i = 0; i < pts1.size(); i++) {
bcv::EdgeProjectXYZRGBDPoseOnly *e = new bcv::EdgeProjectXYZRGBDPoseOnly(
Eigen::Vector3d(pts2[i].x, pts2[i].y, pts2[i].z)
);
e->setVertex(0, p);
e->setMeasurement(Eigen::Vector3d(pts1[i].x, pts1[i].y, pts1[i].z));
e->setInformation(Eigen::Matrix3d::Identity());
optimizer.addEdge(e);
}
auto t1 = std::chrono::system_clock::now();
optimizer.initializeOptimization();
optimizer.optimize(10);
auto t2 = std::chrono::system_clock::now();
auto d = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
std::cout << "duration: " << d << " ms" << std::endl;
std::cout << "after optim:\n";
std::cout << "T=\n" << p->estimate().matrix() << std::endl;
Eigen::Matrix3d R_ = p->estimate().rotationMatrix();
Eigen::Vector3d t_ = p->estimate().translation();
std::cout <<"det(R_)=" << R_.determinant() << std::endl;
std::cout <<"R_R_^T=" << R_ * R_.transpose() << std::endl;
std::cout << "R:\n" << R_ << std::endl;
std::cout << "t:\n" << t_ << std::endl;
R = (cv::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 = (cv::Mat_<double>(3, 1) << t_(0, 0), t_(1, 0), t_(2, 0));
}
完整可运行代码见https://gitee.com/lx_r/basic_cplusplus_examples(opens new window)