拓冰建站拓冰建站
首页 / 资讯中心 / 正文

PointNet编码如何改变点云配准:从ICP到PCRNet的工程实践

1. 为什么PointNet编码能重新定义点云配准做点云配准的人十有八九都被ICP恶心过。初值稍微给偏一点它就给你陷进局部最优里出不来点云稍微大一点每次迭代都要重新找最近邻那个耗时真的感人。我在自己的项目里试过给ICP换各种加速策略从kd-tree到多分辨率金字塔效果是有但本质问题没解决——它太依赖初始位姿了。后来看到PCRNet这篇工作感觉思路一下就打开了。它的核心想法特别直接既然PointNet能把无序点云编码成一个全局特征向量那我把源点云和目标点云分别过一遍PointNet再拿两个特征向量做一个比较直接用比较结果回归出刚体变换参数不就行了吗整个过程完全跳过迭代优化一次前向推理出结果速度比ICP快好几个量级。这篇东西适合谁看如果你在做机器人抓取、SLAM、三维重建或者自动驾驶里的点云配准又苦于传统方法又慢又脆那PCRNet的思路绝对值得借鉴。就算你不打算用它替代ICP这篇论文里“如何用全局特征代替逐点匹配”的思想在很多三维深度学习任务里都能复用。下面我结合自己的复现经历把这套网络的设计思路、数据流程、训练细节和踩过的坑一次讲清楚。2. 网络整体设计什么是“用特征比较代替几何迭代”在展开代码之前先把PCRNet的设计逻辑理一遍。这事儿如果不掰扯清楚后面复现的时候你很容易被各种细节绕晕。2.1 核心思路为什么可以跳过迭代优化传统ICP的流程是给定一个初始变换把源点云变换过去然后找最近邻点对用点对去估计新的变换再重复这个过程直到收敛。每一步都依赖上一步的结果所以初始值差一点后面就全歪了。PCRNet换了个玩法。它认为配准的关键不是逐点找对应而是整体理解两个点云的“形貌”。如果我能用一个向量把整个点云的形状特征浓缩起来那两个点云是否对齐就等价于两个形状特征向量是否逼近同一个“标准答案”。基于这个思想训练阶段的做法是对源点云和目标点云分别做PointNet编码得到两个全局特征向量把两个特征向量的差拼接起来送入一个全连接回归网络输出7个参数3个平移量 4个旋转四元数用预测值和真值算loss端到端反向传播。推理阶段更简单直接前向传播一次拿输出做变换就行。没有最近邻搜索没有迭代没有局部最优陷阱。2.2 结构拆解PointNet编码器 特征比较模块 参数回归头整套网络分成三个部分每个部分都承担了明确的职责。首先是PointNet编码器。注意这里用的是PointNet的全局特征分支不是分割分支。PointNet本身的设计是对每个点做MLP升维然后用最大池化把所有点的特征聚合起来得到全局特征。这个最大池化是整个网络的灵魂——它保证了输入点云的点序不影响输出满足置换不变性。模型Net在配准任务里我们关心的就是这个全局特征因为它概括了点云整体的几何信息。其次是特征比较模块。PCRNet没有把两个特征直接拼接送入回归网络而是先取它们的差或者叫残差再拼上原始特征组成一个更丰富的描述。论文里的做法是把两者的差异信息凸显出来让回归网络能更直接地感知到“两个点云差了多少”。这是一个很重要的工程细节因为如果只送拼接特征网络虽然也能学但差异信号会被淹没在冗余维度里收敛会慢不少。最后是参数回归头。全连接层把特征逐步压到7维输出。这里的关键是四元数部分——网络直接回归出来的是四元数值但你不做归一化直接用旋转矩阵就不是正经的旋转矩阵了。我在复现时踩过这个坑后面在训练细节里细说。2.3 PCRNet与PointNet的定位差异很多人第一次看到PCRNet以为它就是把两个PointNet拼在一起其实没那么简单。经典PointNet的任务是分类和分割分类时把全局特征送进分类器分割时把全局特征和逐点特征拼接后逐点分类。PCRNet复用它的全局特征编码能力但不再做分类或分割而是把编码后的向量作为形状描述子来做跨点云的比较和回归。换句话说PointNet在PCRNet里的角色是“特征提取器”不是“决策器”。如果你在这里用了PointNet或者别的编码器理论上PCRNet的框架也成立只是精度和速度会变。论文的实验里对比了不同变体有兴趣可以翻原论文的ablation study看看不同设计对配准误差的影响。3. 数据准备ModelNet40数据集与点云预处理实操有了网络结构的基本概念接下来是复现过程中的实操环节。我们以论文里用的ModelNet40数据集为例把数据从下载到喂进网络的整个流程走一遍。3.1 ModelNet40下载与目录整理ModelNet40是Princeton ModelNet项目提供的一个三维CAD模型数据集包含40个类别的12311个模型格式是OFF或者OBJ。PCRNet论文里用的是官方提供的mesh版本然后在训练前从mesh表面均匀采样2048个点作为输入。下载地址就是ModelNet官方的页面需要你填一个表单然后它会把下载链接发到你填写的邮箱里。有一个坑是如果你填的是QQ邮箱或者某些国内邮箱可能收不到或者被扔进垃圾箱。建议填Gmail或者企业邮箱成功率更高。下下来的是一个ZIP压缩包解压后结构是这样的ModelNet40/ ├── airplane/ │ ├── airplane_0001.off │ ├── airplane_0002.off │ ├── ... └── monitor/ └── ...每个类别下面有多个模型文件训练集和测试集的划分在论文里是官方预设的每个类都是前N个训练后M个测试你在预处理时就按文件名顺序拆就行。3.2 从mesh到点云采样处理和坐标归一化把OFF文件转成点云需要用到trimesh或者open3d这类库。核心逻辑是读入mesh调用trimesh的sample方法在表面均匀采样。这里有个要点是“均匀”两个字——trimesh默认的采样策略不是纯随机而是按三角形面积加权采样这样大三角形的稠密程度不至于过低点分布更均匀。下面是我自己写的一个采样函数你可以直接参考import trimesh import numpy as np def sample_points_from_mesh(mesh_path, num_points2048): mesh trimesh.load(mesh_path, forcemesh, processFalse) # 在mesh表面均匀采样 points, _ trimesh.sample.sample_surface(mesh, num_points) return points.astype(np.float32)采样完的点云要做什么预处理论文里有一个关键操作把源点云和目标点云分别归一化到一个以重心为原点的局部坐标系。这里的目的是让网络不用关注点云在空间中的绝对位置而是专注于形状配对。如果不做这一步同一个类别里不同模型的尺度差异会干扰训练。归一化逻辑如下def normalize_points(points): # 移动到重心 centroid np.mean(points, axis0) points points - centroid # 按最大半径缩放到单位球内 radius np.max(np.linalg.norm(points, axis1)) points points / radius return points3.3 训练样本生成如何构造配准对这步是整个数据准备环节里最有意思的部分也是决定模型泛化能力的核心。PCRNet的训练数据不是简单地把两个点云丢进去就行而是要人为制造“有位移偏差的点云对”。具体做法是随机选一个模型采样出点云P然后随机生成一个刚体变换旋转 平移施加到P上得到变换后的点云P。训练时把P当源点云P当目标点云网络的监督信号就是这个刚体变换的参数。论文里对旋转和平移的范围有明确约定旋转角度范围是0到45度平移范围是-0.5到0.5。这个范围不是随便定的——太大会让配准变得不现实太小则会导致网络只学到恒等映射。我用的是如下生成方式def generate_random_transform(max_rotation45.0, max_translation0.5): # 随机生成旋转角度 angle_x np.random.uniform(-max_rotation, max_rotation) * np.pi / 180.0 angle_y np.random.uniform(-max_rotation, max_rotation) * np.pi / 180.0 angle_z np.random.uniform(-max_rotation, max_rotation) * np.pi / 180.0 # 构造旋转矩阵 rx np.array([[1, 0, 0], [0, np.cos(angle_x), -np.sin(angle_x)], [0, np.sin(angle_x), np.cos(angle_x)]]) ry np.array([[np.cos(angle_y), 0, np.sin(angle_y)], [0, 1, 0], [-np.sin(angle_y), 0, np.cos(angle_y)]]) rz np.array([[np.cos(angle_z), -np.sin(angle_z), 0], [np.sin(angle_z), np.cos(angle_z), 0], [0, 0, 1]]) rotation rz ry rx # 随机平移 translation np.random.uniform(-max_translation, max_translation, size3) return rotation, translation注意旋转矩阵的乘积顺序。这里用的是内旋还是外旋其实对数据生成没有本质影响因为网络学的是从点云对到变换参数的映射只要训练和推理的约定一致就行。但从可解释性角度我习惯用rz ry rx这样的Z-Y-X欧拉角顺序这样如果你后续要做可视化可以方便地从欧拉角理解旋转含义。4. 训练细节损失函数、优化器与4个关键参数网络结构搭好了数据也准备好了接下来进入训练阶段。这一节我重点讲几个直接影响训练效果的参数和细节包括我调参过程中发现的一些坑。4.1 损失函数如何设计L1还是L2PCRNet论文的损失函数是预测值与真值之间的L1距离。为什么用L1而不是L2因为L2在误差较大时梯度按误差比例增大容易出现梯度爆炸而L1的梯度是常数对离群点更鲁棒训练更稳定。旋转和平移两部分怎么组合论文里是把两者的误差直接相加并没有加权。但我在实际实验中发现如果平移范围偏大平移的loss值会远大于旋转的loss值导致网络把重心放在平移回归上旋转的精度会下降。我的处理方式是分别计算后相加但给平移项乘一个较小的系数比如0.1让两者的量级更均衡。具体代码criterion nn.L1Loss() # 前向传播后 pred_rotation, pred_translation model(source, target) loss_rot criterion(pred_rotation, target_rotation) loss_trans criterion(pred_translation, target_translation) loss loss_rot 0.1 * loss_trans注意四元数的L1损失有一个细节四元数q和-q表示同一个旋转但L1损失会认为它们差得很远。解决方法是在算loss之前检查预测四元数和真值四元数的点积符号如果为负就把真值四元数取反再做loss计算。这个小逻辑能避免训练过程中的振荡现象。4.2 优化器与学习率Adam和它的初始值优化器直接用Adam学习率我试过几个档位最后稳定在1e-3。用Adam的原因点云配准的loss曲面比较崎岖AdaGrad这类自适应方法容易过早衰减SGD收敛又太慢Adam在稳定性和收敛速度之间取得了不错的平衡。关于学习率调度我建议用ReduceLROnPlateau以验证集loss为监控指标连续10个epoch没下降就减半。经验是PCRNet收敛得很快整个训练大约120个epoch就能达到不错的精度到后期小学习率微调很重要否则loss会在一个平台上反复横跳。我的训练循环大致长这样optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience10 ) for epoch in range(epochs): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_loss validate(model, val_loader, criterion) scheduler.step(val_loss)4.3 batch size与PointNet的输入维度PointNet的一个限制是输入点云的点数需要固定。因为最大池化操作虽然对点序不敏感但要求所有输入的点数一致否则batch内无法拼接成张量。论文里使用2048个点我在复现时试过1024和4096区别是采样更密对微小细节的学习更好但显存占用和训练时间也上去了。如果你的GPU显存有限建议先用1024跑通流程再根据情况往上升。一个比较现实的配置是batch size 32单卡训练模型占用显存大约6到8GB取决于编码器的隐层维度一般消费级显卡都能跑起来。4.4 四元数归一化一个不能少的后处理这是最容易踩的一个坑。回归头直接输出的四元数4个浮点数并不能直接用来构造旋转矩阵。如果四元数的模不为1它对应的旋转矩阵会有缩放效应变换出来的点云会变形。解决办法是在网络的最后加一个四元数归一化层或者在前向推理时手动做def quaternion_normalize(q): norm torch.sqrt(torch.sum(q ** 2, dim-1, keepdimTrue)) return q / norm把归一化后的四元数转成旋转矩阵可以用pytorch3d提供的quaternion_to_matrix也可以自己写旋转矩阵构造公式。我自己倾向于用pytorch3d因为它自带梯度且经过了大量测试比自己手写靠谱。如果你不想引入pytorch3d这个重依赖也可以把四元数转旋转矩阵的公式手写出来网上有很多现成实现只是需要注意坐标系约定。5. 推理与应用把网络输出变成点云变换模型训练好了接下来要做的就是把网络的输出真正用起来——把预测的四元数和平移向量应用到源点云上得到配准后的点云。这一节讲推理端到端的流程和一些工程细节。5.1 从四元数到旋转矩阵再到变换后的点云拿到归一化后的四元数之后先转成旋转矩阵。代码逻辑def quaternion_to_matrix(q): w, x, y, z q[..., 0], q[..., 1], q[..., 2], q[..., 3] # 构造旋转矩阵 R torch.zeros((q.shape[0], 3, 3), dtypeq.dtype, deviceq.device) R[..., 0, 0] 1 - 2 * (y * y z * z) R[..., 0, 1] 2 * (x * y - w * z) R[..., 0, 2] 2 * (x * z w * y) R[..., 1, 0] 2 * (x * y w * z) R[..., 1, 1] 1 - 2 * (x * x z * z) R[..., 1, 2] 2 * (y * z - w * x) R[..., 2, 0] 2 * (x * z - w * y) R[..., 2, 1] 2 * (y * z w * x) R[..., 2, 2] 1 - 2 * (x * x y * y) return R注意这里关于四元数的顺序我用的顺序是(w, x, y, z)这跟PyTorch的quaternion_to_matrix一致。如果你使用其他库或者从别的数据源读入四元数一定要先确认它是wxyz还是xyzw顺序这个搞反了旋转矩阵就是错的而且是那种很难察觉的错误。得到旋转矩阵R和平移向量t之后对点云做变换aligned_points (R source_points.T).T t这就是配准结果。你可以用这个结果去跟目标点云做最近邻距离评估算RMSE或者直接可视化对比。5.2 与ICP等传统方法的结合PCRNet不是替代品是加速器PCRNet单次前向的配准精度通常还不如迭代很多轮的ICP精细。所以在实际工程里我更推荐的做法是把PCRNet当成“初值提供器”——先用它预测一个大概的变换把源点云大致对齐到目标点云附近然后再用ICP做精配准。这样做的好处是PCRNet提供了足够好的初值ICP不再容易陷入局部最优因为初值已经比较接近ICP只需要很少的迭代次数就能收敛整体速度比纯ICP快很多。这种“粗配准 精配准”的级联策略在工业界很常见。尤其是对于两帧之间相对位姿变化较大的场景纯ICP可能直接发散而PCRNet可以稳妥地把两片点云拉到“看得见对方”的距离内。5.3 工程部署的几点参考如果你要把模型部署到实际项目里有几个点值得提前规划一是点云采样策略要和训练时保持一致。训练时用2048个点推理时如果输入是10万点的稠密点云建议先做体素降采样到2048个点附近再做配准否则分布差异会影响特征质量。二是数据归一化逻辑必须一致。训练时我们对点云做了“去重心 缩放至单位球”的预处理推理时也要对源点云和目标点云分别做同样的归一化。换算回真实尺度时别忘了把预测的平移量乘以之前除以的那个半径否则平移量在真实世界坐标下是错的。三是对实时性要求高的场景可以考虑TensorRT或ONNX Runtime加速。PCRNet的骨干是PointNet加几个全连接层整体计算量不大量化到FP16后单次推理在GPU上可以到毫秒级在嵌入式设备上也能跑到几十毫秒级别。6. 常见的训练与复现问题直接给你排查清单复现PCRNet的过程里我前前后后遇到过不少问题有些在网上查了半天才找到原因。这里整理成一个速查表希望能帮你省点时间。6.1 问题速查表问题现象可能原因解决方法训练loss不下降学习率过大或过小调整学习率确认四元数归一化没写错旋转精度差平移还行loss权重不平衡给平移loss加小权重让网络专注旋转推理时点云变形四元数没归一化加四元数归一化层再转旋转矩阵数据加载报错点数是NoneOFF文件损坏或trimesh加载失败加try except跳过或者改用open3d读取显存不够batch size或点数过大降batch size到8/16或采用1024点输入训练时loss震荡严重四元数正负号问题检查点积符号取反真值后再算loss验证集效果好真实数据差训练数据分布和真实数据不一致增加数据增强扩大旋转角度范围结果总是接近恒等变换平移范围太小学到的都是0附近适当增大平移范围如0.5到1.06.2 一个容易被忽略的数据加载细节在使用PyTorch的DataLoader加载点云时一个常见问题是每个样本的点数不一致。原因通常是预处理时某些mesh采样失败只返回了几百个点。解决方法是在数据集的最前面加一个检查过滤掉点数不达标的样本或者在采样失败时直接抛异常并跳过而不是让DataLoader在训练中途报错。Dataset类的核心逻辑我贴个精简版本class PCRNetDataset(torch.utils.data.Dataset): def __init__(self, root_dir, num_points2048, trainTrue): self.files self._load_files(root_dir, train) self.num_points num_points def __getitem__(self, idx): mesh_path, category self.files[idx] try: points sample_points_from_mesh(mesh_path, self.num_points) except Exception: # 失败就取下一个样本 return self.__getitem__((idx 1) % len(self.files)) # 归一化 points normalize_points(points) # 生成随机变换 R, t generate_random_transform() transformed points R.T t quat rotation_matrix_to_quaternion(R) return (transformed.astype(np.float32), points.astype(np.float32), quat.astype(np.float32), t.astype(np.float32))6.3 关于PyTorch和PyTorch3d的版本兼容我在复现时用的是PyTorch 2.x PyTorch3d 0.7.x整体比较顺利。但如果你的环境是老版本比如PyTorch 1.7可能会有CUDA算符编译不过的问题。建议直接上PyTorch 2.x一方面算子更完备另一方面对动态形状点云的支持也更好。如果你不打算用PyTorch3d的三维工具函数纯PyTorch也能完整跑通PCRNet只是需要自己写四元数转旋转矩阵、旋转矩阵转四元数这两个工具函数。建议还是用一个稳定的三维几何库省心很多。6.4 训练加速与显存管理的经验在训练时我习惯在每个epoch结束用验证集做一次评估。PCRNet的验证评估比训练简单直接用预测的变换参数去变换源点云然后算源点云变换后与目标点云之间的平均最近邻距离。这个距离能直观反映配准精度比只看loss要更有意义。此外因为PCRNet的模型本身不大骨干网络加回归头总参数可能不到100万多显卡训练的收益不大不如用大的batch size单卡训练。如果你用多卡DataParallel还可能出现特征比较模块在不同卡上分布的问题反而拖慢速度。7. 实操心得PCRNet还能往哪走复现PCRNet的过程中我自己最大的收获不是“学会了跑一个网络”而是明白了“什么时候该用神经网络解决传统算法的问题”。传统ICP很精确但脆弱PCRNet很快但精度上限有限。两者结合正好互补。在三维视觉越来越内卷的今天与其纠结于“纯学习派还是纯几何派”不如想想怎么把两类方法的优势组合起来。PCRNet提供了一个很好的范式用网络解决“大致对齐”用几何算法解决“精细到位”。这套思路你可以迁移到很多其他三维任务里。如果你还想往深做我建议从这几个方向入手在编码器侧换成PointNet或更现代的点云Transformer结构特征表达能力更强配准精度会进一步提升把回归头改成迭代的预测结构类似Deep Closest Point的思路逐步精化变换参数在训练时加更多噪声类型高斯噪声、离群点、部分点云增强模型的鲁棒性考虑把全局特征做循环一致性约束用于配准网络的自监督训练减少对标注的依赖。最后分享一个小技巧训练时如果发现loss曲线在某个值来回震荡先别急着调学习率去检查一下四元数正负号处理有没有写对。这个看似不起眼的小细节可能是你复现它时最大的隐形坑。根据我个人实际操作的经验PCRNet配合PointNet编码的思路虽然在2020年的论文中就已经发表但放到今天的三维视觉流程里依然有很强的参考价值。用它来给传统配准算法提供一个好的初值是我目前觉得性价比最高的用法你可以直接上手试。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门