在三维空间中,「识别」的本质从「图像分类」变成了「拓扑重建与形状推断」。 最大的难点在于:一张平面线框是三维物体在某个视角下的投影,它丢失了最关键的 「深度(Z 轴)」信息,且存在大量非真实相交的「视觉伪交点」。

本文档按「点 → 线 → 面 → 体」从简到繁拆解三维空间中的识别与构建, 并配以可运行代码与演示图。以下为完整技术进化链:

- 点:由二维像素坐标 + 深度预测/三角测量得出。 - 线:通过消失点透视投影或空间坐标连线得出。 - 面:在图论中搜索最小闭合环,并进行共面性聚类得出。 - 体:将所有法向朝外的封闭面组合为 B-rep 实体,或直接用深度学习生成隐式体积。


第一步:点(Point)——从「像素坐标」到「空间坐标」

2D 的点坐标是 (x, y),3D 的点坐标是 (x, y, z)。获取 z 值有两种路径:

  • 多视图三角测量(传统几何法):如果有多张不同角度拍摄的线框图纸(或多视图影像),算法使用对极几何(Epipolar Geometry),在同名点之间做匹配,利用相机视差,通过三角化计算空间交汇点,从而恢复出精确的三维坐标。
  • 单目深度估计(深度学习方法):如果只有一张图,则使用神经网络(如基于 Transformer 的 Depth Anything 模型)直接预测每个像素的深度值,将 z 值附给二维交点。

经典示例七:多视图三角测量恢复三维坐标

模拟两台相机从不同角度观察一个立方体角点云:用已知的相机内参 K 与位姿 (R, t) 构造投影矩阵 P = K[R|t],把真实三维点投影到两个视图(加入少量像素噪声), 再用 DLT 线性三角化从同名点重建三维坐标,并与真实值对比误差。

Python
import numpy as np

# ---- 真实三维点: 立方体 8 个顶点 ----
half = 1.0
true_pts = np.array([[x, y, z]
                     for x in (-half, half)
                     for y in (-half, half)
                     for z in (-half, half)])

# ---- 两台相机: P = K[R|t] ----
K = np.array([[800, 0, 400], [0, 800, 300], [0, 0, 1]])

def make_camera(R, t):
    return K @ np.hstack([R, t.reshape(-1, 1)])

R1, t1 = np.eye(3), np.array([0., 0., 0.])
P1 = make_camera(R1, t1)                     # 相机1: 原点
ang = np.radians(20)
R2 = np.array([[np.cos(ang), 0, np.sin(ang)],
               [0, 1, 0],
               [-np.sin(ang), 0, np.cos(ang)]])
P2 = make_camera(R2, np.array([0.3, 0., 0.]))  # 相机2: 偏转+平移

def project(P, pts):
    ph = (P @ np.hstack([pts, np.ones((len(pts), 1))]).T).T
    return ph[:, :2] / ph[:, 2:3]

u1, u2 = project(P1, true_pts), project(P2, true_pts)

# ---- DLT 线性三角化: 对每个点解 A X = 0 ----
def triangulate(P1, P2, u1, u2):
    rec = []
    for (x1, y1), (x2, y2) in zip(u1, u2):
        A = np.array([
            x1 * P1[2] - P1[0], y1 * P1[2] - P1[1],
            x2 * P2[2] - P2[0], y2 * P2[2] - P2[1],
        ])
        _, _, Vt = np.linalg.svd(A)
        X = Vt[-1]
        rec.append(X[:3] / X[3])
    return np.array(rec)

rec_pts = triangulate(P1, P2, u1, u2)
err = np.linalg.norm(rec_pts - true_pts, axis=1)
print(f"三维重建误差: 平均 {err.mean():.4f}, 最大 {err.max():.4f}")

演示效果图(由 code/demo7_triangulation.py 生成)

多视图三角测量恢复三维坐标
多视图三角测量恢复三维坐标

左图为两个视图下的同名点及其视差连线(同一空间点在不同视图中的像素位置不同, 正是这视差携带了深度信息);右图为三角测量重建的三维点(红星)与真实三维点 (空心圆)的对比,两者几乎重合(实测平均误差约 0.002)。


第二步:线(Line)——从「2D 线段」到「3D 空间边」

2D 的线只是像素连线,3D 的线必须明确其空间方向矢量和空间位置。

  • 破除非真实交点:这是平面线框升维最棘手的问题。在 2D 图中两条线交叉,在 3D 中它们可能一前一后,并不相交。算法会利用「拓扑一致性」校验:如果两条 3D 线在空间中相距超过阈值,则判定为「视觉交叉」,强制断开它们的拓扑连接。
  • 基于消失点(Vanishing Point)的重建:对于人造物体(如建筑、机械零件),算法会检测线框中的三个主消失点,计算出相机的内参和旋转矩阵,然后将 2D 线段反向投影到三维空间中,恢复出完全正交的 3D 边。

