找到围绕由 3d 三角形网格表示的圆柱体的最短路径

计算科学 计算几何 图论 3d
2021-12-11 05:09:24

假设我有一个具有有限圆柱拓扑的 3d 三角形网格。C成为该网格上的一个顶点。

我怎样才能找到最短的路径C围绕圆柱体的自身最短,我的意思是路径上连续顶点之间的欧几里得距离之和。我所说的“围绕圆柱体”是指这样的路径基本上会将圆柱体分成两个新的圆柱体。

我也在数学 SE 上问过这个问题

2个回答

好的,我想了一会儿,想出了一个答案。

第 1 步: 找到圆柱体的顶盖,换句话说,就是沿着图形边界的两条封闭的不相交路径。

第 2 步: 沿着人脸图找到从一个帽子到另一个帽子的路径。

第 3 步: 通过删除位于第 2 步中找到的路径上的所有边来创建一个新的子图。跟踪删除的边,因为它们将在以后使用。由于路径是从一个盖子到另一个盖子,它会切割圆柱体,因此生成的子图具有平面拓扑。

第 4 步: 使用 Dijkstra 算法从C从步骤 3 到子图上的每个其他点。

第 5 步:e是在步骤 3 中删除的边中的一条边。让p1p2是由该边连接的顶点。D(C,p1)D(C,p2)是沿步骤 4 中找到的最短路径的距离,从Cp1p2分别。D(e)是边的长度e. 在步骤 3 中删除的边中,找到最小化的边D(C,p1)+D(C,p2)+D(e)

最短路径是通过将最短路径从Cp1如在步骤 4 中发现的,边缘e然后是最短路径p2C.

我花了最后一天用 python 写一个例子。该示例首先生成具有圆柱拓扑的图,然后根据描述的算法找到围绕圆柱从 C 到自身的最短路径:


import numpy as np

from matplotlib import pyplot as plt
from mpl_toolkits import mplot3d

debugPlot = True
forceEdgePoint = False

#%% Generate random points and duplicate them along the y axis
N = 100
V_orig = np.random.rand(N,2)

V = np.concatenate((V_orig,V_orig+np.array([0,1]),V_orig+np.array([0,2])),axis=0)

from scipy import spatial

#%% Perform delaunay triangulation
tri = spatial.Delaunay(V)

if debugPlot:
    plt.figure()
    plt.triplot(V[:,0],V[:,1],tri.simplices.copy(),'.-b')
    plt.gca().set_aspect('equal')

#%% Create a cylinder topology on the points in the middle
S = tri.simplices.copy()
#remove everything except for the middle:
S = S[np.any((S>=N) & (S < 2*N),axis=1),:]
if debugPlot:
    plt.triplot(V[:,0],V[:,1],S,'+-r')
    plt.gca().set_aspect('equal')

Sc = np.mod(S,N)
#remove duplicates
imin = np.argmin(Sc,axis=1)
reindex = np.vstack((imin,imin+1,imin+2)).transpose() % 3
Sc = Sc[np.arange(Sc.shape[0]).reshape(-1,1),reindex]
Sc = np.unique(Sc,axis=0)    
#remove degenerates
Sc = Sc[~((Sc[:,0] == Sc[:,1]) | (Sc[:,1] == Sc[:,2]) | (Sc[:,0] == Sc[:,2])),:]
Vc = V[:N,:]
plt.figure()
if debugPlot:
    plt.triplot(Vc[:,0],Vc[:,1],Sc,'.-g',linewidth=5)
    axGraph = plt.gca()

#%% Remove the cylinder caps
u = np.sort(Vc[:,0])
capsLowerBound = u[int(len(u) * 0.1)]
capsUpperBound = u[int(len(u) * 0.9)]
caps = (Vc[:,0] < capsLowerBound) | (Vc[:,0] > capsUpperBound)
caps_i = np.nonzero(caps)[0]
#Vc = Vc[~caps,:]
Sc = Sc[~np.any(np.isin(Sc,caps_i),axis=1),:]
plt.triplot(Vc[:,0],Vc[:,1],Sc,'o-b',linewidth=3)
axGraph = plt.gca()

