Back to case study

Detailed technical write-up.

室内结构化重建

室内结构化重建

从带噪多层 mesh 出发,产出闭合、单层、可对齐户型的结构化模型

1. 项目信息

项目 内容
项目名 室内结构化重建(structure_recon)
时间 2022-01 起,主开发约 5 个月,后续为维护
应用场景 Cyclops / Galois 全景重建管线的后处理阶段
角色 结构化建模模块设计与实现
输入 单层室内三角网格 + 全景相机外参 + 对齐信息
输出 结构化 mesh(闭合、单层墙)+ 非结构物 mesh + 分间信息

整体流程见下图。整个系统按五个阶段组织:平面提取 → 占用栅格与分间 → 屋顶和房梁结构识别 → 平面汇总与场景剖分 → 可见性最大流提取结构面,最后再做一次顶饰之上区域裁剪与非结构物分离。相关的算法原理(cell complex 剖分、kinetic framework、graph-cut 表面提取等)参见配套的 算法原理综述

室内结构化重建整体流程

各阶段各司其职:平面提取从三角网中拿到墙、地、顶等候选大平面;占用栅格图把三维场景压成一张二维鸟瞰图,为分间和辅助墙面提供依据;屋顶和梁识别负责把复杂屋顶结构翻译成显式的切割面;平面汇总把所有切割面组织到一起,用增量式的方式对场景做局部剖分,得到一堆凸多面体(Cell Complex);最后用最大流求解每个 Cell 是内还是外,两者之间的分界面就是最终的结构化 mesh。

2. 项目背景

原始重建管线(MVS 或 Poisson 三角化后的结果)产出的是一层”贴合表面”的三角网。它能表现细节,但拿去做量房、拉户型图、算面积这些下游任务时问题很多——墙面往往是抖动的一层皮,家具和结构面混在一起,天花板下面还挂着吊灯投影出来的碎片。对成品指标(户型闭合、墙面单层、面积可信)来说,这一层原始 mesh 更像是”素材”,而不是”成品”。

本项目的定位是把这层素材翻译成一个可用于量房和展示的结构化模型。 它不是要重跑一遍重建,也不做纹理,只在既有 mesh 的基础上,识别出结构性部分(墙、地、顶、门、梁),把非结构部分(家具、电器、装饰)单独拆出来。项目本身是纯几何 + 图优化的处理链,不依赖训练模型,稳定性主要靠工程上的层层兜底。

3. 问题描述

3.1 原始 mesh 的主要缺陷

原始 mesh 的问题不是一类,而是多类叠加。

墙面法向抖动,出现多层墙。 MVS/激光深度本身有 2-5cm 量级的噪声,重建端得到的墙不是一块严格的平面,而是有厚度、有分层的一片”薄壳”。表现在成品上是相邻点位之间存在 5-15 处遮挡,量房时点不清墙线。

存在悬浮碎屑和外挂物。 光线穿过窗户到室外、镜面反射、动态物体这些都会在重建结果里留下小块的悬浮 mesh,散布在房间内外,破坏包围盒也拖累后续几何计算。

破洞与穿透。 遮挡严重的墙角、深柜后面、天花板边角容易出现开孔。可见性射线穿透这些孔,往往会误报”该 Cell 应该是外部”,导致墙面在最大流阶段被削掉。

家具和贴墙电器混入结构面。 沙发靠背、贴墙的橱柜、窗帘杆,法向和面积都跟真墙很接近。如果单纯按”大面积 + 竖直 + 曼哈顿”来选墙,会把它们错认成墙的一部分。

3.2 下游的具体症状

  • 点位间遮挡:VR 漫游时相邻点位无法互相看到,5-15 处/项目
  • 墙面分层:户型图拉不出一条清晰的墙线,标注端投诉多
  • 面积不闭合:户型图上大部分房间出现 0.5-2m² 的漏点
  • 墙面消失:门附近墙面因外部 mask 权重不够而被”切掉”
  • 天花板缺失或超出楼层:多层楼梯间的视点被误纳入当前楼层

这些症状让原始 mesh 无法直接用于面积计算、户型图生成和 VR 展示,必须做结构化。

4. 解决方法

4.1 平面提取与合并

任何后续的空间剖分和 graph-cut 都以”一组质量较高的候选平面”为前提。这一阶段解决”从抖动的三角网里稳定拿到墙、地、顶”。

先按平面性排序种子,再做区域生长。 直接从任意面片开始生长,一旦种子落在曲面区域,出来的平面就是歪的。这里对每个顶点估计均曲率,再把三个顶点的曲率加起来作为面片的”曲率成本”,排序后从最平坦的面片开始生长。生长条件是”三个顶点到候选平面的距离都小于 30cm,且法向夹角小于 10°”。这套排序把种子稳稳地放在墙的正中间,边缘的杂面片留到最后再处理。

