ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MonoScene单目3D语义场景补全:2D到3D投影与复现调参

MonoScene单目3D语义场景补全:2D到3D投影与复现调参

一张普通的室内照片,你能看懂画面里有几张桌子、几把椅子,但镜头没拍到的角落、家具背后被挡住的墙根、沙发底下那一片,全是一团模糊的猜测。MonoScene 想干的事情就是让模型也只凭这一张图,把整个三维空间的占据情况和语义类别一起补出来——哪些体素是地板、哪些是墙、哪些是悬空着的椅子腿,全都要给出答案。这就是单目3D语义场景补全(Monocular Semantic Scene Completion)要解决的问题。它不需要激光雷达,不需要多视角,输入就是一张 RGB 图,输出是一整块带语义标签的体素网格。我接触这个方向是因为手里有个室内机器人导航的小需求,深度相机成本压不下来,退而求其次只能考虑纯视觉方案。翻了一圈资料,MonoScene 算是把“2D 特征怎么合理地送进 3D 空间”这件事讲得比较透的一个方案,代码结构也相对干净。下面我按自己复现和改造的实际过程,把这个模型的设计思路、关键细节、参数取舍和踩过的坑完整拆一遍。不管你是刚接触 3D 视觉的学生,还是想把它挪到自己项目里的工程师,应该都能从里面抠出点能直接用的东西。

1. 先把问题摆清楚:单目3D语义场景补全到底难在哪

1.1 从一张2D图到一整块3D体素,中间隔着什么

语义场景补全这个任务,本质上是“预测一个三维网格里每个小方块属于什么类别”。体素网格里的每个格子有三种状态:空、被占据但不知道是什么、被占据且知道是椅子/桌子/墙。模型要做的是把后两者尽量准确地填出来。这和普通的3D目标检测不一样,检测只关心框住的物体,场景补全关心的是整个空间,包括地板、天花板、墙面这些大面积的东西,也包括那些只露出一个小角的物件。

单目版本又额外加了一层难度。多视角或者带深度的方案,至少知道每个像素对应的三维点在哪儿,投影关系是确定的。单目只有一张图,深度信息完全靠猜。模型必须从纹理、遮挡关系、透视缩放这些线索里推断出“这个像素大概在多远”,再把这个推断结果和语义信息绑在一起。我一开始低估了这件事的难度,觉得无非是“2D 分割 + 深度估计”两个网络拼一拼,实际跑起来才发现,深度误差会沿着视线方向被无限放大,一个 10% 的深度偏差,投射到 4 米远的地方就是 40 厘米的偏移,体素级别早就错位了。

所以这个任务的难点不在分割,也不在深度,而在于如何让 2D 的语义特征和 3D 的空间结构在同一个表示空间里对齐。MonoScene 的核心贡献基本都围绕这件事展开。

1.2 三个绕不开的硬骨头

我把复现过程中遇到的困难归了归类,大致是三个层面。

第一个是遮挡区域的无中生有。一张图里,桌子挡住了它后面的椅子,那部分体素在图像上根本没有对应的像素。模型必须靠场景常识去补:桌子后面通常是椅子或者墙,地板会一直延伸下去。这个“靠常识补”的能力,纯靠卷积很难学到,得在 3D 空间里做足够大的感受野。

第二个是类别极度不平衡。室内场景里,空体素占绝大多数,墙、地板、天花板加起来又能占掉剩余的一大半,真正的椅子、书本、电视这些“小物件”可能只占千分之几。如果直接拿交叉熵去训,模型很快就学会“全猜墙”,损失照样降得很好看,但 mIoU 惨不忍睹。这个坑我在第一个版本里踩得结结实实。

第三个是内存和算力的硬约束。240×144×240 的体素网格,八百多万个格子,用 3D 卷积在上面跑,显存是按 GB 往上跳的。我最早拿单卡 24G 试,batch size 只能开到 1,还时不时 OOM。这个约束会反过来影响你的架构选择、分辨率设置和 batch 配置,不是纯粹的工程问题。

1.3 为什么“单目”这个前缀值得单独拎出来说

