J'ai une liste de coordonnées (x, y) qui représentent un squelette de ligne . La liste est obtenue directement à partir d'une image binaire:
import numpy as np
list=np.where(img_skeleton>0)
Maintenant, les points de la liste sont triés en fonction de leur position dans l'image le long de l'un des axes.
Je voudrais trier la liste de telle sorte que l'ordre représente un chemin lisse le long de la ligne. (Ce n'est actuellement pas le cas où la ligne se courbe en arrière) . Par la suite, je souhaite ajuster une spline à ces points.
Un problème similaire a été décrit et résolu en utilisant arcPy here . Existe-t-il un moyen pratique d’y parvenir en utilisant python, numpy, scipy, openCV (ou une autre bibliothèque?)
ci-dessous est un exemple d'image. il en résulte une liste de 59 (x, y) -coordinates .
lorsque j'envoie la liste à la routine d'ajustement des splines de scipy, je rencontre un problème car les points ne sont pas "ordonnés" sur la ligne:
Je m'excuse pour la longue réponse à l’avance: P (le problème n’est pas ça simple).
Commençons par reformuler le problème. La recherche d’une ligne qui relie tous les points peut être reformulée comme un problème de chemin le plus court dans un graphe, où (1) les nœuds du graphe sont les points de l’espace, (2) chaque nœud est connecté à ses 2 voisins les plus proches et ( 3) le plus court chemin traverse chacun des nœuds une seule fois. Cette dernière contrainte est très importante (et assez difficile à optimiser). Le problème consiste essentiellement à trouver une permutation de longueur N
, où la permutation fait référence à l'ordre de chacun des nœuds (N
est le nombre total de nœuds) dans le chemin.
Trouver toutes les permutations possibles et en évaluer le coût est trop coûteux (il existe des permutations N!
si je ne me trompe pas, ce qui est trop gros pour poser des problèmes). Ci-dessous, je propose une approche qui trouve les N
meilleures permutations (la permutation optimale pour chacun des N
points), puis la permutation (parmi celles N
) qui minimise l'erreur/coût.
Maintenant, commençons à créer un exemple de problème:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
plt.plot(x, y)
plt.show()
Et ici, la version non triée des points [x, y]
pour simuler des points aléatoires dans l’espace connectés en ligne:
idx = np.random.permutation(x.size)
x = x[idx]
y = y[idx]
plt.plot(x, y)
plt.show()
Le problème est alors d’ordonner à ces points de récupérer leur ordre initial afin que la ligne soit tracée correctement.
Nous pouvons d'abord réorganiser les points dans un tableau [N, 2]
:
points = np.c_[x, y]
Ensuite, nous pouvons commencer par créer un graphique du voisin le plus proche pour connecter chacun des nœuds à ses 2 voisins les plus proches:
from sklearn.neighbors import NearestNeighbors
clf = NearestNeighbors(2).fit(points)
G = clf.kneighbors_graph()
G
est une matrice éparse N x N
, où chaque ligne représente un nœud et les éléments non nuls des colonnes la distance euclidienne à ces points.
On peut alors utiliser networkx
pour construire un graphe à partir de cette matrice creuse:
import networkx as nx
T = nx.from_scipy_sparse_matrix(G)
Et, ici commence le magique: nous pouvons extraire les chemins en utilisant dfs_preorder_nodes , ce qui créera essentiellement un chemin à travers tous les noeuds (en passant par chacun d'eux exactement une fois) à partir d'un noeud de départ (si non donné, le nœud 0 sera sélectionné).
order = list(nx.dfs_preorder_nodes(T, 0))
xx = x[order]
yy = y[order]
plt.plot(xx, yy)
plt.show()
Eh bien, ce n’est pas si grave, mais on peut remarquer que la reconstruction n’est pas optimale. En effet, le point 0
dans la liste non ordonnée se trouve au milieu de la ligne, c’est ainsi qu’il va d’abord dans une direction, puis revient et se termine dans l’autre direction.
Donc, afin d'obtenir l'ordre optimal, nous pouvons simplement obtenir le meilleur ordre pour tous les nœuds:
paths = [list(nx.dfs_preorder_nodes(T, i)) for i in range(len(points))]
Maintenant que nous avons le chemin optimal à partir de chacun des nœuds N = 100
, nous pouvons les ignorer et trouver celui qui minimise les distances entre les connexions (problème d'optimisation):
mindist = np.inf
minidx = 0
for i in range(len(points)):
p = paths[i] # order of nodes
ordered = points[p] # ordered nodes
# find cost of that order by the sum of euclidean distances between points (i) and (i+1)
cost = (((ordered[:-1] - ordered[1:])**2).sum(1)).sum()
if cost < mindist:
mindist = cost
minidx = i
Les points sont ordonnés pour chacun des chemins optimaux, puis un coût est calculé (en calculant la distance euclidienne entre toutes les paires de points i
et i+1
). Si le chemin commence au point start
ou end
, son coût sera le plus faible possible car tous les nœuds seront consécutifs. Par contre, si le chemin commence à un nœud situé au milieu de la ligne, le coût sera très élevé à un moment donné, car il faudra parcourir la fin (ou le début) de la ligne vers la ligne initiale. position pour explorer l'autre direction. Le chemin qui minimise ce coût est le chemin partant d'un point optimal.
opt_order = paths[minidx]
Maintenant, nous pouvons reconstruire l'ordre correctement:
xx = x[opt_order]
yy = y[opt_order]
plt.plot(xx, yy)
plt.show()
Une solution possible consiste à utiliser une approche de voisinage le plus proche, possible à l'aide d'un arbre KDTree. Scikit-learn a une interface agréable. Ceci peut ensuite être utilisé pour construire une représentation graphique en utilisant networkx. Cela ne fonctionnera vraiment que si la ligne à tracer passe par les voisins les plus proches:
from sklearn.neighbors import KDTree
import numpy as np
import networkx as nx
G = nx.Graph() # A graph to hold the nearest neighbours
X = [(0, 1), (1, 1), (3, 2), (5, 4)] # Some list of points in 2D
tree = KDTree(X, leaf_size=2, metric='euclidean') # Create a distance tree
# Now loop over your points and find the two nearest neighbours
# If the first and last points are also the start and end points of the line you can use X[1:-1]
for p in X
dist, ind = tree.query(p, k=3)
print ind
# ind Indexes represent nodes on a graph
# Two nearest points are at indexes 1 and 2.
# Use these to form edges on graph
# p is the current point in the list
G.add_node(p)
n1, l1 = X[ind[0][1]], dist[0][1] # The next nearest point
n2, l2 = X[ind[0][2]], dist[0][2] # The following nearest point
G.add_Edge(p, n1)
G.add_Edge(p, n2)
print G.edges() # A list of all the connections between points
print nx.shortest_path(G, source=(0,1), target=(5,4))
>>> [(0, 1), (1, 1), (3, 2), (5, 4)] # A list of ordered points
Mise à jour: si les points de départ et d'arrivée sont inconnus et que vos données sont assez bien séparées, vous pouvez trouver les fins en recherchant des cliques dans le graphique. Les points de départ et d'arrivée formeront une clique. Si le bord le plus long est supprimé de la clique, cela créera une extrémité libre dans le graphique pouvant servir de point de départ et de fin. Par exemple, les points de début et de fin de cette liste apparaissent au milieu:
X = [(0, 1), (0, 0), (2, 1), (3, 2), (9, 4), (5, 4)]
Après avoir construit le graphique, il suffit maintenant de retirer le plus long bord des cliques pour trouver les extrémités libres du graphique:
def find_longest_Edge(l):
e1 = G[l[0]][l[1]]['weight']
e2 = G[l[0]][l[2]]['weight']
e3 = G[l[1]][l[2]]['weight']
if e2 < e1 > e3:
return (l[0], l[1])
Elif e1 < e2 > e3:
return (l[0], l[2])
Elif e1 < e3 > e2:
return (l[1], l[2])
end_cliques = [i for i in list(nx.find_cliques(G)) if len(i) == 3]
Edge_lengths = [find_longest_Edge(i) for i in end_cliques]
G.remove_edges_from(Edge_lengths)
edges = G.edges()
start_end = [n for n,nbrs in G.adjacency_iter() if len(nbrs.keys()) == 1]
print nx.shortest_path(G, source=start_end[0], target=start_end[1])
>>> [(0, 0), (0, 1), (2, 1), (3, 2), (5, 4), (9, 4)] # The correct path
Je travaille sur un problème similaire, mais il a une contrainte importante (un peu comme dans l'exemple donné par l'OP), à savoir que chaque pixel a un ou deux pixels voisins, au sens 8 connectés. Avec cette contrainte, il existe une solution très simple.
def sort_to_form_line(unsorted_list):
"""
Given a list of neighbouring points which forms a line, but in random order, sort them to the correct order
IMPORTANT: Each point must be a neighbour (8-point sense) to a least one other point!
"""
sorted_list = [unsorted_list.pop(0)]
while len(unsorted_list) > 0:
i = 0
while i < len(unsorted_list):
if are_neighbours(sorted_list[0], unsorted_list[i]):
#neighbours at front of list
sorted_list.insert(0, unsorted_list.pop(i))
Elif are_neighbours(sorted_list[-1], unsorted_list[i]):
#neighbours at rear of list
sorted_list.append(unsorted_list.pop(i))
else:
i = i+1
return sorted_list
def are_neighbours(pt1, pt2):
"""
Check if pt1 and pt2 are neighbours, in the 8-point sense
pt1 and pt2 has integer coordinates
"""
return (np.abs(pt1[0]-pt2[0]) < 2) and (np.abs(pt1[1]-pt2[1]) < 2)