1. 从“配准”这件事说起:为什么ICP值得从头推一遍
做过三维重建、SLAM或者激光雷达相关项目的朋友,大概率都绕不开一个词——点云配准。简单说,你手上有两片点云,一片是源点云,一片是目标点云,它们来自不同视角、不同时刻,甚至不同传感器,但描述的是同一个场景或同一个物体。配准要干的事,就是找到一个刚体变换(旋转加平移),把源点云“搬”到目标点云的坐标系下,让两者尽可能重合。
这件事听起来直观,做起来却有不少门道。配准方法分两大类:粗配准和精配准。粗配准负责在完全不知道初始位置的情况下,把两片点云大致拉到一起,常见的有基于特征描述子的方法、基于RANSAC全局搜索的方法等。精配准则是在已经有一个还不错初始位姿的前提下,进一步优化,让配准误差降到最低。而ICP(Iterative Closest Point,迭代最近点),就是精配准里最经典、最常被拿出来讲、也最容易被低估的算法。
我之所以想专门把二维点云ICP的原理从头推一遍,是因为在实际项目里,我发现很多人对ICP的理解停留在“调库”层面——知道有个函数叫ICP,输入两片点云,输出一个变换矩阵,跑就完了。但一旦结果不对,就完全不知道从哪查起。是初始位姿太差?是点云噪声太大?是迭代次数不够?还是对应点匹配策略有问题?这些问题,如果你没有亲手推过一遍ICP的目标函数和求解过程,基本只能靠猜。
二维点云是理解ICP的最佳切入点。相比三维,二维少了一个维度的自由度,旋转只有一个角度参数,平移只有两个分量,数学表达足够简洁,但核心思想——交替进行对应点匹配和变换求解——和三维完全一致。把二维推明白了,三维就是多几个参数的事。这篇文章我会从目标函数写起,一步步推导闭式解,再落到实操细节和踩坑经验,尽量让刚入门的朋友也能跟上,同时给有经验的朋友补一些容易被忽略的细节。
适合读这篇内容的人:正在做点云配准相关项目、想搞懂ICP底层原理的开发者;学过线性代数但不知道怎么用到实际问题里的学生;以及那些调库调出问题、想自己手写一版ICP来排查的工程师。我会尽量把每一步的“为什么”讲清楚,而不是只丢公式。
2. ICP的整体设计思路:为什么是“迭代”加“最近点”
2.1 核心矛盾:对应关系未知,变换也未知
ICP要解决的问题,用一句话概括:给定两片点云,求一个刚体变换,使得变换后的源点云与目标点云之间的某种距离度量最小。
但这里有个死循环。如果我们知道每个源点在目标点云里的对应点是谁,那求变换就是一个标准的最小二乘问题,有闭式解,一步就能算出来。反过来,如果我们知道变换,那找对应点也很简单,把源点变换过去,每个点找目标点云里最近的那个就行。问题是,对应关系和变换都不知道。
ICP的破局思路非常朴素:先假设一个初始变换,然后交替优化。你给我一个初始位姿,我就按这个位姿去找对应点;有了对应点,我就求最优变换;有了新变换,我再重新找对应点……如此反复,直到收敛。这就是“迭代”二字的来源,也是ICP最核心的设计哲学。
这个思路本质上是一种坐标下降法或者说交替最小化。每次固定一个变量(对应关系或变换),优化另一个,保证目标函数单调不增。虽然不能保证收敛到全局最优,但在初始位姿合理的情况下,通常能收敛到一个不错的局部最优解。
2.2 为什么选“最近点”作为对应准则
对应点匹配的策略有很多种,ICP选了最简单粗暴的一种:最近邻。源点变换后,在目标点云里找欧氏距离最近的点作为对应点。
为什么是最近点?因为在没有其他先验信息的情况下,最近邻是几何上最自然的假设——两个点云如果配准得好,每个点离它对应的那个点应该就是最近的。这个假设在点云重叠度高、噪声小、初始位姿不太离谱的时候是成立的。
但最近邻也有明显的问题。如果点云密度不均匀,或者存在部分重叠(一片点云只覆盖了另一片的一部分),最近邻会产生大量错误对应。比如源点云边缘的点,在目标点云里可能根本找不到真正的对应点,最近的那个可能离得很远,这种错误对应会把变换“拉偏”。所以实际实现里,通常会加一个距离阈值,超过阈值的对应点直接丢弃,不参与变换求解。这个细节后面会展开讲。
2.3 目标函数长什么样
设源点云为 ( P = {p_1, p_2, ..., p_n} ),目标点云为 ( Q = {q_1, q_2, ..., q_m} ),我们要找的刚体变换为 ( T = (R, t) ),其中 ( R ) 是旋转矩阵,( t ) 是平移向量。在二维情况下,( R ) 可以写成:
[ R = \begin{bmatrix} \cos\theta & -\sin\theta \ \sin\theta & \cos\theta \end{bmatrix} ]
( t = [t_x, t_y]^T )。
ICP的目标函数是对应点对之间的欧氏距离平方和:
[ E(R, t) = \sum_{i=1}^{n} | R p_i + t - q_{c(i)} |^2 ]
其中 ( q_{c(i)} ) 是 ( p_i ) 在当前变换下的对应点。整个算法就是不断最小化这个 ( E )。
注意这里用的是平方和而不是距离和。平方和的好处是求导方便,有闭式解;而且对大距离的惩罚更重,能更快把偏差大的点拉回来。缺点是对离群点敏感,一个错误对应可能产生很大的误差项,影响整体求解。这也是为什么需要距离阈值和鲁棒核函数(比如Huber核)的原因。
2.4 算法流程的完整描述
把上面的思路串起来,ICP的完整流程是这样的:
- 初始化:给定源点云 ( P )、目标点云 ( Q )、初始变换 ( T_0 )(通常是单位变换或粗配准的结果)、最大迭代次数 ( N_{max} )、收敛阈值 ( \epsilon )、距离阈值 ( d_{max} )。
- 迭代开始,对于第 ( k ) 次迭代:
- 对应点匹配:用当前变换 ( T_{k-1} ) 把源点云变换到目标坐标系,对每个变换后的点,在 ( Q ) 中找最近点作为对应点。如果最近距离超过 ( d_{max} ),标记为无效对应。
- 变换求解:用所有有效对应点对,求解使目标函数最小的 ( R_k, t_k )。
- 应用变换:更新累积变换 ( T_k = (R_k, t_k) \circ T_{k-1} )。
- 收敛判断:如果目标函数的变化量小于 ( \epsilon ),或者达到最大迭代次数,停止;否则继续。
- 输出:最终变换 ( T ) 和配准后的点云。
这个流程看起来简单,但每一步都有讲究。对应点匹配用什么数据结构加速?变换求解怎么推闭式解?收敛条件怎么设才合理?这些细节决定了ICP是“能用”还是“好用”。
3. 核心推导:二维ICP闭式解是怎么来的
3.1 先简化:去中心化把平移分离出去
直接对 ( E(R, t) ) 求导,旋转和平移耦合在一起,不太好处理。标准做法是先去中心化,把平移量分离出来。
设源点云和目标点云的质心分别为:
[ \bar{p} = \frac{1}{n} \sum_{i=1}^{n} p_i, \quad \bar{q} = \frac{1}{n} \sum_{i=1}^{n} q_{c(i)} ]
注意这里目标点云的质心是对对应点求的,不是对整个目标点云求的,因为参与误差计算的是对应点对。
令 ( p_i' = p_i - \bar{p} ),( q_i' = q_{c(i)} - \bar{q} ),代入目标函数:
[ E = \sum_{i=1}^{n} | R(p_i' + \bar{p}) + t - (q_i' + \bar{q}) |^2 ]
展开后可以写成:
[ E = \sum_{i=1}^{n} | R p_i' - q_i' |^2 + n | R\bar{p} + t - \bar{q} |^2 ]
这个分解很关键。第一项只和旋转 ( R ) 有关,第二项只和平移 ( t ) 有关。所以我们可以先求旋转,再求平移。
对于平移,显然当 ( R\bar{p} + t - \bar{q} = 0 ) 时第二项最小,即:
[ t = \bar{q} - R\bar{p} ]
这个结果的几何意义很直观:最优平移就是把源点云的质心变换后对齐到目标对应点云的质心。
3.2 旋转的求解:转化为最大化迹的问题
现在只剩旋转需要求。目标函数简化为:
[ E(R) = \sum_{i=1}^{n} | R p_i' - q_i' |^2 ]
展开:
[ E(R) = \sum_{i=1}^{n} (p_i'^T p_i' + q_i'^T q_i' - 2 q_i'^T R p_i') ]
前两项和 ( R ) 无关,所以最小化 ( E(R) ) 等价于最大化:
[ \sum_{i=1}^{n} q_i'^T R p_i' ]
利用迹的性质 ( q_i'^T R p_i' = \text{tr}(R p_i' q_i'^T) ),求和后:
[ \sum_{i=1}^{n} q_i'^T R p_i' = \text{tr}\left( R \sum_{i=1}^{n} p_i' q_i'^T \right) ]
令 ( H = \sum_{i=1}^{n} p_i' q_i'^T ),这是一个 ( 2 \times 2 ) 的矩阵,称为协方差矩阵或互相关矩阵。问题变成:
[ \max_R \text{tr}(R H) ]
3.3 二维情况下的显式解
二维旋转矩阵只有一个自由度 ( \theta ),我们可以直接把 ( R ) 写开:
[ R H = \begin{bmatrix} \cos\theta & -\sin\theta \ \sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} h_{11} & h_{12} \ h_{21} & h_{22} \end{bmatrix} ]
迹为:
[ \text{tr}(RH) = \cos\theta (h_{11} + h_{22}) + \sin\theta (h_{21} - h_{12}) ]
令 ( a = h_{11} + h_{22} ),( b = h_{21} - h_{12} ),则:
[ \text{tr}(RH) = a\cos\theta + b\sin\theta ]
对 ( \theta ) 求导并令其为零:
[ -a\sin\theta + b\cos\theta = 0 \Rightarrow \tan\theta = \frac{b}{a} ]
所以最优旋转角为:
[ \theta = \text{atan2}(b, a) ]
其中 ( \text{atan2} ) 是四象限反正切,能正确处理 ( a ) 为零或负的情况。这个结果非常干净:二维ICP的旋转角可以直接用协方差矩阵的元素算出来,不需要做SVD分解。
三维情况下就没这么幸运了,需要用SVD或者四元数方法求解,但核心思想是一样的——都是最大化 ( \text{tr}(RH) )。
3.4 完整求解步骤汇总
把上面的推导整理成可执行的步骤:
- 计算源点云质心 ( \bar{p} ) 和对应点云质心 ( \bar{q} )。
- 去中心化:( p_i' = p_i - \bar{p} ),( q_i' = q_{c(i)} - \bar{q} )。
- 计算协方差矩阵 ( H = \sum p_i' q_i'^T )。
- 计算 ( a = h_{11} + h_{22} ),( b = h_{21} - h_{12} )。
- 旋转角 ( \theta = \text{atan2}(b, a) )。
- 平移 ( t = \bar{q} - R\bar{p} )。
这六步就是二维ICP单次迭代的全部核心计算。代码实现起来也就十几行,但背后的推导值得反复琢磨。
注意:这里假设所有对应点对权重相同。如果加了距离阈值或鲁棒核,需要在求和时引入权重,质心和协方差矩阵都要用加权形式计算。
4. 实操落地:从伪代码到可运行实现
4.1 数据结构与最近邻搜索
对应点匹配是ICP里最耗时的环节。朴素做法是双重循环,对每个源点遍历所有目标点,复杂度 ( O(nm) )。点云规模小的时候无所谓,但上万点的点云就会明显卡顿。
实际实现里,通常用KD-Tree来加速最近邻查询。KD-Tree 把目标点云组织成二叉树结构,查询复杂度降到 ( O(\log m) )。Python 里可以用scipy.spatial.cKDTree,C++ 里可以用 PCL 或者 nanoflann。如果点云是规则网格排列的,还可以用空间哈希或者体素栅格,查询更快。
我自己的习惯是:点云点数少于 1000 时直接用暴力搜索,代码简单不容易出错;超过 1000 就上 KD-Tree。KD-Tree 的构建本身有开销,如果只跑几次迭代,可能还不如暴力搜索快,这个要按实际情况权衡。
4.2 距离阈值怎么设
距离阈值 ( d_{max} ) 是ICP里最重要的参数之一。设得太小,有效对应点太少,变换求解不稳定;设得太大,错误对应混进来,结果被拉偏。
一个经验法则是:先看初始配准后对应点距离的分布,取中位数或者某个分位数作为阈值。比如初始变换后,所有最近邻距离的中位数是 0.5,那阈值可以设成 1.0 到 1.5 左右,保留大部分正确对应,同时过滤掉明显离谱的。
另一个做法是动态阈值:每次迭代后,根据当前对应点距离的统计量调整阈值。比如取距离的均值加两倍标准差。这样前期阈值大一些,允许更多点参与;后期阈值收紧,提高精度。
实操心得:如果两片点云重叠区域很小(比如只有 30% 重叠),固定阈值很难兼顾。这时候可以考虑用Trimmed ICP,每次只保留距离最小的前 ( k% ) 的对应点,比如 70%,剩下的直接丢弃。这个方法对部分重叠场景非常有效,实现也简单,就是排序后截断。
4.3 收敛条件的设置
收敛判断通常看两个指标:目标函数变化量和变换增量。
目标函数变化量:如果连续两次迭代的 ( E ) 差值小于某个阈值(比如 ( 10^{-6} )),认为收敛。这个指标直接反映优化目标,比较可靠。
变换增量:如果旋转角变化小于某个弧度值(比如 ( 10^{-4} )),平移变化小于某个距离值,认为收敛。这个指标更直观,但需要手动设阈值。
我一般两个都用,满足任一就停。同时设一个最大迭代次数(通常 50 到 100),防止死循环。实际跑下来,大部分情况 20 到 30 次迭代就收敛了,超过 50 次还没收敛,多半是初始位姿太差或者对应点匹配出了问题。
4.4 一份可参考的Python实现
下面是一份二维ICP的核心实现,去掉了可视化部分,只保留算法逻辑。依赖numpy和scipy。
import numpy as np from scipy.spatial import cKDTree def icp_2d(source, target, T_init=None, max_iter=50, tol=1e-6, dist_thresh=1.0): """ 二维ICP配准 source: (n, 2) 源点云 target: (m, 2) 目标点云 T_init: (3, 3) 初始变换矩阵,默认单位阵 返回: T (3, 3) 累积变换, errors (list) 每次迭代误差 """ if T_init is None: T = np.eye(3) else: T = T_init.copy() tree = cKDTree(target) errors = [] prev_error = np.inf for i in range(max_iter): # 变换源点云 src_homo = np.hstack([source, np.ones((len(source), 1))]) src_trans = (T @ src_homo.T).T[:, :2] # 最近邻搜索 dists, indices = tree.query(src_trans) # 距离阈值过滤 mask = dists < dist_thresh if mask.sum() < 3: print("有效对应点不足,停止迭代") break src_valid = source[mask] tgt_valid = target[indices[mask]] # 计算质心 src_mean = src_valid.mean(axis=0) tgt_mean = tgt_valid.mean(axis=0) # 去中心化 src_centered = src_valid - src_mean tgt_centered = tgt_valid - tgt_mean # 协方差矩阵 H = src_centered.T @ tgt_centered # 计算旋转角 a = H[0, 0] + H[1, 1] b = H[1, 0] - H[0, 1] theta = np.arctan2(b, a) # 旋转矩阵 R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) # 平移向量 t = tgt_mean - R @ src_mean # 构造本次迭代的变换 T_delta = np.eye(3) T_delta[:2, :2] = R T_delta[:2, 2] = t # 累积变换 T = T_delta @ T # 计算误差 error = np.mean(dists[mask]) errors.append(error) # 收敛判断 if abs(prev_error - error) < tol: break prev_error = error return T, errors这段代码有几个细节值得说明。第一,H = src_centered.T @ tgt_centered算出来的矩阵,和前面推导里的 ( H = \sum p_i' q_i'^T ) 是一致的,只是用矩阵乘法一次性算完。第二,theta = np.arctan2(b, a)里的 ( a ) 和 ( b ) 对应推导中的定义,注意 ( b = h_{21} - h_{12} ),不要搞反符号。第三,误差用的是有效对应点的平均距离,不是平方和,这样更直观,也方便观察收敛趋势。
4.5 初始变换的重要性
ICP是局部优化算法,初始变换决定了它能不能收敛到正确的解。如果初始旋转偏差超过 90 度,或者平移偏差超过点云尺度的一半,基本必挂。
实际项目里,初始变换通常来自几个渠道:粗配准的结果、里程计的预测、上一帧的位姿、或者人工给定的粗略值。如果完全没有先验,可以考虑先做主成分分析(PCA)对齐:分别计算两片点云的主轴方向,把主轴对齐作为初始旋转,质心对齐作为初始平移。这个方法对形状比较“长条”的点云效果不错,但对近似圆形的点云就不太靠谱。
踩坑记录:我曾经在一个项目里偷懒,直接用单位变换作为初始,结果两片点云初始夹角大概 120 度,ICP跑完收敛到了一个完全错误的局部最优,两片点云交叉成了一个“X”形。后来加了 PCA 粗对齐,问题解决。所以千万别省粗配准这一步。
5. 常见问题与排查技巧实录
5.1 配准结果“歪了”或者“转多了”
这是最常见的问题,表现是最终变换明显不对,点云没有对齐。原因通常有三个:初始位姿太差、对应点匹配错误太多、或者点云本身有问题。
排查顺序建议这样:先可视化初始变换后的点云,看看大概差多少。如果差得离谱,先解决初始位姿问题。如果初始看起来还行,但跑完反而更歪了,那多半是对应点匹配出了问题。检查距离阈值是不是设得太大,导致大量错误对应参与求解。可以打印每次迭代的有效对应点数量和平均距离,观察变化趋势。
另一个容易被忽略的点是点云尺度。如果两片点云尺度不一致(比如一个单位是米,一个是毫米),ICP是没法处理的,因为刚体变换不改变尺度。这种情况需要先做尺度归一化,或者用带尺度的配准方法。
5.2 迭代不收敛或者震荡
震荡的表现是误差在两次迭代之间来回跳,不下降。原因可能是对应点在两个位置之间反复切换。
一个典型的场景是点云对称性很强,比如一个正方形点云,旋转 90 度后和自己重合。这种情况下,对应点匹配会有歧义,ICP可能在几个等价解之间跳。解决办法是打破对称性,比如加一点随机扰动,或者用点到面的距离代替点到点的距离。
另一个原因是距离阈值设得刚好卡在某个临界值,导致每次迭代有效对应点集合变化很大。可以尝试把阈值稍微调大或调小,观察是否稳定。
5.3 部分重叠场景下的处理
实际扫描的点云很少是完全重叠的。比如两帧激光雷达数据,只有中间一部分是共同区域,边缘各看各的。这种情况下,标准ICP会把边缘的非重叠点也强行找对应,导致变换被拉偏。
处理部分重叠,有几个实用策略:
- Trimmed ICP:每次只保留距离最小的前 ( k% ) 对应点,比如 60% 到 80%。这个方法简单有效,推荐优先尝试。
- 距离阈值加动态调整:初始阈值设大一点,随着迭代逐渐收紧,让非重叠点在后期被过滤掉。
- 基于法向量的一致性检验:如果点云有法向量信息,可以检查对应点的法向量夹角,夹角太大的对应点丢弃。二维情况下法向量就是切线方向,计算也简单。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 结果完全错误 | 初始位姿太差 | 可视化初始变换后的点云 | 加粗配准或PCA对齐 |
| 结果轻微偏移 | 距离阈值过大 | 打印有效对应点平均距离 | 减小阈值或加Trimmed策略 |
| 迭代震荡不收敛 | 对应点反复切换 | 观察误差曲线 | 调整阈值或加扰动 |
| 部分区域对不齐 | 点云部分重叠 | 检查重叠区域比例 | 用Trimmed ICP或动态阈值 |
| 运行速度慢 | 暴力搜索最近邻 | 计时各环节耗时 | 换KD-Tree或体素加速 |
| 对噪声敏感 | 离群点影响求解 | 检查点云噪声水平 | 加鲁棒核或预滤波 |
5.5 几个容易被忽略的细节
质心计算的权重问题。如果用了距离阈值,参与求解的对应点数量每次迭代都在变,质心也会变。这是正常的,但要注意每次都要重新计算,不能用上一轮的质心。
旋转角的周期性。atan2返回的角度在 ( [-\pi, \pi] ) 之间,如果实际旋转超过这个范围,需要额外处理。不过ICP每次迭代的旋转增量通常很小,不太会遇到这个问题。
点云顺序无关性。ICP的结果应该和点云的排列顺序无关,因为目标函数是对所有点求和。如果你发现打乱点云顺序后结果变了,那多半是代码里有隐藏的索引错误。
数值精度。二维情况下,协方差矩阵的元素可能很小,atan2对数值精度比较敏感。如果点云坐标值很大(比如地理坐标),建议先做归一化,把坐标缩放到 0 附近,算完再变换回去。
6. 从二维到三维:扩展思路与性能优化方向
6.1 三维ICP的差异点
二维推明白了,三维的框架完全一样,差异主要在旋转的表示和求解上。三维旋转有三个自由度,不能用单个角度表示。常见的有旋转矩阵(9个元素,带正交约束)、欧拉角(有万向锁问题)、四元数(4个元素,无奇异性)、轴角(3个元素)。
求解三维ICP的旋转,标准做法是SVD分解。计算协方差矩阵 ( H = \sum p_i' q_i'^T )(现在是 ( 3 \times 3 )),做 SVD 分解 ( H = U \Sigma V^T ),最优旋转为 ( R = V U^T )。需要注意行列式符号,如果 ( \det(VU^T) = -1 ),要把 ( V ) 的最后一列取反。
三维ICP的对应点搜索也更耗时,KD-Tree 的维度从 2 变成 3,查询效率会下降一些,但整体还是可用的。
6.2 点到面ICP的改进
标准ICP用的是点到点距离,收敛速度较慢,尤其是平面区域多的场景。点到面ICP(Point-to-Plane ICP)用目标点的法向量构造局部平面,最小化源点到这个平面的距离。目标函数变成:
[ E = \sum_{i} (n_i^T (R p_i + t - q_i))^2 ]
其中 ( n_i ) 是目标点 ( q_i ) 的法向量。这个目标函数对平面区域更敏感,收敛更快,精度也更高。代价是需要计算法向量,而且求解不再是简单的闭式解,需要线性化后迭代求解。
二维情况下,点到面ICP就是点到线的距离,法向量就是线的法方向。实现起来比三维简单,效果提升也很明显,推荐在二维项目里优先考虑。
6.3 加速策略汇总
ICP的性能瓶颈主要在最近邻搜索和迭代次数上。几个实用的加速手段:
- 降采样:配准前先把点云体素降采样,点数减少到原来的 10% 到 20%,精度损失很小,速度提升明显。可以先在降采样点云上跑ICP得到粗略变换,再用原始点云精配准。
- 多分辨率策略:从粗尺度到细尺度逐层配准,每层用上一层的输出作为初始。这个方法对初始位姿的容忍度更高,收敛也更稳。
- 并行化:最近邻搜索可以并行,协方差矩阵计算也可以并行。Python 里可以用多进程或者 numba 加速,C++ 里用 OpenMP。
- 提前终止:如果误差已经很小,或者变换增量已经可以忽略,没必要跑满最大迭代次数。
6.4 一个实际项目的参数配置参考
我在一个二维激光扫描配准项目里用过这样一组参数,场景是室内环境,点云规模每帧约 2000 点,重叠区域约 70%:
| 参数 | 取值 | 说明 |
|---|---|---|
| 最大迭代次数 | 50 | 实际通常 15-25 次收敛 |
| 收敛阈值 | 1e-6 | 误差变化量 |
| 距离阈值 | 0.3m | 约为点云尺度的 5% |
| 降采样体素 | 0.05m | 原始点云先降采样 |
| 对应点保留比例 | 80% | Trimmed ICP |
| 初始变换 | 上一帧位姿 | 帧间配准 |
这组参数在测试集上平均配准误差约 0.02m,单帧耗时约 30ms(Python 实现,含 KD-Tree 构建)。如果换成 C++ 实现,可以降到 5ms 以内。
最后分享一个小技巧:如果你的点云是从传感器连续采集的,帧间位姿变化通常很小,直接用上一帧的配准结果作为下一帧的初始变换,ICP基本一两次迭代就收敛了。这种增量式配准策略在实时系统里非常常用,比每帧都从头配准快得多,也更稳定。