通过ICP配准两组尺度不同点云, 统一坐标系(附代码)
网上大部分的ICP配准点云方法, 是在假定两组点云间尺度一致的情况下, 计算出两组点云间的旋转矩阵R和平移向量t, 若两组点云间还存在尺度不一致, 则无法准确估计. 本文介绍了两种方法, 配准两组“尺度”不同点云, 计算出它们之间的相似变换矩阵, 统一它们的坐标.
摘要
网上大部分的ICP配准点云方法, 是在假定两组点云间尺度一致的情况下, 计算出两组点云间的旋转矩阵R和平移向量t, 若两组点云间还存在尺度不一致, 则无法准确估计. 本文介绍了两种方法, 配准两组“尺度”不同点云, 计算出它们之间的相似变换矩阵, 统一它们的坐标.
注意:本文提出的方法效果不一定好, 如果大家知道更好的方法, 欢迎留言讨论.
本文主要讲了如何估计尺度,对于尺度相同的点云ICP算法的原理细节,可以看另一篇博文《【动手学MVG】ICP算法原理和代码实现》
假设我有3D点集合 S 1 S_1 S1, 对该点集进行缩放, 旋转以及平移操作(即三维空间中的相似变换)后得到3D点集合 S 2 S_2 S2 . 现在问题是:
如果给定上述的点集S1和S2, 并且已知点的匹配关系(即已知点集S1中每个点对应点集S2中的哪个点), 如何估计出一个相似变换, 使得点集S2进行相似变换后与点集1重合?
方法一
分别求解缩放倍数s和刚体变换Rt。
上述问题中, 如果点集
S
1
S_1
S1 和
S
2
S_2
S2 之间不存在缩放关系时(即尺度相同时), 可以用经典ICP( Iterative Closest Point )
方法求解得到旋转矩阵R和平移向量t来进行点集对齐. 如果我们能首先估计出点集S1和S2之间的缩放倍数s(或称为尺度因子, 本文中两个称呼混用), 我们就可以利用ICP算法求解剩下的问题.
估计缩放倍数s
为了配准两组三维点集合, 我们需要找到这么一个相似变换矩阵
[
s
R
t
0
T
1
]
\begin{bmatrix}sR & t \\ 0^T & 1\end{bmatrix}
[sR0Tt1], 使得
[
X
1
S
1
Y
1
S
1
Z
1
S
1
1
]
=
[
s
R
t
0
T
1
]
[
X
1
S
2
Y
1
S
2
Z
1
S
2
1
]
(1)
\begin{bmatrix} X_1^{S_1} \\ Y_1^{S_1} \\ Z_1^{S_1} \\ 1 \end{bmatrix}= \begin{bmatrix} sR & t \\ 0^T & 1 \end{bmatrix} \begin{bmatrix} X_1^{S_2} \\ Y_1^{S_2} \\ Z_1^{S_2} \\ 1 \end{bmatrix} \tag{1}
⎣⎢⎢⎡X1S1Y1S1Z1S11⎦⎥⎥⎤=[sR0Tt1]⎣⎢⎢⎡X1S2Y1S2Z1S21⎦⎥⎥⎤(1)
其中,
[
X
1
S
1
Y
1
S
1
Z
1
S
1
]
\begin{bmatrix}X_1^{S_1} \\ Y_1^{S_1} \\ Z_1^{S_1} \end{bmatrix}
⎣⎡X1S1Y1S1Z1S1⎦⎤ 表示三维点集
S
1
S_1
S1 中的一个三维点坐标
P
1
S
1
P_1^{S_1}
P1S1,
[
X
1
S
2
Y
1
S
2
Z
1
S
2
]
\begin{bmatrix}X_1^{S_2} \\ Y_1^{S_2} \\ Z_1^{S_2} \end{bmatrix}
⎣⎡X1S2Y1S2Z1S2⎦⎤ 表示三维点集
S
2
S_2
S2 中的与
P
1
S
1
P_1^{S_1}
P1S1 相匹配的一个三维点坐标, 记为
P
1
S
2
P_1^{S_2}
P1S2. 相似矩阵中的
s
s
s 表示缩放倍数, R为3x3的旋转矩阵, t为3x1的平移向量,
0
T
0^T
0T 为1x3的零向量, 1为常数.
为了求解缩放倍数s, 我们再找一对3D-3D匹配点
P
2
S
1
P_2^{S_1}
P2S1 和
P
2
S
2
P_2^{S_2}
P2S2 , 有
[
X
2
S
1
Y
2
S
1
Z
2
S
1
1
]
=
[
s
R
t
0
T
1
]
[
X
2
S
2
Y
2
S
2
Z
2
S
2
1
]
(2)
\begin{bmatrix} X_2^{S_1} \\ Y_2^{S_1} \\ Z_2^{S_1} \\ 1 \end{bmatrix}= \begin{bmatrix} sR & t \\ 0^T & 1 \end{bmatrix} \begin{bmatrix} X_2^{S_2} \\ Y_2^{S_2} \\ Z_2^{S_2} \\ 1 \end{bmatrix} \tag{2}
⎣⎢⎢⎡X2S1Y2S1Z2S11⎦⎥⎥⎤=[sR0Tt1]⎣⎢⎢⎡X2S2Y2S2Z2S21⎦⎥⎥⎤(2)
等式(2) - 等式(1), 有
[
X
2
S
1
−
X
1
S
1
Y
2
S
1
−
Y
1
S
1
Z
2
S
1
−
Z
1
S
1
0
]
=
[
s
R
t
0
T
1
]
[
X
2
S
2
−
X
1
S
2
Y
2
S
2
−
Y
1
S
2
Z
2
S
2
−
Z
1
S
2
0
]
(3)
\begin{bmatrix} X_2^{S_1} - X_1^{S_1} \\ Y_2^{S_1} - Y_1^{S_1} \\ Z_2^{S_1} - Z_1^{S_1} \\ 0 \end{bmatrix}= \begin{bmatrix} sR & t \\ 0^T & 1 \end{bmatrix} \begin{bmatrix} X_2^{S_2} - X_1^{S_2} \\ Y_2^{S_2} - Y_1^{S_2} \\ Z_2^{S_2} - Z_1^{S_2} \\ 0 \end{bmatrix} \tag{3}
⎣⎢⎢⎡X2S1−X1S1Y2S1−Y1S1Z2S1−Z1S10⎦⎥⎥⎤=[sR0Tt1]⎣⎢⎢⎡X2S2−X1S2Y2S2−Y1S2Z2S2−Z1S20⎦⎥⎥⎤(3)
即
[
X
2
S
1
−
X
1
S
1
Y
2
S
1
−
Y
1
S
1
Z
2
S
1
−
Z
1
S
1
]
=
s
R
[
X
2
S
2
−
X
1
S
2
Y
2
S
2
−
Y
1
S
2
Z
2
S
2
−
Z
1
S
2
]
(4)
\begin{bmatrix} X_2^{S_1} - X_1^{S_1} \\ Y_2^{S_1} - Y_1^{S_1} \\ Z_2^{S_1} - Z_1^{S_1} \end{bmatrix}= sR \begin{bmatrix} X_2^{S_2} - X_1^{S_2} \\ Y_2^{S_2} - Y_1^{S_2} \\ Z_2^{S_2} - Z_1^{S_2} \end{bmatrix} \tag{4}
⎣⎡X2S1−X1S1Y2S1−Y1S1Z2S1−Z1S1⎦⎤=sR⎣⎡X2S2−X1S2Y2S2−Y1S2Z2S2−Z1S2⎦⎤(4)
等式(4)两边取模值, 因为旋转矩阵R不影响向量长度,所以有
(
X
2
S
1
−
X
1
S
1
)
2
+
(
Y
2
S
1
−
Y
1
S
1
)
2
+
(
Z
2
S
1
−
Z
1
S
1
)
2
=
s
(
X
2
S
2
−
X
1
S
2
)
2
+
(
Y
2
S
2
−
Y
1
S
2
)
2
+
(
Z
2
S
2
−
Z
1
S
2
)
2
(5)
\sqrt{(X_2^{S_1} - X_1^{S_1})^2 + (Y_2^{S_1} - Y_1^{S_1})^2 + (Z_2^{S_1} - Z_1^{S_1})^2}\\ =s\sqrt{(X_2^{S_2} - X_1^{S_2})^2 + (Y_2^{S_2} - Y_1^{S_2} )^2 + (Z_2^{S_2} - Z_1^{S_2})^2} \tag{5}
(X2S1−X1S1)2+(Y2S1−Y1S1)2+(Z2S1−Z1S1)2=s(X2S2−X1S2)2+(Y2S2−Y1S2)2+(Z2S2−Z1S2)2(5)
因此可求得缩放倍数s
s
=
(
X
2
S
1
−
X
1
S
1
)
2
+
(
Y
2
S
1
−
Y
1
S
1
)
2
+
(
Z
2
S
1
−
Z
1
S
1
)
2
(
X
2
S
2
−
X
1
S
2
)
2
+
(
Y
2
S
2
−
Y
1
S
2
)
2
+
(
Z
2
S
2
−
Z
1
S
2
)
2
(6)
s=\frac{\sqrt{(X_2^{S_1} - X_1^{S_1})^2 + (Y_2^{S_1} - Y_1^{S_1})^2 + (Z_2^{S_1} - Z_1^{S_1})^2}}{\sqrt{(X_2^{S_2} - X_1^{S_2})^2 + (Y_2^{S_2} - Y_1^{S_2} )^2 + (Z_2^{S_2} - Z_1^{S_2})^2}} \tag{6}
s=(X2S2−X1S2)2+(Y2S2−Y1S2)2+(Z2S2−Z1S2)2(X2S1−X1S1)2+(Y2S1−Y1S1)2+(Z2S1−Z1S1)2(6)
等式(6)的物理含义是: 两个点集的"形状"相同, 在两个点集中分别找到一条对应的线段, 线段长度的比值就是这两个点集的缩放倍数.
得到缩放倍数后, 我们就可以把问题简化为ICP能处理的形式, 并使用ICP求解.
ICP配准尺度不一致(带缩放)的点云
已知点云间的缩放倍数s后, 上面等式(1)可看作:
[
X
1
S
1
Y
1
S
1
Z
1
S
1
1
]
=
[
R
t
0
T
1
]
[
s
X
1
S
2
s
Y
1
S
2
s
Z
1
S
2
1
]
(7)
\begin{bmatrix} X_1^{S_1} \\ Y_1^{S_1} \\ Z_1^{S_1} \\ 1 \end{bmatrix}= \begin{bmatrix} R & t \\ 0^T & 1 \end{bmatrix} \begin{bmatrix} sX_1^{S_2} \\ sY_1^{S_2} \\ sZ_1^{S_2} \\ 1 \end{bmatrix} \tag{7}
⎣⎢⎢⎡X1S1Y1S1Z1S11⎦⎥⎥⎤=[R0Tt1]⎣⎢⎢⎡sX1S2sY1S2sZ1S21⎦⎥⎥⎤(7)
其中,
[
R
t
0
T
1
]
\begin{bmatrix}R & t \\ 0^T & 1\end{bmatrix}
[R0Tt1] 为刚体变换矩阵, 只包含旋转和平移, 缩放倍数s已知. 等式(7)表示把点集
S
2
S_2
S2 的坐标都乘以缩放倍数s后, 得到的新的点集与点集
S
1
S_1
S1 之间的变换关系变为刚体变换. 而刚体变换可通过ICP算法求解. ( ICP求解过程这里不再赘述, 可参考《视觉SLAM十四讲》第一版, 7.9章《3D-3D: ICP》
)
最后, 将求解出的s和R, t组合,就可以得到配准点集
S
1
S_1
S1 和
S
2
S_2
S2 之间所需的相似变换矩阵
[
s
R
t
0
T
1
]
\begin{bmatrix}sR & t \\ 0^T & 1\end{bmatrix}
[sR0Tt1] .
方法一的代码实现
下面是算法主要代码文件,如果你还是有困惑,可以在这里直接下载整个工程(基于cmake)
PointCloudRegistration.h
:
#ifndef POINTCLOUDREGISTRATION_H
#define POINTCLOUDREGISTRATION_H
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
// 这里的[sR|t]是pts2到pts1的变换。
// pts1 = [sR|t] * pts2
void pose_estimation_3d3d (
const std::vector<cv::Point3f>& pts2,
const std::vector<cv::Point3f>& pts1,
float &scale, cv::Mat& R, cv::Mat& t
);
#endif
PointCloudRegistration.cpp
:
#include "PointCloudRegistration.h"
#include <Eigen/Core>
#include <Eigen/Geometry>
#include <Eigen/SVD>
// 这里的[sR|t]是pts2到pts1的变换。
// pts1 = [sR|t] * pts2
void pose_estimation_3d3d (
const std::vector<cv::Point3f>& pts2,
const std::vector<cv::Point3f>& pts1,
float &scale, cv::Mat& R, cv::Mat& t
)
{
// estimate scale
// TODO: scale计算可以取均值, 或者作为优化问题
cv::Point3f point1_sub = pts1[0] - pts1[1];
cv::Point3f point2_sub = pts2[0] - pts2[1];
float sum1 = point1_sub.x*point1_sub.x + point1_sub.y*point1_sub.y + point1_sub.z*point1_sub.z;
float sum2 = point2_sub.x*point2_sub.x + point2_sub.y*point2_sub.y + point2_sub.z*point2_sub.z;
scale = sqrt(sum1/sum2);
// printf("scale_estimated:%f \n", scale);
std::vector<cv::Point3f> pts2_scaled(pts2.size());
for(int32_t i=0; i<pts2.size(); ++i){
pts2_scaled[i] = pts2[i] * scale;
}
cv::Point3f p1, p2; // center of mass
int N = pts1.size();
for ( int i=0; i<N; i++ )
{
p1 += pts1[i];
p2 += pts2_scaled[i];
}
p1 = cv::Point3f( cv::Vec3f(p1) / N);
p2 = cv::Point3f( cv::Vec3f(p2) / N);
std::vector<cv::Point3f> q1 ( N ), q2 ( N ); // remove the center
for ( int i=0; i<N; i++ )
{
q1[i] = pts1[i] - p1;
q2[i] = pts2_scaled[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();
if (U.determinant() * V.determinant() < 0)
{
for (int x = 0; x < 3; ++x)
{
U(x, 2) *= -1;
}
}
// cout<<"U="<<U<<endl;
// cout<<"V="<<V<<endl;
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
R = ( cv::Mat_<float> ( 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_<float> ( 3,1 ) << t_ ( 0,0 ), t_ ( 1,0 ), t_ ( 2,0 ) );
}
main.cpp
:
#include <iostream>
#include <opencv2/core/core.hpp>
#include "PointCloudRegistration.h"
#include <Eigen/Geometry>
using namespace std;
using namespace cv;
int main(){
// 生成数据
std::vector<cv::Point3f> pts1, pts2;
pts1.push_back({1, 0, 0});
pts1.push_back({1, 1, 0});
pts1.push_back({0, 1, 0});
pts1.push_back({0, 1, 1});
pts1.push_back({1, 1, 1});
float scale = 2.0f;
cv::Mat T = (cv::Mat_<float>(4,4) <<
0.997207*scale, 0.0583427*scale, -0.046639*scale, 0.137988,
-0.0578775*scale, 0.99826*scale, 0.0112663*scale, -0.065517,
0.0472151*scale, -0.00853546*scale, 0.998848*scale, -0.0298169,
0, 0, 0, 1);
for(int32_t i=0; i<pts1.size(); ++i){
cv::Mat point = (cv::Mat_<float>(4,1) << pts1[i].x, pts1[i].y, pts1[i].z, 1.0f);
cv::Mat point2 = T * point;
pts2.push_back({point2.ptr<float>(0)[0], point2.ptr<float>(1)[0], point2.ptr<float>(2)[0]});
// add noise
// pts2.push_back({point2.ptr<float>(0)[0] + (float)rand()/RAND_MAX*0.01f,
// point2.ptr<float>(1)[0] + (float)rand()/RAND_MAX*0.01f,
// point2.ptr<float>(2)[0] + (float)rand()/RAND_MAX*0.01f});
// cout << "pts2:" << pts2 << endl;
}
// 估计s[R|t]
float scale_estimated;
Mat R;
Mat t;
Mat sRt; // [sR|t]
// pts1 = [sR|t] * pts2
pose_estimation_3d3d ( pts1, pts2, scale_estimated, R, t);
std::cout<<"ICP via SVD results: "<<std::endl;
std::cout<<"R = "<<R<<std::endl;
std::cout<<"t = "<<t<<std::endl;
// std::cout<<"R_inv = "<<R.t() <<std::endl;
// std::cout<<"t_inv = "<<-R.t() *t<<std::endl;
hconcat(scale_estimated*R, t, sRt);
// 对比估计值与真值
cout << "sRt:" << sRt << endl;
cout << "T :" << T << endl;
// TODO: 优化sRt
return 0;
}
输出结果:
TODO:方法二
其实文本的问题在点云处理中称之为注册
,Eigen库中已经有类似实现,Eigen::umeyama( x1, x2, true );函数的最后一个参数给true表示连尺度也一起估计。
总结
当然,上述方法一或方法二估计的相似矩阵只是初始值,想要更精确的话可以把该初始值利用ceres库进行优化。
相关/参考链接
《视觉SLAM十四讲》第一版, 7.9章《3D-3D: ICP》
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)