简介该资源面向从事自动驾驶感知、点云语义分割与多模态学习的研究者和开发者提供Seal自监督学习框架的完整代码实现。其核心思路是将视觉基础模型中的现成知识蒸馏到汽车点云中在表示学习阶段约束相机到LiDAR、点到分段之间的空间与时间一致性从而无需2D或3D标注即可完成预训练并可将知识迁移至真实/合成、高低分辨率、大小规模及干净/损坏等多种下游分割任务。压缩包共255个文件、约32.59MB以Python源码为主体辅以编译缓存、示例动图与图片、前端tsx/js资源、Shell脚本及CUDA扩展文件.cu/.cuh/.cpp涵盖模型实现、演示素材与运行配置。已有81人学习下载适合希望复现跨模态蒸馏流程、研究无标注点云预训练或扩展下游分割实验的读者参考。1. 从一份点云分割工程说起为什么“蒸馏”比“重训”更划算手里有一份点云分割工程目标很直接给任意一辆车的激光雷达点云做语义分割把车身、地面、背景这些类别分开。难点不在网络结构而在于标注数据太贵——逐点标注一份车载点云人工成本高得离谱。这个项目的思路是绕开标注拿视觉基础模型VFM在 2D 图像上已经学到的现成知识通过蒸馏迁移到 3D 点云分割网络里2D 和 3D 大模型一起用让 3D 网络在少量甚至没有逐点标签的情况下也能分割任意汽车点云。技术栈是 Python PyTorch配套点云与多相机数据的读取、投影、蒸馏训练脚本。适合两类人想快速复现一套跨模态蒸馏流程的算法工程师以及手里有点云数据、想拿它做二次开发落地的开发者。这个项目最反直觉的一点是它不追求把 3D 网络训到极致而是把 2D 基础模型当成“老师”让 3D 学生网络去对齐老师的特征和预测。你不需要重新标注一遍点云只需要保证相机和激光雷达之间有标定关系剩下的交给蒸馏损失去拉。下面按“这项目是什么 → 怎么跑起来 → 核心代码怎么读 → 怎么改”的顺序拆中间会插一章踩坑记录都是我这类工程里翻过车的地方。2. 工程结构与技术选型数据流从哪进、特征从哪出2.1 目录骨架与各模块职责这类跨模态蒸馏工程目录一般不会太深常见做法是按“数据、模型、损失、训练、配置”五块切。下面是我按项目定位整理出的典型结构实际拿到源码时对照着看即可pointcloud_distill/ ├── configs/ # yaml 配置数据集路径、模型超参、蒸馏权重 │ ├── base.yaml │ └── distill_car.yaml ├── datasets/ │ ├── __init__.py │ ├── pointcloud_dataset.py # 点云读取、体素化、样本组装 │ └── camera_dataset.py # 多相机图像读取与内外参加载 ├── models/ │ ├── teacher_2d.py # 2D 视觉基础模型封装冻结 │ ├── student_3d.py # 3D 点云分割主干 │ └── projection.py # 点云到图像平面的投影与可见性判断 ├── losses/ │ ├── distill_loss.py # 特征蒸馏 预测蒸馏 │ └── seg_loss.py # 少量标签下的分割监督 ├── train.py # 训练入口 ├── evaluate.py # 推理与指标计算 └── utils/ ├── config.py └── logger.pydatasets负责把一帧点云和同一时刻的多相机图像对齐成一条样本models/projection.py是整个工程的枢纽点云点要投影到图像像素上才能拿到对应位置的 2D 特征losses里蒸馏损失是主角分割监督只是辅助。配置驱动的好处是换数据集只改 yaml不动代码。2.2 技术选型为什么是“冻结老师 训练学生”2D 视觉基础模型参数量大直接微调既吃显存又容易把预训练知识训崩所以工程里通常把老师网络整体冻结只在前向时取它的中间层特征和输出 logits。学生网络是一个轻量的 3D 分割主干输入是体素化后的点云输出逐点类别。蒸馏发生在两个层面特征层面对齐投影后的 2D 特征预测层面对齐类别分布。# models/teacher_2d.py 关键片段 import torch import torch.nn as nn class Teacher2D(nn.Module): def __init__(self, backbone): super().__init__() self.backbone backbone # 冻结全部参数只做前向特征提取 for p in self.backbone.parameters(): p.requires_grad False self.backbone.eval() torch.no_grad() def forward(self, images): # 返回中间层特征与分割 logits供蒸馏使用 feats, logits self.backbone.extract(images) return feats, logitstorch.no_grad()是必须的否则显存会被老师的计算图吃掉一大半extract是封装层具体取哪几层特征由配置决定常见做法是取倒数第二层语义信息够强又不至于太抽象。参数上唯一要调的是输入图像分辨率它直接决定投影后每个点能匹配到多细的特征。2.3 点云到图像的投影链路蒸馏能不能成立全看投影准不准。工程里一般用相机内参 K 和外参 [R|t] 把点云点变换到相机坐标系再投到像素平面同时做可见性过滤——被遮挡或落在图像外的点不参与蒸馏。# models/projection.py import torch def project_points(points, K, RT, img_h, img_w): # points: (N, 3) 激光雷达坐标系 # RT: (4, 4) 雷达到相机的外参 pts_h torch.cat([points, torch.ones_like(points[:, :1])], dim1) # 齐次坐标 cam (RT pts_h.T).T[:, :3] # 转到相机坐标系 z cam[:, 2:3].clamp(min1e-3) # 深度防止除零 uv (K (cam / z).T).T[:, :2] # 投到像素平面 valid (z[:, 0] 0) (uv[:, 0] 0) (uv[:, 0] img_w) \ (uv[:, 1] 0) (uv[:, 1] img_h) return uv.long(), valid # 像素坐标 有效掩码RT的标定误差会直接放大成蒸馏噪声这是后面踩坑章要重点说的。valid掩码同时过滤了相机后方和图像外的点少了这一步蒸馏损失会被大量无效点稀释训练曲线看着在降实际分割效果不动。3. 跑起来环境、数据组织与训练入口3.1 环境安装与依赖版本Python 3.8 以上、PyTorch 1.12 以上是这类工程的常见基线点云体素化如果用了稀疏卷积库还要额外装对应的 CUDA 版本。建议先建独立环境再装依赖避免和系统里的 torch 打架conda create -n pc_distill python3.9 -y conda activate pc_distill pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy opencv-python pyyaml tqdm # 若使用稀疏卷积主干按官方说明安装对应版本装完先跑一句python -c import torch; print(torch.cuda.is_available())返回 True 再往下走。CUDA 版本和驱动不匹配是新手最常见的卡点报错往往出现在训练第一步而不是安装时提前确认能省很多时间。3.2 数据组织一帧点云配多张图数据集目录要保证“同一时刻、同一标定”的样本能对上。常见组织方式是按帧编号点云和图像共享文件名前缀标定文件单独放data/car_scene/ ├── calib/ │ └── 000001.yaml # 内含 K、RT、图像尺寸 ├── lidar/ │ └── 000001.npy # (N, 3) 或 (N, 4) 含强度 ├── camera/ │ ├── cam0/000001.jpg │ └── cam1/000001.jpg └── labels/ └── 000001.npy # 可选少量逐点标签labels允许缺失这正是蒸馏方案的价值所在——没有逐点标签的帧照样能参与训练只是少了分割监督那一项损失。读取时按帧号对齐缺任何一路就跳过该帧并打日志别让一条坏样本把整个 epoch 拖崩。3.3 训练入口与关键参数训练脚本一般把配置、模型、损失、优化器串起来蒸馏权重是调参重点python train.py \ --config configs/distill_car.yaml \ --data-root data/car_scene \ --batch-size 2 \ --epochs 30 \ --lr 1e-4 \ --w-feat 1.0 \ --w-logit 0.5--w-feat控制特征蒸馏的强度--w-logit控制预测蒸馏两个权重加起来别压过分割监督太多否则学生网络会只顾模仿老师、丢掉点云自身的几何信息。batch-size受显存限制多相机输入时 2 已经不小显存不够就减相机路数而不是硬降分辨率。训练日志里重点盯两个量蒸馏损失是否稳定下降、验证集上的逐点 mIoU 是否同步上涨只降前者不涨后者说明投影或对齐出了问题。4. 核心模块拆解蒸馏损失与 3D 学生网络4.1 特征蒸馏投影对齐后算距离特征蒸馏的本质是对每个有效点取它投影位置上的 2D 特征和学生网络在该点上的 3D 特征做对齐。维度不一致时用一个线性层投影到同一空间。# losses/distill_loss.py import torch import torch.nn as nn import torch.nn.functional as F class FeatureDistillLoss(nn.Module): def __init__(self, c_2d, c_3d): super().__init__() # 把 3D 特征映射到 2D 特征维度 self.proj nn.Linear(c_3d, c_2d) def forward(self, feat_3d, feat_2d_sampled, valid_mask): # feat_3d: (N, C3) 逐点特征 # feat_2d_sampled: (N, C2) 按投影坐标采样到的 2D 特征 pred self.proj(feat_3d) pred F.normalize(pred, dim-1) target F.normalize(feat_2d_sampled, dim-1) diff (pred - target).pow(2).sum(dim-1) # 只在有效投影点上取平均 return (diff * valid_mask).sum() / valid_mask.sum().clamp(min1)F.normalize是关键一步特征模长在不同网络间差异很大不归一化的话损失会被大模长特征主导。valid_mask保证只有真正投到图像上的点参与计算clamp(min1)防止某一帧全无效时出现除零。proj这一层是要训练的它承担了跨模态维度对齐的职责。4.2 预测蒸馏软标签比硬标签更稳预测蒸馏让学生网络的输出分布去逼近老师的输出分布用温度系数 T 平滑 logits常见做法是 KL 散度def logit_distill_loss(student_logits, teacher_logits, T2.0): # student_logits / teacher_logits: (N, num_classes) s F.log_softmax(student_logits / T, dim-1) t F.softmax(teacher_logits / T, dim-1) return F.kl_div(s, t, reductionbatchmean) * (T * T)温度 T 取 2 左右是经验值太高会让分布过于平坦、区分度下降太低就退化成硬标签。乘T*T是为了在梯度尺度上和 T1 时保持可比这个细节很多复现会漏掉导致换温度后学习率要重调。4.3 3D 学生网络体素化 稀疏卷积主干学生网络输入是体素化后的点云主干用稀疏卷积处理输出再反体素化回逐点特征。体素大小是精度和显存的核心权衡# models/student_3d.py 关键片段 class Student3D(nn.Module): def __init__(self, voxel_size0.1, num_classes10): super().__init__() self.voxel_size voxel_size self.backbone SparseConvBackbone(in_channels4) # xyz 强度 self.head nn.Linear(64, num_classes) def forward(self, points, batch_idx): voxels, coords, inverse voxelize(points, batch_idx, self.voxel_size) feats self.backbone(voxels, coords) # 体素级特征 point_feats feats[inverse] # 反体素化回逐点 return self.head(point_feats)voxel_size0.1意味着 10 厘米一个体素车规场景下这个粒度够用调到 0.05 精度会涨但显存翻几倍先跑通再谈精度。inverse索引是反体素化的桥梁丢了它就没法把体素特征还原到点上蒸馏也就无从谈起。5. 避坑与排查投影、显存、对齐这三关5.1 蒸馏损失在降但 mIoU 不动现象训练日志里特征蒸馏损失平稳下降验证集逐点 mIoU 却长期横盘。原因多半是投影坐标和图像实际分辨率对不上比如标定文件里的图像尺寸和实际读入的图不一致采样到的 2D 特征整体偏移学生学到的是错位特征。解决在project_points后加一步可视化把投影点画到图像上抽查几帧确认点落在车身上而不是飘在空中同时校验img_h/img_w来自实际图像而非硬编码。5.2 显存瞬间打满现象第一个 batch 就 OOM但模型参数量看着不大。原因通常是老师网络没加torch.no_grad()或者多相机图像一次性全送进老师计算图把显存吃光。解决确认老师前向在no_grad下执行多相机按路循环前向而不是拼成一个超大 batch必要时把老师输入分辨率降一档。5.3 有效投影点比例过低现象蒸馏损失数值很小但训练没效果打印valid_mask.sum()发现每帧只有个位数有效点。原因是外参方向搞反了雷达到相机 vs 相机到雷达或者深度裁剪阈值设错。解决先用一帧已知场景验证外参把点云投到图像上肉眼确认z 0的判断要基于相机坐标系别在雷达坐标系上做。5.4 换数据集后类别对不上现象换一份点云数据训练不报错但分割结果全是同一类。原因是老师输出的类别索引和学生网络的类别定义不一致预测蒸馏在拿错位的软标签教学生。解决在配置里显式维护一张类别映射表蒸馏前把老师 logits 按映射重排映射表对不上就跳过预测蒸馏、只保留特征蒸馏。5.5 多卡训练后指标反而降现象单卡能跑出正常指标改成多卡后 mIoU 掉一截。原因常见于蒸馏损失的归一化用了全局valid_mask.sum()多卡各自算再平均等价于给有效点少的卡更高权重。解决用all_reduce汇总有效点总数后再归一化保证各卡损失尺度一致。6. 二次开发把蒸馏流程接到自己的点云上拿到这套工程后最实际的改造不是换网络而是换数据适配层。我的习惯是先只改datasets和configs模型和损失一行不动跑通一条最小链路再谈优化。具体做法是新数据集写一个继承基类的 Dataset实现__len__和__getitem__在__getitem__里返回points、images、K、RT、可选的labels五个字段字段名和现有工程保持一致这样训练脚本完全不用改。class MyCarDataset(BaseDataset): def __getitem__(self, idx): frame self.frames[idx] points np.load(frame[lidar]) # (N, 4) images [read_img(p) for p in frame[cams]] K, RT load_calib(frame[calib]) # 与工程约定一致 labels np.load(frame[label]) if frame.get(label) else None return {points: points, images: images, K: K, RT: RT, labels: labels}字段对齐之后先关掉蒸馏、只用少量标签跑几个 epoch确认分割头能正常收敛这是排除数据问题的基线。基线正常再打开--w-feat从 0.2 开始往上加每次只动一个权重观察 mIoU 和蒸馏损失两条曲线是否同向。想进一步提升可以试两个方向一是把老师特征从单层改成多层加权浅层补细节、深层补语义二是对投影有效点做距离加权近处点投影更准、权重给高一点。验证方法上我一般固定留出几帧完全不参与训练既不用它们的标签做监督也不让它们进蒸馏专门看泛化。改造过程中最容易犯的错是一次性改数据、改损失、改主干出了问题根本定位不到是哪一环。从那以后我每次接新点云数据都强制先跑通“无蒸馏 少量标签”的基线再逐项打开蒸馏开关宁可多花半天也不给自己留糊涂账。希望这套拆解能帮到你把这份工程真正跑在自己的车上。本文还有配套的精品资源点击获取