Back to case study

Detailed technical write-up.

cad2fp

cad2fp

从 CAD 图纸的几何线段到结构化户型 JSON:一条基于限制性三角网 + 区域图规则推理的解析链路

1. 项目信息

字段 内容
项目名 cad2fp(CAD to Floorplan)
时间 2021-10 起,主开发约 4 个月,后续为维护
应用场景 房产平台 CAD 图纸批量数字化,生成前端可编辑的户型图
角色 算法设计与实现
输入 建筑户型的 DXF 或 DWG 文件(单份 ≤ 1000 m²)
输出 结构化的户型 JSON:点 / 墙线(含厚度)/ 门 / 窗 / 入户门

整体流程

处理链路整体分三层。输入层完成格式解析、单位换算、Block 过滤、门 Block 的深度学习分类,得到干净的线段集合。几何层在这些线段上构建限制性三角网,回收外边界,做区域分割,构建邻接图与平行边关系。语义层在图上按规则识别外墙 / 窗户 / 入户门 / 内墙 / 门,并把线段和厚度写成前端使用的户型 JSON。

2. 项目背景

CAD 图纸是建筑户型最常见的原始载体,但它的原生形态是几何线段的集合:没有”墙”、“窗”、“房间”这样的语义标签,也没有拓扑关系。要把这样的图纸接进前端编辑器或做后续的三维展示,必须先做一次”从几何到语义”的解析。这类解析在业内并不新鲜,但公开可用的通用方案大多依赖图层命名约定(WALL、WINDOW 之类),一旦碰到不规范的图纸——图层名任意、双线画法、家具块乱插——识别率就会明显下降。

cad2fp 的定位是一个不依赖图层命名的自动化解析器。 它以几何和拓扑为唯一输入,只对”户型图这种平面结构”做假设:有外墙、有内墙、有窗户成对出现、门是可复用块。不做房间语义识别(卧室 / 客厅),不做尺寸标注还原,也不识别家具位置——这些留给下游或人工。

3. 问题描述

3.1 DXF 输入的三大缺陷

理想输入与实际输入

图纸的几何是”画给人看的”,不是”画给算法用的”。 制图人员会用两条平行线代替一堵墙、把家具作为 Block 反复插入、在墙上留一段空隙好放门符号。这些约定对人类阅读没有障碍,但对算法而言意味着:一堵墙有 2–3 条线、场景里散布着几百个来自 Block 的多余线段、外轮廓在门的位置断开且断口大小随图纸而变。

图层命名不可靠。 同一批 DXF 文件里,“墙”这个概念可能出现在图层名 WALL、Q-WALL、墙体、外墙、0 甚至空字符串下。做过 pilot 的方案会依赖关键字匹配,但覆盖率在实际数据集上很快就会打折。

单位与量级不统一。 DXF 支持从毫米到光年的一整套长度单位,头部信息里可能缺失或错误。同时户型图纸有可能包含整栋楼平面,而实际关心的只是其中一户,直接三角剖分会撑爆内存。

3.2 下游看到的具体症状

  • 墙不闭合:区域轮廓在门口位置断开,无法用作分割
  • 一墙两线:直接把 DXF 的线段当成墙,会出现两条重叠的平行墙
  • 家具串扰:家具 Block 的线段被误判为内墙,把房间切成碎片
  • 尺寸单位错误:整个场景大小差 1000 倍,网格分辨率无从设定
  • 门位置缺失:门符号是 Block,位置只在插入点,没有开口本身的线段

因此需要一个把几何线段升维成”墙 / 窗 / 门 / 房间”语义的解析器,而不是简单的图层过滤脚本。

4. 解决方法

整个 pipeline 的思路是从外向内、从确定到不确定。先解外墙——它是最明显、最不容易搞错的结构;再靠外墙锚定墙厚——这是后续所有阈值的根据;然后按规则识别窗、入户门、内墙,最后处理开口性的门。每一步都缩小下一步的搜索空间。

4.1 前置解析与场景裁剪