市面上做场景补全的方案不少,有拿 RGB-D 的,有多帧融合的,有靠激光点云体素化的。单目方案的价值在于部署成本。一个普通的 USB 摄像头就能跑,不需要标定深度相机,不需要处理多传感器时间同步,装在移动设备或者低成本机器人上都不心疼。代价就是精度会掉一截,尤其是远处和遮挡区域。

我的判断是:如果你的场景是室内、作用距离在 5 米以内、对远处精度要求不高,单目方案完全够用;如果是要做高精度建图或者近距离抓取,那还是老老实实上深度传感器。MonoScene 这个工作的意义不在于它把单目做到了多高的精度,而在于它给出了一套相对完整的“2D 到 3D 特征搬运”的方法论,这套东西你换成 RGB-D 输入照样能用,甚至效果更好。

2. MonoScene的整体设计:2D特征如何被“抬升”到3D

2.1 网络骨架的三段式结构

MonoScene 的整体结构可以粗暴地切成三段:2D 特征提取、2D 到 3D 的投影桥接、3D 编解码与输出。这个三段式不是它独创的,但每一段的具体处理方式有自己的讲究。

第一段是标准的 2D 主干,用 ResNet 系列的编码器把输入图像压成多层特征图。这里有个细节值得注意:它不是只取最后一层,而是取了多个 stage 的输出。原因很直接,深层特征语义强但分辨率低,浅层特征分辨率高但语义弱,而 3D 体素对空间精度要求很高,只用深层特征去投影,边界会糊成一团。多尺度特征分别投影再融合,是我实测下来比较稳的做法。

第二段是投影桥接,也就是论文里的 FLoSP 模块。这一段是整个方法的核心,后面单独展开说。

第三段是 3D 的编码解码结构,用 3D 卷积在体素空间上做特征提取和上采样,中间穿插了关系建模模块。输出层是一个逐体素的分类头,类别数取决于数据集,NYU 那套设置是 12 类(含空类)。

整个前向过程的数据流大致是:图像 → 2D 多尺度特征 → 沿视线投影成 3D 特征体 → 3D 编解码 → 逐体素类别概率。理解这条链路,后面的调试才有方向感。

2.2 FLoSP:沿视线投影这件事为什么成立

FLoSP 全称是 Feature Line of Sight Projection,翻译过来就是“沿视线的特征投影”。这个名字起得很准确,它的做法确实就是沿着相机视线把 2D 特征“射”进 3D 空间。

具体的逻辑是这样:对于 3D 体素网格里的任意一个体素,它的中心点坐标是已知的。用相机内参把这个三维点投影到图像平面上,会落在一个像素位置(或者落在图像外)。取该像素位置上的 2D 特征向量,作为这个体素的特征。所有落在同一条视线上的体素,共享同一个 2D 特征。

这个做法初看有点粗糙——同一条射线上不同深度的体素拿到完全一样的特征,深度信息完全丢失了。但仔细想想,这件事在单目条件下是唯一合理的选择。因为单目本来就没法确定深度,一条视线上的所有点在你的观测里是不可区分的,强行给它们不同的特征反而是无中生有。FLoSP 的聪明之处在于,它把这个“不确定性”原封不动地保留下来,交给后面的 3D 网络去消解。3D 卷积的感受野会结合邻域信息,逐步把这些共享特征分化开——比如一条射线上靠近地面的体素,周围都是地板特征,它自然会被判成地板;射线中段悬空的部分,周围是空的,它就被判成空。

投影的数学表达很简单。给定体素中心的相机坐标 (X, Y, Z),内参矩阵的焦距 (fx, fy) 和主点 (cx, cy),像素坐标为:

u = fx * X / Z + cx v = fy * Y / Z + cy

这里 Z 是沿光轴方向的深度。取整之后去特征图上采样。如果 (u, v) 落在图像边界外,就用零向量填充。这个边界处理看着不起眼,但我在调试时发现它对边缘区域的预测质量影响挺大,后面会细说。

