当前位置:   article > 正文

(超级详细)基于numpy-stl,对不规则柱状实体旋转并求沿轴切片的周长_numpy stl mesh教程

numpy stl mesh教程

一、前言

本文是在我前一篇博文的基础上进一步扩展,除了对实体进行切片,求周长,还新增了旋转实体的操作。

(源代码)基于numpy-stl操作stl文件——读取圆台z轴截面的周长

1. 模型准备

创建一个不规则的实体模型,并保存为stl:

模型与xy平面平行,由两个回转的药丸形状构成。

2. 模型渲染

为了方便可视化模型,我们使用以下代码在notebook里面渲染:

  1. from stl import mesh
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. from mpl_toolkits.mplot3d import Axes3D
  5. import matplotlib.tri as mtri
  6. import time
  7. mesh_ = mesh.Mesh.from_file('test.stl')
  8. def show(mesh_):
  9. # 绘制三维图形
  10. time1 = time.time()
  11. fig = plt.figure(dpi=300)
  12. ax = fig.add_subplot(111, projection='3d')
  13. for mesh in mesh_:
  14. vertices = np.array([mesh[0:3], mesh[3:6], mesh[6:9]])
  15. try:
  16. ax.plot_trisurf(vertices[:, 0], vertices[:, 1], vertices[:, 2], linewidth=0.2, antialiased=True)
  17. except:
  18. print('vertices:', vertices)
  19. # 设置坐标轴标签
  20. ax.set_xlabel('X')
  21. ax.set_ylabel('Y')
  22. ax.set_zlabel('Z')
  23. plt.title('3D Grid Triangles Visualization')
  24. plt.show()
  25. time2 = time.time()
  26. print('caculate time:', time2 - time1)
  27. show(mesh_)

结果如下所示:

二、分析

我们想要对这个平躺着的模型进行切片处理,最开始方便办法就是将模型立起来。但是模型的轴向向量并不可知,如何计算得到模型的轴向向量是本文难题。得到模型轴向向量之后,我们可以使用轴向向量进行坐标变换,将模型的轴与z轴对齐。由此便可以直接套用我之前一篇的博客的工作,得到切片的周长。

1. 如何获取轴向向量

首先我们观察模型可知,模型与xy平面平行。那么,当我们使用一个垂直于xy平面且过原点的平面y=kx于模型相交时,会得到一个椭圆的面。 当且仅当,这个椭圆的周长最小时,这个切平面的法向量就是模型的轴向向量。

2. 代码实现获取轴向向量

在上一节中我们已经说明了几何原理,这一节我们使用代码实现这个想法。

 2.1 导入相关库

  1. from scipy.spatial import ConvexHull
  2. import numpy as np
  3. from stl import mesh
  4. import matplotlib.pyplot as plt
  5. import time

2.2 计算空间三角形与和y=kx平面的交点

  1. def check_intersection_with_plane(A, B, C, k):
  2. def line_plane_intersection(P1, P2, k):
  3. x1, y1, z1 = P1
  4. x2, y2, z2 = P2
  5. # 计算 t 的分母和分子
  6. denominator = k * (x2 - x1) - (y2 - y1)
  7. if denominator == 0:
  8. # 如果分母为0,说明直线平行于平面,没有交点或是无穷多个交点
  9. return None
  10. t = (y1 - k * x1) / denominator
  11. # 判断 t 是否在 [0, 1] 范围内,确定交点是否在线段上
  12. if 0 <= t <= 1:
  13. # 计算交点的坐标
  14. x = x1 + t * (x2 - x1)
  15. y = y1 + t * (y2 - y1)
  16. z = z1 + t * (z2 - z1)
  17. return (x, y, z)
  18. else:
  19. return None
  20. # 检查三角形的每一条边是否与平面 y = kx 相交
  21. edges = [(A, B), (B, C), (C, A)]
  22. intersection_points = []
  23. for P1, P2 in edges:
  24. intersection = line_plane_intersection(P1, P2, k)
  25. if intersection:
  26. intersection_points.append(intersection)
  27. if len(intersection_points) > 2:
  28. # print(len(intersection_points))
  29. intersection_points=[]
  30. return intersection_points if intersection_points else None
'
运行