DXF/DWG 由 libdxfrw / libredwg 解成原始几何。DWG 先在启动时转成 DXF 再走同一条链路,这样解析代码只需要面对一种格式。DXF 的 Block(可复用的图元组合,通常是家具、门、卫浴)会被单独拎出来,只保留它们的线段本身,不参与后续的三角剖分与区域分割——这些线段大多和主体墙无关,混进来只会加噪。

单位处理支持自动识别和外部覆盖。 DXF 头部会声明本文件的长度单位(英寸、毫米、米等),但工程里也见过声明错误的图纸,所以命令行保留一个”传 -1 表示自动识别,非 -1 表示强制指定”的开关。所有数据在进入几何阶段前统一换算到米。

场景裁剪基于线段的包围盒。 对所有主体线段求包围盒得到实际关心的场景范围,超过 1000 m² 直接拒绝——不是能不能算,而是从户型语义上讲已经不是”一户”了。落在包围盒之外的 Block(例如整栋楼的其他单元)会被删除。

门 Block 的过滤走一个小分类模型。 家具、门、卫浴都是 Block,只有门在下游有用。这里没有依赖 Block 的名称(同样不可靠),而是把每个 Block 光栅化成 64×64 的二值图,喂进一个专门训练的二分类模型(门 / 非门),把非门 Block 全部丢掉。这样后续找门的时候,候选集里都是真正长得像门的东西。

4.2 限制性三角网与外边界提取

限制性三角网

动机。 户型图最外层由若干条线段构成一个理论上封闭的多边形,但现实里可能有小的缺口(例如入户门位置断了 5 cm)。直接用图像形态学的轮廓提取方法容易在缺口处漏水;用图上的最短环拟合又对噪声敏感。限制性三角剖分(Constrained Delaunay Triangulation)是一个折中:所有输入线段作为限制边强制出现在剖分结果里,缺口自然由三角形补上,之后把”没有限制边支撑的三角形”从外向内逐个吃掉,最后剩下的是一个可靠的封闭区域。

做法。 先把所有主体线段作为限制边送入 CDT,得到覆盖整个场景包围盒的三角网。然后从边界(外接矩形边)出发做 BFS:一个三角形能被吃掉,当且仅当它与外部相连的边不是限制边。吃到某条边和某条限制边共线时,视同吃到限制边——这是对付”轮廓延长线”的容错处理,也是对付图纸未闭合的关键。剩下的三角形集合的外轮廓就是外墙的外边界多边形。

在这一步同时做了两件辅助工作:把三角网栅格化后的图像做一次区域生长分割,得到”每个三角形属于哪个空间区域”的初步归属;对相邻的狭长三角形做类别合并,避免细碎三角形把一个区域切成很多小块。

4.3 区域分割与关系图构建

区域分割和邻接图

动机。 走到这里,场景已经被切成若干个由限制边围出的多边形区域。墙、窗、房间、门在这个视角下都是”某种形状特点的区域”。如果能把区域之间的邻接关系表达成图,很多”是不是窗户”、“是不是内墙”的问题就变成了图上的属性查询。

做法。 每个区域记录以下属性:面积、周长、外轮廓多边形、简化后的多边形、区域内平行边关系图、内接矩形短边长度的最大值(用来判断”是不是薄条”)、中心线(由内部平行的两条轮廓边生成)。区域间构建邻接图:两个区域若共享至少一条内部三角形边,即为邻接,边上记录共享长度、边数、以及一个 5×5 的类别转移概率矩阵(用于早期做 MRF 松弛,最终没有采用)。

平行边关系单独建图。 一个区域内部的轮廓边如果两两平行、距离落在墙厚合理范围(3 cm–51 cm),就在这两条边之间连一条平行边,边权是它们的距离。这张图直接决定了后面”这个区域是不是一堵墙”、“墙厚是多少”等一系列判定。

4.4 外墙厚度、窗户与入户门

外墙厚度直方图

外墙厚度用直方图定主峰、峰内均值定最终值。 沿外边界多边形每条边所在的区域,把它的平行边距离都投票到 [0, 0.6 m] 的直方图里,桶宽 0.1 m。取样本数最多的桶作为墙厚的粗定位,再对落在该桶内的样本求均值,得到外墙厚度。**用中位数或直方图峰值区间而不是全局均值,是因为家具的双平行线、装饰性线段会带来大量偏离主分布的噪声值,均值会被它们拖偏。**内墙厚度用外墙的一半作为默认值,等实际找到内墙时再基于该内墙自己的平行边关系细化。

