CCG 01-11:网格拓扑体检——把定义逐行翻译成 Python

本课只解决一个问题

给定三角形面数组 faces,怎样检查边界、非流形边、连通性和 Euler 示性数?

代码只实现已经学过的定义。若某一行看不懂,应回到对应理论短课,而不是背代码。

1. 输入约定

import numpy as np
from collections import defaultdict, deque

# 两个三角形拼成一个方形圆盘
faces = np.array([
    [0, 1, 2],
    [0, 2, 3],
], dtype=int)

每行三个整数表示一个有向三角形。这里只需组合结构,不需要顶点坐标。

2. 枚举有向局部边

def directed_face_edges(face):
    i, j, k = map(int, face)
    return [(i, j), (j, k), (k, i)]

def undirected(edge):
    i, j = edge
    return (i, j) if i < j else (j, i)

局部方向用于检查面定向;排序后的无向边用于判断两个面是否共享同一条边。

3. 建立边到面的关联

def edge_incidence(faces):
    uses = defaultdict(list)
    for face_id, face in enumerate(faces):
        for edge in directed_face_edges(face):
            uses[undirected(edge)].append((face_id, edge))
    return uses

uses[(i, j)] 记录无向边属于哪些面,以及它在每个面中的局部方向。

4. 分类边

def classify_edges(faces):
    uses = edge_incidence(faces)
    boundary = []
    interior = []
    nonmanifold = []

    for edge, records in uses.items():
        count = len(records)
        if count == 1:
            boundary.append(edge)
        elif count == 2:
            interior.append(edge)
        else:
            nonmanifold.append((edge, count))

    return boundary, interior, nonmanifold

判据直接来自 边界与定向:一个面为边界,两个面为正常内部边,更多为非流形边。

5. 检查相邻面方向

def orientation_conflicts(faces):
    conflicts = []
    for edge, records in edge_incidence(faces).items():
        if len(records) != 2:
            continue
        (_, e0), (_, e1) = records
        if e0 == e1:
            conflicts.append(edge)
    return conflicts

一致定向的两个相邻面应沿共享边反向。如果两个局部有向边完全相同,就发现方向冲突。

6. Euler 示性数

def euler_characteristic(faces):
    vertices = set(map(int, faces.ravel()))
    edges = set()
    for face in faces:
        edges.update(undirected(e) for e in directed_face_edges(face))
    return len(vertices), len(edges), len(faces), len(vertices) - len(edges) + len(faces)

对示例应得到

V=4, E=5, F=2, chi=1

7. 面连通分支

先通过内部共享边建立面对偶图,再做广度优先搜索:

def face_components(faces):
    adjacency = [set() for _ in range(len(faces))]
    for records in edge_incidence(faces).values():
        incident_faces = [face_id for face_id, _ in records]
        for i in incident_faces:
            for j in incident_faces:
                if i != j:
                    adjacency[i].add(j)

    seen = set()
    components = []
    for start in range(len(faces)):
        if start in seen:
            continue
        queue = deque([start])
        seen.add(start)
        component = []
        while queue:
            i = queue.popleft()
            component.append(i)
            for j in adjacency[i]:
                if j not in seen:
                    seen.add(j)
                    queue.append(j)
        components.append(component)
    return components

8. 组合成报告

def topology_report(faces):
    boundary, interior, nonmanifold = classify_edges(faces)
    V, E, F, chi = euler_characteristic(faces)
    components = face_components(faces)
    conflicts = orientation_conflicts(faces)
    return {
        "V": V,
        "E": E,
        "F": F,
        "chi": chi,
        "boundary_edges": len(boundary),
        "nonmanifold_edges": nonmanifold,
        "face_components": len(components),
        "orientation_conflicts": conflicts,
    }

print(topology_report(faces))

预期关键输出是:

chi = 1
boundary_edges = 4
nonmanifold_edges = []
face_components = 1
orientation_conflicts = []

9. 报告不能替你证明什么

这份代码没有完整检查顶点一环是否为圆盘/半圆盘,也没有计算同调群。它能发现许多常见数据错误,却不能仅凭 chi == 1 证明网格一定是圆盘。

可靠的圆盘型检查至少还要结合:

  1. 单一面连通分支;
  2. 无非流形边与非流形顶点;
  3. 可定向;
  4. 恰好一个边界环;
  5. χ=1\chi=1。

10. 故障实验

依次修改输入并预测结果:

# 方向冲突:第二个面也沿 0 -> 2 使用共享边
bad_orientation = np.array([[0, 1, 2], [0, 3, 2]])

# 非流形边:三张面共享边 (0, 1)
bad_manifold = np.array([[0, 1, 2], [1, 0, 3], [0, 1, 4]])

# 两个不相连三角形
disconnected = np.array([[0, 1, 2], [3, 4, 5]])

先写下预测,再运行;这种“先预测后验证”比直接看输出更能建立拓扑直觉。

11. 常见误区

  1. 不能根据顶点坐标距离推断拓扑邻接,必须使用面索引。
  2. 边界边数量不等于边界环数量。
  3. 面连通不自动保证顶点邻域是流形。
  4. 浮点坐标去重会引入容差问题,本课只处理整数索引。

12. 自测与答案

题 1。 示例网格为什么有五条边而不是六条?

答案。 两个三角形共享对角边(0,2)(0,2),无向去重后只计一次。

题 2。 两个不相连三角形的χ\chi 是多少?

答案。 每个三角形圆盘的χ=1\chi=1,不交并相加得到22;同时有两个连通分支。

题 3。 检查完拓扑后,参数化还必须检查什么?

答案。 几何退化、权重、边界条件、求解残差、三角形翻转及角度/面积畸变。

前后课程

上一课:同调与 Betti 数 | 下一模块参考:微分拓扑与 Hodge 分解