以上代码计算了,如果空间中的三个点构造的三角形与平面y=kx相交,则返回交点。由我前一篇博文的推论可知,交点只会有一点或者两点。具体实现原理是高中几何知识,不在此赘述。

2.3 计算空间中的点的凸包及周长

  1. #计算凸包
  2. def compute_convex_hull(points):
  3. hull = ConvexHull(points)
  4. return hull
  5. def calculate_hull_perimeter(hull):
  6. perimeter = 0
  7. for simplex in hull.simplices:
  8. # 每个 simplex 包含凸包中的两个点的索引
  9. point1 = hull.points[simplex[0]]
  10. point2 = hull.points[simplex[1]]
  11. # 计算两个点之间的欧几里得距离
  12. edge_length = np.linalg.norm(point1 - point2)
  13. perimeter += edge_length
  14. return perimeter
'
运行

以上两个函数计算了一个交点集合的凸包,求出的周长是某一k值下切到的。

2.4 计算单次切片的周长

  1. #切片计算周长
  2. def slides(mesh_,k):
  3. Cross = []
  4. # 遍历每个三角形
  5. for triangle in mesh_.points:
  6. A = np.array([triangle[0], triangle[1], triangle[2]])
  7. B = np.array([triangle[3], triangle[4], triangle[5]])
  8. C = np.array([triangle[6], triangle[7], triangle[8]])
  9. has_intersection = check_intersection_with_plane(A, B, C, k)
  10. if has_intersection:
  11. for h in has_intersection:
  12. if not any(np.array_equal(h, p) for p in Cross):
  13. Cross.append(h)
  14. # print(f"Number of intersection points: {len(Cross)}")
  15. # 创建一个三维图形对象
  16. plt.figure()
  17. M=[]
  18. Z=[]
  19. # 绘制散点图
  20. if Cross:
  21. X, Y, Z = zip(*Cross)
  22. x = np.sqrt(np.array(X)**2 + np.array(Y)**2)
  23. # 根据 X 的正负确定投影后的正负
  24. M = np.sign(X) * x
  25. plt.scatter(M, Z, c='r', s=1)
  26. plt.show()
  27. result = []
  28. for i in range(len(M)):
  29. result.append((M[i],Z[i]))
  30. # return result
  31. # 计算三维凸包
  32. hull = compute_convex_hull(result)
  33. # 计算凸包的周长
  34. perimeter = calculate_hull_perimeter(hull)
  35. return perimeter
'
运行

这个函数输入的mesh:也就是我们模型的网格坐标,和k斜率:平面位置。这个函数能够返回在当前k值下,与模型相交的点的凸包的周长,如下所示:

2.5 计算轴向向量

  1. def uniform_split(a, b, k):
  2. return np.linspace(a, b, k)
  3. def get_normal(mesh_):
  4. # 示例用法
  5. a = -1
  6. b = 1
  7. Num = 10
  8. Min_k = 0
  9. while True:
  10. X=uniform_split(a, b, Num)
  11. Content = []
  12. for k in X:
  13. # if k != 0:
  14. # print(f"y = {k}x")
  15. Content.append(slides(mesh_,k))
  16. plt.figure(dpi=200)
  17. # 绘制折线图
  18. plt.plot(X, Content)
  19. plt.xlabel('k') # 设置x轴标签
  20. plt.ylabel('content') # 设置y轴标签
  21. plt.title('Line Plot') # 设置图标题
  22. plt.show()
  23. print('Stop:',abs(Min_k-X[np.argmin(Content)]) < 0.0001)
  24. if abs(Min_k-X[np.argmin(Content)]) < 0.0001 :
  25. break
  26. else:
  27. print('Min K:',Min_k,'————》 Min X;',X[np.argmin(Content)],'delta:',abs(X[np.argmin(Content)]-Min_k),'Num:',Num)
  28. delta= X[np.argmin(Content)]-Min_k
  29. Min_k = X[np.argmin(Content)]
  30. a = Min_k - 3*delta
  31. b = Min_k + 3*delta
  32. Num +=10
  33. print('Now a:',a,'b:',b,'k:',Num)
  34. return [-Min_k,1,0]
