繁体   English   中英

将曲线样条拟合到 3D 点云

[英]Fit Curve-Spline to 3D Point Cloud

客观的

我有一个 3D facet 模型(例如 .off 文件),例如它看起来像一个管道(见示例图片)。 目标是使用 python 推导出表示该管的 3D 骨架的近似样条(直线和样条的最佳组合)。

最先进的

同一领域的 Stackoverflow 帖子:

一般的:

我的方法(到目前为止)

从示例 facet 模型(图 1)开始,我使用 python 包将 3d 模型转换为点云(图 2)。 该点云可用于体素化表示(图 3)。 因此,这三种类型的数据是我的起点。

基本上,这个问题对我来说似乎并不复杂,但是我缺少一个起始逻辑。 大多数研究论文对于各种更深入的任务来说都过于复杂了。 一种想法是进行 PCA 以导出组件的主轴,并沿这些轴进行扫描。 然而,这似乎并没有以一种高性能的方式产生好的结果。 另一个想法是使用体素化网格并检测由于体素邻接而产生的路径。 另一个想法是使用 KD-Tree 来评估最近的点,以检测正确的平面,通过它们的平面法线定义样条方向。

我尝试的一种方法是从点云中选择 N 个随机点并搜索半径内的所有邻居 (cKDTree.query_ball_point)。 我计算了所有相邻点的中心。 这导致了图像 4 中的结果。结果似乎与第一种方法一样好,但它或多或少是半径参数的调整。

图 1:

初始点

图 2: 点云

图 3: 体素网格

图 4: 在此处输入图片说明

Delaunay/Voronoi 方法可用于此问题,因为中轴是 Voronoi 图的子图(例如,参见 Attali、Boissonnat 和 Edelsbrunner 的这篇论文)。

在下文中,我将演示从小半径 10 和大半径 100 的四分之一圆环表面采样的点的示例(中间路径/骨架从点 (100, 0, 0) 开始,到 (0, 100) 结束) , 0))。

Voronoi 图是 3D Delaunay 四面体化的对偶(从现在开始我将使用术语三角剖分)。 可以使用 scipy 的scipy.spatial.Delaunay包计算 Delaunay 三角剖分。

下图是样本点(在本例中为 200)及其完整的 Delaunay 三角剖分(三角剖分是使用此处的函数绘制的)。 全三角剖分

对应于 Delaunay 四面体的 Voronoi 顶点是四面体外接球面的中心。 以下是计算这些 Delaunay 中心的函数,它是我之前在此处的答案中对 2D 函数的扩展。

def compute_delaunay_tetra_circumcenters(dt):
"""
Compute the centers of the circumscribing circle of each tetrahedron in the Delaunay triangulation.
:param dt: the Delaunay triangulation
:return: array of xyz points
"""
simp_pts = dt.points[dt.simplices]
# (n, 4, 3) array of tetrahedra points where simp_pts[i, j, :] holds the j'th 3D point (of four) of the i'th tetrahedron
assert simp_pts.shape[1] == 4 and simp_pts.shape[2] == 3

# finding the circumcenter (x, y, z) of a simplex defined by four points:
# (x-x0)**2 + (y-y0)**2 + (z-z0)**2 = (x-x1)**2 + (y-y1)**2 + (z-z1)**2
# (x-x0)**2 + (y-y0)**2 + (z-z0)**2 = (x-x2)**2 + (y-y2)**2 + (z-z2)**2
# (x-x0)**2 + (y-y0)**2 + (z-z0)**2 = (x-x3)**2 + (y-y3)**2 + (z-z3)**2
# becomes three linear equations (squares are canceled):
# 2(x1-x0)*x + 2(y1-y0)*y + 2(z1-z0)*y = (x1**2 + y1**2 + z1**2) - (x0**2 + y0**2 + z0**2)
# 2(x2-x0)*x + 2(y2-y0)*y + 2(z2-z0)*y = (x2**2 + y2**2 + z2**2) - (x0**2 + y0**2 + z0**2)
# 2(x3-x0)*x + 2(y3-y0)*y + 2(z3-z0)*y = (x3**2 + y3**2 + z3**2) - (x0**2 + y0**2 + z0**2)

# building the 3x3 matrix of the linear equations
a = 2 * (simp_pts[:, 1, 0] - simp_pts[:, 0, 0])
b = 2 * (simp_pts[:, 1, 1] - simp_pts[:, 0, 1])
c = 2 * (simp_pts[:, 1, 2] - simp_pts[:, 0, 2])
d = 2 * (simp_pts[:, 2, 0] - simp_pts[:, 0, 0])
e = 2 * (simp_pts[:, 2, 1] - simp_pts[:, 0, 1])
f = 2 * (simp_pts[:, 2, 2] - simp_pts[:, 0, 2])
g = 2 * (simp_pts[:, 3, 0] - simp_pts[:, 0, 0])
h = 2 * (simp_pts[:, 3, 1] - simp_pts[:, 0, 1])
i = 2 * (simp_pts[:, 3, 2] - simp_pts[:, 0, 2])