还有一个实现上的关键点:多尺度特征怎么投影。不同 stage 的特征图分辨率不同,投影时要用对应尺度的内参做缩放。比如原图是 480×288,某个 stage 的特征图是 60×36,那像素坐标要按比例缩到 60×36 的坐标系里再采样。这个缩放系数算错了,整个投影就全歪了,而且歪得很隐蔽——损失照样能降,只是结果一直是糊的。

2.3 3D Context Relation Prior:让体素之间会“说话”

光有投影还不够。FLoSP 给每个体素的特征是“这条视线上看到了什么”,但没告诉模型“这个体素和周围的体素是什么关系”。室内场景里有大量强结构先验:椅子一定在地板上,墙一定是竖直连续的,天花板和地板平行。这些关系必须在 3D 空间里建模。

论文里用了一个叫 3D CRP 的模块,本质上是 3D 空间上的自注意力变体。它在 3D 特征图上做通道维度和空间维度的关系建模,让每个体素能“看到”一定范围内的其他体素。这个模块插在 3D 编解码器的中间层,不放在最前面也不放在最后面——放最前面的话特征还没成型,关系建模没有意义;放最后面的话计算量太大,而且此时特征已经定型,改不动了。

我在复现时做过一个对照实验:把 CRP 模块整个去掉,其他不变,mIoU 掉了差不多 3 个点,主要损失集中在墙面边界和家具的支撑结构上。这说明它确实在补“结构一致性”这块短板。但也要注意,这个模块的显存开销不小,我在 24G 卡上跑的时候,把它的通道数砍了一半才勉强塞进去。

3. 关键细节与参数:动笔前必须想明白的几件事

3.1 相机内参与坐标系对齐

这是整个项目里最容易出错、又最容易被忽视的一环。MonoScene 的体素坐标系和图像坐标系之间的转换全靠内参,一旦内参对不上,后面所有的投影都是错的,而且错误不会以“报错”的形式出现,只会表现为“模型怎么训都不准”。

先说清楚坐标系约定。我用的是相机坐标系:X 向右,Y 向下,Z 向前(沿光轴)。体素网格的原点放在相机光心,网格范围是 X 方向 [-2m, 2m]、Y 方向 [-1.44m, 1.44m]、Z 方向 [0, 9.6m] 这一类设置,具体数值跟数据集走。体素尺寸 4cm,那么 X 方向的格子数就是 4m / 0.04m = 100 格……等等,这里要小心。

NYU 那套标准设置是 240×144×240 的网格,体素 4cm,对应物理尺寸是 9.6m × 5.76m × 9.6m。但注意轴向的对应关系:240 对应的是图像的高度方向还是宽度方向,不同代码库的实现不一样。我见过有的实现把第一个维度当高度,有的当宽度,混用的话投影出来会是一个转了 90 度的结果。判断方法很简单:拿一张有明显地板线的图,投影后看地板是不是水平的,歪了就是轴搞反了。

体素中心到相机坐标的转换公式:

X = (i - W/2) * voxel_size Y = (j - H/2) * voxel_size Z = k * voxel_size

其中 i、j、k 是体素索引,W、H 是网格在 X、Y 方向的格子数。这个公式里的偏移量取决于你把原点放在网格中心还是角上。我的建议是:先拿一个已知的三维点做单元测试。手动算一个点的体素坐标,再走一遍投影函数,看能不能落到预期的像素上。这个测试花十分钟,能省掉后面两天的排查。

提示:不要相信任何“默认内参”。不同数据集的焦距、主点差别很大,NYU 用的是 518.86 左右的焦距和接近图像中心的的主点,但具体数值要去数据集官方说明里核对,不要从别的项目里抄。

3.2 体素网格的尺寸取舍与显存账

体素分辨率的选型是个典型的“精度 vs 算力”权衡。4cm 的体素尺寸是 NYU 这套基准的惯例,好处是能和别人的结果直接对比,坏处是显存吃紧。

算一笔账。240×144×240 的网格,总共约 830 万个格子。假设 3D 特征图的通道数是 32,用 float32 存,单层特征就是 830万 × 32 × 4 字节 ≈ 1.06 GB。3D U-Net 编解码器里通常有五六层这样的特征图,再加上梯度、优化器状态,显存轻松破 20G。这还没算 2D 主干和注意力模块。