'
运行

 这段代码实现了我们如何去搜索最优的平面,达到切面的周长最小。我们首先设定k在-1和1之间(依据经验设定,不同的模型应该不同),分为10段,初始k为0。在每一次计算得到切面周长的集合之后,我们更新k的值,并将上下限限制在k的正负三倍delta之间,同时分段数加10。更新上下限和增加分段数,能够加速算法的收索,快速达到最优解。当delta小于0.0001时,结束搜索,并返回平面的法向量,这个法向量便是模型的轴向量。从下图可以看出,最终的搜索结果为:k = 0.72650124左右。(旋转完成是因为,函数都被集成了,所以直接运行下去,可以不看

3.  对模型进行旋转

  1. import numpy as np
  2. from stl import mesh
  3. def rotation_(my_mesh):
  4. # 定义计算旋转矩阵的函数
  5. def rotation_matrix_from_vectors(vec1, vec2):
  6. """ Find the rotation matrix that aligns vec1 to vec2
  7. :param vec1: A 3d "source" vector
  8. :param vec2: A 3d "destination" vector
  9. :return mat: A transform matrix (3x3)
  10. """
  11. a, b = (vec1 / np.linalg.norm(vec1)).reshape(3), (vec2 / np.linalg.norm(vec2)).reshape(3)
  12. v = np.cross(a, b)
  13. c = np.dot(a, b)
  14. s = np.linalg.norm(v)
  15. kmat = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
  16. return np.eye(3) + kmat + kmat @ kmat * ((1 - c) / (s ** 2))
  17. normal = get_normal(my_mesh)
  18. # 定义原始方向向量和目标方向向量(单位向量)
  19. initial_vector = np.array(normal) #
  20. target_vector = np.array([0,0,1]) #z轴方向
  21. target_unit_vector = target_vector / np.linalg.norm(target_vector)
  22. # 计算旋转矩阵
  23. R = rotation_matrix_from_vectors(initial_vector, target_unit_vector)
  24. # 旋转所有顶点
  25. my_mesh.vectors = np.dot(my_mesh.vectors.reshape(-1, 3), R).reshape(-1, 3, 3)
  26. # 保存旋转后的 STL 文件
  27. my_mesh.save('my_rotated_model_surface.stl')
  28. print('旋转完成')
  29. return my_mesh

以上这个函数会根据给出的mesh网格,自动计算轴向量,然后使用库方法惊醒旋转,保存为my_rotated_model_surface.stl文件。结果如下所示:

三、切片求周长

 我们已经得到了想要的标准模型,接下来使用我上一篇博文的方法,进行切分:

  1. from scipy.spatial import ConvexHull
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. #定义计算凸包和周长的函数
  5. def get_hull_and_content(points):
  6. # 计算凸包
  7. try:
  8. hull = ConvexHull(points)
  9. except:
  10. print('points:', points)
  11. print('凸包计算失败')
  12. return 0
  13. # 提取凸包的顶点坐标
  14. convex_hull_points = points[hull.vertices]
  15. # 将最后一个顶点复制到列表的开头以闭合凸包
  16. convex_hull_points = np.vstack([convex_hull_points, convex_hull_points[0]])
  17. # 计算凸包的周长
  18. perimeter = np.sum(np.linalg.norm(convex_hull_points[1:] - convex_hull_points[:-1], axis=1))
  19. # print("凸包的周长为:", perimeter)
  20. return perimeter
  21. # 提取切片周长
  22. def slide_mesh(mesh_,delta):
  23. min_z = np.min(mesh_.z)
  24. max_z = np.max(mesh_.z)
  25. print('min_z:', min_z, 'max_z:', max_z)#整个模型的最低点和最高点
  26. Content = []#当前的index情况下,所有的凸包的周长
  27. index = min_z
  28. time1 = time.time()
  29. x_for_plot = []
  30. while True:
  31. # print('start index:', index)
  32. Cross = []#当前的index情况下,所有的交点
  33. for mesh in mesh_:
  34. #给出当前的最低和最高的z
  35. # print('mesh:', mesh)
  36. z_min = np.min([mesh[2], mesh[5], mesh[8]])
  37. z_max = np.max([mesh[2], mesh[5], mesh[8]])
  38. if z_min <= index and z_max >= index:
  39. # print('z_min:', z_min, 'z_max:', z_max, 'index:', index)
  40. #如果当前的三角形与当前的平面有交点, 直接计算交点
  41. if index == mesh[2]:
  42. #如果当前的平面与三角形的一个点重合
  43. if not any(np.array_equal([mesh[0], mesh[1], mesh[2]], p) for p in Cross):
  44. Cross.append([mesh[0], mesh[1], mesh[2]])
  45. elif index == mesh[5]:
  46. if not any(np.array_equal([mesh[3], mesh[4], mesh[5]], p) for p in Cross):
  47. Cross.append([mesh[3], mesh[4], mesh[5]])
  48. elif index == mesh[8]:
  49. if not any(np.array_equal([mesh[6], mesh[7], mesh[8]], p) for p in Cross):
  50. Cross.append([mesh[6], mesh[7], mesh[8]])
  51. else:
  52. #计算交点
  53. #计算三角形的三个点,因为是网格,所以会有一半的重复点
  54. #print('三角形的三个点')
  55. p1 = np.array([mesh[0], mesh[1], mesh[2]])
  56. p2 = np.array([mesh[3], mesh[4], mesh[5]])
  57. p3 = np.array([mesh[6], mesh[7], mesh[8]])
  58. Lower = []
  59. Upper = []
  60. for p in [p1, p2, p3]:
  61. if p[2] < index:
  62. Lower.append(p)
  63. else:
  64. Upper.append(p)
  65. if len(Lower) == 2:
  66. #如果有两个点在下面,一个点在上面,则交换位置,包证有两个点在上面,一个点在下面
  67. R = Lower
  68. Lower = Upper
  69. Upper = R
  70. for m in Upper:
  71. x1 = m[0]
  72. y1 = m[1]
  73. z1 = m[2]
  74. x3 = Lower[0][0]
  75. y3 = Lower[0][1]
  76. z3 = Lower[0][2]
  77. S = (index - z3)/(z1 - z3)
  78. x2 = x3 + S*(x1 - x3)
  79. y2 = y3 + S*(y1 - y3)
  80. if not any(np.array_equal([x2, y2, index], p) for p in Cross):
  81. Cross.append([x2, y2, index])
  82. #绘制散点图
  83. X=[]
  84. Y=[]
  85. Points = []
  86. for p in Cross:
  87. X.append(p[0])
  88. Y.append(p[1])
  89. Points.append((p[0], p[1]))
  90. # print('Points:', len(Points))
  91. # plt.scatter(X, Y)
  92. # plt.axis('equal')
  93. # plt.show()
  94. # print(Points)
  95. if len(Points) > 2:
  96. #至少三个点,形成一个平面
  97. content = get_hull_and_content(np.array(Points))
  98. Content.append(content)
  99. x_for_plot.append(index)
  100. # print('Cross Number:', len(Cross))
  101. # print('end index:', index)
  102. if index == max_z:
  103. print('退出循环:',index, max_z)
  104. break
  105. else:
  106. time2 = time.time()
  107. print('当前index:', index, '已经运行时间:', time2 - time1)
  108. index = index+delta
  109. if index > max_z:
  110. index = max_z
  111. print('Content:', Content)
  112. #delta = 10 , 60s
  113. #delta = 5, 123s
  114. return x_for_plot, Content

四、结果

接下来的代码将上文的函数组合使用:

  1. # 读取 STL 文件
  2. my_mesh = mesh.Mesh.from_file('test.stl')
  3. my_mesh = rotation_(my_mesh)#旋转模型
  4. delta = 5 #切片的间隔,高度差
  5. x_for_plot, Content = slide_mesh(my_mesh,delta=delta)#计算切片周长
  6. #旋转后的文件名字为:my_rotated_model_surface.stl
  7. # 绘制周长的变化图
  8. plt.figure(dpi=200)
  9. x_max = np.max(x_for_plot)
  10. x_min = np.min(x_for_plot)
  11. X = [x - x_min for x in x_for_plot]
  12. # 绘制折线图
  13. plt.plot(X, Content)
  14. plt.xlabel('location') # 设置x轴标签
  15. plt.ylabel('content') # 设置y轴标签
  16. plt.title('Line Plot') # 设置图标题
  17. plt.show()
  18. #总用时: 192s

按照我们给定的切片间隔,将输出周长随高度变化的折线图:

从结果中可以看出,我们的算法基本上解决了这个问题。

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/795224
推荐阅读
相关标签
  

闽ICP备14008679号