ARTICLE DETAIL

资讯详情

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

SUPnet点云语义分割实战:S3DIS数据预处理与PointNet++训练

SUPnet点云语义分割实战:S3DIS数据预处理与PointNet++训练 简介面向计算机视觉毕设与课程作业的深度学习场景语义分割项目包聚焦城市街景、室内场景等语义分割任务包含二十四个文件、约零点八兆字节以Python脚本为主辅以XML配置、TXT说明、PNG结果图等。涵盖网络定义、训练测试脚本、数据加载与预处理工具、点云工具模块等可支撑从数据准备、模型训练到效果评估的完整流程。项目已有157人学习下载具备一定参考热度。内容融合Python灵活开发与C高效部署的混合思路既能借助TensorFlow、PyTorch等框架快速搭建模型也提供了面向高性能推理的系统构建示例。训练与评估部分包含交叉熵损失、Adam优化器、学习率衰减、像素准确率与平均IoU等关键知识点对完成毕设或课程设计、理解场景语义分割工程化实现具有较高参考价值。1. 场景语义分割毕设这份SUPnet代码包到底能帮你解决什么把压缩包解压第一眼看到的是SUPnet.py、train_SUPnet.py、train_PW-ATM.py、train_FG_F1F2.py再加上data_utils、models两个目录和一份README。这基本就是一个PyTorch写的、跑S3DIS室内场景点云语义分割的完整工程。场景语义分割要干的事是把每个点标到13个类别之一天花板、地板、墙、柱子、桌椅、柜子这些不是对二维图像逐像素分类输入是坐标、法线、归一化位置输出是每个点的类别概率。这套代码适合两类人一类是毕设和课程作业需要「能训练、能出图、能算mIoU」完整流程的学生另一类是想研究PointNet在场景解析上怎么改进、怎么加注意力模块的从业者。它把原始数据转换、数据加载、网络训练、评估一条线串好了拿到手不用从零搭工程重点是理解主网络和两条训练脚本分别在做什么、参数怎么调。下面先从数据链路拆起这一层决定了后续能不能顺利训起来。2. 数据链路拆解从S3DIS原始数据到网络能吃的Batch2.1 collect_indoor3d_data.py原始房间如何切成固定大小的块S3DIS数据集原始是按房间给的每个房间一个点云文件包含xyz坐标、法线、颜色、还有每个点的语义标签。直接用整个房间训练有两个麻烦一来不同房间点数差异很大有的房间几万点有的二十万点二来模型输入要求固定点数直接resample会破坏局部结构。collect_indoor3d_data.py解决这个问题的思路是把每个房间切成一米见方的块每个块再统一采样到固定点数。常见做法是indoor3d_util.py里那个room2blocks函数干这件事核心参数有三个block_size块大小默认1.0米、stride滑动步长默认0.5米、sample_rate采样率默认0.5。stride比block_size小意味着相邻块之间有重叠这是刻意为之目的是让目标在块边界附近时不会被拦腰切断。我在自己机器上复现时推荐先把sample_rate调成1.0跑一个小房间验证格式全量数据用0.5能省不少磁盘空间。# indoor3d_util.py 中关键转换逻辑按项目常见实现还原 def room2blocks(data, label, num_point, block_size1.0, stride0.5, random_sampleFalse, sample_rateNone, block_numNone): # data: [N, 6] 每行是 x y z nx ny nzlabel: [N] # 先把房间用滑动窗口切成多个块 data_blocks, label_blocks [], [] cur_x, cur_y 0, 0 while cur_x block_size data.max(axis0)[0] 1e-4: while cur_y block_size data.max(axis0)[1] 1e-4: # 取出当前窗口内的点和标签 mask_x (data[:, 0] cur_x) (data[:, 0] cur_x block_size) mask_y (data[:, 1] cur_y) (data[:, 1] cur_y block_size) mask mask_x mask_y block_data, block_label data[mask], label[mask] # 对点数不足的块做随机上采样对过多的块做随机下采样 if len(block_data) num_point: choice np.random.choice(len(block_data), num_point, replaceTrue) else: choice np.random.choice(len(block_data), num_point, replaceFalse) block_data, block_label block_data[choice], block_label[choice] data_blocks.append(block_data) label_blocks.append(block_label) cur_y stride cur_y 0 cur_x stride return np.stack(data_blocks), np.stack(label_blocks)这段代码里有两个容易被忽略的细节。第一个是mask用的是全局坐标的x和yz轴不管因为S3DIS房间的z轴范围大按xy平面切块更符合室内场景的空间分布。第二个是上采样用了replaceTrue意味着点数不足的块会重复采样某些点这是为了保证每个块固定输出num_point个点模型不需要处理动态长度。输出是npy文件每个房间对应若干个块块数等于((room_x - block_size) / stride 1) * ((room_y - block_size) / stride 1)。这个公式可以提前算出来如果某个房间块数异常少多半是原始点云里z轴方向有离群点需要先做统计滤波否则块里全是孤立点训练出来的模型在这几个块上基本是乱的。2.2 S3DISDataLoader.py与provider.py数据加载和增强如何配合S3DISDataLoader.py负责把npy文件按批次喂给网络。它和indoor3d_util.py的分工是后者把S3DIS原始数据转成每类场景的块文件前者在训练时加载这些块并提供shuffle、按epoch循环的能力。训练时的数据流我用下表概括方便对照文件找代码文件职责关键产出collect_indoor3d_data.py遍历所有房间调用indoor3d_util.py完成转换每个场景的npy块文件indoor3d_util.py提供room2blocks、room2blocks_plus等转换函数固定点数的块和标签S3DISDataLoader.py实现S3DISDataset和S3DISDataLoader返回batch的points、label、room_idprovider.py训练时实时做数据增强旋转、平移抖动、随机丢弃点S3DISDataLoader.py的__getitem__一般返回三个东西points的形状是[num_point, 9]9个通道是归一化坐标、原始坐标、法线或加上RGB取决于原始数据映射label的形状是[num_point]还有room_id用来在测试时按房间聚合结果。加载阶段还有个常被忽略的细节是点的归一化。因为一个块可能离原点很远直接把绝对坐标喂给网络会让第一层卷积的输入分布偏移严重常见做法是减去块中心坐标把块内坐标归一化到零点附近。这段逻辑在train_SUPnet.py的load_data或DataLoader里经常会看到改动它会影响模型收敛速度。如果发现训练前期loss下降特别慢先检查是不是归一化被注释掉了。provider.py里的数据增强值得单独说。点云增强和图像增强不同不能随便做翻转和裁剪因为坐标是有物理含义的。常见增强组合是绕z轴旋转一个小角度、加高斯抖动、随机丢弃一部分点模拟遮挡。# provider.py 中旋转与抖动增强的常见实现 def rotate_perturbation_point_cloud(pc, angle_sigma0.06, angle_clip0.18): # pc: [B, N, 3]绕z轴随机旋转模拟扫描角度差异 angles np.random.normal(0, angle_sigma, size(pc.shape[0],)) angles np.clip(angles, -angle_clip, angle_clip) cosval np.cos(angles) sinval np.sin(angles) rot_matrix np.array([[cosval, -sinval, 0], [sinval, cosval, 0], [0, 0, 1]]) rot_matrix np.transpose(rot_matrix, (2, 0, 1)) return np.einsum(bij,bnj-bni, rot_matrix, pc) def random_dropout_point_cloud(pc, max_dropout_ratio0.2): # 每个样本随机丢弃最多20%的点模拟遮挡和不完整扫描 dropout_ratio np.random.random() * max_dropout_ratio drop_idx np.where(np.random.random((pc.shape[0], pc.shape[1])) dropout_ratio) pc[drop_idx] pc[0, 0, :] # 用第一个点的坐标填充等价于置为无效点 return pcrotate_perturbation_point_cloud里angle_sigma0.06意味着旋转角度标准差约3.4度这个幅度对室内语义分割是安全的设太大模型会把“墙是竖着的、地板是平的”这个先验学歪。random_dropout_point_cloud里max_dropout_ratio0.2也别乱调大点云本来就稀疏丢太多会让边界类别直接崩掉。测试阶段这两项都必须关掉只保留坐标归一化否则每个测试样本都带着噪声mIoU会虚高或虚低。3. SUPnet与附加模块两条训练脚本背后各是什么网络部件3.1 SUPnet.pyPointNet编码器加特征回传解码器的主干SUPnet这个名字看着像自定义网络实际主干还是PointNet的set abstraction加feature propagation结构。为什么选PointNet做场景语义分割主干而不是直接上Transformer原因在于S3DIS这类室内点云数据量不大PointNet的多尺度局部特征提取在几千个点的块上性价比很高跑一轮只需要一两块GPU调试成本低。输入层的通道数要特别留意。SUPnet.py第一层卷积的输入通道一般是9归一化坐标x y z加绝对坐标x y z再加法线nx ny nz。如果后续你想加RGB就得把输入通道改成12同时数据加载器里也要把颜色拼进去两边任何一个不一致都会出现shape mismatch。主干前向的结构大致如下我把关键层用代码标注出来# SUPnet.py 前向流程按项目代码结构还原主干逻辑 def forward(self, xyz, points): # xyz: [B, N, 3] 归一化坐标points: [B, N, 6] 含绝对坐标与法线 B, N, _ xyz.shape # 第一层set abstraction局部区域采样并提取特征 l1_xyz, l1_points self.sa1(xyz, points) # 下采样到N/4通道升到128 # 第二层set abstraction进一步下采样 l2_xyz, l2_points self.sa2(l1_xyz, l1_points) # 下采样到N/16通道升到256 # 全局set abstraction把整个块的信息压成一个全局特征 l3_xyz, l3_points self.sa3(l2_xyz, l2_points) # 输出 [B, 512, 1] # 特征回传feature propagation逐层上采样并和编码器特征拼接 l2_points self.fp3(l2_xyz, l3_xyz, l2_points, l3_points) # 256 l1_points self.fp2(l1_xyz, l2_xyz, l1_points, l2_points) # 128 l0_points self.fp1(xyz, l1_xyz, points, l1_points) # 64 # 最后一层分类头输出每个点的13类logits logits self.cls(l0_points) # [B, 13, N] return logitssa1、sa2、sa3是set abstraction作用是“先采样再分组再卷积”每个下采样层级只保留一部分关键点再用球邻域把周围点特征聚合成新特征。fp3、fp2、fp1是特征回传负责把全局特征一层层传回原始分辨率。这个编码器解码器的设计保证了最后每个点都有自己的特征向量而不是整块只出一个全局特征。代码里值得反复看的是fp1这层。它把最高分辨率的原始points绝对坐标加法线和上采样后的特征拼在一起再过一个MLP得到64维特征。这个shortcut连接很重要法线信息对区分墙面和地板帮助极大没了它模型只能靠坐标猜。实际训练中如果发现“墙”和“地板”两个类别互相混淆严重优先检查fp1的拼接是否被正确保留而不是急着换损失函数。3.2 F1/F2与PW-ATM两条训练脚本各自在训练哪个模块train_SUPnet.py是最基础的训练脚本训练的是3.1节描述的主干。train_FG_F1F2.py里的F1/F2值得玩味从命名看F1一般是主分支输出F2是辅助分支输出FG多半是feature generator。train_PW-ATM.py里的ATM几乎可以肯定是Attention Transformer Module加了一个逐点注意力模块。这种多训练脚本的结构在毕设项目里很常见先用train_SUPnet.py把主干训收敛再用train_FG_F1F2.py或train_PW-ATM.py加载主干权重只训新加的模块。注意这三个脚本不是三个独立模型而是同一个模型在不同训练阶段的入口。用伪代码描述它们的关系# train_PW-ATM.py 中加载主干并接管注意力模块的常见写法 model SUPnet(num_classes13, use_pw_atmTrue) if args.pretrained: state_dict torch.load(args.pretrained) # 这里是train_SUPnet.py训好的权重 model_dict model.state_dict() # 过滤掉注意力模块新加的层保留主干参数 pretrained_dict {k: v for k, v in state_dict.items() if k in model_dict and atm not in k} model_dict.update(pretrained_dict) model.load_state_dict(model_dict)这个过滤逻辑就是两个脚本能衔接的核心。如果跳过train_SUPnet.py直接随机初始化跑注意力模块效果通常很差因为注意力模块要拟合的是主干输出的特征分布不是原始点云分布。反过来直接加载完整权重会让新模块的随机初始化参数和主干参数尺度不匹配训练初期loss反而会升高所以要做一层过滤只加载旧权重里和新模型共有的部分。从代码组织上看models目录里应该放着pointnet2_utils.py这类PointNet自带工具函数。这些函数是CUDA加速的ball query和grouping操作编译过一次后生成.so文件。如果换机器跑出现ImportError: No module named pointnet2_utils是因为没编译这个扩展。项目README里如果没写编译命令就找setup.py或编译脚本先编译再跑训练这是最容易在开头就把人劝退的地方。4. 训练脚本实战从train_SUPnet.py出发调通一轮训练4.1 train_SUPnet.py训练入口与关键参数对照train_SUPnet.py承接前文的DataLoader和网络结构是整条链路真正跑起来的入口。我拆这个文件时习惯先扫argparse部分因为那暴露了作者认为哪些超参值得暴露给使用者。这个脚本里典型的参数包括batch_size、num_point、learning_rate、step_size、epoch、log_dir具体含义看下表参数名常见默认值作用调参建议batch_size8每次迭代喂几个块显存不够就降到4别轻易动num_pointnum_point4096每个块的采样点数降到2048能省显存但边界类别会变差learning_rate0.001Adam初始学习率换GPU数量时要同步调小step_size10每多少epoch衰减一次lr用小学习率长训时加大到15或20max_epoch120总共训练轮数看验证集mIoU曲线决定是否提前停log_dirlog/保存模型和训练日志建议每次实验一个独立目录训练命令我在Ubuntu上一般这样起# 先激活深度学习环境再让训练在后台跑日志写文件 source activate supnet cd /path/to/project nohup python train_SUPnet.py \ --batch_size 8 \ --num_point 4096 \ --learning_rate 0.001 \ --step_size 10 \ --max_epoch 120 \ --log_dir log/s3dis_run1 \ train_s3dis.log 21 # 实时看loss和mIoU变化 tail -f train_s3dis.log用nohup和重定向日志是血泪经验直接在终端跑训练中途ssh断开进程就没了日志也没了等于白等十几个小时。batch_size8对应的是num_point4096的配置用户显存不够时就调batch_size这是改动最小、对结果影响也最小的做法。log_dir每次实验用独立目录方便后面对比不同参数下的mIoU曲线。这段代码跑起来后会先加载S3DISDataLoader训练集可能有两三百个npy块文件每个文件被batch_size整除后剩余的部分会被丢弃。如果发现一个epoch跑完只迭代了很少的step多半是块总数量不是batch_size的整数倍丢尾部数据导致有效样本变少可以把batch_size改成能整除块数的值或者改DataLoader里drop_last的设置。4.2 从loss到学习率优化器、类权重与训练稳定性train_SUPnet.py里loss设计对场景语义分割这种类别极不均衡的任务影响很大。S3DIS里地板和天花板占比高墙次之桌子和白板这类小众类别可能只占百分之几如果直接用nn.CrossEntropyLoss模型会把精力全放在学天花板和地板上小众类别几乎学不到。常见做法是在损失函数里加类别权重权重按每个类别的样本比例的倒数来算# train_SUPnet.py 中加权损失的一种实现 def compute_class_weights(labels, num_classes13): # labels: 所有训练标签拼起来的一维数组 counts np.bincount(labels, minlengthnum_classes).astype(np.float32) weights np.sum(counts) / (counts 1e-6) # 逆频率 weights weights / np.mean(weights) # 归一化到均值1左右 return torch.from_numpy(weights.astype(np.float32)) criterion nn.CrossEntropyLoss(weightcompute_class_weights(all_labels).cuda())优化器设置上PointNet系模型用Adam很稳betas默认0.9和0.999不用动weight_decay设1e-4到1e-5之间太大模型欠拟合太小后期loss会抖。学习率衰减策略用的是step_size10的StepLR每个epoch衰减0.7倍也就是每10个epochlearning_rate乘以0.7。如果训练曲线前30个epoch就平台期了可以把step_size调小到7如果60轮还在缓慢下降就调大到15或改用余弦退火。训练起来后要盯住的指标只有三个训练loss、验证mIoU、每类IoU。训练loss下降不代表mIoU一定上升尤其在小众类别上。我习惯每5个epoch在验证集上跑一次全量评估单看训练集loss很可能被数据增强的随机性骗了。S3DIS的标准评估方式是按房间聚合把同一个房间所有块的预测结果拼回去再算整体混淆矩阵而不是对每个块单独算mIoU再平均。这是PointNet原始实验的标准做法测试脚本里如果看到按块算mIoU的写法结果会比按房间聚合低好几个点不利于写进论文或答辩材料。5. 场景语义分割复现避坑S3DIS训练中的五个典型问题5.1 现象CUDA out of memorybatch_size设为8直接OOM原因num_point4096加batch_size8意味着一次前向要处理32768个点每个点9个通道中间特征图还有多层set abstraction单卡12G显存很容易被打爆。这不是代码问题是显存容量和参数组合不匹配。解决先把batch_size降到4跑通一个epoch看显存占用再用nvidia-smi监控。如果batch4也爆就把num_point从4096降到2048这是第二选项会牺牲小目标的分类精度。最后一个选项是用混合精度训练AMPPyTorch的torch.cuda.amp可以把显存占用砍掉接近一半代价是训练速度稍微变慢一点。5.2 现象训练集mIoU到55%验证集只有30%出头原因S3DIS数据集本身有区域分布差异训练集和验证集来自不同建筑区域室内布局差异大。另外一个常见原因是train_SUPnet.py里数据增强开太狠了尤其是random_dropout_point_cloud把20%的点的坐标换成了第一个点验证时没这个噪声分布直接错位。解决先检查验证集评估代码是否用了按房间聚合的方式评估不是的话改成按房间聚合。其次把dropout增强关掉对比一轮如果验证集mIoU立刻回到50%以上说明增强参数过猛把max_dropout_ratio从0.2调小到0.1再试。最后检查训练集和验证集是否混入了同一个房间的块S3DIS的Area 5选为验证集是常见约定别自己瞎切。5.3 现象训练中途卡死屏幕上反复出现socket相关报错或者visdom连不上原因项目的训练脚本里可能有visdom可视化逻辑负责实时画loss曲线。visdom默认连localhost:8097端口如果本地没启动visdom服务代码会在等待连接时把主线程卡住GPU利用率掉到0%看起来像训练死了。解决先杀掉卡住的训练进程再另开一个终端启动python -m visdom.server确认8097端口正常监听再重新启动训练。如果不需要可视化直接在脚本里把visdom相关初始化逻辑注释掉或者传入一个use_visdomFalse的参数跳过省得每台机器都要多跑一个服务。5.4 现象运行collect_indoor3d_data.py后几个小时没跑完一个npy文件都没生成原因S3DIS原始数据量大单线程遍历所有房间、每个房间又要做滑动窗口切片和近邻搜索整体耗时会非常久。sample_rate0.5还会让每个块先下采样再做后续处理进一步拖慢速度。更麻烦的是很多项目在数据转换完成后没有打印任何日志看起来就像死循环。解决这是正常性能瓶颈不是死锁。先把循环里每处理完一个房间就print一行进度确认进度在推进。然后给数据转换脚本加multiprocessing按房间号分配到多个进程跑提速接近进程数倍。实在等不及就先只转换一个房间的npy单独跑训练代码通了再全量转换。5.5 现象加载预训练权重时提示Missing key或size mismatch原因train_FG_F1F2.py和train_SUPnet.py的模型结构不一致预训练的state_dict里有主干参数但新模型的注意力模块是随机初始化的反过来新模型里也缺少预训练里fine-tune阶段的分类头参数。解决手动指定load_state_dict的strictFalse先打一遍model.state_dict()和预训练权重的key确认差异集中在新增模块上。如果差异在主干层说明预训练权重和新模型主干定义不一致可能是分类头输出类别数或输入通道数对不上检查num_classes和输入通道定义。6. 最后验证一步写一个预测脚本直接出彩色点云图和每类IoU训练完不等于实验结束毕设答辩要的可视化结果和量化指标最好是脚本一键生成。这里整理一个独立的验证脚本复用train_SUPnet.py里的模型和DataLoader对验证集做预测并把每个点的预测标签转成颜色同时输出每类IoU和平均mIoU整个过程不用进交互式环境。# predict_s3dis.py 验证脚本核心片段 import torch import numpy as np from models.supnet import SUPnet model SUPnet(num_classes13) model.load_state_dict(torch.load(log/s3dis_run1/best_model.pth)) model.cuda().eval() # 类别颜色映射按S3DIS标准13类顺序 class_colors np.array([ [0, 255, 0], [0, 0, 255], [0, 255, 255], [255, 255, 0], [255, 0, 255], [100, 100, 255], [200, 200, 100], [170, 120, 200], [255, 0, 0], [200, 100, 100], [10, 200, 100], [200, 200, 200], [50, 50, 50] ], dtypenp.uint8) for i, (points, label, room_id) in enumerate(val_loader): points points.cuda() with torch.no_grad(): logits model(points) # [B, 13, N] pred logits.argmax(dim1) # [B, N] # 把预测label转成彩色点云保存为ply方便打开 pred_color class_colors[pred.cpu().numpy().astype(np.int32)] np.savetxt(fpred_room_{room_id[0]}.npz, pred_color)这段脚本有两个点要特别注意。一是模型加载后必须调用eval()否则BatchNorm和Dropout还在训练模式预测结果会和训练一样带随机性。二是我把验证集按房间聚合得到每类IoU通常需要把该房间所有块的点坐标恢复成原始位置再与原始label做匹配计算这才是S3DIS标准评估。排错的关键是把保存的pred_color和原始点云的label逐点对比一次。我第一次跑这个验证脚本时用Area 5做验证集出来的平均IoU只有33%但训练集有54%当时以为模型过拟合了。后来发现是验证集评估脚本里没有按房间聚合而是按块算平均导致结果偏低。改成按房间聚合评估后mIoU稳定在45%左右这说明数据链路和分析方式本身就可能造成指标虚低。从那以后我每次复现这类点云分割项目都强制自己先走一遍“数据转换—数据加载—模型前向—验证评估”的最小闭环在跑完整训练之前就用几个batch把每个环节的输出shape和值域检查一遍再开始长训。这个习惯帮我避开了无数次训练中途才发现数据没对齐的返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表