ARTICLE DETAIL

资讯详情

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

基于深度学习的点云去噪实战:从PointNet到期末大作业

基于深度学习的点云去噪实战:从PointNet到期末大作业 简介这是一份基于 Python 深度学习的三维点云去噪完整项目曾以 97 分获导师认可适合高校学生用于课程设计或期末大作业也可作为初学者学习点云处理的实战范例。资源共四十一个文件以三十六个 Python 脚本为主体辅以三个配置文档、一个说明文件及忽略规则文件整体仅 59KB轻量易部署。目前已有 204 人学习下载。代码按功能划分清晰涵盖数据加噪、去噪模型、离群点移除、整体流程与训练评估等模块并配有环境配置说明下载后无需修改即可运行。通过该项目可掌握三维点云去噪的完整实现链路包括数据预处理、模型构建、训练评估与结果量化打分对完成高分课设或快速上手深度学习点云任务有直接帮助尤其适合需要提交完整代码与说明文档的课程验收场景。1. 基于python深度学习的三维点云去噪源码先想清楚这份作业要交出什么拿到“基于python深度学习的三维点云去噪源码高分期末大作业.zip”这样的压缩包先别急着解压双击运行。三维点云去噪这个题目真正考察的是四件事你对噪声来源有没有建模、对深度学习模型有没有正确选型、能不能把训练和评估流程跑通、能不能把结论可视化到让老师一眼看懂。学期末交这门课的大作业最怕的不是模型效果差而是代码和报告对不上——网络结构写的是PointNet代码里却在调open3d的统计滤波指标写的是Chamfer距离实验里却只给了loss曲线。这篇文章我会按一个能拿高分的落地路径来讲从噪声建模、网络选型、训练参数到评价指标可视化以及期末季最容易踩的那几个坑。2. 三维点云去噪为什么需要深度学习先把噪声建模和网络选型的逻辑立住2.1 点云噪声的三种来源与建模方式高斯漂移、离群弹点与局部稀疏做点云去噪第一步不是写网络而是把“噪声”这个词定义清楚。激光雷达或深度相机扫出来的原始点云噪声主要来自三个方向第一是传感器精度波动每个点会围绕真实表面产生一个几毫米到几厘米的随机位移在数学上通常建模为高斯噪声也就是在xyz三个通道上叠加独立同分布的N(0, σ²)扰动第二是镜面反射、遮挡边缘或空气中的尘雾造成的离群点表现为一小撮点悬浮在真实物体表面之外距离主表面可能超过物体半径的一半前后两帧扫描里这些点还会随机跳位置第三是设备丢点造成的局部稀疏直观感受就是物体表面被啃掉了几块这种问题靠“去噪”已经很难处理一般归到补全方向。期末大作业里最常见的组合是“高斯噪声离群点”原因很简单这两种噪声人为可控方便做成不同难度梯度也方便计算去噪前后和真值之间的误差。我一般会把噪声强度分成三档低档σ0.005米、中档σ0.01米、高档σ0.03米离群点比例从0%加到10%。之所以这么做是因为评分老师最想看的是“你的方法在不同噪声强度下是如何退化的”而不是只有一个孤零零的最好效果。判断噪声类型有个很实用的诊断手段算每个点到最近邻的距离画直方图。纯高斯噪声下这个直方图是单峰的均值大于0但集中混入离群点后直方图会出现一条拖得很长的尾巴——这些尾巴就是离群点。做数据准备的阶段先花十分钟做这个诊断能明确告诉你模型需要学的是“细微扰动”还是“飞点”这对后面调loss权重有直接影响。2.2 为什么统计滤波能跑但拿不到高分传统方法的三个边界很多同学拿到题目第一反应是调open3d的统计滤波(Statistical Outlier Removal)也确实能出效果。统计滤波的原理是对每个点计算它到k个近邻的平均距离所有点的平均距离构成一个分布把距离大于“全局均值 n倍标准差”的点判定为离群点删掉。英文文献里通常写成dist_mean n * dist_stdopen3d的接口remove_statistical_outlier(nb_neighbors20, std_ratio2.0)就是这两个参数。这套方法对付第2.1节里的第二类噪声离群点很有效但它的两个短板决定了它在大作业里只能当基线不能当主方法。第一它对高斯噪声几乎无能为力——因为高斯扰动让每个点都带上了小偏移近邻距离分布依然很集中没有任何点会被判为“异常”滤波完点云只是变稀疏离真值的平均距离没有明显下降。第二滤波参数严重依赖物体形态和采样密度同一个k值在密集的平面区域和稀疏的边缘区域表现完全不同你调好一个模型的参数换一个物体效果就崩这种“手调几何假设”的做法和深度学习的自适应能力相比差距很明显。还有个容易忽略的边界统计滤波对薄片状结构极不友好。比如一张桌面的边缘点云只有一两个点厚近邻平均距离天然偏大std_ratio稍微设低一点就会把真实的边缘点当成噪声删掉导致去噪后轮廓收缩。深度学习模型在训练阶段见过大量局部几何结构能学会区分“边缘薄片上的点”和“飞在空中的点”这是传统方法不具备的。我在指导期末项目时会明确要求学生做一组基线对比原始带噪点云、统计滤波结果、深度模型结果三个都算指标。这么做不是为了凑篇幅而是让报告呈现“我知道传统方法哪里不够好所以我选了深度学习”的完整逻辑链。只看最终效果图而不解释方法演进的报告在答辩时很容易被一句“你为什么不直接用滤波”问住。2.3 深度学习方法为什么落到PointNet骨架无序集合与逐点分类的天然契合三维点云和图像最大的区别在于点云是无序集合同一个物体把点的顺序打乱表示的还是同一个几何。普通卷积核依赖规则网格直接把CNN搬过来不现实Transformer虽然能处理集合数据但期末大作业的时间只够跑轻量级网络。PointNet正好卡在这个位置它用共享权重的多层感知机对每个点做特征提取再用最大池化把所有点的特征聚合为一个全局特征——最大池化天然具有置换不变性输入点顺序无论怎么打乱池化结果都一致。这个性质对点云去噪来说是决定性的它保证网络不会在“第几个点”上学到任何伪规律。去噪任务和分类任务在PointNet上的用法有一点关键差异。分类是在全局特征后面接分类头而去噪需要输出“每个点该不该保留”所以要把全局特征广播回每个点和逐点特征拼起来再接一个输出通道为1的卷积头过sigmoid得到保留概率。这样网络同时看到了“我这一段局部几何长什么样”和“整个物体是什么结构”信息量比纯粹逐点判断大得多。为什么不直接选更强的PointNetPointNet多了Set Abstraction和密度自适应层对密度的鲁棒性确实更好但模型体积和训练耗时都上去了。期末大作业考察的是完整链路而不是SOTA用PointNet打底遇到效果不够时再在这个骨架上加模块自注意力、局部邻域聚合是性价比最高的路线。另外也可以考虑DGCNN这种动态图卷积但它在构建k近邻图时比PointNet慢不少数据量大了以后训练时间会很难看。做课程项目永远记得一个原则一切以“能在截止日前完整跑完”为优先。3. 从零搭一个可复现的点云去噪训练流程数据、网络、参数一把梭3.1 生成带噪点云open3d读入、加高斯噪声与离群点、存成ply有监督去噪方案需要真值标签。期末大作业最常用的做法是先用一个干净mesh采样出点云或者从ShapeNet这类数据集里加载干净点云再人工加噪声。这样每个输入点都知道自己的“干净坐标”训练时让模型预测每个点是不是被污染就完成了数据准备。下面这段代码会读入一个ply文件加高斯噪声和离群点然后保存为新的ply。注意这句话代码里“读入—加噪—保存”的过程要写成函数方便后面做多组噪声强度的实验。import numpy as np import open3d as o3d def load_points(path): 读取ply/xyz/obj, 统一返回 [N, 3] float64数组 pcd o3d.io.read_point_cloud(path) return np.asarray(pcd.points) def add_noise(points, sigma0.02, outlier_ratio0.05, seed0): 给干净点云加高斯噪声和离群点, sigma单位与点云坐标一致 rng np.random.default_rng(seed) noisy points rng.normal(0, sigma, points.shape) n_out int(len(points) * outlier_ratio) if n_out 0: # 离群点沿中心到点方向外推, 模拟飞在表面之外的弹点 centroid noisy.mean(axis0) vec noisy[rng.choice(len(noisy), n_out, replaceFalse)] - centroid norm np.linalg.norm(vec, axis1, keepdimsTrue) 1e-8 radius np.linalg.norm(noisy - centroid, axis1).mean() # 外推2倍平均半径, 视觉上明显是离群点 outliers centroid (vec / norm) * (radius * 2.0) noisy np.vstack([noisy, outliers]) # 打乱顺序, 避免模型在点序号上学到任何规律 idx rng.permutation(len(noisy)) return noisy[idx] def save_points(path, points): 写ply, 注意转float32避免类型问题 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points.astype(np.float32)) o3d.io.write_point_cloud(path, pcd)这段代码的细节值得说几句。第一噪声标准差sigma直接用米制单位不要用归一化之后的无单位数——因为报告里需要写“高斯噪声σ0.02m”这样让老师能直观理解的话。第二离群点生成不用全空间随机撒点而是沿“质心到点”的方向外推两倍平均半径这样制造出来的离群点悬浮在表面外侧符合真实传感器在边缘处测到飞点的物理直觉。第三保存时统一转float32open3d内部读写有时会产生float64而PyTorch默认float32后面训练时类型不匹配会报一堆莫名其妙的问题。要点提示seed参数一定要暴露出来。期末报告里写“实验结果可复现”不是空话评测老师现场跑你的代码用了不同seed可能会得到完全不同的指标。固定随机种子是对自己项目负责。3.2 用PyTorch实现PointNet去噪核心网络逐点保留概率输出网络结构这部分只给核心类不去贴PointNet的完整分类头配置——因为去噪任务只需要逐点二分类。import torch import torch.nn as nn import torch.nn.functional as F class PointDenoiser(nn.Module): 输入: [B, N, 3] 带噪点云 输出: [B, N] 每个点的保留概率(0~1) def __init__(self, in_features3, hidden64): super().__init__() # 共享MLP: 逐点提取局部特征 self.mlp1 nn.Sequential( nn.Conv1d(in_features, hidden, 1), nn.BatchNorm1d(hidden), nn.ReLU(), nn.Conv1d(hidden, hidden * 2, 1), nn.BatchNorm1d(hidden * 2), nn.ReLU(), ) self.mlp2 nn.Sequential( nn.Conv1d(hidden * 2, hidden, 1), nn.BatchNorm1d(hidden), nn.ReLU(), ) # 拼接全局特征和原始坐标, 输出单通道概率 self.head nn.Conv1d(hidden hidden * 2 in_features, 1, 1) def forward(self, x): # x: [B, 3, N], 注意输入需要先转置 feat self.mlp1(x) # [B, 128, N] g, _ feat.max(dim2, keepdimTrue) # [B, 128, 1] 全局特征 g g.expand_as(feat) # 广播到每个点 feat2 self.mlp2(feat) # [B, 64, N] cat torch.cat([feat2, g, x], dim1) # [B, 641283, N] prob torch.sigmoid(self.head(cat)) # [B, 1, N] return prob.squeeze(1) # [B, N]代码逻辑拆开看第一段MLP把每个点的3维坐标映射成128维特征这里用的是Conv1d卷积核大小为1本质上就是对每个点做一次独立的全连接卷积只是为了兼容批量维度。然后feat.max(dim2)在N这一维上取最大值得到128维的全局特征向量再expand回每个点。第二段MLP把128维局部特征压缩到64维最后把“64维局部 128维全局 原始3维坐标”拼起来过一个1x1卷积输出一个logitsigmoid之后就是保留概率。这个设计的核心思想是模型的决策依据必须同时包含局部几何和全局形状。单看局部几何一个点可能因为采样不均匀显得像噪声但结合全局形状网络就知道这个点其实落在桌面的连续曲面上。另外把原始xyz也拼进head层是给网络保留了“直接看坐标判断”的短路路径训练早期很好用。参数改动建议hidden64适合N8192以下的小点云如果你的显存紧张或者点数特别大可以降到32。in_features默认3维如果后续把法线拼进输入改成6维。3.3 训练主循环与超参数搭配loss加权是成败关键训练代码本身不复杂难点全在正负样本不均衡。一个典型的训练集里离群点占比通常在5%~10%高斯噪声点虽然每个都有偏移但在二分类标签里它们还是“保留点”真正要删的只有离群点。这意味着负样本保留点占了90%以上如果直接算交叉熵模型只要无脑输出1loss就已经很低了。def train_one_epoch(model, loader, opt, alpha5.0, devicecuda): model.train() total_loss 0.0 for pts, label in loader: # pts: [B, N, 3] label: [B, N] 其中1干净点, 0噪声点 pts pts.transpose(1, 2).to(device) # [B, 3, N] label label.to(device) prob model(pts) # [B, N] # 正负样本加权: 干净点权重alpha, 噪声点权重1 pos_w label * alpha neg_w (1 - label) * 1.0 loss F.binary_cross_entropy( prob, label.float(), weightpos_w neg_w ) opt.zero_grad() loss.backward() opt.step() total_loss loss.item() * pts.size(0) return total_loss / len(loader.dataset)逻辑说明weight参数按元素给每个点的loss乘系数。label为1干净点的点对应的权重是alphalabel为0噪声点的点权重是1。这样网络每把一个真实噪声点误判成干净点承担的loss惩罚是反方向的5倍模型就不得不认真处理那些少数类样本。alpha的取值有规律噪声点占比越低alpha要越高。我的经验是噪声点占5%时alpha设5~10占1%时可能需要20甚至50。超参数搭配我一般这样定优化器Adam初始学习率1e-3batch size 16epoch 50。学习率每20个epoch乘0.5或者直接用torch.optim.lr_scheduler.CosineAnnealingLR。如果模型在30个epoch时loss已经不再下降说明训练数据太简单不要死磕epoch先去检查是不是噪声强度设低了。另外一个经常被忽略的参数是固定点数。训练时要把所有样本降采样到同一个N否则DataLoader里没法组batch。我通常用8192点。点数太小2048会让模型看不到足够完整的全局结构点数太大32768会明显拖慢训练而且对期末作业来说没必要。3.4 训练前的坐标归一化与去噪结果的导出坐标归一化这个步骤一定要放在生成数据和训练之间否则你会遇到loss降不下去或者训练极其不稳定的问题。真实扫描点云的尺度差异可以非常大一个毫米级零件和一个十米级房间坐标数值差了三个数量级。如果不归一化网络第一层MLP的输入数值范围就完全不同BatchNorm虽然能缓解但模型需要浪费大量epoch去适配尺度效果还很差。def normalize_points(points): 以质心为中心, 缩放到半径1的球体内 centroid points.mean(axis0) points points - centroid radius np.linalg.norm(points, axis1).max() return points / radius, centroid, radius def denormalize_points(points, centroid, radius): 推理完成后恢复原始尺度, 便于和真值对比 return points * radius centroid这段代码在训练前调用一次保存centroid和radius推理后把预测结果反归一化回原始坐标再去和真值算Chamfer距离。注意指标一定要在原始尺度上计算否则报告里的“去噪误差0.8cm”就失去物理意义了。我自己踩过这个坑数据归一化到单位球之后所有指标都变成零点几老师问“0.3是什么单位”时很难解释。导出去噪结果建议同时存ply和npz。ply给可视化用npz保存mask、prob和清理后的坐标后面算指标和画对比图不用反复去读ply。def export_denoised(points, prob, thr0.5, pathdenoised.ply): points: [N,3] 原始坐标, prob: [N] 保留概率 mask prob thr cleaned points[mask] prob_keep prob[mask] pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(cleaned) # 用概率染色: 红高置信保留, 蓝勉强保留 colors np.zeros((len(cleaned), 3)) colors[:, 0] prob_keep colors[:, 2] 1.0 - prob_keep pcd.colors o3d.utility.Vector3dVector(colors) o3d.io.write_point_cloud(path, pcd) # 同时存npz, 方便后续指标计算 np.savez(denoised.npz, pointscleaned, probprob_keep)这里的thr0.5是默认分界线。实操中你会发现很多点的概率落在0.4~0.6之间不要急着调阈值。先检查训练数据里噪声点的标签是否干净以及alpha是否偏低。阈值后调是治标把模型训练好才是治本。4. 期末大作业版评价指标Chamfer距离、PSNR与三张必做的图4.1 三个必须算的指标CD、简化PSNR与F-score深度学习的点云去噪论文一般用Chamfer Distance和Earth Movers Distance。EMD计算代价很高期末大作业用Chamfer距离作为主指标就够了。Chamfer距离定义为对去噪结果的每个点找它在真值点云里的最近邻算平均距离反过来再算一次两项相加。这个指标同时惩罚“过度删除”和“欠去噪”如果模型把表面细节全部磨平了去噪点离真值的距离不会小如果模型疯狂删点反向距离会飙升。from scipy.spatial import cKDTree def chamfer_distance(src, tgt): src: 去噪结果[N,3], tgt: 真值[M,3] 返回标量, 越小越好 tree_src cKDTree(src) tree_tgt cKDTree(tgt) d_s2t, _ tree_tgt.query(src, k1) # src每个点最近邻距离 d_t2s, _ tree_src.query(tgt, k1) return d_s2t.mean() d_t2s.mean()逻辑说明这里用cKDTree做最近邻搜索复杂度是O(N log N)比暴力计算快很多。单位与点云输入一致如果你的点云是米制CD就是米如果是毫米制CD就是毫米。报告里CD数值一般在0.005~0.05之间比较常见所以建议在表头标注单位避免出现“差距看着很小”的误解。第二个指标是简化版PSNR。图像PSNR依赖像素值的峰值点云没有这个概念。常见做法是把点云坐标归一化到固定尺度比如1.0然后按坐标均方误差定义峰值信噪比。def psnr_simple(points_gt, points_clean, max_range1.0): 简化版点云PSNR: 先把坐标缩放到max_range尺度再算MSE gt points_gt.astype(np.float64) clean points_clean.astype(np.float64) # 各自归一化 for p in (gt, clean): p - p.mean(axis0) radius np.linalg.norm(p, axis1).max() p / radius 1e-8 mse ((gt - clean) ** 2).mean(axisNone) return 10 * np.log10((max_range ** 2) / (mse 1e-8))注意这个PSNR是简化版答辩老师问起来要能解释清楚。它的问题在于点云是稀疏集合直接比对坐标的点对并不对应所以PSNR只能作为辅助指标主指标还是Chamfer距离。第三个指标F-score也很实用。做法是设定一个距离阈值τ对去噪后的每个点如果它在真值点云里的最近邻距离小于τ就算“命中”。然后统计所有命中点占比得到精度反过来算召回再取调和平均。def f_score(src, tgt, tau0.01): tau是容忍误差阈值, 通常设为噪声标准差或更小 tree_tgt cKDTree(tgt) d_s2t, _ tree_tgt.query(src, k1) precision (d_s2t tau).mean() tree_src cKDTree(src) d_t2s, _ tree_src.query(tgt, k1) recall (d_t2s tau).mean() return 2 * precision * recall / (precision recall 1e-8)F-score的价值在于它直接反映“有没有把该留的点留住该删的删掉”比CD更直观也更接近任务本质。期末报告里建议三个指标都算CD和F-score做主表PSNR放附录或备注。4.2 做一张让老师一眼看懂的效果对比图可视化质量在大作业评分里的权重比很多人想象的高得多。模型效果好不好老师最终是靠眼睛判断的——指标表再漂亮图上看不出差异也会被怀疑。我的做法是用open3d的渲染窗口配固定视角导出一组图后用PIL拼接成四联图原始带噪、真值、统计滤波结果、深度模型去噪效果。四张图的相机位姿必须一致否则观察者会误以为几何差异是视角差异造成的。import open3d as o3d from PIL import Image def render_view(pcd_path, out_path, view_params): 打开渲染窗口并截图保存, view_params控制相机位置 vis o3d.visualization.Visualizer() vis.create_window(width800, height600, visibleFalse) vis.add_geometry(o3d.io.read_point_cloud(pcd_path)) ctr vis.get_view_control() ctr.convert_from_pinhole_camera_parameters(view_params, allow_arbitraryTrue) # 白底合理点大小, 避免渲染成黑色一团 opt vis.get_render_option() opt.background_color np.array([1, 1, 1]) opt.point_size 2.0 vis.poll_events() vis.update_renderer() vis.capture_screen_image(out_path) vis.destroy_window()渲染参数里有几个坑要专门说。第一背景色务必设成白色或浅灰色open3d默认黑色背景会让稀疏点云看起来很压抑而且打印出来费墨水。第二point_size根据点数大小调整点数多时设1.0避免粘连点数少时设3.0避免看不清。第三allow_arbitraryTrue允许所有既有视角直接复用保证四张图位姿完全一致。拼接四联图时注意子图之间的间隔和标注每张图下方用PIL画一行文字标注方法名。最终输出分辨率建议在1600×1200以上放在报告里占半页清晰度足够。4.3 结果表怎么组织噪声强度x方法的指标矩阵才是报告主菜期末报告的实验结果部分最忌讳只给一个“去噪前后对比”的图那更像演示而不是实验。一份能撑住答辩的实验部分至少包含一张“噪声梯度x方法”的指标矩阵表。行方向是噪声强度低/中/高列方向是“原始带噪输入 / 统计滤波 / 深度模型去噪”每个格子里写CD值括号里写F-score。方法低噪声 σ0.005m中噪声 σ0.01m高噪声 σ0.03m原始带噪输入CD 0.85cm / F 0.82CD 1.42cm / F 0.70CD 2.61cm / F 0.51统计滤波CD 0.79cm / F 0.84CD 1.30cm / F 0.72CD 2.44cm / F 0.53PointNet去噪CD 0.33cm / F 0.95CD 0.52cm / F 0.91CD 1.08cm / F 0.86这张表的数字不是我跑出来的精确结果只是用来演示“报告应该呈现什么样的信息结构”。注意每一行的阅读方式从上往下看统计滤波相对原始输入提升有限深度模型在中低噪声下把CD压到了原来的三分之一左右。高噪声下所有方法都在退化但深度模型退化的斜率更平滑——这个结论正是老师想看的“方法论价值”。除了这张主表还要画一条loss曲线。用训练时记录的历史loss值画出横轴epoch、纵轴loss的折线叠加train和val两条线。val loss如果拐头上升说明过拟合这时可以拿掉一部分训练数据或者加dropoutval loss如果不降说明模型容量不够或学习率偏低。这张图不需要复杂工具matplotlib三行代码的事但它能证明“训练过程是健康的”避免老师怀疑你的结果是不是过拟合出来的。5. 避坑跑点云去噪源码最容易翻车的5个地方5.1 现象程序一跑就显存溢出batch size降到2还是崩报错通常是CUDA out of memory但奇怪的是你把batch从16调到2还是崩。原因多半不是batch too大而是单样本点数N太大。PointNet中间特征张量是[B, C, N]N一旦到几十万甚至上百万中间变量轻松吃掉几十GB显存。解决方法是训练前对所有样本降采样到固定点数我一般用voxel_down_sample(voxel_size0.005)或随机采样到8192点。这里强调用体素降采样而不是随机采样是因为随机采样会无差别丢弃表面细节让真值本身失真。体素降采样在每个格子内取平均能在降密度的同时保留几何形态。显存量小的机器还可以把N降到4096模型效果会下降但不会崩。5.2 现象loss一直不降卡在0.6附近不动这是最典型也最磨人的翻车点。loss卡在0.6左右sigmoid输出大概在0.5附近徘徊说明模型在“瞎猜”。先查一个最常见的元凶输入坐标没有归一化。Fl点云的xyz如果是米制物体半径2米网络第一层的输出数值范围会非常大BatchNorm虽然缓解了部分问题但模型要花大量epoch去适应不同样本的尺度差异。另外要查的是标签有没有反。有些同学在生成数据时把“1”标给了噪声点模型学到的映射恰好反了loss也降不下去。调试方法很简单取一个batch打印label.unique()确认标签分布符合预期然后单独取几个样本可视化看看带噪输入和干净真值是不是对应上了。这一步在dataloader写完后立刻做不要等训练跑完再排查。我自己的习惯是训练脚本里加一个debug开关打开后只跑一个batch并直接退出专门用来验证数据流水线。5.3 现象loss降得很好但输出的ply文件里全是NaNloss曲线漂亮但推理结果全是NaN这往往是两个原因之一一是训练后期学习率过大导致head层数值发散sigmoid之前的中间激活数值爆炸成inf二是推理代码少了torch.no_grad()或torch.inference_mode()导致显存和梯度记录疯涨最终溢出。解决思路分两头。训练侧把初始学习率从1e-3降到3e-4如果还有NaN就继续降到1e-4。这在期末场景下完全够用不需要去研究学习率调度策略。推理侧在模型前向外面套一层with torch.inference_mode():并且手动model.eval()关掉BatchNorm的统计更新。还有一个小概率原因是ply读入了非法点比如坐标出现inf清洗数据的阶段顺便做一次np.isfinite(points).all(axis1)过滤一下。5.4 现象可视化窗口打开后一坨黑看不见点云形状open3d默认黑背景加上点数大、点尺寸小渲染出来很容易是一片黑。没有经验的同学会以为模型输出了空点云其实是渲染配置的问题。解决首先把背景色改为白色或浅灰opt.background_color np.array([1, 1, 1])。然后把点尺寸调大opt.point_size max(1.0, 2000.0 / N)N是当前点数。如果点云尺度在毫米级别先做个归一化再显示否则坐标值太大相机初始位置会落在非常远的地方看到的是一团黑。最后显示前用pcd.translate(-pcd.get_center())把质心移到原点相机视角立刻可控。另外一个影响观感的因素是渲染窗口默认显示坐标系背景里会有个小三色轴截图放在报告里显得不专业。opt.show_coordinate_frame False关掉它即可。5.5 现象所有指标都很好但肉眼看去噪结果和输入几乎没差别这是最隐蔽也是最容易翻车的点模型学到了“拷贝输入”。具体表现是CD降低明显、F-score很高但把去噪前后的点云叠在一起看几乎重合。原因在于训练数据中噪声点占比太低网络发现只要把所有点都判为保留loss就已经很低因为大部分点都是干净点交叉熵的贡献被“简单样本”主导了。解决思路在3.3节已经提过但这里再强调一次正样本权重alpha一定要按噪声点比例反推。如果你的离群点只有3%alpha直接拉到20如果噪声点占比已经到10%alpha可以降到5。另外训练完成后单独打印一下“真实噪声点被删除的比例”和“干净点被删除的比例”这两个数分别对应召回和误删率。如果前者低于80%说明模型确实没有学会去噪指标再好看也是假的。还有一个可视化技巧能戳穿这个假象把去噪结果和原始输入用一个半透明渲染叠在一起如果除离群点外几乎完全重合属于正常如果去噪后的点云在表面内侧偏移说明模型把噪声当成了特征来“平滑”——这时候要检查loss定义是不是有问题比如是不是错误地用坐标回归代替了分类。6. 把这份源码改造成自己的三个加倍加分的小改进与答辩验收清单6.1 最轻量的改进把法线特征拼进输入基础模型跑通后最安全且高效的一个改进是把输入从3维xyz扩到6维——额外拼接每个点的法线。用open3d的estimate_normals从带噪点云估算噪声大时法线会被污染但网络会学会一个很有意思的行为当法线方向与邻域趋势不一致时这个点很可能就是噪声点。实现时只需要把in_features改成6并把法线拼接到坐标后面。从报告角度讲这也能体现你对“深度学习如何利用几何属性”有思考而不仅仅是调包。6.2 加一个轻量的自注意力模块如果法线还不够提分可以在feat.max(dim2)之前加一个轻量自注意力。思路是用点的局部特征两两算相似度得到一个注意力权重矩阵对邻域特征做加权聚合。实现的代价很小通常十来行代码但注意点云点数多时自注意力的O(N²)开销不可忽略可以先降采样到4096点再做注意力。这属于“我能写但老师不一定会问细节”的加分项前提是你能在答辩时把它讲清楚。6.3 答辩验收清单五样东西备齐再提交期末大作业不是代码跑完就能交的验收时老师可能随时打断你问问题。我给自己的项目定过一份答辩清单第一程序能从原始ply一路跑到输出ply和指标csv全程不需要人工干预第二报告里有噪声建模明确写了高斯噪声sigma和离群点生成方式第三指标表里有不同噪声难度下的对比证明方法的普适性第四可视化存了至少三组不同噪声等级的对比图全部用同一相机位姿第五能现场说出正样本权重alpha为什么这么设、阈值0.5是怎么来的。这五条对应的是完整闭环数据有根据、方法有选型、实验有梯度、结果可复现、答辩能解释。我自己复现过多次类似项目最后发现丢分的从来不是模型性能而是说不清楚每个环节为什么存在、参数为什么这么设。你把这个逻辑理顺了期末作业拿高分只是顺带的事真正收获的是从数据到评估的完整动手流程这个能力在后面的深度学习课程里会一直用得上。希望帮到你。本文还有配套的精品资源点击获取
返回列表