我的实际配置是:单卡 24G,batch size 2,3D 部分的基础通道数从论文的 32 砍到 24,并且把 CRP 模块只在最低分辨率层启用。这样能跑起来,但训练速度是每轮四十多分钟,一个完整的训练周期得按天算。

如果你也想复现,我给几条路:

方案做法代价
降分辨率体素尺寸从 4cm 放到 8cm,网格变成 120×72×120细节丢失,小物件基本没了
降通道3D 基础通道 32→16mIoU 掉 1-2 点
稀疏化只对非空体素做卷积实现复杂,需要额外写稀疏卷积
混合精度开 AMP显存省 30% 左右,偶尔有数值不稳

我个人优先推荐混合精度 + 适度降通道,这两个组合起来显存能压到 12G 以内,损失可控。稀疏卷积效果最好但工程量最大,除非你打算长期在这个方向上做,否则不建议一上来就啃。

3.3 损失函数的构成与权重分配

MonoScene 的损失由两块组成:体素级的交叉熵,和论文提出的场景类别亲和损失(SCAL)。交叉熵好理解,逐体素算分类损失。SCAL 是专门对付类别不平衡和空间结构一致性的,值得单独说。

SCAL 分两个子项。第一个是语义亲和项,思路是让同一类别的体素特征在特征空间里靠拢,不同类别的推开。它不看单个体素的分类对不对,而是看整类的特征分布是不是紧凑。这个设计对长尾类别特别有效,因为即使某个类别的样本很少,只要它们特征聚在一起,模型就能学到这个类的“形状”。

第二个是视锥比例项,这个更巧妙。它利用了一个统计先验:在相机前方不同距离的视锥里,各类别占据的比例应该大致稳定。比如近处的地板占比高,远处墙占比高,但总体上不会出现“某个视锥里全是椅子”这种离谱情况。这一项约束的是全局分布,能防止模型在局部过拟合。

权重设置上,我的经验是交叉熵和 SCAL 大概 1:1 起步,SCAL 内部两个子项再各占一半。如果发现小类别完全不学,可以把语义亲和项的权重往上调,调到 1.5 左右试。但别调太高,太高会让特征过度聚拢,边界变得模糊,墙和地板的交界处会出现大片误判。

注意:SCAL 的实现依赖 batch 内的类别统计,batch size 太小(比如 1)的时候统计量不够,损失会非常不稳定。我当时 batch size 设为 1 跑了一晚上,损失曲线跟心电图一样,后来改成 2 才稳下来。

4. 动手复现:从数据到第一个可视化结果

4.1 数据管道的搭建要点

数据这块的坑主要在标注格式上。NYU 那套场景补全的标注是体素级的,但每个样本给的是一堆稀疏的点云标注再体素化,还是直接给体素网格,不同发布版本不一样。我拿到的是点云版本,需要自己写体素化脚本。

体素化的逻辑不复杂:对每个标注点,算它落在哪个体素格子里,把类别填进去。但有几个细节必须处理。第一,同一格子可能落多个点,类别冲突时按优先级取,我的做法是面积大的结构类(墙、地板)优先,小物件其次。第二,深度截断,超过 9.6 米的点直接丢掉,不然网格装不下。第三,坐标系对齐,标注点云和图像必须是同一时刻、同一相机坐标系下的,这一点如果数据集给的是世界坐标,还得先转到相机坐标。

数据增强方面,我用的是随机水平翻转加颜色抖动。这里要特别小心:水平翻转之后,相机内参的 cx 也要跟着翻,不然投影会镜像错位。这个 bug 我调了大半天才发现,症状是模型在验证集上表现正常,但只要开了增强,训练损失就居高不下。原因是翻转后的图像和没翻转的内参不匹配,投影出来的特征全是错的。

加载效率也值得提一句。体素标注单个样本就有几 MB,如果每个 batch 都现读现算,GPU 大部分时间在等数据。我的做法是预处理阶段把体素标注压成稀疏格式(只存非空格子的坐标和类别),加载时再还原,IO 量能降一个数量级。

4.2 FLoSP与投影模块的代码骨架