#%% Plot in 3d a cylinder generated from the 2d coordinates generated above
Z = Vc[:,0]
X = np.cos(Vc[:,1]*2*np.pi)
Y = np.sin(Vc[:,1]*2*np.pi)
XYZ = np.vstack((X,Y,Z)).transpose()
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax3d = fig.add_subplot(111, projection='3d')
h = ax3d.plot_trisurf(X,Y,Sc,Z)
#h.set_facecolor(None)
h.set_edgecolor('k')

#%% Prepare halfedges
halfEdges = np.concatenate((np.array([Sc[:,0],Sc[:,1]]),
                        np.array([Sc[:,1],Sc[:,2]]),
                        np.array([Sc[:,2],Sc[:,0]])),axis=1).transpose()
halfEdge2Tri = {tuple(x):(y % Sc.shape[0]) for y,x in enumerate(halfEdges)}

#%% find non unique half-edges (there should be none)
iSEdges = np.lexsort(np.flipud(halfEdges.transpose()))
halfEdges[iSEdges,:]
iNonUnique = np.nonzero(np.all(np.diff(halfEdges[iSEdges,:],axis=0)==0,axis=1))[0]
nnuEdges = halfEdges[iSEdges,:][iNonUnique,:]
nnuVc = Vc[nnuEdges,:]
from matplotlib.collections import LineCollection
linen = LineCollection(nnuVc,color='r',linewidth=2,linestyle='-.')
axGraph.add_collection(linen)
if len(nnuEdges)>0:
    raise Exception('Found non unique half-edges')
halfEdgesSet = set(list(tuple(x) for x in halfEdges))

#%% Find borders of the cylinder (caps)
borderHalfEdges = {x for x in halfEdgesSet if x[::-1] not in halfEdgesSet}
borderHalfEdgePerVertex = dict()
for e in borderHalfEdges:
    borderHalfEdgePerVertex[e[0]] = e

def FindClosedBorderPath(halfEdgeSubset):
    try:
        e = next(iter(halfEdgeSubset))
    except StopIteration:
        return []
    capPath = [e[0],e[1]]
    halfEdgeSubset.remove(e)
    while capPath[-1] != capPath[0]:
        e = borderHalfEdgePerVertex[capPath[-1]]
        capPath.append(e[1])
        halfEdgeSubset.remove(e)
    return capPath

capPath1 = FindClosedBorderPath(borderHalfEdges)
capPath2 = FindClosedBorderPath(borderHalfEdges)
capPath3 = FindClosedBorderPath(borderHalfEdges)
if len(capPath3) != 0:
    raise Exception("For some reason there are 3 borders")

#%% Find a path from  one cap to the other along the face graph
c1 = (capPath1[0],capPath1[1])
c2 = (capPath2[0],capPath2[1])
q = [c1[::-1]]
predecessor = dict()
found = False

def GenEdgesForTri(t):
    yield (t[0],t[1])
    yield (t[1],t[2])
    yield (t[2],t[0])

while len(q) > 0 and not found:
    c = q.pop(0)
    try:
        t = Sc[halfEdge2Tri[c[::-1]]]
    except KeyError:
        continue
    for e in GenEdgesForTri(t):
        if e not in predecessor:
            q.append(e)
            predecessor[e] = c
            if e == c2:
                found = True
                break
assert(found)

c1_to_c2_path = [c2]
n = c2
while n!=c1[::-1]:
    n = predecessor[n]
    c1_to_c2_path.append(n)
c1_to_c2_path.reverse()

#%%plot the newly found path
if debugPlot:
    c1_c2_path_xyz = XYZ[c1_to_c2_path,:]
    linen = mplot3d.art3d.Line3DCollection(XYZ[c1_to_c2_path,:],color=[1,0.7,0],linewidth=5,linestyle='-')
    ax3d.add_collection(linen)
    linen = LineCollection(Vc[c1_to_c2_path,:],color=[1,0.7,0],linewidth=5,linestyle='-')
    axGraph.add_collection(linen)

#%% Remove all triangles that are part of the path and save it as a new subgraph
subgraphHalfEdges = set(halfEdgesSet) #copy
removedEdges=set(c1_to_c2_path) | {x[::-1] for x in c1_to_c2_path}
for e in removedEdges:
    try:
        subgraphHalfEdges.remove(e)
    except KeyError:
        pass
#%% Choose C       
if forceEdgePoint:
    iSorted = np.argsort(Vc[Sc.flat,0])
    iC = Sc.flat[iSorted[0]]
else:
    iC = np.random.choice(Sc.flat)
