PointNetLK点云配准实战:从原理到ModelNet40训练全解析
去年夏天做一批结构件扫描点云的对齐初始角度偏差只要超过15度ICP就各种发散试了PCA初始化、试了粗配准加精配准的组合最后还是被队友推荐换上了PointNetLK才把问题解决。那会儿我第一反应是点云配准这么成熟的领域怎么就轮到一个基于深度学习的特征方法后来把论文和开源代码翻了一遍又用ModelNet40完整跑了一轮训练和测试才算真正搞明白PointNetLK这支方法的边界和脾气。这篇东西就从一个实际使用者的角度出发把PointNetLK的落地路径拆成5步环境准备、ModelNet40数据预处理、特征提取器实现、LK配准模块实现、训练与评估每一步都附上我踩过坑之后的版本顺便把ModelNet40上的训练技巧一起写了适合刚接触深度点云配准、或者被ICP折腾得够呛的人参考。1. ICP搞不定的场景PointNetLK为什么能搞定1.1 一次实测翻车初始位姿偏了20度就发散先讲个真实场景。当时我手里有两片结构件扫描点云一片是CAD导出的模板一片是实际扫描件目标是把扫描件对齐到模板上做偏差检测。点云大概每片12万个点表面是各种自由曲面人工特征很少只有几个圆孔边缘能勉强当特征。我用CloudCompare里的标准ICP跑了一遍把初始旋转设在20度附近结果迭代到第30次左右就陷入了明显的局部极小值两个曲面错开了大概一个壁厚但对应点最近邻距离已经不再明显下降。我又试了重新旋转初始化、加大迭代次数、调整最大对应距离效果都很有限。原因其实不复杂ICP的目标函数是欧氏空间最近邻距离的平方和这玩意儿在特征稀少的自由曲面上到处都是局部极小值初始位姿稍微差一点就会掉坑。后来换了FPFH特征加RANSAC做粗配准效果比纯ICP强一些但两个问题很突出一是光滑曲面上FPFH的描述子区分度很低错匹配率能到三成以上二是每对点做特征直方图计算、匹配再加RANSAC采样耗时基本是秒级起步数据一多就扛不住。1.2 PointNetLK的原理把对齐从空间域挪到特征域PointNetLK的核心思路是换个赛道做配准。传统ICP直接对着原始点坐标搞最近邻搜索而PointNetLK先用PointNet把点云编码成一个1024维的全局特征向量然后用Lucas-Kanade算法在这个特征空间里求解旋转和平移让模板点云的特征与经过变换后的源点云特征尽可能一致。用大白话讲ICP是拿两坨点云的坐标逐点找邻居硬凑PointNetLK则是给每坨点云拍一张证件照全局特征然后通过不断微调外部参数让两张证件照尽量重合。因为全局特征是由整个点云形状决定的它对局部表面噪声、点密度变化和少量离群点都不太敏感所以这个特征空间里的优化曲面比原始坐标空间平滑不少收敛半径因此大很多这是它对初始位姿不那么敏感的根本原因。从数学上讲PointNetLK把配准问题写成一个最小二乘形式让φ(模板)减去φ(T(ξ)·源)的差尽量小然后绕SE(3)参数空间做高斯牛顿迭代。具体实现用的是inverse compositional策略在模板特征处预计算雅可比矩阵每次迭代只需要把源点云按当前位姿变换一次重算特征剩下就是一串矩阵乘法和一个线性方程组求解效率比每一步都重新算雅可比高很多。1.3 这套方法适合谁、不适合谁根据我后续在公开数据集和实际扫描数据上的测试这套方法适配的场景大致是适合两片点云重叠度较高七成以上、初始位姿误差在45度以内、表面几何特征不明显、需要批量自动化处理。不太适合重叠度极低的拼接任务、跨传感器异构数据、以及对实时性要求达到毫秒级的场景。拿地形点云配准这类任务来说如果两片地形数据有较大重叠PointNetLK做个初始配准再交给ICP精配是可行的但如果重叠率很低全局特征因为缺失大量信息会变得不稳定效果就会明显下降。这个边界最好在选型之前就清楚否则容易满怀希望换模型,结果比ICP还惨。2. 第1步环境准备——版本搭配比想象中更容易翻车2.1 我的依赖清单与版本建议整个项目我建议直接用一个干净的conda环境避免和已有项目互相污染。核心依赖如下conda create -n pointnetlk python3.9 -y conda activate pointnetlk pip install torch2.0.1cu118 torchvision --index-url https://download.pytorch.org/whl/cu118 pip install open3d0.17.0 pip install trimesh4.0.5 pip install numpy scipy tqdm tensorboard版本搭配上我有一句忠告PyTorch的版本和CUDA的版本一定要对齐否则装完之后运行import torch不报错但一放到GPU上就提示算子找不到。我一开始图省事装了最新的torch 2.1配的还是老掉牙的cu102结果折腾了一晚上最后直接按上面的组合重装二十分钟就解决。open3d和trimesh是用来做点云可视化和网格采样的。Open3D自带高效的KDTree和ICP实现方便后期做对比trimesh则是读取ModelNet40的OFF文件、做网格表面均匀采样最顺手的库。如果你只做训练和精度评估其实不装open3d也行但强烈建议装上因为调试配准效果的时候肉眼看点云叠合情况比任何指标都直观。2.2 一个容易忽略的小习惯先做可视化验证数据准备阶段我会随手写一段脚本把采样后的点云用open3d显示出来import open3d as o3d import numpy as np def show_points(pts, color[0.5, 0.5, 0.5]): pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(pts) pcd.paint_uniform_color(color) o3d.visualization.draw_geometries([pcd]) # 查看采样后的点云 pts load_points(modelnet40_ply/chair/train/chair_0001.txt) show_points(pts)别小看这一步。ModelNet40原始数据是CAD网格模型用不同工具读、不同参数采样出来的点云分布差异很大如果不先看一眼就灌进网络后面出了问题你根本不知道是数据的问题还是网络的问题。2.3 环境搭建中最容易中招的三个坑OFF文件解析失败trimesh在读取部分OFF文件时可能因为面表里包含颜色信息而报错。遇到这种情况加processFalse参数跳过自动处理或者先离线把OFF转成ply/xyz再读取。BatchNorm的坑PointNet主干里有多个BatchNorm层训练时开train模式正常算batch统计量但验证和测试时一定要切到eval模式否则特征分布不一致配准效果会明显波动。这个问题很多人会忽略因为它不像loss爆炸那么明显但精度就是上不去。随机种子PyTorch的数据加载、CUDA算子都存在随机性复现实验时记得设置随机种子否则你调了半天参数最后发现效果差异是随机性造成的白白浪费时间。3. 第2步ModelNet40数据集预处理3.1 下载与OFF文件解析的细节ModelNet40是Princeton提供的CAD模型数据集40个常见物体类别一共12311个CAD模型其中9843个作为训练集2468个作为测试集。原始文件有OFF格式和PLY格式两个版本PointNetLK的原作者用的是OFF版本。下载解压后的目录结构大概是ModelNet40/ ├── airplane/ │ ├── airplane_0001.off │ ├── airplane_0002.off │ ├── train/ │ └── test/ ├── bathtub/ ├── bed/ └── ...注意一点网上很多教程会把ModelNet40的OFF文件直接当作点云读入这是不对的。OFF文件存的是三角网格顶点三角面片不是点云直接读顶点会导致点分布极不均匀密集三角区域一堆点、稀疏区域几乎没有点。正确做法是从网格表面均匀采样出固定数量的点。import trimesh import numpy as np def sample_off_to_points(path, num_points1024): mesh trimesh.load(path, processFalse) # 从三角网格表面按面积均匀采样 points, face_idx trimesh.sample.sample_surface(mesh, num_points) return points.astype(np.float32)trimesh.sample.sample_surface会按照三角形面片的面积占比做重要性采样这样不管网格拓扑怎么变采出来的点云在表面上是均匀分布的这是后面所有训练和评估的基础。3.2 采样数量与归一化的选择采样数量方面PointNetLK原论文用的是1024点这也是很多点云深度模型的默认配置。我在实际实验里试过2048点和4096点特征表达确实更丰富配准精度略微提升但训练显存占用和推理耗时同步上升了不少。对于ModelNet40上的对比实验1024点是性价比最高的选择还能直接和论文结果对齐。采样完成后必须做归一化否则模型学不动。ModelNet40里不同类别的CAD模型原始尺寸相差很大有的一米见方有的只有十几厘米直接丢进网络尺度差异会把特征空间搞乱。我的归一化策略和原论文保持一致先减去质心再除以所有点到质心的最大距离把点云约束在单位球范围内。def normalize_unit_sphere(pts): centroid np.mean(pts, axis0) pts pts - centroid max_dist np.max(np.linalg.norm(pts, axis1)) pts pts / max_dist return pts.astype(np.float32)顺带提一句很多复现项目会在归一化时除以标准差而不是最大距离这两种做法结果差别不小。除以最大距离可以把整个点云稳定地压到单位球内这对LK迭代时的旋转参数初始化比较友好除以标准差则保留了更多尺度信息但不同样本间的尺度变化会给训练引入额外噪声。我建议跑PointNetLK时就用单位球归一化。3.3 在线生成配准对训练数据应该长什么样配准网络训练时需要的是成对样本而不只是一个点云。每次迭代我从训练集随机挑一个点云作为模板然后随机生成一个旋转矩阵和平移向量把模板变换一次得到源点云所以网络的输入是(源点云, 模板点云)监督信号是生成时用到的旋转和平移。旋转矩阵的生成方式有讲究。我用的轴角方式先随机采样一个单位轴再随机采样一个旋转角度用Rodrigues公式转成旋转矩阵。def random_rotation(angle_range_deg45.0): axis np.random.randn(3) axis axis / np.linalg.norm(axis) angle np.random.uniform(0, np.radians(angle_range_deg)) K np.array([[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]], dtypenp.float32) R np.eye(3) np.sin(angle) * K (1 - np.cos(angle)) * (K K) return R平移向量则在[-0.5, 0.5]区间均匀采样因为点云已经归一化到单位球内这个量级的平移已经算不小的扰动了。训练时我还会给源点云加一点点高斯噪声标准差取0.01左右。这个做法的实际作用是模拟扫描噪声让网络学会忽略局部几何扰动而不是去死记点云的精确坐标。不加噪声的网络在无噪声测试数据上表现尚可一遇到真实扫描数据就露馅鲁棒性差距非常明显。4. 第3~4步PointNet特征提取器与LK迭代模块实现4.1 特征提取器PointNet去掉分类头PointNetLK的特征提取器沿用PointNet的全局特征分支结构上就是几个共享的一维卷积加BatchNorm层最后通过最大池化聚合全局特征。原论文用的是修改过的PointNet结构输出1024维特征向量。我直接照着分类网络的encoder部分实现代码非常简洁import torch import torch.nn as nn import torch.nn.functional as F class PointNetFeat(nn.Module): def __init__(self, feat_dim1024): super().__init__() self.conv1 nn.Conv1d(3, 64, 1) self.conv2 nn.Conv1d(64, 128, 1) self.conv3 nn.Conv1d(128, feat_dim, 1) self.bn1 nn.BatchNorm1d(64) self.bn2 nn.BatchNorm1d(128) self.bn3 nn.BatchNorm1d(feat_dim) def forward(self, x): # x: [B, N, 3] x x.transpose(2, 1) # [B, 3, N] x F.relu(self.bn1(self.conv1(x))) x F.relu(self.bn2(self.conv2(x))) x self.bn3(self.conv3(x)) x torch.max(x, dim2)[0] # [B, feat_dim] return x这里有个小细节值得展开这个特征提取器在训练开始时是随机初始化的不需要单独预训练一个分类任务再拿来做配准。原论文的做法是端到端训练特征提取器会在训练过程中自己演化出适合配准任务的空间表达。我一开始还担心刚从随机权重出来的特征空间会不会乱到LK根本迭代不动实测下来只要学习率别太大收敛是没问题的只是前几百个batch的loss下降比预训练版本慢一些。4.2 LK配准核心迭代求解SE(3)增量LK的核心是循环迭代。每一轮迭代都做三件事按当前估计的李代数参数把源点云变换一次、计算模板特征和变换后源特征的残差、根据预计算的雅可比求解增量参数并更新。旋转和平移需要表示成一个6维向量3维平移3维旋转向量这样才能做梯度下降和线性化。每次迭代我通过se(3)指数映射把这个6维向量变成4x4变换矩阵再对源点云做刚体变换def se3_exp(xi): xi: [B, 6] - T: [B, 4, 4] t xi[:, :3] rvec xi[:, 3:] theta torch.norm(rvec, dim1, keepdimTrue) 1e-8 axis rvec / theta # Rodrigues公式 K torch.zeros(xi.size(0), 3, 3, devicexi.device) K[:, 0, 1] -axis[:, 2] K[:, 0, 2] axis[:, 1] K[:, 1, 0] axis[:, 2] K[:, 1, 2] -axis[:, 0] K[:, 2, 0] -axis[:, 1] K[:, 2, 1] axis[:, 0] I torch.eye(3, devicexi.device).unsqueeze(0) R I torch.sin(theta).unsqueeze(2) * K (1 - torch.cos(theta)).unsqueeze(2) * (K K) T torch.zeros(xi.size(0), 4, 4, devicexi.device) T[:, :3, :3] R T[:, :3, 3] t T[:, 3, 3] 1.0 return T完整迭代循环的伪代码如下def pointnetlk_solve(encoder, src_pts, tgt_pts, max_iter10, tol1e-6): # src_pts / tgt_pts: [B, N, 3] xi torch.zeros(src_pts.size(0), 6, devicesrc_pts.device) feat_tgt encoder(tgt_pts) # 模板特征只算一次 for i in range(max_iter): T se3_exp(xi) warped transform_points(src_pts, T) # 源点云按当前位姿变换 feat_src encoder(warped) # 当前特征 residual feat_tgt - feat_src # [B, C] # 雅可比矩阵的数值近似从论文源码里的做法改编 J numerical_jacobian(encoder, src_pts, warped, xi) # [B, C, 6] H J.transpose(1, 2) J 1e-6 * torch.eye(6, devicexi.device) delta torch.linalg.solve(H, J.transpose(1, 2) residual.unsqueeze(-1)) xi xi delta.squeeze(-1) if torch.norm(delta) tol: break return se3_exp(xi)4.3 Inverse Compositional到底省在哪这里有个关键点值得单独说为什么要用inverse compositional而不是直接对源点云求导。直接做的话源点云每变换一次雅可比都需要重新计算也就是encoder要前向传播两次以上。而inverse compositional把优化视角换了一下不是在源点云的当前位姿处线性化而是在模板点云的初始位姿处线性化。因为模板每次迭代都不变雅可比只需要在训练开始前算一次后面全是复用。对应到代码上我会在进入迭代循环之前把模板的雅可比算好# 建立模板的点云、初始化位姿为单位阵 # 对xi数值求导得到 [B, C, 6] J_template numerical_jacobian(encoder, tgt_pts, identity_pose)然后每次迭代残差用法不一样的地方在于最后更新时需要做一次合成操作实际就是把增量变换施加到当前累计估计上。这个操作本质上是矩阵乘法的问题但实现时建议直接维护一个4x4变换矩阵而不是对6维向量做加法训练更稳定。如果你只希望快速跑通流程可以直接把数值雅可比放在迭代内部每步重算代码更简单只是慢一些。我最初就是这么干的ModelNet40上训练一轮要整整6个小时改成inverse compositional并且预计算雅可比之后直接缩到2.5小时左右效果一点没打折扣。省出来的时间都花在调参上了这是整个实现里最值得做的一个优化。4.4 关于雅可比的一个实现提醒数值雅可比的实现方式有很多种我参考了原论文开源仓库里的写法对6维参数的每个维度做一个微小扰动扰动量为1e-2量级然后计算特征变化量。扰动步长既不能太大也不能太小太大线性近似失效太小浮点精度会产生噪声。我实测下来0.01左右的扰动步长在ModelNet40上表现比较稳。如果你追求更干净的梯度可以考虑把se3_exp和transform_points写成全可微形式用PyTorch的autograd自动求特征对xi的梯度。这样代码量会少一些但反向传播需要穿过多层卷积显存占用会显著上升。我在训练时没有采用这个方案因为数值雅可比在一轮训练里只算一次不会成为性能瓶颈。5. 第5步训练循环、损失函数与评估指标5.1 训练循环怎么搭训练核心逻辑其实很短每次迭代构造一个batch的训练对把源点云和模板点云丢进PointNetLK模块得到预测变换矩阵和真实变换算loss反向传播更新参数。model PointNetLKModel().cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.5) for epoch in range(100): for batch in train_loader: src_pts, tgt_pts, R_gt, t_gt [x.cuda() for x in batch] R_pred, t_pred model(src_pts, tgt_pts) loss_feat feature_loss(model, src_pts, tgt_pts, R_gt, t_gt) loss_pose pose_loss(R_pred, t_pred, R_gt, t_gt) loss loss_feat 0.01 * loss_pose optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()注意这个循环里两个loss的作用不一样。feature_loss是通过encoder提取特征用真实变换参数去重投影源点云让变换后的源特征与模板特征尽量接近它直接影响LK迭代过程中的残差定义。pose_loss则是直接约束预测的旋转平移和真值之间的差距像一个引导信号让网络参数不至于跑偏。5.2 损失函数怎么设计特征空间为主位姿监督为辅原论文的loss分成两块第一块是特征匹配损失第二块是变换矩阵的Frobenius范数损失。我在复现时沿用了这个思路但对第二块做了点微调。特征匹配损失的形式是def feature_loss(model, src_pts, tgt_pts, R_gt, t_gt): # 用真值位姿变换源点云希望变换后的源特征和模板特征尽量一致 src_warped transform_points(src_pts, R_gt, t_gt) feat_src model.encoder(src_warped) feat_tgt model.encoder(tgt_pts) return F.mse_loss(feat_src, feat_tgt)这个loss是主loss它告诉网络什么样的特征空间对配准最有利。如果只用位姿loss而不加特征loss理论上也能训但实验发现收敛速度明显更慢而且特征空间和LK迭代脱节测试时效果不稳定。位姿监督loss我的实现是对齐旋转矩阵的Frobenius范数误差和平移的L2误差直接相加def pose_loss(R_pred, t_pred, R_gt, t_gt): loss_R torch.norm(R_pred - R_gt, pfro, dim(1, 2)).mean() loss_t F.mse_loss(t_pred, t_gt) return loss_R loss_t这个loss的权重不用很大0.01就够。权重太大网络会把精力全部放在回归位姿上反而削弱了特征表达的学习太小则起不到引导作用前期收敛曲线波动比较大。我尝试过0.1和0.0010.1时RRE虽然低但测试时在某些类别上突然崩掉0.001时前期loss掉得很慢最后固定在0.01。5.3 评估指标RRE和RTE训练和测试阶段的评估指标业界常用的两个是相对旋转误差RRE和相对平移误差RTE分别衡量预测旋转矩阵与真值旋转矩阵的轴角误差、预测平移与真值平移的欧氏距离误差。def rotation_error(R_pred, R_gt): # 李代数角误差 trace torch.clamp((R_gt.transpose(1, 2) R_pred).diagonal(dim1-2, dim2-1).sum(-1), -1, 1) return torch.acos(trace / 2 - 0.5).deg2rad() * 180 / torch.pi def translation_error(t_pred, t_gt): return torch.norm(t_pred - t_gt, dim1)测试阶段的做法是从测试集随机挑一批点云对分别施加不同范围的初始旋转平移然后跑PointNetLK推理统计RRE和RTE的均值和中位数。建议同时准备一个ICP基线做对比这样能看出深度方法相比传统方法的提升到底在哪里。在我的实测里初始旋转误差45度以内时PointNetLK的RRE中位数通常在2度以内而标准ICP在相同初始条件下基本已经发散。但当初始误差超过90度PointNetLK也会出现部分样本失败原因不是网络崩了而是LK迭代本身在线性化失效看到一个完全翻转的点云全局特征差异和真实位姿误差之间不再单调。6. ModelNet40训练技巧与踩坑记录6.1 旋转采样范围从易到难的课程式策略一开始我直接按照原论文设置旋转角度在[0, 45]度内均匀采样训练出来的模型在45度以内的测试样本上表现不错但一旦测试时初始误差扩大到60度精度立刻跳水。后来我改成课程式训练策略前20个epoch用[0, 15]度的小范围数据让网络先学会在小扰动下稳定收敛中间30个epoch扩大到[0, 30]度最后50个epoch才切到[0, 45]度。这样做的原因是随机初始化的特征空间还很粗糙大角度扰动会让LK迭代一步就跑飞梯度方向混乱网络很难学到有效的特征表达从小角度开始迭代始终在容易收敛的区域特征空间会先被塑造得比较平滑再逐步加大难度就顺理成章。这个策略把最终的RRE显著降低了一点更重要的是训练过程稳定很多loss曲线不会隔三差五就冒一个尖峰出来。我建议任何复现PointNetLK的读者都试一试这个思路。6.2 归一化尺度一个让loss好看但效果变差的陷阱有一次我换了一种归一化方式把点云除以标准差而不是最大距离。训练loss下降得比之前还快正当我美滋滋地以为发现了更优配置时测试RRE直接翻了3倍。原因在于平移和旋转对点云尺度的敏感度不一样特征空间中的平移误差会随着点云尺度变化而LK迭代的雅可比矩阵又依赖缩放比例。统一除以最大距离之后不同样本的尺度完全一致网络学到的特征表达才能保持稳定换成标准差后部分点云会被放大到半径2甚至3的区间这对固定学习率的优化过程来说完全是灾难。6.3 迭代次数的选择训练和测试可以不一样LK的迭代次数在训练和测试阶段可以分开设置。训练时我固定为10次每一轮迭代都走完整的前向计算和梯度回传这样网络能看到若干步优化后的结果而不是只优化一步。测试时迭代次数可以加大到20次甚至50次配合收敛阈值提前跳出循环。实测结果很有意思训练时迭代次数太少比如3步测试时再加大迭代次数也不会带来精度提升因为特征空间没有被充分训练训练时迭代10步以上效果逐渐饱和继续增加只会拖慢训练速度。我最终选了训练10步、测试20步这个组合算是在效果和速度之间比较平衡的点。6.4 Batch Size和显存之间的取舍PointNetLK的显存占用主要由特征提取器的中间激活值和LK迭代的次数决定。我用的显卡是单张RTX 309024GB显存batch size可以开到64训练很快如果你只有12GB显存建议batch size降到32以下或者把采样点数从1024降到768否则很容易OOM。如果显存实在不够还有一个更省显存的做法训练时不把所有LK迭代都纳入梯度计算只在最后一步回传梯度前面的迭代当作不可导的固定过程。这样显存占用会显著下降收敛速度略有损失但整体效果还在可接受范围内。这个技巧在一些低显存复现项目里还挺常用的。6.5 从CAD点云到真实扫描地形的延伸思考ModelNet40是CAD模型网格干净、表面完整、没有噪声训练出来的网络直接迁移到真实扫描数据上确实会有一定落差。我的经验是如果做地形点云配准这类任务最好在训练阶段就加入符合目标场景的数据增强比如增大高斯噪声幅度到0.03、模拟部分区域点缺失。真正的地形点云尺度动辄几百米使用PointNetLK前一定要做归一化配准完成后再把预测的变换矩阵映射回原始尺度否则平移向量会完全失去意义。CloudCompare中可以先手动大致对齐两片地形点云把初始误差控制到45度以内再交给PointNetLK做精细配准效果远好于直接把两个完全任意朝向的点云丢进去。另外地形点云的表面起伏往往比较平缓全局特征可能提取不到足够尖锐的几何信息这时候可以考虑把点云裁切成包含显著地形特征如山脊、建筑物边缘的子块分别配准再综合变换结果会比整片直接配准更稳妥。6.6 踩坑最深的三个细节第一条规范化和反规范化的时机。我一开始在图里显示配准结果时忘记把归一化还原看到的点云大小明显不对差点误判为模型失效。强烈建议把加载-采样-归一化-配准-反归一化封装成一个完整的流程函数。第二条BatchNorm的动量参数。默认动量是0.1训练一小段后特征提取器会过度依赖当前batch的统计量导致验证不稳定。在配准这种小batch场景下我建议把BatchNorm的momentum调到0.01特征分布的平滑性会有明显改善。第三条也是最重要的一条初始位姿真的要给准。PointNetLK相比ICP的鲁棒性提升很大但绝不是说可以随便给一个初始位姿。它解决的是中等初始误差下的精配准问题而不是无初始值的全自动配准问题。实际工程中用PCA主轴做一次粗对齐或者用上一帧位姿做预测是完全有必要的。把PointNetLK当作粗配准之后的精配准前级再接一次ICP做最终修正这套组合拳在我的项目里一直用到现在稳定性比任何单一方法都强。