handle_simple.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. from PyQt5.QtCore import QObject, pyqtSlot
  2. import open3d as o3d
  3. import numpy as np
  4. from stl import mesh as stl_mesh
  5. import os
  6. from trimesh import Trimesh
  7. from OCC.Core.TopoDS import TopoDS_Face
  8. class Datahandle(QObject):
  9. def __init__(self):
  10. super().__init__()
  11. self.qml_item = None
  12. self.vertices = np.array([])
  13. self.colors = np.array([])
  14. self.triangles = np.array([]) # 新增:三角面索引
  15. self.normals = np.array([]) # 新增:法线
  16. def load_data(self):
  17. """
  18. 加载 3D 数据(这里用随机点云代替实际文件)
  19. 返回: vertices, colors (numpy arrays)
  20. """
  21. pcd = o3d.geometry.PointCloud()
  22. pcd.points = o3d.utility.Vector3dVector(np.random.rand(5000, 3))
  23. pcd.colors = o3d.utility.Vector3dVector(np.random.rand(5000, 3))
  24. vertices = np.asarray(pcd.points)
  25. colors = np.asarray(pcd.colors)
  26. return vertices, colors
  27. @pyqtSlot('QVariant')
  28. def set3DItem(self, item):
  29. """接收 QML 中的 QML3DItem 实例"""
  30. self.qml_item = item
  31. if self.qml_item:
  32. self.qml_item.set_data(self.vertices, self.colors)
  33. print("✅ 数据已传递给 QML3DItem")
  34. @pyqtSlot(float, float, float)
  35. def onPointPicked(self, x, y, z):
  36. """接收从 QML3DItem 发出的拾取信号"""
  37. print(f"✅ 拾取到点: ({x:.3f}, {y:.3f}, {z:.3f})")
  38. # 这里可以扩展:保存坐标、发送到其他模块等
  39. class Dataload(QObject):
  40. def __init__(self):
  41. super().__init__()
  42. self.qml_item = None
  43. self.vertices = np.array([])
  44. self.colors = np.array([])
  45. self.original_colors = np.array([])
  46. self.triangles = np.array([]) # 新增:三角面索引
  47. self.normals = np.array([]) # 新增:法线
  48. self.picking_color_map = {}
  49. self.face_normals = np.array([]) # 面法线
  50. def load_data_from_file(self, file_path: str):
  51. """
  52. 使用 numpy-stl 自动识别 STL 格式,并提取轮廓线(边界边 + 锐角边)。
  53. """
  54. print(f"📁 正在加载: {file_path}")
  55. if not os.path.exists(file_path):
  56. print(f"❌ 文件不存在: {file_path}")
  57. return np.array([]), np.array([]), np.array([]), np.array([]), np.array([])
  58. ext = os.path.splitext(file_path)[1].lower()
  59. if ext != '.stl':
  60. print(f"❌ 不支持的格式: {ext}")
  61. return np.array([]), np.array([]), np.array([]), np.array([]), np.array([])
  62. try:
  63. # ✅ 自动识别格式(旧版本也支持)
  64. stl_data = stl_mesh.Mesh.from_file(file_path)
  65. # 获取三角面 (N, 3, 3)
  66. vectors = stl_data.vectors.astype(np.float32)
  67. print(f"✅ 加载 {len(vectors)} 个三角面")
  68. # 展平顶点
  69. vertices = vectors.reshape(-1, 3)
  70. print(f"原始顶点数: {len(vertices)}")
  71. # 三角面索引
  72. num_triangles = len(vectors)
  73. triangles = np.arange(num_triangles * 3, dtype=np.int32).reshape(-1, 3)
  74. print(f"三角面数量: {num_triangles}")
  75. # 法线(每个三角面对应一个法线)
  76. normals = stl_data.normals.astype(np.float32)
  77. # --- 归一化(保持不变)---
  78. if len(vertices) > 0:
  79. min_coords = np.min(vertices, axis=0)
  80. max_coords = np.max(vertices, axis=0)
  81. center = (min_coords + max_coords) / 2.0
  82. scale = np.max(max_coords - min_coords)
  83. if scale > 0:
  84. vertices = (vertices - center) / scale * 2.0
  85. z_range = np.max(vertices[:, 2]) - np.min(vertices[:, 2])
  86. if z_range < 0.1:
  87. z_center = (np.min(vertices[:, 2]) + np.max(vertices[:, 2])) / 2.0
  88. vertices[:, 2] = (vertices[:, 2] - z_center) * (0.2 / z_range) + z_center
  89. print(f"调整Z轴范围: {z_range:.6f} -> 0.2")
  90. # ===================================================================
  91. # ✅ 新增:顶点去重 + 重建三角面索引
  92. # ===================================================================
  93. print("🔍 正在合并重复顶点...")
  94. rounded_vertices = np.round(vertices, decimals=6)
  95. unique_vertices, unique_indices = np.unique(rounded_vertices, axis=0, return_inverse=True)
  96. # 重建 triangles:每个旧顶点 → 映射到新唯一顶点索引
  97. new_triangles = unique_indices.reshape(-1, 3) # 每3个一组
  98. vertices = unique_vertices.astype(np.float32)
  99. triangles = new_triangles.astype(np.int32)
  100. print(f"✅ 顶点数: {len(vertices)} (原: {len(unique_indices)})")
  101. print(f"✅ 三角面数: {len(triangles)}")
  102. # ===================================================================
  103. # ✅ 重新计算每个面的法线(用于边提取)
  104. # ===================================================================
  105. def compute_face_normal(v0, v1, v2):
  106. u = v1 - v0
  107. v = v2 - v0
  108. n = np.cross(u, v)
  109. norm = np.linalg.norm(n)
  110. return n / norm if norm > 1e-8 else np.array([0.0, 0.0, 1.0])
  111. face_normals = []
  112. for tri in triangles:
  113. v0, v1, v2 = vertices[tri[0]], vertices[tri[1]], vertices[tri[2]]
  114. face_normals.append(compute_face_normal(v0, v1, v2))
  115. face_normals = np.array(face_normals)
  116. # ===================================================================
  117. # ✅ 构建边 -> 面映射(使用 (min, max) 保证方向一致)
  118. # ===================================================================
  119. from collections import defaultdict
  120. edge_faces = defaultdict(list)
  121. for tri_idx, tri in enumerate(triangles):
  122. a, b, c = tri
  123. edges = [
  124. (min(a, b), max(a, b)),
  125. (min(b, c), max(b, c)),
  126. (min(c, a), max(c, a))
  127. ]
  128. for edge in edges:
  129. if tri_idx not in edge_faces[edge]:
  130. edge_faces[edge].append(tri_idx)
  131. # ===================================================================
  132. # ✅ 提取轮廓线:边界边(1个面) + 锐角边(2个面且夹角大)
  133. # ===================================================================
  134. silhouette_edges = []
  135. crease_threshold_rad = np.radians(30.0) # 30度
  136. for (v0, v1), face_list in edge_faces.items():
  137. if len(face_list) == 1:
  138. # 边界边
  139. silhouette_edges.append([v0, v1])
  140. elif len(face_list) == 2:
  141. # 锐角边
  142. n1 = face_normals[face_list[0]]
  143. n2 = face_normals[face_list[1]]
  144. cos_angle = np.dot(n1, n2)
  145. if cos_angle < np.cos(crease_threshold_rad):
  146. silhouette_edges.append([v0, v1])
  147. silhouette_edges = np.array(silhouette_edges, dtype=np.int32)
  148. print(f"✅ 提取 {len(silhouette_edges)} 条轮廓线(边界 + 折痕)")
  149. # ===================================================================
  150. # ✅ 重建 vertex_normals 和 colors(基于新顶点)
  151. # ===================================================================
  152. print("🎨 重建顶点法线和颜色...")
  153. num_vertices = len(vertices)
  154. vertex_normals = np.zeros((num_vertices, 3), dtype=np.float32)
  155. # 构建顶点 -> 面索引 映射
  156. vertex_face_map = [[] for _ in range(num_vertices)]
  157. for tri_idx, tri in enumerate(triangles):
  158. for v_idx in tri:
  159. vertex_face_map[v_idx].append(tri_idx)
  160. # 计算每个顶点的平均法线(取相邻面法线平均)
  161. for i in range(num_vertices):
  162. if vertex_face_map[i]:
  163. avg_normal = np.mean(face_normals[vertex_face_map[i]], axis=0)
  164. norm = np.linalg.norm(avg_normal)
  165. if norm > 1e-8:
  166. vertex_normals[i] = avg_normal / norm
  167. else:
  168. vertex_normals[i] = np.array([0.0, 0.0, 1.0])
  169. else:
  170. vertex_normals[i] = np.array([0.0, 0.0, 1.0])
  171. # 生成颜色:法线映射到 [0,1]
  172. colors = (vertex_normals + 1.0) / 2.0
  173. colors = np.clip(colors, 0.0, 1.0)
  174. if np.any(np.isnan(colors)) or np.any(np.isinf(colors)):
  175. colors = np.ones_like(vertex_normals) * 0.8
  176. # 保存原始颜色,防止被高亮逻辑破坏
  177. self.original_colors = self.colors.copy() # 可选:用于恢复
  178. print(f"✅ 顶点法线数量: {len(vertex_normals)}")
  179. print(f"✅ 颜色数量: {len(colors)}")
  180. # ===================================================================
  181. # ✅ 加载成功
  182. # ===================================================================
  183. print(f"✅ 加载成功: {file_path}")
  184. self.face_normals = face_normals
  185. self._build_picking_color_map()
  186. # print(f"131313131://///////////////////////////{vertices},{colors},{triangles},{vertex_normals},{silhouette_edges}")
  187. return vertices, colors, triangles, vertex_normals, silhouette_edges
  188. except Exception as e:
  189. print(f"❌ 加载失败: {e}")
  190. return np.array([]), np.array([]), np.array([]), np.array([]), np.array([])
  191. def _build_picking_color_map(self):
  192. self.picking_color_map.clear()
  193. for i in range(len(self.vertices)):
  194. r, g, b = self._get_picking_color(i)
  195. key = (round(r, 5), round(g, 5), round(b, 5))
  196. self.picking_color_map[key] = i
  197. print("✅ picking_color_map 构建完成")
  198. # 调试:打印前几个
  199. print("📊 前5个映射:", list(self.picking_color_map.items())[:5])
  200. def _get_picking_color(self, index):
  201. """根据顶点索引生成唯一颜色 (r,g,b 归一化到 0~1)"""
  202. idx = index + 1 # 避免 0,0,0 黑色
  203. r = (idx) & 0xFF
  204. g = (idx >> 8) & 0xFF
  205. b = (idx >> 16) & 0xFF
  206. return (r / 255.0, g / 255.0, b / 255.0)
  207. def extract_edge_loops(vertices, triangles, boundary_only=False, crease_threshold_deg=30.0):
  208. """
  209. 提取模型的边界边和/或锐角边(折痕边)
  210. 返回: list of [idx1, idx2] (顶点索引对)
  211. """
  212. import numpy as np
  213. from collections import defaultdict
  214. # 构建边 -> 面列表
  215. edge_faces = defaultdict(list)
  216. for tri_idx, (a, b, c) in enumerate(triangles):
  217. edges = [(a,b), (b,c), (c,a)]
  218. for u, v in edges:
  219. key = (min(u, v), max(u, v)) # 规范化边
  220. edge_faces[key].append(tri_idx)
  221. # 计算每个面的法线
  222. def compute_face_normal(v0, v1, v2):
  223. u = v1 - v0
  224. v = v2 - v0
  225. n = np.cross(u, v)
  226. norm = np.linalg.norm(n)
  227. return n / norm if norm > 1e-8 else np.array([0.0, 0.0, 1.0])
  228. face_normals = []
  229. for tri in triangles:
  230. v0, v1, v2 = vertices[tri[0]], vertices[tri[1]], vertices[tri[2]]
  231. face_normals.append(compute_face_normal(v0, v1, v2))
  232. face_normals = np.array(face_normals)
  233. # 收集边
  234. edges = []
  235. threshold_rad = np.radians(crease_threshold_deg)
  236. for (v0, v1), face_list in edge_faces.items():
  237. if len(face_list) == 1:
  238. # 边界边
  239. edges.append([v0, v1])
  240. elif not boundary_only and len(face_list) == 2:
  241. n1 = face_normals[face_list[0]]
  242. n2 = face_normals[face_list[1]]
  243. cos_angle = np.dot(n1, n2)
  244. if cos_angle < np.cos(threshold_rad): # 夹角大于阈值
  245. edges.append([v0, v1])
  246. return edges