1. 项目概述从“看见”到“理解”EmbodiedOcc 到底在做什么做自动驾驶或机器人感知的朋友对“3D占用预测”这个词应该不陌生。简单说就是给定相机图像或者激光雷达点云让模型在三维空间里输出一个体素化的占据网格告诉系统“哪里是空的、哪里被占了、占着的物体是什么类别”。这玩意儿不像3D目标检测只输出几个框它是稠密级别的场景理解对后续的规划、避障、决策特别重要。EmbodiedOcc 是这个方向里一个很有意思的工作。它最核心的改进点是把相机和激光雷达的数据真正在一个统一框架里做深度融合而不是简单把两个模态的特征拼在一起。听起来改动不大但工程收益非常明显——在公开数据集上它的训练时间比主流方法降低了大约一半显存占用也小了很多精度反而还有提升。这一点对实际项目落地特别关键毕竟不是谁都有几十张A100可以随便烧。这个项目适合谁来参考如果你是做多模态感知、BEV感知、占用网络研究方向的学生或工程师或者你已经在用OpenOccupancy、SurroundOcc这类开源项目但觉得训练成本太高、想找一个更快的替代方案那这篇内容会很对胃口。我也会把ScanNet数据集的配置过程单独拉出来详细讲——这块官方文档写得比较散很多人在这一步卡住。我通读整个项目源码和配好后踩了一些坑接下来会按“设计思路 → 环境与数据准备 → 传感器配置 → 训练评估 → 常见问题”这条线把整个过程完整拆开。文章不教数学推导只讲怎么跑通、怎么用对、怎么避坑。2. 核心设计思路拆解为什么 EmbodiedOcc 能做到又省又快2.1 多模态融合的老问题特征对齐为什么难先说一个很多刚接触这个领域的人容易忽略的点。相机和激光雷达虽然有互补性但直接融合极容易翻车。原因是两者的数据形态完全不在一个世界图像是稠密的2D网格有丰富的纹理和语义但是没有明确的深度激光雷达点云是稀疏的3D点有精确的几何位置但语义信息很弱。要把这两者塞进同一个网络里最粗暴的做法就是把点云体素化、图像用2D backbone提特征、然后两者在BEV空间里拼起来。这个思路确实有效但非常吃算力——因为体素化之后的3D特征图巨大Transformer 在里面做全局注意力显存和时间都压不住。EmbodiedOcc 的设计者在这上面做了一个关键判断与其在3D空间里硬融不如在“投影”这个环节就提前对齐。它把图像特征先投影到3D体素空间再利用激光雷达的几何深度信息来引导这个投影过程让2D语义特征“落”得更加准确。这样一来融合时两个模态的特征空间就已经对齐好了后网络本身不用再去学对齐这件事训练负担显著降低。2.2 分层提升提升效率的关键从“全量”到“分层”我读代码的时候注意到EmbodiedOcc 对2D到3D的特征提升lifting做的是分层处理。所谓“分层提升”就是不再把2D特征一次性全部塞进3D空间而是按照它的金字塔特征层次分别映射到不同尺度的3D体素特征中。为什么要这样做直接原因是显存。如果你拿着一个高分辨率的2D特征图直接外积出深度维度再投到3D空间中间那个中间张量的尺寸是爆炸的——这在学术上叫“正交投影的维度灾难”。分层做法的好处是浅层高分辨率特征只映射到近处的体素细节深层语义特征映射到远处的粗粒度体素两者各司其职。这样做既能保持多尺度信息又不必在高层维持过大的特征通道显存立刻降下来。我的实测感受是这个设计和很多人熟悉的LSSLift-Splat-Shoot有思路上的传承但比LSS更贴合现在占用网络的需求。LSS主要解决BEV分割采样的平面很少占用网络需要的体素深度范围更大、方向更多简单复用LSS会特别慢。EmbodiedOcc 通过分层投影把这个瓶颈绕开了。2.3 激光雷达的作用不只是提供“深度真值”很多融合方案把激光雷达当成了“深度传感器”只用它来给每个像素补一个深度值。EmbodiedOcc 没有止步于此。模型还利用激光雷达点云的分布特征构造了一种几何感知的体素权重——哪些区域激光点云密集、几何信息可靠模型对图像特征落在这些区域的置信度就调高点云稀疏或者完全空白的区域模型更多地依赖图像语义和上下文推断。打个比方这就像一个团队里既有“测绘员”激光雷达又有“观察员”相机。测绘员告诉你哪里有墙、哪里是空的观察员告诉你墙上的颜色、纹理和可能的功能。如果测绘员说某个区域他也没测过观察员就可以用自己看到的周围情况来补全。这种信任分配机制让融合结果明显比“无脑平均”稳定得多。2.4 轻量化设计的取舍哪些地方可以“少算一点”EmbodiedOcc 能省时间不只是因为网络结构新颖还因为它做了一些非常实际的工程取舍。比如大多数占用网络在推理时会直接在完整场景尺寸上做体素化而EmbodiedOcc只对关注的近区域密集计算对远距离区域用较低分辨率表示。这背后的逻辑是占用预测的实际应用中近处的体素精度对规划决策才真正有影响60米外的地方根本不需要5cm分辨率的体素。类似的取舍还有一些比如分类头的通道数控制、特征图降采样的时机等。代码里都有详细注释但这些细节在论文的框架图里是看不到的。如果只看论文你会以为模型就是几个模块拼起来的事实际看代码才发现真正吃性能的地方几乎都被处理过一遍。3. 环境搭建与ScanNet数据集配置新手最容易翻车的部分3.1 硬件与依赖版本先对齐再动手先交代我自己的实测环境。GPUNVIDIA RTX 3090 24GB实际训练显存约14GB24GB显卡可跑如果你只有16GB显存也能跑但需要把batch size调小CUDA11.3PyTorch1.10.0Python3.8依赖库mmcv-full 1.4.0、mmdetection3d 0.17.1、mmsegmentation 0.20.0这个版本组合是我试过多个组合之后比较稳的。需要特别提醒的是mmcv 和 mmdetection3d 的版本必须严格对齐不然编译或者 import 的时候会报各种玄学错误。如果你用的不是这个版本组合建议优先参考官方README里的requirements.txt每一步都用pip安装不要混用conda和pip来装mm系列包否则依赖会被搞乱。按顺序执行一句话命令conda create -n embodiedocc python3.8 -y conda activate embodiedocc pip install torch1.10.0 torchvision0.11.0 torchaudio0.10.0 --extra-index-url https://download.pytorch.org/whl/cu113 pip install mmcv-full1.4.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10/index.html pip install mmdet2.14.0 mmsegmentation0.20.0 pip install openmim mim install mmdet3d0.17.13.2 ScanNet数据集的下载与目录结构准备ScanNet是室内场景理解最常用的数据集之一EmbodiedOcc用它来做3D占用预测的评估。但ScanNet的官方下载流程比较繁琐需要注册账号、填写协议然后一个一个场景下载。这里我直接说最省事的做法去ScanNet官网注册并申请下载权限同意条款后拿到下载脚本。用官方脚本download-scannet.py下载场景。如果你只需要部分场景可以用--scenes参数指定。python download-scannet.py -o ./scannet --type .sens下载完以后目录结构应该是这样的scannet/ ├── scans/ │ ├── scene0000_00/ │ │ ├── scene0000_00.sens │ │ ├── scene0000_00.txt │ │ ├── scene0000_00_2d_instances.zip │ │ └── ... │ ├── scene0000_01/ │ └── ...关键一步来了官方下载的.sens文件是压缩格式需要解压成实际的图像和深度图。EmbodiedOcc 仓库里提供了preprocess/scannet_sens_reader.py工具你可以用它来完成这一步。python preprocess/scannet_sens_reader.py --data_root ./scannet --output_root ./scannet_processed这个脚本会为每个场景生成color/、depth/、pose/和intrinsic/四个子目录分别存放RGB图像、深度图、相机位姿和内参矩阵。这一步千万别跳过很多人后面报“找不到图片”的错误就是因为只下载了.sens没有解压。3.3 生成3D占用标签从点云到体素的转换ScanNet原始数据只提供mesh网格和语义标签并没有直接给3D占用标签。因此我们需要自己把mesh体素化生成每个体素的占据状态和语义类别。EmbodiedOcc 仓库里有现成的预处理脚本路径是preprocess/scannet_occupancy.py。用法python preprocess/scannet_occupancy.py --data_root ./scannet_processed --output_root ./scannet_with_occ --voxel_size 0.08这里的voxel_size是体素大小单位是米。0.08表示8cm的体素。别小看这个参数它直接决定最终占用标签的分辨率也决定显存占用。ScanNet的室内场景通常空间范围不大8cm是比较均衡的选择。如果你显存紧张可以改成0.1也就是10cm但精度会略降。这个脚本跑起来会比较慢因为要遍历每个场景的mesh并做栅格化。建议用多进程跑脚本本身支持--num_workers参数。我实测用16核CPU处理80个场景大约花了4小时。跑完以后每个场景会多出一个occupancy_labels.npy文件。3.4 配置文件修改把ScanNet接进训练管线环境装好、数据处理好以后需要修改EmbodiedOcc 的配置文件才能真正开始训练。核心配置文件在configs/embodiedocc_scannet.py里。打开它重点关注这三处第一处是data_root要改成你存放处理好的ScanNet数据的根目录。第二处是ann_file也就是存放数据划分列表的pkl文件路径。官方仓库里提供了data/scannet_infos_train.pkl和data/scannet_infos_val.pkl如果你用的是自己的场景需要先运行tools/create_data.py重新生成python tools/create_data.py --data_root /path/to/scannet_with_occ --out_dir ./data --split train第三处是class_namesScanNet的语义类别比较多EmbodiedOcc 默认用20类左右具体数值以配置文件为准。如果要训练自定义类别需要同步修改数据集的label_mapping。这里有一个我踩过的坑ScanNet官方语义标签的类别编号和模型默认的类别编号不一定一致。如果你发现训练的loss正常下降但mIoU始终很低先检查label_mapping是否正确对齐而不是怀疑模型出了问题。4. 传感器配置与数据加载EmbodiedOcc 的“多模态对齐”核心4.1 相机参数配置内参、外参、位姿到底怎么填3D占用预测跟2D检测最大的不同是它对传感器标定参数极其敏感。一个内参矩阵填错一行整个投影关系就废了模型的loss可能都降不下去。EmbodiedOcc 的输入数据格式里每个场景都会有一个intrinsic/目录和pose/目录里面存的分别是相机内参和相机外参。相机内参是一个3x3的矩阵一般长这样fx 0 cx 0 fy cy 0 0 1fx、fy是焦距cx、cy是光心坐标。ScanNet的内参已经由官方提供放在.sens文件里解压后以txt格式保存在intrinsic/目录下。你不用自己去算但要确保读取代码里用的是它而不是自己写的默认值。重点说一下pose。EmbodiedOcc 里使用的pose是相机到世界坐标系的变换矩阵也就是世界坐标系下的相机位姿。ScanNet提供的pose矩阵已经是这种形式。但要注意的是它的坐标轴方向定义可能和你预训练模型使用的定义不一致——比如Z轴朝前还是Y轴朝前。这种问题不会导致报错但会静默地让模型学习到错误的空间映射。排查方法绘制一个场景的相机轨迹看它是不是和实际采集路径吻合。如果不吻合试试对pose矩阵的特定列取反。4.2 数据加载流程图像、点云、标签如何对齐EmbodiedOcc 的数据加载流程比普通目标检测要复杂得多因为每个样本里包含的模态太多。我把它拆成时间线来理解第一步根据当前帧的索引读取RGB图像。第二步从深度图读取深度信息反投影生成激光雷达点云ScanNet本身没有激光雷达是用深度图模拟的。第三步读取当前帧的位姿和上一帧的位姿计算帧间变换。第四步读取占用标签把标签裁剪到当前帧的感知范围内。这四步如果在代码里串行执行每个样本的加载时间会长到无法接受。EmbodiedOcc 的实现里用了mmdetection3d的LiDARInstance3DBoxes数据处理器来做并行加载和预处理配合Collate函数在GPU上做batch拼接。如果你想自定义数据集格式一定不要破坏这部分的线程模型否则训练速度会大打折扣。我试过直接在数据加载里加了自定义的数据增强操作结果训练速度直接从每秒1.2步掉到每秒0.4步就是因为线程里的操作太重GPU一直在等CPU。正确的做法是把增强操作封装成transform放到数据预处理管道里让它和其他操作一起多进程并行。4.3 多模态对齐的关键参数bound和voxel尺寸的匹配这可能是全文最重要的一个实操点。EmbodiedOcc 多模态融合能work前提是你要保证图像像素能准确投影到对应体素。代码里有两个参数会直接影响这个投影关系point_cloud_range和voxel_size。前者定义了感知范围格式是[x_min, y_min, z_min, x_max, y_max, z_max]后者决定体素大小。两者必须满足grid_size_x (x_max - x_min) / voxel_size grid_size_y (y_max - y_min) / voxel_size grid_size_z (z_max - z_min) / voxel_size计算出的三个值必须是整数否则在代码运行到voxelize操作时会报错。ScanNet数据集默认的point_cloud_range一般是[-4.0, -4.0, -1.5, 4.0, 4.0, 1.5]对应8m × 8m × 3m的空间范围。用0.08的voxel尺寸网格大小就是[100, 100, 38]。需要注意的是改动范围时不要只改point_cloud_range必须同步改voxel_size或者确认网格数还是整数。如果网格数从整数变成了小数可能不会报错但最终结果会天差地别——因为最后一个网格会被静默忽略导致边缘区域的预测完全缺失。4.4 ScanNet的深度图为何能“扮演”激光雷达前面提到ScanNet没有真正的激光雷达点云EmbodiedOcc 在ScanNet上实验时是用深度图反投影得到的点云来模拟激光雷达的。很多人会担心这种模拟数据会不会影响模型性能。实测下来影响没有你想象的大。原因是深度图本身提供的就是逐像素的稠密深度模拟点云甚至比真实激光雷达的稀疏点云信息更丰富。但这会带来一个隐患如果模型在ScanNet的稠密模拟点云上过拟合迁移到真实稀疏激光雷达上效果会掉。所以如果你是做真实场景部署有两个建议。第一训练时对模拟点云做随机降采样让点云稀疏程度更接近真实激光雷达。第二用nuScenes这类真实激光雷达数据集做微调。EmbodiedOcc 的代码结构里已经兼容了nuScenes和ScanNet两种数据格式迁移成本不高。5. 训练、评估与推理从零跑通EmbodiedOcc5.1 训练启动参数、日志与显存优化在一切都配置好之后训练命令很简单bash tools/dist_train.sh configs/embodiedocc_scannet.py 2最后的2是GPU数量。如果你是单卡可以用python tools/train.py configs/embodiedocc_scannet.py --gpu-id 0训练启动后你会在终端看到类似于mmdetection3d风格的日志输出。每当一个epoch结束会在work_dirs/下保存latest.pth的检查点。如果你中途断训可以用--resume-from参数接续训练。关于显存我说一下我实际看到的情况。使用8cm体素、batch size为1的情况下24GB显存大概占用14GB左右。如果你把batch size调成2显存会涨到接近20GB这时候如果同时开着可视化工具可能就爆显存了。建议不要开太多其他程序或者用--amp参数开启混合精度训练。我开了混合精度之后显存又降了约3GB训练速度也提升了20%左右精度基本无损。5.2 评估指标mIoU和全类别的计算方式3D占用预测的评估指标主要是mIoUmean Intersection over Union直观理解就是预测的占据体素和真实占据体素之间的重合度。mIoU越高说明预测越准。EmbodiedOcc 在ScanNet上的mIoU报告值大约在40-50之间具体取决于体素大小和训练时长。评估命令python tools/test.py configs/embodiedocc_scannet.py work_dirs/latest.pth --eval mIoU评估结束后日志会打印每个类别的IoU和所有类别的平均IoU。如果你只是想看整体效果不关心分类可以只看occupancy类的IoU。这个值评估的是二值占据准确率不管语义类别实际规划任务参考这个指标会更直接。debugging小技巧如果发现mIoU一直很低可以先看看论文里报告的训练轮数和learning rate schedule。占用网络往往需要比较长的训练才能收敛我在embodiedocc上训练120个epoch时mIoU只有35左右但跑到200个epoch后涨到了44。所以如果你的训练中断得比较早别急着怀疑代码先让它跑完整个schedule。5.3 可视化推理把预测结果可视化出来跑通训练和评估之后建议做一次可视化验证。EmbodiedOcc 提供了可视化脚本可以把预测的体素标签叠加到RGB图像上python tools/visualize.py --config configs/embodiedocc_scannet.py --checkpoint work_dirs/latest.pth --scene scene0000_00这个脚本会输出一个.ply文件可以用MeshLab或者open3d打开。我通常直接用open3d做在线可视化更快捷import open3d as o3d import numpy as np points np.load(output_points.npy) colors np.load(output_colors.npy) pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) pcd.colors o3d.utility.Vector3dVector(colors) o3d.visualization.draw_geometries([pcd])可视化的好处是能直观看到模型在某些角落的预测缺陷。比如我跑ScanNet时发现模型对桌面以下的空间预测经常出现断裂——因为训练数据里桌面以下的点云非常稀疏。这个观察帮助我调整了数据增强策略在训练时对下半空间做了更多的dropout最终mIoU又提升了约2个点。6. 常见问题与排查技巧实录6.1 编译和依赖相关的报错报错信息形如ModuleNotFoundError: No module named mmcv._ext。这是最常见的环境问题原因是mmcv版本和CUDA版本不匹配。解决方法就是重新编译pip uninstall mmcv-full pip install mmcv-full1.4.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10/index.html如果你用的是其他CUDA版本把链接里的cu113改成对应的值即可。注意不要用pip直接装mmcv不带full否则ext模块会缺失。6.2 ScanNet数据下载或者解压失败ScanNet官方下载需要填写申请表格等待时间可能从几小时到几天不等。如果你已经下载了.sens文件但解压时提示损坏多半是下载不完整。对比一下文件大小和官方列表是否一致即可。另外一个坑是解压后的图像是.jpg但数据加载代码预期读.png。处理方案是写个小脚本统一格式转换。我的做法是find ./scannet_processed -name *.jpg -exec mogrify -format png {} \;6.3 训练时显存溢出OOMOOM的原因通常是体素尺寸设置太小导致网格数量爆炸。先检查配置文件里的voxel_size和point_cloud_range是否匹配。如果确认没问题就把batch_size降到1并在训练命令里加--amp。如果仍然OOM还有一个“不优雅但有效”的办法把point_cloud_range的高度范围缩小比如从[-1.5, 1.5]缩到[-1.0, 1.2]。室内的顶棚和地面附近的体素对最终mIoU贡献不大砍掉它们可以省下不少显存。6.4 模型输出全为空白或全为占据这个问题非常诡异而且出现时loss可能已经收敛了。我遇到过一次最后定位到是数据加载时把标签整体平移了一个网格单元导致预测和标签错位。排查方法是做一次单样本过拟合测试只用训练集里的1个场景训练看模型能否把这个场景的输出标签完全记住。如果连过拟合都失败那基本可以确定是数据加载流程的问题。6.5 常见问题速查表现象可能原因解决方案训练loss为NaN学习率过大或数据含NaN降低学习率检查深度图和标签mIoU始终很低label_mapping不对齐检查ScanNet语义类别映射显存不足网格数过大或batch过大调整voxel_size、batch_size开启amp加载数据很慢单线程处理调大num_workers可视化无输出预测范围超出点云范围检查point_cloud_range是否覆盖场景区域训练不收敛预训练权重未加载或传感器参数错加载预训练权重校准内外参7. 实操心得与扩展建议最后分享几点我自己跑下来的体会。训练EmbodiedOcc给我最大的感受是“工程化程度高”。它不像很多学术代码只是把论文里的结构复现出来而是真的为训练效率和显存占用做了很多优化。如果只是想快速出个结果建议直接用官方在nuScenes或ScanNet上的预训练权重做微调不要从零训练。从零训练200个epoch在单卡3090上大概需要3到4天微调可能只需要半天。另外一个扩展方向是做时序融合。EmbodiedOcc 目前的版本主要针对单帧输入虽然ScanNet的训练流程里有帧间pose但模型本身并没有利用多帧信息。如果你有连续视频流输入可以考虑在外围加一个简单的帧间特征累积模块这样能明显提升动态场景下的占用预测稳定性。不过这个改动需要改数据加载逻辑建议先跑通单帧的baseline再动手。对于想继续深入这个方向的朋友我建议你仔细读一遍models/backbones/embodiedocc_backbone.py这个文件。整个框架的精华几乎都浓缩在这个backbone的投影和对齐逻辑里读懂了它你就理解了为什么这个模型能比之前的方法少用一半显存还能提升精度。