v1 = (simp_pts[:, 1, 0] ** 2 + simp_pts[:, 1, 1] ** 2 + simp_pts[:, 1, 2] ** 2) - (simp_pts[:, 0, 0] ** 2 + simp_pts[:, 0, 1] ** 2 + simp_pts[:, 0, 2] ** 2)
v2 = (simp_pts[:, 2, 0] ** 2 + simp_pts[:, 2, 1] ** 2 + simp_pts[:, 2, 2] ** 2) - (simp_pts[:, 0, 0] ** 2 + simp_pts[:, 0, 1] ** 2 + simp_pts[:, 0, 2] ** 2)
v3 = (simp_pts[:, 3, 0] ** 2 + simp_pts[:, 3, 1] ** 2 + simp_pts[:, 3, 2] ** 2) - (simp_pts[:, 0, 0] ** 2 + simp_pts[:, 0, 1] ** 2 + simp_pts[:, 0, 2] ** 2)

# solve a 3x3 system by inversion (see https://en.wikipedia.org/wiki/Invertible_matrix#Inversion_of_3_%C3%97_3_matrices)
A = e*i - f*h
B = -(d*i - f*g)
C = d*h - e*g
D = -(b*i - c*h)
E = a*i - c*g
F = -(a*h - b*g)
G = b*f - c*e
H = -(a*f - c*d)
I = a*e - b*d

det = a*A + b*B + c*C

# multiplying inv*[v1, v2, v3] to get solution point (x, y, z)
x = (A*v1 + D*v2 + G*v3) / det
y = (B*v1 + E*v2 + H*v3) / det
z = (C*v1 + F*v2 + I*v3) / det

return (np.vstack((x, y, z))).T

我们想过滤掉原始表面之外的四面体(例如参见上图中的长四面体)。 这可以通过在原始表面上测试四面体来完成。 然而,一种非常适合输入管/管道表面的更简单的方法是过滤掉具有大外接半径的四面体。 这就是alpha-shape算法所做的。 这在我们的上下文中很容易完成,因为半径只是中心和任何四面体点之间的距离。

下图显示了过滤掉半径大于 20 的四面体后的 Delaunay 三角剖分。

3d 阿尔法形状

我们现在可以使用这些构建块来构建通过半径条件的四面体的 Voronoi 子图。 下面的函数使用 Delaunay 三角剖分中的连通性信息来构建 Voronoi 子图,表示为边列表。

def compute_voronoi_vertices_and_edges(points, r_thresh=np.inf):
"""
Compute (finite) Voronoi edges and vertices of a set of points.
:param points: input points.
:param r_thresh: radius value for filtering out vertices corresponding to
Delaunay tetrahedrons with large radii of circumscribing sphere (alpha-shape condition).
:return: array of xyz Voronoi vertex points and an edge list.
"""
dt = Delaunay(points)
xyz_centers = compute_delaunay_tetra_circumcenters(dt)

# filtering tetrahedrons that have radius > thresh
simp_pts_0 = dt.points[dt.simplices[:, 0]]
radii = np.linalg.norm(xyz_centers - simp_pts_0, axis=1)
is_in = radii < r_thresh

# build an edge list from (filtered) tetrahedrons neighbor relations
edge_lst = []
for i in range(len(dt.neighbors)):
    if not is_in[i]:
        continue  # i is an outside tetra
    for j in dt.neighbors[i]:
        if j != -1 and is_in[j]:
            edge_lst.append((i, j))

return xyz_centers, edge_lst

结果仍然不够充分,如下图所示,其中子图边缘是黑色线段。 原因是 3D Delaunay 三角剖分因具有细四面体(Shewchuk 在本文中称为条子、针)而臭名昭著,这会导致结果中出现外部“尖峰”。

Voronoi 子图

虽然有去除这些不需要的尖峰的通用方法(例如,参见Amenta 和 Bern ),但在管表面的情况下有一个简单的解决方案。 我们正在寻找的路径可以计算为图中最短的欧几里得路径,从最接近管起点的点开始,到最接近终点的点结束。 以下代码使用权重设置为边长度的networkx图执行此操作。

# get closest vertex to start and end points
xyz_centers, edge_lst = compute_voronoi_vertices_and_edges(pts, r_thresh=20.)
kdt = cKDTree(xyz_centers)
dist0, idx0 = kdt.query(np.array([100., 0, 0]))
dist1, idx1 = kdt.query(np.array([0, 100., 0]))

# compute shortest weighted path
edge_lengths = [np.linalg.norm(xyz_centers[e[0], :] - xyz_centers[e[1], :]) for e in edge_lst]
g = nx.Graph((i, j, {'weight': dist}) for (i, j), dist in zip(edge_lst, edge_lengths))
path_s = nx.shortest_path(g,source=idx0,target=idx1, weight='weight')

下图显示了原始 200 点的结果。

200分的最终路径

这是 1000 个点的更密集样本的结果。

1000 点的最终路径

现在您可以通过路径点传递近似样条 - 插值或最小二乘拟合。 您可以按照链接中的建议使用scipy.interpolate.UnivariateSplinescipy.interpolate.splrep如此或任何其他标准样条实现。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM