# 使用非线性优化方法求解ICP

# 前情提要

迭代最近点算法ICP及SVD求解 (opens new window)中介绍了ICP问题及使用SVD分解求解ICP的方法。除了SVD,还可以使用非线性优化的方法来求解ICP

# ICP问题回顾

还记得,ICP优化的目标函数为:

minR,t12in||pi(Rqi+t)||22

我们知道旋转和平移可以写成齐次变换的形式,

T=[Rt00]

ICP优化的目标函数可以写成:

minT12in||piTqi||22

这里要优化的变量是,而是矩阵?如何对矩阵求导数呢?需要使用李群与李代数的概念。

# 对矩阵变量求导数

根据《视觉SLAM十四讲》第4讲的介绍,属于特殊欧式群具有连续性质,是李群。

李群局部正切空间上的表示是李代数,李代数相对于李群,类似于导数对函数的意义。

对应的李代数为:

se(3)={ξ=[ρϕ]R6,ρR3,ϕso(3),ξ=[ϕρ0T0]R4×4}

其中,的元素,是包含平移和旋转的六维向量。其实是的元素。

符号这里表示将六维向量转换成四维矩阵。

附近,齐次变换矩阵可以由计算出来,由此可以看到,齐次变换矩阵与一个反对称矩阵建立了联系,在一时刻对于给定的,可以求得一个,其描述了在局部的导数关系。

矩阵的指数运算显然没有定义,如何计算呢?这里可以用泰勒公式,

exp(A)=n=01n!An

写成旋转加平移的形式,再将写成模长和方向的形式,代入上面的公式可得:

exp(ξ)=[n=01n!(ϕ)nn=01(n+1)!(ϕ)nρ01][RJρ0T1]=T

时,推导可得

J=sinθθI+(1sinθθ)aaT+1cosθθa

当在李群中完成两个矩阵的乘法时,也就是依次进行两个变换时,李代数上对应会发生怎样的变化呢?

的指数映射,那么

是否等于呢?对于矩阵的指数运算,上式并不成立。

两个李代数指数映射乘积的完整形式,由Baker-Campbell-Hausdorff(BCH)公式给出,

[,]是李括号,表示二元运算。

以旋转矩阵组成的特殊正交群为例,

考虑上的李代数,当为小量,BCH公式中李括号二次以上的项可以忽略掉,则可得近似线性表示为,

(ln(exp(ϕ1)(ϕ2))){Jl(ϕ2)1ϕ1+ϕ2ϕ1Jr(ϕ1)1ϕ2+ϕ1ϕ2

上面公式中第一种相当于是对原旋转矩阵左乘一个微小变换矩阵,第二种相当于是对原旋转矩阵右乘一个微小变换矩阵。乘以微小变换矩阵相当于是发生了微小的位移和姿态变化。

左乘微小变换矩阵的近似雅可比矩阵同推导的指数映射中作用在平移向量上的雅可比

Jl=J=sinθθI+(1sinθθ)aaT+1cosθθa$$$R$,$ϕ$,$R$,$ϕ$$RR$,BCH$Jl1(ϕ)ϕ+ϕ$,$$exp(ϕ)exp(ϕ)=exp((ϕ+Jl1(ϕ)ϕ))

上面公式中,如果左乘的微小旋转矩阵对应的李代数带雅可比矩阵,则上式就变为(以左乘为例):

exp((Jl(ϕ)ϕ))exp(ϕ)=exp((ϕ+Jl1(ϕ)Jl(ϕ)ϕ))=exp((ϕ+ϕ))

由上,李代数上的加法,可近似为李群上带左或右雅可比的乘法:

exp((ϕ+ϕ))=exp((Jl(ϕ)ϕ))exp(ϕ)=exp(ϕ)exp((Jr(ϕ)ϕ))

因为旋转矩阵组成的李群各个变换之间只定义了有意义的乘法而没有定义有意义的加法,因此不能直接在李群上定义导数。

而考虑李群与李代数之间的关系,以旋转矩阵为例对上的李代数求导,

RPR

由于上只对乘法满足封闭性,因此不能按照常规的微分定义来求导,设对应的李代数为,由可得

exp(ϕ)Pϕ=limδϕ0exp((ϕ+δϕ))pexp(ϕ)pδϕ=limδϕ0exp(Jl(ϕ)δϕ))exp(ϕ)pexp(ϕ)pδϕ=limδϕ0(I+(Jl(ϕ)δϕ))exp(ϕ)pexp(ϕ)pδϕ=limδϕ0(Jl(ϕ)δϕ)exp(ϕ)pδϕ=limδϕ0(exp(ϕ)p)Jl(ϕ)δϕδϕ=(Rp)Jl(ϕ)

上面推导过程中1到2用到了BCH近似公式,2到3用到了泰勒公式,4到5用到了反对称矩阵的性质,最终得到了旋转后的点相对于旋转矩阵李代数的导数。

通过转换成李代数求导得到的最终的导数包含一个矩阵,其形式和计算过于复杂。另外一种李群求导的方法是扰动模型,对进行一个左乘或右乘扰动,将结果相对于扰动的变化率作为导数,以左乘扰动为例,其对应的李代数为,推导如下:

RpR=limφ0exp(φ)exp(ϕ)pexp(ϕ)pφ

该式的求导可写成,

RpR=limφ0exp(φ)exp(ϕ)pexp(ϕ)pφ=limφ0(I+φ)exp(ϕ)pexp(ϕ)pφ=limφ0φRpφ=limφ0(Rp)φφ=(Rp)

相比于对李代数直接求导,省去了一个雅可比的计算。

# ICP问题的非线性解法

考虑前面ICP优化的目标函数:

e=minT12in||piTqi||22=minξ12i=1n||(piexp(ξ)qi)||22

对应的李代数。

单个误差项关于位姿的导数,使用李代数的扰动模型可得,

eξ=limδξ0e(δξξ)e(ξ)δξ

表示左乘模型

对于n对三维空间中的点及其投影,希望计算的相机旋转和平移分别为其对应的李群为。对于空间中的一点,其对应的投影点像素坐标为,根据相机模型,有以下关系,

si[μivi1]=KT[XiYiZi1]

写成矩阵的形式为:

siμi=KTpi

上式隐含了一次齐次坐标往三维坐标的转换。

对于n对空间点和对应的图像中投影的观测点,要估计相机的位姿,目标函数就是最小化两者之间的误差,

T=argminT12i=1n||μi1siKTpi||

记通过相机外参变换到相机坐标系下点对应的点的坐标为

根据相机模型关于点有:

sμ=Kq

展开有:

[sμsvs]=[fx0cxofycy001][XYZ]

重投影误差的左乘扰动求导数为:

eξ=eqqξ

根据目标函数和相机模型的公式有:

eq=[μXμYμZvXvYvZ]=[fxZ0fxXZ20fyZfyYZ2]

而第二项为变换后的点关于李代数的导数,

(Tp)T=[Iq00]

根据定义取出前三维,

qT=[I,q]T

最后将两者相乘可以求得

# 代码示例

使用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)