投影这块是整个项目里最需要自己写清楚的部分,我给一个简化版的实现骨架,重点看坐标变换和边界处理。

import torch import torch.nn.functional as F def build_voxel_coords(grid_size, voxel_size, device): """生成体素中心的相机坐标,形状 [N, 3]""" W, H, D = grid_size xs = (torch.arange(W, device=device) - W / 2) * voxel_size ys = (torch.arange(H, device=device) - H / 2) * voxel_size zs = torch.arange(D, device=device) * voxel_size grid_x, grid_y, grid_z = torch.meshgrid(xs, ys, zs, indexing='ij') coords = torch.stack([grid_x, grid_y, grid_z], dim=-1) return coords.reshape(-1, 3) def project_to_image(voxel_coords, intrinsic, feat_hw): """体素坐标投影到特征图坐标,返回归一化采样网格""" fx, fy = intrinsic[0, 0], intrinsic[1, 1] cx, cy = intrinsic[0, 2], intrinsic[1, 2] X, Y, Z = voxel_coords[:, 0], voxel_coords[:, 1], voxel_coords[:, 2] Z = Z.clamp(min=1e-3) u = fx * X / Z + cx v = fy * Y / Z + cy # 缩放到特征图分辨率 fh, fw = feat_hw u = u / intrinsic[2, 2] * (fw / (2 * cx)) * 2 - 1 # 简化写法,实际按图像宽度算 v = v / intrinsic[2, 2] * (fh / (2 * cy)) * 2 - 1 return torch.stack([u, v], dim=-1) def flosp(feature_2d, voxel_coords, intrinsic): """把2D特征沿视线投到3D""" B, C, fh, fw = feature_2d.shape grid = project_to_image(voxel_coords, intrinsic, (fh, fw)) grid = grid.unsqueeze(0).expand(B, -1, -1) sampled = F.grid_sample(feature_2d, grid, align_corners=False, mode='bilinear', padding_mode='zeros') return sampled.squeeze(-1)

上面这段是示意,实际用的时候有几个地方要改。project_to_image里的归一化那两行我写得比较糙,正确的做法是先把像素坐标除以特征图宽高,再映射到 [-1, 1],这样边界处理才准。padding_mode='zeros'保证图像外的体素拿到零特征,这一点很重要,不然会在视野边缘产生虚假响应。

另外,多尺度特征投影时,每个尺度都要单独跑一遍flosp,然后把结果在通道维拼接。拼接之前记得做一次通道对齐,不同 stage 的输出通道数不一样。

3D 解码器那部分我是直接用的现成 3D U-Net 结构,把下采样次数从 4 次减到 3 次,因为网格在深度方向的有效范围其实只有前半段,过度下采样会让远端的体素直接消失。

4.3 训练配置与个人调参记录

我的训练配置大致是这样:优化器 Adam,初始学习率 1e-4,weight decay 1e-4,余弦退火,batch size 2,训练 30 轮左右。这个配置是在 24G 单卡上摸索出来的,跟原论文的多卡设置肯定有出入,但趋势是对的。

调参过程中有几个印象比较深的节点。第一轮跑完,验证集 mIoU 只有 0.12,比瞎猜好不了多少。排查发现是 SCAL 权重设得太大,把交叉熵压死了,模型只顾着让特征聚拢,完全不管分类对不对。把 SCAL 权重从 1.0 降到 0.3,mIoU 立刻跳到 0.18。

第二个节点是发现模型对“天花板”这个类几乎完全预测不出来。原因是训练集里天花板被裁掉的比例很高——很多样本的图像顶部根本拍不到天花板,标注里有但图像里没有,模型自然学不会。我的处理是把图像上方 10% 的区域在 3D 监督里屏蔽掉,让模型只对“看得到”的部分负责,mIoU 反而涨了。

第三个节点是学习率。1e-4 在前期降得很快,但到第 15 轮左右就平了。换成 2e-4 配合更长的预热,最终结果好了大约 1 个点。这个和 batch size 有关,batch 小的时候学习率不能太大,否则梯度噪声太大。

下面是我整理的一组对照记录,供参考:

配置项尝试值观察结果
SCAL 权重1.0 / 0.5 / 0.30.3 最稳,1.0 会导致分类退化
学习率1e-4 / 2e-42e-4 配合预热略好
3D 通道数32 / 24 / 1624 是当前显存下的最优折中
batch size1 / 21 时损失震荡严重,不推荐
混合精度开 / 关开,显存省约 30%,精度无损

4.4 推理、评估与可视化

评估这块,标准的指标是场景补全 IoU 和各类平均 IoU。计算的时候要注意,空类通常不参与 mIoU 平均,不然所有模型都能刷到 0.9 以上。我见过有人把空类算进去,结果报出来的数字好看得离谱,实际一点用没有。

可视化我认为是这个项目里最有价值的一环。体素结果直接看数字很难判断好坏,必须画出来。我的做法是把预测结果转成点云,用不同颜色标不同类别,然后从侧视角和俯视角各渲一张图。俯视图特别能暴露问题——如果墙是歪的、地板有洞、椅子悬空,俯视图上一眼就能看出来。

推理速度方面,单帧前向在单卡上大约 200 毫秒,这个速度做离线处理没问题,做实时控制就有点勉强了。如果真要上机器人,得做模型剪枝或者换成更轻的 3D 主干,这是另一个话题了。

5. 踩坑记录:那些让训练白跑一整晚的问题

5.1 投影错位:最常见的假bug

投影错位是我遇到最多的坑,而且它的表现形式特别有迷惑性——损失在降,loss 曲线很漂亮,但预测出来的体素永远是糊的,边界不清,位置整体偏移。

排查这类问题的第一步是可视化中间结果。把 FLoSP 输出的 3D 特征沿着深度方向做一个最大值投影,看看它在俯视图上是不是和图像的语义分布对得上。如果对不上,说明投影链路有问题。我做过一个简单的调试脚本:取图像上一个明显的物体(比如一把椅子),手动算它的三维中心,然后检查这个位置上的 3D 特征是不是和椅子所在像素的 2D 特征一致。不一致就往前追,看是坐标算错了还是采样网格算错了。

常见的错位原因有这么几个。内参的主点 offset 方向搞反了,这在一些数据集里很常见,因为不同库对主点的定义有差异。特征图分辨率缩放时用了整数除法,导致小数部分丢失,投影出现半个像素的系统性偏移。还有体素坐标原点放在角上还是中心,这两种约定差出来的偏移量正好是网格尺寸的一半。

提示:写一个单元测试,固定一组内参和一个三维点,断言投影结果的像素坐标在容差范围内。这个测试在改代码之后跑一遍,能挡住大部分回归问题。

5.2 类别塌缩与损失不降

类别塌缩的症状是:训练损失一路下降,看起来很健康,但验证集上 mIoU 卡在 0.15 左右不动,可视化出来发现除了墙和地板,其他全空。

这本质上还是类别不平衡的问题。我的解决办法是三层。第一层在损失函数上加权,对小类别给更高的交叉熵权重,权重按类别频率的倒数开根号来算,开根号是为了不让权重过分极端。第二层靠 SCAL 的特征聚拢,这个前面说过。第三层在采样上做文章,保证每个 batch 里至少包含一个有稀有类别的样本。

还有一个隐蔽的原因是标注噪声。有些样本的标注里,被遮挡区域的类别填得不准,模型学到这些错误标注之后反而拉低了整体表现。我当时的处理是把这些样本的对应区域在损失里 mask 掉,相当于告诉模型“这里不确定,别学”。

损失不降的另一种可能是梯度消失。3D U-Net 深了之后,浅层的梯度传不回去。加残差连接是最直接的办法,我在每一层 3D 卷积后面都加了残差,训练稳定性提升明显。

5.3 显存爆掉后的几条活路

