PointCNN点云分割与自动驾驶三维定位:从数据预处理到包围盒生成全解析

📅 发布时间:2026/10/22 9:12:29
PointCNN点云分割与自动驾驶三维定位:从数据预处理到包围盒生成全解析
简介基于点云的PointCNN点卷积神经网络系统源码面向无人车定位车辆三维位置的应用场景适合深度学习、计算机视觉方向的在校学生、研究者或工程师用于毕业设计、课程项目及算法移植学习。资源包共24个文件含12个Python脚本覆盖KITTI数据转换、特征预处理、模型训练、测试评估与可视化、6个说明性txt、2个Markdown文档、2个Shell脚本一键执行训练与测试另有1个数据压缩包和1个编译后pyc文件压缩包整体仅108KB轻量易部署。目前已有129人学习下载。项目以PointCNN核心网络为骨架提供了kitti_velodyne_prepare数据准备、pointcnn_kitti训练推理、eval_vis分割与3D框合并可视化等完整模块可直接运行或二次开发。配套部署教程文档清晰说明环境配置与复现路径训练好的模型可帮助快速验证无人车点云定位效果是理解点云卷积与自动驾驶感知的良好参考。1. 点云做无人车定位摄像头会骗人PointCNN 不会要在一片稀疏点云里找到前车并估计它的三维位置光靠吃图像数据的神经网络并不靠谱——夜里逆光时摄像头拍出来的车位置基本靠猜。基于点云的 PointCNN 点卷积神经网络走的是另一条路用 X-变换把无序点云变得可卷积再对激光雷达每一帧扫描做语义分割把点归到车辆、行人、路面等类别最后从车辆类别的点云聚类出三维包围盒。这套项目把上述流程全部做完了源码、预处理好的数据、训练好的模型都打包在一个压缩包里。适合做自动驾驶感知方向毕设、课程设计的人也适合想搞清点云分割工程落地链路、但不想从零造轮子的从业者。2. 数据管线从 Velodyne 原始扫描到可训练的六通道点集拿到压缩包之后先别急着跑训练这套代码的输入不是原始.bin文件而是已经按块切好、带标签的.npy文件。压缩包里的data_conversions和kitti_velodyne_prepare两个目录就是负责把原始激光雷达扫描转换成 PointCNN 能吃的格式。很多人一上来直接跑pointcnn_seg_kitti.py报“文件不存在”才回头补数据准备白白浪费一晚上。先把数据管线捋清楚后面训练和推理才不翻车。2.1 数据转换脚本prepare_kitti_partseg_data.py 在做什么kitti_velodyne_prepare目录下的prepare_kitti_partseg_data.py是整个数据链路的起点。它做的事可以拆成三步读原始激光雷达扫描文件读出每一帧的三维坐标、颜色和反射强度把完整点云按固定尺寸切成小块同时把同一帧的标注文件转成每个点一个类别标签最后把点云块和标签一起写成.npy方便训练时随机抽样。原始扫描的一个帧文件里包含约十万个点不同帧点数不一样神经网络的输入维度却是固定的所以必须切成块。常见做法是沿地面平面把点云切成若干个block比如每个block覆盖 1.0 米 × 1.0 米的水平范围高度方向保留全部点。每帧点云最后会得到几百个块每个块再随机采样出固定数量的点。转换命令的长相通常是这样# 进入数据转换目录 cd data_conversions/kitti_velodyne_prepare # 常见做法是脚本接收原始数据目录和输出目录两个参数 # 如果脚本里路径写死了就直接改文件头部的 DATA_DIR 和 SAVE_DIR python prepare_kitti_partseg_data.py \ --data_dir ../../data/kitti_raw \ --save_dir ../../data/kitti_npy参数含义这里解释一下--data_dir指向原始扫描文件所在目录目录结构要按“日期/序列/激光雷达数据”的层级放好否则脚本按固定索引找文件时会直接跳过--save_dir是预处理结果的输出目录脚本会在里面生成每个序列对应的子目录。有的版本把参数写成全局变量不带--那就在if __name__ __main__之前改路径字符串效果一样。跑完转换后检查输出目录如果发现某个序列只生成了几十个.npy先别怀疑脚本坏了多半是标注文件没对齐。KITTI 语义分割的标注是逐帧逐点的文件和扫描文件必须一一对应错一位文件名后面训练时标签和点云就对不上了模型会学到“车的位置都在左边”这类假规律。2.2 输入特征 xyrgbi六个通道为什么要这么排压缩包里有个文件名很关键kitti_x4_2048_xyrgbi.py。后半段xyrgbi就是 PointCNN 在 KITTI 语义分割任务上用的特征组合x、y两个水平坐标加上r、g、b三个颜色通道再加一个i反射强度。一共六维不包含高度z。为什么不放z因为点云切成水平块之后块内各点的高度变化本身是强判别特征比如车身的点集中在 1.5 米以下、树冠的点在 3 米以上。但 PointCNN 的做法是先对每个块做局部坐标归一化把块内的点移到以块中心为原点、以地面为参考的局部坐标系里这时候全局z被减掉了剩下的高度差信息反而更干净。如果你自己改代码想加回z注意把维度从 6 改成 7模型第一层卷积的输入通道也要同步改不然直接报 shape 不匹配。反射强度i容易被忽略但它在区分金属车身和沥青路面时效果明显。车身表面反射强、数值稳定路面的强度则随材质和湿度波动很大这个通道对车辆类别分割的贡献比颜色还大。颜色通道来自激光雷达和相机的联合标定如果换到没有相机的纯雷达数据集上可以把rgb置为 0只保留xyi三通道模型精度会掉但流程不会断。组装特征的时候注意列顺序PointCNN 原版实现里约定顺序是xyrgb再加i也就是[x, y, r, g, b, i]。如果你习惯[x, y, z, i]的雷达坐标格式转换时一定要按目标脚本的顺序 reorder用 numpy 拼出来的代码大致是这样的import numpy as np # pts: (N, 3) 原始 xyz # colors: (N, 3) rgb 0-255 # intensity: (N, 1) feat np.concatenate( [pts[:, :2], colors[:, :3], intensity[:, :1]], axis1 ) # 最终形状 (N, 6)列顺序为 x, y, r, g, b, i # 归一化到 [-1, 1]PointCNN 对输入尺度很敏感 feat[:, :2] feat[:, :2] / np.max(np.abs(feat[:, :2]) 1e-6) feat[:, 2:5] feat[:, 2:5] / 255.0这里的np.concatenate指定axis1保证是在特征维度上拼接而不是在点数量维度上拼接新手经常在这里拼错成(6, N)后面喂给模型维度全乱。归一化那两行是经验值坐标除以最大绝对值的做法比减均值除方差更稳因为点云坐标范围大、分布偏斜标准化的稳定性不如最大值归一化。2.3 数据加载与采样x4_2048 这两个数字的工程含义kitti_x4_2048_xyrgbi.py中间的x4_2048也是理解这套代码的关键。2048是每个块采样点的数量x4是 PointCNN 采样时的密度系数表示在聚合特征时每个块的实际覆盖点数是基础值的四倍。显存不够的时候先动2048不要动x4x4影响的是网络感受野调小了精度掉得比点数减少还快。数据加载器的工作方式是每个训练轮次随机抽一批块每个块里如果点数多于 2048就用最远点采样筛出 2048 个如果少于 2048就重复采样补足。这个采样的随机性本身就是数据增强同一个块在不同轮次会以不同点集进入网络所以不需要额外做旋转扰动模型也能收敛得比较稳。加载器里有个容易被忽略的 block 权重逻辑。KITTI 语义分割标签里路面和建筑占了大部分点车辆点占比小如果每个块等概率抽样模型会被背景类带跑。预处理脚本通常会统计每个块的类别分布给包含车辆点的块更高的采样权重。代码里对应的是block_list和sample_weight两个数组训练时按权重做非均匀随机抽样# 每个 block 的采样权重车辆点多的块权重更高 block_indices np.arange(len(block_list)) selected np.random.choice( block_indices, sizebatch_size, replaceFalse, pblock_weights / block_weights.sum() # 归一化成概率分布 ) # 对选中的 block 做最远点采样到 2048 点 for idx in selected: block_pts np.load(block_list[idx]) # (M, 6) block_label np.load(label_list[idx]) # (M,) if block_pts.shape[0] 2048: chosen farthest_point_sample(block_pts, 2048) else: chosen np.random.choice(block_pts.shape[0], 2048, replaceTrue)p参数指定每个块的采样概率如果项目里没做类别权重训练初期 loss 会长时间不降这时候手动给车辆点占比高的块把权重乘 2 到 3收敛会明显变快。farthest_point_sample是点卷积任务里的标准采样算法保证采出来的 2048 个点在空间上尽量均匀不会全挤在一个角落。这个函数在很多点云库里有现成实现自己写的话用距离矩阵迭代找最远点效率低但逻辑清晰适合调试。3. 训练与推理让模型输出每个点的类别再把类别还原成三维位置数据管线通了之后进入真正的主角训练脚本、推理脚本和位置还原。看压缩包里的文件结构pointcnn_kitti.py是网络结构定义pointcnn_seg_kitti.py是分割主程序train_val_seg_kitti.sh和test.sh是训练和测试的入口脚本。推理输出的是每个点的类别标签但我们要的是“车在哪”——所以还差一步后处理把标成车辆的散乱点聚成一个个物体再算出中心坐标和朝向。这一章从训练参数讲到包围盒生成完整走一遍。3.1 训练入口 train_val_seg_kitti.py超参与模型结构对照训练脚本入口是train_val_seg_kitti.sh内容通常是一行 Python 调用加一串超参。直接执行就行# 在项目根目录执行确保 data 目录下已经有预处理好的 .npy sh train_val_seg_kitti.sh进去之后看脚本内容核心超参大致是batch_size6、max_epoch100、base_learning_rate0.001、num_classes10。num_classes10对应语义分割的十个类别车是其中一个类。如果预处理时只保留了几类这个数字要同步改小否则最后一层全连接输出和标签取值对不上训练 loss 会出现NaN。PointCNN 的网络主体在pointcnn_kitti.py里核心是多次 X-变换加卷积的堆叠。X-变换做的事情是对每个中心点找 K 个邻居先用一个多层感知机从邻居特征里学出一组变换矩阵再用这组矩阵把邻居特征“对齐”到一个规范顺序最后做标准卷积。这一步直接解决了点云无序性的问题不需要像体素法那样先把空间网格化也不会像 PointNet 那样丢失局部结构。训练时如果看到 loss 曲线下降很快但验证集精度不动多半是 X-变换的变换矩阵过拟合了局部噪声这时候调低学习率比加大数据更有效。训练脚本里通常有个--phase train和--phase val的切换跑完训练会自动在验证集上评估 mIoU平均交并比。这个指标对类别不平衡很敏感车辆类别虽然点少但分割得准不准直接影响后面定位所以单独看每个类别的 IoU 比看整体 mIoU 更有指导意义。用到 PyTorch 的版本时注意PointCNN 原版是 TensorFlow 1.x 的接口风格代码里大量出现tf.nn和tf.Variable迁移到 PyTorch 时要把数据布局从(N, C)换成(B, C, N)否则卷积算子的维度语义全反。3.2 pointcnn_seg_kitti.py推理脚本的输出组织推理入口在pointcnn_seg_kitti.py配套的test.sh会加载训练好的模型对验证集逐帧推理。它的输出是每个点的类别索引形状(M,)M是该帧点云的总点数。代码里典型的推理循环是这样# 伪代码逻辑对应 pointcnn_seg_kitti.py 的测试阶段 logits model(block_points) # block_points: (1, 2048, 6) pred np.argmax(logits, axis-1) # (1, 2048) 每个点一个类别号 # 把每个 block 的预测结果拼回完整帧 frame_pred np.zeros(frame_points_count, dtypenp.int32) for block_id, (start, end) in enumerate(block_ranges): frame_pred[start:end] pred[block_id]logits的最后一维大小等于num_classesnp.argmax取最大概率对应的类别。注意这里拿到的是块内 2048 个点的预测但每个块在采样时可能补了点真正有效的点数是原始点数所以拼回完整帧时要按block_ranges记录的点索引范围切回去把多补的点丢弃。这一步漏了的话输出的类别图会比实际点云多出一截可视化时看起来像车旁边多了一圈“鬼影”。推理时有个加速技巧块与块之间没有重叠可以并行推理但 PointCNN 的 X-变换依赖局部邻居batch 内不同块之间不互相干扰所以把--batch_size调大对显存不敏感不像图像卷积那样 batch 大了显存成倍涨。实测下来 2080 级别的卡可以开到 12 到 16 不爆显存。3.3 从预测类别到三维位置聚类与包围盒装配拿到逐点类别之后车辆定位的核心步骤是把所有类别为“车”的点聚成一个个独立的物体。第一步用基于距离的聚类常见选择是欧式聚类提取或 DBSCAN。DBSCAN 的好处是不用预先指定有多少辆车只需要调eps两个点属于同一物体的最大距离和min_samples。对 KITTI 点云eps取 0.5 到 1.0 米比较合适min_samples取 10 左右太小会把路面的噪点也聚进来太大则会把一辆车拆成两半。聚完类之后每个聚类是一组三维点下一步是算三维包围盒。最直接的做法是取坐标最小值和最大值生成轴对齐包围盒这适合粗略定位但车辆有朝向轴对齐包围盒会把车和旁边的行道树框在一起。更好的做法是对聚类点做主成分分析第一主成分方向就是车头朝向第二第三主成分对应车宽和车高。然后把点投影到这三个方向轴上取投影长度作为长宽高中心点取投影均值import numpy as np from sklearn.cluster import DBSCAN from sklearn.decomposition import PCA # cluster_pts: (M, 3) 某辆车对应的全部点 def compute_vehicle_pose(cluster_pts): pca PCA(n_components3) pca.fit(cluster_pts) axes pca.components_ # (3, 3) 行向量是三个主轴 proj np.dot(cluster_pts, axes.T) dims proj.max(axis0) - proj.min(axis0) center (proj.max(axis0) proj.min(axis0)) / 2.0 center_3d np.dot(center, axes) # 从局部坐标换回全局坐标 yaw np.arctan2(axes[0, 1], axes[0, 0]) # 第一主轴朝向 return center_3d, dims, yaw这里有个明显的坑PCA 得到的朝向有 180 度二义性车头朝前和朝后算出来的第一主成分方向相反但 PCA 本身不区分正负。解决方法是引入点云的反射强度分布车头进气格栅和车尾保险杠的强度特征不同用部分先验知识把朝向掰到正确方向或者简单一点把 yaw 限定在[-90, 90]范围内对停车位场景够用对行驶中的车还需要额外的运动方向约束。这个二义性问题当年卡了我整整两天最后才发现不是聚类的问题是朝向定义的问题。中心点center_3d就是这辆车在雷达坐标系下的三维位置输出格式可以组织成每行一个目标center_x center_y center_z length width height yaw。这套格式和 KITTI 的标签格式兼容可以直接拿验证集标签对比算定位误差。4. 避坑排查复现这套源码的五个典型翻车现场压缩包标注“已测试运行成功”但那是开发者自己的环境。换一台机器换一个环境问题就可能冒出来。这一章写的五个坑是我实际复现这类项目时必踩的每一条都按“现象 → 原因 → 解决”拆开照着排查能省大半天。坑一import pointfly 直接报 TensorFlow 贡献模块不存在现象运行train_val_seg_kitti.py时抛出ModuleNotFoundError: No module named tensorflow.contrib。原因原版 PointCNN 基于 TensorFlow 1.x 编写pointfly.py里大量使用tf.contrib.layers和tf.nn.conv2d等旧接口TensorFlow 2.x 移除了contrib模块。解决装 TensorFlow 1.15 版本最省事pip install tensorflow-gpu1.15.0配合 CUDA 10.0 可以跑通。不想降环境的话在pointfly.py头部加一段兼容层把tf.contrib.layers映射到tf.keras.layers但注意tf.nn的很多参数语义变了改起来比降版本更费时间。我的建议是直接开一个 Python 3.6 加 TensorFlow 1.15 的虚拟环境一天内能跑通别跟新版框架较劲。坑二数据路径找不到模型加载失败现象执行test.sh后很快报FileNotFoundError指向某个.ckpt或.npy文件不存在。原因项目里路径是相对路径基于作者的目录结构写死。解压后如果挪动了文件夹层级或者把data目录单独移走相对路径就断了。解决先跑tree看一遍实际目录再把脚本里的路径改成绝对路径。常见做法是在pointcnn_seg_kitti.py头部加一个ROOT os.path.dirname(os.path.abspath(__file__))所有相对路径基于ROOT拼接。模型文件.ckpt通常有三个文件.index、.data-*、.meta三个都要在同一个目录里只拷贝了.data文件的话TensorFlow 会认为模型损坏。坑三训练时 loss 变成 NaN现象训练跑到第几十轮loss 突然变成nan之后一路保持nan不恢复。原因最常见是学习率太大特别是 Adam 优化器配合点云这种非均匀分布的数据某些 batch 的梯度范数过大参数直接溢出。另一个原因是预处理后的点数里有Inf或NaN值多半是坐标归一化时除以了零。解决先把base_learning_rate从 0.001 降到 0.0003观察 loss 是否稳定。再写一段数据检查遍历所有.npy用np.isnan(feat).any()检查是否有非法值有就定位到具体文件回到预处理步骤修复。如果 loss 从第一轮就 NaN基本可以断定是数据有问题不是超参问题。坑四车载点云分割精度尚可但聚类结果严重错乱现象可视化时车辆周围的点被分到五六个不同的聚类里或者一辆车被拆成两截。原因eps和min_samples设置不当或者点云密度在不同距离上差异太大。雷达点云是近密远疏同一辆 10 米外的车点间距可能是 5 米外车的两倍固定eps必然出问题。解决改用自适应距离阈值按聚类点云的平均密度倒推eps或者用OPTICS聚类替代 DBSCAN。工程上更简单的方法是先降采样把整个帧的点云用体素滤波统一到固定分辨率比如体素边长 0.2 米然后再聚类eps就稳定了。体素滤波会损失一些细节但对定位任务影响不大。坑五可视化脚本输出的包围盒和点云不在同一个位置现象kitti_seg_bbox_merge_vis.py画出来的包围盒要么悬空要么嵌进地面和点云错开一大截。原因这个脚本作用是把语义分割结果和原始点云合并可视化但点云坐标来自雷达坐标系包围盒坐标来自推理输出两个坐标系之间没有做变换对齐。部分版本的脚本默认了某个坐标变换矩阵换数据后矩阵不适用。解决检查脚本里是否引用了data_conversions下的标定文件如果有把标定参数打印出来和点云的实际坐标范围对比。最粗暴的验证方法是取一帧点云的最小和最大坐标看包围盒中心是否落在这个范围内如果明显超出就手动把包围盒中心减去点云中心偏移。这个坑不影响模型精度但可视化结果会误导你对分割质量的判断。5. 进阶验证用训练好的模型批量输出车辆位置并做误差校验训练好的模型已经打包在压缩包里省去了自己训一遍的时间。但直接拿来输出位置还不够你还需要知道输出有多准。这里给一个批量验证的框架把验证集每一帧跑完推理、聚类、包围盒生成三步再和真值对比算中心点位置误差。# eval_vehicle_position.py 核心片段 import numpy as np def eval_position_errors(pred_boxes, gt_boxes): errors [] for pred, gt in zip(pred_boxes, gt_boxes): pos_err np.linalg.norm(pred[:3] - gt[:3]) # 中心点距离 yaw_err abs(pred[-1] - gt[-1]) yaw_err min(yaw_err, 2 * np.pi - yaw_err) # 角度归一化 errors.append((pos_err, yaw_err)) return errors # 统计结果位置误差中位数、90 分位数、朝向误差中位数 # 一般验证集上位置误差中位数在 0.5~1.0 米之间算正常如果你的场景不需要精确朝向只用中心点位置做路径规划就把yaw_err忽略掉误差会小不少。注意真值框的长宽高和聚类算出来的长宽高有系统性偏差因为聚类只覆盖激光雷达扫到的表面点车身的另一侧是空的所以聚类尺寸通常会比真值小 0.2 到 0.4 米。这个偏差是传感器造成的不是模型问题合理做法是只评估中心点误差不评估尺寸误差。想进一步确认模型有没有学到车辆语义可以单独算车辆类别的 IoU把预测为车的点集合和真值标注为车的点集合做交并比。KITTI 分割任务里车辆类 IoU 能到 0.7 以上说明模型训练充分如果只有 0.5那定位误差也会大问题出在分割阶段不在聚类阶段。还有一个值得做的验证是“时间维度一致性”把连续几帧的车辆位置串起来看中心点轨迹是否平滑。单帧误差小不代表定位可用如果相邻两帧的同一辆车位置跳了 1 米说明分割结果在帧间不稳定可以在聚类之后加一个卡尔曼滤波做跟踪把位置序列平滑一遍。这个压缩包里的eval_vis目录已经有可视化和合并代码先跑通它再叠加你自己的评价脚本会比直接盲调盲猜高效得多。从那以后我每次拿到别人给的模型包哪怕 README 写“已跑通”也会先走一遍数据检查、单帧推理、中心误差统计这条完整验证链确认输入通道、坐标系、输出格式和代码对得上再进入正式调参。这个习惯帮我避开了好多次“模型是坏的其实数据没对齐”的隐性麻烦希望帮到你。本文还有配套的精品资源点击获取