窗户的判定是一套邻接图上的规则。一个窗户在 DXF 里的画法是两到三条平行线并排,所以窗户在区域视角下必然对应两个或以上相邻区域,它们共享一条较长的邻接边、周长和中心线长度都接近、彼此的中心线平行、且都邻接同一个”更外面的”区域(也就是墙面)。具体的判定按以下顺序做条件筛选:

  1. 两个区域中心线总长都超过窗户最小宽度(默认 0.4 m)
  2. 两者周长差异比例小于 30%
  3. 两者中心线长度差异比例小于 30%,且中心线长度和它们的共享边长度差异比例小于 30%
  4. 存在至少一对近似平行的中心线
  5. 存在共同的邻居区域
  6. 在共同邻居里,与两个候选区域的邻接都是”短边、单条”(长度小于平均墙厚),即候选窗户是被同一堵墙”夹住”的

满足全部六个条件的区域对被合并为一个窗户 pattern,多个 pattern 相互连通再一次合并,最终一个 pattern 对应一扇窗。这套规则的好处是没有可调的模型参数——除了窗户的合理宽度和平行角度阈值,其余都是相对量。

入户门在外墙上做打分。 沿外墙中心线的每一段,若长度落在门的合理宽度(0.55–1.5 m)内且不属于窗户区域,就参与打分。打分函数看三个信号:所在区域是不是有”房间”特征(内接大三角形短边超过 0.8 m)、是不是一个纯粹的插入 Block 区域、以及它是不是四边形且只含一条中心线。三种情况分别对应 1.0、0.5、0.25 的分数,最高分即为入户门位置。入户门只找一个——户型图里通常也就一个。

4.5 内墙、门与户型 JSON

内墙延长与门识别

内墙识别用”薄区域 + 迭代扩展”策略。 满足以下条件的区域是内墙的候选:内接矩形短边长度落在 [minWallThickness, maxWallThickness] 之间(默认 0.08–0.51 m);至少有一条中心线;至少和一个已识别的墙面或窗户相邻。迭代的意义在于”墙面识别可以传播”——第一轮识别到的内墙会作为下一轮判定”是否与墙邻接”的种子,直到没有新的内墙冒出来。这样一个 T 形结构的内墙不会因为初始时它只连着房间而被漏掉。

内墙在迭代中还有几个反例过滤:所有邻居都是房间的孤立薄区域不算内墙(多半是家具的柜体轮廓);四边形且只连接两个墙面的矩形角落区域不算内墙(多半是墙角的三角带)。

内墙中心线做两次延长。 第一次延长处理的是内墙自身之间的连通——两条平行且共线的短内墙如果端点距离小于外墙厚度,就沿方向合并;然后所有内墙沿正反两个方向延长到外墙或其他内墙上,让整体成为一张封闭的图。第二次延长比第一次的距离长一些(等于门的最大宽度),专门用来暴露”因为有门而缺失的墙段”——那些原本应该有墙但被门开口打断的位置,通过延长会显式出现在结果里,这些延长段就是后面要判定为门的地方。

门的识别把延长段和门 Block 做匹配。 前面已经用分类模型选出了所有门 Block,每个门 Block 内部两条线段的几何关系(互相垂直 + 45° 对角)会被拟合出一条”门开口所在直线”和一条”门旋转朝向”。用这条门开口直线去和延长段做重合匹配:如果延长段和它平行、距离小于外墙厚度的 80%、长度接近门宽,就把这段延长认作门。旋转方向(左开 / 右开)也从 Block 的几何关系里推出来,供前端渲染门的开启动画。

户型 JSON 是最后一步。 内墙用来切断外墙,得到一段段的墙线;窗户和门都对齐到最近的墙线,并记录所属墙的引用。点、墙、门窗都用 UUID 标识,坐标以场景中心为原点做一次平移,Y 轴翻转以匹配前端坐标系。厚度单位从米换算成毫米,因为前端历史约定如此。