用邻接图迭代合并。 单次生长后得到的平面很多且碎。构建平面邻接图,对每对邻接平面:法向夹角小于 10°、两侧”点到对方平面距离 < 10cm”的顶点数超过阈值 $N_t = \min(|S_i|,|S_j|)/5$,就合并到面积更大的那个。合并阈值取双方顶点数的 20%,是经验值——太小会把不同方向的墙误合并,太大又合并不动。合并后重新拟合平面,反复至收敛。

Manhattan 修正与小平面吞并。 一次可选的额外步骤:把大平面按面积从大到小排序,法向偏离 $[\hat x, \hat y, \hat z]$ 不超过 10° 的平面强制对齐到该轴。对齐后的平面再做一次”距离与法向都相近”的吞并,把碎小平面并到最近的大平面。这一步在墙面稍歪、又需要输出规整户型的场景里效果明显。工程上这一步是可选开关,默认不开——大部分场景直接吃自然拟合的结果就够了,强制对齐反而会把真实存在的斜墙拉歪。

区域生长产生的原始平面很多、很碎,经过邻接合并后大部分同法向平面归并成大块,最终这些平面会被送到后面的场景剖分阶段生成 cell complex。下图是同一份数据从初步过分割 → 迭代合并 → 剖分后 cell complex 的三张俯视:

平面分割与场景剖分俯视

(a) 原始 mesh 的初步平面分割,过分割、碎面片非常多;(b) 迭代合并后相邻同法向的平面已归并;(c) 汇总切割面后的场景剖分俯视(cell complex 线框)。

4.2 占用栅格图与几何分间

分间是整套系统的枢纽。房梁识别、门位置、天花板/地板高度、辅助墙面全部需要一张可靠的”鸟瞰视角自由空间图”。

思路是把三维可见性压到二维。 对每个视点做一次到 mesh 的全景光线投射,得到一堆”视点 → 首个命中点”的线段。把这些线段一段一段绘制到水平面上,命中过的像素标记为 1(自由空间),未被命中过的标记为 0。同一像素还记录扫过的最小 y(当做天花板高度)和最大 y(当做地板高度)。分辨率取 1cm/px,粒度足够刻画墙厚,又不至于让下游算法慢下来。

占用栅格图的构建

左:原始 mesh 俯视。右上:视点到 mesh 的射线段绘制到水平面。右下:射线穿过的自由空间形成的二值占用栅格图。

从占用栅格出发,走三条并行的辨识路线。 单一线索很容易被局部缺陷带偏,所以工程上并没有押注一种方法——先各自跑,再互为兜底

三种初始分间方法对比

  • (a) 基于墙体可见性的聚类:把每个像素对轮廓采样点的可见性做成一个二值向量,用 K-Medoids 按 Hamming 距离聚类,再合并中心距离小的类。理论上等价于”能看到同一批墙线的像素属于同一房间”,但 K 取多少不好定,收敛慢,大房间容易过分割阳台
  • (b) MaskRCNN 直接推测:训练一个语义分割模型直接输出分间。速度快但泛化性差,跨数据集(Cyclops → Galois)掉点严重
  • (c) 基于屋顶高度的聚类:房间之间往往被门框或垭口分隔开,天花板高度在垭口处会突变。对天花板高度图做 region grow,天然拿到房间的过分割结果

这三条路线互为对手方案。 墙体可见性的强项是”物理上能看到同一批墙的像素归一类”,弱点是 K-Medoids 慢和大房间过分割;MaskRCNN 是数据驱动的,能利用图像先验,但依赖训练分布;屋顶高度是纯几何的,速度快、部署简单,但遇到吊顶或非水平天花就会退化。三条路径都能拿到合理的初分。工程上最终选了屋顶高度这一路作为默认——理由是速度快、无需训练、失败模式与后续 MRF 精修正好互补(前者出过分割,后者收敛到干净边界)。

精修用约束三角化 + 多标签 graph-cut。 只有初分是不够的。工程上先对占用栅格的外轮廓做 轮廓简化,然后交给 三角剖分库做约束 Delaunay。三角面片继承所在像素的初始分间标签,形成一张”三角面片邻接图 + 面标签”的图。然后:

  1. 每个类别里挑外接圆最大的三角面片作为种子,把它的数据项代价设为 0,其它面片对该类的代价设为 1000
  2. 相邻面片如果标签相同,平滑项代价为 0;标签不同,代价取两片共边的世界坐标长度
  3. 用 GCO(Multi-label Graph Cuts)解一次最小化,得到每片三角面的最终房间归属
  4. 迭代:删掉面积小于阈值的房间标签,合并共边长度大于门宽(1.5m)的两类;直到删无可删、并无可并

