SVD求解两组点云(2D/3D)刚体变换-数学推导/C++ Eigen实现


本文通过C++非类型模板形参(Non-type template parameters),实现基于SVD方法的2D/3D空间中两组点云的刚体变换求解,并基于C++17实现了多线程版本。

1、原理推导

笔者在 二维ICP配准算法-C++ 直接法实现中实现了对目标函数直接求导,并使导数等于0的直接配准算法。本文基于上述问题重新使用SVD求解2D/3D的刚体变换。

1.1 目标函数

已知空间中的两组点云 P = [ p 1 , p 2 , . . . . . . , p n ] P=[p_1,p_2,......,p_n] P=[p1,p2,......,pn] Q = [ q 1 , q 2 , . . . . . . , q n ] Q=[q_1,q_2,......,q_n] Q=[q1,q2,......,qn],则求解刚体变换的最小而成问题可以被定义为:式(1)
( R , T ) = arg min ⁡ ∑ i = 1 n ∣ ∣ ( R p i + T ) − q i ∣ ∣ 2 (R,T)=\argmin \sum_{i=1}^{n}||(Rp_i+T)-q_i||^2 (R,T)=argmini=1n∣∣(Rpi+T)qi2

1.2 计算平移

首先假设旋转矩阵R是固定的,求目标函数对T的偏导数,并使导数等于零:式(2)
0 = ∑ i = 1 n 2 ( R p i + T − q i ) 0=\sum_{i=1}^n 2(Rp_i+T-q_i) 0=i=1n2(Rpi+Tqi)
展开得:式(3)
0 = 2 R ∑ i = 1 n p i + 2 ∑ i = 1 n T − 2 ∑ i = 1 n q i 0=2R\sum_{i=1}^{n}p_i+2\sum_{i=1}^{n}T-2\sum_{i=1}^{n}q_i 0=2Ri=1npi+2i=1nT2i=1nqi
移项得:式(4)
∑ i = 1 n T = ∑ i = 1 n q i − R ∑ i = 1 n p i \sum_{i=1}^{n}T = \sum_{i=1}^{n}q_i-R\sum_{i=1}^{n}p_i i=1nT=i=1nqiRi=1npi
化简得:式(5)
T = ∑ i = 1 n q i ∑ i = 1 n − R ∑ i = 1 n p i ∑ i = 1 n T = \frac{ \sum_{i=1}^{n}q_i}{ \sum_{i=1}^{n}}-R\frac{\sum_{i=1}^{n}p_i }{ \sum_{i=1}^{n}} T=i=1ni=1nqiRi=1ni=1npi
若:
q c e n t e r = ∑ i = 1 n q i ∑ i = 1 n , p c e n t e r = ∑ i = 1 n p i ∑ i = 1 n q_{center}=\frac{ \sum_{i=1}^{n}q_i}{ \sum_{i=1}^{n}}, p_{center}=\frac{\sum_{i=1}^{n}p_i }{ \sum_{i=1}^{n}} qcenter=i=1ni=1nqi,pcenter=i=1ni=1npi
则:
T = q c e n t e r − R p c e n t e r T=q_{center}-Rp_{center} T=qcenterRpcenter
此时T表示的是两组点云质心之间的位移,将上述的T带入到原目标函数中:式(6)
( R , T ) = arg min ⁡ ∑ i = 1 n ∣ ∣ ( R p i + q c e n t e r − R p c e n t e r ) − q i ∣ ∣ 2 (R,T)=\argmin \sum_{i=1}^{n}||(Rp_i+q_{center}-Rp_{center})-q_i||^2 (R,T)=argmini=1n∣∣(Rpi+qcenterRpcenter)qi2
整理得:式(7)
( R , T ) = arg min ⁡ ∑ i = 1 n ∣ ∣ R ( p i − p c e n t e r ) − ( q i − q c e n t e r ) ∣ ∣ 2 (R,T)=\argmin \sum_{i=1}^{n}||R(p_i-p_{center})-(q_i-q_{center})||^2 (R,T)=argmini=1n∣∣R(pipcenter)(qiqcenter)2
根据上述公式可以得知,我们可以通过对点云进行去质心操作,来使位移为零,进而能够使我们单独求解旋转。令:
x i = p i − p c e n t e r , y i = q i − q c e n t e r x_i = p_i-p_{center},y_i=q_i-q_{center} xi=pipcenter,yi=qiqcenter
则原目标函数可以被表示为:式(8)
R = arg min ⁡ ∑ i = 1 n ∣ ∣ R x i − y i ∣ ∣ 2 R=\argmin \sum_{i=1}^{n}||Rx_i-y_i||^2 R=argmini=1n∣∣Rxiyi2

