
1. 项目概述当CAD模型需要“分家”时在CAD计算机辅助设计领域我们经常面对一个看似简单实则棘手的问题拿到一个复杂的装配体模型文件比如一台完整的发动机、一个建筑结构或者一个电子产品的外壳我们如何能自动、准确地把其中一个个独立的零件识别并分割出来这个问题就是几何实例分割。传统方法要么依赖模型构建时的历史树Feature Tree要么依赖简单的几何属性如颜色、图层一旦模型来自外部历史树丢失或者属性信息混乱分割就变得异常困难。“STEP-Parts”这个项目直指这个痛点。它提出了一种基于B-Rep边界表示法的CAD几何实例分割方法。简单来说B-Rep是描述三维实体最主流的方式它用面Face、边Edge、顶点Vertex的拓扑关系来定义一个实体。STEPStandard for the Exchange of Product model data则是国际通用的、中性的产品数据交换标准一个STEP文件里通常就包含了用B-Rep描述的完整几何信息。这个项目的核心思想就是绕开对建模历史和人为属性的依赖直接从最底层的、最可靠的B-Rep几何与拓扑数据中挖掘出哪些面、边、体属于同一个物理零件。这听起来有点抽象我举个生活中的例子。想象你收到一个乐高积木拼好的城堡但所有积木块都粘在了一起没有说明书。你想知道它用了多少种不同的积木块每种有多少个。STEP-Parts要做的就是通过分析每一块积木的凸起、凹槽、接触面的形状和连接方式自动判断出哪些部分原本是同一个标准的积木零件即便它们现在紧密地连接在一起。这对于逆向工程、模型轻量化、仿真前处理、零件库管理等领域价值巨大。特别是当你从供应商、开源社区或老旧资料库中拿到一个“哑模型”Dumb Model即无特征历史时这种方法几乎是实现自动化处理的唯一途径。2. 核心原理从B-Rep拓扑中嗅探“零件”的边界要理解STEP-Parts如何工作我们必须先深入B-Rep的本质。一个B-Rep实体由“壳”Shell构成一个封闭的壳代表一个实体Solid。而壳由“面”Face围成面之间通过“边”Edge连接边由“环”Loop限定最终指向“顶点”Vertex。拓扑关系定义了这些元素如何连接几何信息则定义了面是平面、圆柱面还是更复杂的NURBS曲面边是直线还是曲线。2.1 分割的底层逻辑连接性与独立性几何实例分割的核心判据是物理连接的连续性和逻辑组装的独立性。在B-Rep层面这通常转化为对“壳”Shell的识别。理想情况下一个独立的零件对应一个封闭的壳。但现实很骨感布尔运算后的粘连两个零件通过布尔并集Union运算合并后在它们的接触区域原本属于两个独立壳的面会合并成一个共享面。从拓扑上看它们变成了一个壳。间隙与干涉实际装配中零件间可能存在微小间隙Gap或过盈配合Interference这模糊了“连接”的定义。复合曲面一个复杂的曲面可能被分割成多个B-Rep面但这些面属于同一个零件的同一个连续表面。因此STEP-Parts的方法不能简单地认为“一个壳就是一个零件”。它需要更智能的算法我将其核心思路拆解为以下几个层次第一层基于拓扑的初步分割这是最直接的一步。算法首先会遍历模型中的所有壳Shell。对于每个壳检查其封闭性。一个封闭的、不与任何其他壳共享面的壳大概率是一个独立零件。这一步可以快速筛选出模型中“孤立”的部件。第二层穿透共享面的分割——关键所在对于通过布尔运算合并的模型挑战在于如何“切开”那个共享面还原出原始的零件边界。这里常用的策略包括几何连续性分析虽然拓扑上是一个面但在这个共享面的内部可能存在几何上的不连续点或线例如曲率突变、法向突变。这些不连续线往往是原始零件拼合的边界。算法可以通过分析面的参数域、计算曲率或法向量的变化来探测这些边界。凸凹性分割观察共享面所连接的两个体。如果从共享面处看两个体分别位于面的两侧并且连接处形成“凸角”就像两个方块并排贴在一起这通常是装配关系。如果形成“凹角”像一个方块嵌入另一个的凹槽则更可能是一个单一零件上的特征。算法可以通过计算连接边两侧面的二面角等几何属性进行判断。第三层基于几何与语义的聚类在分割出候选的零件区域可能是一组面后需要判断这些区域是否真的属于同一个逻辑零件。这里会用到聚类算法考虑的特征包括面片法向一致性同一个零件上相邻的面其法向量变化通常是平滑的。曲率相似性属于同一张复杂曲面如汽车车身曲面的不同B-Rep面片其主曲率、高斯曲率等属性会相近。对称性许多机械零件具有对称性。算法可以检测对称面并将对称部分归类为同一类零件的不同实例。尺寸与比例螺栓、螺母、标准件等具有特定的尺寸和比例特征。注意没有任何一种单一特征是万能的。一个鲁棒的STEP-Parts系统通常会采用多特征融合的策略并引入机器学习尤其是无监督学习或基于少量标注数据的微调来提升分割的准确率。例如可以将每个“面”或“面簇”表示为一个特征向量包含几何、拓扑属性然后使用聚类算法如DBSCAN、谱聚类进行分组。2.2 STEP文件解析一切的基础所有上述操作的前提是正确无误地从STEP文件通常是AP203或AP214协议中解析出B-Rep模型。这个过程本身就是一个技术点。你需要一个可靠的STEP解析器如OpenCASCADE的STEPControl_Reader或者一些商业库。解析的关键在于实体映射将STEP文件中的高级实体如ADVANCED_FACE EDGE_LOOP正确映射到内存中的B-Rep数据结构。几何精度处理STEP文件中的几何数据如曲线、曲面方程是精确表示的但计算机处理时存在浮点数精度问题。这可能导致本应连接的两个顶点在数值上略有偏差破坏拓扑一致性。必须在解析后引入“缝合”Sewing或“容差合并”操作。单位与坐标系确保从STEP文件中读取的几何数据单位与后续处理系统单位一致并正确处理模型坐标系。3. 实现流程与关键技术点拆解理解了原理我们来看如何一步步实现一个简易但核心功能完整的“STEP-Parts”分割工具。我将以Python为语言结合开源几何内核OpenCASCADEOCC的Python绑定如pythonOCC或occt来演示核心流程。选择OCC是因为它在处理B-Rep和STEP方面非常成熟且开源。3.1 环境搭建与依赖准备首先你需要一个能运行OCC的环境。对于Python最直接的方式是安装pythonocc-core。但由于其安装可能因系统而异一个更稳定的方案是使用Conda。# 创建一个新的conda环境 conda create -n step_parts python3.9 conda activate step_parts # 安装pythonocc-core。请注意版本需要匹配。 # 以下命令可能因平台而异建议查阅pythonocc-core官方文档。 conda install -c conda-forge pythonocc-core7.7.0 # 安装其他辅助库 pip install numpy scikit-learn # 用于特征计算和聚类 pip install trimesh # 可选用于网格可视化或辅助计算如果pythonocc-core安装困难可以考虑使用Docker镜像或者直接使用OCC的C库通过PyBind11自行制作Python绑定但这需要更多的开发工作量。3.2 STEP文件读取与B-Rep模型提取这是所有操作的起点。我们需要将STEP文件读入内存并转换为OCC的TopoDS_Shape对象。from OCC.Core.STEPControl import STEPControl_Reader from OCC.Core.IFSelect import IFSelect_RetDone, IFSelect_ItemsByEntity from OCC.Core.TopoDS import TopoDS_Shape from OCC.Core.BRep import BRep_Builder from OCC.Core.BRepTools import breptools_Read import os def read_step_file(step_file_path): 读取STEP文件返回TopoDS_Shape对象。 参数: step_file_path: STEP文件的完整路径。 返回: TopoDS_Shape: 加载的模型形状。 bool: 是否成功。 str: 错误信息如果失败。 if not os.path.exists(step_file_path): return None, False, f文件不存在: {step_file_path} reader STEPControl_Reader() status reader.ReadFile(step_file_path) if status ! IFSelect_RetDone: return None, False, 读取STEP文件失败。 # 转移所有根实体 reader.TransferRoots() # 获取转换后的形状 shape reader.OneShape() if shape.IsNull(): return None, False, 转换后的形状为空。 # 可选进行几何修复合并容差内的顶点和边 # from OCC.Core.ShapeFix import ShapeFix_Shape # fixer ShapeFix_Shape(shape) # fixer.Perform() # shape fixer.Shape() return shape, True, 读取成功。 # 使用示例 step_path assembly.step model_shape, success, msg read_step_file(step_path) if not success: print(f错误: {msg}) exit() print(STEP模型加载成功。)3.3 拓扑遍历与面Face特征提取拿到TopoDS_Shape后我们需要遍历其中所有的面Face并为每个面计算一组特征用于后续的聚类分割。from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_SOLID, TopAbs_SHELL from OCC.Core.TopoDS import topods_Face, topods_Solid, topods_Shell from OCC.Core.BRep import BRep_Tool from OCC.Core.Geom import Geom_Surface from OCC.Core.GeomLProp import GeomLProp_SLProps from OCC.Core.gp import gp_Pnt, gp_Vec import numpy as np def extract_face_features(shape): 遍历模型中的所有面提取几何和拓扑特征。 参数: shape: TopoDS_Shape对象。 返回: list: 每个面的特征字典列表。 list: 对应的面对象列表。 face_features [] face_list [] # 使用探索器遍历所有面 explorer TopExp_Explorer(shape, TopAbs_FACE) while explorer.More(): face topods_Face(explorer.Current()) face_list.append(face) features {} # 1. 获取面的几何曲面 surface BRep_Tool.Surface(face) # 2. 计算面中心点近似和法向量 # 这里简化处理取面的第一个参数点中心 umin, umax, vmin, vmax surface.Bounds() u_mid (umin umax) / 2.0 v_mid (vmin vmax) / 2.0 pnt surface.Value(u_mid, v_mid) # 计算该点的法向量 props GeomLProp_SLProps(surface, u_mid, v_mid, 1, 1e-6) # 1表示计算一阶导数法向 if props.IsNormalDefined(): normal props.Normal() features[normal] (normal.X(), normal.Y(), normal.Z()) else: features[normal] (0.0, 0.0, 1.0) # 默认值 features[center] (pnt.X(), pnt.Y(), pnt.Z()) # 3. 计算面的面积近似特征用于区分大小面 from OCC.Core.GProp import GProp_GProps from OCC.Core.BRepGProp import brepgprop_SurfaceProperties props_area GProp_GProps() brepgprop_SurfaceProperties(face, props_area) features[area] props_area.Mass() # 4. 曲面类型平面、圆柱面、球面等- 简化分类 surf_type surface.DynamicType().Name() features[type] surf_type # 5. 面的凹凸性通过分析其边界的二面角均值- 这是一个高级特征 # 此处省略详细实现可通过遍历面的所有边计算相邻面的法向夹角来估算 features[convexity] estimate_face_convexity(face) # 假设已实现 face_features.append(features) explorer.Next() return face_features, face_list def estimate_face_convexity(face): 粗略估计面的凹凸性。 返回一个标量值正值表示更凸负值表示更凹。 这是一个简化实现实际应用需要更严谨的计算。 # 此处为示例返回一个随机值。实际应计算与相邻面的二面角。 return np.random.randn() * 0.13.4 基于特征的面的聚类分割有了每个面的特征向量我们就可以使用聚类算法将面分组。每组面理论上对应一个零件或一个零件的连续部分。from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler def cluster_faces_by_features(face_features): 将面特征聚类以识别潜在的零件。 参数: face_features: extract_face_features返回的特征字典列表。 返回: labels: 每个面对应的聚类标签。 -1表示噪声点。 # 1. 构建特征矩阵 # 选择用于聚类的特征。这里是一个示例组合。 feature_vectors [] for feat in face_features: vec [] # 中心坐标归一化到模型包围盒内 vec.extend(feat[center]) # 法向量 vec.extend(feat[normal]) # 面积取对数以平滑量级差异 vec.append(np.log1p(feat[area])) # 凹凸性 vec.append(feat[convexity]) # 可以加入更多特征如曲率... feature_vectors.append(vec) X np.array(feature_vectors) # 2. 标准化特征重要 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 3. 应用聚类算法。DBSCAN能自动发现簇数量并处理噪声。 # eps和min_samples是关键参数需要根据模型规模和特征调整。 clustering DBSCAN(eps1.5, min_samples5, metriceuclidean).fit(X_scaled) labels clustering.labels_ print(f聚类完成。发现 {len(set(labels)) - (1 if -1 in labels else 0)} 个簇噪声点数量{list(labels).count(-1)}) return labels3.5 后处理与零件重组聚类得到的标签可能将同一个零件的不同部分如一个立方体的六个面分到不同的簇因为它们的法向量差异很大。因此我们需要一个后处理步骤基于拓扑连接性将属于同一零件但被误分的面合并。from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_VERTEX from OCC.Core.TopoDS import topods_Edge, topods_Vertex from OCC.Core.BRep import BRep_Tool from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape from OCC.Core.TopExp import TopExp_MapShapesAndAncestors def merge_clusters_by_connectivity(face_list, initial_labels): 根据面的拓扑连接性共享边合并初始聚类。 参数: face_list: 面对象列表。 initial_labels: 初始聚类标签数组。 返回: final_labels: 合并后的最终标签。 num_faces len(face_list) final_labels initial_labels.copy() # 构建面-边邻接关系哪些面共享同一条边 # 这里简化处理如果两个面共享一条边且它们的初始标签不同但都不是噪声点则考虑合并。 # 实际算法更复杂可能需要构建邻接图并使用并查集(Union-Find)。 # 使用OCC工具快速获取共享边的面 shape face_list[0].Father() # 假设所有面来自同一个顶层Shape edge_face_map TopTools_IndexedDataMapOfShapeListOfShape() TopExp_MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_face_map) # 遍历所有边 for i in range(1, edge_face_map.Extent() 1): edge edge_face_map.FindKey(i) face_list_for_edge edge_face_map.FindFromIndex(i) if face_list_for_edge.Size() 2: # 只处理连接两个面的边 face1 topods_Face(face_list_for_edge.First()) face2 topods_Face(face_list_for_edge.Last()) # 找到这两个面在face_list中的索引 try: idx1 face_list.index(face1) idx2 face_list.index(face2) except ValueError: continue label1 final_labels[idx1] label2 final_labels[idx2] # 如果两个面标签不同且都不是噪声则合并将其中一个簇的所有成员标签改为另一个 if label1 ! label2 and label1 ! -1 and label2 ! -1: # 简单策略将标签值较大的簇合并到标签值较小的簇 target_label min(label1, label2) source_label max(label1, label2) final_labels[final_labels source_label] target_label # 注意合并后需要重新映射标签为连续的整数此处省略。 # 重新映射标签为0,1,2,...连续的整数 unique_labels np.unique(final_labels[final_labels ! -1]) label_mapping {old: new for new, old in enumerate(unique_labels)} label_mapping[-1] -1 final_labels np.vectorize(label_mapping.get)(final_labels) return final_labels3.6 结果输出与可视化最后我们需要将分割结果输出例如为每个零件生成独立的STEP文件或可视化。from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Iterator from OCC.Core.BRep import BRep_Builder from OCC.Core.STEPControl import STEPControl_Writer from OCC.Core.Interface import Interface_Static from OCC.Display.SimpleGui import init_display import matplotlib.cm as cm def export_and_visualize(face_list, final_labels, output_diroutput_parts): 根据最终标签将不同簇的面分别组合成Compound并导出为STEP文件或可视化。 import os os.makedirs(output_dir, exist_okTrue) unique_labels set(final_labels) # 为每个标签除了噪声-1创建一个Compound compounds {} builder BRep_Builder() for label in unique_labels: if label -1: continue comp TopoDS_Compound() builder.MakeCompound(comp) compounds[label] comp # 将面添加到对应的Compound中 for idx, (face, label) in enumerate(zip(face_list, final_labels)): if label ! -1: builder.Add(compounds[label], face) # 导出每个Compound为单独的STEP文件 writer STEPControl_Writer() Interface_Static.SetCVal(write.step.schema, AP203) # 设置STEP协议 for label, comp in compounds.items(): if comp.IsNull(): continue writer.Transfer(comp, STEPControl_AsIs) step_filename os.path.join(output_dir, fpart_{label:03d}.step) status writer.Write(step_filename) if status: print(f零件 {label} 已导出至: {step_filename}) writer.Clear() # 清除准备下一个 # 可视化使用pythonocc-core的显示功能 display, start_display, add_menu, add_function_to_menu init_display() # 为不同簇分配不同颜色 colors cm.rainbow(np.linspace(0, 1, len(compounds))) for (label, comp), color in zip(compounds.items(), colors): display.DisplayShape(comp, color(color[0], color[1], color[2]), updateFalse) display.FitAll() start_display() # 整合主流程 def main(step_file_path): # 1. 读取STEP shape, success, msg read_step_file(step_file_path) if not success: print(msg); return # 2. 提取面特征 face_features, face_list extract_face_features(shape) # 3. 聚类 initial_labels cluster_faces_by_features(face_features) # 4. 基于连接性合并 final_labels merge_clusters_by_connectivity(face_list, initial_labels) # 5. 输出与可视化 export_and_visualize(face_list, final_labels) if __name__ __main__: main(your_assembly.step)4. 应用场景与实战价值STEP-Parts这类技术绝非纸上谈兵它在工业软件和工程实践中有着广泛且深刻的应用场景。场景一逆向工程与模型修复从三维扫描仪得到的点云数据经过重建后往往生成一个“整体”的网格或B-Rep模型所有零件都粘连在一起。使用STEP-Parts技术可以自动将不同部件分割开极大简化了逆向建模的工作量。例如扫描一个旧机床自动分割出它的床身、主轴箱、导轨等主要部件工程师可以分别对每个部件进行参数化再设计。场景二CAE仿真前处理在进行有限元分析FEA或计算流体力学CFD仿真前通常需要对装配体进行“几何清理”和“理想化”。这包括移除不影响分析的细小特征如圆角、倒角、识别接触面、为不同材料或属性的部件分配不同的网格属性和边界条件。自动化的实例分割是这一步的基础。系统能自动识别出螺栓、垫片、密封圈等标准件或者将复杂的铸件本体与附加的加强筋、安装座区分开从而允许工程师对不同部分应用不同的网格划分策略和物理属性。场景三轻量化与可视化在Web端或移动端展示大型装配体如飞机、船舶时需要将模型轻量化。如果能把模型按零件分割就可以实现按需加载只加载视野内的零件以及更精细的LOD层次细节控制。例如在数字孪生应用中距离观察者远的零件用粗糙模型近的用精细模型这都需要以零件为单位进行管理。场景四零件库管理与重复利用许多装配体包含大量相同或相似的零件如标准件、重复的结构件。STEP-Parts可以识别出这些几何上相同或相似的实例并进行计数和归类。这对于生成物料清单BOM、成本估算、以及从旧模型中挖掘可重用零件库非常有帮助。设计师可以快速知道一个装配体用了多少个M6的螺栓或者找到所有几何形状相似的支架。场景五增材制造3D打印支撑生成与切片优化对于金属3D打印有时需要将一个大部件拆分成多个小块分别打印以减少热应力或适应打印舱尺寸。基于几何的分割算法可以帮助找到合理的“分型面”。同时识别出零件中的悬垂结构有助于更智能地生成支撑结构。实操心得在实际项目中纯粹的几何分割往往不够。结合轻量级的语义信息能大幅提升准确率。例如如果STEP文件中保留了图层Layer或颜色Color信息即使它们不完整也可以作为聚类特征的强先验。一个常见的策略是首先用图层/颜色进行粗分然后在每个颜色组内再用纯几何方法进行细分。这比单纯使用几何聚类要稳定得多。5. 常见挑战、陷阱与优化策略实现一个健壮的STEP-Parts系统会面临诸多挑战以下是我在实践中踩过的一些坑和对应的解决思路。挑战一几何精度与容差问题这是B-Rep处理中最经典的问题。两个理论上应该共享一条边的面由于数值计算误差它们的边可能并未精确重合。这会导致拓扑探索器认为它们是分离的从而破坏连接性分析。对策在读取STEP文件后必须进行几何修复Healing操作。这包括缝合Sewing容差内的边和顶点修复微小的间隙统一相邻面的几何连续性。OpenCASCADE的ShapeFix模块提供了相关工具。设置一个合理的容差值通常比模型尺寸小几个数量级如1e-6或1e-7是关键。挑战二复杂曲面与特征面的误分割一个复杂的自由曲面如汽车A面在B-Rep中可能被表示为数十甚至上百个小的NURBS面片。纯几何聚类很容易将这些本应属于同一张“逻辑曲面”的面片错误地分割开。对策在特征提取阶段需要加入曲面内在属性作为特征。例如计算每个面片中心点的高斯曲率、平均曲率以及相邻面片之间的曲率连续性。对于属于同一张光滑过渡曲面的面片这些曲率值会非常接近。可以将“与相邻面片的平均曲率差”作为一个特征值越小越可能属于同一曲面。挑战三薄壁件与接触面的处理对于钣金件或存在紧密配合的零件如轴与轴承它们之间的间隙可能远小于模型的整体尺寸和算法设定的距离容差。基于空间距离的聚类如DBSCAN的eps参数很容易将它们错误地合并。对策引入局部厚度分析。对于每一个面可以估算其所属区域的局部壁厚。两个非常接近但属于不同零件的面其法向量方向通常是相对或呈特定角度的。而同一个零件上两个接近的面如薄壁的两侧其法向量基本是反向的。通过分析面对之间的距离和法向关系可以更好地区分装配接触和薄壁结构。挑战四性能与大规模模型一个大型装配体可能有数万甚至数十万个面。计算每个面的曲率、构建全局邻接关系、进行聚类计算量会非常大。对策多尺度分割先使用简单的、计算量小的特征如面类型、面积、法向进行快速粗分割将模型分成若干大块。然后再在每个大块内部使用更精细的特征进行二次分割。空间划分使用八叉树Octree或包围盒层次结构BVH来管理面片在查找相邻面或进行空间查询时能大幅加速。并行计算特征提取和局部计算如面曲率可以很容易地并行化。聚类算法本身如DBSCAN的某些变种也支持并行。挑战五评估与参数调优如何评价分割结果的好坏没有绝对的标准答案通常需要与人工标注的“Ground Truth”对比。但在无监督场景下调优聚类参数如DBSCAN的eps和min_samples是个经验活。对策可视化检查这是最直接的方法。用不同颜色渲染不同簇人工检查分割边界是否合理。内部指标使用聚类内部指标如轮廓系数Silhouette Score来评估簇的紧密度和分离度。但这对几何数据不一定完全适用。基于规则的验证编写一些启发式规则来验证结果的合理性。例如“一个零件通常应形成一个封闭的壳”、“标准件如螺栓的几何特征应高度一致”、“装配接触面的面积不应超过零件自身表面积的某个比例”。当分割结果违反这些规则时可以给出警告或自动调整。最后我想强调的是完全通用的、全自动的、100%准确的CAD几何实例分割是一个尚未完全解决的学术难题。目前的工业级解决方案大多是“算法交互”的半自动工具。算法提供一个高质量的初始分割结果并高亮显示不确定的区域最后由工程师进行快速的手动合并、分割或修正。STEP-Parts方法的价值在于它提供了一个强大、自动化的起点能将工程师从繁琐的、重复性的手动选择操作中解放出来专注于更高层次的决策和验证。在实现你自己的分割工具时设定合理的期望值并始终将用户体验如何呈现结果、如何允许用户干预放在重要位置是项目成功的关键。