早期版本对整个 free space 做 Delaunay 后再用 graph-cut,种子选外接圆最大的三角形;这个做法在房间形状规则时可用,但阳台、狭长走廊经常被割错。后来改成先用栅格分割给每个三角形一个初始标签,再用 graph-cut 精修,稳定性明显提升:

精细分间流程的两代做法

这里做迭代而不是一次到位,是因为 graph-cut 的解依赖标签数量本身。 每次删除或合并后,标签空间发生变化,重新求解才能对剩下的房间做更精细的边界优化。收敛后房间之间的共边长度就是门/垭口的位置,直接拿来生成门平面并送到下一阶段。

4.3 屋顶与房梁识别

动机:室内经常出现挑高、房梁、垭口、吊顶。 单张鸟瞰图看不出这些垂直方向的结构,但它们必须要被识别出来——否则最大流阶段会因为缺少切割面而在梁的底部糊成一大块,或者天花板延伸到不该延伸的地方。

梁结构与 Grammar 解析

思路是把”从天花板到天花板的路径”编码成 Grammar 字符串。 观察一段梁的侧剖面:天花板(左)→ 竖直下沿 → 水平梁底 → 竖直下沿 → 天花板(右)。同样地,一个门框、一个垭口的上边缘也是这个形状。把每种结构翻译成同一个模式,再用正则表达式匹配就能识别。

具体做法:

  1. 建平面邻接图。 只保留视点高度以下的平面(防止路径经过地板绕远路),根据 mesh 面片邻接关系统计平面之间的邻接数
  2. 给边打权重。 两个邻接平面的法向叉乘给出交线方向,交线越接近水平(越垂直于 y 轴),代价越小;两平面平行则给一个较小的正则代价。这样”沿一段房梁走一圈”是一条低代价路径
  3. 对每对天花板平面跑 Dijkstra。 起点和终点都是天花板,中间可能穿过若干竖直墙面、水平面和噪声面片
  4. Grammar 编码。 沿路径把每个平面翻译成一个字符:2 = 天花板、1 = 竖直大墙、0 = 水平面、x = 其他/噪声
  5. 正则匹配。 匹配 ^2x*1[^2]*1x*2 这样的模式——两端天花板、中间至少两根竖墙、之间可穿插水平面但不能再出现天花板。命中的路径就是候选梁

门和垭口的识别复用同一个框架,但多一个约束: 门框还必须落在两个相反法向的墙面之间,且落在某个房间的墙内。识别到门线段后,沿门线方向裁出局部 mesh,找到左右墙面的中间位置,生成三个 ghost 平面(左门框、右门框、上门框)送到下一阶段。

这一阶段是纯几何 + 图论,没有任何训练数据。 它的稳定性来自”梁/门/垭口在物理上确实是这个形状”这个先验,而不是数据分布。代价是对严重不规则的屋顶(例如尖顶)会漏识别,工程上通过限定”路径至少两根竖墙”的最小约束来控制误报。

4.4 平面汇总与场景剖分

到这里,可用的切割面已经收集齐了:来自区域生长的原始平面 + 屋顶延伸出的天花板平面 + 户型辅助墙面 + 门框 ghost + 顶底 bounding box 面。把这些平面按优先级和面积排好,逐个插入到”当前场景划分”中。

做法是增量式局部剖分。 下图 (a) 展示了插入一个平面时的处理逻辑。

增量式局部剖分与最大流求解

对每个新平面:

  1. 先测试它与哪些 Cell 相交,把这些 Cell 标为 mask=1
  2. 只对 mask=1 的 Cell 做切分,一分为二
  3. 与 mask Cell 邻接但自身非 mask 的 Cell,只在共面部分被切开,用于保持拓扑一致性
  4. 局部更新拓扑关系(Cell-Cell 邻接、Face-Cell 归属)

只切局部,不切全局,是因为室内场景天然具有强局部性。 一间卧室的墙不会去切另一头的厨房。全局 arrangement 会产生大量无用 Cell,浪费内存并拖累后续最大流。这里参考了 [Boulch et al., 2014] 的做法,但更进一步——把插入顺序按语义分层:先竖直墙面 → 再天花板/地板 → 再门 → 最后 ghost。这样每一层都在前一层构造的 Cell 结构上做局部细化,而不是把所有平面挤在一起。

Ghost 平面的作用是补齐”真实平面延伸不到但拓扑上需要闭合”的地方。 门框上沿、垭口边界这些位置没有实际点云支撑,但如果不切开,两边房间的 Cell 会连在一起。Ghost 就是给一个不带可见性权重的辅助切割面,让拓扑成立但不干扰最大流。

4.5 可见性最大流提取结构面

内外划分是整套系统的最后一步,也是最关键的图优化环节。 剖分之后有几百上千个凸多面体 Cell,需要判断每个 Cell 是室内还是室外。分界面(Cut)就是最终的结构化 mesh。