1.3 计算旋转

对式(8)进行简化得:式(9)
arg min ⁡ ∑ i = 1 n ∣ ∣ R x i − y i ∣ ∣ 2 = ∑ i = 1 n ( R x i − y i ) T ( R x i − y i ) = ∑ i = 1 n ( x i T R T − y i T ) ( R x i − y i ) = ∑ i = 1 n x i T R T R x i − y i T R x i − x i T R T y i + y i T y i \argmin \sum_{i=1}^{n}||Rx_i-y_i||^2 \\ =\sum_{i=1}^n (Rx_i-y_i)^T(Rx_i-y_i) \\ = \sum_{i=1}^n(x_i^TR^T-y_i^T)(Rx_i-y_i) \\ = \sum_{i=1}^n x_i^TR^TRx_i-y_i^TRx_i-x_i^TR^Ty_i+y_i^Ty_i argmini=1n∣∣Rxiyi2=i=1n(Rxiyi)T(Rxiyi)=i=1n(xiTRTyiT)(Rxiyi)=i=1nxiTRTRxiyiTRxixiTRTyi+yiTyi
由于R是正交矩阵满足 R R T = I RR^T=I RRT=I,进一步可以化简为:式(10)
R = arg min ⁡ ∑ i = 1 n x i T x i − 2 y i T R x i + y i T y i = arg min ⁡ ∑ i = 1 n − 2 y i T R x i = arg max ⁡ ∑ i = 1 n y i T R x i R=\argmin \sum_{i=1}^n x_i^Tx_i-2y_i^TRx_i+y_i^Ty_i \\ = \argmin \sum_{i=1}^n-2y_i^TRx_i \\ = \argmax \sum_{i=1}^{n}y_i^TRx_i R=argmini=1nxiTxi2yiTRxi+yiTyi=argmini=1n2yiTRxi=argmaxi=1nyiTRxi
考虑如下形式的矩阵乘法:
Y T R X = [ y 1 T y 2 T . . y n T ] [ R ] [ x 1 x 2 . . x n ] = [ y 1 T y 2 T . . y n T ] [ R x 1 R x 2 . . R x n ] = [ y 1 T R x 1 ∗ y 2 T R x 2 . . ∗ y n T R x n ] Y^TRX =\left[ \begin{matrix} y_1^T \\ y_2^T \\ .\\ .\\ y_n^T\\ \end{matrix} \right] \left[ \begin{matrix} & & \\ & R & \\ & & \end{matrix} \right] \left[ \begin{matrix} x_1 & x_2 & . & .& x_n \end{matrix} \right] \\[10pt] = \left[ \begin{matrix} y_1^T \\ y_2^T \\ .\\ .\\ y_n^T\\ \end{matrix} \right]\left[ \begin{matrix} Rx_1 & Rx_2 & . & .& Rx_n \end{matrix} \right] \\[10pt] = \left[ \begin{matrix} y_1^TRx_1 & & & & *\\ & y_2^TRx_2 & & &\\ & & . & &\\ & & & . &\\ * & & & &y_n^TRx_n\\ \end{matrix} \right] YTRX= y1Ty2T..ynT R [x1x2..xn]= y1Ty2T..ynT [Rx1Rx2..Rxn]= y1TRx1y2TRx2..ynTRxn
如上所示,我们注意到: ∑ i = 1 n y i T R x i = t r ( Y T R X ) \sum_{i=1}^{n}y_i^TRx_i=tr(Y^TRX) i=1nyiTRxi=tr(YTRX)
因此式(10)的优化问题可以转化成求解 M = Y T R X M=Y^TRX M=YTRX矩阵迹最大化的问题;矩阵的迹有如下性质:
t r ( A B ) = t r ( B A ) tr(AB)=tr(BA) tr(AB)=tr(BA)
因此有 式(11):
t r ( Y T R X ) = t r ( Y T ( R X ) ) = t r ( ( R X ) Y T ) tr(Y^TRX)=tr(Y^T(RX))=tr((RX)Y^T) tr(YTRX)=tr(YT(RX))=tr((RX)YT)
X Y T = M XY^T=M XYT=M,则有M矩阵的SVD:
S = U D V T S=UDV^T S=UDVT
将S的SVD结果带入到式11得:式(12)
t r ( R X Y T ) = t r ( ( R U ) ( D V T ) ) = t r ( D V T R U ) tr(RXY^T)=tr((RU)(DV^T))=tr(DV^TRU) tr(RXYT)=tr((RU)(DVT))=tr(DVTRU)
由于U,R和V都是正交矩阵,则根据SO(3)得性质 M = V T R U M=V^TRU M=VTRU也是正交矩阵,这意味着M得每一列都是正交向量,正交向量有如下性质:
m j T m j = 1 ⇒ ∑ i = 1 n m i j 2 = 1 ⇒ ∣ m i j ∣ ≤ 1 m_j^Tm_j=1 \Rightarrow \sum_{i=1}^{n}m_{ij}^2=1 \Rightarrow |m_{ij}| \leq1 mjTmj=1i=1nmij2=1mij1
根据上述性质,可以推出:
t r ( D M ) ≤ t r ( D ) tr(DM) \leq tr(D) tr(DM)tr(D)
矩阵D(奇异值降序排列)是各个元素都大于0的对角矩阵;因此当M对角线上的各个元素等于1的时候tr(DM)有最大值,最大值为tr(D)。换句话说由于M是正交矩阵,且其对角线元素等于1的时候有最大值,那么这就意味着此时M是单位矩阵。因此可以推出:
I = M = V T R U ⇒ R = V U T I=M=V^TRU \Rightarrow R=VU^T I=M=VTRUR=VUT

