ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

三维点云去噪实战:基于Python深度学习的完整实现与避坑指南

三维点云去噪实战:基于Python深度学习的完整实现与避坑指南 简介一套基于Python深度学习框架实现的三维点云去噪源码面向计算机视觉、三维数据处理方向的本科生或研究生尤其适合作为课程设计或期末大作业的参考。项目已在导师指导下完成并获得97分的高分评价整体流程完整从数据准备、噪声模拟、去噪训练到效果评估均有覆盖下载后即可直接运行使用。压缩包内共包含41个文件其中36个为Python脚本另有3个JSON配置文件、1个Markdown说明文档及1个.gitignore文件整体体积约59KB结构轻量。代码按模块化思路组织涵盖噪声添加、去噪、离群点去除、评估指标等功能模块并提供训练、整体处理、F1评估等关键流程脚本便于理解点云去噪的完整处理链路。当前已有204人浏览学习对于需要快速搭建三维点云去噪实验环境、学习深度学习框架在点云处理中应用的同学来说这套高分完整项目具有很好的参考价值既能直接用于作业提交也能作为二次开发的基础模板。1. 三维点云去噪为什么期末大作业都绕不开深度学习拿到激光扫描得到的点云第一件事永远是去噪——那些悬浮在物体表面的飞点会让配准、重建和识别全部跑偏。传统统计滤波和半径滤波能杀掉远处的离群点但遇到贴近表面的高斯噪声、薄壁结构上的抖动点阈值一高把细节抹平阈值一低噪声还在这就是为什么近几年的课程设计都在转向基于python深度学习的三维点云去噪源码让网络自己去学噪声模式而不是靠人肉调阈值。这套源码把数据生成、网络训练、评估可视化串成完整闭环适合期末大作业、毕设开题也适合想快速把手伸进点云深度学习的新手。下面按我自己的落地顺序从数据准备一路讲到避坑。2. 把点云变成训练样本噪声模型和数据集的Python实现2.1 去噪任务先想清楚学残差还是学干净点云点云去噪和图像去噪很像但有个关键区别图像是规则网格点云是无序集合。给网络输入一个点坐标(u, v)输出一个干净点坐标大部分大作业会直接做回归。问题是直接回归干净坐标时网络要同时记住物体的整体形状和局部偏移收敛慢还容易把细小的凸起凹槽平均掉。我一般会换一种做法让网络预测“噪声点到干净点”的位移向量输出等于输入加位移。这样网络只需要在原始坐标附近做微调几何结构由输入本身保持训练容易稳定高分作业基本都是这个思路。这个设计也决定了后面所有数据处理方式加噪声时要同时保存成对的“干净点云”和“带噪点云”而不是只给一个文件。训练时喂进去的是带噪版本监督信号是干净版本损失直接算在位移后的坐标上。另外残差学习还有一个隐含好处网络对点云的绝对平移不敏感。因为训练前我们都会做归一化残差方式相当于让网络只关注“这个点相对于周围邻居该往哪挪”而不是“这个点的绝对坐标是多少”。真实扫描时物体位置变化模型也不会失效。这一点在期末答辩时往往是加分项。还有一个容易混淆的点有人说去噪就是“把离群点删掉”。深度学习路线不做删点而是把每个点挪到它应该待的位置。如果你的大作业要求输出点云点数不变这个范式正好合适如果允许删点可以在网络输出后再加一步统计滤波用来剔除仍远离表面的点。但核心网络一定是在做坐标回归。后续所有评估也要围绕“坐标准不准”来做。2.2 生成训练数据用Python给点云加高斯噪声和离群点常见做法是用公开数据集里干净的点云当监督教程里最常见的是ShapeNet或ModelNet子集。读取方式就用numpy或open3d。下面这段代码负责给每个点加两类噪声贴近表面的高斯噪声以及少数明显飞出去的离群点。注意参数设定要能对应上真实激光雷达误差模型比如sigma要大一些模拟机械式雷达的测距抖动离群点比例要低一些模拟偶然误匹配。import numpy as np import open3d as o3d def add_noise(clean_pc, sigma0.005, outlier_ratio0.05, seed42): rng np.random.default_rng(seed) noisy clean_pc rng.normal(0.0, sigma, sizeclean_pc.shape) n len(noisy) n_out int(n * outlier_ratio) idx rng.choice(n, n_out, replaceFalse) # 沿随机方向加入离群点幅度比sigma大一个数量级 direction rng.normal(size(n_out, 3)) direction / np.linalg.norm(direction, axis1, keepdimsTrue) noisy[idx] direction * rng.uniform(0.02, 0.08, size(n_out, 1)) return clean_pc.astype(np.float32), noisy.astype(np.float32) # 读取一个点云文件 pcd o3d.io.read_point_cloud(object.ply) clean np.asarray(pcd.points) clean, noisy add_noise(clean)逻辑说明第一行noisy clean_pc rng.normal生成贴近表面噪声sigma给的是单位球尺度下的std。np.linalg.norm用来归一化随机方向这样离群点不会偏向坐标轴。返回时两种点云都是float32后续直接转torch.Tensor。参数说明sigma0.005表示噪声标准差约为点云包围盒尺寸的0.5%对高精度扫描偏大但对大作业足够outlier_ratio0.05表示5%的点变离群点太多会主导损失太少又学不到鲁棒性。先跑通再调这两个数。批量生成训练数据时建议把每个样本存成独立的npz文件用savez_compressed压缩里面至少包含clean和noisy两个数组。大作业里有人把所有样本打成一个h5文件其实没必要npz方便调试单个文件坏了可以单独重新生成。文件命名带编号比如001.npz。还需要固定点数有人问为什么固定因为PyTorch的DataLoader默认用torch.stack合并batch点数不一致会直接报错。统一方法见下一节。如果你手头只有一张含噪扫描没有干净版本不要硬做监督学习。可以退一步做“自监督”把同一点云分成两份用体素滤波平滑后的结果当伪干净标签或者用不同参数去噪的结果做交叉监督。大作业里最好还是把仿真数据做足直接拿公开数据集当干净源省去后面跟老师解释“那个伪标签合理吗”的时间。2.3 归一化、体素下采样与数据增强的参数选择点云数据进网络前必须有统一坐标尺度不然不同物体距离差很多网络学的是尺度而不是形状。常见做法是先平移到质心再除以最远距离把点云放进单位球内。下采样用体素滤波注意voxel_size选得太大细节会被吃掉太小显存不够。下面是一段预处理函数。def normalize(pc): centroid pc.mean(axis0, keepdimsTrue) pc pc - centroid max_dist np.linalg.norm(pc, axis1).max() pc pc / max_dist return pc, centroid, max_dist def voxel_downsample(pc, voxel_size0.01): pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(pc) pcd pcd.voxel_down_sample(voxel_size) return np.asarray(pcd.points) # 完整流程 clean voxel_downsample(clean, 0.01) clean, _, _ normalize(clean) clean, noisy add_noise(clean)逻辑说明normalize里的keepdimsTrue是为了后面反变换时方便。max_dist取最远点到中心的距离除以它之后所有点都在半径1的球内。voxel_downsample用open3d内置实现返回索引重排后的新点云点数会下降但密度更均匀。参数上voxel_size0.01对单位球而言通常能把ShapeNet物体从几万个点压到两千到五千点既保留大部分细节又让网络训练快很多如果内存不够就提到0.015。注意不要在体素滤波之后立刻加噪声先降采样再加这样可以模拟真实扫描中“每个采样点都带噪声”的物理过程。数据增强这一段是拉高分的重点。常见做法是随机旋转、随机缩放、再加一次小幅抖动。旋转要用旋转矩阵不能直接对xyz加角偏移。另外还要在加载每个epoch时重新打乱点序防止网络记忆顺序。我给一版增强代码放在每个epoch的数据加载时调用这样相当于无限数据量。def augment(pc): # 随机旋转绕三个轴角度全覆盖 angles np.random.uniform(-np.pi, np.pi, size3) sx, sy, sz np.sin(angles) cx, cy, cz np.cos(angles) rot_x np.array([[1,0,0],[0,cx,-sx],[0,sx,cx]]) rot_y np.array([[cy,0,sy],[0,1,0],[-sy,0,cy]]) rot_z np.array([[cz,-sz,0],[sz,cz,0],[0,0,1]]) pc pc (rot_z rot_y rot_x).T # 随机缩放0.9~1.1倍 scale np.random.uniform(0.9, 1.1) pc pc * scale return pc参数说明旋转角度范围取全角度避免网络记住固定方向。缩放范围不要太大1.1倍足够过大相当于改物体尺度去噪任务会误学成形状不变性。实际使用中旋转要在加噪声之后做因为噪声的方向也应当随物体一起转这样网络不会依赖“噪声总是沿z轴”这类伪特征。增强只在训练时做测试时只做归一化不做旋转否则评估指标会偏低。最后是数据集划分。我习惯按物体划分而不是按点划分训练集用15个物体验证集用3个测试集用2个。如果按点云切片划分同一物体被切开后几何结构不完整训练时网络看到一半局部区域测试时遇到完整物体反而懵。划分完建议把所有样本在内存里做一次随机打乱并写入一个train_files.txt这样每次训练顺序一致报告里能复现。3. 网络主干选型用PointNet变体还是自己拼MLP3.1 为什么选PointNet而不是普通多层感知机点云和图像最本质的差别是无序性。同样的形状点云里的点顺序换了普通MLP输入就变了输出也变但语义应该一样。PointNet的解决办法是先共享一组MLP逐点提特征再用对称函数max pooling把特征聚合。这样网络对输入顺序不敏感。去噪任务恰好只需要每个点的局部信息PointNet天然适合。但直接拿分类用的PointNet过来会发现输出的点数可能变少或者局部细节被max pooling丢失。所以大作业里最常用的不是原始PointNet而是“PointNet的编码器 残差回归头”。那为什么不用图神经网络GNN确实能建模点云邻接关系但训练时要维护边的特征传播复杂度高而且每轮点序打乱后图的边还要重算。期末大作业追求“老师能看懂、代码能跑通、结论能复现”GNN容易让代码量和报错次数翻倍。PointNet这样逐点MLP的方案整个网络就是几个全连接层backward过程也容易跟老师讲清楚。有人问PointNet不是比PointNet强很多吗强在很多需要上下文语义的任务上比如分割和分类。但去噪是一个低频细节任务PointNet的层级下采样会丢失非均匀密度下的细小结构。大作业点云通常只有几千个点PointNet足够。如果测试发现薄壁结构去完噪变成一块平板再换成PointNet不迟。3.2 网络结构代码局部坐标差加残差回归我常用的一版结构是这样的对每个点先找出k个最近邻居把邻居相对该点的局部坐标拼接成特征再过一个三层MLP预测位移。核心代码很简单但包含很多大作业里容易忽略的细节。请看下面实现。local特征为什么重要纯坐标x/y/z输入的话网络只能感知全局位置两个相距很远的点如果坐标相似会被当成同类但邻居相对坐标描述的是点的“局部地形”同样是球面上的点在凹陷处和凸起处邻居相对分布完全不同后者才是去噪真正需要的信息。import torch import torch.nn as nn import torch.nn.functional as F class PointDenoiser(nn.Module): def __init__(self, k16, hidden128): super().__init__() self.k k # 输入([x,y,z] k个邻居的局部坐标)共 3 3*k self.offset_mlp nn.Sequential( nn.Linear(3 3 * self.k, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 3) ) def forward(self, x): # x: (B, N, 3)已经归一化 B, N, _ x.shape # 计算所有点两两距离取前k个最近邻居索引 dist torch.cdist(x, x) _, topk_idx dist.topk(self.k 1, dim-1, largestFalse) neighbor_idx topk_idx[:, :, 1:] # 去掉自己 # 教学版逐batch取邻居理解用 knn torch.stack([x[b][neighbor_idx[b]] for b in range(B)]) # knn shape: (B, N, k, 3) local knn - x.unsqueeze(2) # 邻居相对坐标 local local.reshape(B, N, self.k * 3) feature torch.cat([x, local], dim-1) offset self.offset_mlp(feature) return x offset逻辑说明torch.cdist计算B个batch内所有点对欧氏距离返回形状(B,N,N)。topk取最小的k1个第一个一定是自己所以去掉第一列。knn那行用逐batch循环取邻居思路直白x[b][neighbor_idx[b]]的结果是(N,k,3)。知道这个逻辑后工程上可以换成torch.gather或torch_geometric的knn_gather速度更快大作业里逐batch通常也够用。参数说明k值控制局部感受野16是经验值。k太小网络只看到紧邻的几个点对贴面噪声有效但大块团簇噪声去不动k太大局部坐标差异变小输出会整体向质心收缩物体变“胖”。hidden128是对2048个点、3维输入而言足够。中间两层ReLU不能换成sigmoid或tanh否则梯度消失会让末尾层学不到位移。3.3 损失函数和训练超参数怎么定去噪训练的监督信号是同一个点位置上的干净坐标。因为点没有消失位置一一对应可以用L1或L2距离。L2对大误差的惩罚更狠模型会倾向于把点往平均位置拉结果是物体表面变平滑但细节丢失L1对异常点更鲁棒适合点云里天然存在的稀疏离群点。我一般用L1作为主损失再加一个小的L2正则把位移幅度压住。def loss_fn(clean, predict, weight0.1): point_loss F.l1_loss(predict, clean) # 惩罚过大的偏移量防止输出飞离输入太远 reg (predict - clean).pow(2).mean() return point_loss weight * reg逻辑说明point_loss是主损失reg是位移平方均值weight取0.1时不会压制主损失但能防止网络跳过输入直接输出常数。注意predict - clean在训练阶段是网络学到的偏移量我们可以直接算它不需要独立变量。训练超参数一般这样定Adam优化器学习率1e-3batch size看显存4够用epoch 100到200每50步把学习率乘0.5。下面给出优化器部分。model PointDenoiser(k16, hidden128) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size50, gamma0.5)参数说明step_size50符合200轮训练到100轮和150轮各降一半。如果你的训练集很小把step_size改成20。batch size不要直接拉高我们的torch.cdist会占据N×N内存2048点的batch_size4已经是多数普通显卡的舒适区。如果只有一个GPU且显存只有6GB把hidden降到64或N降到1024。3.4 训练稳定性的三个小技巧第一个技巧是梯度裁剪。L1损失在离群点存在时不稳定极端情况下一步更新能把权重推出正常范围。在loss.backward()之后加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm10.0)基本不用调能避免大部分“过了一轮loss变成nan”的翻车。第二个技巧是每个epoch重新生成噪声。固定噪声的NPC文件虽然能训练但网络会背下噪声的具体位置测试时换个种子就露馅。我在Dataset的__getitem__里直接调用add_noise这样每个epoch看到的噪声都不相同天然做了数据增强。注意此时batch_files里的noisy数组可以不用只存clean。第三个技巧是记录每个epoch在验证集上的CD值而不是只盯训练loss。训练loss只反映回归误差验证CD才反映去噪效果。大作业群里常见的情况是训练loss降得很好但验证CD不降甚至升这说明过拟合。判断标准train loss 0.01, val CD 0.05基本是过拟合减少epoch或加大增强。4. 训练与评估让Loss下降不只在日志里好看4.1 训练脚本数据加载、训练循环与checkpoint保存训练脚本是老师最会抽查的部分。只有模型结构没有训练代码等于没交作业。下面给一套完整的加载循环包含一个最小可用的Dataset和训练主循环。数据文件组织形式采用我常用的约定每个样本一个.npz内含clean和noisy两个(N,3)的数组。import torch from torch.utils.data import Dataset, DataLoader import numpy as np import os class PointNoiseDataset(Dataset): def __init__(self, folder): self.files [os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(.npz)] def __len__(self): return len(self.files) def __getitem__(self, idx): data np.load(self.files[idx]) clean data[clean].astype(np.float32) # 每个epoch重新加噪声模拟无限数据 _, noisy add_noise(clean) # 随机打乱点顺序避免网络记住索引 perm np.random.permutation(len(noisy)) return torch.from_numpy(noisy[perm]), torch.from_numpy(clean[perm])在__getitem__里做随机置换很关键。如果不打乱网络可能学到“第i个点的邻居是第i±1个点”这种顺序关系测试时一换顺序就崩。这里故意把加噪声放到读取时执行配合前面的add_noise默认seed42每调用一次就会生成不同的噪声。DataLoader会在batch维度上stack所以每个样本要等长生成时统一用voxel_downsample固定到2048点差异大的话用pad或者随机裁剪。训练循环也要写得规范包括梯度裁剪、断点续训、日志输出。下面是完整主循环def train_one_epoch(model, loader, optimizer, device): model.train() total_loss 0.0 for noisy, clean in loader: noisy, clean noisy.to(device), clean.to(device) out model(noisy) loss loss_fn(clean, out) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm10.0) optimizer.step() total_loss loss.item() * len(noisy) return total_loss / len(loader.dataset) for epoch in range(1, 201): avg_loss train_one_epoch(model, loader, optimizer, device) scheduler.step() if epoch % 10 0: torch.save({model: model.state_dict(), epoch: epoch, optimizer: optimizer.state_dict()}, fckpt_{epoch:03d}.pth) print(fepoch {epoch} loss {avg_loss:.5f})逻辑说明train_one_epoch里累计loss时乘以了len(noisy)因为最后一个batch可能不满。torch.save里保存optimizer是为了从断点恢复训练这个习惯建议保留。print到标准输出大作业的log文件可以直接用python train.py log.txt生成。要注意model.train()不能省略虽然当前网络没有Dropout和BN但后续一改就踩坑。断点续训时用torch.load读回model和optimizer即可。4.2 评估指标CD、EMD和点到面距离怎么算角度不同指标差异很大。常见评估指标有CD、EMD还有点到平面距离。CD是双向最近邻距离的均值计算简单大作业默认用这个。但CD对点云分布不均匀不敏感所以还要考虑EMD。EMD需要求解最优运输问题计算慢小样本演示没问题。我建议至少实现CD并把EMD作为加分项写在报告里。def chamfer_distance(pred, gt): # pred, gt: (N,3) diff pred[:, None, :] - gt[None, :, :] dist diff.pow(2).sum(dim-1).sqrt() return dist.min(dim1).values.mean() dist.min(dim0).values.mean()逻辑说明pred[:, None, :]把(N,3)变成(N,1,3)gt[None, :, :]变成(1,M,3)广播后得到(N,M)距离矩阵。min(dim1)是每个预测点找最近的真实点min(dim0)是每个真实点找最近的预测点双向相加保证“没被覆盖到”的方向也能反映。参数说明CD的单位和点云归一化尺度一致归一化到单位球后典型值在1e-3到1e-4之间。如果算出来大于0.1多半是单位没对齐先检查归一化。EMD可以用geomloss库或者scipy.optimize.linear_sum_assignment做近似。大作业里不要自己写匈牙利算法太慢。我只会在最终对比时用EMD因为它的结果更符合人眼认知EMD会把“点云整体往左偏”和“点云局部扭曲”区分开而CD认为两者分数可以一样。点到面距离需要干净点云的法向量用open3d.estimate_normals估算后再做点面投影。这个指标适合你最后的报告展示但法向量估算受噪声影响噪声很重时容易误导。4.3 可视化对比把点云存成PLY给MeshLab看指标会骗人人眼也会骗人所以要两边一起看。我习惯把网络输出的点云、输入含噪点云、干净点云三个文件放在同一个文件夹用CloudCompare对比。下面代码把tensor转成ply。def save_ply(points, path): pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) if not o3d.io.write_point_cloud(path, pcd): print(write failed:, path) # 假设out是模型输出centroid/max_dist来自预处理 save_ply(denormalize(out, centroid, max_dist), output.ply)保存前记得把点云反标准化回原始尺度。这里是一个常被忽略的问题训练时我们减了质心、除了max_dist测试时输出也在单位球里直接保存会让人看不出物体原来多大。denormalize函数就是normalize的逆操作把单位球坐标乘回max_dist再加回质心。可视化时给三个点云设置不同颜色比如输入红色、输出绿色、干净蓝色这样飞点一目了然。4.4 报告里怎么放结果才不容易被挑刺期末大作业的评估部分常见毛病是只贴一个loss曲线。老师会问你凭什么说这个模型好我给一个实用的报告结构一张表格行是不同噪声强度sigma0.002/0.005/0.01列是CD、EMD、点数偏差每格填3次实验的均值±方差。下面这张表是示意不是真实数据sigmaCDEMD点数偏差0.0020.00060.001100.0050.00180.003200.0100.00450.00780注意去噪网络不删点点数偏差应该恒为0。如果出现偏差说明你的实现里发生了什么意想不到的采样要去查是不是DataLoader里丢了样本。报告里还要附三张CloudCompare截图同一视角下输入、输出、干净点云。截图不要拉伸视角不然几何变形会掩盖细节。5. 避坑指南三维点云去噪最容易翻车的5个地方5.1 现象Loss收敛了输出却有大量飞点训练损失从0.05降到0.02看着很漂亮但拉出来看输出点云里有一簇离群点悬在物体外面。原因多半是训练数据里离群点比例过高且损失函数用L2。L2对离群点的误差平方放大模型宁可把正常点也往那边拽一点来降低单个大误差。解决先确认噪声生成代码里outlier_ratio没有超过0.1再把主损失换成L1。如果飞点还在写一个简单mask在loss里忽略掉距离clean超过3倍sigma的预测点到clean的距离项这样网络不会为极端离群点浪费容量。def loss_with_mask(clean, predict, sigma0.005): dist (predict - clean).norm(dim-1) mask dist 3 * sigma if mask.sum() 10: # 防止mask全假 return F.l1_loss(predict, clean) return F.l1_loss(predict[mask], clean[mask])逻辑说明mask保留离群点范围之外的正常点让网络主要学正常表面。参数3 * sigma这个阈值要跟训练噪声一致太大等于没mask太小会把正常点也丢掉。实际使用中如果发现某几个样本的飞点集中在物体内部还要检查是不是torch.cdist的topk在重复点较多时索引不稳定可以在加噪声前给点云加极小抖动打破重复坐标。5.2 现象Batch Size一调大就OOM显存不够最常见的原因是torch.cdist计算了B×N×N的距离矩阵。N2048时一个距离矩阵是2048×2048×4字节约16MB看起来不大但backward时要保存中间变量batch_size8就超过2GB。解决把N降到1024或512代价是细节损失或者换用torch_geometric的knn_graph和radius_graph它们内部用C实现不显式构造全矩阵。大作业里最省事的做法是把batch_size固定为1用梯度累积模拟大batch。model.zero_grad() for i, (noisy, clean) in enumerate(loader): loss loss_fn(clean, model(noisy)) loss.backward() if (i 1) % 4 0: # 每4步更新一次 optimizer.step() optimizer.zero_grad()模拟batch_size4的梯度累积。注意每步backward会叠加梯度4步后再step效果接近batch_size4但显存只占用batch_size1。这个技巧在报告里写清楚比单纯说“显存不够所以调小”要显得有工程素养。5.3 现象训练集指标好测试集指标崩典型场景是用三个物体训练的模型到第四个物体上点数多了一倍效果立刻变差。原因有两层一是训练和测试的点云尺度没统一模型学到了“点间距0.01”的绝对尺度遇到点间距0.02的扫描就失效二是数据增强里忘了随机缩放网络对密度变化没有适应力。解决在预处理阶段强制所有样本voxel_size一致数据增强时加随机缩放并保证测试时做同款归一化。另外测试集和训练集来自不同数据域时比如训练用CAD模型、测试用真实扫描点云密度和噪声分布都差很多需要把测试集也过一遍同样的体素滤波和归一化尽可能拉回同一分布。如果测试文件是真实扫描没有干净真值不能直接套训练集的loss指标。我一般会先手动框出几个明显是物体表面的区域用CloudCompare的区域选择功能截取小片点云再用统计滤波粗去噪后当作参考点云。对比只做局部不奢求全局老师也不会要求真实扫描必须给出定量指标。5.4 现象CD指标很低但人眼看着很糊CD衡量的是“存在一个近邻点”只要输出点云在干净点云附近哪怕所有点挤在一起也得分很低。所以你的模型可能把物体表面收缩成一团CD照样好。解决在报告里增加“输出点云的邻近点距离直方图”或者密度分布明显出现双峰就说明点云有聚集。更正式的做法是加一个排斥正则让相邻输出点之间的距离不要过小def repulsion_loss(points, eps0.01, alpha0.5): # points: (N,3)计算两两距离 dist torch.cdist(points, points) # 把对角线排除避免自己惩罚自己 dist dist torch.eye(dist.shape[0], devicepoints.device) * 999 penalty torch.clamp(eps - dist, min0) ** 2 return alpha * penalty.mean()整体loss变成chamfer_loss repulsion_loss。eps要小于平均点间距否则会把正常点焊在一起。用CD排斥损失视觉效果会从糊恢复成锐利。这里的参数alpha0.5需要根据点密度微调如果输出点云仍然聚集把alpha提到1.0如果出现点云炸开降到0.1。5.5 现象推理速度太慢没法放到实时路径里我们的模型里每次前向都要算一次torch.cdist复杂度是O(N²)。N2048时单次前向在GPU上要几十毫秒CPU上直接上秒。解决把最近邻搜索从模型里拆出来用scipy的cKDTree在CPU上算再把邻居索引作为额外输入送进网络。这样网络只剩下MLP推理速度能快一个数量级。下面第6章会专门讲这个改造和ONNX导出这里先记着不要把kNN写死在网络forward里否则后面所有优化都会卡在这一步。还有个小坑torch.cdist的backward计算量是前向的两倍所以训练时OOM往往比推理更早。如果你只是想演示推理完全可以把输入点云先体素降采样到1024点推理时间会大幅度缩短代价是输出点云也变成1024点。这个取舍要在报告的“局限”里写清楚别藏着。6. 让去噪结果真正可用邻居索引外置与ONNX加速训练好的模型如果只能在GPU上跑且每次前向都算一遍距离矩阵那这个期末大作业只能叫“演示”。想让老师眼前一亮就把推理路径改成“CPU上kNNGPU或ONNX上纯MLP”。改造方法很直接训练时把邻居索引当输入forward里不再用torch.cdist。from scipy.spatial import cKDTree def build_knn_input(x, k): if isinstance(x, torch.Tensor): x x.cpu().numpy() tree cKDTree(x) _, idx tree.query(x, kk 1) idx idx[:, 1:] # 去掉自身(N,k) knn x[idx] # (N,k,3) local knn - x[:, None, :] return x.astype(np.float32), local.astype(np.float32)改造后的模型只吃[x, local]不再自己找邻居forward里删掉torch.cdist和torch.stack只留MLP。这样就可以用torch.onnx.export导出dummy_x torch.randn(1, 2048, 3) dummy_local torch.randn(1, 2048, 16 * 3) torch.onnx.export(model, (dummy_x, dummy_local), denoiser.onnx, input_names[x, local], dynamic_axes{x: {0: batch, 1: num_points}})这段代码只导出纯MLP避免kNN算子进入ONNX。如果你的模型里还有循环先把它改成cat或view。ONNX Runtime推理时用session.run输出直接就是去噪后的坐标。至于密度自适应可以在kNN阶段根据邻居距离动态调整偏移量局部点间距大就减少偏移幅度稀疏区域就不会被拉平。这个技巧写在报告里很加分但代码量不大其实就是在build_knn_input里多返回一个mean_neighbor_dist拼到特征里让网络自己学会“稀疏区域少动一点”。我自己的习惯是先跑通再优化。第一版永远用最笨的方法torch.cdist能跑就先用指标正常后再做kNN外置和ONNX导出。期末大作业最怕的不是效果差而是网络结构复杂到你自己都解释不清。把数据、模型、损失、指标每一步都留下中间文件老师问起来你能当场打开CloudCompare指出飞点在哪轮开始消失这比任何“调参玄学”都有说服力。希望这篇笔记帮你在点云去噪这条路上少走几个来回。本文还有配套的精品资源点击获取
返回列表