4.6 中间可视化:区域分类

区域分类可视化

在把最终 JSON 生成出来之前,管线内部会保留一张”区域分类”的可视化:蓝色是房间、黄色和紫色是墙面(分别对应外墙和内墙)、绿色是窗户、红色/黑色短线是入户门、白色是未定类。这张图在联调阶段用来判断问题出在哪一层——如果窗户漏了,是 pattern 判定错还是墙厚估计偏;如果房间被切碎,是区域分割偏还是内墙误增。管线里的每一步都有类似的中间产物落盘,只在 debug 模式打开。

4.7 端到端效果示例

下面 8 组是从测试集里挑出的 CAD 原图(左)和 cad2fp 生成的户型(右)配对。样本按结构从简单到复杂排列,涵盖矩形户型、L 形、多房间、多门、异形外轮廓等典型形态。

样例 1:紧凑矩形户型

样例 2:L 形户型 + 门 block 明显

样例 3:多分间 + 弧形阳台

样例 4:三室结构 + 多门开口

样例 5:多房间紧凑户型

样例 6:小户型 + 卫生间

样例 7:斜墙 / 非正交外轮廓

样例 8:长条形一室一厅

以典型的 60–120 m² 户型为基准,主观量化的口径如下:

指标 只用图层命名匹配 cad2fp
外墙轮廓完整率 视图纸规范 60%–90% ≥ 95%
双线墙误识别为两堵墙 常见 基本不出现(中心线合并处理)
家具 Block 干扰 若未过滤则严重 分类模型 + 场景裁剪后 ≤ 5% 残留
门定位可用率 依赖 Block 名 60%–85%(受门 Block 画法差异影响)
尺寸单位错误率 需要人工检查 自动识别 + 命令行强制覆盖兜底

从样例上可以看到几处一致的表现和局限:外墙轮廓的还原稳定,即使非正交、斜边、凸凹形状也能保持一致的墙线走向和厚度;内墙的位置和厚度大多与原图相符,个别短内墙会因为延长与外墙相交时角度过窄被切掉;门和入户门的位置定得住,但门开启方向偶尔会翻转(依赖 Block 拟合的对角线方向判定,图纸风格不同时会有偏差);斜墙户型的外墙识别没有问题,斜方向的窗户会被识别为普通线段而不是窗——这是当前的一个已知短板,规则里只覆盖了近乎水平/垂直的墙面情形。

5. 总结

从几何到语义的核心不是模型,而是层级化的规则串联。 门是最不确定的,如果先解门,任何噪声都会让后面的墙识别失败;反过来,先把外墙定死,再靠外墙定墙厚,靠墙厚定窗户的合理形状,靠窗户排除后再找入户门,每一步都在给下一步降噪。这条链路里几乎没有神经网络,只有一个分类门 Block 的二分类小模型——它在链路里的位置也是用来降噪(把家具从候选门里剔除),而不是直接输出门位置。

限制性三角网是一把万能钥匙。 对户型这种”由线段围出的封闭区域”,CDT 一次剖分同时解决了外边界回收、区域分割、区域邻接、平行边关系四件事,而后续所有语义识别都在这四张图上做规则查询。相比先做像素级栅格分割再回矢量的路线,几何精度和拓扑一致性都更好。

厚度用直方图峰而不是均值。 户型图纸里少数派离群值几乎总是存在——一根装饰线、一段家具轮廓、一处画错的双线。用直方图定主峰再在主峰内求均值,是一种代价极小的鲁棒统计,在工程实践里比中位数更容易解释,也比 M-estimator 更好写。

可能的后续方向:

  • 门识别当前依赖门 Block 的完整性,缺 Block 或者门画法非常规时会漏掉;一个可能的兜底是在内墙延长段上直接做形状分类
  • 房间语义(卧室 / 客厅 / 厨房 / 卫生间)当前完全不解,若下游需要,可以在语义识别层之后接一个基于面积、朝向、邻接门窗特征的分类器
  • 支持带电梯井、公摊区域的整层图纸,需要放宽”单户 ≤ 1000 m²”的硬限制,并对多户拆分做前置处理