做法是构造一个 S-T 图,用最大流求解最小割。 边权设计如图 7(b)、(c) 所示:

  • 数据项:视点所在的 Cell 强连到 S(源,代表”内部”),无限延伸出场景之外的 Cell 强连到 T(汇,代表”外部”)
  • 可见性权重:对每个可见点,从视点向该点画射线,射线穿过的每个中间面片累加代价 $\alpha_{vis} \cdot \text{plane2dis} \cdot \text{viewAngle}$。plane2dis 是点到面的距离,越小说明这个点越”落在面上”,权重越大;viewAngle 是视线与面法向的夹角余弦,越小说明视线越平行于面,权重越低(因为平行观测不可靠)
  • 正则项:每对邻接 Cell 的共同面片都有一个正比于面积的正则代价 $\lambda \cdot \text{area} \cdot \text{pointDensity}$,逼迫解优先切较小的面
  • Floorplan 硬约束:占用栅格内轮廓内部的 Cell 强连 S(无穷权重),外轮廓外部的 Cell 强连 T。这一条硬约束是稳定户型闭合最关键的一步——它保证了即便某个墙面因为局部破洞而被数据项”投票”到外部,最终割也还会把它切在正确的位置
  • Floorplan wall 加权:户型墙面的可见性权重乘以 10,压过一般的散点权重

求出最小割后,把 S 一侧的 Cell 标记为内部、T 一侧的标记为外部,两侧邻接的面翻出来就是结构 mesh。

4.6 顶饰清理与非结构物分离

清理天花板以上区域。 挑高楼层的视点有时候会跨越两个天花板,导致算法把上层空间也当作室内。收尾时按面片重心高度做一次筛选:如果面片位于天花板平面之上并超出 5cm 阈值,就直接扣除。这个阈值给了 5cm 的容差,是为了避免天花板本身有细微起伏的面片被误删。

非结构物分离。 得到结构化 mesh 后,把原始 mesh 中”离结构面距离小于 5cm”的三角面片标记为结构,反之标记为非结构。这一步用 CGAL 的 AABB 树做距离查询,速度可以接受。分开的两份 mesh 有各自的用途: 结构 mesh 用于户型和量房,非结构 mesh 用于家具展示和物体检测。可选的是把非结构 mesh 再合并回结构 mesh,得到一个”闭合的结构 + 家具”的完整场景。

4.7 效果示意

结构化 mesh 的最终效果

(a) 结构化 mesh 的 3D 透视,分间清晰、墙面单层闭合;(b) 同一场景的多角度视图;(c) 在场景剖分中识别到的门框位置(红色矩形);(d) 结构化后穿门通行的自然视角——门作为洞口保留,透过门看到的是原始 mesh 中未清理的家具区域。

前后指标(量化口径为项目级的经验分布):

指标 优化前 优化后
相邻点位遮挡 5–15 处/项目 0–2 处
墙面分层数 2–3 层 单层
户型闭合度 频繁漏点 完全闭合
家具与墙面混淆 常见 已分离到非结构 mesh
房间数识别 显式分间信息

5. 总结

这个项目本质上是”如何在噪声、破洞、家具遮挡都存在的原始 mesh 上,稳定地拿到一个闭合的结构化模型”。回过头看,有几个设计取舍值得保留:

用占用栅格图作为整个系统的中枢,而不是直接在 3D mesh 上工作。 三维空间的分间和结构识别问题非常难,但压到 2D 鸟瞰图后大多数问题变成经典的图像和图论问题(region grow、Delaunay、graph-cut、Dijkstra)。这些问题有成熟的库和明确的复杂度上界。代价是丢失部分垂直信息,但通过屋顶高度图、多层平面辅助等手段能够弥补。

分间、房梁、门框识别都统一到”图上的路径 + 语义 Grammar”的表达。 平面邻接图承载了几乎所有的语义结构判断——房间是节点、墙是标签、梁是路径、门是路径的端点。这种统一表达让工程实现可以复用同一套 Dijkstra + 正则匹配框架,而不是每种结构都写一遍识别器。

最终的内外划分完全依赖最大流,而不是启发式规则。 数据项、正则项、Floorplan 硬约束、Ghost 平面全部翻译成图的权重,交给最大流一次求解。局部规则(例如”这里应该有墙”)通过加大对应面片的可见性权重来引导求解器,而不是通过后处理硬改结果。这让整个系统的行为可控、可调试。

后续可能的方向:

  • 用深度学习产生的门/窗/家具语义作为额外权重加入最大流,进一步减少人工阈值
  • 支持多楼层重建,跨楼层间用楼板作为强分割面
  • 分间结果直接生成矢量户型图,作为对外发布的产品形态