flkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flkit/__init__.py +2 -0
- flkit/alg/__init__.py +68 -0
- flkit/alg/build_idx.py +9 -0
- flkit/alg/c_track.py +88 -0
- flkit/alg/collierydb.py +1217 -0
- flkit/alg/del2.py +421 -0
- flkit/alg/delaunay_triangulation.py +476 -0
- flkit/alg/finite_plane_distance.py +239 -0
- flkit/alg/get_up_low.py +32 -0
- flkit/alg/in_tin.py +139 -0
- flkit/alg/index.py +9 -0
- flkit/alg/inter/BasePredictor.py +69 -0
- flkit/alg/inter/Kriging.py +226 -0
- flkit/alg/inter/KrigingM.py +262 -0
- flkit/alg/inter/LossFuncs.py +164 -0
- flkit/alg/inter/__init__.py +178 -0
- flkit/alg/inter/idw.py +253 -0
- flkit/alg/inter/midw.py +145 -0
- flkit/alg/inter/nearest_neighbor.py +445 -0
- flkit/alg/inter/rbf.py +345 -0
- flkit/alg/inter/trilinear.py +572 -0
- flkit/alg/point_in_polygon.py +126 -0
- flkit/alg/point_in_volume.py +188 -0
- flkit/alg/point_in_volume_rs.pyi +7 -0
- flkit/alg/point_to_triangle.py +116 -0
- flkit/alg/point_to_triangle_mesh.py +309 -0
- flkit/alg/point_to_triangle_rs.pyi +9 -0
- flkit/alg/rotated_rect.py +184 -0
- flkit/db/__init__.py +17 -0
- flkit/db/db_model.py +724 -0
- flkit/db/duck_model.py +151 -0
- flkit/db/fileobj.py +72 -0
- flkit/db/path_utl.py +174 -0
- flkit/mtcli/__init__.py +18 -0
- flkit/mtcli/core.py +489 -0
- flkit/mtcli/parse.py +218 -0
- flkit/py.typed +0 -0
- flkit/tools/__init__.py +28 -0
- flkit/tools/del_key.py +13 -0
- flkit/tools/dict_to_list.py +10 -0
- flkit/tools/get_region.py +42 -0
- flkit/tools/max_key.py +7 -0
- flkit/tools/mongo.py +119 -0
- flkit/tools/mt_dash.py +121 -0
- flkit/tools/prase_type.py +27 -0
- flkit/typr/Res.py +21 -0
- flkit/typr/__init__.py +107 -0
- flkit/typr/c_grid.py +425 -0
- flkit/typr/console.py +283 -0
- flkit/typr/engine.py +59 -0
- flkit/typr/getenv.py +89 -0
- flkit/typr/result.py +81 -0
- flkit/typr/table.py +34 -0
- flkit/typr/utils.py +542 -0
- flkit/typr/verify.py +64 -0
- flkit/typr/webview.py +219 -0
- flkit/typr/wstask.py +344 -0
- flkit-0.1.0.dist-info/METADATA +186 -0
- flkit-0.1.0.dist-info/RECORD +60 -0
- flkit-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import numpy as np
|
|
3
|
+
import triangle as tr
|
|
4
|
+
from scipy.spatial import ConvexHull
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def convex_hull_boundary(points):
|
|
8
|
+
"""
|
|
9
|
+
使用凸包算法计算点云边界点
|
|
10
|
+
适用于寻找最外层的凸边界
|
|
11
|
+
"""
|
|
12
|
+
points = np.asarray(points)
|
|
13
|
+
hull = ConvexHull(points[:, :2])
|
|
14
|
+
boundary_points = points[hull.vertices]
|
|
15
|
+
return boundary_points.tolist()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def interpolate_delaunary_z(xy, triangle):
|
|
19
|
+
"""
|
|
20
|
+
计算 Delaunary 三角形内一点xy的 z 坐标。
|
|
21
|
+
|
|
22
|
+
参数:
|
|
23
|
+
triangle: 长度为3的列表或元组,表示三角形三个顶点的 (x, y, z)
|
|
24
|
+
xy: 长度为2的列表或元组,表示待求点的 (x, y)
|
|
25
|
+
返回:
|
|
26
|
+
z: 插值得到的 z 坐标
|
|
27
|
+
"""
|
|
28
|
+
x1, y1, z1 = triangle[0]
|
|
29
|
+
x2, y2, z2 = triangle[1]
|
|
30
|
+
x3, y3, z3 = triangle[2]
|
|
31
|
+
x, y = xy[:2]
|
|
32
|
+
|
|
33
|
+
# 计算两个边向量
|
|
34
|
+
ux, uy, uz = x2 - x1, y2 - y1, z2 - z1
|
|
35
|
+
vx, vy, vz = x3 - x1, y3 - y1, z3 - z1
|
|
36
|
+
|
|
37
|
+
# 计算法向量(叉积)
|
|
38
|
+
nx = uy * vz - uz * vy
|
|
39
|
+
ny = uz * vx - ux * vz
|
|
40
|
+
nz = ux * vy - uy * vx
|
|
41
|
+
|
|
42
|
+
# 检查三角形是否退化
|
|
43
|
+
if nx == 0 and ny == 0 and nz == 0:
|
|
44
|
+
raise ValueError("三点共线,无法构成三角形")
|
|
45
|
+
|
|
46
|
+
# 平面方程: nx*(X-x1) + ny*(Y-y1) + nz*(Z-z1) = 0
|
|
47
|
+
# 解出 Z
|
|
48
|
+
if nz == 0:
|
|
49
|
+
raise ValueError("三角形垂直于 xy 平面,无法由 xy 唯一确定 z")
|
|
50
|
+
z = z1 - (nx * (x - x1) + ny * (y - y1)) / nz
|
|
51
|
+
return z
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def bound_triangulation(points: List[List[float]]):
|
|
55
|
+
"""
|
|
56
|
+
构建边界三角网:采用扇形三角剖分(0, i, i+1)。
|
|
57
|
+
假设边界为凸多边形;若为非凸,多边形需另行剖分。
|
|
58
|
+
"""
|
|
59
|
+
n = len(points)
|
|
60
|
+
if n < 3:
|
|
61
|
+
return []
|
|
62
|
+
triangles = []
|
|
63
|
+
for i in range(1, n - 1):
|
|
64
|
+
triangles.append([0, i, i + 1])
|
|
65
|
+
return triangles
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def point_in_polygon(point, polygon, epsilon=1e-12):
|
|
69
|
+
"""
|
|
70
|
+
简洁的射线法(奇偶规则),返回:
|
|
71
|
+
1 -> 内部
|
|
72
|
+
0 -> 边缘
|
|
73
|
+
-1 -> 外部
|
|
74
|
+
|
|
75
|
+
:param point: 点坐标
|
|
76
|
+
:param polygon: 多边形顶点坐标列表
|
|
77
|
+
:param epsilon: 浮点数精度
|
|
78
|
+
:return: 1/0/-1, (边索引1, 边索引2)
|
|
79
|
+
"""
|
|
80
|
+
x, y = point[:2]
|
|
81
|
+
n = len(polygon)
|
|
82
|
+
|
|
83
|
+
# 先判断是否在边界上
|
|
84
|
+
for i in range(n):
|
|
85
|
+
x1, y1 = polygon[i][:2]
|
|
86
|
+
x2, y2 = polygon[(i + 1) % n][:2]
|
|
87
|
+
|
|
88
|
+
cross = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1)
|
|
89
|
+
if (
|
|
90
|
+
abs(cross) < epsilon
|
|
91
|
+
and min(x1, x2) - epsilon <= x <= max(x1, x2) + epsilon
|
|
92
|
+
and min(y1, y2) - epsilon <= y <= max(y1, y2) + epsilon
|
|
93
|
+
):
|
|
94
|
+
# 返回该点所在的边索引
|
|
95
|
+
return 0, (i, (i + 1) % n)
|
|
96
|
+
|
|
97
|
+
# 射线法判断内外
|
|
98
|
+
inside = False
|
|
99
|
+
for i in range(n):
|
|
100
|
+
x1, y1 = polygon[i][:2]
|
|
101
|
+
x2, y2 = polygon[(i + 1) % n][:2]
|
|
102
|
+
if (y1 > y) != (y2 > y):
|
|
103
|
+
x_intersect = (x2 - x1) * (y - y1) / (y2 - y1) + x1
|
|
104
|
+
if x_intersect > x:
|
|
105
|
+
inside = not inside
|
|
106
|
+
|
|
107
|
+
return (1, (-1, -1)) if inside else (-1, (-1, -1))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class DelaunayTriangulation:
|
|
111
|
+
"""
|
|
112
|
+
Delaunay三角剖分类
|
|
113
|
+
|
|
114
|
+
>>> dt = DelaunayTriangulation([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])
|
|
115
|
+
>>> dt.add_point([0.5, 0.5])
|
|
116
|
+
True
|
|
117
|
+
>>> dt.triangles
|
|
118
|
+
[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 1], [1, 2, 5], [2, 3, 5], [3, 4, 5]]
|
|
119
|
+
>>> dt.add_point([0.25, 0.25])
|
|
120
|
+
True
|
|
121
|
+
>>> dt.triangles
|
|
122
|
+
[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 1], [1, 2, 5], [2, 3, 5], [3, 4, 5]]
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, bound: List[List[float]]):
|
|
126
|
+
"""
|
|
127
|
+
初始化Delaunay三角网
|
|
128
|
+
|
|
129
|
+
:param bound: 边界多边形顶点坐标列表
|
|
130
|
+
:param points: 内部点坐标列表
|
|
131
|
+
|
|
132
|
+
>>> dt = DelaunayTriangulation([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])
|
|
133
|
+
>>> dt.add_point([0.5, 0.5])
|
|
134
|
+
True
|
|
135
|
+
"""
|
|
136
|
+
self.bound = bound
|
|
137
|
+
self.points = list(bound)
|
|
138
|
+
self.triangles = bound_triangulation(bound)
|
|
139
|
+
|
|
140
|
+
def add_point(self, point: List[float]):
|
|
141
|
+
"""
|
|
142
|
+
添加内部点
|
|
143
|
+
|
|
144
|
+
:param point: 内部点坐标
|
|
145
|
+
"""
|
|
146
|
+
# 先判断点是否在边界上
|
|
147
|
+
sign, _ = point_in_polygon(point, self.bound)
|
|
148
|
+
if sign == -1:
|
|
149
|
+
return False
|
|
150
|
+
# 检查点是否已存在
|
|
151
|
+
if point in self.points:
|
|
152
|
+
return False
|
|
153
|
+
self.points.append(point)
|
|
154
|
+
n_p_idx = len(self.points) - 1
|
|
155
|
+
# 查找包含点的三角形
|
|
156
|
+
# 如果点在边界上则将三角形拆分为2个三角形
|
|
157
|
+
for _, triangle in enumerate(self.triangles):
|
|
158
|
+
sign, edge = point_in_polygon(point, [self.points[idx] for idx in triangle])
|
|
159
|
+
if sign == 1:
|
|
160
|
+
# 点在内部,删除包含点的三角形,将其拆分为3个三角形
|
|
161
|
+
self.triangles.remove(triangle)
|
|
162
|
+
self.triangles.append([triangle[0], triangle[1], n_p_idx])
|
|
163
|
+
self.triangles.append([triangle[1], triangle[2], n_p_idx])
|
|
164
|
+
self.triangles.append([triangle[2], triangle[0], n_p_idx])
|
|
165
|
+
return True
|
|
166
|
+
elif sign == 0:
|
|
167
|
+
# 点在边界上,删除包含点的三角形,将其拆分为2个三角形
|
|
168
|
+
# 找到点所在的线的位置
|
|
169
|
+
self.triangles.remove(triangle)
|
|
170
|
+
if edge == (0, 1):
|
|
171
|
+
# 在0-1之间,则拆分为1-2-n_p_idx;2-0-n_p_idx;
|
|
172
|
+
self.triangles.append([triangle[1], triangle[2], n_p_idx])
|
|
173
|
+
self.triangles.append([triangle[2], triangle[0], n_p_idx])
|
|
174
|
+
elif edge == (1, 2):
|
|
175
|
+
# 在1-2之间,则拆分为0-1-n_p_idx;2-0-n_p_idx;
|
|
176
|
+
self.triangles.append([triangle[0], triangle[1], n_p_idx])
|
|
177
|
+
self.triangles.append([triangle[2], triangle[0], n_p_idx])
|
|
178
|
+
elif edge == (2, 0):
|
|
179
|
+
# 在2-0之间,则拆分为0-1-n_p_idx;1-2-n_p_idx;
|
|
180
|
+
self.triangles.append([triangle[0], triangle[1], n_p_idx])
|
|
181
|
+
self.triangles.append([triangle[1], triangle[2], n_p_idx])
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def add_points(self, points: List[List[float]]):
|
|
185
|
+
"""
|
|
186
|
+
添加内部点
|
|
187
|
+
|
|
188
|
+
:param points: 内部点坐标列表
|
|
189
|
+
"""
|
|
190
|
+
for point in points:
|
|
191
|
+
self.add_point(point)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class OptimizedDelaunayTriangulation(DelaunayTriangulation):
|
|
195
|
+
"""
|
|
196
|
+
优化版 Delaunay 三角剖分类(继承自 DelaunayTriangulation)
|
|
197
|
+
|
|
198
|
+
在添加新点后,进行局部的边翻转优化,尽量减少钝角三角形。
|
|
199
|
+
优化策略基于“最大角度下降”原则:若翻转共享边后,两个相邻三角形的最大角度减小,则执行翻转。
|
|
200
|
+
|
|
201
|
+
用法示例:
|
|
202
|
+
>>> dt = OptimizedDelaunayTriangulation([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])
|
|
203
|
+
>>> dt.add_point([0.5, 0.5])
|
|
204
|
+
True
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def add_point(self, point: List[float]):
|
|
208
|
+
"""
|
|
209
|
+
添加内部点,并在添加后对新点邻域进行局部优化,减少钝角三角形。
|
|
210
|
+
"""
|
|
211
|
+
inserted = super().add_point(point)
|
|
212
|
+
if not inserted:
|
|
213
|
+
return False
|
|
214
|
+
n_p_idx = len(self.points) - 1
|
|
215
|
+
self._optimize_around_point(n_p_idx)
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
# ---------- 以下为局部优化所需的辅助函数 ----------
|
|
219
|
+
def _xy(self, idx):
|
|
220
|
+
p = self.points[idx]
|
|
221
|
+
return float(p[0]), float(p[1])
|
|
222
|
+
|
|
223
|
+
def _angle_at(self, a_idx, b_idx, c_idx, eps=1e-12):
|
|
224
|
+
import math
|
|
225
|
+
|
|
226
|
+
ax, ay = self._xy(a_idx)
|
|
227
|
+
bx, by = self._xy(b_idx)
|
|
228
|
+
cx, cy = self._xy(c_idx)
|
|
229
|
+
v1x, v1y = ax - bx, ay - by
|
|
230
|
+
v2x, v2y = cx - bx, cy - by
|
|
231
|
+
n1 = (v1x * v1x + v1y * v1y) ** 0.5
|
|
232
|
+
n2 = (v2x * v2x + v2y * v2y) ** 0.5
|
|
233
|
+
if n1 < eps or n2 < eps:
|
|
234
|
+
return 0.0
|
|
235
|
+
cos_val = (v1x * v2x + v1y * v2y) / (n1 * n2)
|
|
236
|
+
if cos_val < -1.0:
|
|
237
|
+
cos_val = -1.0
|
|
238
|
+
elif cos_val > 1.0:
|
|
239
|
+
cos_val = 1.0
|
|
240
|
+
return math.degrees(math.acos(cos_val))
|
|
241
|
+
|
|
242
|
+
def _max_angle(self, tri):
|
|
243
|
+
a, b, c = tri
|
|
244
|
+
return max(
|
|
245
|
+
self._angle_at(b, a, c),
|
|
246
|
+
self._angle_at(a, b, c),
|
|
247
|
+
self._angle_at(a, c, b),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _find_adjacent_triangle_edge(self, i, j, exclude_triangle):
|
|
251
|
+
# 查找共享边(i, j)的另一侧三角形
|
|
252
|
+
for t in self.triangles:
|
|
253
|
+
if t is exclude_triangle:
|
|
254
|
+
continue
|
|
255
|
+
if i in t and j in t:
|
|
256
|
+
return t
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def _degenerate(self, tri, eps=1e-12):
|
|
260
|
+
ax, ay = self._xy(tri[0])
|
|
261
|
+
bx, by = self._xy(tri[1])
|
|
262
|
+
cx, cy = self._xy(tri[2])
|
|
263
|
+
area2 = abs((bx - ax) * (cy - ay) - (by - ay) * (cx - ax))
|
|
264
|
+
return area2 < eps
|
|
265
|
+
|
|
266
|
+
def _should_flip_by_angle(self, p, a, b, c, margin=1e-9):
|
|
267
|
+
# 翻转判据:翻转后两个三角形的“最大角”更小
|
|
268
|
+
old1 = [p, a, b]
|
|
269
|
+
old2 = [p, a, c]
|
|
270
|
+
new1 = [p, b, c]
|
|
271
|
+
new2 = [a, b, c]
|
|
272
|
+
# 避免翻转后出现退化三角形
|
|
273
|
+
if self._degenerate(new1) or self._degenerate(new2):
|
|
274
|
+
return False
|
|
275
|
+
old_max = max(self._max_angle(old1), self._max_angle(old2))
|
|
276
|
+
new_max = max(self._max_angle(new1), self._max_angle(new2))
|
|
277
|
+
return new_max + margin < old_max
|
|
278
|
+
|
|
279
|
+
def _optimize_around_point(self, p_idx, max_iterations=100):
|
|
280
|
+
"""
|
|
281
|
+
以新插入点为中心,对其相邻边执行局部边翻转,
|
|
282
|
+
若翻转能降低相邻两三角形的最大角度,则执行翻转。
|
|
283
|
+
"""
|
|
284
|
+
iterations = 0
|
|
285
|
+
changed = True
|
|
286
|
+
while changed and iterations < max_iterations:
|
|
287
|
+
iterations += 1
|
|
288
|
+
changed = False
|
|
289
|
+
triangles_with_p = [t for t in self.triangles if p_idx in t]
|
|
290
|
+
for t1 in triangles_with_p:
|
|
291
|
+
# t1 = [p_idx, a, b]
|
|
292
|
+
ab = [v for v in t1 if v != p_idx]
|
|
293
|
+
if len(ab) != 2:
|
|
294
|
+
continue
|
|
295
|
+
for a in ab:
|
|
296
|
+
b = ab[0] if a == ab[1] else ab[1]
|
|
297
|
+
neighbor = self._find_adjacent_triangle_edge(
|
|
298
|
+
p_idx, a, exclude_triangle=t1
|
|
299
|
+
)
|
|
300
|
+
if neighbor is None:
|
|
301
|
+
continue
|
|
302
|
+
# neighbor = [p_idx, a, c]
|
|
303
|
+
c_candidates = [v for v in neighbor if v != p_idx and v != a]
|
|
304
|
+
if len(c_candidates) != 1:
|
|
305
|
+
continue
|
|
306
|
+
c = c_candidates[0]
|
|
307
|
+
if self._should_flip_by_angle(p_idx, a, b, c):
|
|
308
|
+
# 执行边翻转:移除旧三角形,加入新三角形
|
|
309
|
+
try:
|
|
310
|
+
self.triangles.remove(t1)
|
|
311
|
+
except ValueError:
|
|
312
|
+
pass
|
|
313
|
+
try:
|
|
314
|
+
self.triangles.remove(neighbor)
|
|
315
|
+
except ValueError:
|
|
316
|
+
pass
|
|
317
|
+
self.triangles.append([p_idx, b, c])
|
|
318
|
+
self.triangles.append([a, b, c])
|
|
319
|
+
changed = True
|
|
320
|
+
break
|
|
321
|
+
if changed:
|
|
322
|
+
break
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class TriangleLibTriangulation:
|
|
326
|
+
"""
|
|
327
|
+
基于Triangle库的Delaunay三角剖分。
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(self, bound):
|
|
331
|
+
"""
|
|
332
|
+
初始化三角网。
|
|
333
|
+
|
|
334
|
+
参数:
|
|
335
|
+
boundary: 边界点列表,格式为[(x1,y1), (x2,y2), ...]
|
|
336
|
+
"""
|
|
337
|
+
# 使用统一的坐标键(四舍五入)避免浮点误差导致的键不匹配
|
|
338
|
+
self._key_precision = 8
|
|
339
|
+
self.ori_bound = {self._round_key(b): b for b in bound}
|
|
340
|
+
self.ori_points = {}
|
|
341
|
+
self.__points = []
|
|
342
|
+
self.__triangles = []
|
|
343
|
+
|
|
344
|
+
def _round_key(self, p):
|
|
345
|
+
return (
|
|
346
|
+
round(float(p[0]), self._key_precision),
|
|
347
|
+
round(float(p[1]), self._key_precision),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def unique_points(self, points):
|
|
351
|
+
"""
|
|
352
|
+
去除点云中的重复点。
|
|
353
|
+
|
|
354
|
+
参数:
|
|
355
|
+
points: 点云数据列表 [[x1,y1], [x2,y2], ...]
|
|
356
|
+
"""
|
|
357
|
+
seen = set()
|
|
358
|
+
unique_pts = []
|
|
359
|
+
for p in points:
|
|
360
|
+
key = (float(p[0]), float(p[1]))
|
|
361
|
+
if key not in seen:
|
|
362
|
+
seen.add(key)
|
|
363
|
+
unique_pts.append(p)
|
|
364
|
+
return unique_pts
|
|
365
|
+
|
|
366
|
+
def group_unique(self, target, source):
|
|
367
|
+
"""去除target中与source重复的点"""
|
|
368
|
+
seen = set((float(p[0]), float(p[1])) for p in source)
|
|
369
|
+
return [p for p in target if (float(p[0]), float(p[1])) not in seen]
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def bound(self):
|
|
373
|
+
# 返回原始三维边界点(带 z),而不是键
|
|
374
|
+
return [pt for pt in self.ori_bound.values()]
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def points(self):
|
|
378
|
+
all_points = []
|
|
379
|
+
# 快速访问映射(边界+内部)
|
|
380
|
+
lookup = {}
|
|
381
|
+
lookup.update(self.ori_bound)
|
|
382
|
+
lookup.update(self.ori_points)
|
|
383
|
+
for idx, p in enumerate(self.__points):
|
|
384
|
+
key_point = self._round_key(p)
|
|
385
|
+
point = lookup.get(key_point)
|
|
386
|
+
# print(p, key_point, point)
|
|
387
|
+
if point is None:
|
|
388
|
+
raise ValueError(f"{point} 不在点集中")
|
|
389
|
+
all_points.append(point)
|
|
390
|
+
return all_points
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def triangles(self):
|
|
394
|
+
return self.__triangles
|
|
395
|
+
|
|
396
|
+
def add_points(self, points):
|
|
397
|
+
"""
|
|
398
|
+
添加内部点云数据。
|
|
399
|
+
|
|
400
|
+
参数:
|
|
401
|
+
points: 内部点云数据列表 [[x1,y1], [x2,y2], ...]
|
|
402
|
+
"""
|
|
403
|
+
points = self.unique_points(points)
|
|
404
|
+
self.ori_points = {self._round_key(p): p for p in points}
|
|
405
|
+
points_new = []
|
|
406
|
+
polygon = [key for key in self.ori_bound]
|
|
407
|
+
if polygon[0] != polygon[-1]:
|
|
408
|
+
polygon.append(polygon[0])
|
|
409
|
+
for p, value in self.ori_points.items():
|
|
410
|
+
if point_in_polygon(p, polygon)[0] not in [0, 1]:
|
|
411
|
+
continue
|
|
412
|
+
points_new.append((p, value))
|
|
413
|
+
self.ori_points = {p[0]: p[1] for p in points_new if p[0] not in self.ori_bound}
|
|
414
|
+
|
|
415
|
+
boundary_2d = np.array([key for key in self.ori_bound], dtype=float)
|
|
416
|
+
cloud_2d = np.array([key for key in self.ori_points], dtype=float)
|
|
417
|
+
|
|
418
|
+
result = self.point_cloud_triangulation(boundary_2d, cloud_2d)
|
|
419
|
+
|
|
420
|
+
vertices = result.get("vertices")
|
|
421
|
+
triangles = result.get("triangles", [])
|
|
422
|
+
if vertices is None:
|
|
423
|
+
vertices = np.vstack([boundary_2d, cloud_2d])
|
|
424
|
+
self.__points = vertices.tolist()
|
|
425
|
+
self.__triangles = (
|
|
426
|
+
triangles.tolist() if isinstance(triangles, np.ndarray) else triangles
|
|
427
|
+
)
|
|
428
|
+
return result
|
|
429
|
+
|
|
430
|
+
def point_cloud_triangulation(
|
|
431
|
+
self, boundary_points, cloud_points, quality_params="pq20a0.001", switches="p"
|
|
432
|
+
):
|
|
433
|
+
"""
|
|
434
|
+
对点云数据生成带边界约束的三角网
|
|
435
|
+
|
|
436
|
+
参数:
|
|
437
|
+
boundary_points: 边界点列表 [[x1,y1], [x2,y2], ...]
|
|
438
|
+
cloud_points: 内部点云数据列表
|
|
439
|
+
quality_params: 三角化质量参数
|
|
440
|
+
"""
|
|
441
|
+
# 合并所有点
|
|
442
|
+
boundary_points = np.array(boundary_points, dtype=float)
|
|
443
|
+
cloud_points = np.array(cloud_points, dtype=float)
|
|
444
|
+
if cloud_points.size == 0:
|
|
445
|
+
all_points = boundary_points
|
|
446
|
+
else:
|
|
447
|
+
all_points = np.vstack([boundary_points, cloud_points])
|
|
448
|
+
n_boundary = len(boundary_points)
|
|
449
|
+
|
|
450
|
+
# 创建边界段(连接边界点)
|
|
451
|
+
segments = []
|
|
452
|
+
for i in range(n_boundary):
|
|
453
|
+
segments.append([i, (i + 1) % n_boundary]) # 确保边界闭合
|
|
454
|
+
|
|
455
|
+
# 准备输入数据
|
|
456
|
+
# Normalize coordinates to improve numerical stability
|
|
457
|
+
bbox_min = np.min(all_points, axis=0)
|
|
458
|
+
bbox_max = np.max(all_points, axis=0)
|
|
459
|
+
center = (bbox_min + bbox_max) / 2.0
|
|
460
|
+
scale = max(bbox_max[0] - bbox_min[0], bbox_max[1] - bbox_min[1])
|
|
461
|
+
if scale == 0:
|
|
462
|
+
scale = 1.0
|
|
463
|
+
norm_points = (all_points - center) / scale
|
|
464
|
+
mesh_data = {"vertices": norm_points, "segments": segments}
|
|
465
|
+
# 执行三角化(带约束和质量控制)
|
|
466
|
+
# 组合开关:p(约束) + 质量参数 + D(Delaunay)
|
|
467
|
+
switches = switches
|
|
468
|
+
result = tr.triangulate(mesh_data, switches)
|
|
469
|
+
|
|
470
|
+
# De-normalize vertices back to original coordinate scale
|
|
471
|
+
verts = result.get("vertices")
|
|
472
|
+
if verts is not None:
|
|
473
|
+
verts = np.asarray(verts, dtype=float)
|
|
474
|
+
result["vertices"] = verts * scale + center
|
|
475
|
+
|
|
476
|
+
return result
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
有限平面距离计算模块
|
|
3
|
+
处理四个三维点确定的有限平面,计算点到平面的距离和位置判断
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Tuple, Optional
|
|
9
|
+
from scipy.spatial import ConvexHull
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def comp_sliding_diff(d_h: float, angle: float, qj: float) -> list[float]:
|
|
13
|
+
"""
|
|
14
|
+
计算点沿倾斜平面移动后的XY坐标差值
|
|
15
|
+
|
|
16
|
+
根据给定的高差、倾向角和倾角,计算点在平面上的位移。
|
|
17
|
+
适用于地质断层分析、滑坡计算等应用场景。
|
|
18
|
+
"""
|
|
19
|
+
d_x = d_h * math.tan(qj / 180 * math.pi) * math.sin(angle / 180 * math.pi)
|
|
20
|
+
d_y = d_h * math.tan(qj / 180 * math.pi) * math.cos(angle / 180 * math.pi)
|
|
21
|
+
|
|
22
|
+
return [d_x, d_y]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def finite_plane_distance(
|
|
26
|
+
point: np.ndarray, plane_points: np.ndarray, check_projection: bool = True
|
|
27
|
+
) -> Tuple[float, int, bool, Optional[np.ndarray]]:
|
|
28
|
+
"""
|
|
29
|
+
计算点到有限平面的距离和位置关系
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
point: 目标点 [x, y, z]
|
|
33
|
+
plane_points: 确定平面的四个点,形状为 (4, 3)
|
|
34
|
+
check_projection: 是否检查投影点是否在平面范围内
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple[float, int, bool, Optional[np.ndarray]]:
|
|
38
|
+
- 垂直距离: 点到平面的垂直距离
|
|
39
|
+
- 侧面标识: 1表示正法向量方向,-1表示负法向量方向,0表示在平面上
|
|
40
|
+
- 在范围内: 投影点是否在有限平面范围内
|
|
41
|
+
- 投影点: 点在平面上的投影坐标,如果不在范围内则为None
|
|
42
|
+
"""
|
|
43
|
+
if len(plane_points) != 4:
|
|
44
|
+
raise ValueError("需要恰好4个点来确定有限平面")
|
|
45
|
+
|
|
46
|
+
# 1. 计算平面方程
|
|
47
|
+
plane_normal, plane_d = _calculate_plane_equation(plane_points)
|
|
48
|
+
|
|
49
|
+
# 2. 计算垂直距离和侧面
|
|
50
|
+
vertical_distance, side = _calculate_vertical_distance(point, plane_normal, plane_d)
|
|
51
|
+
|
|
52
|
+
# 3. 计算投影点
|
|
53
|
+
projection_point = _calculate_projection_point(point, plane_normal, plane_d)
|
|
54
|
+
|
|
55
|
+
# 4. 检查投影点是否在有限平面范围内
|
|
56
|
+
if check_projection:
|
|
57
|
+
is_inside = _is_point_inside_finite_plane(
|
|
58
|
+
projection_point, plane_points, plane_normal
|
|
59
|
+
)
|
|
60
|
+
if not is_inside:
|
|
61
|
+
projection_point = None
|
|
62
|
+
else:
|
|
63
|
+
is_inside = True
|
|
64
|
+
|
|
65
|
+
return vertical_distance, side, is_inside, projection_point
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def finite_plane_closest_distance(
|
|
69
|
+
point: np.ndarray, plane_points: np.ndarray
|
|
70
|
+
) -> Tuple[float, np.ndarray, str]:
|
|
71
|
+
"""计算点到有限平面的最短距离(考虑边界)"""
|
|
72
|
+
vertical_distance, side, is_inside, projection_point = finite_plane_distance(
|
|
73
|
+
point, plane_points, check_projection=True
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if is_inside:
|
|
77
|
+
return vertical_distance, projection_point, "vertical"
|
|
78
|
+
min_distance = float("inf")
|
|
79
|
+
closest_point = None
|
|
80
|
+
distance_type = "edge"
|
|
81
|
+
|
|
82
|
+
for i in range(4):
|
|
83
|
+
j = (i + 1) % 4
|
|
84
|
+
edge_start = plane_points[i]
|
|
85
|
+
edge_end = plane_points[j]
|
|
86
|
+
|
|
87
|
+
# 计算点到线段的距离
|
|
88
|
+
edge_distance, edge_closest = _point_to_line_segment_distance(
|
|
89
|
+
point, edge_start, edge_end
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if edge_distance < min_distance:
|
|
93
|
+
min_distance = edge_distance
|
|
94
|
+
closest_point = edge_closest
|
|
95
|
+
distance_type = "edge"
|
|
96
|
+
|
|
97
|
+
for vertex in plane_points:
|
|
98
|
+
vertex_distance = np.linalg.norm(point - vertex)
|
|
99
|
+
if vertex_distance < min_distance:
|
|
100
|
+
min_distance = vertex_distance
|
|
101
|
+
closest_point = vertex
|
|
102
|
+
distance_type = "vertex"
|
|
103
|
+
|
|
104
|
+
return round(min_distance, 3), closest_point, distance_type
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _calculate_plane_equation(plane_points: np.ndarray) -> Tuple[np.ndarray, float]:
|
|
108
|
+
"""计算平面方程的法向量和常数项"""
|
|
109
|
+
p1, p2, p3 = plane_points[0], plane_points[1], plane_points[2]
|
|
110
|
+
|
|
111
|
+
v1 = p2 - p1
|
|
112
|
+
v2 = p3 - p1
|
|
113
|
+
|
|
114
|
+
normal = np.cross(v1, v2)
|
|
115
|
+
|
|
116
|
+
if np.allclose(normal, 0):
|
|
117
|
+
raise ValueError("给定的点共线,无法确定平面")
|
|
118
|
+
|
|
119
|
+
normal = normal / np.linalg.norm(normal)
|
|
120
|
+
|
|
121
|
+
d = -np.dot(normal, p1)
|
|
122
|
+
|
|
123
|
+
return normal, d
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _calculate_vertical_distance(
|
|
127
|
+
point: np.ndarray, plane_normal: np.ndarray, plane_d: float
|
|
128
|
+
) -> Tuple[float, int]:
|
|
129
|
+
"""计算点到平面的垂直距离和侧面"""
|
|
130
|
+
signed_distance = np.dot(plane_normal, point) + plane_d
|
|
131
|
+
|
|
132
|
+
vertical_distance = abs(signed_distance)
|
|
133
|
+
|
|
134
|
+
if abs(signed_distance) < 1e-10:
|
|
135
|
+
side = 0
|
|
136
|
+
elif signed_distance > 0:
|
|
137
|
+
side = 1
|
|
138
|
+
else:
|
|
139
|
+
side = -1
|
|
140
|
+
|
|
141
|
+
return round(vertical_distance, 3), side
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _calculate_projection_point(
|
|
145
|
+
point: np.ndarray, plane_normal: np.ndarray, plane_d: float
|
|
146
|
+
) -> np.ndarray:
|
|
147
|
+
"""计算点在平面上的投影"""
|
|
148
|
+
signed_distance = np.dot(plane_normal, point) + plane_d
|
|
149
|
+
|
|
150
|
+
projection = point - signed_distance * plane_normal
|
|
151
|
+
|
|
152
|
+
return projection
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_point_inside_finite_plane(
|
|
156
|
+
point: np.ndarray, plane_points: np.ndarray, plane_normal: np.ndarray
|
|
157
|
+
) -> bool:
|
|
158
|
+
"""判断点是否在有限平面范围内"""
|
|
159
|
+
try:
|
|
160
|
+
if abs(plane_normal[2]) < 0.9:
|
|
161
|
+
u = np.cross(plane_normal, [0, 0, 1])
|
|
162
|
+
else:
|
|
163
|
+
u = np.cross(plane_normal, [1, 0, 0])
|
|
164
|
+
u = u / np.linalg.norm(u)
|
|
165
|
+
v = np.cross(plane_normal, u)
|
|
166
|
+
v = v / np.linalg.norm(v)
|
|
167
|
+
|
|
168
|
+
plane_2d = np.column_stack([np.dot(plane_points, u), np.dot(plane_points, v)])
|
|
169
|
+
point_2d = np.array([np.dot(point, u), np.dot(point, v)])
|
|
170
|
+
|
|
171
|
+
hull = ConvexHull(plane_2d)
|
|
172
|
+
return _point_in_convex_polygon(point_2d, plane_2d[hull.vertices])
|
|
173
|
+
|
|
174
|
+
except Exception:
|
|
175
|
+
return _point_in_quadrilateral_simple(point, plane_points, plane_normal)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _point_in_convex_polygon(point: np.ndarray, polygon: np.ndarray) -> bool:
|
|
179
|
+
"""判断点是否在凸多边形内(2D)"""
|
|
180
|
+
n = len(polygon)
|
|
181
|
+
for i in range(n):
|
|
182
|
+
j = (i + 1) % n
|
|
183
|
+
cross = np.cross(polygon[j] - polygon[i], point - polygon[i])
|
|
184
|
+
if cross < 0:
|
|
185
|
+
return False
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _point_in_quadrilateral_simple(
|
|
190
|
+
point: np.ndarray, plane_points: np.ndarray, plane_normal: np.ndarray
|
|
191
|
+
) -> bool:
|
|
192
|
+
"""简单的四边形内点判断"""
|
|
193
|
+
signs = []
|
|
194
|
+
|
|
195
|
+
for i in range(4):
|
|
196
|
+
j = (i + 1) % 4
|
|
197
|
+
edge_vector = plane_points[j] - plane_points[i]
|
|
198
|
+
to_point = point - plane_points[i]
|
|
199
|
+
|
|
200
|
+
cross = np.cross(edge_vector, to_point)
|
|
201
|
+
sign = np.dot(cross, plane_normal)
|
|
202
|
+
signs.append(sign)
|
|
203
|
+
|
|
204
|
+
positive = sum(1 for s in signs if s > 1e-10)
|
|
205
|
+
negative = sum(1 for s in signs if s < -1e-10)
|
|
206
|
+
|
|
207
|
+
return positive == 0 or negative == 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _point_to_line_segment_distance(
|
|
211
|
+
point: np.ndarray, line_start: np.ndarray, line_end: np.ndarray
|
|
212
|
+
) -> Tuple[float, np.ndarray]:
|
|
213
|
+
"""计算点到线段的最短距离"""
|
|
214
|
+
line_vector = line_end - line_start
|
|
215
|
+
line_length_sq = np.dot(line_vector, line_vector)
|
|
216
|
+
|
|
217
|
+
if line_length_sq < 1e-10:
|
|
218
|
+
return np.linalg.norm(point - line_start), line_start
|
|
219
|
+
|
|
220
|
+
t = np.dot(point - line_start, line_vector) / line_length_sq
|
|
221
|
+
|
|
222
|
+
t = max(0, min(1, t))
|
|
223
|
+
|
|
224
|
+
closest_point = line_start + t * line_vector
|
|
225
|
+
|
|
226
|
+
distance = np.linalg.norm(point - closest_point)
|
|
227
|
+
|
|
228
|
+
return distance, closest_point
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def batch_finite_plane_distance(
|
|
232
|
+
points: np.ndarray, plane_points: np.ndarray, check_projection: bool = True
|
|
233
|
+
) -> list:
|
|
234
|
+
"""批量计算多个点到有限平面的距离"""
|
|
235
|
+
results = []
|
|
236
|
+
for point in points:
|
|
237
|
+
result = finite_plane_distance(point, plane_points, check_projection)
|
|
238
|
+
results.append(result)
|
|
239
|
+
return results
|