1.4 瑕旋转(旋转+反射(镜像))

上述的推导过程只能保证R是正交矩阵,而无法保证其是旋转矩阵(旋转矩阵一定是正交矩阵,但正交矩阵不一定是旋转矩阵,旋转矩阵的行列式为1,正交矩阵行列式为 ± 1 \pm1 ±1)。因此可以通过 V U T VU^T VUT的行列式的值,判断其是否包含反射。如下矩阵是反射变换(反射再乘一个反射是没反射)。
M r e f l e c t = [ 1 1 . . − 1 ] M_{reflect}= \left[ \begin{matrix} 1 & & & & \\ & 1 & & &\\ & & . & &\\ & & & . &\\ & & & &-1\\ \end{matrix} \right] Mreflect= 11..1
因此完整的计算旋转的公式为:
R = V [ 1 1 . . d e t ( V U T ) ] U T R=V\left[ \begin{matrix} 1 & & & & \\ & 1 & & &\\ & & . & &\\ & & & . &\\ & & & &det(VU^T)\\ \end{matrix} \right]U^T R=V 11..det(VUT) UT

1.4 算法流程

  1. 计算两个点云的质心
    q c e n t e r = ∑ i = 1 n q i ∑ i = 1 n , p c e n t e r = ∑ i = 1 n p i ∑ i = 1 n q_{center}=\frac{ \sum_{i=1}^{n}q_i}{ \sum_{i=1}^{n}}, p_{center}=\frac{\sum_{i=1}^{n}p_i }{ \sum_{i=1}^{n}} qcenter=i=1ni=1nqi,pcenter=i=1ni=1npi
  2. 点云去质心
    x i = p i − p c e n t e r , y i = q i − q c e n t e r x_i = p_i-p_{center},y_i=q_i-q_{center} xi=pipcenter,yi=qiqcenter
  3. 求解相关性矩阵S
    S = X Y T S=XY^T S=XYT
  4. 对矩阵S进行奇异值分解 S = U D V T S=UDV^T S=UDVT,计算旋转
    R = V [ 1 1 . . d e t ( V U T ) ] U T R=V\left[ \begin{matrix} 1 & & & & \\ & 1 & & &\\ & & . & &\\ & & & . &\\ & & & &det(VU^T)\\ \end{matrix} \right]U^T R=V 11..det(VUT) UT
  5. 计算平移
    T = q c e n t e r − R p c e n t e r T=q_{center}-Rp_{center} T=qcenterRpcenter

2、C++ Eigen实现

// rigid_transform.hpp
#ifndef RIGID_TRANSFORM_H
#define RIGID_TRANSFORM_H
#include <Eigen/Dense>
#include <execution>

template <int dim>
class RigidTransform
{
public:
    using PointType = Eigen::Matrix<double, dim, 1>;
    using RigidTransformType = Eigen::Matrix<double, dim + 1, dim + 1>;

