假设我有一个具有有限圆柱拓扑的 3d 三角形网格。让成为该网格上的一个顶点。
我怎样才能找到最短的路径围绕圆柱体的自身?最短,我的意思是路径上连续顶点之间的欧几里得距离之和。我所说的“围绕圆柱体”是指这样的路径基本上会将圆柱体分成两个新的圆柱体。
我也在数学 SE 上问过这个问题。
假设我有一个具有有限圆柱拓扑的 3d 三角形网格。让成为该网格上的一个顶点。
我怎样才能找到最短的路径围绕圆柱体的自身?最短,我的意思是路径上连续顶点之间的欧几里得距离之和。我所说的“围绕圆柱体”是指这样的路径基本上会将圆柱体分成两个新的圆柱体。
我也在数学 SE 上问过这个问题。
好的,我想了一会儿,想出了一个答案。
第 1 步: 找到圆柱体的顶盖,换句话说,就是沿着图形边界的两条封闭的不相交路径。
第 2 步: 沿着人脸图找到从一个帽子到另一个帽子的路径。
第 3 步: 通过删除位于第 2 步中找到的路径上的所有边来创建一个新的子图。跟踪删除的边,因为它们将在以后使用。由于路径是从一个盖子到另一个盖子,它会切割圆柱体,因此生成的子图具有平面拓扑。
第 4 步: 使用 Dijkstra 算法从从步骤 3 到子图上的每个其他点。
第 5 步: 让是在步骤 3 中删除的边中的一条边。让和是由该边连接的顶点。让和是沿步骤 4 中找到的最短路径的距离,从到和分别。让是边的长度. 在步骤 3 中删除的边中,找到最小化的边
最短路径是通过将最短路径从到如在步骤 4 中发现的,边缘然后是最短路径到.
我花了最后一天用 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 中移除的边缘,洋红色星形是, 青色线是最短路径。为清楚起见,将图作为圆柱体进行了 3d 嵌入,尽管边长取自该 3d 嵌入,但没有必要解决该问题。
找到圆柱体的纵轴(对所有点进行最小二乘线性拟合将产生此结果)。
构造一个通过该轴的平面。任何方向都应该没问题,但是假设它垂直于从轴传递到.
重新定向,使轴垂直,平面被轴分成“左”和“右”两半。(这一步比数学更概念化。)
开始一条 Dijkstra 最短路径遍历.
当一条边穿过平面时,根据它穿过哪一侧,将其着色为“左”或“右”。
Dijkstra 检查每个节点的邻居并做出选择。
如果邻居未被访问,则将其添加到边界并为其父代颜色(如果有)。
如果已经访问过邻居,则执行另一次检查。
如果邻居没有颜色,则被拒绝。
如果邻居的颜色与正在考虑的节点的颜色相匹配,则它被拒绝。
如果邻居的颜色与所考虑的节点不同,则所考虑的节点与该邻居之间的边完成最短圆周路径。
使用父指针回溯路径。
左右平面相交应该是简单的几何检查,您不必担心浮点问题,因为检查是否完美并不重要:有四分之一圆柱体的摆动空间。