ax3d.plot([X[iC]],[Y[iC]],[Z[iC]],'*m',markersize=30)
axGraph.plot([V[iC,0]],[V[iC,1]],'*m',markersize = 20)


#%% Make Dijkstra on subgraph
import bisect

def DistFunc(e):
    d = XYZ[e[0],:] - XYZ[e[1],:]
    return np.sqrt(d.dot(d))

from collections import defaultdict
v2e = defaultdict(lambda : set())
for e in subgraphHalfEdges:
    v2e[e[0]].add(e)
    v2e[e[1]].add(e[::-1])

distances = defaultdict(lambda : np.inf)
visited = set()
predecessor = {}
q = [(0,iC)]
distances[iC]=0
while len(q) > 0:
    c = q.pop(0)[1]
    if c in visited:
        continue
    visited.add(c)
    thisDist = distances[c]
    for e in v2e[c]:
        v = e[1]
        if v in visited:
            continue
        lenE = DistFunc(e)
        vCurDist = distances[v]
        if vCurDist > thisDist + lenE:
            distances[v] = thisDist + lenE
            predecessor[v] = c
            bisect.insort(q,(thisDist+lenE,v))
#%% plot the result
if debugPlot:
    l=list(distances.items())
    xyz = XYZ[list(x[0] for x in l),:]
    d = np.array([x[1] for x in l])
    c = d / np.max(d)
    ax3d.scatter(xyz[:,0],xyz[:,1],c=c,zs=xyz[:,2])
    predecessors_a = np.array(list(predecessor.items()))
    linen = mplot3d.art3d.Line3DCollection(XYZ[predecessors_a,:],color='w',linewidth=2,linestyle=':')
    ax3d.add_collection(linen)
    linen = LineCollection(Vc[predecessors_a,:],color='r',linewidth=2,linestyle=':')
    axGraph.add_collection(linen)

#%% Find the path through the removed edges that would make the shortest total path
candidates = []
for e in removedEdges:
    d = DistFunc(e)
    candidates.append( (d + distances[e[0]] + distances[e[1]], e))
candidates.sort()
chosenE = candidates[0][1]

subpaths = []
for i in range(2):
    n = chosenE[i]
    path_C_to_p = [n]
    while n!=iC:
        n = predecessor[n]
        path_C_to_p.append(n)
    subpaths.append(path_C_to_p)

shortestPath = list(reversed(subpaths[0])) + subpaths[1]
shortestPathXyz = XYZ[shortestPath,:]
shortestPathV = Vc[shortestPath,:]
axGraph.plot(shortestPathV[:,0],shortestPathV[:,1],'-c',linewidth=3)
ax3d.plot(shortestPathXyz[:,0],shortestPathXyz[:,1],shortestPathXyz[:,2],'-c',linewidth=3)

在下图中,橙色线是在步骤 3 中移除的边缘,洋红色星形是C, 青色线是最短路径。为清楚起见,将图作为圆柱体进行了 3d 嵌入,尽管边长取自该 3d 嵌入,但没有必要解决该问题。

二维圆柱图上的最短路径

圆柱 3d 嵌入的最短路径

  1. 找到圆柱体的纵轴(对所有点进行最小二乘线性拟合将产生此结果)。

  2. 构造一个通过该轴的平面。任何方向都应该没问题,但是假设它垂直于从轴传递到C.

  3. 重新定向,使轴垂直,平面被轴分成“左”和“右”两半。(这一步比数学更概念化。)

  4. 开始一条 Dijkstra 最短路径遍历C.

  5. 当一条边穿过平面时,根据它穿过哪一侧,将其着色为“左”或“右”。

  6. Dijkstra 检查每个节点的邻居并做出选择。

    1. 如果邻居未被访问,则将其添加到边界并为其父代颜色(如果有)。

    2. 如果已经访问过邻居,则执行另一次检查。

      1. 如果邻居没有颜色,则被拒绝。

      2. 如果邻居的颜色与正在考虑的节点的颜色相匹配,则它被拒绝。

      3. 如果邻居的颜色与所考虑的节点不同,则所考虑的节点与该邻居之间的边完成最短圆周路径。

  7. 使用父指针回溯路径。

左右平面相交应该是简单的几何检查,您不必担心浮点问题,因为检查是否完美并不重要:有四分之一圆柱体的摆动空间。