    RigidTransform() = default;
    ~RigidTransform() = default;
    RigidTransformType solve(const std::vector<PointType> &P,
                             const std::vector<PointType> &Q);
    RigidTransformType solve(const Eigen::Matrix<double, dim, dim> &M,
                             const Eigen::Matrix<double, dim, 1> &P_center,
                             const Eigen::Matrix<double, dim, 1> &Q_center);

private:
    PointType normalize_point_(std::vector<PointType> &points);
    PointType normalize_point_mt_(std::vector<PointType> &points);
    Eigen::Matrix<double, dim, dim> compute_M(const std::vector<PointType> &X,
                                              const std::vector<PointType> &Y);
    Eigen::Matrix<double, dim, dim> compute_M_mt(const std::vector<PointType> &X,
                                                 const std::vector<PointType> &Y);
};

template <int dim>
Eigen::Matrix<double, dim + 1, dim + 1> RigidTransform<dim>::solve(const std::vector<PointType> &P,
                                                                   const std::vector<PointType> &Q)
{
    if (P.size() != Q.size() || P.size() < dim)
    {
        throw std::runtime_error("point cloud size must be >= 3 and source size == target size!");
    }

    auto X = P;
    auto Y = Q;

#ifdef USE_CPP17_MT
    // Point cloud normalize multi thread
    PointType P_center = normalize_point_mt_(X);
    PointType Q_center = normalize_point_mt_(Y);
    // Compute M multi thread
    Eigen::Matrix<double, dim, dim> S = compute_M_mt(X, Y);
#else
    // Point cloud normalize single thread
    PointType P_center = normalize_point_(X);
    PointType Q_center = normalize_point_(Y);
    // Compute M single thread
    Eigen::Matrix<double, dim, dim> S = compute_M(X, Y);
#endif

    return solve(S, P_center, Q_center);
}

template <int dim>
Eigen::Matrix<double, dim + 1, dim + 1> RigidTransform<dim>::solve(const Eigen::Matrix<double, dim, dim> &S,
                                                                   const Eigen::Matrix<double, dim, 1> &P_center,
                                                                   const Eigen::Matrix<double, dim, 1> &Q_center)
{
    // Compute Rotation
    Eigen::JacobiSVD<Eigen::Matrix<double, dim, dim>> svd(S, Eigen::ComputeFullU | Eigen::ComputeFullV);
    Eigen::Matrix<double, dim, dim> U = svd.matrixU();
    Eigen::Matrix<double, dim, dim> V = svd.matrixV();

    Eigen::Matrix<double, dim, dim> D = Eigen::Matrix<double, dim, dim>::Identity();
    D(dim - 1, dim - 1) = (V * U.transpose()).determinant();
    Eigen::Matrix<double, dim, dim> R = V * D * U.transpose();

    // Compute Translation
    PointType T = Q_center - R * P_center;

    // Construct SE(3)
    RigidTransformType SE3 = RigidTransformType::Identity();
    SE3.block<dim, dim>(0, 0) = R;
    SE3.block<dim, 1>(0, dim) = T;

    return SE3;
}

template <int dim>
Eigen::Matrix<double, dim, 1> RigidTransform<dim>::normalize_point_(std::vector<PointType> &points)
{
    // Get point cloud center
    PointType center = PointType::Zero();
    for (size_t i = 0; i < points.size(); ++i)
    {
        center += points[i];
    }
    center = center / points.size();

    // Remove center
    for (size_t i = 0; i < points.size(); ++i)
    {
        points[i] = points[i] - center;
    }

    return center;
}

template <int dim>
Eigen::Matrix<double, dim, 1> RigidTransform<dim>::normalize_point_mt_(std::vector<PointType> &points)
{
    // Get point cloud center
    PointType center = std::reduce(points.begin(), points.end(), PointType::Zero().eval(),
                                   [](const PointType &sum, const PointType &item)
                                   { return sum + item; }) /
                       points.size();

    // Remove center
    std::transform(points.begin(), points.end(), points.begin(), [&center](const PointType &p)
                   { return p - center; });

    return center;
}

template <int dim>
Eigen::Matrix<double, dim, dim> RigidTransform<dim>::compute_M(const std::vector<PointType> &X,
                                                               const std::vector<PointType> &Y)
{
    Eigen::Matrix<double, dim, dim> M = Eigen::Matrix<double, dim, dim>::Zero();

    for (size_t i = 0; i < X.size(); ++i)
    {
        M += X[i] * Y[i].transpose();
    }

    return M;
}