经典示例九:破除非真实交点与虚拟延伸补全

场景:两条 3D 线段在 2D 正投影中交叉(看起来相交),但实际一前一后。 用「拓扑一致性」校验(两条空间直线的最近距离是否小于阈值)判定并断开。 同时演示「共线合并与虚拟延伸」:被遮挡断开的同向线段,按其空间方向延长并补全。

Python
import numpy as np

def closest_distance_between_lines(p1, d1, p2, d2):
    """两条空间直线(点+方向)之间的最近距离。"""
    w0 = p1 - p2
    a, b, c, d, e = d1@d1, d1@d2, d2@d2, d1@w0, d2@w0
    denom = a * c - b * b
    if abs(denom) < 1e-9:
        return np.linalg.norm(w0 - (e / c) * d2)
    s = (b * e - c * d) / denom
    t = (a * e - b * d) / denom
    return np.linalg.norm((p1 + s * d1) - (p2 + t * d2))

# 前墙水平线 z=0 与后墙垂直线 z=2: 2D 投影交叉, 3D 相距 2.0
pA, dA = np.array([-1.2, 0.3, 0.0]), np.array([2.4, 0.0, 0.0])
pB, dB = np.array([0.3, -1.2, 2.0]), np.array([0.0, 2.4, 0.0])
dist = closest_distance_between_lines(pA, dA, pB, dB)
is_real = dist < 0.5
print(f"最近距离 {dist:.3f} -> "
      f"{'真实相交' if is_real else '视觉伪交点, 强制断开拓扑连接'}")

演示效果图(由 code/demo9_pseudo_intersection.py 生成)

破除非真实交点
破除非真实交点

左图为 2D 正投影:前墙红色水平线与后墙蓝色垂直线在图中明显交叉(黑点处), 看起来是一个交点;右图为 3D 视图:两条线一前一后,空间最近距离 2.0 远超阈值, 被判定为「视觉伪交点」并断开拓扑连接。


第三步:面(Face)——从「空间骨架」到「闭合平面区域」

这是「点线面体」中算法含金量最高的一步。拥有 3D 线框(骨架)后,需要找出哪些边 围成了一个「面」。这完全是一个图论(Graph Theory)问题,而非图像问题。

  1. 图构建:将所有的 3D 线段的端点和交点视为图的顶点(Vertex),线段视为图的边(Edge)。
  2. 寻找最小环(Minimum Cycle Basis):算法在图中搜索最短闭合回路。例如,一个长方体拥有 12 条边,通过最小环算法能找到 6 个最小的四边形环,这 6 个环即为长方体的 6 个表面。
  3. 平面拟合校验:找到候选环后,取环上所有顶点的三维坐标,用最小二乘法拟合一个空间平面。如果所有顶点到该平面的距离方差极小,则确认这是一个有效的平面(Plane Face)。若拟合残差过大,则判定为曲面(B-Spline/NURBS 曲面)。
  4. 处理复杂共享边:识别出相邻面共享的边(如正方体相邻面的公共棱),确保拓扑结构「漏水」检测(Watertight)。

经典示例八:长方体最小环基识别 6 个表面

用图论方法处理一个长方体的 3D 线框(8 顶点、12 条边): 枚举所有简单四边形环得到 6 个表面,再对每个环做最小二乘空间平面拟合, 用拟合残差校验共面性(残差为 0 即确认平面)。

Python
import numpy as np
import networkx as nx

# ---- 长方体 8 顶点(尺寸 2x1.5x1) + 12 条棱 ----
w, h, d = 2.0, 1.5, 1.0
verts3d = np.array([
    [0, 0, 0], [w, 0, 0], [w, h, 0], [0, h, 0],     # 底面
    [0, 0, d], [w, 0, d], [w, h, d], [0, h, d],     # 顶面
])
edges = [(0,1),(1,2),(2,3),(3,0), (4,5),(5,6),(6,7),(7,4),
         (0,4),(1,5),(2,6),(3,7)]

G = nx.Graph(); G.add_edges_from(edges)

# ---- 枚举简单四边形环: 得到 6 个表面 ----
DG = nx.DiGraph()
for u, v in edges:
    DG.add_edge(u, v); DG.add_edge(v, u)
quad_cycles = {frozenset(c) for c in nx.simple_cycles(DG) if len(c) == 4}
cycles = [list(c) for c in quad_cycles]

# ---- 平面拟合校验共面性 ----
def plane_fit(pts):
    centroid = pts.mean(axis=0)
    _, s, Vt = np.linalg.svd(pts - centroid)
    return np.linalg.norm((pts - centroid) @ Vt[-1])   # 残差

for cyc in cycles:
    res = plane_fit(verts3d[list(cyc)])
    print(f"环 {len(cyc)} 顶点, 平面拟合残差 {res:.5f} -> "
          f"{'平面' if res < 1e-6 else '曲面'}")

