1. 从“点云配准”说起:为什么ICP值得你花时间搞懂
做三维重建、SLAM、机器人定位或者工业检测的朋友,大概率都绕不开一个词——点云配准。简单说,就是把两片从不同角度、不同时间扫描得到的点云,对齐到同一个坐标系里。比如你拿一个深度相机绕着桌子扫了一圈,得到十几帧点云,每一帧都只覆盖桌子的一部分,只有把它们“拼”起来,才能得到完整的桌面模型。这个“拼”的过程,核心就是配准。
而ICP(Iterative Closest Point,迭代最近点),就是点云配准里最经典、最基础、也是被用得最多的算法之一。它的思路朴素到令人发指:假设两片点云已经“大致对齐”了,那么对于源点云里的每一个点,在目标点云里找离它最近的那个点,认为它们是一对对应点;然后根据这些对应点,算出一个最优的旋转和平移,把源点云往目标点云那边挪一挪;挪完之后再重新找最近点,再算变换,如此反复迭代,直到误差不再下降或者达到最大迭代次数。
听起来是不是特别简单?但真正动手写代码或者推导公式的时候,你会发现坑一个接一个:最近点怎么找才快?对应点里混进了错误匹配怎么办?旋转矩阵怎么求?为什么我推出来的公式和网上看到的不一样?二维和三维的ICP有什么区别?为什么我的ICP总是不收敛?
这篇博文,我就以二维点云ICP为例,把它的原理从头到尾推一遍。不跳步、不甩公式,每一步都告诉你“为什么要这么做”。同时我会把实际写代码时遇到的坑、参数怎么调、怎么判断收敛、怎么处理外点,都一并讲清楚。目标很明确:你看完之后,能自己手写一个可用的二维ICP,并且知道每一行代码在干什么。
适合谁看?如果你正在学SLAM、做机器人定位、搞三维重建,或者单纯对点云配准感兴趣,这篇内容应该能帮到你。不需要你有很深的数学背景,只要知道矩阵乘法、向量减法、最小二乘大概是怎么回事就行。我会尽量用生活化的类比把数学讲明白。
2. ICP的整体思路拆解:为什么是“迭代”而不是“一步到位”
2.1 配准问题的本质:找一个刚体变换
我们先明确一下问题。假设有两片二维点云:
- 源点云( P = {p_1, p_2, ..., p_n} ),每个 ( p_i ) 是一个二维坐标 ((x_i, y_i))。
- 目标点云( Q = {q_1, q_2, ..., q_m} ),每个 ( q_j ) 也是二维坐标。
我们假设这两片点云描述的是同一个物体或者同一个场景,只是源点云经过了一个刚体变换(旋转+平移)之后,变成了目标点云的样子。也就是说,存在一个旋转矩阵 ( R ) 和一个平移向量 ( t ),使得:
[ q = R \cdot p + t ]
对于二维情况,旋转矩阵 ( R ) 可以写成:
[ R = \begin{bmatrix} \cos\theta & -\sin\theta \ \sin\theta & \cos\theta \end{bmatrix} ]
其中 ( \theta ) 是旋转角度。平移向量 ( t = [t_x, t_y]^T )。
配准的目标,就是找到最优的 ( R ) 和 ( t ),使得变换后的源点云和目标点云尽可能重合。所谓“尽可能重合”,通常用点到点距离的平方和来衡量:
[ E(R, t) = \sum_{i=1}^{n} | R p_i + t - q_{c(i)} |^2 ]
这里的 ( q_{c(i)} ) 是目标点云中与 ( p_i ) 对应的那个点。如果对应关系是已知的,那这个问题就是一个标准的最小二乘问题,有闭式解,一步就能算出来。但问题就在于——我们不知道对应关系。
2.2 鸡生蛋蛋生鸡:对应关系与变换互相依赖
这就是ICP的核心矛盾:要知道变换,得先知道哪些点是一对;要知道哪些点是一对,得先有一个大致正确的变换。这是一个典型的“鸡生蛋蛋生鸡”问题。
ICP的解决思路非常务实:先假设一个初始变换,然后交替优化。具体来说:
- 找对应点:在当前变换下,对源点云里的每个点,在目标点云里找最近的点,作为对应点。
- 算变换:根据这些对应点,求解最优的旋转和平移。
- 应用变换:把算出来的变换作用到源点云上。
- 判断收敛:如果误差变化很小,或者达到最大迭代次数,就停止;否则回到第1步。
这个“交替优化”的策略,就是ICP中“Iterative”的由来。它不是一步到位,而是像剥洋葱一样,一层一层逼近最优解。
注意:ICP只能保证收敛到局部最优,不能保证全局最优。如果初始变换离真实值太远,或者两片点云重叠部分太少,ICP很容易陷入错误的局部极小值。这也是为什么实际使用中,通常会先用其他方法(比如特征匹配、RANSAC)做一个粗配准,再用ICP做精配准。
2.3 为什么选点到点距离作为误差度量
你可能会问:为什么不用点到面距离、或者别的度量方式?这其实是一个权衡。
点到点距离的优点是简单、直观、有闭式解。对于二维和三维情况,只要对应关系确定,最优刚体变换可以通过SVD(奇异值分解)或者四元数方法直接算出来,不需要迭代优化。这使得ICP的每一轮迭代都很快。
缺点是收敛速度可能较慢,尤其是当两片点云初始位置相差较远时,最近点对应关系变化剧烈,容易震荡。点到面距离(比如Point-to-Plane ICP)在某些场景下收敛更快,但需要知道目标点云的表面法向量,而且求解更复杂。
对于二维点云,点到点ICP已经足够用了,而且推导过程最能体现ICP的核心思想。所以我们先从它入手。
3. 核心数学推导:从对应点到最优变换
3.1 问题简化:先假设对应关系已知
为了推导最优变换,我们先假设对应关系是已知的。也就是说,对于源点云里的每个点 ( p_i ),我们已经知道它在目标点云里的对应点是 ( q_i )。注意这里为了简化,假设两片点云点数相同,且一一对应。实际中点数可能不同,但推导思路是一样的。
我们的目标是找到 ( R ) 和 ( t ),最小化:
[ E = \sum_{i=1}^{n} | R p_i + t - q_i |^2 ]
这是一个无约束优化问题,可以对 ( t ) 求导并令其为零,先解出 ( t )。
3.2 先求平移:质心对齐是关键
把误差展开:
[ E = \sum_{i=1}^{n} (R p_i + t - q_i)^T (R p_i + t - q_i) ]
对 ( t ) 求偏导:
[ \frac{\partial E}{\partial t} = \sum_{i=1}^{n} 2(R p_i + t - q_i) = 0 ]
整理得:
[ n t + R \sum_{i=1}^{n} p_i - \sum_{i=1}^{n} q_i = 0 ]
[ t = \frac{1}{n} \sum_{i=1}^{n} q_i - R \cdot \frac{1}{n} \sum_{i=1}^{n} p_i ]
我们定义:
- 源点云质心 ( \bar{p} = \frac{1}{n} \sum_{i=1}^{n} p_i )
- 目标点云质心 ( \bar{q} = \frac{1}{n} \sum_{i=1}^{n} q_i )
那么:
[ t = \bar{q} - R \bar{p} ]
这个结果非常直观:最优平移就是让源点云的质心,经过旋转后,和目标点云的质心重合。换句话说,平移的作用就是把两个点云的质心对齐。
实操心得:在实际写代码时,我通常会先把两片点云都减去各自的质心,变成“去质心”的点云。这样平移就自动被消掉了,只需要求旋转。求完旋转之后,再根据质心差算出平移。这样做的好处是数值更稳定,而且旋转的求解只和点云的相对形状有关,不受绝对位置影响。
3.3 再求旋转:SVD分解的妙用
把 ( t = \bar{q} - R \bar{p} ) 代回误差表达式,经过一番推导(这里省略中间展开步骤,直接给结果),误差可以写成:
[ E = \sum_{i=1}^{n} | R (p_i - \bar{p}) - (q_i - \bar{q}) |^2 + \text{常数项} ]
常数项和 ( R ) 无关,所以最小化 ( E ) 等价于最大化:
[ \sum_{i=1}^{n} (R (p_i - \bar{p}))^T (q_i - \bar{q}) ]
令 ( p_i' = p_i - \bar{p} ),( q_i' = q_i - \bar{q} ),则目标变为最大化:
[ \sum_{i=1}^{n} (R p_i')^T q_i' = \sum_{i=1}^{n} \text{tr}(q_i'^T R p_i') = \text{tr}\left( R \sum_{i=1}^{n} p_i' q_i'^T \right) ]
定义协方差矩阵( H ):
[ H = \sum_{i=1}^{n} p_i' q_i'^T ]
注意这里的顺序:是源点云去质心后的点乘以目标点云去质心后的点的转置。对于二维情况,( H ) 是一个 ( 2 \times 2 ) 矩阵。
我们要最大化 ( \text{tr}(R H) )。对 ( H ) 做奇异值分解:
[ H = U \Sigma V^T ]
其中 ( U ) 和 ( V ) 是正交矩阵,( \Sigma ) 是对角矩阵,对角线元素是非负奇异值。
可以证明,当 ( R = V U^T ) 时,( \text{tr}(R H) ) 取得最大值。但有一个细节需要注意:如果 ( \det(V U^T) = -1 ),说明出现了反射,需要修正。对于二维情况,修正方法是把 ( V ) 的最后一列取反,或者等价地,把 ( \Sigma ) 的最小奇异值对应的方向翻转。
具体来说:
- 计算 ( H = \sum p_i' q_i'^T )。
- 对 ( H ) 做 SVD:( H = U \Sigma V^T )。
- 计算 ( R = V U^T )。
- 如果 ( \det(R) < 0 ),则令 ( V ) 的最后一列乘以 -1,重新计算 ( R = V U^T )。
对于二维情况,旋转矩阵的行列式必须是 +1,所以这个修正步骤是必要的。
注意:这里 ( H ) 的定义顺序很重要。有些资料写成 ( H = \sum q_i' p_i'^T ),那么最后 ( R = U V^T )。两种写法本质一样,但一定要前后一致,否则算出来的旋转方向会反。我刚开始写代码时就因为顺序搞反,导致点云越配越远,排查了半天。
3.4 完整算法流程
把上面的推导整理一下,二维ICP的完整流程如下:
- 输入:源点云 ( P ),目标点云 ( Q ),初始变换 ( R_0, t_0 ),最大迭代次数 ( N ),收敛阈值 ( \epsilon )。
- 初始化:令 ( R = R_0 ),( t = t_0 )。
- 迭代(重复 ( N ) 次或直到收敛):
- 变换源点云:( P' = R \cdot P + t )。
- 找对应点:对 ( P' ) 中的每个点,在 ( Q ) 中找最近点,得到对应关系 ( { (p_i', q_i) } )。
- 计算质心:( \bar{p}' = \frac{1}{n} \sum p_i' ),( \bar{q} = \frac{1}{n} \sum q_i )。
- 去质心:( p_i'' = p_i' - \bar{p}' ),( q_i' = q_i - \bar{q} )。
- 计算协方差矩阵:( H = \sum p_i'' q_i'^T )。
- SVD分解:( H = U \Sigma V^T )。
- 计算旋转:( R_{\Delta} = V U^T ),若 ( \det(R_{\Delta}) < 0 ) 则修正。
- 计算平移:( t_{\Delta} = \bar{q} - R_{\Delta} \bar{p}' )。
- 更新变换:( R = R_{\Delta} R ),( t = R_{\Delta} t + t_{\Delta} )。
- 计算误差:( E = \sum | R_{\Delta} p_i' + t_{\Delta} - q_i |^2 )。
- 判断收敛:如果 ( |E_{prev} - E| < \epsilon ),停止。
- 输出:最终的 ( R ) 和 ( t )。
这个流程里,每一步都有明确的几何意义。变换源点云是为了在当前估计下看两片点云的对齐情况;找对应点是为了建立点与点之间的临时联系;去质心是为了分离旋转和平移;SVD是为了求最优旋转;更新变换是把新算出来的增量变换累积到总变换上。
4. 动手实现:从零写一个二维ICP
4.1 环境准备与数据生成
我用Python来演示,因为NumPy和Matplotlib足够方便,而且代码读起来清晰。如果你用C++或者MATLAB,思路完全一样。
先导入必要的库:
import numpy as np import matplotlib.pyplot as plt然后生成一些测试数据。为了模拟真实场景,我生成一片随机点云,然后对它施加一个已知的旋转和平移,得到目标点云。这样我们就知道真实变换是什么,方便验证算法是否正确。
np.random.seed(42) # 生成源点云:一个矩形加上一些随机点 n_points = 200 source = np.random.rand(n_points, 2) * 10 # 范围 [0, 10] x [0, 10] # 真实变换:旋转 30 度,平移 (5, 3) theta_true = np.deg2rad(30) R_true = np.array([[np.cos(theta_true), -np.sin(theta_true)], [np.sin(theta_true), np.cos(theta_true)]]) t_true = np.array([5.0, 3.0]) # 生成目标点云 target = (R_true @ source.T).T + t_true # 加一点噪声,模拟真实扫描误差 target += np.random.randn(n_points, 2) * 0.1这样我们就有了两片点云,它们之间的真实变换是旋转30度、平移(5, 3)。ICP的任务就是把这个变换估计出来。
4.2 最近点搜索:暴力法 vs KD树
找最近点最直接的方法就是暴力搜索:对源点云里的每个点,遍历目标点云里的所有点,算距离,取最小的。时间复杂度是 ( O(n \cdot m) )。对于200个点,这完全没问题。但如果点数上万,暴力法就太慢了。
实际项目中,我会用KD树或者网格哈希来加速。Python的scipy库提供了KDTree,用起来很方便:
from scipy.spatial import KDTree tree = KDTree(target) distances, indices = tree.query(source_transformed)indices就是每个源点对应的目标点索引。KD树把最近点搜索的平均复杂度降到了 ( O(n \log m) ),对于大规模点云非常必要。
实操心得:如果两片点云的点数差异很大,比如源点云有10万个点,目标点云只有1万个点,那么应该把目标点云建成KD树,然后拿源点云去查询。反过来建树也可以,但查询次数会多很多。原则是:用点数少的那片建树,用点数多的那片查询。
4.3 核心迭代代码
下面是完整的ICP迭代代码:
def icp_2d(source, target, max_iter=50, tol=1e-6): """ 二维 ICP 配准 source: (n, 2) 源点云 target: (m, 2) 目标点云 返回: R (2x2), t (2,), 误差历史 """ # 初始化变换 R = np.eye(2) t = np.zeros(2) # 构建 KD 树 tree = KDTree(target) prev_error = np.inf error_history = [] for i in range(max_iter): # 1. 变换源点云 source_transformed = (R @ source.T).T + t # 2. 找最近点 distances, indices = tree.query(source_transformed) target_matched = target[indices] # 3. 计算质心 centroid_source = np.mean(source_transformed, axis=0) centroid_target = np.mean(target_matched, axis=0) # 4. 去质心 source_centered = source_transformed - centroid_source target_centered = target_matched - centroid_target # 5. 计算协方差矩阵 H H = source_centered.T @ target_centered # 6. SVD 分解 U, S, Vt = np.linalg.svd(H) V = Vt.T # 7. 计算旋转 R_delta = V @ U.T # 8. 处理反射情况 if np.linalg.det(R_delta) < 0: V[:, -1] *= -1 R_delta = V @ U.T # 9. 计算平移 t_delta = centroid_target - R_delta @ centroid_source # 10. 更新变换 R = R_delta @ R t = R_delta @ t + t_delta # 11. 计算误差 error = np.mean(distances ** 2) error_history.append(error) # 12. 判断收敛 if abs(prev_error - error) < tol: print(f"ICP 在第 {i+1} 次迭代收敛") break prev_error = error return R, t, error_history这段代码里,有几个地方需要特别注意:
- 第5步的 ( H ) 计算:我用的是
source_centered.T @ target_centered,得到的是 ( 2 \times 2 ) 矩阵。这对应的是 ( H = \sum p_i' q_i'^T )。如果你写成target_centered.T @ source_centered,那么后面旋转的计算公式也要相应调整。 - 第8步的反射处理:二维旋转矩阵的行列式必须是 +1。如果SVD算出来的 ( V U^T ) 行列式为 -1,说明出现了镜像,需要把 ( V ) 的最后一列取反。这个步骤在三维里也一样,只是修正方式略有不同。
- 第10步的变换更新:注意顺序。新的总变换是增量变换左乘旧的总变换。平移的更新是 ( t = R_{\Delta} t + t_{\Delta} ),而不是简单的 ( t + t_{\Delta} )。因为增量平移是在增量旋转之后的坐标系里定义的。
4.4 结果验证与可视化
跑一下上面的代码,看看效果:
R_est, t_est, errors = icp_2d(source, target) print("真实旋转:\n", R_true) print("估计旋转:\n", R_est) print("真实平移:", t_true) print("估计平移:", t_est) # 可视化 source_final = (R_est @ source.T).T + t_est plt.figure(figsize=(8, 6)) plt.scatter(source[:, 0], source[:, 1], c='blue', label='源点云', alpha=0.5) plt.scatter(target[:, 0], target[:, 1], c='red', label='目标点云', alpha=0.5) plt.scatter(source_final[:, 0], source_final[:, 1], c='green', label='配准后源点云', alpha=0.5) plt.legend() plt.axis('equal') plt.title('二维ICP配准结果') plt.show()如果一切正常,你会看到绿色点云和红色点云基本重合,估计出来的旋转矩阵和平移向量也和真实值非常接近。
我实测下来,对于200个点、初始位置相差不大的情况,通常10到20次迭代就能收敛,误差可以降到噪声水平以下。
5. 常见问题与排查技巧实录
5.1 ICP不收敛或者收敛到错误结果
这是最常见的问题。原因通常有几个:
初始变换太差。ICP是局部优化算法,如果初始旋转和平移离真实值太远,最近点对应关系会大量出错,导致迭代方向错误。解决办法是先用粗配准。对于二维点云,可以用主成分分析(PCA)对齐主方向,或者用RANSAC基于特征点做粗配准。
两片点云重叠区域太小。如果源点云只有30%的部分能在目标点云里找到对应,那么大部分对应点都是错误的。解决办法是引入重叠率估计,或者使用Trimmed ICP,只保留距离最小的前一定比例的点对参与变换计算。
外点太多。噪声点、离群点会严重干扰最近点搜索。可以在找对应点之后,设置一个最大距离阈值,超过阈值的点对直接丢弃。或者使用鲁棒核函数,比如Huber核,降低外点的权重。
避坑技巧:我通常会在ICP之前先做一次下采样。把点云体素化,每个体素只保留一个点。这样既能减少点数、加速计算,又能平滑噪声。对于二维点云,可以简单地按网格划分,每个格子取平均。下采样之后再做ICP,收敛更稳定。
5.2 旋转方向反了或者平移符号错了
这通常是协方差矩阵 ( H ) 的定义顺序和旋转计算公式不匹配导致的。记住一个口诀:源乘目标转置,旋转等于 V 乘 U 转置。如果你写成目标乘源转置,那么旋转就是 U 乘 V 转置。两种写法都对,但必须前后一致。
另外,平移的更新顺序也容易搞错。增量平移 ( t_{\Delta} ) 是在增量旋转 ( R_{\Delta} ) 的坐标系下定义的,所以更新总平移时,要先对旧平移做增量旋转,再加上增量平移。
5.3 迭代次数很多但误差下降很慢
这可能是因为点云分布不均匀,或者对应点中存在大量“一对多”的情况。比如源点云里很多点都对应到目标点云的同一个点,这会导致协方差矩阵退化,求出来的旋转不稳定。
解决办法是对称化ICP。不仅从源点云找目标点云的最近点,也从目标点云找源点云的最近点,然后把两组对应关系合并起来算变换。这样可以平衡两片点云的贡献,收敛更快。
另一个技巧是点到面ICP。对于二维点云,可以把目标点云拟合成线段,然后最小化源点到这些线段的距离。这比点到点距离收敛更快,但实现起来复杂一些。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 点云越配越远 | 旋转方向反了 | 检查 ( H ) 定义和 ( R ) 公式是否匹配 | 统一写成源乘目标转置 |
| 迭代震荡不收敛 | 初始位置太差 | 打印每次迭代的误差 | 先做粗配准或下采样 |
| 误差降到一定程度不再下降 | 陷入局部最优 | 可视化对应点连线 | 增加随机扰动或换初值 |
| 配准后部分区域错位 | 重叠区域小或外点多 | 统计对应点距离分布 | 设置距离阈值或Trimmed ICP |
| 迭代次数过多 | 点云分布不均 | 检查对应点是否一对多 | 对称化ICP或下采样 |
6. 进阶话题:让ICP更稳更快的几个方向
6.1 从二维到三维:核心变化在哪里
二维ICP和三维ICP的框架完全一样,区别只在旋转的表示和SVD的维度。二维旋转只有一个自由度(角度),三维旋转有三个自由度(可以用旋转矩阵、四元数、欧拉角表示)。三维情况下,协方差矩阵 ( H ) 是 ( 3 \times 3 ),SVD分解后同样用 ( R = V U^T ) 求旋转,反射修正也一样。
但三维ICP的坑更多:点云规模更大,最近点搜索更慢;旋转的表示和插值更复杂;点云的法向量估计更困难。如果你搞懂了二维ICP,三维ICP的代码改动其实不大,主要是把2换成3,然后注意旋转矩阵的更新顺序。
6.2 用点到面距离加速收敛
点到点ICP的收敛速度是线性的,有时候需要几十次迭代。点到面ICP利用目标点云的局部表面信息,把源点到目标点的距离替换为源点到目标点切平面的距离。对于二维点云,切平面就是切线。这样每次迭代的步长更大,通常5到10次就能收敛。
实现点到面ICP的关键是估计目标点云的法向量。对于二维点云,可以用相邻点的连线方向来估计切线方向。然后最小化源点到切线的距离,这同样可以转化为一个最小二乘问题,但求解比点到点复杂一些。
6.3 处理部分重叠:Trimmed ICP
实际扫描中,两片点云往往只有部分重叠。如果强行让所有点都参与配准,错误对应会拖偏结果。Trimmed ICP的思路很简单:每次迭代时,只保留距离最小的前 ( k% ) 的对应点对,用它们来算变换。比如保留70%的点对,丢弃最远的30%。
这个比例需要根据实际重叠率来调。如果重叠率大概50%,那就保留50%到60%。比例太低会导致有效点太少,旋转估计不稳定;比例太高又起不到剔除外点的作用。我通常从80%开始试,逐步降低,观察误差曲线。
6.4 多分辨率ICP:先粗后精
这是我最常用的加速策略。先把两片点云下采样到很稀疏的程度(比如每10个点取一个),在这个粗糙尺度上跑ICP,快速得到一个大致正确的变换。然后逐步增加点云密度,用上一层的变换作为初值,继续精配准。这样既避免了陷入局部最优,又减少了总计算量。
具体流程:
- 对源点云和目标点云分别做体素下采样,得到三层不同分辨率的点云。
- 从最粗的一层开始跑ICP,迭代次数可以少一些。
- 把粗层的结果作为下一层的初值,继续跑ICP。
- 最后一层用原始点云,做精细配准。
实测下来,多分辨率ICP比直接在原始点云上跑,速度快3到5倍,而且收敛更稳定。
7. 一些个人体会和实用建议
写ICP代码这些年,踩过的坑比写过的代码还多。有几个经验我觉得值得分享。
第一,永远不要相信没有可视化的ICP结果。误差曲线下降不代表配准正确,可能只是点云被拉到了一个错误的局部极小值。每次跑完ICP,我都会把配准前后的点云画出来看一眼。如果看到点云被“拧”成了奇怪的形状,那肯定是出问题了。
第二,初始变换的重要性怎么强调都不为过。ICP本质上是一个精配准算法,它的能力边界就在初始位置附近。如果两片点云初始位置相差超过它们自身尺寸的一半,ICP基本没戏。这时候应该先上粗配准,哪怕粗配准的精度只有十几度,也能把ICP拉回正轨。
第三,参数没有万能值。最大迭代次数、收敛阈值、距离阈值、Trimmed比例,这些参数都要根据你的数据特点来调。点云噪声大,距离阈值就要放宽;点云重叠少,Trimmed比例就要降低。我通常会写一个简单的参数扫描脚本,把不同参数组合的误差曲线画在一起,选收敛最快、误差最低的那组。
第四,对称化ICP几乎总是比单向ICP好。多写几行代码,从两个方向找对应点,收敛稳定性和精度都有明显提升。尤其是当两片点云点数差异较大时,对称化能避免点数多的一方主导变换。
最后,如果你是在做SLAM或者里程计,ICP通常只是前端的一部分。帧间配准用ICP没问题,但累积误差需要后端优化来消除。这时候可以考虑把ICP的结果作为位姿图的边,用图优化来做全局一致。这个展开就太大了,以后有机会再聊。
二维点云ICP的原理推导,说到底就是“找对应点、算变换、迭代”这三件事。数学上最漂亮的地方在于,一旦对应关系确定,最优变换有闭式解,不需要梯度下降。这个性质让ICP既简单又高效。但工程上最麻烦的地方也在于对应关系——最近点不一定是正确对应,错误对应会把变换带偏。所以实际使用中,大部分精力都花在如何找更可靠的对应关系上。把这个想明白了,ICP就算真正入门了。