赞
踩
点云配准有很多的用途,比如说在三维重建中会将2.5D的点云通过点云配准技术获得准确的3D点云;再比如点云配准可以用来做位姿估计;除此以外在自动驾驶、考古、地图测绘等领域都有应用到这项技术。这篇文章我们聊聊最经典的ICP(Iterative Closest Point)算法。
ICP算法需要输入两个点云,一个是目标点云,另一个是源点云,输出的是从源点云到目标点云的位姿变换。目标点云不会移动,而源点云则会在迭代的过程中不断接近目标点云,直到收敛。
但是ICP算法很容易陷入局部最优解,通常需要目标点云和源点云在配准前有比较高的重合率,因此在执行ICP之前通常会用其他方法进行一次粗配准,来获得比较好的初值,然后再使用ICP进行精配准。
经典的ICP算法会对下面的目标函数进行优化:
J
=
1
2
Σ
i
=
1
n
∣
∣
q
i
−
(
R
p
i
+
t
)
∣
∣
2
J=\frac{1}{2}\Sigma_{i=1}^n \left||q_i-(Rp_i+t)|\right|^2
J=21Σi=1n∣∣qi−(Rpi+t)∣∣2
式中的 p p p 和 q q q 分别是源点云和目标点云中的点, n n n是源点云的总点数。这个目标函数很容易理解,就是要找到由 R R R、 t t t组成的刚体变换,来使得源点云和目标点云之间的差异尽可能小。
如何衡量这两个点云之间的差异呢?经典的ICP会遍历 < R , t > <R, t> <R,t>变换后的源点云中的所有点 p i ′ p_i' pi′,从目标点云中搜索出每个 p i ′ p_i' pi′的最近邻 q i q_i qi, ( p i ′ , q i ) (p_i', q_i) (pi′,qi)形成一个匹配点对,计算二者之间距离的平方。 对所有点对的距离平方求和,总和最小则认为差异最小。
当匹配点对确定好之后,ICP的优化函数其实是一个凸函数,可以找到最优解,求解过程如下:
定义源点云和目标点云的质心:
μ
p
=
1
n
Σ
i
=
1
n
p
i
,
μ
q
=
1
n
Σ
i
=
1
n
q
i
\mu_p=\frac{1}{n}\Sigma_{i=1}^n p_i,\mu_q=\frac{1}{n}\Sigma_{i=1}^n q_i
μp=n1Σi=1npi,μq=n1Σi=1nqi
然后对
J
J
J进行改写,就是在后边加一项又减一项
J
=
1
2
Σ
i
=
1
n
∣
∣
q
i
−
(
R
p
i
+
t
)
∣
∣
2
=
1
2
Σ
i
=
1
n
∣
∣
q
i
−
R
p
i
−
t
−
(
μ
q
−
R
μ
p
)
+
(
μ
q
−
R
μ
p
)
∣
∣
2
=
1
2
Σ
i
=
1
n
(
∣
∣
q
i
−
μ
q
−
R
(
p
i
−
μ
p
)
∣
∣
2
+
∣
∣
μ
q
−
R
μ
p
−
t
∣
∣
2
)
+
Σ
i
=
1
n
(
q
i
−
μ
q
−
R
(
p
i
−
μ
p
)
)
T
(
μ
q
−
R
μ
p
−
t
)
其中,对最后一项进行化简,可以发现前半部分为0,因为
Σ
i
=
1
n
q
i
=
Σ
i
=
1
n
μ
q
\Sigma_{i=1}^n q_i=\Sigma_{i=1}^n \mu_q
Σi=1nqi=Σi=1nμq,
Σ
i
=
1
n
p
i
=
Σ
i
=
1
n
μ
p
\Sigma_{i=1}^n p_i=\Sigma_{i=1}^n \mu_p
Σi=1npi=Σi=1nμp,所以
J
J
J可以改写为:
J
=
1
2
Σ
i
=
1
n
(
∣
∣
q
i
−
μ
q
−
R
(
p
i
−
μ
p
)
∣
∣
2
+
∣
∣
μ
q
−
R
μ
p
−
t
∣
∣
2
)
J=\frac{1}{2}\Sigma_{i=1}^n (\left||q_i-\mu_q-R(p_i-\mu_p)|\right|^2+\left||\mu_q-R\mu_p-t|\right|^2)
J=21Σi=1n(∣∣qi−μq−R(pi−μp)∣∣2+∣∣μq−Rμp−t∣∣2)
观察一下这个式子,对于后一项
Σ
i
=
1
n
∣
∣
μ
q
−
R
μ
p
−
t
∣
∣
2
\Sigma_{i=1}^n\left||\mu_q-R\mu_p-t|\right|^2
Σi=1n∣∣μq−Rμp−t∣∣2,很明显,不管选什么
R
R
R,都可以找到对应的
t
t
t使得这一项为0。因此我们只需要对前一项进行优化,找出使得其值最小的
R
R
R,然后再用后一项求解
t
t
t,即可获得最小的
J
J
J值。
将点云去中心化,
q
i
′
=
q
i
−
μ
q
q_i'=q_i-\mu_q
qi′=qi−μq,
p
i
′
=
p
i
−
μ
p
p_i'=p_i-\mu_p
pi′=pi−μp,然后对第一部分进行简化,最优的旋转矩阵
R
∗
R^*
R∗:
R
∗
=
a
r
g
m
i
n
R
Σ
i
=
1
n
(
q
i
′
T
q
i
′
+
p
i
′
T
R
T
R
p
i
′
−
2
q
i
′
T
R
p
i
′
)
因为
R
T
R
=
E
R^TR=E
RTR=E,所以前两项与
R
R
R无关,因此
R
∗
=
a
r
g
m
i
n
R
Σ
i
=
1
n
(
−
2
q
i
′
T
R
p
i
′
)
相当于求使得下式最大的
R
R
R,取迹成立是因为
E
E
E是一个标量
E
=
Σ
i
=
1
n
(
q
i
′
T
R
p
i
′
)
=
t
r
(
Σ
i
=
1
n
(
q
i
′
T
R
p
i
′
)
)
E=\Sigma_{i=1}^n(q_i'^{T}Rp_i')=tr(\Sigma_{i=1}^n(q_i'^{T}Rp_i'))
E=Σi=1n(qi′TRpi′)=tr(Σi=1n(qi′TRpi′))
然后利用迹的性质,对于满足矩阵乘法规则的
A
B
AB
AB和
B
A
BA
BA,
t
r
(
A
B
)
=
t
r
(
B
A
)
tr(AB)=tr(BA)
tr(AB)=tr(BA)
E
=
t
r
(
Σ
i
=
1
n
(
R
p
i
′
q
i
′
T
)
)
=
t
r
(
R
H
)
E=tr(\Sigma_{i=1}^n(Rp_i'q_i'^{T}))=tr(RH)
E=tr(Σi=1n(Rpi′qi′T))=tr(RH)
其中
H
=
Σ
i
=
1
n
(
p
i
′
q
i
′
T
)
=
P
′
Q
′
T
H=\Sigma_{i=1}^n(p_i'q_i'^{T})=P'Q'^T
H=Σi=1n(pi′qi′T)=P′Q′T,对其进行SVD分解可得
H
=
U
Σ
V
T
H=U\Sigma V^T
H=UΣVT
∴
t
r
(
R
H
)
=
t
r
(
R
U
Σ
V
T
)
=
t
r
(
Σ
V
T
R
U
)
设
M
=
V
T
R
U
M=V^TRU
M=VTRU,由SVD的定义可知
M
M
M仍为正交阵
∴
t
r
(
Σ
M
)
=
t
r
(
(
σ
1
0
0
0
σ
2
0
0
0
σ
3
)
(
m
11
m
12
m
13
m
21
m
22
m
23
m
31
m
32
m
33
)
)
\therefore tr(\Sigma M)=tr(
上式等于
σ
1
m
11
+
σ
2
m
22
+
σ
3
m
33
\sigma_1m_{11}+\sigma_2m_{22}+\sigma_3m_{33}
σ1m11+σ2m22+σ3m33,因为正交矩阵元素值小于等于1,所以当
M
M
M为单位阵时,
t
r
(
Σ
M
)
tr(\Sigma M)
tr(ΣM)有最大值,所以
R
∗
=
V
U
T
R^*=VU^T
R∗=VUT。然后令
μ
q
−
R
μ
p
−
t
=
0
\mu_q-R\mu_p-t=0
μq−Rμp−t=0,可得
t
∗
=
μ
q
−
R
∗
μ
p
t^*=\mu_q-R^*\mu_p
t∗=μq−R∗μp。
对视角点云施加 ( R ∗ , t ∗ ) (R^*,t^*) (R∗,t∗)的变换,然后重新进行点对匹配,再求解最优变换,直到满足收敛条件。
简单总结一下ICP的流程
网上有很多对源码的分析,我直接把文章引过来,就不再赘述了,【PCL】ICP 源码分析
通过W,A,S,D,Z,C,R,P,Y
,可以对源点云进行移动,来实现粗配准,最后按Space
来执行ICP
#include <pcl/io/ply_io.h> #include <pcl/io/pcd_io.h> #include <pcl/point_types.h> #include <pcl/point_cloud.h> #include <pcl/visualization/pcl_visualizer.h> #include <pcl/common/transforms.h> #include <pcl/registration/icp.h> #include <pcl/filters\voxel_grid.h> using namespace std; typedef pcl::PointXYZ PointT; typedef pcl::PointCloud<PointT> PointCloud; PointCloud::Ptr cloud_target(new PointCloud); PointCloud::Ptr cloud_source(new PointCloud); PointCloud::Ptr cloud_icp(new PointCloud); pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("global_visualizer")); const double translation_step = 5; // 设定一个平移的步长 const double rotation_step = M_PI / 24; // 设定一个旋转的步长 void keyboardEventOccurred(const pcl::visualization::KeyboardEvent& event, void* viewer_void); Eigen::Affine3f G = Eigen::Affine3f::Identity(); int main() { //加载点云 string target_path = "自己的目标点云路径"; string source_path = "自己的源点云路径"; pcl::io::loadPLYFile(target_path, *cloud_target); pcl::io::loadPLYFile(source_path, *cloud_source); //点云降采样 pcl::VoxelGrid<pcl::PointXYZ> VG; VG.setInputCloud(cloud_target); VG.setLeafSize(5.0f, 5.0f, 5.0f); VG.filter(*cloud_target); VG.setInputCloud(cloud_source); VG.setLeafSize(5.0f, 5.0f, 5.0f); VG.filter(*cloud_source); //可视化相关 int v1 = 1; viewer->createViewPort(0.0, 0.0, 1.0, 1.0, v1); viewer->setBackgroundColor(255, 255, 255, v1); viewer->addPointCloud(cloud_target, pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>(cloud_target, 0.0, 0.0, 0.0), "cloud_target", v1); viewer->addPointCloud(cloud_source, pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>(cloud_source, 255.0, 0.0, 0.0), "cloud_source", v1); viewer->registerKeyboardCallback(keyboardEventOccurred, (void*)viewer.get()); while (!viewer->wasStopped()) { // 应用变换 pcl::transformPointCloud(*cloud_source, *cloud_source, G); G = Eigen::Affine3f::Identity(); // 更新视图 viewer->removePointCloud("cloud_source"); viewer->addPointCloud<pcl::PointXYZ>(cloud_source, pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>(cloud_source, 255.0, 0.0, 0.0), "cloud_source", v1); viewer->removePointCloud("cloud_target"); viewer->addPointCloud<pcl::PointXYZ>(cloud_target, pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>(cloud_target, 0.0, 0.0, 0.0), "cloud_target", v1); viewer->spinOnce(10); // 每次更新等待10ms } } //键盘回调函数 void keyboardEventOccurred(const pcl::visualization::KeyboardEvent& event, void* viewer_void) { if (event.keyDown()) { if (event.getKeySym() == "space") { pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp; icp.setInputSource(cloud_source); icp.setInputTarget(cloud_target); icp.setTransformationEpsilon(1e-10); icp.setMaxCorrespondenceDistance(100); icp.setEuclideanFitnessEpsilon(0.001); icp.setMaximumIterations(10000); icp.setUseReciprocalCorrespondences(false); icp.align(*cloud_icp); G = icp.getFinalTransformation(); } else { switch (event.getKeySym()[0]) { case 'w': G.translation() += Eigen::Vector3f(translation_step, 0, 0); break; case 's': G.translation() -= Eigen::Vector3f(translation_step, 0, 0); break; case 'a': G.translation() -= Eigen::Vector3f(0, translation_step, 0); break; case 'd': G.translation() += Eigen::Vector3f(0, translation_step, 0); break; case 'z': G.translation() += Eigen::Vector3f(0, 0, translation_step); break; case 'c': G.translation() -= Eigen::Vector3f(0, 0, translation_step); break; case 'r': G.rotate(Eigen::AngleAxisf(rotation_step, Eigen::Vector3f::UnitX())); break; // Roll case 'p': G.rotate(Eigen::AngleAxisf(rotation_step, Eigen::Vector3f::UnitY())); break; // Pitch case 'y': G.rotate(Eigen::AngleAxisf(rotation_step, Eigen::Vector3f::UnitZ())); break; // Yaw default: break; } } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。