4 changed files with 367 additions and 748 deletions
@ -0,0 +1,300 @@ |
|||||
|
""" |
||||
|
CAD模型处理脚本 |
||||
|
功能:将STEP格式的CAD模型转换为结构化数据,包括: |
||||
|
- 几何信息:面、边、顶点的坐标数据 |
||||
|
- 拓扑信息:面-边-顶点的邻接关系 |
||||
|
- 空间信息:包围盒数据 |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
import pickle # 用于数据序列化 |
||||
|
import argparse # 命令行参数解析 |
||||
|
import numpy as np |
||||
|
from tqdm import tqdm # 进度条显示 |
||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed, TimeoutError # 并行处理 |
||||
|
import logging |
||||
|
from datetime import datetime |
||||
|
from scipy.spatial import cKDTree |
||||
|
from brep2sdf.utils.logger import logger |
||||
|
import tempfile |
||||
|
import trimesh |
||||
|
from trimesh.proximity import ProximityQuery |
||||
|
|
||||
|
# 导入OpenCASCADE相关库 |
||||
|
from OCC.Core.STEPControl import STEPControl_Reader # STEP文件读取器 |
||||
|
from OCC.Core.TopExp import TopExp_Explorer, topexp # 拓扑结构遍历 |
||||
|
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX # 拓扑类型定义 |
||||
|
from OCC.Core.BRep import BRep_Tool # B-rep工具 |
||||
|
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh # 网格剖分 |
||||
|
from OCC.Core.TopLoc import TopLoc_Location # 位置变换 |
||||
|
from OCC.Core.IFSelect import IFSelect_RetDone,IFSelect_RetError, IFSelect_RetFail, IFSelect_RetVoid # 操作状态码 |
||||
|
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape # 形状映射 |
||||
|
from OCC.Core.BRepBndLib import brepbndlib # 包围盒计算 |
||||
|
from OCC.Core.Bnd import Bnd_Box # 包围盒 |
||||
|
from OCC.Core.TopoDS import TopoDS_Shape, topods, TopoDS_Vertex # 拓扑数据结构 |
||||
|
from OCC.Core.StlAPI import StlAPI_Writer |
||||
|
|
||||
|
# 导入配置 |
||||
|
from brep2sdf.config.default_config import get_default_config |
||||
|
config = get_default_config() |
||||
|
|
||||
|
# 设置最大面数阈值,用于加速处理 |
||||
|
MAX_FACE = config.data.max_face |
||||
|
|
||||
|
|
||||
|
def _sample_uniform_points(num_points: int) -> np.ndarray: |
||||
|
"""在 [-0.5, 0.5] 范围内均匀采样点 |
||||
|
|
||||
|
参数: |
||||
|
num_points: 要采样的点数 |
||||
|
|
||||
|
返回: |
||||
|
np.ndarray: 形状为 (num_points, 3) 的采样点数组 |
||||
|
""" |
||||
|
return np.random.uniform(-0.5, 0.5, (num_points, 3)) |
||||
|
|
||||
|
def _sample_near_surface_points( |
||||
|
mesh: trimesh.Trimesh, |
||||
|
num_points: int, |
||||
|
std_dev: float |
||||
|
) -> np.ndarray: |
||||
|
"""在网格表面附近采样点 |
||||
|
|
||||
|
参数: |
||||
|
mesh: 输入的trimesh网格 |
||||
|
num_points: 要采样的点数 |
||||
|
std_dev: 沿法线方向的扰动标准差 |
||||
|
|
||||
|
返回: |
||||
|
np.ndarray: 形状为 (num_points, 3) 的采样点数组 |
||||
|
""" |
||||
|
if mesh.faces.shape[0] == 0: |
||||
|
logger.warning("网格没有面,无法执行近表面采样。将替换为均匀采样点。") |
||||
|
return _sample_uniform_points(num_points) |
||||
|
|
||||
|
try: |
||||
|
near_points_on_surface = mesh.sample(num_points) |
||||
|
proximity_query_near = ProximityQuery(mesh) |
||||
|
closest_points_near, _, face_indices_near = proximity_query_near.on_surface(near_points_on_surface) |
||||
|
|
||||
|
if np.any(face_indices_near >= len(mesh.face_normals)): |
||||
|
raise IndexError("Face index out of bounds during near-surface normal lookup") |
||||
|
|
||||
|
normals_near = mesh.face_normals[face_indices_near] |
||||
|
perturbations = np.random.randn(num_points, 1) * std_dev |
||||
|
near_points = near_points_on_surface + normals_near * perturbations |
||||
|
return np.clip(near_points, -0.5, 0.5) |
||||
|
|
||||
|
except Exception as e: |
||||
|
logger.warning(f"近表面采样失败: {e}。将替换为均匀采样点。") |
||||
|
return _sample_uniform_points(num_points) |
||||
|
|
||||
|
def sample_points( |
||||
|
trimesh_mesh_ncs: trimesh.Trimesh, |
||||
|
num_uniform_samples: int, |
||||
|
num_near_surface_samples: int, |
||||
|
sdf_sampling_std_dev: float |
||||
|
) -> np.ndarray | None: |
||||
|
"""组合均匀采样和近表面采样的点 |
||||
|
|
||||
|
参数: |
||||
|
trimesh_mesh_ncs: 归一化的trimesh网格 |
||||
|
num_uniform_samples: 均匀采样点数 |
||||
|
num_near_surface_samples: 近表面采样点数 |
||||
|
sdf_sampling_std_dev: 近表面采样的标准差 |
||||
|
|
||||
|
返回: |
||||
|
np.ndarray | None: 合并后的采样点数组,失败时返回None |
||||
|
""" |
||||
|
sampled_points_list = [] |
||||
|
|
||||
|
# 均匀采样 |
||||
|
if num_uniform_samples > 0: |
||||
|
uniform_points = _sample_uniform_points(num_uniform_samples) |
||||
|
sampled_points_list.append(uniform_points) |
||||
|
|
||||
|
# 近表面采样 |
||||
|
if num_near_surface_samples > 0: |
||||
|
near_points = _sample_near_surface_points( |
||||
|
trimesh_mesh_ncs, |
||||
|
num_near_surface_samples, |
||||
|
sdf_sampling_std_dev |
||||
|
) |
||||
|
sampled_points_list.append(near_points) |
||||
|
|
||||
|
# 合并采样点 |
||||
|
if not sampled_points_list: |
||||
|
logger.warning("没有采样到任何点。") |
||||
|
return None |
||||
|
|
||||
|
return np.vstack(sampled_points_list).astype(np.float32) |
||||
|
|
||||
|
# 在原始的sample_sdf_points_and_normals函数中使用新的采样函数 |
||||
|
def sample_sdf_points_and_normals( |
||||
|
trimesh_mesh_ncs: trimesh.Trimesh, |
||||
|
surf_bbox_ncs: np.ndarray, |
||||
|
num_sdf_samples: int = 4096, |
||||
|
sdf_sampling_std_dev: float = 0.01 |
||||
|
) -> np.ndarray | None: |
||||
|
""" |
||||
|
在归一化坐标系(NCS)下采样固定数量的点,并计算它们的SDF值和最近表面法线。 |
||||
|
采用均匀采样和近表面采样的混合策略。 |
||||
|
|
||||
|
参数: |
||||
|
trimesh_mesh_ncs: 归一化的 Trimesh 对象。 |
||||
|
surf_bbox_ncs: 归一化坐标系下各面的包围盒 [num_faces, 6]。 |
||||
|
num_sdf_samples: 要采样的总点数。 |
||||
|
sdf_sampling_std_dev: 近表面采样时沿法线扰动的标准差。 |
||||
|
|
||||
|
返回: |
||||
|
np.ndarray | None: 形状为 (N, 7) 的数组 [x, y, z, nx, ny, nz, sdf], |
||||
|
如果采样或计算失败则返回 None。 |
||||
|
""" |
||||
|
logger.debug("为 SDF 计算采样点 (固定数量策略)...") |
||||
|
if trimesh_mesh_ncs is None or not isinstance(trimesh_mesh_ncs, trimesh.Trimesh): |
||||
|
logger.error("无效的 Trimesh 对象提供给 SDF 采样。") |
||||
|
return None |
||||
|
if num_sdf_samples <= 0: |
||||
|
logger.warning("请求的 SDF 采样点数为零或负数,不进行采样。") |
||||
|
return None |
||||
|
|
||||
|
# 使用固定的采样边界 [-0.5, 0.5],因为我们已经做过归一化 |
||||
|
min_bound_ncs = np.array([-0.5, -0.5, -0.5], dtype=np.float32) |
||||
|
max_bound_ncs = np.array([0.5, 0.5, 0.5], dtype=np.float32) |
||||
|
bbox_size_ncs = max_bound_ncs - min_bound_ncs |
||||
|
|
||||
|
# --- 使用固定的总样本数分配点数 --- |
||||
|
num_uniform_samples = num_sdf_samples // 2 |
||||
|
num_near_surface_samples = num_sdf_samples - num_uniform_samples |
||||
|
logger.debug(f"固定 SDF 采样点数: {num_sdf_samples} (均匀: {num_uniform_samples}, 近表面: {num_near_surface_samples})") |
||||
|
|
||||
|
# --- 执行采样 --- |
||||
|
sampled_points_ncs = sample_points( |
||||
|
trimesh_mesh_ncs, |
||||
|
num_uniform_samples, |
||||
|
num_near_surface_samples, |
||||
|
sdf_sampling_std_dev |
||||
|
) |
||||
|
|
||||
|
try: |
||||
|
proximity_query = ProximityQuery(trimesh_mesh_ncs) |
||||
|
|
||||
|
# 分批计算SDF以避免内存问题 |
||||
|
batch_size = 1000 |
||||
|
sdf_values = [] |
||||
|
closest_points = [] |
||||
|
face_indices = [] |
||||
|
|
||||
|
for i in range(0, len(sampled_points_ncs), batch_size): |
||||
|
batch_points = sampled_points_ncs[i:i + batch_size] |
||||
|
|
||||
|
# 计算当前批次的最近点和面 |
||||
|
batch_closest, batch_distances, batch_faces = proximity_query.on_surface(batch_points) |
||||
|
|
||||
|
# 计算点到最近面的向量 |
||||
|
direction_vectors = batch_points - batch_closest |
||||
|
|
||||
|
# 使用batch_compute_normals计算最近点的法向量 |
||||
|
# 将batch_closest重新组织为所需的格式 (N,) 数组,每个元素是 (M, 3) 数组 |
||||
|
closest_points_reshaped = np.array([batch_closest], dtype=object) |
||||
|
closest_points_reshaped[0] = batch_closest |
||||
|
|
||||
|
# 计算法向量 |
||||
|
normals_batch = batch_compute_normals( |
||||
|
trimesh_mesh_ncs, |
||||
|
closest_points_reshaped, |
||||
|
normal_type='vertex', # 使用顶点法向量 |
||||
|
k_neighbors=3 |
||||
|
)[0] # 取第一个元素因为我们只传入了一个批次 |
||||
|
|
||||
|
# 计算方向向量与法向量的点积 |
||||
|
dot_products = np.sum(direction_vectors * normals_batch, axis=1) |
||||
|
signs = np.sign(dot_products) |
||||
|
|
||||
|
# 确保零点处的符号处理 |
||||
|
zero_mask = np.abs(batch_distances) < 1e-6 |
||||
|
signs[zero_mask] = 0.0 |
||||
|
|
||||
|
# 计算带符号距离 |
||||
|
batch_sdf = batch_distances * signs |
||||
|
|
||||
|
# 限制SDF值的范围 |
||||
|
batch_sdf = np.clip(batch_sdf, -1.414, 1.414) # sqrt(2) |
||||
|
|
||||
|
# 添加调试信息 |
||||
|
if i == 0: # 只打印第一个批次的统计信息 |
||||
|
logger.debug(f"批次统计 (首批次):") |
||||
|
logger.debug(f" 法向量范围: [{normals_batch.min():.4f}, {normals_batch.max():.4f}]") |
||||
|
logger.debug(f" 法向量长度: {np.linalg.norm(normals_batch, axis=1).mean():.4f}") |
||||
|
logger.debug(f" 距离范围: [{batch_distances.min():.4f}, {batch_distances.max():.4f}]") |
||||
|
logger.debug(f" 点积范围: [{dot_products.min():.4f}, {dot_products.max():.4f}]") |
||||
|
logger.debug(f" 符号分布: 正={np.sum(signs > 0)}, 负={np.sum(signs < 0)}, 零={np.sum(signs == 0)}") |
||||
|
logger.debug(f" SDF范围: [{batch_sdf.min():.4f}, {batch_sdf.max():.4f}]") |
||||
|
|
||||
|
sdf_values.append(batch_sdf) |
||||
|
closest_points.append(batch_closest) |
||||
|
face_indices.append(batch_faces) |
||||
|
|
||||
|
# 合并批次结果 |
||||
|
sdf_values = np.concatenate(sdf_values) |
||||
|
closest_points = np.concatenate(closest_points) |
||||
|
|
||||
|
# 为所有点计算法向量 |
||||
|
all_points_reshaped = np.array([closest_points], dtype=object) |
||||
|
all_points_reshaped[0] = closest_points |
||||
|
sampled_normals = batch_compute_normals( |
||||
|
trimesh_mesh_ncs, |
||||
|
all_points_reshaped, |
||||
|
normal_type='vertex', |
||||
|
k_neighbors=3 |
||||
|
)[0] |
||||
|
|
||||
|
# 验证法向量 |
||||
|
normal_lengths = np.linalg.norm(sampled_normals, axis=1) |
||||
|
logger.debug(f"最终法向量统计:") |
||||
|
logger.debug(f" 形状: {sampled_normals.shape}") |
||||
|
logger.debug(f" 长度: min={normal_lengths.min():.4f}, max={normal_lengths.max():.4f}, mean={normal_lengths.mean():.4f}") |
||||
|
logger.debug(f" 分量范围: x=[{sampled_normals[:,0].min():.4f}, {sampled_normals[:,0].max():.4f}]") |
||||
|
logger.debug(f" 分量范围: y=[{sampled_normals[:,1].min():.4f}, {sampled_normals[:,1].max():.4f}]") |
||||
|
logger.debug(f" 分量范围: z=[{sampled_normals[:,2].min():.4f}, {sampled_normals[:,2].max():.4f}]") |
||||
|
|
||||
|
# 添加验证 |
||||
|
valid_mask = ( |
||||
|
~np.isnan(sdf_values) & ~np.isinf(sdf_values) & |
||||
|
~np.isnan(sampled_normals).any(axis=1) & ~np.isinf(sampled_normals).any(axis=1) & |
||||
|
~np.isnan(sampled_points_ncs).any(axis=1) & ~np.isinf(sampled_points_ncs).any(axis=1) |
||||
|
) |
||||
|
|
||||
|
if not np.all(valid_mask): |
||||
|
num_invalid = sampled_points_ncs.shape[0] - np.sum(valid_mask) |
||||
|
logger.warning(f"在 SDF/法线计算中发现 {num_invalid} 个无效条目。将它们过滤掉。") |
||||
|
sampled_points_ncs = sampled_points_ncs[valid_mask] |
||||
|
sampled_normals = sampled_normals[valid_mask] |
||||
|
sdf_values = sdf_values[valid_mask] |
||||
|
|
||||
|
if sampled_points_ncs.shape[0] > 0: |
||||
|
combined_data = np.hstack(( |
||||
|
sampled_points_ncs, |
||||
|
sampled_normals, |
||||
|
sdf_values[:, np.newaxis] |
||||
|
)).astype(np.float32) |
||||
|
|
||||
|
# 添加SDF分布验证 |
||||
|
final_sdf = combined_data[:, -1] |
||||
|
logger.debug(f"最终SDF分布验证:") |
||||
|
logger.debug(f" 正值点数: {np.sum(final_sdf > 0)}") |
||||
|
logger.debug(f" 负值点数: {np.sum(final_sdf < 0)}") |
||||
|
logger.debug(f" 零值点数: {np.sum(np.abs(final_sdf) < 1e-6)}") |
||||
|
logger.debug(f" SDF范围: [{final_sdf.min():.4f}, {final_sdf.max():.4f}]") |
||||
|
|
||||
|
# 验证分布是否合理 |
||||
|
if np.sum(final_sdf > 0) == 0 or np.sum(final_sdf < 0) == 0: |
||||
|
logger.warning("警告:SDF值分布异常,没有正值或负值!") |
||||
|
|
||||
|
return combined_data |
||||
|
else: |
||||
|
logger.warning("过滤 SDF/法线结果后没有剩余有效点。") |
||||
|
return None |
||||
|
except Exception as e: |
||||
|
logger.error(f"计算 SDF 或法线时失败: {str(e)}") |
||||
|
return None |
Loading…
Reference in new issue