template <int dim>
Eigen::Matrix<double, dim, dim> RigidTransform<dim>::compute_M_mt(const std::vector<PointType> &X,
                                                                  const std::vector<PointType> &Y)
{
    std::vector<size_t> index(X.size());
    std::for_each(index.begin(), index.end(), [idx = 0](size_t &i) mutable
                  { i = idx++; });
    std::vector<Eigen::Matrix<double, dim, dim>> M_vec(index.size());
    std::for_each(std::execution::par_unseq, index.begin(), index.end(), [&](const size_t &i)
                  { M_vec[i] = X[i] * Y[i].transpose(); });

    return std::reduce(M_vec.begin(), M_vec.end(), Eigen::Matrix<double, dim, dim>::Zero().eval(),
                           [](const Eigen::Matrix<double, dim, dim> &sum, const Eigen::Matrix<double, dim, dim> &item)
                           { return sum + item; });
}

#endif

3、测试

// test.cc
#define USE_CPP17_MT

#include "rigid_transform.hpp"

template <typename FuncT>
void evaluate_and_call(FuncT &&func, int run_num)
{
    double total_time = 0.f;
    for (int i = 0; i < run_num; ++i)
    {
        auto t1 = std::chrono::high_resolution_clock::now();
        func();
        auto t2 = std::chrono::high_resolution_clock::now();
        total_time +=  std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1).count() * 1000;;
    }

    std::cout << "run cost: " << total_time / 1000.f << std::endl;
}

void test_2Drigid_transform()
{
    std::vector<Eigen::Vector2d> P;
    P.push_back(Eigen::Vector2d(100, 0));
    P.push_back(Eigen::Vector2d(0, 100));

    Eigen::Matrix2d R;
    double theta = CV_PI / 6;
    R << std::cos(theta), -std::sin(theta),
        std::sin(theta), std::cos(theta);

    Eigen::Vector2d T(10, 10);

    std::vector<Eigen::Vector2d> Q;
    for (const auto &p : P)
    {
        Eigen::Vector2d transformed = R * p + T;
        Q.push_back(transformed);
    }

    RigidTransform<2> rigid_transform_2d;
    Eigen::Matrix3d SE2 = rigid_transform_2d.solve(P, Q);
    std::cout << "SE2: " << std::endl;
    std::cout << SE2 << std::endl;
}

void test_3Drigid_transform()
{
    std::vector<Eigen::Vector3d> P;
    P.push_back(Eigen::Vector3d(100, 0, 0));
    P.push_back(Eigen::Vector3d(0, 100, 0));
    P.push_back(Eigen::Vector3d(0, 0, 100));

    Eigen::AngleAxisd rotateX(CV_PI / 6, Eigen::Vector3d::UnitX());
    Eigen::Vector3d T(10, 10, 10);

    std::vector<Eigen::Vector3d> Q;
    for (const auto &p : P)
    {
        Eigen::Vector3d transformed = rotateX.matrix() * p + T;
        Q.push_back(transformed);
    }

    RigidTransform<3> rigid_transform_2d;
    Eigen::Matrix4d SE3 = rigid_transform_2d.solve(P, Q);
    std::cout << "SE3: " << std::endl;
    std::cout << SE3 << std::endl;
}

int main()
{
    std::cout << "================3D================" << std::endl;
    evaluate_and_call(test_3Drigid_transform, 1);
    std::cout << "================2D================" << std::endl;
    evaluate_and_call(test_2Drigid_transform, 1);

    return 0;
}

4、程序输出

================3D================
SE3:
           1            0            0           10
           0     0.866025         -0.5           10
           0          0.5     0.866025           10
           0            0            0            1
run cost: 0.0028997
================2D================
SE2:
0.866025     -0.5       10
     0.5 0.866025       10
       0        0        1
run cost: 0.0008708

4、总结

本文通过C++非类型模板形参(Non-type template parameters),实现了ICP算法中常用的刚体变换求解方法。只需要通过在声明类模板的时候设置2或3,就可以无缝切换2D和3D刚体变换求解(当然你也可以选择直接把3D点的最后一个维度置为0,直接使用3D算法求解2D问题,但这需要进行数据转换,本文通过C++模板避免了这个问题),并且你可以通过定义宏的方式来切换成多线程算法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值