显存问题几乎是绕不过去的,我前后试过五六种方法,按效果排序大概是:

  1. 混合精度训练。改造成本最低,一行torch.cuda.amp的事,显存直接省三成。唯一要注意的是损失里如果有除法或者对数运算,要做数值保护。
  2. 梯度累积。显存不够开大 batch,就用累积的方式模拟。设accum_steps=4,等效 batch 就是 4 倍。代价是训练时间变长,但结果基本等价。
  3. 3D 特征通道裁剪。这个要小心,砍太多会伤精度,我的经验是基础通道不低于 16。
  4. 激活检查点。用时间换显存,前向的时候不存中间激活,反向的时候重算。显存能省一半,速度慢三成左右。
  5. 稀疏卷积。效果最好,工程量最大,建议放到最后考虑。

还有一个容易被忽视的点:数据加载器本身也吃显存。如果用了pin_memory=True加上多进程预取,会额外占掉一两个 G。显存紧的时候把这个关掉,虽然加载慢一点,但能救回来不少。

5.4 问题速查表

我把这一路踩过的坑整理成一张表,遇到问题时可以按症状对照着查。

症状可能原因排查方向
损失下降但预测全糊投影尺度对不上检查多尺度特征的坐标缩放系数
某个方向系统性偏移体素坐标原点约定错误用单点测试验证坐标换算
小类别完全不出现类别权重不足 / 采样不均加类别权重,检查 batch 内类别分布
开启增强后性能骤降内参没跟着翻转检查增强管线里的内参同步
损失震荡剧烈batch 太小导致统计不稳增大 batch 或改用梯度累积
边缘区域预测乱图像外体素的特征填充方式确认 padding 为 zero 且归一化正确
推理显存不够一次处理所有体素分块推理,沿深度方向切开
训练到一半 loss 变 nan混合精度下的除零在对数、除法处加 eps 保护

6. 这套思路还能往哪延展

6.1 稀疏深度先验的接入方式

纯单目的精度上限摆在那儿,但如果手头有一点点深度信息——哪怕是从手机上拿到的稀疏深度点,或者是从结构光模组拿到的低分辨率深度图——把它接进 MonoScene 的框架里,效果会有明显提升。

接入方式有两种。第一种是改投影方式,不再沿视线均匀投影,而是根据深度先验把 2D 特征放在特定的深度位置上,其余位置做插值填充。这个改法对代码改动小,但需要对深度先验做平滑处理,不然会出现明显的分层。第二种是加一个深度引导的特征调制模块,用深度图作为条件,对 3D 特征做逐体素的加权。这个改动大一些,但更灵活,也更容易训。

我自己试过第一种,在深度先验覆盖率 10% 左右的情况下,mIoU 能提升 2 到 3 个点,主要收益在物体边界上。深度覆盖率再低就没啥效果了,稀疏到 1% 以下基本等于没有。

6.2 换到别的场景要改哪些地方

MonoScene 的默认设置是室内场景,挪到别的地方要动几处东西。

如果要做室外场景,体素尺寸得放大,4cm 在外面连一辆车都装不下,至少得 20cm 起。同时网格尺寸要拉长,深度方向可能要延伸到 50 米。这会带来显存爆炸,必须配合稀疏化或者分块处理。类别体系也要换,室内的“椅子”“桌子”在室外没意义,得换成“车辆”“行人”“路面”这些。

如果要做工业场景,比如仓库或者车间,结构先验会比室内更强——货架是规则排列的,地面是平的,通道是直的。这种情况下可以引入更强的几何约束,比如强制地面体素的法向朝上,这能显著提升地面区域的准确率。

如果是做动态场景,那就要引入时序信息了。单帧的 MonoScene 处理不了运动物体,得在特征层面做帧间融合,或者干脆改成用视频输入。这个方向的工程量比单帧大得多,建议先把单帧跑稳再说。

我个人的体会是,MonoScene 这套框架的价值在于它给出了一个清晰的“2D 特征如何进 3D”的范式。FLoSP 这个思路可以迁移到很多地方——你把 2D 主干换成别的任务的分割网络,把 3D 解码器换成点云补全或者占据栅格预测的网络,中间的投影桥接逻辑基本不用改。我后来把它挪到一个仓库货物体积估算的小项目里,只改了类别体系和体素尺寸,投影部分原封不动,跑出来的结果基本可用。这种可迁移性,比单纯的精度数字更有意思。

返回列表