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 usesuses[(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=17. 面连通分支
先通过内部共享边建立面对偶图,再做广度优先搜索:
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 components8. 组合成报告
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 证明网格一定是圆盘。
可靠的圆盘型检查至少还要结合:
- 单一面连通分支;
- 无非流形边与非流形顶点;
- 可定向;
- 恰好一个边界环;
- 。
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. 常见误区
- 不能根据顶点坐标距离推断拓扑邻接,必须使用面索引。
- 边界边数量不等于边界环数量。
- 面连通不自动保证顶点邻域是流形。
- 浮点坐标去重会引入容差问题,本课只处理整数索引。
12. 自测与答案
题 1。 示例网格为什么有五条边而不是六条?
答案。 两个三角形共享对角边,无向去重后只计一次。
题 2。 两个不相连三角形的 是多少?
答案。 每个三角形圆盘的,不交并相加得到;同时有两个连通分支。
题 3。 检查完拓扑后,参数化还必须检查什么?
答案。 几何退化、权重、边界条件、求解残差、三角形翻转及角度/面积畸变。
前后课程
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/ccg/01-11-mesh-topology-code/
更新于:2026年10月12日