说明:minimum_cycle_basis 返回的是环空间的一组基(维数为 E−V+1=5), 而长方体的 6 个表面是平面嵌入的「面」;因此示例中改用「枚举简单四边形环」, 恰好得到全部 6 个表面,再配合平面拟合校验确认。

演示效果图(由 code/demo8_box_faces.py 生成)

长方体表面识别
长方体表面识别

左图为长方体 3D 线框(8 顶点 / 12 条边);右图为识别出的 6 个表面, 每个环均通过平面拟合校验(残差为 0),用不同颜色区分。


第四步:体(Solid)——从「封闭曲面」到「物理实体」

当所有的「面」组成了一个封闭且无缝隙的水密(Watertight)外壳时,「体」就诞生了。 识别「体」意味着系统确认这是一个具有体积属性的三维实体。

  • 实体建模(B-rep / CSG):在工业软件(如 SolidWorks、AutoCAD)内核中,体是通过边界表示法(Boundary Representation, B-rep)来存储的,即「体由面构成,面由边构成,边由点构成」。算法会检查每个面法向是否朝外,确保所有面围成的封闭空间指向一致(即没有法向翻转)。
  • 构造实体几何(CSG)识别:对于复杂体(如带孔的底座),传统方法会将封闭体分解,识别出这是「一个长方体减去一个圆柱体」的布尔运算结果。

现代深度学习端到端方案:除了上述「传统几何推理」流水线,近年来业界主流(如微软、Autodesk 的研究)更多采用端到端的深度网络,直接从一张 2D 线框图生成 3D 体素(Voxel)或网格(Mesh):

  • 基于 Transformer 的生成:将 2D 线框图作为条件输入,利用类似 TripoSR 或 LGM(Large Multi-View Gaussian Model)的扩散模型,直接回归出三维空间的点云和三角面片(Mesh)。
  • 隐式神经表示(NeRF / SDF):网络不直接输出面,而是输出一个符号距离函数(SDF),即空间中任意一点到物体表面的有符号距离。提取 SDF 的零等值面(Zero-level Set),就能获得完美的、水密的三维体(Solid)。

说明:B-rep 的「法向朝外校验」与「CSG 布尔分解」依赖工业 CAD 内核,超出本文档 可运行示例范围;深度学习端到端方案需要预训练权重与 GPU 环境。这两部分以理论为主。


三大核心算法挑战与解法

挑战点平面 2D 线框三维空间扩展(核心解法)
歧义性无歧义(像素即数据)多假设推理:利用马尔可夫随机场(MRF)或图神经网络,在多个候选 3D 结构中选出满足「曼哈顿世界假设」(即直角结构)最优解。
遮挡(Occlusion)线断开共线合并与虚拟延伸:算法将被遮挡断开的线段按其空间方向延长,在后端空间中寻找交汇点,补全残缺的边。
线与面的从属关系不涉及拓扑分类器:判断一条 3D 边是位于平面上(凸棱/凹棱),还是穿过平面(相交线),还是仅作为辅助线(标注线),并据此过滤掉非实体线。

总结

层次从 2D 到 3D 的核心方法
点二维像素坐标 + 深度预测/三角测量
线消失点透视投影 或 空间坐标连线 + 拓扑一致性校验破除伪交点
面图论中搜索最小闭合环 + 共面性聚类/平面拟合校验
体所有法向朝外的封闭面组合为 B-rep 实体,或深度学习生成隐式体积

如果从事逆向工程(如扫描点云转 CAD),传统几何图论(B-rep 搜索)仍是精度保障; 如果从事 AI 生成 3D 内容(AIGC),直接上扩散模型(如 Stable-Dreamfusion)会更高效。 两者应用场景不同,可按需选择。


附录:运行演示脚本

本文档的可运行脚本位于 code/ 目录,全部已实测通过(依赖:numpy, matplotlib, opencv-python, networkx)。

Bash
# 进入 code 目录后运行
python demo7_triangulation.py        # 第一步: 多视图三角测量(点)
python demo9_pseudo_intersection.py  # 第二步: 破除非真实交点(线)
python demo8_box_faces.py            # 第三步: 长方体表面识别(面)
脚本对应章节输出图
code/demo7_triangulation.py第一步 示例七figures/fig_3d_triangulation.png
code/demo9_pseudo_intersection.py第二步 示例九figures/fig_3d_pseudo_intersection.png
code/demo8_box_faces.py第三步 示例八figures/fig_3d_box_faces.png
code/figure_style.py—中文字体与绘图公共配置(被各脚本引用)

与前两文档衔接:本文档的「面」识别(图论最小环)直接复用 线框图识别-经典示例与代码.md 中流派一的方法;「点」的三角测量则把 图形识别-经典示例与代码.md 的二维交点提升到了三维。

本文档的「点·线·面·体」原理是《三维模型处理全景》的几何底座: 识别分类、分割、快速建模、自动化建模与模型分析五大任务的工程全景见 三维模型处理全景.md。